ultralytics / JSON2YOLO

Convert JSON annotations into YOLO format.
https://docs.ultralytics.com
GNU Affero General Public License v3.0
878 stars 232 forks source link

Converting the COCO keypoints format to YOLOv8 pose format. #45

Open ryouchinsa opened 1 year ago

ryouchinsa commented 1 year ago

We improved the script so that it converts the COCO keypoints format to YOLOv8 format. https://github.com/ryouchinsa/Rectlabel-support/blob/master/general_json2yolo.py

if use_keypoints:
    k = (np.array(ann['keypoints']).reshape(-1, 3) / np.array([w, h, 1])).reshape(-1).tolist()
    k = box + k
    keypoints.append(k) 

The function show_kpt_shape_flip_idx() shows the kpt_shape and flip_idx for the yaml file. Copy the 2 lines and paste to your yaml file. Please let us know your opinion.

def show_kpt_shape_flip_idx(data):
    for category in data['categories']:
        if 'keypoints' not in category:
            continue
        keypoints = category['keypoints']
        num = len(keypoints)
        print('kpt_shape: [' + str(num) + ', 3]')
        flip_idx = list(range(num))
        for i, name in enumerate(keypoints):
            name = name.lower()
            left_pos = name.find('left')
            if left_pos < 0:
                continue
            name_right = name.replace('left', 'right')
            for j, namej in enumerate(keypoints):
                namej = namej.lower()
                if namej == name_right:
                    flip_idx[i] = j
                    flip_idx[j] = i
                    break
        print('flip_idx: [' + ', '.join(str(x) for x in flip_idx) + ']')
AyushExel commented 1 year ago

Thanks for your work. I'll make a PR to update the main script with your code. Let me know if you want to do that yourself.

ryouchinsa commented 1 year ago

Thanks for reviewing the script. We created a pull request. https://github.com/ultralytics/JSON2YOLO/pull/46

kaplansinan commented 1 year ago

For those who are still looking for a script to convert coco keypoint json file to yolo8 pose format, I am sharing the script that I have written for yolo-pose detection task.

import argparse
import json
import os
import shutil
from collections import defaultdict
from pathlib import Path

import numpy as np
from tqdm import tqdm

parser = argparse.ArgumentParser()

parser.add_argument(
    "--coco_json_path",
    default="/home/hodor/dev/data/coco_test/person_keypoints_train2017.json",
    type=str,
    help="input: coco format(json)",
)

parser.add_argument(
    "--yolo_save_root_dir",
    default="/home/hodor/dev/data/coco_test/yolo/",
    type=str,
    help="specify where to save the output dir of labels",
)

def convert_bbox_to_yolo(size, box):
    dw = 1.0 / (size[0])
    dh = 1.0 / (size[1])
    x = box[0] + box[2] / 2.0
    y = box[1] + box[3] / 2.0
    w = box[2]
    h = box[3]
    # The round function determines the number of decimal places in (xmin, ymin, xmax, ymax)
    x = round(x * dw, 6)
    w = round(w * dw, 6)
    y = round(y * dh, 6)
    h = round(h * dh, 6)
    return (x, y, w, h)

def convert_keypoints2_list(keypoints, img_width, img_height):
    xiaoshu = 10 ** 6  
    arry_x = np.zeros([17, 1])
    num_1 = 0
    for x in keypoints[0:51:3]:
        arry_x[num_1, 0] = int((x / img_width) * xiaoshu) / xiaoshu
        num_1 += 1

    arry_y = np.zeros([17, 1])
    num_2 = 0
    for y in keypoints[1:51:3]:
        arry_y[num_2, 0] = int((y / img_height) * xiaoshu) / xiaoshu
        num_2 += 1

    arry_v = np.zeros([17, 1])
    num_3 = 0
    for v in keypoints[2:51:3]:
        arry_v[num_3, 0] = v
        num_3 += 1

    list_1 = []
    num_4 = 0
    for i in range(17):
        list_1.append(float(arry_x[num_4]))
        list_1.append(float(arry_y[num_4]))
        list_1.append(float(arry_v[num_4]))
        num_4 += 1
    return list_1

