diff --git a/changelog.d/20231010_113810_sizow.k.d_fix_tracks_splitting.md b/changelog.d/20231010_113810_sizow.k.d_fix_tracks_splitting.md new file mode 100644 index 000000000000..4dd4a1e2d07e --- /dev/null +++ b/changelog.d/20231010_113810_sizow.k.d_fix_tracks_splitting.md @@ -0,0 +1,4 @@ +### Fixed + +- Splitting skeleton tracks on jobs + () diff --git a/cvat/apps/dataset_manager/annotation.py b/cvat/apps/dataset_manager/annotation.py index e13a4349e6d7..2cb8a5a2f8b1 100644 --- a/cvat/apps/dataset_manager/annotation.py +++ b/cvat/apps/dataset_manager/annotation.py @@ -100,9 +100,12 @@ def filter_track_shapes(shapes): track = deepcopy(track_) segment_shapes = filter_track_shapes(deepcopy(track['shapes'])) + track["elements"] = [ + cls._slice_track(element, start, stop, dimension) + for element in track.get('elements', []) + ] + if len(segment_shapes) < len(track['shapes']): - for element in track.get('elements', []): - element = cls._slice_track(element, start, stop, dimension) interpolated_shapes = TrackManager.get_interpolated_shapes( track, start, stop, dimension) scoped_shapes = filter_track_shapes(interpolated_shapes) @@ -909,7 +912,7 @@ def propagate(shape, end_frame, *, included_frames=None): break # The track finishes here if prev_shape: - assert curr_frame > prev_shape["frame"] # Catch invalid tracks + assert curr_frame > prev_shape["frame"], f"{curr_frame} > {prev_shape['frame']}. Track id: {track['id']}" # Catch invalid tracks # Propagate attributes for attr in prev_shape["attributes"]: diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 81a49d6a26ff..01f4214e897a 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -161,7 +161,7 @@ def _add_missing_shape(self, track, first_shape): missing_shape = deepcopy(first_shape) missing_shape["frame"] = track["frame"] missing_shape["outside"] = True - missing_shape.pop("id") + missing_shape.pop("id", None) track["shapes"].append(missing_shape) def _correct_frame_of_tracked_shapes(self, track): diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 9d646565f3e2..228470e0a085 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -34,6 +34,7 @@ from shared.utils.config import ( BASE_URL, USER_PASS, + delete_method, get_method, make_api_client, patch_method, @@ -534,6 +535,63 @@ def test_remove_first_keyframe(self): response = patch_method("admin1", endpoint, annotations, action="update") assert response.status_code == HTTPStatus.OK + def test_can_split_skeleton_tracks_on_jobs(self, jobs): + # https://github.com/opencv/cvat/pull/6968 + task_id = 21 + + task_jobs = [job for job in jobs if job["task_id"] == task_id] + + frame_ranges = {} + for job in task_jobs: + frame_ranges[job["id"]] = set(range(job["start_frame"], job["stop_frame"] + 1)) + + # skeleton track that covers few jobs + annotations = { + "tracks": [ + { + "frame": 0, + "label_id": 58, + "shapes": [{"type": "skeleton", "frame": 0, "points": []}], + "elements": [ + { + "label_id": 59, + "frame": 0, + "shapes": [ + {"type": "points", "frame": 0, "points": [1.0, 2.0]}, + {"type": "points", "frame": 2, "points": [1.0, 2.0]}, + {"type": "points", "frame": 7, "points": [1.0, 2.0]}, + ], + }, + ], + } + ] + } + + # clear task annotations + response = delete_method("admin1", f"tasks/{task_id}/annotations") + assert response.status_code == 204, f"Cannot delete task's annotations: {response.content}" + + # create skeleton track that covers few jobs + 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}" + + # check that server splitted skeleton track's elements on jobs correctly + for job_id, job_frame_range in frame_ranges.items(): + response = get_method("admin1", f"jobs/{job_id}/annotations") + assert response.status_code == 200, f"Cannot get job's annotations: {response.content}" + + job_annotations = response.json() + assert len(job_annotations["tracks"]) == 1, "Expected to see only one track" + + track = job_annotations["tracks"][0] + assert track.get("elements", []), "Expected to see track with elements" + + for element in track["elements"]: + element_frames = set(shape["frame"] for shape in element["shapes"]) + assert element_frames <= job_frame_range, "Track shapes get out of job frame range" + @pytest.mark.usefixtures("restore_db_per_class") class TestGetTaskDataset: