Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix track interpolation in tasks with deleted frames #9059

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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,4 @@
### Fixed

- Fixed invalid server-side track interpolation in tasks with deleted frames
(<https://github.com/cvat-ai/cvat/pull/9059>)
44 changes: 35 additions & 9 deletions cvat/apps/dataset_manager/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,19 +202,26 @@ def clear_frames(self, frames: Container[int]):
def to_shapes(self,
end_frame: int,
*,
included_frames: Optional[Sequence[int]] = None,
deleted_frames: Sequence[int] | None = None,
included_frames: Sequence[int] | None = None,
include_outside: bool = False,
use_server_track_ids: bool = False
use_server_track_ids: bool = False,
) -> list:
shapes = self.data.shapes
tracks = TrackManager(self.data.tracks, dimension=self.dimension)

if included_frames is not None:
shapes = [s for s in shapes if s["frame"] in included_frames]

return shapes + tracks.to_shapes(end_frame,
included_frames=included_frames, include_outside=include_outside,
use_server_track_ids=use_server_track_ids
if deleted_frames is not None:
shapes = [s for s in shapes if s["frame"] not in deleted_frames]

return shapes + tracks.to_shapes(
end_frame,
included_frames=included_frames,
deleted_frames=deleted_frames,
include_outside=include_outside,
use_server_track_ids=use_server_track_ids,
)

def to_tracks(self):
Expand Down Expand Up @@ -464,6 +471,7 @@ def _modify_unmatched_object(self, obj, end_frame):
class TrackManager(ObjectManager):
def to_shapes(self, end_frame: int, *,
included_frames: Optional[Sequence[int]] = None,
deleted_frames: Optional[Sequence[int]] = None,
include_outside: bool = False,
use_server_track_ids: bool = False
) -> list:
Expand All @@ -479,6 +487,7 @@ def to_shapes(self, end_frame: int, *,
self.dimension,
include_outside=include_outside,
included_frames=included_frames,
deleted_frames=deleted_frames,
):
shape["label_id"] = track["label_id"]
shape["group"] = track["group"]
Expand All @@ -498,10 +507,12 @@ def to_shapes(self, end_frame: int, *,
element_included_frames = set(track_shapes.keys())
if included_frames is not None:
element_included_frames = element_included_frames.intersection(included_frames)
element_shapes = track_elements.to_shapes(end_frame,
element_shapes = track_elements.to_shapes(
end_frame,
included_frames=element_included_frames,
deleted_frames=deleted_frames,
include_outside=True, # elements are controlled by the parent shape
use_server_track_ids=use_server_track_ids
use_server_track_ids=use_server_track_ids,
)

for shape in element_shapes:
Expand Down Expand Up @@ -588,10 +599,23 @@ def _modify_unmatched_object(self, obj, end_frame):

@staticmethod
def get_interpolated_shapes(
track, start_frame, end_frame, dimension, *,
track: dict,
start_frame: int,
end_frame: int,
dimension: DimensionType | str,
*,
included_frames: Optional[Sequence[int]] = None,
deleted_frames: Optional[Sequence[int]] = None,
include_outside: bool = False,
):
# If a task or contains deleted frames that contain track keyframes,
# these keyframes should be excluded from the interpolation.
# In jobs having specific frames included (e.g. GT jobs),
# deleted frames should not be confused with included frames during track interpolation.
# Deleted frames affect existing shapes in tracks.
# Included frames filter the resulting annotations after interpolation
# to produce the requested track frames.

def copy_shape(source, frame, points=None, rotation=None):
copied = source.copy()
copied["attributes"] = faster_deepcopy(source["attributes"])
Expand Down Expand Up @@ -930,7 +954,7 @@ def propagate(shape, end_frame, *, included_frames=None):
prev_shape = None
for shape in sorted(track["shapes"], key=lambda shape: shape["frame"]):
curr_frame = shape["frame"]
if included_frames is not None and curr_frame not in included_frames:
if deleted_frames is not None and curr_frame in deleted_frames:
continue
if prev_shape and end_frame <= curr_frame:
# If we exceed the end_frame and there was a previous shape,
Expand Down Expand Up @@ -982,6 +1006,8 @@ def propagate(shape, end_frame, *, included_frames=None):
shapes = [
shape for shape in shapes

if deleted_frames is None or shape["frame"] not in deleted_frames

# 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
Expand Down
6 changes: 4 additions & 2 deletions cvat/apps/dataset_manager/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,9 @@ def get_frame(idx):
self.stop + 1,
# Skip outside, deleted and excluded frames
included_frames=included_frames,
deleted_frames=self.deleted_frames.keys(),
include_outside=False,
use_server_track_ids=self._use_server_track_ids
use_server_track_ids=self._use_server_track_ids,
),
key=lambda shape: shape.get("z_order", 0)
):
Expand Down Expand Up @@ -1307,8 +1308,9 @@ def get_frame(task_id: int, idx: int) -> ProjectData.Frame:
anno_manager.to_shapes(
task.data.size,
included_frames=task_included_frames,
deleted_frames=set(f for t_id, f in self.deleted_frames if t_id == task.id),
include_outside=False,
use_server_track_ids=self._use_server_track_ids
use_server_track_ids=self._use_server_track_ids,
),
key=lambda shape: shape.get("z_order", 0)
):
Expand Down
Loading
Loading