diff --git a/changelog.d/20240403_135916_yeek020407_switch_outside.md b/changelog.d/20240403_135916_yeek020407_switch_outside.md new file mode 100644 index 000000000000..3e6b98a57421 --- /dev/null +++ b/changelog.d/20240403_135916_yeek020407_switch_outside.md @@ -0,0 +1,4 @@ +### Fixed + +- Formats with the custom `track_id` attribute should import `outside` track shapes properly (e.g. `COCO`, `COCO Keypoints`, `Datumaro`, `PASCAL VOC`) + () diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 2790f7947d25..083c0334f59a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -15,6 +15,7 @@ from typing import (Any, Callable, DefaultDict, Dict, Iterable, List, Literal, Mapping, NamedTuple, Optional, OrderedDict, Sequence, Set, Tuple, Union) +from attrs.converters import to_bool import datumaro as dm import defusedxml.ElementTree as ET import numpy as np @@ -1953,7 +1954,8 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa 'sly_pointcloud', 'coco', 'coco_instances', - 'coco_person_keypoints' + 'coco_person_keypoints', + 'voc' ] label_cat = dm_dataset.categories()[dm.AnnotationType.label] @@ -2031,9 +2033,9 @@ def reduce_fn(acc, v): # because in some formats return type can be different # from bool / None # https://github.com/openvinotoolkit/datumaro/issues/719 - occluded = dm.util.cast(ann.attributes.pop('occluded', None), bool) is True - keyframe = dm.util.cast(ann.attributes.get('keyframe', None), bool) is True - outside = dm.util.cast(ann.attributes.pop('outside', None), bool) is True + occluded = dm.util.cast(ann.attributes.pop('occluded', None), to_bool) is True + keyframe = dm.util.cast(ann.attributes.get('keyframe', None), to_bool) is True + outside = dm.util.cast(ann.attributes.pop('outside', None), to_bool) is True track_id = ann.attributes.pop('track_id', None) source = ann.attributes.pop('source').lower() \ @@ -2080,7 +2082,7 @@ def reduce_fn(acc, v): )) continue - if keyframe or outside: + if dm_dataset.format in track_formats: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, @@ -2107,11 +2109,8 @@ def reduce_fn(acc, v): if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: - element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool, True) element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent - if not element_keyframe and not element_outside: - continue if element.label not in tracks[track_id]['elements']: tracks[track_id]['elements'][element.label] = instance_data.Track( @@ -2120,6 +2119,7 @@ def reduce_fn(acc, v): source=source, shapes=[], ) + element_attributes = [ instance_data.Attribute(name=n, value=str(v)) for n, v in element.attributes.items() @@ -2151,10 +2151,54 @@ def reduce_fn(acc, v): raise CvatImportError("Image {}: can't import annotation " "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e - for track in tracks.values(): - track['elements'] = list(track['elements'].values()) - instance_data.add_track(instance_data.Track(**track)) - + def _validate_track_shapes(shapes): + shapes = sorted(shapes, key=lambda t: t.frame) + new_shapes = [] + prev_shape = None + # infer the keyframe shapes and keep only them + for shape in shapes: + prev_is_visible = prev_shape and not prev_shape.outside + cur_is_visible = shape and not shape.outside + + has_gap = False + if prev_is_visible: + has_gap = prev_shape.frame + instance_data.frame_step < shape.frame + + if has_gap: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_shapes.append(prev_shape) + + if prev_is_visible != cur_is_visible or cur_is_visible and (has_gap or shape.keyframe): + shape = shape._replace(keyframe=True) + new_shapes.append(shape) + + prev_shape = shape + + if prev_shape and not prev_shape.outside and ( + prev_shape.frame + instance_data.frame_step <= stop_frame + # has a gap before the current instance segment end + ): + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_shapes.append(prev_shape) + + return new_shapes + + stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) + for track_id, track in tracks.items(): + track['shapes'] = _validate_track_shapes(track['shapes']) + + if ann.type == dm.AnnotationType.skeleton: + new_elements = {} + for element_id, element in track['elements'].items(): + new_element_shapes = _validate_track_shapes(element.shapes) + new_elements[element_id] = element._replace(shapes=new_element_shapes) + track['elements'] = new_elements + + if track['shapes'] or track['elements']: + track['elements'] = list(track['elements'].values()) + instance_data.add_track(instance_data.Track(**track)) def import_labels_to_project(project_annotation, dataset: dm.Dataset): labels = [] diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index f65415eb447d..bbbcc44b7d01 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -39,6 +39,7 @@ make_api_client, patch_method, post_method, + put_method, ) from shared.utils.helpers import ( generate_image_file, @@ -2819,42 +2820,880 @@ def test_import_annotations(self, task_kind, annotation_kind, expect_success): assert b"Could not match item id" in capture.value.body - def test_can_export_and_import_skeleton_tracks_in_coco_format(self): - task = self.client.tasks.retrieve(14) - dataset_file = self.tmp_dir / "some_file.zip" - format_name = "COCO Keypoints 1.0" + def delete_annotation_and_import_annotations( + self, task_id, annotations, format_name, dataset_file + ): + task = self.client.tasks.retrieve(task_id) + labels = task.get_labels() + sublabels = labels[0].sublabels - original_annotations = task.get_annotations() + # if the annotations shapes label_id does not exist, the put it in the task + for shape in annotations["shapes"]: + if "label_id" not in shape: + shape["label_id"] = labels[0].id + + for track in annotations["tracks"]: + if "label_id" not in track: + track["label_id"] = labels[0].id + for element_idx, element in enumerate(track["elements"]): + if "label_id" not in element: + element["label_id"] = sublabels[element_idx].id + + response = put_method( + "admin1", f"tasks/{task_id}/annotations", annotations, action="create" + ) + assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" task.export_dataset(format_name, dataset_file, include_images=False) - task.remove_annotations() + + # get the original annotations + response = get_method("admin1", f"tasks/{task.id}/annotations") + assert response.status_code == 200, f"Cannot get task's annotations: {response.content}" + original_annotations = response.json() + + # import the annotations task.import_annotations(format_name, dataset_file) - imported_annotations = task.get_annotations() + response = get_method("admin1", f"tasks/{task.id}/annotations") + assert response.status_code == 200, f"Cannot get task's annotations: {response.content}" + imported_annotations = response.json() + + return original_annotations, imported_annotations + + def compare_original_and_import_annotations(self, original_annotations, imported_annotations): + assert ( + DeepDiff( + original_annotations, + imported_annotations, + ignore_order=True, + exclude_regex_paths=[ + r"root(\['\w+'\]\[\d+\])+\['id'\]", + r"root(\['\w+'\]\[\d+\])+\['label_id'\]", + r"root(\['\w+'\]\[\d+\])+\['attributes'\]\[\d+\]\['spec_id'\]", + ], + ) + == {} + ) - # Number of shapes and tracks hasn't changed - assert len(original_annotations.shapes) == len(imported_annotations.shapes) - assert len(original_annotations.tracks) == len(imported_annotations.tracks) + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_outside_true(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "outside_true_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 0, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + { + "type": "rectangle", + "frame": 3, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + "outside": True, + }, + ], + "elements": [], + } + ], + } - # Frames of shapes, tracks and track elements hasn't changed - assert set([s.frame for s in original_annotations.shapes]) == set( - [s.frame for s in imported_annotations.shapes] + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file ) - assert set([t.frame for t in original_annotations.tracks]) == set( - [t.frame for t in imported_annotations.tracks] + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check if frame 3 is imported correctly with outside = True + assert imported_annotations["tracks"][0]["shapes"][1]["outside"] + + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_intermediate_keyframe(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "intermediate_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 0, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + { + "type": "rectangle", + "frame": 3, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + ], + "elements": [], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file ) - assert set( - [ - tes.frame - for t in original_annotations.tracks - for te in t.elements - for tes in te.shapes - ] - ) == set( - [ - tes.frame - for t in imported_annotations.tracks - for te in t.elements - for tes in te.shapes - ] + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 2 + + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_outside_without_keyframe(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "outside_without_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 0, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + { + "type": "rectangle", + "frame": 3, + "points": [1.0, 2.0, 3.0, 2.0], + "outside": True, + }, + ], + "elements": [], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 2 + + # check that frame 3 is imported correctly with outside = True + assert imported_annotations["tracks"][0]["shapes"][1]["outside"] + + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_no_keyframe(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "no_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 0, + "points": [1.0, 2.0, 3.0, 2.0], + }, + ], + "elements": [], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check if first frame is imported correctly with keyframe = True + assert len(imported_annotations["tracks"][0]["shapes"]) == 1 + + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_one_outside(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "one_outside_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 3, + "points": [1.0, 2.0, 3.0, 2.0], + "outside": True, + }, + ], + "elements": [], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # only outside=True shape is imported, means there is no visible shape + assert len(imported_annotations["tracks"]) == 0 + + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_export_and_import_tracked_format_with_gap(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "with_gap_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "rectangle", + "frame": 0, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + { + "type": "rectangle", + "frame": 2, + "points": [1.0, 2.0, 3.0, 2.0], + "outside": True, + }, + { + "type": "rectangle", + "frame": 4, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + { + "type": "rectangle", + "frame": 5, + "points": [1.0, 2.0, 3.0, 2.0], + "outside": True, + }, + { + "type": "rectangle", + "frame": 6, + "points": [1.0, 2.0, 3.0, 2.0], + "keyframe": True, + }, + ], + "elements": [], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 5 + + outside_count = sum( + 1 for shape in imported_annotations["tracks"][0]["shapes"] if shape["outside"] + ) + assert outside_count == 2, "Outside shapes are not imported correctly" + + def test_export_and_import_coco_keypoints_with_outside_true(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "outside_true_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + { + "type": "skeleton", + "frame": 3, + "points": [], + "keyframe": True, + "outside": True, + }, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "keyframe": True, + "outside": True, + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check if frame 3 is imported correctly with outside = True + assert imported_annotations["tracks"][0]["shapes"][1]["outside"] + + def test_export_and_import_coco_keypoints_with_intermediate_keyframe(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "intermediate_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + { + "type": "skeleton", + "frame": 3, + "points": [], + "keyframe": True, + }, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "keyframe": True, + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 2 + + def test_export_and_import_coco_keypoints_with_outside_without_keyframe(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "outside_without_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + { + "type": "skeleton", + "frame": 3, + "points": [], + "outside": True, + }, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "outside": True, + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 2 + + # check that frame 3 is imported correctly with outside = True + assert imported_annotations["tracks"][0]["shapes"][1]["outside"] + + def test_export_and_import_coco_keypoints_with_no_keyframe(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "with_no_keyframe_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": []}, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check if first frame is imported correctly with keyframe = True + assert len(imported_annotations["tracks"][0]["shapes"]) == 1 + + def test_export_and_import_coco_keypoints_with_one_outside(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "with_one_outside_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 3, "points": [], "outside": True}, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "outside": True, + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # only outside=True shape is imported, means there is no visible shape + assert len(imported_annotations["tracks"]) == 0 + + def test_export_and_import_coco_keypoints_with_gap(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "with_gap_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + {"type": "skeleton", "frame": 2, "points": [], "outside": True}, + {"type": "skeleton", "frame": 4, "points": [], "keyframe": True}, + {"type": "skeleton", "frame": 5, "points": [], "outside": True}, + {"type": "skeleton", "frame": 6, "points": [], "keyframe": True}, + ], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 2, + "points": [1.0, 2.0], + "outside": True, + }, + { + "type": "points", + "frame": 4, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 5, + "points": [1.0, 2.0], + "outside": True, + }, + { + "type": "points", + "frame": 6, + "points": [1.0, 2.0], + "keyframe": True, + }, + ], + }, + ], + } + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + # check if all the keyframes are imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 5 + + outside_count = sum( + 1 for shape in imported_annotations["tracks"][0]["shapes"] if shape["outside"] + ) + assert outside_count == 2, "Outside shapes are not imported correctly" + + def test_export_and_import_complex_coco_keypoints_annotations(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "complex_annotations_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "outside": False, "frame": 0}, + {"type": "skeleton", "outside": False, "frame": 1}, + {"type": "skeleton", "outside": False, "frame": 2}, + {"type": "skeleton", "outside": False, "frame": 4}, + {"type": "skeleton", "outside": False, "frame": 5}, + ], + "attributes": [], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [256.67, 719.25], + "frame": 0, + }, + { + "type": "points", + "outside": False, + "points": [256.67, 719.25], + "frame": 1, + }, + { + "type": "points", + "outside": True, + "points": [256.67, 719.25], + "frame": 2, + }, + { + "type": "points", + "outside": False, + "points": [256.67, 719.25], + "frame": 4, + }, + { + "type": "points", + "outside": False, + "points": [256.67, 719.25], + "frame": 5, + }, + ], + }, + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [318.25, 842.06], + "frame": 0, + }, + { + "type": "points", + "outside": True, + "points": [318.25, 842.06], + "frame": 1, + }, + { + "type": "points", + "outside": False, + "points": [318.25, 842.06], + "frame": 2, + }, + { + "type": "points", + "outside": True, + "points": [318.25, 842.06], + "frame": 4, + }, + ], + }, + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [199.2, 798.71], + "frame": 0, + }, + { + "type": "points", + "outside": False, + "points": [199.2, 798.71], + "frame": 1, + }, + { + "type": "points", + "outside": True, + "points": [199.2, 798.71], + "frame": 2, + }, + { + "type": "points", + "outside": False, + "points": [199.2, 798.71], + "frame": 4, + }, + { + "type": "points", + "outside": True, + "points": [199.2, 798.71], + "frame": 5, + }, + ], + }, + ], + }, + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "outside": False, "frame": 0}, + {"type": "skeleton", "outside": True, "frame": 1}, + {"type": "skeleton", "outside": False, "frame": 3}, + {"type": "skeleton", "outside": False, "frame": 4}, + {"type": "skeleton", "outside": False, "frame": 5}, + ], + "attributes": [], + "elements": [ + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [416.16, 244.31], + "frame": 0, + }, + { + "type": "points", + "outside": True, + "points": [416.16, 244.31], + "frame": 1, + }, + { + "type": "points", + "outside": False, + "points": [416.16, 244.31], + "frame": 3, + }, + { + "type": "points", + "outside": False, + "points": [416.16, 244.31], + "frame": 4, + }, + { + "type": "points", + "outside": False, + "points": [416.16, 244.31], + "frame": 5, + }, + ], + }, + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [486.17, 379.65], + "frame": 0, + }, + { + "type": "points", + "outside": True, + "points": [486.17, 379.65], + "frame": 1, + }, + { + "type": "points", + "outside": True, + "points": [486.17, 379.65], + "frame": 3, + }, + { + "type": "points", + "outside": False, + "points": [486.17, 379.65], + "frame": 4, + }, + { + "type": "points", + "outside": False, + "points": [486.17, 379.65], + "frame": 5, + }, + ], + }, + { + "frame": 0, + "group": 0, + "shapes": [ + { + "type": "points", + "outside": False, + "points": [350.83, 331.88], + "frame": 0, + }, + { + "type": "points", + "outside": True, + "points": [350.83, 331.88], + "frame": 1, + }, + { + "type": "points", + "outside": True, + "points": [350.83, 331.88], + "frame": 3, + }, + { + "type": "points", + "outside": False, + "points": [350.83, 331.88], + "frame": 5, + }, + ], + }, + ], + }, + ], + } + + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + def check_element_outside_count(track_idx, element_idx, expected_count): + outside_count = sum( + 1 + for shape in imported_annotations["tracks"][0]["elements"][element_idx]["shapes"] + if shape["outside"] + ) + assert ( + outside_count == expected_count + ), f"Outside shapes for track[{track_idx}]element[{element_idx}] are not imported correctly" + + # check track[0] elements outside count + check_element_outside_count(0, 0, 1) + check_element_outside_count(0, 1, 2) + check_element_outside_count(0, 2, 2) + + # check track[1] elements outside count + check_element_outside_count(1, 0, 1) + check_element_outside_count(1, 1, 2) + check_element_outside_count(1, 2, 2)