From 7a8f22f91190c98528cba5d81664a918ff383c3f Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Thu, 4 May 2023 14:36:43 +0900 Subject: [PATCH 1/6] Added use_keypoints to convert he COCO keypoints format to YOLOv8 pose format. --- general_json2yolo.py | 63 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/general_json2yolo.py b/general_json2yolo.py index f1ada2c..efc7af4 100644 --- a/general_json2yolo.py +++ b/general_json2yolo.py @@ -5,6 +5,7 @@ import pandas as pd from PIL import Image from collections import defaultdict +from pycocotools import mask from utils import * @@ -250,7 +251,7 @@ def convert_ath_json(json_dir): # dir contains json annotations and images print(f'Done. Output saved to {Path(dir).absolute()}') -def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91to80=False): +def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, use_keypoints=False, cls91to80=False): save_dir = make_dirs() # output directory coco80 = coco91_to_coco80_class() @@ -268,6 +269,9 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91 for ann in data['annotations']: imgToAnns[ann['image_id']].append(ann) + if use_keypoints: + show_kpt_shape_flip_idx(data) + # Write labels file for img_id, anns in tqdm(imgToAnns.items(), desc=f'Annotations {json_file}'): img = images['%g' % img_id] @@ -275,6 +279,7 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91 bboxes = [] segments = [] + keypoints = [] for ann in anns: if ann['iscrowd']: continue @@ -290,8 +295,12 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91 box = [cls] + box.tolist() if box not in bboxes: bboxes.append(box) - # Segments if use_segments: + if len(ann['segmentation']) == 0: + segments.append([]) + continue + if isinstance(ann['segmentation'], dict): + ann['segmentation'] = rle2polygon(ann['segmentation']) if len(ann['segmentation']) > 1: s = merge_multi_segment(ann['segmentation']) s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist() @@ -301,14 +310,57 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91 s = [cls] + s if s not in segments: segments.append(s) + 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) # Write with open((fn / f).with_suffix('.txt'), 'a') as file: for i in range(len(bboxes)): - line = *(segments[i] if use_segments else bboxes[i]), # cls, box or segments + if use_keypoints: + line = *(keypoints[i]), # cls, box, keypoints + else: + line = *(segments[i] if use_segments and len(segments[i]) > 0 else bboxes[i]), # cls, box or segments file.write(('%g ' * len(line)).rstrip() % line + '\n') +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) + ']') + + +def rle2polygon(segmentation): + m = mask.decode(segmentation) + m[m > 0] = 255 + contours, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS) + polygons = [] + for contour in contours: + epsilon = 0.001 * cv2.arcLength(contour, True) + contour_approx = cv2.approxPolyDP(contour, epsilon, True) + polygon = contour_approx.flatten().tolist() + polygons.append(polygon) + return polygons + + def min_index(arr1, arr2): """Find a pair of indexes with the shortest distance. Args: @@ -385,8 +437,9 @@ def delete_dsstore(path='../datasets'): if source == 'COCO': convert_coco_json('../datasets/coco/annotations', # directory with *.json - use_segments=True, - cls91to80=True) + use_segments=False, + use_keypoints=True, + cls91to80=False) elif source == 'infolks': # Infolks https://infolks.info/ convert_infolks_json(name='out', From f51305d70d9850f10d9b7dd34c80a2fa1b64b5c3 Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Wed, 22 Nov 2023 22:05:38 +0900 Subject: [PATCH 2/6] Update general_json2yolo.py --- general_json2yolo.py | 103 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/general_json2yolo.py b/general_json2yolo.py index efc7af4..6027540 100644 --- a/general_json2yolo.py +++ b/general_json2yolo.py @@ -276,13 +276,12 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, use_k 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'] - + f = f.split('/')[-1] + bboxes = [] segments = [] keypoints = [] 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 @@ -348,18 +347,100 @@ def show_kpt_shape_flip_idx(data): print('flip_idx: [' + ', '.join(str(x) for x in flip_idx) + ']') -def rle2polygon(segmentation): - m = mask.decode(segmentation) - m[m > 0] = 255 - contours, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS) +def is_clockwise(contour): + value = 0 + num = len(contour) + for i, point in enumerate(contour): + p1 = contour[i] + if i < num - 1: + p2 = contour[i + 1] + else: + p2 = contour[0] + value += (p2[0][0] - p1[0][0]) * (p2[0][1] + p1[0][1]); + return value < 0 + +def get_merge_point_idx(contour1, contour2): + idx1 = 0 + idx2 = 0 + distance_min = -1 + for i, p1 in enumerate(contour1): + for j, p2 in enumerate(contour2): + distance = pow(p2[0][0] - p1[0][0], 2) + pow(p2[0][1] - p1[0][1], 2); + if distance_min < 0: + distance_min = distance + idx1 = i + idx2 = j + elif distance < distance_min: + distance_min = distance + idx1 = i + idx2 = j + return idx1, idx2 + +def merge_contours(contour1, contour2, idx1, idx2): + contour = [] + for i in list(range(0, idx1 + 1)): + contour.append(contour1[i]) + for i in list(range(idx2, len(contour2))): + contour.append(contour2[i]) + for i in list(range(0, idx2 + 1)): + contour.append(contour2[i]) + for i in list(range(idx1, len(contour1))): + contour.append(contour1[i]) + contour = np.array(contour) + return contour + +def merge_with_parent(contour_parent, contour): + if not is_clockwise(contour_parent): + contour_parent = contour_parent[::-1] + if is_clockwise(contour): + contour = contour[::-1] + idx1, idx2 = get_merge_point_idx(contour_parent, contour) + return merge_contours(contour_parent, contour, idx1, idx2) + +def mask2polygon(image): + contours, hierarchies = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_KCOS) + contours_approx = [] polygons = [] for contour in contours: epsilon = 0.001 * cv2.arcLength(contour, True) contour_approx = cv2.approxPolyDP(contour, epsilon, True) - polygon = contour_approx.flatten().tolist() + contours_approx.append(contour_approx) + + contours_parent = [] + for i, contour in enumerate(contours_approx): + parent_idx = hierarchies[0][i][3] + if parent_idx < 0 and len(contour) >= 3: + contours_parent.append(contour) + else: + contours_parent.append([]) + + for i, contour in enumerate(contours_approx): + parent_idx = hierarchies[0][i][3] + if parent_idx >= 0 and len(contour) >= 3: + contour_parent = contours_parent[parent_idx] + if len(contour_parent) == 0: + continue + contours_parent[parent_idx] = merge_with_parent(contour_parent, contour) + + contours_parent_tmp = [] + for contour in contours_parent: + if len(contour) == 0: + continue + contours_parent_tmp.append(contour) + + polygons = [] + for contour in contours_parent_tmp: + polygon = contour.flatten().tolist() polygons.append(polygon) - return polygons + return polygons +def rle2polygon(segmentation): + if isinstance(segmentation["counts"], list): + segmentation = mask.frPyObjects(segmentation, *segmentation["size"]) + m = mask.decode(segmentation) + m[m > 0] = 255 + polygons = mask2polygon(m) + return polygons def min_index(arr1, arr2): """Find a pair of indexes with the shortest distance. @@ -437,8 +518,8 @@ def delete_dsstore(path='../datasets'): if source == 'COCO': convert_coco_json('../datasets/coco/annotations', # directory with *.json - use_segments=False, - use_keypoints=True, + use_segments=True, + use_keypoints=False, cls91to80=False) elif source == 'infolks': # Infolks https://infolks.info/ From 920399767efa666183dd5f1e3bf473dd41cc68a2 Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Thu, 23 Nov 2023 16:29:34 +0900 Subject: [PATCH 3/6] Update general_json2yolo.py --- general_json2yolo.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/general_json2yolo.py b/general_json2yolo.py index 6027540..6740876 100644 --- a/general_json2yolo.py +++ b/general_json2yolo.py @@ -310,9 +310,13 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, use_k if s not in segments: segments.append(s) 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) + if 'keypoints' not in ann: + keypoints.append([]) + continue + else: + k = (np.array(ann['keypoints']).reshape(-1, 3) / np.array([w, h, 1])).reshape(-1).tolist() + k = box + k + keypoints.append(k) # Write with open((fn / f).with_suffix('.txt'), 'a') as file: From c0daf1103027cc2e1ee5adcaf3ac19ad53e5b189 Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Mon, 27 Nov 2023 18:52:24 +0900 Subject: [PATCH 4/6] Create FUNDING.yml --- .github/FUNDING.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ec1ccc8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [ryouchinsa] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 3c92ed09ea49f91cddda1e5ff171658353d9abca Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Wed, 29 Nov 2023 16:36:38 +0900 Subject: [PATCH 5/6] added bbox_from_keypoints --- general_json2yolo.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/general_json2yolo.py b/general_json2yolo.py index 6740876..688b570 100644 --- a/general_json2yolo.py +++ b/general_json2yolo.py @@ -283,7 +283,10 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, use_k keypoints = [] for ann in anns: # The COCO box format is [top left x, top left y, width, height] - box = np.array(ann['bbox'], dtype=np.float64) + if len(ann['bbox']) == 0: + box = bbox_from_keypoints(ann) + else: + 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 @@ -327,6 +330,13 @@ def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, use_k line = *(segments[i] if use_segments and len(segments[i]) > 0 else bboxes[i]), # cls, box or segments file.write(('%g ' * len(line)).rstrip() % line + '\n') +def bbox_from_keypoints(ann): + if 'keypoints' not in ann: + return + k = np.array(ann['keypoints']).reshape(-1, 3) + x_list, y_list, v_list = zip(*k) + box = [min(x_list), min(y_list), max(x_list) - min(x_list), max(y_list) - min(y_list)] + return np.array(box, dtype=np.float64) def show_kpt_shape_flip_idx(data): for category in data['categories']: From 7e3d57c839ddca337334021cd9e5884eecbbd5a7 Mon Sep 17 00:00:00 2001 From: Ryo Kawamura Date: Wed, 29 Nov 2023 17:05:36 +0900 Subject: [PATCH 6/6] Delete .github directory --- .github/FUNDING.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ec1ccc8..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,13 +0,0 @@ -# These are supported funding model platforms - -github: [ryouchinsa] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']