From ec108d1ab61e26bb4e94141cee3eb8de398a104c Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 30 Dec 2021 13:43:04 +0100 Subject: [PATCH 1/2] added support for superannotate tags --- darwin/importer/formats/superannotate.py | 22 +- .../importer/formats/superannotate_schemas.py | 28 ++- .../importer/formats/import_labelbox_test.py | 2 +- .../formats/import_superannotate_test.py | 200 ++++++++++-------- 4 files changed, 156 insertions(+), 96 deletions(-) diff --git a/darwin/importer/formats/superannotate.py b/darwin/importer/formats/superannotate.py index cc87b073e..8356ca1b8 100644 --- a/darwin/importer/formats/superannotate.py +++ b/darwin/importer/formats/superannotate.py @@ -20,6 +20,7 @@ make_keypoint, make_line, make_polygon, + make_tag, ) from darwin.importer.formats.superannotate_schemas import ( classes_export, @@ -47,6 +48,7 @@ def parse_path(path: Path) -> Optional[AnnotationFile]: }, // { ... } ], + "tags": ["orange"], "metadata": { "name": "a_file_name.json" } @@ -66,7 +68,7 @@ def parse_path(path: Path) -> Optional[AnnotationFile]: .. code-block:: javascript [ - {"name": "a_name_here", "id": 1}, + {"name": "a_name_here", "id": 1, "attribute_groups": []}, // { ... } ] @@ -108,8 +110,9 @@ def parse_path(path: Path) -> Optional[AnnotationFile]: instances: List[Dict[str, Any]] = data.get("instances") metadata: Dict[str, Any] = data.get("metadata") + tags: List[str] = data.get("tags") - return _convert(instances, path, classes, metadata) + return _convert(instances, path, classes, metadata, tags) def _convert( @@ -117,11 +120,14 @@ def _convert( annotation_file_path: Path, superannotate_classes: List[Dict[str, Any]], metadata: Dict[str, Any], + tags: List[str], ) -> AnnotationFile: - filename: str = str(metadata.get("name")) + conver_to_darwin_object = partial(_convert_instance, superannotate_classes=superannotate_classes) - convert_with_classes = partial(_convert_objects, superannotate_classes=superannotate_classes) - annotations: List[Annotation] = _map_to_list(convert_with_classes, instances) + filename: str = str(metadata.get("name")) + darwin_tags: List[Annotation] = _map_to_list(_convert_tag, tags) + darwin_objects: List[Annotation] = _map_to_list(conver_to_darwin_object, instances) + annotations: List[Annotation] = darwin_objects + darwin_tags classes: Set[AnnotationClass] = _map_to_set(_get_class, annotations) return AnnotationFile( @@ -133,7 +139,7 @@ def _convert( ) -def _convert_objects(obj: Dict[str, Any], superannotate_classes: List[Dict[str, Any]]) -> Annotation: +def _convert_instance(obj: Dict[str, Any], superannotate_classes: List[Dict[str, Any]]) -> Annotation: type: str = str(obj.get("type")) if type == "point": @@ -157,6 +163,10 @@ def _convert_objects(obj: Dict[str, Any], superannotate_classes: List[Dict[str, raise ValueError(f"Unknown label object {obj}") +def _convert_tag(tag: str) -> Annotation: + return make_tag(tag) + + def _to_keypoint_annotation(point: Dict[str, Any], classes: List[Dict[str, Any]]) -> Annotation: x: float = cast(float, point.get("x")) y: float = cast(float, point.get("y")) diff --git a/darwin/importer/formats/superannotate_schemas.py b/darwin/importer/formats/superannotate_schemas.py index bb60e67c5..050de3c97 100644 --- a/darwin/importer/formats/superannotate_schemas.py +++ b/darwin/importer/formats/superannotate_schemas.py @@ -208,18 +208,40 @@ superannotate_export = { "type": "object", + "required": ["instances", "metadata", "tags"], "properties": { "instances": {"type": "array", "items": {"oneOf": [point, ellipse, cuboid, polygon, bbox, polyline]},}, "metadata": {"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}}, + "tags": {"type": "array", "items": {"type": "string"}}, + }, +} + +attribute_groups = { + "type": "array", + "items": { + "type": "object", + "required": ["id", "name", "attributes"], + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "attributes": { + "type": "array", + "itmes": { + "type": "object", + "required": ["id", "name"], + "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, + }, + }, + }, }, - "required": ["instances", "metadata"], } classes_export = { "type": "array", "items": { "type": "object", - "required": ["name", "id"], - "properties": {"name": {"type": "string"}, "id": {"type": "integer"}}, + "required": ["name", "id", "attribute_groups"], + "properties": {"name": {"type": "string"}, "id": {"type": "integer"}, "attribute_groups": attribute_groups}, }, } + diff --git a/tests/darwin/importer/formats/import_labelbox_test.py b/tests/darwin/importer/formats/import_labelbox_test.py index 11aa3fa69..89bbf2b20 100644 --- a/tests/darwin/importer/formats/import_labelbox_test.py +++ b/tests/darwin/importer/formats/import_labelbox_test.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Callable, List, Optional, cast +from typing import List, Optional, cast import pytest from darwin.datatypes import ( diff --git a/tests/darwin/importer/formats/import_superannotate_test.py b/tests/darwin/importer/formats/import_superannotate_test.py index 2413689d0..e4cda7f3a 100644 --- a/tests/darwin/importer/formats/import_superannotate_test.py +++ b/tests/darwin/importer/formats/import_superannotate_test.py @@ -56,6 +56,7 @@ def it_returns_empty_file_if_there_are_no_annotations(annotations_file_path: Pat annotations_json: str = """ { "instances": [], + "tags": [], "metadata": { "name": "demo-image-0.jpg" } @@ -176,6 +177,7 @@ def it_raises_if_point_has_missing_coordinate(annotations_file_path: Path, class "y": 0 } ], + "tags": [], "metadata": { "name": "demo-image-0.jpg" } @@ -197,12 +199,13 @@ def it_imports_point_vectors(annotations_file_path: Path, classes_file_path: Pat "instances": [ {"type": "point", "x": 1.93, "y": 0.233, "classId": 1, "attributes": []} ], + "tags": [], "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -228,17 +231,10 @@ def it_raises_if_ellipse_has_missing_coordinate(annotations_file_path: Path, cla annotations_json: str = """ { "instances": [ - { - "type": "ellipse", - "cy": 0, - "cx": 0, - "rx": 0, - "angle": 0 - } + {"type": "ellipse", "cy": 0, "cx": 0, "rx": 0, "angle": 0} ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """[]""" @@ -266,14 +262,13 @@ def it_imports_ellipse_vectors(annotations_file_path: Path, classes_file_path: P "attributes": [] } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -305,24 +300,14 @@ def it_raises_if_cuboid_has_missing_point(annotations_file_path: Path, classes_f "type": "cuboid", "classId": 1, "points": { - "f2": { - "x": 3023.31, - "y": 2302.75 - }, - "r1": { - "x": 1826.19, - "y": 1841.44 - }, - "r2": { - "x": 2928, - "y": 2222.69 - } + "f2": {"x": 3023.31, "y": 2302.75}, + "r1": {"x": 1826.19, "y": 1841.44}, + "r2": {"x": 2928, "y": 2222.69} } } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """[]""" @@ -344,33 +329,20 @@ def it_imports_cuboid_vectors(annotations_file_path: Path, classes_file_path: Pa "type": "cuboid", "classId": 1, "points": { - "f1": { - "x": 1742.31, - "y": 1727.06 - }, - "f2": { - "x": 3023.31, - "y": 2302.75 - }, - "r1": { - "x": 1826.19, - "y": 1841.44 - }, - "r2": { - "x": 2928, - "y": 2222.69 - } + "f1": {"x": 1742.31, "y": 1727.06}, + "f2": {"x": 3023.31, "y": 2302.75}, + "r1": {"x": 1826.19, "y": 1841.44}, + "r2": {"x": 2928, "y": 2222.69} } } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -407,6 +379,7 @@ def it_raises_if_polygon_has_missing_points(annotations_file_path: Path, classes "classId": 1 } ], + "tags": [], "metadata": { "name": "demo-image-0.jpg" } @@ -430,24 +403,16 @@ def it_imports_polygon_vectors(annotations_file_path: Path, classes_file_path: P "attributes": [], "type": "polygon", "classId": 1, - "points": [ - 1053, - 587.2, - 1053.1, - 586, - 1053.8, - 585.4 - ] + "points": [1053, 587.2, 1053.1, 586, 1053.8, 585.4] } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -480,6 +445,7 @@ def it_raises_if_polyline_has_missing_points(annotations_file_path: Path, classe "classId": 1 } ], + "tags": [], "metadata": { "name": "demo-image-0.jpg" } @@ -503,24 +469,16 @@ def it_imports_polyline_vectors(annotations_file_path: Path, classes_file_path: "attributes": [], "type": "polyline", "classId": 1, - "points": [ - 1053, - 587.2, - 1053.1, - 586, - 1053.8, - 585.4 - ] + "points": [1053, 587.2, 1053.1, 586, 1053.8, 585.4] } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -551,16 +509,11 @@ def it_raises_if_bbox_has_missing_points(annotations_file_path: Path, classes_fi { "type": "bbox", "classId": 1, - "points": { - "x2": 1920, - "y1": 516.5, - "y2": 734 - } + "points": {"x2": 1920, "y1": 516.5, "y2": 734} } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """[]""" @@ -584,14 +537,13 @@ def it_imports_bbox_vectors(annotations_file_path: Path, classes_file_path: Path "attributes": [] } ], - "metadata": { - "name": "demo-image-0.jpg" - } + "tags": [], + "metadata": {"name": "demo-image-0.jpg"} } """ classes_json: str = """ [ - {"name": "Person", "id": 1} + {"name": "Person", "id": 1, "attribute_groups": []} ] """ @@ -624,6 +576,7 @@ def it_raises_if_an_attributes_is_missing(annotations_file_path: Path, classes_f "points": {"x1": 1642.9, "x2": 1920, "y1": 516.5, "y2": 734} } ], + "tags": [], "metadata": {"name": "demo-image-0.jpg"} } """ @@ -665,6 +618,7 @@ def it_raises_if_an_attribute_from_a_group_is_missing(annotations_file_path: Pat "attributes": [{"id": 2, "groupId": 1}] } ], + "tags": [], "metadata": {"name": "demo-image-0.jpg"} } """ @@ -706,6 +660,7 @@ def it_imports_attributes(annotations_file_path: Path, classes_file_path: Path): "attributes": [{"id": 2, "groupId": 1}, {"id": 3, "groupId": 2}] } ], + "tags": [], "metadata": {"name": "demo-image-0.jpg"} } """ @@ -757,6 +712,79 @@ def it_imports_attributes(annotations_file_path: Path, classes_file_path: Path): assert_subannotations(bbox_annotation.subs, [SubAnnotation("attributes", ["Sex:Female", "Emotion:Smiling"])]) + def it_raises_if_tags_is_missing(annotations_file_path: Path, classes_file_path: Path): + + annotations_json: str = """ + { + "instances": [ + { + "type": "bbox", + "classId": 1, + "points": {"x1": 1642.9, "x2": 1920, "y1": 516.5, "y2": 734}, + "attributes": [] + } + ], + "metadata": {"name": "demo-image-0.jpg"} + } + """ + classes_json: str = """ + [ + {"attribute_groups": [], "id": 1, "name": "Person"} + ] + """ + + annotations_file_path.write_text(annotations_json) + classes_file_path.write_text(classes_json) + + with pytest.raises(ValidationError) as error: + parse_path(annotations_file_path) + + assert "'tags' is a required property" in str(error.value) + + def it_imports_tags(annotations_file_path: Path, classes_file_path: Path): + + annotations_json: str = """ + { + "instances": [ + { + "type": "bbox", + "classId": 1, + "points": {"x1": 1642.9, "x2": 1920, "y1": 516.5, "y2": 734}, + "attributes": [] + } + ], + "tags": ["street", "night"], + "metadata": {"name": "demo-image-0.jpg"} + } + """ + classes_json: str = """ + [ + { + "attribute_groups": [], + "id": 1, + "name": "Person" + } + ] + """ + + annotations_file_path.write_text(annotations_json) + classes_file_path.write_text(classes_json) + + annotation_file: Optional[AnnotationFile] = parse_path(annotations_file_path) + assert annotation_file is not None + assert annotation_file.path == annotations_file_path + assert annotation_file.filename == "demo-image-0.jpg" + assert annotation_file.annotation_classes + assert annotation_file.remote_path == "/" + + assert annotation_file.annotations + + street_tag: Annotation = cast(Annotation, annotation_file.annotations[1]) + assert_annotation_class(street_tag.annotation_class, "street", "tag") + + night_tag: Annotation = cast(Annotation, annotation_file.annotations[2]) + assert_annotation_class(night_tag.annotation_class, "night", "tag") + def assert_cuboid(annotation: Annotation, cuboid: CuboidData) -> None: cuboid_back: Dict[str, float] = cast(Dict[str, float], cuboid.get("back")) From 0cb2a88a382fb13ad96728331e602b1deca65ee2 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 30 Dec 2021 13:51:15 +0100 Subject: [PATCH 2/2] documentation improvements --- darwin/importer/formats/superannotate.py | 5 ++++- darwin/importer/formats/superannotate_schemas.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/darwin/importer/formats/superannotate.py b/darwin/importer/formats/superannotate.py index 8356ca1b8..cef3533f9 100644 --- a/darwin/importer/formats/superannotate.py +++ b/darwin/importer/formats/superannotate.py @@ -42,13 +42,15 @@ def parse_path(path: Path) -> Optional[AnnotationFile]: { "instances": [ { + "classId": 1, + "attributes": [], "type": "point", "x": 1, "y": 0 }, // { ... } ], - "tags": ["orange"], + "tags": ["a_tag_here"], "metadata": { "name": "a_file_name.json" } @@ -62,6 +64,7 @@ def parse_path(path: Path) -> Optional[AnnotationFile]: - bbox ``Vector`` (not rotated): https://doc.superannotate.com/docs/vector-json#bounding-box-and-rotated-bounding-box - polygon and polyline ``Vector``s: https://doc.superannotate.com/docs/vector-json#polyline-and-polygon + We also support attributes and tags. Each file must also have in the same folder a ``classes.json`` file with information about the classes. This file must have a structure simillar to: diff --git a/darwin/importer/formats/superannotate_schemas.py b/darwin/importer/formats/superannotate_schemas.py index 050de3c97..43ab39df7 100644 --- a/darwin/importer/formats/superannotate_schemas.py +++ b/darwin/importer/formats/superannotate_schemas.py @@ -1,3 +1,7 @@ +################################## +# import_file.json # +################################## + attributes = { "type": "array", "items": { @@ -216,6 +220,10 @@ }, } +################################## +# classes.json # +################################## + attribute_groups = { "type": "array", "items": {