Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Fixed

- Task and project exports could contain extra static copies of tracks
continuing to the end of the task when the same object was tracked in two
overlapping jobs and the track in the later job started earlier in the overlap
(<https://github.com/cvat-ai/cvat/issues/11204>)
12 changes: 12 additions & 0 deletions cvat/apps/dataset_manager/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,21 @@ def merge(self, objects, start_frame, overlap):
if frame in old_objects_by_frame:
int_objects = int_objects_by_frame[frame]
old_objects = old_objects_by_frame[frame]
matched_old_objects = list(old_objects)
new_objects = self._merge_objects_on_one_frame(
int_objects, old_objects, start_frame, overlap
)

# The united object can differ from the old one it was matched with
# (e.g. a track that starts earlier in the next job), so replace it.
united_objects = {
id(old_obj): obj
for old_obj, obj in zip(matched_old_objects, old_objects)
if obj is not old_obj
}
if united_objects:
self.objects[:] = [united_objects.get(id(obj), obj) for obj in self.objects]

self.objects.extend(new_objects)
else:
# We don't have old objects on the frame. Let's add all new ones.
Expand Down
184 changes: 183 additions & 1 deletion cvat/apps/dataset_manager/tests/test_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.test import TestCase

from cvat.apps.dataset_manager import task as task_module
from cvat.apps.dataset_manager.annotation import AnnotationIR, TrackManager
from cvat.apps.dataset_manager.annotation import AnnotationIR, AnnotationManager, TrackManager
from cvat.apps.engine import models
from cvat.apps.engine.models import DimensionType, JobType, ShapeType
from cvat.apps.engine.tests.utils import compare_objects
Expand Down Expand Up @@ -467,6 +467,188 @@ def test_slice_track_does_not_duplicate_outside_frame_on_the_end(self):
self.assertEqual(sliced_annotation.data["tracks"][0]["shapes"], shapes[0:2])


class AnnotationManagerTest(TestCase):
def test_merge_keeps_track_starting_earlier_in_next_job(self):
for dimension in [DimensionType.DIM_2D, DimensionType.DIM_3D]:
with self.subTest(dimension=dimension):
# job 1 covers frames [0; 9], job 2 covers frames [5; 14]
jobs = [
(
0,
[
make_track(
[make_shape(6, dimension=dimension)], frame=6, source="auto"
),
make_track(
[
make_shape(0, base=50, dimension=dimension),
make_shape(2, base=50, outside=True, dimension=dimension),
],
source="semi-auto",
),
],
),
(
5,
[
make_track(
[
make_shape(5, dimension=dimension),
make_shape(13, outside=True, dimension=dimension),
],
frame=5,
)
],
),
]

task_annotations = AnnotationIR(dimension)
for start_frame, tracks in jobs:
job_annotations = AnnotationIR(
dimension, {"tags": [], "shapes": [], "tracks": tracks, "intervals": []}
)
AnnotationManager(task_annotations, dimension=dimension).merge(
job_annotations, start_frame, overlap=5
)

self.assertEqual(
[
(
track["source"],
[(s["frame"], s["outside"]) for s in track["shapes"]],
)
for track in task_annotations.tracks
],
[
("manual", [(5, False), (6, False), (13, True)]),
("semi-auto", [(0, False), (2, True)]),
],
)

exported_frames = [
s["frame"]
for s in AnnotationManager(task_annotations, dimension=dimension).to_shapes(15)
]
self.assertEqual(exported_frames, [0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13])

def test_merge_keeps_replaced_track_reachable(self):
# Each case merges jobs the same way as
# test_merge_keeps_track_starting_earlier_in_next_job, then checks a
# different consequence of replacing the matched track in self.objects.
cases = {
"two tracks matched in the same merge are both replaced": (
[
(
0,
[
make_track([make_shape(6, base=10)], frame=6, source="auto"),
make_track([make_shape(6, base=20)], frame=6, source="auto"),
],
),
(
5,
[
make_track(
[make_shape(5, base=10), make_shape(13, base=10, outside=True)],
frame=5,
),
make_track(
[make_shape(5, base=20), make_shape(13, base=20, outside=True)],
frame=5,
),
],
),
],
[
("manual", [(5, False), (6, False), (13, True)]),
("manual", [(5, False), (6, False), (13, True)]),
],
),
"a track replaced by job 2 is united again by job 3": (
[
(0, [make_track([make_shape(6, base=30)], frame=6, source="auto")]),
(5, [make_track([make_shape(5, base=30), make_shape(12, base=30)], frame=5)]),
(
10,
[
make_track(
[make_shape(11, base=30), make_shape(16, base=30, outside=True)],
frame=11,
)
],
),
],
[("manual", [(5, False), (6, False), (11, False), (12, False), (16, True)])],
),
}
for name, (jobs, expected) in cases.items():
with self.subTest(name=name):
task_annotations = AnnotationIR(DimensionType.DIM_2D)
for start_frame, tracks in jobs:
job_annotations = AnnotationIR(
DimensionType.DIM_2D,
{"tags": [], "shapes": [], "tracks": tracks, "intervals": []},
)
AnnotationManager(task_annotations, dimension=DimensionType.DIM_2D).merge(
job_annotations, start_frame, overlap=5
)
self.assertEqual(
[
(
track["source"],
[(s["frame"], s["outside"]) for s in track["shapes"]],
)
for track in task_annotations.tracks
],
expected,
)

def test_merge_keeps_track_starting_earlier_in_next_job_with_elements(self):
def skeleton(shapes, *, frame, source="manual"):
return {
"frame": frame,
"label_id": 0,
"group": None,
"source": source,
"attributes": [],
"shapes": shapes,
"elements": [
{
"frame": frame,
"label_id": 0,
"group": None,
"source": source,
"attributes": [],
"shapes": [dict(s) for s in shapes],
"elements": [],
}
],
}

job1 = [skeleton([make_shape(6)], frame=6, source="auto")]
job2 = [skeleton([make_shape(5), make_shape(13, outside=True)], frame=5)]

task_annotations = AnnotationIR(DimensionType.DIM_2D)
for start_frame, tracks in [(0, job1), (5, job2)]:
job_annotations = AnnotationIR(
DimensionType.DIM_2D,
{"tags": [], "shapes": [], "tracks": tracks, "intervals": []},
)
AnnotationManager(task_annotations, dimension=DimensionType.DIM_2D).merge(
job_annotations, start_frame, overlap=5
)

track = task_annotations.tracks[0]
self.assertEqual(
[(s["frame"], s["outside"]) for s in track["shapes"]],
[(5, False), (6, False), (13, True)],
)
self.assertEqual(
[(s["frame"], s["outside"]) for s in track["elements"][0]["shapes"]],
[(5, False), (13, True)],
)


class TestTaskAnnotation(TestCase):
def test_reads_ordered_jobs(self):
user = get_user_model().objects.create_superuser(username="admin", email="", password="")
Expand Down
Loading