From ec399f9edfd6791d8cbad4c56043c032c7997967 Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Thu, 31 Aug 2023 09:32:05 +0200 Subject: [PATCH 01/17] Use cv2.drawFrameAxes in calibration (#84) * use cv2.drawFrameAxes to draw charuco pose * return image --- .../calibration/fiducial_markers.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py b/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py index 4a877bca..b130a72a 100644 --- a/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py +++ b/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py @@ -9,7 +9,6 @@ import cv2 import numpy as np -from airo_camera_toolkit.reprojection import project_frame_to_image_plane from airo_spatial_algebra import SE3Container from airo_typing import CameraIntrinsicsMatrixType, HomogeneousMatrixType, OpenCVIntImageType from cv2 import aruco @@ -152,27 +151,24 @@ def get_pose_of_charuco_board( def draw_frame_on_image( image: OpenCVIntImageType, frame_pose_in_camera: HomogeneousMatrixType, camera_matrix: CameraIntrinsicsMatrixType ) -> OpenCVIntImageType: - """draws a 2D projection of a frame on the iamge. Be careful when interpreting this visually, it is often hard to estimate the true 3D direction of an axis' 2D projection.""" - project_points = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) - origin, x_pos, y_pos, z_pos = project_frame_to_image_plane( - project_points, camera_matrix, frame_pose_in_camera - ).astype(int) - image = cv2.line(image, x_pos, origin, color=(0, 0, 255), thickness=2) - image = cv2.line(image, y_pos, origin, color=(0, 255, 0), thickness=2) - image = cv2.line(image, z_pos, origin, color=(255, 0, 0), thickness=2) + """Draws a 2D projection of a frame on the image. Be careful when interpreting this visually, it is often hard to estimate the true 3D direction of an axis' 2D projection.""" + charuco_se3 = SE3Container.from_homogeneous_matrix(frame_pose_in_camera) + rvec = charuco_se3.orientation_as_rotation_vector + tvec = charuco_se3.translation + image = cv2.drawFrameAxes(image, camera_matrix, None, rvec, tvec, 0.2) return image def visualize_aruco_detections( image: OpenCVIntImageType, aruco_result: ArucoMarkerDetectionResult ) -> OpenCVIntImageType: - """draws the aruco marker countours/corners and their IDs on the image""" + """Draws the aruco marker countours/corners and their IDs on the image""" image = aruco.drawDetectedMarkers(image, [x for x in aruco_result.corners], aruco_result.ids) return image def visualize_charuco_detection(image: OpenCVIntImageType, result: CharucoCornerDetectionResult) -> OpenCVIntImageType: - """draws the charuco checkerboard corners and their IDs on the image""" + """Draws the charuco checkerboard corners and their IDs on the image""" image = aruco.drawDetectedCornersCharuco(image, np.array(result.corners), np.array(result.ids), (255, 255, 0)) return image From 4822229d791c66c2f6433cda11687ff0236492c8 Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Thu, 31 Aug 2023 09:32:27 +0200 Subject: [PATCH 02/17] Bugfix: `orientation_as_axis_angle` dtype is not always float (#85) * cast axis and angle to float * remove newline --- airo-spatial-algebra/airo_spatial_algebra/se3.py | 2 +- airo-spatial-algebra/test/test_se3_container.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/airo-spatial-algebra/airo_spatial_algebra/se3.py b/airo-spatial-algebra/airo_spatial_algebra/se3.py index 0f569603..c9ff9cf8 100644 --- a/airo-spatial-algebra/airo_spatial_algebra/se3.py +++ b/airo-spatial-algebra/airo_spatial_algebra/se3.py @@ -113,7 +113,7 @@ def orientation_as_euler_angles(self) -> EulerAnglesType: @property def orientation_as_axis_angle(self) -> AxisAngleType: angle, axis = self.se3.angvec() - return axis, angle + return axis.astype(np.float64), float(angle) @property def orientation_as_rotation_vector(self) -> Vector3DType: diff --git a/airo-spatial-algebra/test/test_se3_container.py b/airo-spatial-algebra/test/test_se3_container.py index 2cce3d0e..26f26689 100644 --- a/airo-spatial-algebra/test/test_se3_container.py +++ b/airo-spatial-algebra/test/test_se3_container.py @@ -100,3 +100,11 @@ def test_quaternion_scalar_conversion(): quat, SE3Container.scalar_first_quaternion_to_scalar_last(SE3Container.scalar_last_quaternion_to_scalar_first(quat)), ).all() + + +def test_axis_angle_dtypes(): + se3 = SE3Container.from_homogeneous_matrix(np.identity(4)) + axis, angle = se3.orientation_as_axis_angle + assert isinstance(angle, float) + assert isinstance(axis, np.ndarray) + assert axis.dtype == np.float64 From 14b4dfaf1a03fa90a5a4a9d2a822a29550208652 Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Thu, 31 Aug 2023 09:47:10 +0200 Subject: [PATCH 03/17] Add issue template: bug report --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..32d1be6c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment:** + - Python: + +**Additional context** +Add any other context about the problem here. From 1203b89851926d18f4a748a18d9a68bd6122a872 Mon Sep 17 00:00:00 2001 From: victorlouisdg Date: Thu, 31 Aug 2023 10:03:33 +0200 Subject: [PATCH 04/17] formatting --- .github/ISSUE_TEMPLATE/bug_report.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 32d1be6c..6e350dca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,7 +24,8 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Environment:** - - Python: + +- Python: **Additional context** Add any other context about the problem here. From aafe5993cdf8a49246d619d01724a1efe99718da Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Thu, 31 Aug 2023 11:02:11 +0200 Subject: [PATCH 05/17] Update issue templates (#86) * Update issue templates * feature request and question template * format again * documentation template --- .github/ISSUE_TEMPLATE/documentation.md | 10 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/question.md | 10 ++++++++++ 3 files changed, 36 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/documentation.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 00000000..3cd053c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,10 @@ +--- +name: Documentation +about: Suggest a change to the documentation +title: "" +labels: documentation +assignees: "" +--- + +**Documentation** +Describe where you believe documentation is missing or should be improved. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..f6b35731 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement +assignees: "" +--- + +**Describe the feature you'd like** +A clear and concise description of what you'd like to have in airo-mono. + +**Use cases** +Describe for who and when your feature would be useful. + +**Possible implementation** +Describe any ideas you have for how this could work. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..bdbf9168 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: Question +about: Ask a question about airo-mono +title: "" +labels: question +assignees: "" +--- + +**Question** +Ask your question here. From 69a1c750b67838592d8667efbdc1ac9cf93eadda Mon Sep 17 00:00:00 2001 From: tlpss Date: Thu, 31 Aug 2023 11:30:58 +0200 Subject: [PATCH 06/17] CLI tool for splitting coco datasets --- airo-dataset-tools/airo_dataset_tools/cli.py | 19 +++ .../coco_tools/split_dataset.py | 109 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index d09e1af1..cdacf64c 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -7,6 +7,7 @@ import albumentations as A import click from airo_dataset_tools.coco_tools.coco_instances_to_yolo import create_yolo_dataset_from_coco_instances_dataset +from airo_dataset_tools.coco_tools.split_dataset import split_and_save_coco_dataset from airo_dataset_tools.coco_tools.transform_dataset import apply_transform_to_coco_dataset from airo_dataset_tools.cvat_labeling.convert_cvat_to_coco import cvat_image_to_coco from airo_dataset_tools.data_parsers.coco import CocoKeypointsDataset @@ -93,3 +94,21 @@ def coco_intances_to_yolo(coco_json: str, target_dir: str, use_segmentation: boo """Create a YOLO detections/segmentations dataset from a coco instances dataset""" print(f"converting coco instances dataset {coco_json} to yolo dataset {target_dir}") create_yolo_dataset_from_coco_instances_dataset(coco_json, target_dir, use_segmentation=use_segmentation) + + +@cli.command(name="split-coco-dataset") +@click.argument("json-path", type=click.Path(exists=True)) +@click.option("--split-ratios", type=float, multiple=True, required=True) +@click.option("--shuffle-before-splitting", is_flag=True, default=True) +def split_coco_dataset_cli(json_path: str, split_ratios: List[float], shuffle_before_splitting: bool) -> None: + """Split a COCO dataset into subsets according to the specified relative ratios and save them to disk. + Images are split with their corresponding annotations. No guarantees on class balance or annotation ratios. + + If two ratios are specified, the dataset will be split into two subsets. these will be called train/val by default. + If three ratios are specified, the dataset will be split into three subsets. these will be called train/val/test by default. + """ + split_and_save_coco_dataset(json_path, split_ratios, shuffle_before_splitting=shuffle_before_splitting) + + +if __name__ == "__main__": + cli() diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py new file mode 100644 index 00000000..cc966abf --- /dev/null +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py @@ -0,0 +1,109 @@ +""" Split a COCO dataset into subsets""" +import json +import pathlib +import random +from typing import List + +from airo_dataset_tools.data_parsers.coco import CocoInstanceAnnotation, CocoInstancesDataset, CocoKeypointsDataset + + +def split_coco_dataset( + coco_dataset: CocoInstancesDataset, split_ratios: List[float], shuffle_before_splitting: bool = True +) -> List[CocoInstancesDataset]: + """Split a COCO dataset into subsets by splitting the images according to the specified relative ratios. + All annotations for an image will be placed in the same subset as the image. + + Note that this does not guarantee the ratio of the annotations OR an equal class balance in each subset. + + Ratios must sum to 1.0. + """ + + ratio_sum = sum(split_ratios) + if abs(ratio_sum - 1.0) > 2e-2: + raise ValueError(f"Ratios must sum to 1.0. Ratios sum to {ratio_sum}.") + + # split the images into 2 subsets (random or ordered) + images = coco_dataset.images + + if shuffle_before_splitting: + random.shuffle(images) + + image_splits = [] + split_sizes = [round(ratio * len(images)) for ratio in split_ratios] + split_sizes[-1] = len(images) - sum(split_sizes[:-1]) # make sure the total number of images is correct + print(f"Split sizes: {split_sizes}") + + for size in split_sizes: + image_splits.append(images[:size]) + images = images[size:] + + image_id_to_split_id = {} + for split_id, image_split in enumerate(image_splits): + for image in image_split: + image_id_to_split_id[image.id] = split_id + + # gather the annotations for each subset + annotation_splits: List[List[CocoInstanceAnnotation]] = [[] for _ in range(len(split_ratios))] + for annotation in coco_dataset.annotations: + image_id = annotation.image_id + split_id = image_id_to_split_id[image_id] + # keep original image_ids and annotation_ids so that you could still reference the original dataset + annotation_splits[split_id].append(annotation) + + # create a new COCO dataset for each subset + dataset_type: type + if isinstance(coco_dataset, CocoKeypointsDataset): + dataset_type = CocoKeypointsDataset + else: + dataset_type = CocoInstancesDataset + + coco_dataset_splits: List[CocoInstancesDataset] = [] + for annotation_split, image_split in zip(annotation_splits, image_splits): + coco_dataset_split = dataset_type( + categories=coco_dataset.categories, images=image_split, annotations=annotation_split + ) + coco_dataset_splits.append(coco_dataset_split) + + return coco_dataset_splits + + +def split_and_save_coco_dataset( + coco_json_path: str, split_ratios: List[float], shuffle_before_splitting: bool = True +) -> None: + """Split a COCO dataset into subsets according to the specified relative ratios and save them to disk. + Images are split with their corresponding annotations. No guarantees on class balance or annotation ratios. + + Ratios must sum to 1.0. + + If two ratios are specified, the dataset will be split into two subsets. these will be called train/val by default. + If three ratios are specified, the dataset will be split into three subsets. these will be called train/val/test by default. + """ + split_names = ["train", "val", "test"] + if len(split_ratios) > len(split_names): + raise ValueError(f"Only {len(split_names)} splits are supported. {len(split_ratios)} splits were specified.") + + coco_dataset: CocoInstancesDataset + with open(coco_json_path, "r") as f: + try: + coco_dataset = CocoKeypointsDataset(**json.load(f)) + except TypeError: + print("Could not load as CocoKeypointsDataset. Trying CocoInstancesDataset") + coco_dataset = CocoInstancesDataset(**json.load(f)) + finally: + if not isinstance(coco_dataset, CocoKeypointsDataset) and not isinstance( + coco_dataset, CocoInstancesDataset + ): + raise ValueError("Could not load as CocoKeypointsDataset or CocoInstancesDataset") + + coco_dataset_splits = split_coco_dataset(coco_dataset, split_ratios, shuffle_before_splitting) + + for split_id, coco_dataset_split in enumerate(coco_dataset_splits): + + file_name = coco_json_path.replace(".json", f"_{split_names[split_id]}.json") + with open(file_name, "w") as f: + json.dump(coco_dataset_split.dict(), f) + + +if __name__ == "__main__": + json_path = pathlib.Path(__file__).parents[2] / "test" / "test_data" / "instances_val2017_small.json" + split_and_save_coco_dataset(str(json_path), [0.8, 0.2]) From 0ba8faf60d2462ee616327277852471191007549 Mon Sep 17 00:00:00 2001 From: tlpss Date: Thu, 31 Aug 2023 14:44:49 +0200 Subject: [PATCH 07/17] move fiftyone viewer --- airo-dataset-tools/airo_dataset_tools/cli.py | 2 +- .../airo_dataset_tools/{ => coco_tools}/fiftyone_viewer.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename airo-dataset-tools/airo_dataset_tools/{ => coco_tools}/fiftyone_viewer.py (100%) diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index cdacf64c..0749ec44 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -7,11 +7,11 @@ import albumentations as A import click from airo_dataset_tools.coco_tools.coco_instances_to_yolo import create_yolo_dataset_from_coco_instances_dataset +from airo_dataset_tools.coco_tools.fiftyone_viewer import view_coco_dataset from airo_dataset_tools.coco_tools.split_dataset import split_and_save_coco_dataset from airo_dataset_tools.coco_tools.transform_dataset import apply_transform_to_coco_dataset from airo_dataset_tools.cvat_labeling.convert_cvat_to_coco import cvat_image_to_coco from airo_dataset_tools.data_parsers.coco import CocoKeypointsDataset -from airo_dataset_tools.fiftyone_viewer import view_coco_dataset @click.group() diff --git a/airo-dataset-tools/airo_dataset_tools/fiftyone_viewer.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/fiftyone_viewer.py similarity index 100% rename from airo-dataset-tools/airo_dataset_tools/fiftyone_viewer.py rename to airo-dataset-tools/airo_dataset_tools/coco_tools/fiftyone_viewer.py From cd51b358d306b291c506b5f780c1c2d67113f567 Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Fri, 1 Sep 2023 13:28:23 +0200 Subject: [PATCH 08/17] Fix link in README.md --- airo-camera-toolkit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airo-camera-toolkit/README.md b/airo-camera-toolkit/README.md index ed99c76c..8ec93a39 100644 --- a/airo-camera-toolkit/README.md +++ b/airo-camera-toolkit/README.md @@ -86,7 +86,7 @@ See [reprojection.py](./airo_camera_toolkit/reprojection.py) for more details. ## Annotation tool -See [annotation_tool.md](annotation_tool.md) for usage instructions. +See [annotation_tool.md](./airo_camera_toolkit/annotation_tool.md) for usage instructions. ## Image Transforms From cc353e09ebd7a9a7a55b5c8f7f682cf1c1addcb8 Mon Sep 17 00:00:00 2001 From: tlpss Date: Fri, 15 Sep 2023 14:39:40 +0200 Subject: [PATCH 09/17] use PIL for image resizing in CLI --- airo-dataset-tools/airo_dataset_tools/cli.py | 4 +-- .../coco_tools/albumentations.py | 34 +++++++++++++++++++ airo-dataset-tools/test/test_pillow_resize.py | 9 +++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py create mode 100644 airo-dataset-tools/test/test_pillow_resize.py diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index 0749ec44..dac8ea69 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -4,8 +4,8 @@ import os from typing import List, Optional -import albumentations as A import click +from airo_dataset_tools.coco_tools.albumentations import PillowResize from airo_dataset_tools.coco_tools.coco_instances_to_yolo import create_yolo_dataset_from_coco_instances_dataset from airo_dataset_tools.coco_tools.fiftyone_viewer import view_coco_dataset from airo_dataset_tools.coco_tools.split_dataset import split_and_save_coco_dataset @@ -74,7 +74,7 @@ def resize_coco_keypoints_dataset(annotations_json_path: str, width: int, height ) os.makedirs(transformed_dataset_dir, exist_ok=True) - transforms = [A.Resize(height, width)] + transforms = [PillowResize(height, width)] coco_json = json.load(open(annotations_json_path, "r")) coco_dataset = CocoKeypointsDataset(**coco_json) transformed_dataset = apply_transform_to_coco_dataset( diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py new file mode 100644 index 00000000..976b5f80 --- /dev/null +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py @@ -0,0 +1,34 @@ +import albumentations as A +from PIL import Image +import numpy as np + + +class PillowResize(A.Resize): + """Use Pillow (instead of OpenCV) to resize the input to the given height and width. + always uses Bicubic interpolation. + + PIllow instead of Opencv because opencv does not adapt the filter size to the scaling factor, + which creates artifacts in the output for large downscaling factors. + cf. https://arxiv.org/pdf/2104.11222.pdf + + Args: + height (int): desired height of the output. + width (int): desired width of the output. + p (float): probability of applying the transform. Default: 1. + + Targets: + image, mask, bboxes, keypoints + + Image types: + uint8, float32 + """ + + def __init__(self, height, width, interpolation=Image.BICUBIC, always_apply=False, p=1): + super(PillowResize, self).__init__(height,width, always_apply=always_apply, p=p) + self.height = height + self.width = width + self.interpolation = interpolation + + def apply(self, img, **params): + return np.array(Image.fromarray(img).resize((self.width, self.height), self.interpolation)) + diff --git a/airo-dataset-tools/test/test_pillow_resize.py b/airo-dataset-tools/test/test_pillow_resize.py new file mode 100644 index 00000000..a49f4bbf --- /dev/null +++ b/airo-dataset-tools/test/test_pillow_resize.py @@ -0,0 +1,9 @@ +from airo_dataset_tools.coco_tools.albumentations import PillowResize +import numpy as np +import albumentations as A + +def test_resize(): + img = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) + resize = PillowResize(10,10) + resized_img = resize(image=img)['image'] + assert resized_img.shape == (10,10,3) From d518d674f6f63ea0e16219f9699ccf117c323e3d Mon Sep 17 00:00:00 2001 From: tlpss Date: Fri, 15 Sep 2023 14:51:47 +0200 Subject: [PATCH 10/17] mypy fixes for Resize --- .../coco_tools/albumentations.py | 17 ++++++++++------- airo-dataset-tools/test/test_pillow_resize.py | 10 +++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py index 976b5f80..fe5ca1a6 100644 --- a/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/albumentations.py @@ -1,14 +1,16 @@ +from typing import Any + import albumentations as A +import numpy as np from PIL import Image -import numpy as np -class PillowResize(A.Resize): +class PillowResize(A.Resize): # type: ignore """Use Pillow (instead of OpenCV) to resize the input to the given height and width. always uses Bicubic interpolation. PIllow instead of Opencv because opencv does not adapt the filter size to the scaling factor, - which creates artifacts in the output for large downscaling factors. + which creates artifacts in the output for large downscaling factors. cf. https://arxiv.org/pdf/2104.11222.pdf Args: @@ -23,12 +25,13 @@ class PillowResize(A.Resize): uint8, float32 """ - def __init__(self, height, width, interpolation=Image.BICUBIC, always_apply=False, p=1): - super(PillowResize, self).__init__(height,width, always_apply=always_apply, p=p) + def __init__( + self, height: int, width: int, interpolation: Any = Image.BICUBIC, always_apply: bool = False, p: float = 1.0 + ): + super(PillowResize, self).__init__(height, width, always_apply=always_apply, p=p) self.height = height self.width = width self.interpolation = interpolation - def apply(self, img, **params): + def apply(self, img: np.ndarray, **params: dict) -> np.ndarray: return np.array(Image.fromarray(img).resize((self.width, self.height), self.interpolation)) - diff --git a/airo-dataset-tools/test/test_pillow_resize.py b/airo-dataset-tools/test/test_pillow_resize.py index a49f4bbf..a1b578ec 100644 --- a/airo-dataset-tools/test/test_pillow_resize.py +++ b/airo-dataset-tools/test/test_pillow_resize.py @@ -1,9 +1,9 @@ +import numpy as np from airo_dataset_tools.coco_tools.albumentations import PillowResize -import numpy as np -import albumentations as A + def test_resize(): img = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8) - resize = PillowResize(10,10) - resized_img = resize(image=img)['image'] - assert resized_img.shape == (10,10,3) + resize = PillowResize(10, 10) + resized_img = resize(image=img)["image"] + assert resized_img.shape == (10, 10, 3) From 89f74bb0f9436aab37654c0ef0d5cc47362743ca Mon Sep 17 00:00:00 2001 From: Thomas Lips <37955681+tlpss@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:15:19 +0200 Subject: [PATCH 11/17] Coco split tool (2/2) (#90) * allow for extra fields in the coco dataset * make split agnostic to keypoints or instances (and fix pydantic) * add tests for split tool --- .../coco_tools/split_dataset.py | 39 +++++++++---------- .../airo_dataset_tools/data_parsers/coco.py | 6 ++- airo-dataset-tools/test/test_coco_load.py | 11 ++++++ airo-dataset-tools/test/test_coco_split.py | 33 ++++++++++++++++ .../person_keypoints_val2017_small.json | 2 +- 5 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 airo-dataset-tools/test/test_coco_split.py diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py index cc966abf..c0e98155 100644 --- a/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/split_dataset.py @@ -2,9 +2,10 @@ import json import pathlib import random -from typing import List +from typing import List, Optional -from airo_dataset_tools.data_parsers.coco import CocoInstanceAnnotation, CocoInstancesDataset, CocoKeypointsDataset +from airo_dataset_tools.data_parsers.coco import CocoImage, CocoInstanceAnnotation, CocoInstancesDataset +from pydantic.error_wrappers import ValidationError def split_coco_dataset( @@ -28,7 +29,7 @@ def split_coco_dataset( if shuffle_before_splitting: random.shuffle(images) - image_splits = [] + image_splits: List[List[CocoImage]] = [] split_sizes = [round(ratio * len(images)) for ratio in split_ratios] split_sizes[-1] = len(images) - sum(split_sizes[:-1]) # make sure the total number of images is correct print(f"Split sizes: {split_sizes}") @@ -50,16 +51,16 @@ def split_coco_dataset( # keep original image_ids and annotation_ids so that you could still reference the original dataset annotation_splits[split_id].append(annotation) + # check if none of the annotation splits are empty: + for split_id, annotation_split in enumerate(annotation_splits): + if len(annotation_split) == 0: + raise ValueError( + f"Split {split_id} is empty, which is not allowed. Please use a larger dataset or a smaller split ratio." + ) # create a new COCO dataset for each subset - dataset_type: type - if isinstance(coco_dataset, CocoKeypointsDataset): - dataset_type = CocoKeypointsDataset - else: - dataset_type = CocoInstancesDataset - coco_dataset_splits: List[CocoInstancesDataset] = [] for annotation_split, image_split in zip(annotation_splits, image_splits): - coco_dataset_split = dataset_type( + coco_dataset_split = CocoInstancesDataset( categories=coco_dataset.categories, images=image_split, annotations=annotation_split ) coco_dataset_splits.append(coco_dataset_split) @@ -82,18 +83,13 @@ def split_and_save_coco_dataset( if len(split_ratios) > len(split_names): raise ValueError(f"Only {len(split_names)} splits are supported. {len(split_ratios)} splits were specified.") - coco_dataset: CocoInstancesDataset + coco_dataset: Optional[CocoInstancesDataset] = None with open(coco_json_path, "r") as f: try: - coco_dataset = CocoKeypointsDataset(**json.load(f)) - except TypeError: - print("Could not load as CocoKeypointsDataset. Trying CocoInstancesDataset") coco_dataset = CocoInstancesDataset(**json.load(f)) - finally: - if not isinstance(coco_dataset, CocoKeypointsDataset) and not isinstance( - coco_dataset, CocoInstancesDataset - ): - raise ValueError("Could not load as CocoKeypointsDataset or CocoInstancesDataset") + except ValidationError as e: + print(e) + raise ValueError("Could not load CocoInstancesDataset") coco_dataset_splits = split_coco_dataset(coco_dataset, split_ratios, shuffle_before_splitting) @@ -105,5 +101,6 @@ def split_and_save_coco_dataset( if __name__ == "__main__": - json_path = pathlib.Path(__file__).parents[2] / "test" / "test_data" / "instances_val2017_small.json" - split_and_save_coco_dataset(str(json_path), [0.8, 0.2]) + json_path = pathlib.Path(__file__).parents[2] / "test" / "test_data" / "person_keypoints_val2017_small.json" + print(json_path) + split_and_save_coco_dataset(str(json_path), [0.5, 0.5]) diff --git a/airo-dataset-tools/airo_dataset_tools/data_parsers/coco.py b/airo-dataset-tools/airo_dataset_tools/data_parsers/coco.py index b86487a3..6c58951b 100644 --- a/airo-dataset-tools/airo_dataset_tools/data_parsers/coco.py +++ b/airo-dataset-tools/airo_dataset_tools/data_parsers/coco.py @@ -85,7 +85,8 @@ class CocoImage(BaseModel): __hash__ = object.__hash__ # make hashable for use in set -class CocoCategory(BaseModel): +class CocoCategory(BaseModel, extra="allow"): + supercategory: str # should be set to "name" for root category id: CategoryID name: str @@ -96,7 +97,8 @@ class CocoKeypointCategory(CocoCategory): skeleton: Optional[List[List[int]]] = None -class CocoInstanceAnnotation(BaseModel): +class CocoInstanceAnnotation(BaseModel, extra="allow"): # allow extra fields, to parse subclasses + id: int # unique id for the annotation image_id: ImageID category_id: CategoryID diff --git a/airo-dataset-tools/test/test_coco_load.py b/airo-dataset-tools/test/test_coco_load.py index 3069efea..a7b2b737 100644 --- a/airo-dataset-tools/test/test_coco_load.py +++ b/airo-dataset-tools/test/test_coco_load.py @@ -52,6 +52,17 @@ def test_coco_load_keypoints(): assert isinstance(coco_keypoints.categories[0], CocoKeypointCategory) +def test_coco_load_keypoints_as_instances_keeps_additional_fields(): + # to parse data that actually belongs to a subclass, useful for some coco tools where it does not matter what category the dataset is.. + test_dir = os.path.dirname(os.path.realpath(__file__)) + annotations = os.path.join(test_dir, "test_data/person_keypoints_val2017_small.json") + + with open(annotations, "r") as file: + data = json.load(file) + coco_keypoints = CocoInstancesDataset(**data) + assert len(coco_keypoints.annotations[0].keypoints) > 0 + + def test_coco_load_instances_incorrectly(): """Test whether an exception is raised when we try to load the regular instances as a keypoints dataset.""" test_dir = os.path.dirname(os.path.realpath(__file__)) diff --git a/airo-dataset-tools/test/test_coco_split.py b/airo-dataset-tools/test/test_coco_split.py new file mode 100644 index 00000000..46b19f8e --- /dev/null +++ b/airo-dataset-tools/test/test_coco_split.py @@ -0,0 +1,33 @@ +import json +import os + +import pytest +from airo_dataset_tools.coco_tools.split_dataset import split_coco_dataset +from airo_dataset_tools.data_parsers.coco import CocoInstancesDataset + + +def test_keypoints_split(): + test_dir = os.path.dirname(os.path.realpath(__file__)) + annotations = os.path.join(test_dir, "test_data/person_keypoints_val2017_small.json") + + with open(annotations, "r") as file: + data = json.load(file) + coco_keypoints = CocoInstancesDataset(**data) + datasets = split_coco_dataset(coco_keypoints, [0.5, 0.5]) + assert len(datasets) == 2 + assert len(datasets[0].annotations) == 1 + assert len(datasets[1].annotations) == 1 + assert len(datasets[0].images) == 1 + assert len(datasets[1].images) == 1 + assert len(datasets[0].annotations[0].keypoints) > 0 + + +def test_empty_annotations_split_raises_error(): + test_dir = os.path.dirname(os.path.realpath(__file__)) + annotations = os.path.join(test_dir, "test_data/person_keypoints_val2017_small.json") + + with open(annotations, "r") as file: + data = json.load(file) + coco_keypoints = CocoInstancesDataset(**data) + with pytest.raises(ValueError): + split_coco_dataset(coco_keypoints, [0.9, 0.1], shuffle_before_splitting=False) diff --git a/airo-dataset-tools/test/test_data/person_keypoints_val2017_small.json b/airo-dataset-tools/test/test_data/person_keypoints_val2017_small.json index ed669218..1c771ec8 100644 --- a/airo-dataset-tools/test/test_data/person_keypoints_val2017_small.json +++ b/airo-dataset-tools/test/test_data/person_keypoints_val2017_small.json @@ -93,7 +93,7 @@ 133, 2, 396, 162, 2, 489, 173, 2, 0, 0, 0, 0, 0, 0, 419, 214, 2, 458, 215, 2, 411, 274, 2, 458, 273, 2, 402, 333, 2, 465, 334, 2 ], - "image_id": 397133, + "image_id": 37777, "bbox": [388.66, 69.92, 109.41, 277.62], "category_id": 1, "id": 200887 From 2a8cf539210dca7c1ce4e1438b09c5ee52c2767f Mon Sep 17 00:00:00 2001 From: tlpss Date: Thu, 21 Sep 2023 14:42:47 +0200 Subject: [PATCH 12/17] replace 'images' in path for coco2yolo --- .../coco_tools/coco_instances_to_yolo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/coco_instances_to_yolo.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/coco_instances_to_yolo.py index 4720ddd1..e8714210 100644 --- a/airo-dataset-tools/airo_dataset_tools/coco_tools/coco_instances_to_yolo.py +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/coco_instances_to_yolo.py @@ -73,6 +73,13 @@ def create_yolo_dataset_from_coco_instances_dataset( relative_image_path = image_path.relative_to(_coco_dataset_json_path.parent) + # ultralytics parser finds the 'latest' occurance of 'images' in the dataset path, + # so we need to replace any occurance of 'image' with 'img' + # https://github.com/ultralytics/ultralytics/issues/3581 + + relative_image_path_str = str(relative_image_path).replace("image", "img") + relative_image_path = pathlib.Path(relative_image_path_str) + image = cv2.imread(str(image_path)) height, width, _ = image.shape @@ -124,8 +131,7 @@ def create_yolo_dataset_from_coco_instances_dataset( if __name__ == "__main__": - """example usage of the above function to resize all images in a coco dataset. - Copy the following lines into your own codebase and modify as needed.""" + """example usage""" import os path = pathlib.Path(__file__).parents[1] / "cvat_labeling" / "example" / "coco.json" From f0e10ab093e6a1fad0d09b6188a18ec9fcd199a9 Mon Sep 17 00:00:00 2001 From: tlpss Date: Thu, 21 Sep 2023 15:51:12 +0200 Subject: [PATCH 13/17] bugfix --- airo-dataset-tools/airo_dataset_tools/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index dac8ea69..3c521af4 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -86,7 +86,7 @@ def resize_coco_keypoints_dataset(annotations_json_path: str, width: int, height json.dump(transformed_dataset_dict, f) -@click.command(name="coco-instances-to-yolo") +@cli.command(name="coco-instances-to-yolo") @click.option("--coco_json", type=str) @click.option("--target_dir", type=str) @click.option("--use_segmentation", is_flag=True) From b75b0a61a4e4e60ad89250f16b513940c89981ca Mon Sep 17 00:00:00 2001 From: tlips Date: Tue, 26 Sep 2023 12:50:19 +0200 Subject: [PATCH 14/17] update install script to specify branch/commit --- scripts/install-airo-mono.sh | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/install-airo-mono.sh b/scripts/install-airo-mono.sh index 53938dc5..92bf40cd 100644 --- a/scripts/install-airo-mono.sh +++ b/scripts/install-airo-mono.sh @@ -1,4 +1,21 @@ #!/bin/bash +""" +This is a convenience installation script for the airo mono repo. +It installs all packages in the airo mono repo from a given branch/commit, if nothing is provided, it defaults to main. +This script can take up to a few minutes to complete. + +usage: +run this command from the desired python environment: +bash /install-airo-mono.sh [branch/commit ID] + +e.g. bash install-airo-mono.sh main to install the main. +""" + +# optional argument branch name +branch=${1:-main} + + +echo "Installing airo-mono from branch/commit $branch." package_names=( "airo-camera-toolkit" @@ -10,12 +27,16 @@ package_names=( ) # Base URL for the Git repository -base_url="https://github.com/airo-ugent/airo-mono@main#subdirectory=" +base_url="https://github.com/airo-ugent/airo-mono@${branch}#subdirectory=" # Loop through package names and execute pip install command for package_name in "${package_names[@]}" do - cmd="python -m pip install '${package_name}[external] @ git+$base_url$package_name'" + cmd="python -m pip install '${package_name}[external] @ git+${base_url}${package_name}'" + echo $cmd eval $cmd echo "Installed $package_name." -done \ No newline at end of file +done + +echo "Finished installing airo-mono from branch/commit $branch." +exit 0 \ No newline at end of file From 7353868ed5f721fc1b360b858632b1dc6d64e556 Mon Sep 17 00:00:00 2001 From: Victorlouisdg Date: Wed, 27 Sep 2023 21:34:43 +0200 Subject: [PATCH 15/17] convenience function to detect charco board pose in an image (#88) Co-authored-by: Thomas Lips <37955681+tlpss@users.noreply.github.com> --- .../calibration/fiducial_markers.py | 33 ++++++++++++++++++- airo-camera-toolkit/docs/live_charuco_pose.py | 21 ++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 airo-camera-toolkit/docs/live_charuco_pose.py diff --git a/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py b/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py index b130a72a..e225a5d4 100644 --- a/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py +++ b/airo-camera-toolkit/airo_camera_toolkit/calibration/fiducial_markers.py @@ -143,6 +143,38 @@ def get_pose_of_charuco_board( return charuco_pose_in_camera_frame +def detect_charuco_board( + image: OpenCVIntImageType, + camera_matrix: CameraIntrinsicsMatrixType, + dist_coeffs: Optional[np.ndarray] = None, + aruco_markers: ArucoDictType = AIRO_DEFAULT_ARUCO_DICT, + charuco_board: CharucoDictType = AIRO_DEFAULT_CHARUCO_BOARD, +) -> Optional[HomogeneousMatrixType]: + """Detect the pose of a charuco board from an image and the camera's intrinsics. + + Args: + image: An image that might contain a charuco board. + camera_matrix: The intrinsics of the camera that took the image. + dist_coeffs: The distortion coefficients of the camera that took the image. + aruco_markers: The dictionary from OpenCV that specifies the aruco marker parameters. + charuco_board: The dictionary from OpenCV that specifies the charuco board parameters. + + Returns: + Optional[HomogeneousMatrixType]: The pose of the charuco board in the camera frame, if it was detected. + """ + + aruco_result = detect_aruco_markers(image, aruco_markers) + if not aruco_result: + return None + + charuco_result = detect_charuco_corners(image, aruco_result, charuco_board) + if not charuco_result: + return None + + charuco_pose = get_pose_of_charuco_board(charuco_result, charuco_board, camera_matrix, dist_coeffs) + return charuco_pose + + ################# # visualization # ################# @@ -193,7 +225,6 @@ def visualize_marker_detections( charuco_y_count: Optional[int] = None, charuco_tile_size: Optional[int] = None, ) -> None: - aruco_dict = AIRO_DEFAULT_ARUCO_DICT detect_charuco = charuco_x_count is not None and charuco_y_count is not None and charuco_tile_size is not None if detect_charuco: diff --git a/airo-camera-toolkit/docs/live_charuco_pose.py b/airo-camera-toolkit/docs/live_charuco_pose.py new file mode 100644 index 00000000..41d8143b --- /dev/null +++ b/airo-camera-toolkit/docs/live_charuco_pose.py @@ -0,0 +1,21 @@ +import cv2 +from airo_camera_toolkit.calibration.fiducial_markers import detect_charuco_board, draw_frame_on_image +from airo_camera_toolkit.cameras.zed2i import Zed2i +from airo_camera_toolkit.utils import ImageConverter + +camera = Zed2i(fps=30) +intrinsics = camera.intrinsics_matrix() + +window_name = "Charuco Pose" +cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) + +while True: + image = camera.get_rgb_image_as_int() + image = ImageConverter.from_numpy_int_format(image).image_in_opencv_format + pose = detect_charuco_board(image, intrinsics) + if pose is not None: + draw_frame_on_image(image, pose, intrinsics) + cv2.imshow(window_name, image) + key = cv2.waitKey(1) + if key == ord("q"): + break From 60abe9b651c1d3cee7e36b2dc329ae5ef43b6092 Mon Sep 17 00:00:00 2001 From: tlpss Date: Tue, 17 Oct 2023 17:36:01 +0200 Subject: [PATCH 16/17] manually specify coco categories for cvat2coco --- airo-dataset-tools/airo_dataset_tools/cli.py | 11 +- .../cvat_labeling/convert_cvat_to_coco.py | 66 ++-- .../cvat_labeling/example/coco.json | 365 +----------------- .../example/coco_categories.json | 24 ++ 4 files changed, 75 insertions(+), 391 deletions(-) create mode 100644 airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco_categories.json diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index 3c521af4..7c37a6e1 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -43,11 +43,16 @@ def view_coco_dataset_cli( @cli.command(name="convert-cvat-to-coco-keypoints") @click.argument("cvat_xml_file", type=str, required=True) +@click.argument("coco_categories_json_file", type=str, required=True) @click.option("--add_bbox", is_flag=True, default=False, help="include bounding box in coco annotations") @click.option("--add_segmentation", is_flag=True, default=False, help="include segmentation in coco annotations") -def convert_cvat_to_coco_cli(cvat_xml_file: str, add_bbox: bool, add_segmentation: bool) -> None: - """Convert CVAT XML to COCO keypoints json""" - coco = cvat_image_to_coco(cvat_xml_file, add_bbox=add_bbox, add_segmentation=add_segmentation) +def convert_cvat_to_coco_cli( + cvat_xml_file: str, coco_categories_json_file: str, add_bbox: bool, add_segmentation: bool +) -> None: + """Convert CVAT XML to COCO keypoints json according to specified coco categories""" + coco = cvat_image_to_coco( + cvat_xml_file, coco_categories_json_file, add_bbox=add_bbox, add_segmentation=add_segmentation + ) path = os.path.dirname(cvat_xml_file) filename = os.path.basename(cvat_xml_file) path = os.path.join(path, filename.split(".")[0] + ".json") diff --git a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py index d10a55bf..6f15a795 100644 --- a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py +++ b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py @@ -9,6 +9,7 @@ import tqdm from airo_dataset_tools.cvat_labeling.load_xml_to_dict import get_dict_from_xml from airo_dataset_tools.data_parsers.coco import ( + CocoCategory, CocoImage, CocoKeypointAnnotation, CocoKeypointCategory, @@ -19,7 +20,7 @@ def cvat_image_to_coco( # noqa: C901, too complex - cvat_xml_path: str, add_bbox: bool = True, add_segmentation: bool = True + cvat_xml_path: str, coco_configuration_json_path: str, add_bbox: bool = True, add_segmentation: bool = True ) -> dict: """Function that converts an annotation XML in the CVAT 1.1 Image format to the COCO keypoints format. If you don't need keypoints, you can simply use CVAT to create a COCOinstances format and should not use this function! @@ -31,6 +32,7 @@ def cvat_image_to_coco( # noqa: C901, too complex Args: cvat_xml_path (str): _description_ + coco_configuration_json_path (str): path to the COCO categories to use for annotating this dataset. add_bbox (bool): add bounding box annotations to the COCO dataset, requires all keypoint annotations to have a bbox annotation add_segmentation (bool): add segmentation annotations to the COCO dataset, requires all keypoint annotations to have a mask annotation. Bboxes will be created from the segmentation masks. @@ -44,29 +46,18 @@ def cvat_image_to_coco( # noqa: C901, too complex coco_annotations: List[CocoKeypointAnnotation] = [] coco_categories: List[CocoKeypointCategory] = [] - annotation_id_counter = 1 # counter for the annotation ID - - # create the COCOKeypointCatgegories - categories_dict = defaultdict(list) - - for annotation_category in cvat_parsed.annotations.meta.get_job_or_task().labels.label: - assert isinstance(annotation_category, LabelItem) - category_str, annotation_name = annotation_category.name.split(".") - categories_dict[category_str].append(annotation_name) + # load the COCO categories from the configuration file + with open(coco_configuration_json_path, "r") as file: + coco_categories_config = json.load(file) + for category_dict in coco_categories_config["categories"]: + category = CocoCategory(**category_dict) + coco_categories.append(category) - for category_str, semantic_types in categories_dict.items(): - if add_bbox: - assert "bbox" in semantic_types, "bbox annotations are required" - if add_segmentation: - assert "mask" in semantic_types, "segmentation masks are required" + _validate_coco_categories_are_in_cvat( + cvat_parsed, coco_categories, add_bbox=add_bbox, add_segmentation=add_segmentation + ) - semantic_types = [ - semantic_type for semantic_type in semantic_types if semantic_type != "bbox" and semantic_type != "mask" - ] - coco_category = CocoKeypointCategory( - name=category_str, id=len(coco_categories) + 1, keypoints=semantic_types, supercategory="" - ) - coco_categories.append(coco_category) + annotation_id_counter = 1 # counter for the annotation ID # iterate over all cvat annotations (grouped per image) # and create the COCO Keypoint annotations @@ -124,6 +115,31 @@ def cvat_image_to_coco( # noqa: C901, too complex #################### +def _validate_coco_categories_are_in_cvat( + cvat_parsed: CVATImagesParser, coco_categories: List[CocoKeypointCategory], add_bbox: bool, add_segmentation: bool +) -> None: + # gather the annotation from CVAT + cvat_categories_dict = defaultdict(list) + + for annotation_category in cvat_parsed.annotations.meta.get_job_or_task().labels.label: + assert isinstance(annotation_category, LabelItem) + category_str, annotation_name = annotation_category.name.split(".") + cvat_categories_dict[category_str].append(annotation_name) + + for category_str, semantic_types in cvat_categories_dict.items(): + if add_bbox: + assert "bbox" in semantic_types, "bbox annotations are required" + if add_segmentation: + assert "mask" in semantic_types, "segmentation masks are required" + # find the matching COCO category + coco_category = None + for coco_category in coco_categories: + if coco_category.name == category_str: + break + for category_keypoint in coco_category.keypoints: + assert category_keypoint in semantic_types, f"semantic type {category_keypoint.name} not found" + + def _get_n_category_instances_in_image(cvat_image: ImageItem, category_name: str) -> int: """returns the number of instances for the specified category in the CVAT ImageItem. @@ -197,7 +213,7 @@ def _get_segmentation_for_instance_from_cvat_image(cvat_image: ImageItem, instan """returns the segmentation polygon for the instance in the cvat image.""" instance_id_str = str(instance_id) if cvat_image.polygon is None: - raise ValueError("segmentation annotations are required for image {cvat_image.name}") + raise ValueError(f"segmentation annotations are required for image {cvat_image.name}") if not isinstance(cvat_image.polygon, list): if instance_id_str == cvat_image.polygon.group_id: polygon_str = cvat_image.polygon.points @@ -274,6 +290,8 @@ def _extract_coco_keypoint_from_cvat_point(cvat_point: Point) -> List: path = pathlib.Path(__file__).parent.absolute() cvat_xml_file = str(path / "example" / "annotations.xml") - coco = cvat_image_to_coco(cvat_xml_file, add_bbox=True, add_segmentation=False) + coco_categories_file = str(path / "example" / "coco_categories.json") + + coco = cvat_image_to_coco(cvat_xml_file, coco_categories_file, add_bbox=True, add_segmentation=False) with open("coco.json", "w") as file: json.dump(coco, file) diff --git a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco.json b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco.json index dcaacdca..84f6446e 100644 --- a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco.json +++ b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco.json @@ -1,364 +1 @@ -{ - "categories": [ - { - "supercategory": "", - "id": 1, - "name": "towel", - "keypoints": [ - "corner1", - "corner2", - "corner3", - "corner4" - ] - }, - { - "supercategory": "", - "id": 2, - "name": "tshirt", - "keypoints": [ - "waist_left", - "waist_right", - "shoulder_left", - "shoulder_right" - ] - } - ], - "images": [ - { - "id": 1, - "width": 600, - "height": 338, - "file_name": "images/1.png" - }, - { - "id": 2, - "width": 500, - "height": 281, - "file_name": "images/2.png" - }, - { - "id": 3, - "width": 256, - "height": 256, - "file_name": "images/3.jpeg" - }, - { - "id": 4, - "width": 600, - "height": 338, - "file_name": "images/4.png" - } - ], - "annotations": [ - { - "id": 1, - "image_id": 1, - "category_id": 2, - "segmentation": [ - [ - 192.83, - 208.8, - 297.88, - 225.74, - 311.43, - 219.39, - 327.53, - 218.12, - 346.59, - 223.63, - 441.04, - 176.19, - 407.58, - 132.98, - 394.87, - 134.68, - 386.82, - 133.41, - 379.2, - 130.87, - 381.74, - 128.33, - 369.88, - 101.22, - 372.42, - 94.44, - 355.9, - 38.95, - 324.56, - 41.07, - 319.05, - 43.61, - 298.72, - 47.85, - 262.3, - 53.78, - 246.2, - 59.71, - 240.27, - 63.94, - 234.34, - 75.38, - 240.7, - 77.08, - 251.71, - 76.65, - 245.78, - 82.58, - 240.7, - 94.02, - 238.15, - 110.54, - 238.58, - 132.14, - 242.81, - 144.84, - 236.88, - 152.47, - 210.2, - 159.67 - ] - ], - "area": 0.0, - "bbox": [ - 192.41, - 35.99, - 252.02, - 191.45 - ], - "iscrowd": 0, - "keypoints": [ - 234.8, - 75.5, - 2.0, - 355.5, - 38.6, - 2.0, - 248.7, - 217.7, - 2.0, - 389.8, - 200.8, - 2.0 - ], - "num_keypoints": 4 - }, - { - "id": 2, - "image_id": 2, - "category_id": 1, - "segmentation": [ - [ - 167.5, - 179.56, - 198.43, - 188.84, - 250.4, - 159.14, - 257.83, - 126.97, - 226.89, - 81.81, - 173.06, - 105.94, - 145.22, - 118.31 - ] - ], - "area": 0.0, - "bbox": [ - 143.99, - 77.48, - 118.78999999999996, - 115.07000000000001 - ], - "iscrowd": 0, - "keypoints": [ - 192.24, - 171.52, - 1.0, - 240.5, - 141.82, - 1.0, - 224.42, - 80.57, - 2.0, - 143.99, - 118.93, - 2.0 - ], - "num_keypoints": 4 - }, - { - "id": 3, - "image_id": 3, - "category_id": 2, - "segmentation": [ - [ - 42.85, - 98.34, - 42.43, - 220.32, - 101.3, - 222.44, - 99.18, - 92.41, - 104.27, - 107.66, - 123.33, - 100.03, - 111.89, - 46.24, - 87.33, - 27.6, - 58.52, - 27.18, - 36.5, - 42.85, - 19.56, - 100.88, - 38.19, - 111.89 - ] - ], - "area": 0.0, - "bbox": [ - 13.2, - 24.21, - 114.78999999999999, - 207.54999999999998 - ], - "iscrowd": 0, - "keypoints": [ - 40.73, - 217.36, - 2.0, - 99.61, - 219.05, - 2.0, - 34.6, - 48.0, - 2.0, - 108.8, - 46.4, - 2.0 - ], - "num_keypoints": 4 - }, - { - "id": 4, - "image_id": 3, - "category_id": 2, - "segmentation": [ - [ - 178.39, - 28.45, - 196.6, - 28.45, - 224.98, - 50.9, - 236.84, - 101.3, - 221.59, - 112.74, - 218.63, - 100.88, - 214.82, - 117.82, - 214.82, - 161.45, - 217.36, - 216.09, - 207.19, - 224.13, - 172.46, - 224.56, - 157.64, - 217.78, - 158.48, - 177.97, - 160.18, - 135.19, - 158.91, - 97.49, - 155.09, - 111.04, - 138.58, - 103.42, - 142.81, - 86.05, - 150.44, - 46.24 - ] - ], - "area": 0.0, - "bbox": [ - 137.31, - 24.64, - 101.22999999999999, - 208.39 - ], - "iscrowd": 0, - "keypoints": [ - 216.09, - 219.05, - 2.0, - 158.48, - 217.78, - 2.0, - 223.8, - 48.2, - 2.0, - 153.9, - 49.5, - 2.0 - ], - "num_keypoints": 4 - }, - { - "id": 5, - "image_id": 4, - "category_id": 1, - "segmentation": [ - [ - 202.02, - 137.24, - 309.05, - 282.64, - 351.74, - 249.84, - 405.57, - 191.07, - 350.51, - 132.29, - 291.73, - 72.28, - 250.28, - 98.26 - ] - ], - "area": 0.0, - "bbox": [ - 197.69, - 67.95, - 218.39999999999998, - 220.25 - ], - "iscrowd": 0, - "keypoints": [ - 203.3, - 137.2, - 2.0, - 292.35, - 74.13, - 2.0, - 408.04, - 186.74, - 2.0, - 308.43, - 277.69, - 2.0 - ], - "num_keypoints": 4 - } - ] -} \ No newline at end of file +{"categories": [{"supercategory": "", "id": 1, "name": "towel", "keypoints": ["corner1", "corner2", "corner3", "corner4"]}, {"supercategory": "", "id": 2, "name": "tshirt", "keypoints": ["waist_left"]}], "images": [{"id": 1, "width": 600, "height": 338, "file_name": "1.png"}, {"id": 2, "width": 500, "height": 281, "file_name": "2.png"}, {"id": 3, "width": 256, "height": 256, "file_name": "3.jpeg"}, {"id": 4, "width": 600, "height": 338, "file_name": "4.png"}], "annotations": [{"id": 1, "image_id": 1, "category_id": 2, "bbox": [192.41, 35.99, 252.02, 191.45], "keypoints": [234.8, 75.5, 2.0], "num_keypoints": 1}, {"id": 2, "image_id": 2, "category_id": 1, "bbox": [143.99, 77.48, 118.78999999999996, 115.07000000000001], "keypoints": [192.24, 171.52, 1.0, 240.5, 141.82, 1.0, 224.42, 80.57, 2.0, 143.99, 118.93, 2.0], "num_keypoints": 4}, {"id": 3, "image_id": 3, "category_id": 2, "bbox": [13.2, 24.21, 114.78999999999999, 207.54999999999998], "keypoints": [40.73, 217.36, 2.0], "num_keypoints": 1}, {"id": 4, "image_id": 3, "category_id": 2, "bbox": [137.31, 24.64, 101.22999999999999, 208.39], "keypoints": [216.09, 219.05, 2.0], "num_keypoints": 1}, {"id": 5, "image_id": 4, "category_id": 1, "bbox": [197.69, 67.95, 218.39999999999998, 220.25], "keypoints": [203.3, 137.2, 2.0, 292.35, 74.13, 2.0, 408.04, 186.74, 2.0, 308.43, 277.69, 2.0], "num_keypoints": 4}]} \ No newline at end of file diff --git a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco_categories.json b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco_categories.json new file mode 100644 index 00000000..87819396 --- /dev/null +++ b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/example/coco_categories.json @@ -0,0 +1,24 @@ +{ + "categories": [ + { + "supercategory": "", + "id": 1, + "name": "towel", + "keypoints": [ + "corner1", + "corner2", + "corner3", + "corner4" + ] + }, + { + "supercategory": "", + "id": 2, + "name": "tshirt", + "keypoints": [ + "waist_left" + + ] + } + ] +} \ No newline at end of file From 7bc6753ff3b5d064b591258af3399b53364ad027 Mon Sep 17 00:00:00 2001 From: tlpss Date: Thu, 19 Oct 2023 11:54:48 +0200 Subject: [PATCH 17/17] separate resize functionality from CLI --- airo-dataset-tools/airo_dataset_tools/cli.py | 25 ++--------- .../coco_tools/transform_dataset.py | 45 ++++++++++++------- .../cvat_labeling/convert_cvat_to_coco.py | 2 +- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/airo-dataset-tools/airo_dataset_tools/cli.py b/airo-dataset-tools/airo_dataset_tools/cli.py index 7c37a6e1..7043c83a 100644 --- a/airo-dataset-tools/airo_dataset_tools/cli.py +++ b/airo-dataset-tools/airo_dataset_tools/cli.py @@ -5,13 +5,11 @@ from typing import List, Optional import click -from airo_dataset_tools.coco_tools.albumentations import PillowResize from airo_dataset_tools.coco_tools.coco_instances_to_yolo import create_yolo_dataset_from_coco_instances_dataset from airo_dataset_tools.coco_tools.fiftyone_viewer import view_coco_dataset from airo_dataset_tools.coco_tools.split_dataset import split_and_save_coco_dataset -from airo_dataset_tools.coco_tools.transform_dataset import apply_transform_to_coco_dataset +from airo_dataset_tools.coco_tools.transform_dataset import resize_coco_keypoints_dataset from airo_dataset_tools.cvat_labeling.convert_cvat_to_coco import cvat_image_to_coco -from airo_dataset_tools.data_parsers.coco import CocoKeypointsDataset @click.group() @@ -64,31 +62,14 @@ def convert_cvat_to_coco_cli( @click.argument("annotations-json-path", type=click.Path(exists=True)) @click.option("--width", type=int, required=True) @click.option("--height", type=int, required=True) -def resize_coco_keypoints_dataset(annotations_json_path: str, width: int, height: int) -> None: +def resize_coco_keypoints_dataset_cli(annotations_json_path: str, width: int, height: int) -> None: """Resize a COCO dataset. Will create a new directory with the resized dataset on the same level as the original dataset. Dataset is assumed to be /dir annotations.json # contains relative paths w.r.t. /dir ... """ - coco_dataset_dir = os.path.dirname(annotations_json_path) - annotations_file_name = os.path.basename(annotations_json_path) - dataset_parent_dir = os.path.dirname(coco_dataset_dir) - transformed_dataset_dir = os.path.join( - dataset_parent_dir, f"{annotations_file_name.split('.')[0]}_resized_{width}x{height}" - ) - os.makedirs(transformed_dataset_dir, exist_ok=True) - - transforms = [PillowResize(height, width)] - coco_json = json.load(open(annotations_json_path, "r")) - coco_dataset = CocoKeypointsDataset(**coco_json) - transformed_dataset = apply_transform_to_coco_dataset( - transforms, coco_dataset, coco_dataset_dir, transformed_dataset_dir - ) - - transformed_dataset_dict = transformed_dataset.dict(exclude_none=True) - with open(os.path.join(transformed_dataset_dir, annotations_file_name), "w") as f: - json.dump(transformed_dataset_dict, f) + resize_coco_keypoints_dataset(annotations_json_path, width, height) @cli.command(name="coco-instances-to-yolo") diff --git a/airo-dataset-tools/airo_dataset_tools/coco_tools/transform_dataset.py b/airo-dataset-tools/airo_dataset_tools/coco_tools/transform_dataset.py index 8b121d1d..ca9a363c 100644 --- a/airo-dataset-tools/airo_dataset_tools/coco_tools/transform_dataset.py +++ b/airo-dataset-tools/airo_dataset_tools/coco_tools/transform_dataset.py @@ -4,6 +4,7 @@ import albumentations as A import numpy as np import tqdm +from airo_dataset_tools.coco_tools.albumentations import PillowResize from airo_dataset_tools.data_parsers.coco import ( CocoImage, CocoInstanceAnnotation, @@ -13,6 +14,7 @@ ) from airo_dataset_tools.segmentation_mask_converter import BinarySegmentationMask from PIL import Image +import json def apply_transform_to_coco_dataset( # type: ignore # noqa: C901 @@ -153,26 +155,39 @@ def apply_transform_to_coco_dataset( # type: ignore # noqa: C901 return coco_dataset +def resize_coco_keypoints_dataset(annotations_json_path: str, width: int, height: int) -> None: + """Resize a COCO dataset. Will create a new directory with the resized dataset on the same level as the original dataset. + Dataset is assumed to be + /dir + annotations.json # contains relative paths w.r.t. /dir + ... + """ + coco_dataset_dir = os.path.dirname(annotations_json_path) + annotations_file_name = os.path.basename(annotations_json_path) + dataset_parent_dir = os.path.dirname(coco_dataset_dir) + transformed_dataset_dir = os.path.join( + dataset_parent_dir, f"{annotations_file_name.split('.')[0]}_resized_{width}x{height}" + ) + os.makedirs(transformed_dataset_dir, exist_ok=True) + + transforms = [PillowResize(height, width)] + coco_json = json.load(open(annotations_json_path, "r")) + coco_dataset = CocoKeypointsDataset(**coco_json) + transformed_dataset = apply_transform_to_coco_dataset( + transforms, coco_dataset, coco_dataset_dir, transformed_dataset_dir + ) + + transformed_dataset_dict = transformed_dataset.dict(exclude_none=True) + with open(os.path.join(transformed_dataset_dir, annotations_file_name), "w") as f: + json.dump(transformed_dataset_dict, f) + + if __name__ == "__main__": """example usage of the above function to resize all images in a coco dataset. Copy the following lines into your own codebase and modify as needed.""" - import json import pathlib path = pathlib.Path(__file__).parents[1] / "cvat_labeling" / "example" / "coco.json" coco_json_path = str(path) - coco_dir = os.path.dirname(coco_json_path) - coco_file_name = os.path.basename(coco_json_path) - coco_target_dir = os.path.join(os.path.dirname(coco_dir), "transformed") - os.makedirs(coco_target_dir, exist_ok=True) - - transforms = [A.Resize(128, 128)] - - coco_json = json.load(open(coco_json_path, "r")) - coco_dataset = CocoKeypointsDataset(**coco_json) - transformed_dataset = apply_transform_to_coco_dataset(transforms, coco_dataset, coco_dir, coco_target_dir) - - transformed_dataset_dict = transformed_dataset.dict(exclude_none=True) - with open(os.path.join(coco_target_dir, coco_file_name), "w") as f: - json.dump(transformed_dataset_dict, f) + resize_coco_keypoints_dataset(coco_json_path, 640, 480) diff --git a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py index 6f15a795..fae31dd1 100644 --- a/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py +++ b/airo-dataset-tools/airo_dataset_tools/cvat_labeling/convert_cvat_to_coco.py @@ -137,7 +137,7 @@ def _validate_coco_categories_are_in_cvat( if coco_category.name == category_str: break for category_keypoint in coco_category.keypoints: - assert category_keypoint in semantic_types, f"semantic type {category_keypoint.name} not found" + assert category_keypoint in semantic_types, f"semantic type {category_keypoint} not found" def _get_n_category_instances_in_image(cvat_image: ImageItem, category_name: str) -> int: