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

Merged
merged 13 commits into from
Feb 7, 2025
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,
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
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,
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
# 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:
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not task_data.deleted_frames.keys()?

Copy link
Contributor Author

@zhiltsov-max zhiltsov-max Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same. But there is an extra check below, which can be removed now.

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