From bfce63b487411e587597637ce6382df99059d8ec Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Sat, 23 Mar 2024 22:52:47 +0800 Subject: [PATCH 01/44] disable reading of track id --- cvat/apps/dataset_manager/bindings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 2790f7947d25..757fdc5e51ff 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1757,9 +1757,9 @@ def _convert_shape(self, if shape.type == ShapeType.RECTANGLE: dm_attr['rotation'] = shape.rotation - if hasattr(shape, 'track_id'): - dm_attr['track_id'] = shape.track_id - dm_attr['keyframe'] = shape.keyframe + # if hasattr(shape, 'track_id'): + # dm_attr['track_id'] = shape.track_id + # dm_attr['keyframe'] = shape.keyframe dm_points = shape.points From 099fe0c85121371fb06beb6a822fa0479dd8f6ae Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Wed, 3 Apr 2024 14:02:07 +0800 Subject: [PATCH 02/44] Fix switch outside --- ...240403_135916_yeek020407_switch_outside.md | 4 ++ cvat/apps/dataset_manager/bindings.py | 28 +++++++++-- .../tests/assets/annotations.json | 46 +++++++++++++++++++ .../tests/test_rest_api_formats.py | 41 +++++++++++++++++ 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 changelog.d/20240403_135916_yeek020407_switch_outside.md 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..b0781623af57 --- /dev/null +++ b/changelog.d/20240403_135916_yeek020407_switch_outside.md @@ -0,0 +1,4 @@ +### Fixed + +- Check the end of outside attribute for tracked format + () \ No newline at end of file diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 757fdc5e51ff..aa4b9821e6ea 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1757,9 +1757,9 @@ def _convert_shape(self, if shape.type == ShapeType.RECTANGLE: dm_attr['rotation'] = shape.rotation - # if hasattr(shape, 'track_id'): - # dm_attr['track_id'] = shape.track_id - # dm_attr['keyframe'] = shape.keyframe + if hasattr(shape, 'track_id'): + dm_attr['track_id'] = shape.track_id + dm_attr['keyframe'] = shape.keyframe dm_points = shape.points @@ -2080,7 +2080,7 @@ def reduce_fn(acc, v): )) continue - if keyframe or outside: + if track_id is not None: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, @@ -2151,6 +2151,26 @@ 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['shapes'].sort(key=lambda t: t.frame) + prev_shape_idx = 0 + prev_shape = track['shapes'][0] + for shape in track['shapes'][1:]: + has_skip = instance_data.frame_step < shape.frame - prev_shape.frame + if has_skip and not prev_shape.outside: + prev_shape = prev_shape._replace(outside=True, + frame=prev_shape.frame + instance_data.frame_step) + prev_shape_idx += 1 + track['shapes'].insert(prev_shape_idx, prev_shape) + prev_shape = shape + prev_shape_idx += 1 + + # if the last shape 'outside' is False, we need to add to stop the tracking + if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= frame_number: + prev_shape = prev_shape._replace(outside=True, + frame=prev_shape.frame + instance_data.frame_step) + track['shapes'].append(prev_shape) + for track in tracks.values(): track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) diff --git a/cvat/apps/dataset_manager/tests/assets/annotations.json b/cvat/apps/dataset_manager/tests/assets/annotations.json index 6035e40fbd30..bf0fd739a05f 100644 --- a/cvat/apps/dataset_manager/tests/assets/annotations.json +++ b/cvat/apps/dataset_manager/tests/assets/annotations.json @@ -807,6 +807,52 @@ ], "tracks": [] }, + "Datumaro 1.0 outside true": { + "version": 0, + "tags": [ + { + "frame": 0, + "label_id": null, + "group": 0, + "source": "manual", + "attributes": [] + } + ], + "shapes": [], + "tracks": [ + { + "frame": 0, + "label_id": null, + "group": 0, + "source": "manual", + "shapes": [ + { + "type": "rectangle", + "occluded": false, + "z_order": 0, + "points": [5.54, 3.5, 19.64, 11.19], + "frame": 0, + "keyframe": true, + "outside": false, + "rotation":0, + "attributes": [] + }, + { + "type": "rectangle", + "occluded": false, + "z_order": 0, + "points": [5.54, 3.5, 19.64, 11.19], + "frame": 1, + "keyframe": false, + "outside": true, + "rotation":0, + "attributes": [] + } + ], + "attributes": [] + } + ] + }, "CVAT for images 1.1 many jobs": { "version": 0, "tags": [ diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py index 7c30344d2bab..981b01de45de 100644 --- a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py +++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py @@ -1045,6 +1045,47 @@ def test_api_v2_tasks_annotations_dump_and_upload_many_jobs_with_datumaro(self): data_from_task_after_upload = self._get_data_from_task(task_id, include_images) compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload) + def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro_outside_true(self): + test_name = self._testMethodName + import_format = "Datumaro 1.0" + dump_format_name = "Datumaro 1.0 outside true" + include_images_params = (False, True) + + for include_images in include_images_params: + with self.subTest(import_format): + + # use Datumaro 1.0 annotations that contains outside property with True value + images = self._generate_task_images(3) + task = self._create_task(tasks["main"], images) + self._create_annotations(task, dump_format_name, "default") + task_id = task["id"] + data_from_task_before_upload = self._get_data_from_task(task_id, include_images_params[0]) + + with TestDir() as test_dir: + # download annotations using track_formats + url = self._generate_url_dump_tasks_annotations(task_id) + file_zip_name = osp.join(test_dir, f'{test_name}_{import_format}.zip') + data = { + "format": import_format, + "action": "download", + } + self._download_file(url, data, self.admin, file_zip_name) + self._check_downloaded_file(file_zip_name) + + # remove annotations + self._remove_annotations(url, self.admin) + + # upload annotations + upload_format_name = import_format + url = self._generate_url_upload_tasks_annotations(task_id, upload_format_name) + with open(file_zip_name, 'rb') as binary_file: + self._upload_file(url, binary_file, self.admin) + + # equals annotations + data_from_task_after_upload = self._get_data_from_task(task_id, include_images) + compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload) + + def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro(self): test_name = self._testMethodName # get formats From 341fd80ae565efece76574d3dc9f9748d845c873 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:41:42 +0800 Subject: [PATCH 03/44] Update changelog.d/20240403_135916_yeek020407_switch_outside.md Co-authored-by: Maxim Zhiltsov --- changelog.d/20240403_135916_yeek020407_switch_outside.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20240403_135916_yeek020407_switch_outside.md b/changelog.d/20240403_135916_yeek020407_switch_outside.md index b0781623af57..3bc2eeae28e6 100644 --- a/changelog.d/20240403_135916_yeek020407_switch_outside.md +++ b/changelog.d/20240403_135916_yeek020407_switch_outside.md @@ -1,4 +1,4 @@ ### Fixed -- Check the end of outside attribute for tracked format +- Formats with the custom `track_id` attribute should import `outside`track shapes properly (e.g. `COCO`, `Datumaro`, `PASCAL VOC`) () \ No newline at end of file From 11655a7884a72bb0708b2da7f3c9d004b38178ca Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Sun, 7 Apr 2024 16:18:11 +0800 Subject: [PATCH 04/44] add test import of outside track shapes --- cvat/apps/dataset_manager/bindings.py | 8 ++- tests/python/rest_api/test_tasks.py | 96 +++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index aa4b9821e6ea..f12c1ca28066 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1953,7 +1953,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] @@ -2080,7 +2081,7 @@ def reduce_fn(acc, v): )) continue - if track_id is not None: + if keyframe or outside: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, @@ -2169,7 +2170,8 @@ def reduce_fn(acc, v): if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= frame_number: prev_shape = prev_shape._replace(outside=True, frame=prev_shape.frame + instance_data.frame_step) - track['shapes'].append(prev_shape) + prev_shape_idx += 1 + track['shapes'].insert(prev_shape_idx, prev_shape) for track in tracks.values(): track['elements'] = list(track['elements'].values()) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index f65415eb447d..d8e40992c65e 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2858,3 +2858,99 @@ def test_can_export_and_import_skeleton_tracks_in_coco_format(self): for tes in te.shapes ] ) + + @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): + imageFileNames = [ + "1.jpg", + "2.jpg", + "3.jpg", + "4.jpg", + "5.jpg", + ] + images = generate_image_files(len(imageFileNames), filenames=imageFileNames) + + source_archive_path = self.tmp_dir / "source_data.zip" + with zipfile.ZipFile(source_archive_path, "w") as zip_file: + for image in images: + zip_file.writestr(image.name, image.getvalue()) + + task = self.client.tasks.create_from_data( + { + "name": "test_tracked_format_with_outside_true_{format_name}", + "labels": [{"name": "cat"}], + }, + resources=[source_archive_path], + ) + + labels = task.get_labels() + task.set_annotations( + models.LabeledDataRequest( + shapes=[ + models.LabeledShapeRequest( + frame=0, + label_id=labels[0].id, + type="rectangle", + points=[1, 1, 2, 2], + ) + ], + tracks=[ + models.LabeledTrackRequest( + frame=0, + label_id=labels[0].id, + shapes=[ + models.TrackedShapeRequest( + frame=0, type="rectangle", points=[3, 2, 2, 3] + ), + models.TrackedShapeRequest( + frame=1, type="rectangle", points=[3, 2, 2, 3] + ), + models.TrackedShapeRequest( + frame=2, type="rectangle", points=[3, 2, 2, 3], outside=True + ), + ], + ) + ], + ) + ) + + dataset_file = self.tmp_dir / (format_name + "some_file.zip") + task.export_dataset(format_name, dataset_file, include_images=False) + + original_annotations = task.get_annotations() + task.remove_annotations() + task.import_annotations(format_name, dataset_file) + + imported_annotations = task.get_annotations() + + # 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) + + for i in range(len(original_annotations.tracks)): + assert len(original_annotations.tracks[i].shapes) == len( + imported_annotations.tracks[i].shapes + ) + + # 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] + ) + assert set([t.frame for t in original_annotations.tracks]) == set( + [t.frame for t in imported_annotations.tracks] + ) + 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 + ] + ) From 946bca37c7aa7ec39814fe51292b02c86fadc196 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Sun, 7 Apr 2024 22:12:25 +0800 Subject: [PATCH 05/44] fix skeleton format bug --- cvat/apps/dataset_manager/bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index f12c1ca28066..dbc2d1dc075b 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2081,7 +2081,7 @@ def reduce_fn(acc, v): )) continue - if keyframe or outside: + if keyframe or not outside: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, @@ -2106,7 +2106,7 @@ def reduce_fn(acc, v): tracks[track_id]['shapes'].append(track) - if ann.type == dm.AnnotationType.skeleton: + if ann.type == dm.AnnotationType.skeleton and (keyframe or outside): 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 From 2ad165f991748336efe5a8bde6a42c248d9f0018 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:57:06 +0800 Subject: [PATCH 06/44] fixed issues by CI --- changelog.d/20240403_135916_yeek020407_switch_outside.md | 2 +- tests/python/rest_api/test_tasks.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/changelog.d/20240403_135916_yeek020407_switch_outside.md b/changelog.d/20240403_135916_yeek020407_switch_outside.md index 3bc2eeae28e6..849171339c0e 100644 --- a/changelog.d/20240403_135916_yeek020407_switch_outside.md +++ b/changelog.d/20240403_135916_yeek020407_switch_outside.md @@ -1,4 +1,4 @@ ### Fixed - Formats with the custom `track_id` attribute should import `outside`track shapes properly (e.g. `COCO`, `Datumaro`, `PASCAL VOC`) - () \ No newline at end of file + () diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index d8e40992c65e..97ac7cc2cd7d 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2927,10 +2927,8 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): assert len(original_annotations.shapes) == len(imported_annotations.shapes) assert len(original_annotations.tracks) == len(imported_annotations.tracks) - for i in range(len(original_annotations.tracks)): - assert len(original_annotations.tracks[i].shapes) == len( - imported_annotations.tracks[i].shapes - ) + for i, original_track in enumerate(original_annotations.tracks): + assert len(original_track.shapes) == len(imported_annotations.tracks[i].shapes) # Frames of shapes, tracks and track elements hasn't changed assert set([s.frame for s in original_annotations.shapes]) == set( From c94b4a6902045401226c9ae7e6d1851f287edf42 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 8 Apr 2024 21:17:50 +0300 Subject: [PATCH 07/44] Update changelog.d/20240403_135916_yeek020407_switch_outside.md --- changelog.d/20240403_135916_yeek020407_switch_outside.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20240403_135916_yeek020407_switch_outside.md b/changelog.d/20240403_135916_yeek020407_switch_outside.md index 849171339c0e..3e6b98a57421 100644 --- a/changelog.d/20240403_135916_yeek020407_switch_outside.md +++ b/changelog.d/20240403_135916_yeek020407_switch_outside.md @@ -1,4 +1,4 @@ ### Fixed -- Formats with the custom `track_id` attribute should import `outside`track shapes properly (e.g. `COCO`, `Datumaro`, `PASCAL VOC`) +- Formats with the custom `track_id` attribute should import `outside` track shapes properly (e.g. `COCO`, `COCO Keypoints`, `Datumaro`, `PASCAL VOC`) () From bda889a21653a8e61f0fd37c16f45e451a63a8e7 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 9 Apr 2024 21:14:33 +0800 Subject: [PATCH 08/44] add outside attribute when it is coco keypoints --- cvat/apps/dataset_manager/bindings.py | 43 +++++++++++++---- .../tests/assets/annotations.json | 46 ------------------- .../tests/test_rest_api_formats.py | 41 ----------------- 3 files changed, 35 insertions(+), 95 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index dbc2d1dc075b..7d4a98c82e81 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2081,7 +2081,7 @@ def reduce_fn(acc, v): )) continue - if keyframe or not outside: + if track_id is not None: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, @@ -2106,7 +2106,7 @@ def reduce_fn(acc, v): tracks[track_id]['shapes'].append(track) - if ann.type == dm.AnnotationType.skeleton and (keyframe or outside): + 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 @@ -2163,21 +2163,48 @@ def reduce_fn(acc, v): frame=prev_shape.frame + instance_data.frame_step) prev_shape_idx += 1 track['shapes'].insert(prev_shape_idx, prev_shape) + if ann.type == dm.AnnotationType.skeleton: + for element in prev_shape.elements: + element = element._replace(outside=True, + frame=element.frame + instance_data.frame_step) + track['elements'][element.label].shapes.append(element) prev_shape = shape prev_shape_idx += 1 # if the last shape 'outside' is False, we need to add to stop the tracking - if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= frame_number: - prev_shape = prev_shape._replace(outside=True, - frame=prev_shape.frame + instance_data.frame_step) - prev_shape_idx += 1 - track['shapes'].insert(prev_shape_idx, prev_shape) + last_shape = track['shapes'][-1] + if last_shape.frame + instance_data.frame_step <= \ + int(instance_data.meta[instance_data.META_FIELD]['stop_frame']): + track['shapes'].append(last_shape._replace(outside=True, + frame=last_shape.frame + instance_data.frame_step) + ) + + if ann.type == dm.AnnotationType.skeleton: + for element in track['elements'].values(): + element.shapes.sort(key=lambda t: t.frame) + prev_element_shape_idx = 0 + prev_element_shape = element.shapes[0] + for shape in element.shapes[1:]: + has_skip = instance_data.frame_step < shape.frame - prev_element_shape.frame + if has_skip and not prev_element_shape.outside: + prev_element_shape = prev_element_shape._replace(outside=True, + frame=prev_element_shape.frame + instance_data.frame_step) + prev_element_shape_idx += 1 + element.shapes.insert(prev_element_shape_idx, prev_element_shape) + prev_element_shape = shape + prev_element_shape_idx += 1 + + last_shape = element.shapes[-1] + if last_shape.frame + instance_data.frame_step <= \ + int(instance_data.meta[instance_data.META_FIELD]['stop_frame']): + element.shapes.append(last_shape._replace(outside=True, + frame=last_shape.frame + instance_data.frame_step) + ) for track in tracks.values(): 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 = [] label_colors = [] diff --git a/cvat/apps/dataset_manager/tests/assets/annotations.json b/cvat/apps/dataset_manager/tests/assets/annotations.json index bf0fd739a05f..6035e40fbd30 100644 --- a/cvat/apps/dataset_manager/tests/assets/annotations.json +++ b/cvat/apps/dataset_manager/tests/assets/annotations.json @@ -807,52 +807,6 @@ ], "tracks": [] }, - "Datumaro 1.0 outside true": { - "version": 0, - "tags": [ - { - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "attributes": [] - } - ], - "shapes": [], - "tracks": [ - { - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "shapes": [ - { - "type": "rectangle", - "occluded": false, - "z_order": 0, - "points": [5.54, 3.5, 19.64, 11.19], - "frame": 0, - "keyframe": true, - "outside": false, - "rotation":0, - "attributes": [] - }, - { - "type": "rectangle", - "occluded": false, - "z_order": 0, - "points": [5.54, 3.5, 19.64, 11.19], - "frame": 1, - "keyframe": false, - "outside": true, - "rotation":0, - "attributes": [] - } - ], - "attributes": [] - } - ] - }, "CVAT for images 1.1 many jobs": { "version": 0, "tags": [ diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py index 981b01de45de..7c30344d2bab 100644 --- a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py +++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py @@ -1045,47 +1045,6 @@ def test_api_v2_tasks_annotations_dump_and_upload_many_jobs_with_datumaro(self): data_from_task_after_upload = self._get_data_from_task(task_id, include_images) compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload) - def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro_outside_true(self): - test_name = self._testMethodName - import_format = "Datumaro 1.0" - dump_format_name = "Datumaro 1.0 outside true" - include_images_params = (False, True) - - for include_images in include_images_params: - with self.subTest(import_format): - - # use Datumaro 1.0 annotations that contains outside property with True value - images = self._generate_task_images(3) - task = self._create_task(tasks["main"], images) - self._create_annotations(task, dump_format_name, "default") - task_id = task["id"] - data_from_task_before_upload = self._get_data_from_task(task_id, include_images_params[0]) - - with TestDir() as test_dir: - # download annotations using track_formats - url = self._generate_url_dump_tasks_annotations(task_id) - file_zip_name = osp.join(test_dir, f'{test_name}_{import_format}.zip') - data = { - "format": import_format, - "action": "download", - } - self._download_file(url, data, self.admin, file_zip_name) - self._check_downloaded_file(file_zip_name) - - # remove annotations - self._remove_annotations(url, self.admin) - - # upload annotations - upload_format_name = import_format - url = self._generate_url_upload_tasks_annotations(task_id, upload_format_name) - with open(file_zip_name, 'rb') as binary_file: - self._upload_file(url, binary_file, self.admin) - - # equals annotations - data_from_task_after_upload = self._get_data_from_task(task_id, include_images) - compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload) - - def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro(self): test_name = self._testMethodName # get formats From d2f2ac1033b24f183e6b43ac85b6fc9d99f01143 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 9 Apr 2024 22:51:26 +0800 Subject: [PATCH 09/44] test for coco keypoints --- tests/python/rest_api/test_tasks.py | 107 ++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 97ac7cc2cd7d..b3315acc2b94 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2952,3 +2952,110 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): for tes in te.shapes ] ) + + def test_export_and_import_coco_keypoints_with_outside_true(self): + format_name = "COCO Keypoints 1.0" + imageFileNames = [ + "1.jpg", + "2.jpg", + "3.jpg", + "4.jpg", + "5.jpg", + ] + images = generate_image_files(len(imageFileNames), filenames=imageFileNames) + + source_archive_path = self.tmp_dir / "source_data.zip" + with zipfile.ZipFile(source_archive_path, "w") as zip_file: + for image in images: + zip_file.writestr(image.name, image.getvalue()) + + task = self.client.tasks.create_from_data( + { + "name": "test_tracked_format_with_outside_true_{format_name}", + "labels": [{"name": "cat", + "sublabels": [{"name": "body"}]}], + }, + resources=[source_archive_path], + ) + + labels = task.get_labels() + sublabels = labels[0].sublabels + + task.set_annotations( + models.LabeledDataRequest( + tracks=[ + models.LabeledTrackRequest( + frame=0, + label_id=labels[0].id, + shapes=[ + models.TrackedShapeRequest( + frame=0, type="skeleton",outside=False + ), + models.TrackedShapeRequest( + frame=1, type="skeleton",outside=False + ), + models.TrackedShapeRequest( + frame=2, type="skeleton",outside=True + ), + ], + elements=[ + models.SubLabeledTrackRequest( + frame=0, + label_id=sublabels[0].id, + shapes=[ + models.TrackedShapeRequest( + frame=0, type="points", points=[1, 1], outside=False + ), + models.TrackedShapeRequest( + frame=1, type="points", points=[1, 1], outside=False + ), + models.TrackedShapeRequest( + frame=2, type="points", points=[1, 1], outside=True + ), + ] + ) + ] + ) + ], + ) + ) + + + dataset_file = self.tmp_dir / (format_name + "some_file.zip") + task.export_dataset(format_name, dataset_file, include_images=False) + + original_annotations = task.get_annotations() + task.remove_annotations() + task.import_annotations(format_name, dataset_file) + + imported_annotations = task.get_annotations() + + # 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) + + for i, original_track in enumerate(original_annotations.tracks): + assert len(original_track.shapes) == len(imported_annotations.tracks[i].shapes) + + # 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] + ) + assert set([t.frame for t in original_annotations.tracks]) == set( + [t.frame for t in imported_annotations.tracks] + ) + 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 + ] + ) \ No newline at end of file From bc727aa78a9a251d8088cfd8debd7b6dfb0e5ce7 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Wed, 10 Apr 2024 19:56:38 +0800 Subject: [PATCH 10/44] added test for coco keypoints with outside true --- cvat/apps/dataset_manager/bindings.py | 2 +- tests/python/rest_api/test_tasks.py | 278 ++++++++++++-------------- 2 files changed, 128 insertions(+), 152 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 7d4a98c82e81..6e01f885d40f 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2081,7 +2081,7 @@ def reduce_fn(acc, v): )) continue - if track_id is not None: + if dm_dataset.format in track_formats: if track_id not in tracks: tracks[track_id] = { 'label': label_cat.items[ann.label].name, diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index b3315acc2b94..33d878fae1bf 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2861,201 +2861,177 @@ def test_can_export_and_import_skeleton_tracks_in_coco_format(self): @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): - imageFileNames = [ - "1.jpg", - "2.jpg", - "3.jpg", - "4.jpg", - "5.jpg", - ] - images = generate_image_files(len(imageFileNames), filenames=imageFileNames) + task_id = 21 + task = self.client.tasks.retrieve(task_id) - source_archive_path = self.tmp_dir / "source_data.zip" - with zipfile.ZipFile(source_archive_path, "w") as zip_file: - for image in images: - zip_file.writestr(image.name, image.getvalue()) + # delete all annotations + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - task = self.client.tasks.create_from_data( - { - "name": "test_tracked_format_with_outside_true_{format_name}", - "labels": [{"name": "cat"}], - }, - resources=[source_archive_path], - ) + annotations = { + "tracks": [ + { + "frame": 0, + "label_id": 58, + "shapes": [ + {"type": "rectangle", "frame": 0, "points": [3.0, 2.0, 2.0, 3.0]}, + {"type": "rectangle", "frame": 1, "points": [3.0, 2.0, 2.0, 3.0]}, + { + "type": "rectangle", + "frame": 2, + "points": [3.0, 2.0, 2.0, 3.0], + "outside": True, + }, + ], + } + ] + } - labels = task.get_labels() - task.set_annotations( - models.LabeledDataRequest( - shapes=[ - models.LabeledShapeRequest( - frame=0, - label_id=labels[0].id, - type="rectangle", - points=[1, 1, 2, 2], - ) - ], - tracks=[ - models.LabeledTrackRequest( - frame=0, - label_id=labels[0].id, - shapes=[ - models.TrackedShapeRequest( - frame=0, type="rectangle", points=[3, 2, 2, 3] - ), - models.TrackedShapeRequest( - frame=1, type="rectangle", points=[3, 2, 2, 3] - ), - models.TrackedShapeRequest( - frame=2, type="rectangle", points=[3, 2, 2, 3], outside=True - ), - ], - ) - ], - ) + # create annotations with outside true + response = patch_method( + "admin1", f"tasks/{task_id}/annotations", annotations, action="create" ) + assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" - dataset_file = self.tmp_dir / (format_name + "some_file.zip") + dataset_file = self.tmp_dir / (format_name + "source_data.zip") task.export_dataset(format_name, dataset_file, include_images=False) - original_annotations = task.get_annotations() - 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() + + # delete all annotations + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" + + # 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() # 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) + assert len(original_annotations["shapes"]) == len(imported_annotations["shapes"]) + assert len(original_annotations["tracks"]) == len(imported_annotations["tracks"]) - for i, original_track in enumerate(original_annotations.tracks): - assert len(original_track.shapes) == len(imported_annotations.tracks[i].shapes) + for i, original_track in enumerate(original_annotations["tracks"]): + assert len(original_track["shapes"]) == len(imported_annotations["tracks"][i]["shapes"]) # 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] + assert set([s["frame"] for s in original_annotations["shapes"]]) == set( + [s["frame"] for s in imported_annotations["shapes"]] ) - assert set([t.frame for t in original_annotations.tracks]) == set( - [t.frame for t in imported_annotations.tracks] + assert set([t["frame"] for t in original_annotations["tracks"]]) == set( + [t["frame"] for t in imported_annotations["tracks"]] ) assert set( [ - tes.frame - for t in original_annotations.tracks - for te in t.elements - for tes in te.shapes + 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 + tes["frame"] + for t in imported_annotations["tracks"] + for te in t["elements"] + for tes in te["shapes"] ] ) - def test_export_and_import_coco_keypoints_with_outside_true(self): + def test_export_and_import_coco_keypoints_with_outside_true(self, jobs): + task_id = 21 format_name = "COCO Keypoints 1.0" - imageFileNames = [ - "1.jpg", - "2.jpg", - "3.jpg", - "4.jpg", - "5.jpg", - ] - images = generate_image_files(len(imageFileNames), filenames=imageFileNames) - - source_archive_path = self.tmp_dir / "source_data.zip" - with zipfile.ZipFile(source_archive_path, "w") as zip_file: - for image in images: - zip_file.writestr(image.name, image.getvalue()) + task = self.client.tasks.retrieve(task_id) - task = self.client.tasks.create_from_data( - { - "name": "test_tracked_format_with_outside_true_{format_name}", - "labels": [{"name": "cat", - "sublabels": [{"name": "body"}]}], - }, - resources=[source_archive_path], - ) + # delete all annotations in task 21 + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - labels = task.get_labels() - sublabels = labels[0].sublabels + annotations = { + "tracks": [ + { + "frame": 0, + "label_id": 58, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": []}, + {"type": "skeleton", "frame": 1, "points": []}, + {"type": "skeleton", "frame": 2, "points": [], "outside": True}, + ], + "elements": [ + { + "label_id": 59, + "frame": 0, + "shapes": [ + {"type": "points", "frame": 0, "points": [1.0, 2.0]}, + {"type": "points", "frame": 1, "points": [2.0, 4.0]}, + { + "type": "points", + "frame": 2, + "points": [3.0, 6.0], + "outside": True, + }, + ], + }, + ], + } + ] + } - task.set_annotations( - models.LabeledDataRequest( - tracks=[ - models.LabeledTrackRequest( - frame=0, - label_id=labels[0].id, - shapes=[ - models.TrackedShapeRequest( - frame=0, type="skeleton",outside=False - ), - models.TrackedShapeRequest( - frame=1, type="skeleton",outside=False - ), - models.TrackedShapeRequest( - frame=2, type="skeleton",outside=True - ), - ], - elements=[ - models.SubLabeledTrackRequest( - frame=0, - label_id=sublabels[0].id, - shapes=[ - models.TrackedShapeRequest( - frame=0, type="points", points=[1, 1], outside=False - ), - models.TrackedShapeRequest( - frame=1, type="points", points=[1, 1], outside=False - ), - models.TrackedShapeRequest( - frame=2, type="points", points=[1, 1], outside=True - ), - ] - ) - ] - ) - ], - ) + # create annotations of coco keypoints with outside true + response = patch_method( + "admin1", f"tasks/{task_id}/annotations", annotations, action="create" ) + assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" - - dataset_file = self.tmp_dir / (format_name + "some_file.zip") + dataset_file = self.tmp_dir / (format_name + "source_data.zip") task.export_dataset(format_name, dataset_file, include_images=False) - original_annotations = task.get_annotations() - 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() + + # delete all annotations + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" + + # 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() # 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) + assert len(original_annotations["shapes"]) == len(imported_annotations["shapes"]) + assert len(original_annotations["tracks"]) == len(imported_annotations["tracks"]) - for i, original_track in enumerate(original_annotations.tracks): - assert len(original_track.shapes) == len(imported_annotations.tracks[i].shapes) + for i, original_track in enumerate(original_annotations["tracks"]): + assert len(original_track["shapes"]) == len(imported_annotations["tracks"][i]["shapes"]) # 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] + assert set([s["frame"] for s in original_annotations["shapes"]]) == set( + [s["frame"] for s in imported_annotations["shapes"]] ) - assert set([t.frame for t in original_annotations.tracks]) == set( - [t.frame for t in imported_annotations.tracks] + assert set([t["frame"] for t in original_annotations["tracks"]]) == set( + [t["frame"] for t in imported_annotations["tracks"]] ) assert set( [ - tes.frame - for t in original_annotations.tracks - for te in t.elements - for tes in te.shapes + 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 + tes["frame"] + for t in imported_annotations["tracks"] + for te in t["elements"] + for tes in te["shapes"] ] - ) \ No newline at end of file + ) From bb71350278816b563035b5b9f702ded7ac323177 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Thu, 11 Apr 2024 21:11:10 +0800 Subject: [PATCH 11/44] use ending_tracks to track last frame with outside true --- cvat/apps/dataset_manager/bindings.py | 119 +++++++------ cvat/apps/dataset_manager/util.py | 16 ++ tests/python/rest_api/test_tasks.py | 244 ++++++++++++-------------- 3 files changed, 197 insertions(+), 182 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 6e01f885d40f..82cbf63e3e18 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -24,6 +24,7 @@ from django.db.models import QuerySet from django.utils import timezone +from cvat.apps.dataset_manager.util import to_boolean from cvat.apps.dataset_manager.formats.utils import get_label_color from cvat.apps.dataset_manager.util import add_prefetch_fields from cvat.apps.engine.frame_provider import FrameProvider @@ -1962,6 +1963,7 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa root_hint = find_dataset_root(dm_dataset, instance_data) tracks = {} + ending_tracks = {} for item in dm_dataset: frame_number = instance_data.abs_frame_id( @@ -2032,9 +2034,10 @@ 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(to_boolean(ann.attributes.pop('occluded', None)), bool) is True + + keyframe = dm.util.cast(to_boolean(ann.attributes.get('keyframe', None)), bool) is True + outside = dm.util.cast(to_boolean(ann.attributes.pop('outside', None)), bool) is True track_id = ann.attributes.pop('track_id', None) source = ann.attributes.pop('source').lower() \ @@ -2090,6 +2093,13 @@ def reduce_fn(acc, v): 'shapes': [], 'elements':{}, } + ending_tracks[track_id] = { + 'label': label_cat.items[ann.label].name, + 'group': group_map.get(ann.group, 0), + 'source': source, + 'shapes': [], + 'elements':{}, + } track = instance_data.TrackedShape( type=shapes[ann.type], @@ -2104,15 +2114,17 @@ def reduce_fn(acc, v): attributes=attributes, ) - tracks[track_id]['shapes'].append(track) + if keyframe or outside: + tracks[track_id]['shapes'].append(track) + ending_tracks[track_id]['shapes'] = [] + else: + ending_tracks[track_id]['shapes'].append(track) if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: - element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool, True) + element_keyframe = dm.util.cast(to_boolean(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( @@ -2121,6 +2133,13 @@ def reduce_fn(acc, v): source=source, shapes=[], ) + ending_tracks[track_id]['elements'][element.label] = instance_data.Track( + label=label_cat.items[element.label].name, + group=0, + source=source, + shapes=[], + ) + element_attributes = [ instance_data.Attribute(name=n, value=str(v)) for n, v in element.attributes.items() @@ -2128,7 +2147,7 @@ def reduce_fn(acc, v): element_source = element.attributes.pop('source').lower() \ if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' - tracks[track_id]['elements'][element.label].shapes.append(instance_data.TrackedShape( + new_element_shape = instance_data.TrackedShape( type=shapes[element.type], frame=frame_number, occluded=element_occluded, @@ -2138,7 +2157,12 @@ def reduce_fn(acc, v): z_order=element.z_order, source=element_source, attributes=element_attributes, - )) + ) + if (element_keyframe or element_outside) and (keyframe or outside): + tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) + ending_tracks[track_id]['elements'][element.label].shapes.clear() + else: + ending_tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) elif ann.type == dm.AnnotationType.label: instance_data.add_tag(instance_data.Tag( @@ -2152,54 +2176,41 @@ 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(): + for track_id, track in tracks.items(): + # to handle annotation without keyframe + if len(track['shapes']) == 0: + if len(ending_tracks[track_id]['shapes']) != 0: + track['shapes'].append(ending_tracks[track_id]['shapes'][0]._replace(outside=False, frame=frame_number)) track['shapes'].sort(key=lambda t: t.frame) - prev_shape_idx = 0 - prev_shape = track['shapes'][0] - for shape in track['shapes'][1:]: - has_skip = instance_data.frame_step < shape.frame - prev_shape.frame - if has_skip and not prev_shape.outside: - prev_shape = prev_shape._replace(outside=True, - frame=prev_shape.frame + instance_data.frame_step) - prev_shape_idx += 1 - track['shapes'].insert(prev_shape_idx, prev_shape) - if ann.type == dm.AnnotationType.skeleton: - for element in prev_shape.elements: - element = element._replace(outside=True, - frame=element.frame + instance_data.frame_step) - track['elements'][element.label].shapes.append(element) - prev_shape = shape - prev_shape_idx += 1 - - # if the last shape 'outside' is False, we need to add to stop the tracking - last_shape = track['shapes'][-1] - if last_shape.frame + instance_data.frame_step <= \ - int(instance_data.meta[instance_data.META_FIELD]['stop_frame']): - track['shapes'].append(last_shape._replace(outside=True, - frame=last_shape.frame + instance_data.frame_step) - ) + stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) + if not track['shapes'][-1].outside: + if len(ending_tracks[track_id]['shapes']) == 0: + continue + if len(ending_tracks[track_id]['shapes']) == 0: + next_frame = track['shapes'][-1].frame + instance_data.frame_step + if next_frame < stop_frame: + track['shapes'].append(track['shapes'][-1]._replace(outside=True, frame=next_frame)) + else: + ending_tracks[track_id]['shapes'].sort(key=lambda t: t.frame) + next_frame = ending_tracks[track_id]['shapes'][-1].frame + instance_data.frame_step + if next_frame < stop_frame: + track['shapes'].append(ending_tracks[track_id]['shapes'][-1]._replace(outside=True, frame=next_frame)) if ann.type == dm.AnnotationType.skeleton: - for element in track['elements'].values(): - element.shapes.sort(key=lambda t: t.frame) - prev_element_shape_idx = 0 - prev_element_shape = element.shapes[0] - for shape in element.shapes[1:]: - has_skip = instance_data.frame_step < shape.frame - prev_element_shape.frame - if has_skip and not prev_element_shape.outside: - prev_element_shape = prev_element_shape._replace(outside=True, - frame=prev_element_shape.frame + instance_data.frame_step) - prev_element_shape_idx += 1 - element.shapes.insert(prev_element_shape_idx, prev_element_shape) - prev_element_shape = shape - prev_element_shape_idx += 1 - - last_shape = element.shapes[-1] - if last_shape.frame + instance_data.frame_step <= \ - int(instance_data.meta[instance_data.META_FIELD]['stop_frame']): - element.shapes.append(last_shape._replace(outside=True, - frame=last_shape.frame + instance_data.frame_step) - ) + ending_elements = ending_tracks[track_id]['elements'] + for element_id, element in track['elements'].items(): + if len(element.shapes) == 0: + continue + if not element.shapes[-1].outside: + if len(ending_elements.get(element_id).shapes) == 0: + next_frame = element.shapes[-1].frame + instance_data.frame_step + if next_frame < stop_frame: + element.shapes.append(element.shapes[-1]._replace(outside=True, frame=next_frame)) + else: + ending_elements.get(element_id).shapes.sort(key=lambda t: t.frame) + next_frame = ending_elements.get(element_id).shapes[-1].frame + instance_data.frame_step + if next_frame < stop_frame: + element.shapes.append(ending_elements.get(element_id).shapes[-1]._replace(outside=True, frame=next_frame)) for track in tracks.values(): track['elements'] = list(track['elements'].values()) diff --git a/cvat/apps/dataset_manager/util.py b/cvat/apps/dataset_manager/util.py index 387b74d21777..e1ce6563d5a1 100644 --- a/cvat/apps/dataset_manager/util.py +++ b/cvat/apps/dataset_manager/util.py @@ -80,3 +80,19 @@ def deepcopy_simple(v): return v else: return deepcopy(v) + +def to_boolean(value): + """ + Converts an input to a boolean. Strings that case-insensitively match 'true' or 'false' are converted accordingly. + Returns None if the input is None, allowing distinction from False. Other types are converted to boolean using standard truthiness rules. + + :param value: The input value to convert. + :return: True, False, or None based on the input. + """ + if value is None: + return None + elif isinstance(value, str): + return value.lower() == 'true' + else: + return bool(value) + diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 33d878fae1bf..b91f80c11439 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2819,81 +2819,34 @@ 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" - - original_annotations = task.get_annotations() - - task.export_dataset(format_name, dataset_file, include_images=False) - task.remove_annotations() - task.import_annotations(format_name, dataset_file) - - imported_annotations = task.get_annotations() - - # 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) - - # 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] - ) - assert set([t.frame for t in original_annotations.tracks]) == set( - [t.frame for t in imported_annotations.tracks] - ) - 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 - ] - ) - - @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 = 21 + 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 # delete all annotations response = delete_method("admin1", f"tasks/{task_id}/annotations") assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - annotations = { - "tracks": [ - { - "frame": 0, - "label_id": 58, - "shapes": [ - {"type": "rectangle", "frame": 0, "points": [3.0, 2.0, 2.0, 3.0]}, - {"type": "rectangle", "frame": 1, "points": [3.0, 2.0, 2.0, 3.0]}, - { - "type": "rectangle", - "frame": 2, - "points": [3.0, 2.0, 2.0, 3.0], - "outside": True, - }, - ], - } - ] - } + # 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 - # create annotations with outside true response = patch_method( "admin1", f"tasks/{task_id}/annotations", annotations, action="create" ) assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" - dataset_file = self.tmp_dir / (format_name + "source_data.zip") task.export_dataset(format_name, dataset_file, include_images=False) # get the original annotations @@ -2912,6 +2865,9 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): 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): # 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"]) @@ -2942,96 +2898,128 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): ] ) - def test_export_and_import_coco_keypoints_with_outside_true(self, jobs): - task_id = 21 + def test_can_export_and_import_skeleton_tracks_in_coco_format(self): + task_id = 14 format_name = "COCO Keypoints 1.0" - task = self.client.tasks.retrieve(task_id) - - # delete all annotations in task 21 - response = delete_method("admin1", f"tasks/{task_id}/annotations") - assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - + dataset_file = self.tmp_dir / (format_name + "source_data.zip") annotations = { + "shapes": [], "tracks": [ { "frame": 0, - "label_id": 58, "shapes": [ - {"type": "skeleton", "frame": 0, "points": []}, - {"type": "skeleton", "frame": 1, "points": []}, - {"type": "skeleton", "frame": 2, "points": [], "outside": True}, + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + {"type": "skeleton", "frame": 3, "points": [], "keyframe": True}, ], "elements": [ { - "label_id": 59, "frame": 0, "shapes": [ - {"type": "points", "frame": 0, "points": [1.0, 2.0]}, - {"type": "points", "frame": 1, "points": [2.0, 4.0]}, { "type": "points", - "frame": 2, - "points": [3.0, 6.0], - "outside": True, + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "keyframe": True, }, ], }, ], } - ] + ], } - # create annotations of coco keypoints with outside true - response = patch_method( - "admin1", f"tasks/{task_id}/annotations", annotations, action="create" + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file ) - assert response.status_code == 200, f"Cannot update task's annotations: {response.content}" - - dataset_file = self.tmp_dir / (format_name + "source_data.zip") - task.export_dataset(format_name, dataset_file, include_images=False) - - # 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() - # delete all annotations - response = delete_method("admin1", f"tasks/{task_id}/annotations") - assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" + self.compare_original_and_import_annotations(original_annotations, imported_annotations) - # import the annotations - task.import_annotations(format_name, dataset_file) + @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, + "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": [], + } + ], + } - 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() + original_annotations, imported_annotations = self.delete_annotation_and_import_annotations( + task_id, annotations, format_name, dataset_file + ) - # 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"]) + self.compare_original_and_import_annotations(original_annotations, imported_annotations) - for i, original_track in enumerate(original_annotations["tracks"]): - assert len(original_track["shapes"]) == len(imported_annotations["tracks"][i]["shapes"]) + 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, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + { + "type": "skeleton", + "frame": 3, + "points": [], + "keyframe": True, + "outside": True, + }, + ], + "elements": [ + { + "frame": 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, + }, + ], + }, + ], + } + ], + } - # 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"]] - ) - assert set([t["frame"] for t in original_annotations["tracks"]]) == set( - [t["frame"] for t in imported_annotations["tracks"]] - ) - 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"] - ] + 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) From bb77078e4d077109ac77855ba8765444356c8152 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:09:26 +0800 Subject: [PATCH 12/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 82cbf63e3e18..620101bef5d0 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2034,10 +2034,10 @@ 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(to_boolean(ann.attributes.pop('occluded', None)), bool) is True + occluded = dm.util.cast(ann.attributes.pop('occluded', None), to_boolean) is True - keyframe = dm.util.cast(to_boolean(ann.attributes.get('keyframe', None)), bool) is True - outside = dm.util.cast(to_boolean(ann.attributes.pop('outside', None)), bool) is True + keyframe = dm.util.cast(ann.attributes.get('keyframe', None), to_boolean) is True + outside = dm.util.cast(ann.attributes.pop('outside', None), to_boolean) is True track_id = ann.attributes.pop('track_id', None) source = ann.attributes.pop('source').lower() \ From 9a7021452a711537e5d9ba86685c3704f8d84c9c Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:09:36 +0800 Subject: [PATCH 13/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 620101bef5d0..1d68bff645e1 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2122,7 +2122,7 @@ def reduce_fn(acc, v): if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: - element_keyframe = dm.util.cast(to_boolean(element.attributes.get('keyframe', None)), bool, True) + element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), to_boolean, True) element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent From 8772fbcab005cb6410ac1b086f1a905508caf2a9 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Thu, 11 Apr 2024 23:27:48 +0800 Subject: [PATCH 14/44] change to_boolean to to_bool --- cvat/apps/dataset_manager/bindings.py | 11 +++++------ cvat/apps/dataset_manager/util.py | 16 ---------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 1d68bff645e1..4626f932122b 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 @@ -24,7 +25,6 @@ from django.db.models import QuerySet from django.utils import timezone -from cvat.apps.dataset_manager.util import to_boolean from cvat.apps.dataset_manager.formats.utils import get_label_color from cvat.apps.dataset_manager.util import add_prefetch_fields from cvat.apps.engine.frame_provider import FrameProvider @@ -2034,10 +2034,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), to_boolean) is True - - keyframe = dm.util.cast(ann.attributes.get('keyframe', None), to_boolean) is True - outside = dm.util.cast(ann.attributes.pop('outside', None), to_boolean) 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() \ @@ -2122,7 +2121,7 @@ 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), to_boolean, True) + element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), to_bool, True) element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent diff --git a/cvat/apps/dataset_manager/util.py b/cvat/apps/dataset_manager/util.py index e1ce6563d5a1..387b74d21777 100644 --- a/cvat/apps/dataset_manager/util.py +++ b/cvat/apps/dataset_manager/util.py @@ -80,19 +80,3 @@ def deepcopy_simple(v): return v else: return deepcopy(v) - -def to_boolean(value): - """ - Converts an input to a boolean. Strings that case-insensitively match 'true' or 'false' are converted accordingly. - Returns None if the input is None, allowing distinction from False. Other types are converted to boolean using standard truthiness rules. - - :param value: The input value to convert. - :return: True, False, or None based on the input. - """ - if value is None: - return None - elif isinstance(value, str): - return value.lower() == 'true' - else: - return bool(value) - From b6c509e9e6e7df6786290fc349fc4c7b16bddb73 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Fri, 12 Apr 2024 00:50:39 +0800 Subject: [PATCH 15/44] rearrage tracks into finalized_tracks --- cvat/apps/dataset_manager/bindings.py | 110 +++++++++++++------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 4626f932122b..ef1777c3220a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -1963,7 +1963,6 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa root_hint = find_dataset_root(dm_dataset, instance_data) tracks = {} - ending_tracks = {} for item in dm_dataset: frame_number = instance_data.abs_frame_id( @@ -2092,13 +2091,6 @@ def reduce_fn(acc, v): 'shapes': [], 'elements':{}, } - ending_tracks[track_id] = { - 'label': label_cat.items[ann.label].name, - 'group': group_map.get(ann.group, 0), - 'source': source, - 'shapes': [], - 'elements':{}, - } track = instance_data.TrackedShape( type=shapes[ann.type], @@ -2113,11 +2105,7 @@ def reduce_fn(acc, v): attributes=attributes, ) - if keyframe or outside: - tracks[track_id]['shapes'].append(track) - ending_tracks[track_id]['shapes'] = [] - else: - ending_tracks[track_id]['shapes'].append(track) + tracks[track_id]['shapes'].append(track) if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: @@ -2132,12 +2120,6 @@ def reduce_fn(acc, v): source=source, shapes=[], ) - ending_tracks[track_id]['elements'][element.label] = instance_data.Track( - label=label_cat.items[element.label].name, - group=0, - source=source, - shapes=[], - ) element_attributes = [ instance_data.Attribute(name=n, value=str(v)) @@ -2157,11 +2139,8 @@ def reduce_fn(acc, v): source=element_source, attributes=element_attributes, ) - if (element_keyframe or element_outside) and (keyframe or outside): - tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) - ending_tracks[track_id]['elements'][element.label].shapes.clear() - else: - ending_tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) + + tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) elif ann.type == dm.AnnotationType.label: instance_data.add_tag(instance_data.Tag( @@ -2175,43 +2154,64 @@ def reduce_fn(acc, v): raise CvatImportError("Image {}: can't import annotation " "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e + finalize_tracks = {} + stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) for track_id, track in tracks.items(): - # to handle annotation without keyframe - if len(track['shapes']) == 0: - if len(ending_tracks[track_id]['shapes']) != 0: - track['shapes'].append(ending_tracks[track_id]['shapes'][0]._replace(outside=False, frame=frame_number)) + finalize_tracks[track_id] = { + 'label': track['label'], + 'group': track['group'], + 'source': track['source'], + 'shapes': [], + 'elements':{}, + } track['shapes'].sort(key=lambda t: t.frame) - stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) - if not track['shapes'][-1].outside: - if len(ending_tracks[track_id]['shapes']) == 0: - continue - if len(ending_tracks[track_id]['shapes']) == 0: - next_frame = track['shapes'][-1].frame + instance_data.frame_step - if next_frame < stop_frame: - track['shapes'].append(track['shapes'][-1]._replace(outside=True, frame=next_frame)) + prev_shape = track['shapes'][0] + + for shape in track['shapes'][1:]: + if prev_shape.keyframe or prev_shape.outside: + finalize_tracks[track_id]['shapes'].append(prev_shape) else: - ending_tracks[track_id]['shapes'].sort(key=lambda t: t.frame) - next_frame = ending_tracks[track_id]['shapes'][-1].frame + instance_data.frame_step - if next_frame < stop_frame: - track['shapes'].append(ending_tracks[track_id]['shapes'][-1]._replace(outside=True, frame=next_frame)) + has_skip = instance_data.frame_step < shape.frame - prev_shape.frame + if has_skip and not prev_shape.outside: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + finalize_tracks[track_id]['shapes'].append(prev_shape) + prev_shape = shape + + if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= stop_frame: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + finalize_tracks[track_id]['shapes'].append(prev_shape) if ann.type == dm.AnnotationType.skeleton: - ending_elements = ending_tracks[track_id]['elements'] - for element_id, element in track['elements'].items(): - if len(element.shapes) == 0: - continue - if not element.shapes[-1].outside: - if len(ending_elements.get(element_id).shapes) == 0: - next_frame = element.shapes[-1].frame + instance_data.frame_step - if next_frame < stop_frame: - element.shapes.append(element.shapes[-1]._replace(outside=True, frame=next_frame)) + for element in track['elements'].values(): + finalize_tracks[track_id]['elements'][element.label] = instance_data.Track( + label=element.label, + group=element.group, + source=element.source, + shapes=[], + ) + current_shapes = finalize_tracks[track_id]['elements'][element.label].shapes + element.shapes.sort(key=lambda t: t.frame) + prev_shape = element.shapes[0] + + for shape in element.shapes[1:]: + if prev_shape.keyframe or prev_shape.outside: + current_shapes.append(prev_shape) else: - ending_elements.get(element_id).shapes.sort(key=lambda t: t.frame) - next_frame = ending_elements.get(element_id).shapes[-1].frame + instance_data.frame_step - if next_frame < stop_frame: - element.shapes.append(ending_elements.get(element_id).shapes[-1]._replace(outside=True, frame=next_frame)) - - for track in tracks.values(): + has_skip = instance_data.frame_step < shape.frame - prev_shape.frame + if has_skip and not prev_shape.outside: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + current_shapes.append(prev_shape) + prev_shape = shape + + if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= stop_frame: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + current_shapes.append(prev_shape) + + for track in finalize_tracks.values(): track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) From 3ed07a1c6e3aaa84a23df92c19e9ba4bfbb9f840 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:52:14 +0800 Subject: [PATCH 16/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index ef1777c3220a..192c2602cbf5 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2211,7 +2211,6 @@ def reduce_fn(acc, v): frame=prev_shape.frame + instance_data.frame_step) current_shapes.append(prev_shape) - for track in finalize_tracks.values(): track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) From 7000254e9a20468af0c3f7874fd9b0e1efd5a443 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Fri, 12 Apr 2024 21:35:28 +0800 Subject: [PATCH 17/44] remove finalized_tracks --- cvat/apps/dataset_manager/bindings.py | 105 +++++++++++++++----------- tests/python/rest_api/test_tasks.py | 7 +- 2 files changed, 61 insertions(+), 51 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index ef1777c3220a..ea0bb14ce3cd 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2128,7 +2128,7 @@ def reduce_fn(acc, v): element_source = element.attributes.pop('source').lower() \ if element.attributes.get('source', '').lower() in {'auto', 'semi-auto', 'manual', 'file'} else 'manual' - new_element_shape = instance_data.TrackedShape( + tracks[track_id]['elements'][element.label].shapes.append(instance_data.TrackedShape( type=shapes[element.type], frame=frame_number, occluded=element_occluded, @@ -2138,9 +2138,7 @@ def reduce_fn(acc, v): z_order=element.z_order, source=element_source, attributes=element_attributes, - ) - - tracks[track_id]['elements'][element.label].shapes.append(new_element_shape) + )) elif ann.type == dm.AnnotationType.label: instance_data.add_tag(instance_data.Tag( @@ -2154,64 +2152,79 @@ def reduce_fn(acc, v): raise CvatImportError("Image {}: can't import annotation " "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e - finalize_tracks = {} stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) for track_id, track in tracks.items(): - finalize_tracks[track_id] = { - 'label': track['label'], - 'group': track['group'], - 'source': track['source'], - 'shapes': [], - 'elements':{}, - } + # skip tracks with no shapes and elements + if len(track['shapes']) == 0 and len(track['elements']) == 0: + continue + track['shapes'].sort(key=lambda t: t.frame) prev_shape = track['shapes'][0] - - for shape in track['shapes'][1:]: + # add keyframe to the first shape + prev_shape = prev_shape._replace(keyframe=True) + new_shapes = [] + + # avoid skipping the first frame + if len(track['shapes']) > 1: + for shape in track['shapes'][1:]: + if prev_shape.keyframe or prev_shape.outside: + prev_shape = prev_shape._replace(keyframe=True) + new_shapes.append(prev_shape) + else: + has_skip = instance_data.frame_step < shape.frame - prev_shape.frame + if has_skip and not prev_shape.outside: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_shapes.append(prev_shape) + prev_shape = shape + else: if prev_shape.keyframe or prev_shape.outside: - finalize_tracks[track_id]['shapes'].append(prev_shape) - else: - has_skip = instance_data.frame_step < shape.frame - prev_shape.frame - if has_skip and not prev_shape.outside: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - finalize_tracks[track_id]['shapes'].append(prev_shape) - prev_shape = shape + new_shapes.append(prev_shape) - if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= stop_frame: + if not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: prev_shape = prev_shape._replace(outside=True, keyframe=True, frame=prev_shape.frame + instance_data.frame_step) - finalize_tracks[track_id]['shapes'].append(prev_shape) + new_shapes.append(prev_shape) + + track['shapes'] = new_shapes if ann.type == dm.AnnotationType.skeleton: - for element in track['elements'].values(): - finalize_tracks[track_id]['elements'][element.label] = instance_data.Track( - label=element.label, - group=element.group, - source=element.source, - shapes=[], - ) - current_shapes = finalize_tracks[track_id]['elements'][element.label].shapes + new_elements = {} + for element_id, element in track['elements'].items(): element.shapes.sort(key=lambda t: t.frame) prev_shape = element.shapes[0] - - for shape in element.shapes[1:]: + new_elements[element_id] = instance_data.Track( + label=element.label, + group=element.group, + source=element.source, + shapes=[], + ) + new_element_shapes = new_elements[element_id].shapes + + # avoid skipping the first frame + if len(element.shapes) > 1: + for shape in element.shapes[1:]: + if prev_shape.keyframe or prev_shape.outside: + prev_shape = prev_shape._replace(keyframe=True) + new_element_shapes.append(prev_shape) + else: + has_skip = instance_data.frame_step < shape.frame - prev_shape.frame + if has_skip and not prev_shape.outside: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_element_shapes.append(prev_shape) + prev_shape = shape + else: if prev_shape.keyframe or prev_shape.outside: - current_shapes.append(prev_shape) - else: - has_skip = instance_data.frame_step < shape.frame - prev_shape.frame - if has_skip and not prev_shape.outside: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - current_shapes.append(prev_shape) - prev_shape = shape - - if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= stop_frame: + new_element_shapes.append(prev_shape) + + if not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: prev_shape = prev_shape._replace(outside=True, keyframe=True, frame=prev_shape.frame + instance_data.frame_step) - current_shapes.append(prev_shape) + new_element_shapes.append(prev_shape) + + track['elements'] = new_elements - for track in finalize_tracks.values(): track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index b91f80c11439..5817638223bd 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, @@ -2826,10 +2827,6 @@ def delete_annotation_and_import_annotations( labels = task.get_labels() sublabels = labels[0].sublabels - # delete all annotations - response = delete_method("admin1", f"tasks/{task_id}/annotations") - assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - # 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: @@ -2842,7 +2839,7 @@ def delete_annotation_and_import_annotations( if "label_id" not in element: element["label_id"] = sublabels[element_idx].id - response = patch_method( + 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}" From 92324554ef51059dd8152f60ebbee9fb201b2fb5 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Fri, 12 Apr 2024 23:59:46 +0800 Subject: [PATCH 18/44] remove element_keyframe --- cvat/apps/dataset_manager/bindings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index ea0bb14ce3cd..795af9017303 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2109,7 +2109,6 @@ 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), to_bool, True) element_occluded = element.visibility[0] == dm.Points.Visibility.hidden element_outside = element.visibility[0] == dm.Points.Visibility.absent From 397b6ccbde34375d68afe4ada140fe09e956b9b8 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Sat, 13 Apr 2024 14:56:20 +0800 Subject: [PATCH 19/44] only add keyframe true to non-outside first shape --- cvat/apps/dataset_manager/bindings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 795af9017303..7253252166da 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2159,8 +2159,9 @@ def reduce_fn(acc, v): track['shapes'].sort(key=lambda t: t.frame) prev_shape = track['shapes'][0] - # add keyframe to the first shape - prev_shape = prev_shape._replace(keyframe=True) + # add keyframe to the first non-outside shape + if not prev_shape.outside: + prev_shape = prev_shape._replace(keyframe=True) new_shapes = [] # avoid skipping the first frame From ff55b207c1ebd1c416efaf5da42f84277a349516 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 14:07:58 +0800 Subject: [PATCH 20/44] handle last track with outside true or keyframe true --- cvat/apps/dataset_manager/bindings.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 7253252166da..31e67381f341 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2177,15 +2177,18 @@ def reduce_fn(acc, v): frame=prev_shape.frame + instance_data.frame_step) new_shapes.append(prev_shape) prev_shape = shape + + if prev_shape.outside or prev_shape.keyframe: + new_shapes.append(prev_shape) + elif not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_shapes.append(prev_shape) + else: if prev_shape.keyframe or prev_shape.outside: new_shapes.append(prev_shape) - if not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_shapes.append(prev_shape) - track['shapes'] = new_shapes if ann.type == dm.AnnotationType.skeleton: @@ -2214,15 +2217,18 @@ def reduce_fn(acc, v): frame=prev_shape.frame + instance_data.frame_step) new_element_shapes.append(prev_shape) prev_shape = shape + + if prev_shape.outside or prev_shape.keyframe: + new_element_shapes.append(prev_shape) + elif not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: + prev_shape = prev_shape._replace(outside=True, keyframe=True, + frame=prev_shape.frame + instance_data.frame_step) + new_element_shapes.append(prev_shape) + else: if prev_shape.keyframe or prev_shape.outside: new_element_shapes.append(prev_shape) - if not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_element_shapes.append(prev_shape) - track['elements'] = new_elements track['elements'] = list(track['elements'].values()) From feddc51b4363d1ebcbabf34205d7062a3af735f1 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 18:10:27 +0800 Subject: [PATCH 21/44] minor fix --- cvat/apps/dataset_manager/bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 31e67381f341..25707123f2fb 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2186,7 +2186,7 @@ def reduce_fn(acc, v): new_shapes.append(prev_shape) else: - if prev_shape.keyframe or prev_shape.outside: + if prev_shape.keyframe: new_shapes.append(prev_shape) track['shapes'] = new_shapes @@ -2226,7 +2226,7 @@ def reduce_fn(acc, v): new_element_shapes.append(prev_shape) else: - if prev_shape.keyframe or prev_shape.outside: + if prev_shape.keyframe: new_element_shapes.append(prev_shape) track['elements'] = new_elements From 667f2ac6998cecf716307849f9e2b30625c2ead1 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 18:23:51 +0800 Subject: [PATCH 22/44] only append shapes with keyframe equals to true --- cvat/apps/dataset_manager/bindings.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 25707123f2fb..1585366b3bd0 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2167,8 +2167,7 @@ def reduce_fn(acc, v): # avoid skipping the first frame if len(track['shapes']) > 1: for shape in track['shapes'][1:]: - if prev_shape.keyframe or prev_shape.outside: - prev_shape = prev_shape._replace(keyframe=True) + if prev_shape.keyframe: new_shapes.append(prev_shape) else: has_skip = instance_data.frame_step < shape.frame - prev_shape.frame @@ -2207,8 +2206,7 @@ def reduce_fn(acc, v): # avoid skipping the first frame if len(element.shapes) > 1: for shape in element.shapes[1:]: - if prev_shape.keyframe or prev_shape.outside: - prev_shape = prev_shape._replace(keyframe=True) + if prev_shape.keyframe: new_element_shapes.append(prev_shape) else: has_skip = instance_data.frame_step < shape.frame - prev_shape.frame From 67920da597b44618c4b1188a9312087ea9a08ee0 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:20:22 +0800 Subject: [PATCH 23/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 47 +++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 1585366b3bd0..068b6c49f78d 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2158,35 +2158,34 @@ def reduce_fn(acc, v): continue track['shapes'].sort(key=lambda t: t.frame) - prev_shape = track['shapes'][0] - # add keyframe to the first non-outside shape - if not prev_shape.outside: - prev_shape = prev_shape._replace(keyframe=True) new_shapes = [] + prev_shape = None + # infer the keyframe shapes and keep only them + for shape in track['shapes']: + cur_is_visible = shape and not shape.outside + if not cur_is_visible: + continue - # avoid skipping the first frame - if len(track['shapes']) > 1: - for shape in track['shapes'][1:]: - if prev_shape.keyframe: + prev_is_visible = prev_shape and not prev_shape.outside + if not prev_is_visible or ( + 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) - else: - has_skip = instance_data.frame_step < shape.frame - prev_shape.frame - if has_skip and not prev_shape.outside: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_shapes.append(prev_shape) - prev_shape = shape - if prev_shape.outside or prev_shape.keyframe: - new_shapes.append(prev_shape) - elif not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_shapes.append(prev_shape) + shape = shape._replace(keyframe=True) + new_shapes.append(shape) + prev_shape = shape - else: - if prev_shape.keyframe: - new_shapes.append(prev_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) track['shapes'] = new_shapes From 37eb05d58126490d432a23ad564a2def671d9a48 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 22:34:15 +0800 Subject: [PATCH 24/44] refactor code --- cvat/apps/dataset_manager/bindings.py | 65 +++++++++------------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 068b6c49f78d..c49dcdfe467d 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2151,23 +2151,21 @@ def reduce_fn(acc, v): raise CvatImportError("Image {}: can't import annotation " "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e - stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) - for track_id, track in tracks.items(): - # skip tracks with no shapes and elements - if len(track['shapes']) == 0 and len(track['elements']) == 0: - continue - - track['shapes'].sort(key=lambda t: t.frame) + def append_necessary_outside_attribute(shapes): new_shapes = [] prev_shape = None # infer the keyframe shapes and keep only them - for shape in track['shapes']: - cur_is_visible = shape and not shape.outside + for shape in shapes: + cur_is_visible = shape is not None and not shape.outside if not cur_is_visible: + prev_shape = shape continue - prev_is_visible = prev_shape and not prev_shape.outside - if not prev_is_visible or ( + prev_is_visible = prev_shape is not None and not prev_shape.outside + if prev_shape is None or shape.keyframe: + shape = shape._replace(keyframe=True) + new_shapes.append(shape) + elif not prev_is_visible or ( has_gap := prev_shape.frame + instance_data.frame_step < shape.frame ): if has_gap: @@ -2177,7 +2175,8 @@ def reduce_fn(acc, v): shape = shape._replace(keyframe=True) new_shapes.append(shape) - prev_shape = shape + + prev_shape = shape if prev_shape and not prev_shape.outside and ( prev_shape.frame + instance_data.frame_step <= stop_frame @@ -2187,49 +2186,29 @@ def reduce_fn(acc, v): frame=prev_shape.frame + instance_data.frame_step) new_shapes.append(prev_shape) - track['shapes'] = new_shapes + return new_shapes + + stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) + for track_id, track in tracks.items(): + track['shapes'].sort(key=lambda t: t.frame) + track['shapes'] = append_necessary_outside_attribute(track['shapes']) if ann.type == dm.AnnotationType.skeleton: new_elements = {} for element_id, element in track['elements'].items(): element.shapes.sort(key=lambda t: t.frame) - prev_shape = element.shapes[0] + new_element_shapes = append_necessary_outside_attribute(element.shapes) new_elements[element_id] = instance_data.Track( label=element.label, group=element.group, source=element.source, - shapes=[], + shapes=new_element_shapes, ) - new_element_shapes = new_elements[element_id].shapes - - # avoid skipping the first frame - if len(element.shapes) > 1: - for shape in element.shapes[1:]: - if prev_shape.keyframe: - new_element_shapes.append(prev_shape) - else: - has_skip = instance_data.frame_step < shape.frame - prev_shape.frame - if has_skip and not prev_shape.outside: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_element_shapes.append(prev_shape) - prev_shape = shape - - if prev_shape.outside or prev_shape.keyframe: - new_element_shapes.append(prev_shape) - elif not prev_shape.outside and prev_shape.frame + instance_data.frame_step <= stop_frame: - prev_shape = prev_shape._replace(outside=True, keyframe=True, - frame=prev_shape.frame + instance_data.frame_step) - new_element_shapes.append(prev_shape) - - else: - if prev_shape.keyframe: - new_element_shapes.append(prev_shape) - track['elements'] = new_elements - track['elements'] = list(track['elements'].values()) - instance_data.add_track(instance_data.Track(**track)) + if not(len(track['shapes']) == 0 and len(track['elements']) == 0): + 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 = [] From 8c5662b46a9c805119b8da5c8a3d8d8adcb94baa Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 22:58:54 +0800 Subject: [PATCH 25/44] fix minor bug --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index c49dcdfe467d..195cf3f50ced 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2206,7 +2206,7 @@ def append_necessary_outside_attribute(shapes): ) track['elements'] = new_elements - if not(len(track['shapes']) == 0 and len(track['elements']) == 0): + if not(len(track['shapes']) == 0 or len(track['elements']) == 0): track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) From 3cd7dccf0af3f4070d2ccf342d678c5c96bd6c6f Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:01:28 +0800 Subject: [PATCH 26/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 195cf3f50ced..850d2cab5569 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2206,7 +2206,7 @@ def append_necessary_outside_attribute(shapes): ) track['elements'] = new_elements - if not(len(track['shapes']) == 0 or len(track['elements']) == 0): + if track['shapes']) or track['elements']: track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) From a8d5c72c230dbbe5645e147529833c034cb1e198 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:15:25 +0800 Subject: [PATCH 27/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 850d2cab5569..8cf62516ae52 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2151,7 +2151,7 @@ def reduce_fn(acc, v): raise CvatImportError("Image {}: can't import annotation " "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e - def append_necessary_outside_attribute(shapes): + def _validate_track_shapes(shapes): new_shapes = [] prev_shape = None # infer the keyframe shapes and keep only them From 7a4d8defdcd386c69de2a8d38a6fcd4fbd8a69ca Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Mon, 15 Apr 2024 23:39:23 +0800 Subject: [PATCH 28/44] add corner tests --- cvat/apps/dataset_manager/bindings.py | 6 +- tests/python/rest_api/test_tasks.py | 346 +++++++++++++++++++++++++- 2 files changed, 336 insertions(+), 16 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 8cf62516ae52..64e69197b5cb 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2191,13 +2191,13 @@ def _validate_track_shapes(shapes): stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) for track_id, track in tracks.items(): track['shapes'].sort(key=lambda t: t.frame) - track['shapes'] = append_necessary_outside_attribute(track['shapes']) + track['shapes'] = _validate_track_shapes(track['shapes']) if ann.type == dm.AnnotationType.skeleton: new_elements = {} for element_id, element in track['elements'].items(): element.shapes.sort(key=lambda t: t.frame) - new_element_shapes = append_necessary_outside_attribute(element.shapes) + new_element_shapes = _validate_track_shapes(element.shapes) new_elements[element_id] = instance_data.Track( label=element.label, group=element.group, @@ -2206,7 +2206,7 @@ def _validate_track_shapes(shapes): ) track['elements'] = new_elements - if track['shapes']) or track['elements']: + if track['shapes'] or track['elements']: track['elements'] = list(track['elements'].values()) instance_data.add_track(instance_data.Track(**track)) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 5817638223bd..69363c045142 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2895,6 +2895,198 @@ def compare_original_and_import_annotations(self, original_annotations, imported ] ) + @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) + def test_can_export_and_import_tracked_format(self, format_name): + task_id = 14 + dataset_file = self.tmp_dir / (format_name + "tracked_format_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 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 + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + @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, + "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": [], + } + ], + } + + 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) + + @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, + "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 + ) + + self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + @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, + "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) + + @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, + "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) + + @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, + "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) + def test_can_export_and_import_skeleton_tracks_in_coco_format(self): task_id = 14 format_name = "COCO Keypoints 1.0" @@ -2937,9 +3129,9 @@ def test_can_export_and_import_skeleton_tracks_in_coco_format(self): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - @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): + 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": [], @@ -2947,21 +3139,82 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): { "frame": 0, "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, { - "type": "rectangle", - "frame": 0, - "points": [1.0, 2.0, 3.0, 2.0], + "type": "skeleton", + "frame": 3, + "points": [], "keyframe": True, + "outside": True, }, + ], + "elements": [ { - "type": "rectangle", + "frame": 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) + + 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, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, + { + "type": "skeleton", "frame": 3, - "points": [1.0, 2.0, 3.0, 2.0], + "points": [], "keyframe": True, - "outside": True, }, ], - "elements": [], + "elements": [ + { + "frame": 0, + "shapes": [ + { + "type": "points", + "frame": 0, + "points": [1.0, 2.0], + "keyframe": True, + }, + { + "type": "points", + "frame": 3, + "points": [1.0, 2.0], + "keyframe": True, + }, + ], + }, + ], } ], } @@ -2972,10 +3225,10 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - def test_export_and_import_coco_keypoints_with_outside_true(self): + 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_true_source_data.zip") + dataset_file = self.tmp_dir / (format_name + "outside_without_keyframe_source_data.zip") annotations = { "shapes": [], "tracks": [ @@ -2987,7 +3240,6 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): "type": "skeleton", "frame": 3, "points": [], - "keyframe": True, "outside": True, }, ], @@ -3005,7 +3257,6 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): "type": "points", "frame": 3, "points": [1.0, 2.0], - "keyframe": True, "outside": True, }, ], @@ -3020,3 +3271,72 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): ) self.compare_original_and_import_annotations(original_annotations, imported_annotations) + + 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, + "shapes": [ + {"type": "skeleton", "frame": 3, "points": [], "outside": True}, + ], + "elements": [ + { + "frame": 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) + + 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, + "shapes": [ + {"type": "skeleton", "frame": 0, "points": []}, + ], + "elements": [ + { + "frame": 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) From f9cbe17192b47ae6bf02aaccecddb1a1e254a322 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:41:37 +0800 Subject: [PATCH 29/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 64e69197b5cb..782762aafb6c 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2162,10 +2162,8 @@ def _validate_track_shapes(shapes): continue prev_is_visible = prev_shape is not None and not prev_shape.outside - if prev_shape is None or shape.keyframe: - shape = shape._replace(keyframe=True) - new_shapes.append(shape) - elif not prev_is_visible or ( + has_gap = False + if not prev_is_visible or shape.keyframe ( has_gap := prev_shape.frame + instance_data.frame_step < shape.frame ): if has_gap: From c471921ba263c3043f4b90da30071bedba7c0e67 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 00:45:01 +0800 Subject: [PATCH 30/44] remove delete_method before import annotations --- tests/python/rest_api/test_tasks.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 69363c045142..6007a61fbb81 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2851,10 +2851,6 @@ def delete_annotation_and_import_annotations( assert response.status_code == 200, f"Cannot get task's annotations: {response.content}" original_annotations = response.json() - # delete all annotations - response = delete_method("admin1", f"tasks/{task_id}/annotations") - assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" - # import the annotations task.import_annotations(format_name, dataset_file) From 68ed9928b73a02227d1b4496fffbafe52851528b Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 00:46:14 +0800 Subject: [PATCH 31/44] fix bug --- cvat/apps/dataset_manager/bindings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 782762aafb6c..4b9d823ab15d 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2163,7 +2163,7 @@ def _validate_track_shapes(shapes): prev_is_visible = prev_shape is not None and not prev_shape.outside has_gap = False - if not prev_is_visible or shape.keyframe ( + if not prev_is_visible or shape.keyframe or ( has_gap := prev_shape.frame + instance_data.frame_step < shape.frame ): if has_gap: From 4e5dcbef49ad5ebb02e0beb0b9f1c08dd2f704e9 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 00:59:00 +0800 Subject: [PATCH 32/44] use DeepDiff --- tests/python/rest_api/test_tasks.py | 35 +++++------------------------ 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 6007a61fbb81..09ef7867af0a 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2861,35 +2861,12 @@ def delete_annotation_and_import_annotations( return original_annotations, imported_annotations def compare_original_and_import_annotations(self, original_annotations, imported_annotations): - # 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"]) - - for i, original_track in enumerate(original_annotations["tracks"]): - assert len(original_track["shapes"]) == len(imported_annotations["tracks"][i]["shapes"]) - - # 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"]] - ) - assert set([t["frame"] for t in original_annotations["tracks"]]) == set( - [t["frame"] for t in imported_annotations["tracks"]] - ) - 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"] - ] - ) + 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'\]", + r"root(\['\w+'\]\[\d+\])+\['group'\]", + ],) == {} @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) def test_can_export_and_import_tracked_format(self, format_name): From 45ec84754bdda4a7ec97b3c58ef76f9092eacdf0 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 01:00:57 +0800 Subject: [PATCH 33/44] format code --- tests/python/rest_api/test_tasks.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 09ef7867af0a..5f84c7a88621 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2861,12 +2861,20 @@ def delete_annotation_and_import_annotations( 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'\]", - r"root(\['\w+'\]\[\d+\])+\['group'\]", - ],) == {} + 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'\]", + r"root(\['\w+'\]\[\d+\])+\['group'\]", + ], + ) + == {} + ) @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) def test_can_export_and_import_tracked_format(self, format_name): From 9d1651a98fc32aafd66aa67cdb999c7488f53025 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 12:04:17 +0800 Subject: [PATCH 34/44] more corner tests --- tests/python/rest_api/test_tasks.py | 132 ++++++++++------------------ 1 file changed, 47 insertions(+), 85 deletions(-) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 5f84c7a88621..141522e38a26 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2870,46 +2870,11 @@ def compare_original_and_import_annotations(self, original_annotations, imported r"root(\['\w+'\]\[\d+\])+\['id'\]", r"root(\['\w+'\]\[\d+\])+\['label_id'\]", r"root(\['\w+'\]\[\d+\])+\['attributes'\]\[\d+\]\['spec_id'\]", - r"root(\['\w+'\]\[\d+\])+\['group'\]", ], ) == {} ) - @pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"]) - def test_can_export_and_import_tracked_format(self, format_name): - task_id = 14 - dataset_file = self.tmp_dir / (format_name + "tracked_format_source_data.zip") - annotations = { - "shapes": [], - "tracks": [ - { - "frame": 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 - ) - - self.compare_original_and_import_annotations(original_annotations, imported_annotations) - @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 @@ -2919,6 +2884,7 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "rectangle", @@ -2954,6 +2920,7 @@ def test_export_and_import_tracked_format_with_intermediate_keyframe(self, forma "tracks": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "rectangle", @@ -2979,6 +2946,9 @@ def test_export_and_import_tracked_format_with_intermediate_keyframe(self, forma 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 @@ -2988,6 +2958,7 @@ def test_export_and_import_tracked_format_with_outside_without_keyframe(self, fo "tracks": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "rectangle", @@ -3013,6 +2984,9 @@ def test_export_and_import_tracked_format_with_outside_without_keyframe(self, fo 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_no_keyframe(self, format_name): task_id = 14 @@ -3022,6 +2996,7 @@ def test_export_and_import_tracked_format_with_no_keyframe(self, format_name): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "rectangle", @@ -3040,6 +3015,9 @@ def test_export_and_import_tracked_format_with_no_keyframe(self, format_name): 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 @@ -3049,6 +3027,7 @@ def test_export_and_import_tracked_format_with_one_outside(self, format_name): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "rectangle", @@ -3068,47 +3047,8 @@ def test_export_and_import_tracked_format_with_one_outside(self, format_name): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - def test_can_export_and_import_skeleton_tracks_in_coco_format(self): - task_id = 14 - format_name = "COCO Keypoints 1.0" - dataset_file = self.tmp_dir / (format_name + "source_data.zip") - annotations = { - "shapes": [], - "tracks": [ - { - "frame": 0, - "shapes": [ - {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, - {"type": "skeleton", "frame": 3, "points": [], "keyframe": True}, - ], - "elements": [ - { - "frame": 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) + # 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_outside_true(self): task_id = 14 @@ -3119,6 +3059,7 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, { @@ -3132,6 +3073,7 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): "elements": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "points", @@ -3168,6 +3110,7 @@ def test_export_and_import_coco_keypoints_with_intermediate_keyframe(self): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, { @@ -3180,6 +3123,7 @@ def test_export_and_import_coco_keypoints_with_intermediate_keyframe(self): "elements": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "points", @@ -3206,6 +3150,9 @@ def test_export_and_import_coco_keypoints_with_intermediate_keyframe(self): 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" @@ -3215,6 +3162,7 @@ def test_export_and_import_coco_keypoints_with_outside_without_keyframe(self): "tracks": [ { "frame": 0, + "group": 0, "shapes": [ {"type": "skeleton", "frame": 0, "points": [], "keyframe": True}, { @@ -3227,6 +3175,7 @@ def test_export_and_import_coco_keypoints_with_outside_without_keyframe(self): "elements": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "points", @@ -3253,27 +3202,31 @@ def test_export_and_import_coco_keypoints_with_outside_without_keyframe(self): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - def test_export_and_import_coco_keypoints_with_one_outside(self): + # check that all the keyframe is imported correctly + assert len(imported_annotations["tracks"][0]["shapes"]) == 2 + + 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_one_outside_source_data.zip") + dataset_file = self.tmp_dir / (format_name + "with_no_keyframe_source_data.zip") annotations = { "shapes": [], "tracks": [ { "frame": 0, + "group": 0, "shapes": [ - {"type": "skeleton", "frame": 3, "points": [], "outside": True}, + {"type": "skeleton", "frame": 0, "points": []}, ], "elements": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "points", - "frame": 3, + "frame": 0, "points": [1.0, 2.0], - "outside": True, }, ], }, @@ -3288,26 +3241,32 @@ def test_export_and_import_coco_keypoints_with_one_outside(self): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - def test_export_and_import_coco_keypoints_with_no_keyframe(self): + # 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_no_keyframe_source_data.zip") + dataset_file = self.tmp_dir / (format_name + "with_one_outside_source_data.zip") annotations = { "shapes": [], "tracks": [ { "frame": 0, + "group": 0, "shapes": [ - {"type": "skeleton", "frame": 0, "points": []}, + {"type": "skeleton", "frame": 3, "points": [], "outside": True}, ], "elements": [ { "frame": 0, + "group": 0, "shapes": [ { "type": "points", - "frame": 0, + "frame": 3, "points": [1.0, 2.0], + "outside": True, }, ], }, @@ -3321,3 +3280,6 @@ def test_export_and_import_coco_keypoints_with_no_keyframe(self): ) 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 From f91c2eb5db049175551d7f0f8060f9656bc673c0 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 17:26:39 +0800 Subject: [PATCH 35/44] add test for outside true --- tests/python/rest_api/test_tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 141522e38a26..01b12e5fa60b 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2911,6 +2911,9 @@ def test_export_and_import_tracked_format_with_outside_true(self, format_name): 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 @@ -3101,6 +3104,9 @@ def test_export_and_import_coco_keypoints_with_outside_true(self): 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" From dcf6b59a699f27c8274d0859edbd0664f763489b Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 17:47:26 +0800 Subject: [PATCH 36/44] check for outside true --- tests/python/rest_api/test_tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 01b12e5fa60b..7629da8e0f34 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -2990,6 +2990,9 @@ def test_export_and_import_tracked_format_with_outside_without_keyframe(self, fo # 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 @@ -3211,6 +3214,9 @@ def test_export_and_import_coco_keypoints_with_outside_without_keyframe(self): # 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" From 18a5b2de4ed53a4a4f1ab885998e7b2eb91b5d19 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Tue, 16 Apr 2024 20:45:00 +0800 Subject: [PATCH 37/44] add more complex tests --- cvat/apps/dataset_manager/bindings.py | 4 +- tests/python/rest_api/test_tasks.py | 134 ++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 4b9d823ab15d..eee0438b1ae3 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2163,9 +2163,9 @@ def _validate_track_shapes(shapes): prev_is_visible = prev_shape is not None and not prev_shape.outside has_gap = False - if not prev_is_visible or shape.keyframe or ( + if not prev_is_visible or ( has_gap := prev_shape.frame + instance_data.frame_step < shape.frame - ): + ) or shape.keyframe: if has_gap: prev_shape = prev_shape._replace(outside=True, keyframe=True, frame=prev_shape.frame + instance_data.frame_step) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 7629da8e0f34..00cf1f63b170 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -3056,6 +3056,67 @@ def test_export_and_import_tracked_format_with_one_outside(self, format_name): # 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" @@ -3295,3 +3356,76 @@ def test_export_and_import_coco_keypoints_with_one_outside(self): # 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" From ac65678fc65302cf26bc253164503ff01ee3fe66 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:00:19 +0800 Subject: [PATCH 38/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index eee0438b1ae3..266a34620f5e 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2156,16 +2156,20 @@ def _validate_track_shapes(shapes): prev_shape = None # infer the keyframe shapes and keep only them for shape in shapes: - cur_is_visible = shape is not None and not shape.outside + prev_is_visible = prev_shape and not prev_shape.outside + cur_is_visible = shape and not shape.outside if not cur_is_visible: + if prev_is_visible: + shape = shape._replace(keyframe=True) + new_shapes.append(shape) prev_shape = shape continue - prev_is_visible = prev_shape is not None and not prev_shape.outside has_gap = False - if not prev_is_visible or ( - has_gap := prev_shape.frame + instance_data.frame_step < shape.frame - ) or shape.keyframe: + if prev_is_visible: + has_gap = prev_shape.frame + instance_data.frame_step < shape.frame + + if not prev_is_visible or has_gap or shape.keyframe: if has_gap: prev_shape = prev_shape._replace(outside=True, keyframe=True, frame=prev_shape.frame + instance_data.frame_step) From 034cd122900226212af400a20e5cb6d877bfacdb Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Thu, 18 Apr 2024 13:55:25 +0800 Subject: [PATCH 39/44] add test for element with gap for coco keypoints --- tests/python/rest_api/test_tasks.py | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 00cf1f63b170..ec9edb95efea 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -3429,3 +3429,171 @@ def test_export_and_import_coco_keypoints_with_gap(self): 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_gap_in_elements(self): + task_id = 14 + format_name = "COCO Keypoints 1.0" + dataset_file = self.tmp_dir / (format_name + "with_gap_in_elements_source_data.zip") + annotations = { + "shapes": [], + "tracks": [ + { + "frame": 0, + "group": 0, + "shapes": [ + {"type": "skeleton", "outside": False, "points": [], "frame": 0}, + {"type": "skeleton", "outside": False, "points": [], "frame": 1}, + {"type": "skeleton", "outside": False, "points": [], "frame": 2}, + {"type": "skeleton", "outside": False, "points": [], "frame": 4}, + {"type": "skeleton", "outside": False, "points": [], "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, + }, + ], + "attributes": [], + }, + { + "frame": 0, + "group": 0, + "source": "manual", + "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, + }, + ], + "attributes": [], + }, + { + "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, + }, + ], + "attributes": [], + }, + ], + } + ], + } + + 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) + + track_outside_count = sum( + 1 for shape in imported_annotations["tracks"][0]["shapes"] if shape["outside"] + ) + assert track_outside_count == 0, "Outside shapes are not imported correctly" + + element_0_outside_count = sum( + 1 + for shape in imported_annotations["tracks"][0]["elements"][0]["shapes"] + if shape["outside"] + ) + assert ( + element_0_outside_count == 1 + ), "Outside shapes for element[0] are not imported correctly" + + element_1_outside_count = sum( + 1 + for shape in imported_annotations["tracks"][0]["elements"][1]["shapes"] + if shape["outside"] + ) + assert ( + element_1_outside_count == 2 + ), "Outside shapes for element[1] are not imported correctly" + + element_2_outside_count = sum( + 1 + for shape in imported_annotations["tracks"][0]["elements"][2]["shapes"] + if shape["outside"] + ) + assert ( + element_2_outside_count == 2 + ), "Outside shapes for element[2] are not imported correctly" From 408b68ba135ba7901d21a2f06b356bfcfdf95e80 Mon Sep 17 00:00:00 2001 From: Yeek Sheng <104289235+Yeek020407@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:06:16 +0800 Subject: [PATCH 40/44] Update cvat/apps/dataset_manager/bindings.py Co-authored-by: Maxim Zhiltsov --- cvat/apps/dataset_manager/bindings.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 266a34620f5e..bfbb1446c04c 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2158,23 +2158,17 @@ def _validate_track_shapes(shapes): for shape in shapes: prev_is_visible = prev_shape and not prev_shape.outside cur_is_visible = shape and not shape.outside - if not cur_is_visible: - if prev_is_visible: - shape = shape._replace(keyframe=True) - new_shapes.append(shape) - prev_shape = shape - continue has_gap = False if prev_is_visible: has_gap = prev_shape.frame + instance_data.frame_step < shape.frame - if not prev_is_visible or has_gap or shape.keyframe: - 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 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) From 8ae6cce884c04f6d18bd94aa20128d1f7e542e40 Mon Sep 17 00:00:00 2001 From: Yeek020407 Date: Thu, 18 Apr 2024 21:36:08 +0800 Subject: [PATCH 41/44] add complex coco annotations test --- tests/python/rest_api/test_tasks.py | 186 +++++++++++++++++++++------- 1 file changed, 143 insertions(+), 43 deletions(-) diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index ec9edb95efea..bbbcc44b7d01 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -3430,10 +3430,10 @@ def test_export_and_import_coco_keypoints_with_gap(self): ) assert outside_count == 2, "Outside shapes are not imported correctly" - def test_export_and_import_coco_keypoints_with_gap_in_elements(self): + 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 + "with_gap_in_elements_source_data.zip") + dataset_file = self.tmp_dir / (format_name + "complex_annotations_source_data.zip") annotations = { "shapes": [], "tracks": [ @@ -3441,11 +3441,11 @@ def test_export_and_import_coco_keypoints_with_gap_in_elements(self): "frame": 0, "group": 0, "shapes": [ - {"type": "skeleton", "outside": False, "points": [], "frame": 0}, - {"type": "skeleton", "outside": False, "points": [], "frame": 1}, - {"type": "skeleton", "outside": False, "points": [], "frame": 2}, - {"type": "skeleton", "outside": False, "points": [], "frame": 4}, - {"type": "skeleton", "outside": False, "points": [], "frame": 5}, + {"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": [ @@ -3484,12 +3484,10 @@ def test_export_and_import_coco_keypoints_with_gap_in_elements(self): "frame": 5, }, ], - "attributes": [], }, { "frame": 0, "group": 0, - "source": "manual", "shapes": [ { "type": "points", @@ -3516,7 +3514,6 @@ def test_export_and_import_coco_keypoints_with_gap_in_elements(self): "frame": 4, }, ], - "attributes": [], }, { "frame": 0, @@ -3553,10 +3550,125 @@ def test_export_and_import_coco_keypoints_with_gap_in_elements(self): "frame": 5, }, ], - "attributes": [], }, ], - } + }, + { + "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, + }, + ], + }, + ], + }, ], } @@ -3566,34 +3678,22 @@ def test_export_and_import_coco_keypoints_with_gap_in_elements(self): self.compare_original_and_import_annotations(original_annotations, imported_annotations) - track_outside_count = sum( - 1 for shape in imported_annotations["tracks"][0]["shapes"] if shape["outside"] - ) - assert track_outside_count == 0, "Outside shapes are not imported correctly" - - element_0_outside_count = sum( - 1 - for shape in imported_annotations["tracks"][0]["elements"][0]["shapes"] - if shape["outside"] - ) - assert ( - element_0_outside_count == 1 - ), "Outside shapes for element[0] are not imported correctly" - - element_1_outside_count = sum( - 1 - for shape in imported_annotations["tracks"][0]["elements"][1]["shapes"] - if shape["outside"] - ) - assert ( - element_1_outside_count == 2 - ), "Outside shapes for element[1] are not imported correctly" - - element_2_outside_count = sum( - 1 - for shape in imported_annotations["tracks"][0]["elements"][2]["shapes"] - if shape["outside"] - ) - assert ( - element_2_outside_count == 2 - ), "Outside shapes for element[2] are not imported correctly" + 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) From f06f560d3371b4fa8bb18b3e5bd62672894dc64f Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 19 Apr 2024 10:36:03 +0300 Subject: [PATCH 42/44] Move shape sorting into the shape validation function --- cvat/apps/dataset_manager/bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index bfbb1446c04c..677bd584ad5a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2152,6 +2152,8 @@ def reduce_fn(acc, v): "#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e 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 @@ -2186,13 +2188,11 @@ def _validate_track_shapes(shapes): stop_frame = int(instance_data.meta[instance_data.META_FIELD]['stop_frame']) for track_id, track in tracks.items(): - track['shapes'].sort(key=lambda t: t.frame) track['shapes'] = _validate_track_shapes(track['shapes']) if ann.type == dm.AnnotationType.skeleton: new_elements = {} for element_id, element in track['elements'].items(): - element.shapes.sort(key=lambda t: t.frame) new_element_shapes = _validate_track_shapes(element.shapes) new_elements[element_id] = instance_data.Track( label=element.label, From d4388c5e189c56770cda4484a555c0f80d7916fd Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 19 Apr 2024 10:36:42 +0300 Subject: [PATCH 43/44] Refactor element shapes update --- cvat/apps/dataset_manager/bindings.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 677bd584ad5a..c7128f2b3f1a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2194,12 +2194,7 @@ def _validate_track_shapes(shapes): new_elements = {} for element_id, element in track['elements'].items(): new_element_shapes = _validate_track_shapes(element.shapes) - new_elements[element_id] = instance_data.Track( - label=element.label, - group=element.group, - source=element.source, - shapes=new_element_shapes, - ) + new_elements[element_id] = element._replace(shapes=new_element_shapes) track['elements'] = new_elements if track['shapes'] or track['elements']: From 525d48905c8d36c5a09d545686586f325c7066b3 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 19 Apr 2024 10:39:15 +0300 Subject: [PATCH 44/44] Remove whitespace --- cvat/apps/dataset_manager/bindings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index c7128f2b3f1a..083c0334f59a 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -2153,7 +2153,6 @@ def reduce_fn(acc, v): 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