Skip to content

Commit 0567afa

Browse files
Merge pull request #9080 from cvat-ai/release-2.29.0
Release v2.29.0
2 parents 5e39e40 + eaa30e4 commit 0567afa

File tree

20 files changed

+682
-48
lines changed

20 files changed

+682
-48
lines changed

CHANGELOG.md

+23
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
<!-- scriv-insert-here -->
1818

19+
<a id='changelog-2.29.0'></a>
20+
## \[2.29.0\] - 2025-02-10
21+
22+
### Added
23+
24+
- Tasks created from cloud storage can be backed up now
25+
(<https://github.com/cvat-ai/cvat/pull/8972>)
26+
27+
- \[CLI\] `function create-native` now sends the function's declared label types
28+
to the server
29+
(<https://github.com/cvat-ai/cvat/pull/9035>)
30+
31+
### Changed
32+
33+
- When invoking Nuclio functions, labels of type `any` can now be mapped to
34+
labels of all types except `skeleton`
35+
(<https://github.com/cvat-ai/cvat/pull/9050>)
36+
37+
### Fixed
38+
39+
- Fixed invalid server-side track interpolation in tasks with deleted frames
40+
(<https://github.com/cvat-ai/cvat/pull/9059>)
41+
1942
<a id='changelog-2.28.0'></a>
2043
## \[2.28.0\] - 2025-02-06
2144

cvat-cli/requirements/base.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cvat-sdk==2.28.0
1+
cvat-sdk==2.29.0
22

33
attrs>=24.2.0
44
Pillow>=10.3.0

cvat-cli/src/cvat_cli/_internal/commands_functions.py

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ def execute(
6262
"name": label_spec.name,
6363
}
6464
)
65+
66+
if getattr(label_spec, "type", "any") != "any":
67+
# Add the type conditionally, to stay compatible with older
68+
# CVAT versions when the function doesn't define label types.
69+
remote_function["labels_v2"][-1]["type"] = label_spec.type
6570
else:
6671
raise cvataa.BadFunctionError(
6772
f"Unsupported function spec type: {type(function.spec).__name__}"

cvat-cli/src/cvat_cli/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "2.28.0"
1+
VERSION = "2.29.0"

cvat-sdk/gen/generate.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ set -e
88

99
GENERATOR_VERSION="v6.0.1"
1010

11-
VERSION="2.28.0"
11+
VERSION="2.29.0"
1212
LIB_NAME="cvat_sdk"
1313
LAYER1_LIB_NAME="${LIB_NAME}/api_client"
1414
DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"

cvat-ui/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cvat-ui",
3-
"version": "2.28.0",
3+
"version": "2.29.0",
44
"description": "CVAT single-page application",
55
"main": "src/index.tsx",
66
"scripts": {

cvat-ui/src/components/model-runner-modal/labels-mapper.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function labelsCompatible(modelLabel: LabelInterface, jobLabel: LabelInterface):
3939
const compatibleTypes = [[LabelType.MASK, LabelType.POLYGON]];
4040
return modelLabelType === jobLabelType ||
4141
(jobLabelType === 'any' && modelLabelType !== LabelType.SKELETON) ||
42-
(modelLabelType === 'unknown' && jobLabelType !== LabelType.SKELETON) || // legacy support
42+
((modelLabelType === 'any' || modelLabelType === 'unknown') && jobLabelType !== LabelType.SKELETON) || // legacy support
4343
compatibleTypes.some((compatible) => compatible.includes(jobLabelType) && compatible.includes(modelLabelType));
4444
}
4545

cvat/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
from cvat.utils.version import get_version
66

7-
VERSION = (2, 28, 0, "final", 0)
7+
VERSION = (2, 29, 0, "final", 0)
88

99
__version__ = get_version(VERSION)

cvat/apps/dataset_manager/annotation.py

+42-12
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,26 @@ def clear_frames(self, frames: Container[int]):
202202
def to_shapes(self,
203203
end_frame: int,
204204
*,
205-
included_frames: Optional[Sequence[int]] = None,
205+
deleted_frames: Sequence[int] | None = None,
206+
included_frames: Sequence[int] | None = None,
206207
include_outside: bool = False,
207-
use_server_track_ids: bool = False
208+
use_server_track_ids: bool = False,
208209
) -> list:
209210
shapes = self.data.shapes
210211
tracks = TrackManager(self.data.tracks, dimension=self.dimension)
211212

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

215-
return shapes + tracks.to_shapes(end_frame,
216-
included_frames=included_frames, include_outside=include_outside,
217-
use_server_track_ids=use_server_track_ids
216+
if deleted_frames is not None:
217+
shapes = [s for s in shapes if s["frame"] not in deleted_frames]
218+
219+
return shapes + tracks.to_shapes(
220+
end_frame,
221+
included_frames=included_frames,
222+
deleted_frames=deleted_frames,
223+
include_outside=include_outside,
224+
use_server_track_ids=use_server_track_ids,
218225
)
219226

220227
def to_tracks(self):
@@ -462,10 +469,14 @@ def _modify_unmatched_object(self, obj, end_frame):
462469

463470

464471
class TrackManager(ObjectManager):
465-
def to_shapes(self, end_frame: int, *,
466-
included_frames: Optional[Sequence[int]] = None,
472+
def to_shapes(
473+
self,
474+
end_frame: int,
475+
*,
476+
included_frames: Sequence[int] | None = None,
477+
deleted_frames: Sequence[int] | None = None,
467478
include_outside: bool = False,
468-
use_server_track_ids: bool = False
479+
use_server_track_ids: bool = False,
469480
) -> list:
470481
shapes = []
471482
for idx, track in enumerate(self.objects):
@@ -479,6 +490,7 @@ def to_shapes(self, end_frame: int, *,
479490
self.dimension,
480491
include_outside=include_outside,
481492
included_frames=included_frames,
493+
deleted_frames=deleted_frames,
482494
):
483495
shape["label_id"] = track["label_id"]
484496
shape["group"] = track["group"]
@@ -498,10 +510,12 @@ def to_shapes(self, end_frame: int, *,
498510
element_included_frames = set(track_shapes.keys())
499511
if included_frames is not None:
500512
element_included_frames = element_included_frames.intersection(included_frames)
501-
element_shapes = track_elements.to_shapes(end_frame,
513+
element_shapes = track_elements.to_shapes(
514+
end_frame,
502515
included_frames=element_included_frames,
516+
deleted_frames=deleted_frames,
503517
include_outside=True, # elements are controlled by the parent shape
504-
use_server_track_ids=use_server_track_ids
518+
use_server_track_ids=use_server_track_ids,
505519
)
506520

507521
for shape in element_shapes:
@@ -588,10 +602,24 @@ def _modify_unmatched_object(self, obj, end_frame):
588602

589603
@staticmethod
590604
def get_interpolated_shapes(
591-
track, start_frame, end_frame, dimension, *,
605+
track: dict,
606+
start_frame: int,
607+
end_frame: int,
608+
dimension: DimensionType | str,
609+
*,
592610
included_frames: Optional[Sequence[int]] = None,
611+
deleted_frames: Optional[Sequence[int]] = None,
593612
include_outside: bool = False,
594613
):
614+
# If a task or job contains deleted frames that contain track keyframes,
615+
# these keyframes should be excluded from the interpolation.
616+
# In jobs having specific frames included (e.g. GT jobs),
617+
# deleted frames should not be confused with included frames during track interpolation.
618+
# Deleted frames affect existing shapes in tracks.
619+
# Included frames filter the resulting annotations after interpolation
620+
# to produce the requested track frames.
621+
deleted_frames = deleted_frames or []
622+
595623
def copy_shape(source, frame, points=None, rotation=None):
596624
copied = source.copy()
597625
copied["attributes"] = faster_deepcopy(source["attributes"])
@@ -930,7 +958,7 @@ def propagate(shape, end_frame, *, included_frames=None):
930958
prev_shape = None
931959
for shape in sorted(track["shapes"], key=lambda shape: shape["frame"]):
932960
curr_frame = shape["frame"]
933-
if included_frames is not None and curr_frame not in included_frames:
961+
if curr_frame in deleted_frames:
934962
continue
935963
if prev_shape and end_frame <= curr_frame:
936964
# If we exceed the end_frame and there was a previous shape,
@@ -982,6 +1010,8 @@ def propagate(shape, end_frame, *, included_frames=None):
9821010
shapes = [
9831011
shape for shape in shapes
9841012

1013+
if shape["frame"] not in deleted_frames
1014+
9851015
# After interpolation there can be a finishing frame
9861016
# outside of the task boundaries. Filter it out to avoid errors.
9871017
# https://github.com/openvinotoolkit/cvat/issues/2827

cvat/apps/dataset_manager/bindings.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,9 @@ def get_frame(idx):
508508
self.stop + 1,
509509
# Skip outside, deleted and excluded frames
510510
included_frames=included_frames,
511+
deleted_frames=self.deleted_frames.keys(),
511512
include_outside=False,
512-
use_server_track_ids=self._use_server_track_ids
513+
use_server_track_ids=self._use_server_track_ids,
513514
),
514515
key=lambda shape: shape.get("z_order", 0)
515516
):
@@ -1307,14 +1308,12 @@ def get_frame(task_id: int, idx: int) -> ProjectData.Frame:
13071308
anno_manager.to_shapes(
13081309
task.data.size,
13091310
included_frames=task_included_frames,
1311+
deleted_frames=task_data.deleted_frames.keys(),
13101312
include_outside=False,
1311-
use_server_track_ids=self._use_server_track_ids
1313+
use_server_track_ids=self._use_server_track_ids,
13121314
),
13131315
key=lambda shape: shape.get("z_order", 0)
13141316
):
1315-
if shape['frame'] in task_data.deleted_frames:
1316-
continue
1317-
13181317
assert (task.id, shape['frame']) in self._frame_info
13191318

13201319
if 'track_id' in shape:

0 commit comments

Comments
 (0)