From 1562449069a22598f43d0b2e9f1baa81c503115d Mon Sep 17 00:00:00 2001 From: John Wilkie <124276291+JBWilkie@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:43:03 +0000 Subject: [PATCH] Fixed regression with converting complex polygons to coco (#969) --- darwin/exporter/formats/coco.py | 86 +++++++++++++------ .../convert/coco/from/base_annotation.json | 28 +++++- e2e_tests/data/convert/coco/to/output.json | 39 +++++---- .../exporter/formats/export_coco_test.py | 32 ++++++- 4 files changed, 136 insertions(+), 49 deletions(-) diff --git a/darwin/exporter/formats/coco.py b/darwin/exporter/formats/coco.py index a66c1b53a..e66975383 100644 --- a/darwin/exporter/formats/coco.py +++ b/darwin/exporter/formats/coco.py @@ -6,6 +6,7 @@ import numpy as np import orjson as json +from upolygon import draw_polygon, rle_encode import darwin.datatypes as dt from darwin.utils import convert_polygons_to_sequences @@ -175,35 +176,64 @@ def _build_annotation( ) -> Optional[Dict[str, Any]]: annotation_type = annotation.annotation_class.annotation_type if annotation_type == "polygon": - sequences = convert_polygons_to_sequences( - annotation.data["paths"], rounding=False - ) - x_coords = [s[0::2] for s in sequences] - y_coords = [s[1::2] for s in sequences] - min_x = np.min([np.min(x_coord) for x_coord in x_coords]) - min_y = np.min([np.min(y_coord) for y_coord in y_coords]) - max_x = np.max([np.max(x_coord) for x_coord in x_coords]) - max_y = np.max([np.max(y_coord) for y_coord in y_coords]) - w = max_x - min_x - h = max_y - min_y - # Compute the area of the polygon - poly_area = np.sum( - [ - _polygon_area(x_coord, y_coord) - for x_coord, y_coord in zip(x_coords, y_coords) - ] - ) + if len(annotation.data["paths"]) == 1: + sequences = convert_polygons_to_sequences( + annotation.data["paths"], rounding=False + ) + x_coords = [s[0::2] for s in sequences] + y_coords = [s[1::2] for s in sequences] + min_x = np.min([np.min(x_coord) for x_coord in x_coords]) + min_y = np.min([np.min(y_coord) for y_coord in y_coords]) + max_x = np.max([np.max(x_coord) for x_coord in x_coords]) + max_y = np.max([np.max(y_coord) for y_coord in y_coords]) + w = max_x - min_x + h = max_y - min_y + # Compute the area of the polygon + poly_area = np.sum( + [ + _polygon_area(x_coord, y_coord) + for x_coord, y_coord in zip(x_coords, y_coords) + ] + ) - return { - "id": annotation_id, - "image_id": _build_image_id(annotation_file), - "category_id": categories[annotation.annotation_class.name], - "segmentation": sequences, - "area": poly_area, - "bbox": [min_x, min_y, w, h], - "iscrowd": 0, - "extra": _build_extra(annotation), - } + return { + "id": annotation_id, + "image_id": _build_image_id(annotation_file), + "category_id": categories[annotation.annotation_class.name], + "segmentation": sequences, + "area": poly_area, + "bbox": [min_x, min_y, w, h], + "iscrowd": 0, + "extra": _build_extra(annotation), + } + elif len(annotation.data["paths"]) > 1: + mask = np.zeros((annotation_file.image_height, annotation_file.image_width)) + sequences = convert_polygons_to_sequences(annotation.data["paths"]) + draw_polygon(mask, sequences, 1) + counts = rle_encode(mask) + + x_coords = [s[0::2] for s in sequences] + y_coords = [s[1::2] for s in sequences] + min_x = np.min([np.min(x_coord) for x_coord in x_coords]) + min_y = np.min([np.min(y_coord) for y_coord in y_coords]) + max_x = np.max([np.max(x_coord) for x_coord in x_coords]) + max_y = np.max([np.max(y_coord) for y_coord in y_coords]) + w = max_x - min_x + 1 + h = max_y - min_y + 1 + + return { + "id": annotation_id, + "image_id": _build_image_id(annotation_file), + "category_id": categories[annotation.annotation_class.name], + "segmentation": { + "counts": counts, + "size": [annotation_file.image_height, annotation_file.image_width], + }, + "area": 0, + "bbox": [min_x, min_y, w, h], + "iscrowd": 1, + "extra": _build_extra(annotation), + } elif annotation_type == "tag": pass elif annotation_type == "bounding_box": diff --git a/e2e_tests/data/convert/coco/from/base_annotation.json b/e2e_tests/data/convert/coco/from/base_annotation.json index b6b6e2d3c..4d6e2c67f 100644 --- a/e2e_tests/data/convert/coco/from/base_annotation.json +++ b/e2e_tests/data/convert/coco/from/base_annotation.json @@ -3,7 +3,15 @@ "schema_ref": "https://darwin-public.s3.eu-west-1.amazonaws.com/darwin_json/2.0/schema.json", "item": { "name": "", - "path": "/" + "path": "/", + "slots": [ + { + "type": "image", + "slot_name": "0", + "width": 1080, + "height": 1920 + } + ] }, "annotations": [ { @@ -59,6 +67,24 @@ "x": 0.0, "y": 1.0 } + ], + [ + { + "x": 1.0, + "y": 0.0 + }, + { + "x": 1.0, + "y": 1.0 + }, + { + "x": 0.0, + "y": 1.0 + }, + { + "x": 0.0, + "y": 0.0 + } ] ] } diff --git a/e2e_tests/data/convert/coco/to/output.json b/e2e_tests/data/convert/coco/to/output.json index 74ca00a02..5d93b0800 100644 --- a/e2e_tests/data/convert/coco/to/output.json +++ b/e2e_tests/data/convert/coco/to/output.json @@ -19,8 +19,8 @@ "license": 0, "file_name": "", "coco_url": "n/a", - "height": null, - "width": null, + "height": 1920, + "width": 1080, "date_captured": "", "flickr_url": "n/a", "darwin_url": null, @@ -60,26 +60,27 @@ "id": 3, "image_id": 2043925204, "category_id": 3961009249, - "segmentation": [ - [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0 + "segmentation": { + "counts": [ + 0, + 2, + 1918, + 2, + 2071678 + ], + "size": [ + 1920, + 1080 ] - ], - "area": 1.0, + }, + "area": 0, "bbox": [ - 0.0, - 0.0, - 1.0, - 1.0 + 0, + 0, + 2, + 2 ], - "iscrowd": 0, + "iscrowd": 1, "extra": {} } ], diff --git a/tests/darwin/exporter/formats/export_coco_test.py b/tests/darwin/exporter/formats/export_coco_test.py index b3471ddf3..d58899c2a 100644 --- a/tests/darwin/exporter/formats/export_coco_test.py +++ b/tests/darwin/exporter/formats/export_coco_test.py @@ -14,12 +14,14 @@ def annotation_file(self) -> dt.AnnotationFile: filename="test.json", annotation_classes=set(), annotations=[], + image_height=1920, + image_width=1080, ) def test_polygon_include_extras(self, annotation_file: dt.AnnotationFile): polygon = dt.Annotation( dt.AnnotationClass("polygon_class", "polygon"), - {"paths": [{"x": 1, "y": 1}, {"x": 2, "y": 2}, {"x": 1, "y": 2}]}, + {"paths": [[{"x": 1, "y": 1}, {"x": 2, "y": 2}, {"x": 1, "y": 2}]]}, [dt.make_instance_id(1)], ) @@ -29,6 +31,34 @@ def test_polygon_include_extras(self, annotation_file: dt.AnnotationFile): "extra" ] == {"instance_id": 1} + def test_complex_polygon(self, annotation_file: dt.AnnotationFile): + polygon = dt.Annotation( + dt.AnnotationClass("polygon_class", "polygon"), + { + "paths": [ + [{"x": 1, "y": 1}, {"x": 2, "y": 2}, {"x": 1, "y": 2}], + [{"x": 3, "y": 3}, {"x": 4, "y": 4}, {"x": 3, "y": 4}], + ] + }, + [], + ) + + categories = {"polygon_class": 1} + + annotations = coco._build_annotation(annotation_file, 1, polygon, categories) + assert annotations["segmentation"]["counts"] == [ + 1921, + 2, + 1919, + 1, + 1920, + 2, + 1919, + 1, + 2065915, + ] + assert annotations["segmentation"]["size"] == [1920, 1080] + def test_bounding_boxes_include_extras(self, annotation_file: dt.AnnotationFile): bbox = dt.Annotation( dt.AnnotationClass("bbox_class", "bounding_box"),