def main(root_dir, ana_txt_save_path_txt, json_file):
    xiaoshu = 10 ** 6  
    data = json.load(open(json_file, "r"))
    # if not os.path.exists(ana_txt_save_path_txt):
    #     os.makedirs(ana_txt_save_path_txt)

    id_map = (
        {}
    )  # The ids of the coco dataset are not continuous! Remap and output again!
    with open(os.path.join(root_dir, "classes.txt"), "w") as f:
        for i, category in enumerate(data["categories"]):
            f.write(f"{category['name']}\n")
            id_map[category["id"]] = i

    fn = Path(ana_txt_save_path_txt)

    images = {"%g" % x["id"]: x for x in data["images"]}
    # Create image-annotations dict
    imgToAnns = defaultdict(list)
    for ann in data["annotations"]:
        imgToAnns[ann["image_id"]].append(ann)

    list_file = open(os.path.join(root_dir, "train.txt"), "w")

    # Write labels file
    for img_id, anns in tqdm(imgToAnns.items(), desc=f"Annotations {json_file}"):
        img = images["%g" % img_id]
        h, w, f = img["height"], img["width"], img["file_name"]

        bboxes = []
        segments = []
        for ann in anns:
            # if ann['iscrowd']:
            #     continue
            # The COCO box format is [top left x, top left y, width, height]
            box = np.array(ann["bbox"], dtype=np.float64)
            box[:2] += box[2:] / 2  # xy top-left corner to center
            box[[0, 2]] /= w  # normalize x
            box[[1, 3]] /= h  # normalize y
            if box[2] <= 0 or box[3] <= 0:  # if w <= 0 and h <= 0
                continue

            # keypoints
            keypoints = ann["keypoints"]
            keypoints_list = convert_keypoints2_list(keypoints, w, h)
            # conver_keypoins2_list(keypoints,w,h)
            # print(keypoints)

            cls = id_map[ann["category_id"]]
            # cls = coco80[ann['category_id'] - 1] if cls91to80 else ann['category_id'] - 1  # class
            box = [cls] + box.tolist() + keypoints_list
            if box not in bboxes:
                bboxes.append(box)
            # Segments
            # if use_segments:
            #     if len(ann['segmentation']) > 1:
            #         s = merge_multi_segment(ann['segmentation'])
            #         s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist()
            #     else:
            #         s = [j for i in ann['segmentation'] for j in i]  # all segments concatenated
            #         s = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()
            #     s = [cls] + s
            #     if s not in segments:
            #         segments.append(s)

            # Write
            # head, tail = os.path.splitext(filename)
            # ana_txt_name = head + ".txt"
            # replace_txt_name = os.path.join(ana_txt_save_path_txt, ana_txt_name)
        with open((fn / f).with_suffix(".txt"), "w") as file:
            for i in range(len(bboxes)):
                line = (*(bboxes[i]),)  # cls, box,keypoins
                file.write(("%g " * len(line)).rstrip() % line + "\n")
        list_file.write("./images/train/%s\n" % (f))
    list_file.close()

if __name__ == "__main__":
    args = parser.parse_args()
    print("Parsing and creating directories...")
    ROOT_DIR = args.yolo_save_root_dir
    COCO_JSON_FILE = args.coco_json_path
    YOLO_ANNO_TXT_SAVE_PATH = ROOT_DIR+"/labels/train/"
    print(ROOT_DIR,COCO_JSON_FILE, YOLO_ANNO_TXT_SAVE_PATH)
    # try:
    #     os.makedirs(YOLO_ANNO_TXT_SAVE_PATH, exist_ok=True)
    # except:
    #     print("Permission error!")
    main(ROOT_DIR, YOLO_ANNO_TXT_SAVE_PATH, COCO_JSON_FILE)

## USAGE 
# python coco_keypointjson2yolo.py  --coco_json_path /home/hodor/dev/data/coco_test/person_keypoints_train2017.json  --yolo_save_root_dir /home/hodor/dev/data/coco_test/yolo/
Rya8n commented 1 year ago

It seems like your code only works for converting 17 keypoints from the coco format. Maybe you should make that clear in case someone tried to use it for different number of keypoints(like I did which left me frustrated for the past 8 hours).

Rya8n commented 1 year ago

I have modified @kaplansinan 's code for converting coco dataset to yolo. I added a feature in which you can define the number of keypoints on your dataset.

