diff --git a/CHANGELOG.md b/CHANGELOG.md index b514f5104134..14cfa6c6ae4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed of receiving function variable () - Shortcuts with CAPSLOCK enabled and with non-US languages activated () - Fixed label editor name field validator () +- An error about track shapes outside of the task frames during export () ### Security diff --git a/cvat/apps/dataset_manager/annotation.py b/cvat/apps/dataset_manager/annotation.py index b6dbe1c17482..f10c3af587e1 100644 --- a/cvat/apps/dataset_manager/annotation.py +++ b/cvat/apps/dataset_manager/annotation.py @@ -110,7 +110,7 @@ def filter_track_shapes(shapes): # Track and TrackedShape models don't expect these fields del track['interpolated_shapes'] for shape in segment_shapes: - del shape['keyframe'] + shape.pop('keyframe', None) track['shapes'] = segment_shapes track['frame'] = track['shapes'][0]['frame'] @@ -746,6 +746,10 @@ def interpolate(shape0, shape1): curr_frame = shape["frame"] prev_shape = shape + # keep at least 1 shape + if end_frame <= curr_frame: + break + if not prev_shape["outside"]: shape = copy(prev_shape) shape["frame"] = end_frame diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 38444d4d18bc..a336a00715d5 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -267,6 +267,11 @@ def get_frame(idx): anno_manager = AnnotationManager(self._annotation_ir) for shape in sorted(anno_manager.to_shapes(self._db_task.data.size), key=lambda shape: shape.get("z_order", 0)): + if shape['frame'] not in self._frame_info: + # After interpolation there can be a finishing frame + # outside of the task boundaries. Filter it out to avoid errors. + # https://github.com/openvinotoolkit/cvat/issues/2827 + continue if 'track_id' in shape: if shape['outside']: continue diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index a01e60b78ae7..0c23eea73dbd 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -71,6 +71,13 @@ def _put_api_v1_task_id_annotations(self, tid, data): return response + def _put_api_v1_job_id_annotations(self, jid, data): + with ForceLogin(self.user, self.client): + response = self.client.put("/api/v1/jobs/%s/annotations" % jid, + data=data, format="json") + + return response + def _create_task(self, data, image_data): with ForceLogin(self.user, self.client): response = self.client.post('/api/v1/tasks', data=data, format="json") @@ -87,6 +94,10 @@ def _create_task(self, data, image_data): return task class TaskExportTest(_DbTestBase): + def _generate_custom_annotations(self, annotations, task): + self._put_api_v1_task_id_annotations(task["id"], annotations) + return annotations + def _generate_annotations(self, task): annotations = { "version": 0, @@ -204,8 +215,7 @@ def _generate_annotations(self, task): }, ] } - self._put_api_v1_task_id_annotations(task["id"], annotations) - return annotations + return self._generate_custom_annotations(annotations, task) def _generate_task_images(self, count): # pylint: disable=no-self-use images = { @@ -215,7 +225,7 @@ def _generate_task_images(self, count): # pylint: disable=no-self-use images["image_quality"] = 75 return images - def _generate_task(self, images): + def _generate_task(self, images, **overrides): task = { "name": "my task #1", "overlap": 0, @@ -242,6 +252,7 @@ def _generate_task(self, images): {"name": "person"}, ] } + task.update(overrides) return self._create_task(task, images) @staticmethod @@ -422,6 +433,47 @@ def test_can_make_abs_frame_id_from_known(self): self.assertEqual(5, task_data.abs_frame_id(2)) + def test_frames_outside_are_not_generated(self): + # https://github.com/openvinotoolkit/cvat/issues/2827 + images = self._generate_task_images(10) + images['start_frame'] = 0 + task = self._generate_task(images, overlap=3, segment_size=6) + annotations = { + "version": 0, + "tags": [], + "shapes": [], + "tracks": [ + { + "frame": 6, + "label_id": task["labels"][0]["id"], + "group": None, + "source": "manual", + "attributes": [], + "shapes": [ + { + "frame": 6, + "points": [1.0, 2.1, 100, 300.222], + "type": "rectangle", + "occluded": False, + "outside": False, + "attributes": [], + }, + ] + }, + ] + } + self._put_api_v1_job_id_annotations( + task["segments"][2]["jobs"][0]["id"], annotations) + + task_ann = TaskAnnotation(task["id"]) + task_ann.init_from_db() + task_data = TaskData(task_ann.ir_data, Task.objects.get(pk=task['id'])) + + i = -1 + for i, frame in enumerate(task_data.group_by_frame()): + self.assertTrue(frame.frame in range(6, 10)) + self.assertEqual(i + 1, 4) + class FrameMatchingTest(_DbTestBase): def _generate_task_images(self, paths): # pylint: disable=no-self-use f = BytesIO()