import argparse
import json
import os
import shutil
from collections import defaultdict
from pathlib import Path

import numpy as np
from tqdm import tqdm

parser = argparse.ArgumentParser()

numOfKpts = 21 #replace this with the number of keypoints inside the dataset
kptsInfo  = numOfKpts * 3 

parser.add_argument(
    "--coco_json_path",
    default="/home/hodor/dev/data/coco_test/person_keypoints_train2077.json",
    type=str,
    help="input: coco format(json)",
)

parser.add_argument(
    "--yolo_save_root_dir",
    default="/home/hodor/dev/data/coco_test/yolo/",
    type=str,
    help="specify where to save the output dir of labels",
)

def convert_bbox_to_yolo(size, box):
    dw = 1.0 / (size[0])
    dh = 1.0 / (size[1])
    x = box[0] + box[2] / 2.0
    y = box[1] + box[3] / 2.0
    w = box[2]
    h = box[3]
    # The round function determines the number of decimal places in (xmin, ymin, xmax, ymax)
    x = round(x * dw, 6)
    w = round(w * dw, 6)
    y = round(y * dh, 6)
    h = round(h * dh, 6)
    return (x, y, w, h)

def convert_keypoints2_list(keypoints, img_width, img_height):
    xiaoshu = 10 ** 6  
    arry_x = np.zeros([numOfKpts, 1])
    num_1 = 0
    for x in keypoints[0:kptsInfo:3]:
        arry_x[num_1, 0] = int((x / img_width) * xiaoshu) / xiaoshu
        num_1 += 1

    arry_y = np.zeros([numOfKpts, 1])
    num_2 = 0
    for y in keypoints[1:kptsInfo:3]:
        arry_y[num_2, 0] = int((y / img_height) * xiaoshu) / xiaoshu
        num_2 += 1

    arry_v = np.zeros([numOfKpts, 1])
    num_3 = 0
    for v in keypoints[2:kptsInfo:3]:
        arry_v[num_3, 0] = v
        num_3 += 1

    list_1 = []
    num_4 = 0
    for i in range(numOfKpts):
        list_1.append(float(arry_x[num_4]))
        list_1.append(float(arry_y[num_4]))
        list_1.append(float(arry_v[num_4]))
        num_4 += 1
    return list_1

def main(root_dir, ana_txt_save_path_txt, json_file):
    xiaoshu = 10 ** 6  
    data = json.load(open(json_file, "r"))
    # if not os.path.exists(ana_txt_save_path_txt):
    #     os.makedirs(ana_txt_save_path_txt)

    id_map = (
        {}
    )  # The ids of the coco dataset are not continuous! Remap and output again!
    with open(os.path.join(root_dir, "classes.txt"), "w") as f:
        for i, category in enumerate(data["categories"]):
            f.write(f"{category['name']}\n")
            id_map[category["id"]] = i

    fn = Path(ana_txt_save_path_txt)

    images = {"%g" % x["id"]: x for x in data["images"]}
    # Create image-annotations dict
    imgToAnns = defaultdict(list)
    for ann in data["annotations"]:
        imgToAnns[ann["image_id"]].append(ann)

    list_file = open(os.path.join(root_dir, "train.txt"), "w")

    # Write labels file
    for img_id, anns in tqdm(imgToAnns.items(), desc=f"Annotations {json_file}"):
        img = images["%g" % img_id]
        h, w, f = img["height"], img["width"], img["file_name"]

        bboxes = []
        segments = []
        for ann in anns:
            # if ann['iscrowd']:
            #     continue
            # The COCO box format is [top left x, top left y, width, height]
            box = np.array(ann["bbox"], dtype=np.float64)
            box[:2] += box[2:] / 2  # xy top-left corner to center
            box[[0, 2]] /= w  # normalize x
            box[[1, 3]] /= h  # normalize y
            if box[2] <= 0 or box[3] <= 0:  # if w <= 0 and h <= 0
                continue

            # keypoints
            keypoints = ann["keypoints"]
            keypoints_list = convert_keypoints2_list(keypoints, w, h)
            # conver_keypoins2_list(keypoints,w,h)
            # print(keypoints)

            cls = id_map[ann["category_id"]]
            # cls = coco80[ann['category_id'] - 1] if cls91to80 else ann['category_id'] - 1  # class
            box = [cls] + box.tolist() + keypoints_list
            if box not in bboxes:
                bboxes.append(box)
            # Segments
            # if use_segments:
            #     if len(ann['segmentation']) > 1:
            #         s = merge_multi_segment(ann['segmentation'])
            #         s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist()
            #     else:
            #         s = [j for i in ann['segmentation'] for j in i]  # all segments concatenated
            #         s = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()
            #     s = [cls] + s
            #     if s not in segments:
            #         segments.append(s)

            # Write
            # head, tail = os.path.splitext(filename)
            # ana_txt_name = head + ".txt"
            # replace_txt_name = os.path.join(ana_txt_save_path_txt, ana_txt_name)
        with open((fn / f).with_suffix(".txt"), "w") as file:
            for i in range(len(bboxes)):
                line = (*(bboxes[i]),)  # cls, box,keypoins
                file.write(("%g " * len(line)).rstrip() % line + "\n")
        list_file.write("./images/train/%s\n" % (f))
    list_file.close()

if __name__ == "__main__":
    args = parser.parse_args()
    print("Parsing and creating directories...")
    ROOT_DIR = args.yolo_save_root_dir
    COCO_JSON_FILE = args.coco_json_path
    YOLO_ANNO_TXT_SAVE_PATH = ROOT_DIR+"/labels/train/"
    print(ROOT_DIR,COCO_JSON_FILE, YOLO_ANNO_TXT_SAVE_PATH)
    # try:
    #     os.makedirs(YOLO_ANNO_TXT_SAVE_PATH, exist_ok=True)
    # except:
    #     print("Permission error!")
    main(ROOT_DIR, YOLO_ANNO_TXT_SAVE_PATH, COCO_JSON_FILE)

## USAGE 
# replace the value of the variable "numOfKpts" with the number of keypoints on your dataset
# python coco_keypointjson2yolo_v02.py  --coco_json_path /home/hodor/dev/data/coco_test/person_keypoints_train2077.json  --yolo_save_root_dir /home/hodor/dev/data/coco_test/yolo/

Many thanks to @kaplansinan for the base code

glenn-jocher commented 1 year ago

It's great to see the improvements you've made to the script for converting COCO keypoints to YOLO format. Allowing users to define the number of keypoints in the dataset is a useful addition and will make it more flexible for different use cases. Thank you for sharing the updated script and the clear usage instructions! Your collaboration and contributions are appreciated. Keep up the good work!

ryouchinsa commented 1 year ago

We updated the general_json2yolo.py script so that the RLE mask with holes are converted to YOLO segmentation format.

We believe that this script would be beneficial for your company and users. Could you review the script before making a PR?

glenn-jocher commented 1 year ago

@ryouchinsa thank you for sharing the updated script! We appreciate your contribution and willingness to improve the tool. I will review the script and provide feedback as soon as possible. Your effort is valuable to the YOLO community and our users.

mycodeDev786 commented 7 months ago

I am trying to convert coco formate Pose estimation dataset into yolo8-pose. but no script is working

pderrenger commented 7 months ago

I'm sorry to hear you're having trouble with the conversion. πŸ˜• For converting a COCO format pose estimation dataset into YOLOv8 format, you might need to ensure your script handles the keypoints correctly. Here's a basic outline you can follow or adjust your script against:

  1. Parse COCO JSON: Load your COCO dataset JSON file.
  2. Convert Bounding Boxes: For each annotation, convert the COCO bounding boxes to YOLO format using the formula (x_center / image_width, y_center / image_height, width / image_width, height / image_height).
  3. Handle Keypoints: Convert COCO keypoints to the format expected by YOLOv8.
  4. Save Annotations: Write these converted annotations to a new file suited for YOLOv8 training.

If this aligns with your approach and the issue persists, please share a snippet of your script or the error you're encountering. This will help pinpoint the problem more effectively. πŸ‘

For more detailed assistance, you might find the Ultralytics Docs helpful: https://docs.ultralytics.com.

pderrenger commented 7 months ago

I'm sorry to hear you're experiencing difficulties with converting your COCO pose estimation dataset for YOLOv8 pose. Our goal is to support our community the best we can, and feedback like yours is crucial for us. 🌟

Converting from COCO keypoints format to YOLOv8 pose does require handling the data carefully. Although we strive to provide comprehensive solutions, gaps can indeed occur. However, we definitely do not intend to drive users towards paid solutions through lack of support.

Using yolo-nas-pose can indeed be a great alternative if it suits your project's requirements directly. Also, I'd recommend checking the latest scripts and documentation on our repository or Ultralytics Docs for potentially updated tools or guidance.

If you're open to it, could you share which scripts you've tried and what issues you encountered? This information could help us improve or guide you more effectively.

Thank you for your feedback, and we're here to help!

ryouchinsa commented 7 months ago

Hi @mycodeDev786 , if you could share the COCO pose file with us, we can fix our script so that our script can convert your COCO file to the YOLO pose format.

HI @wesboyt, it is not true. We are not driving users to paid solutions. Here, to convert the COCO file to YOLO pose format, we can support without paid solutions.

mycodeDev786 commented 7 months ago

Hi @mycodeDev786 , if you could share the COCO pose file with us, we can fix our script so that our script can convert your COCO file to the YOLO pose format.

HI @wesboyt, it is not true. We are not driving users to paid solutions. Here, to convert the COCO file to YOLO pose format, we can support without paid solutions.

hi there thanks for the response i am uploading the JSON file of keypoints as coco formate of 14 keypoints. i want to use it for pose estimation [ExLPose_train_WL.json](https://githu@ryouchinsa .com/ultralytics/JSON2YOLO/files/15125856/ExLPose_train_WL.json) @ryouchinsa

mycodeDev786 commented 7 months ago

Hi @mycodeDev786 , if you could share the COCO pose file with us, we can fix our script so that our script can convert your COCO file to the YOLO pose format.

HI @wesboyt, it is not true. We are not driving users to paid solutions. Here, to convert the COCO file to YOLO pose format, we can support without paid solutions.

here is the file ExLPose_train_WL.json

@ryouchinsa

ryouchinsa commented 7 months ago

Hi @mycodeDev786, thanks for sharing the COCO file.

We updated the general_json2yolo.py so that your ExLPose_train_WL.json is correctly converted to YOLOv8 pose format.

In the JSON2YOLO directory, run the script. python general_json2yolo.py

γ‚Ήγ‚―γƒͺγƒΌγƒ³γ‚·γƒ§γƒƒγƒˆ 2024-04-26 23 44 48

mycodeDev786 commented 7 months ago

Hi @mycodeDev786, thanks for sharing the COCO file.

We updated the general_json2yolo.py so that your ExLPose_train_WL.json is correctly converted to YOLOv8 pose format.

In the JSON2YOLO directory, run the script. python general_json2yolo.py

γ‚Ήγ‚―γƒͺγƒΌγƒ³γ‚·γƒ§γƒƒγƒˆ 2024-04-26 23 44 48

Thanks, it works for me now. I am working on pose estimation in low light conditions and want to optimize yolov8 for this purpose but i can't find any documentation about cutomizing yolov8-pose. can you guide me? thanks again ....... @ryouchinsa

ryouchinsa commented 7 months ago

Hi @mycodeDev786, this is a document how to train the pose model. https://docs.ultralytics.com/tasks/pose

For the low light conditions, the augmentation parameter hsv_v might be useful.

hsv_v modifies the value (brightness) of the image by a fraction, helping the model to perform well under various lighting conditions. https://github.com/ultralytics/ultralytics/issues/8308

Gnoot01 commented 1 month ago

@ryouchinsa

Hi, thank you for your work, it really helped me! I would like to add usage of 133 keypoints for coco-wholebody pose detection to your script. Feel free to incorporate this into your PR, if you want πŸ‘ . Hoping it helps any lost souls too

ryouchinsa commented 1 month ago

Hi @Gnoot01, great work! please add your code and submit the new PR by yourself. My PR is not accepted yet.

Seeeeeyo commented 2 weeks ago

Hey @Gnoot01 , I added some code to your repo to handle the different use cases (17, 23 and 133 keypoints). I opened a pull request on your repo. Thanks for the codebase!