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

[GSoC2024] Fix import of outside track shapes in Datumaro-based formats #7669

Merged
merged 51 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bfce63b
disable reading of track id
Yeek020407 Mar 23, 2024
099fe0c
Fix switch outside
Yeek020407 Apr 3, 2024
341fd80
Update changelog.d/20240403_135916_yeek020407_switch_outside.md
Yeek020407 Apr 3, 2024
96735a3
Merge branch 'opencv:develop' into switch-outside
Yeek020407 Apr 3, 2024
3c61355
Merge branch 'cvat-ai:develop' into switch-outside
Yeek020407 Apr 5, 2024
a13aa4a
Merge branch 'cvat-ai:develop' into switch-outside
Yeek020407 Apr 7, 2024
11655a7
add test import of outside track shapes
Yeek020407 Apr 7, 2024
946bca3
fix skeleton format bug
Yeek020407 Apr 7, 2024
2ad165f
fixed issues by CI
Yeek020407 Apr 8, 2024
c94b4a6
Update changelog.d/20240403_135916_yeek020407_switch_outside.md
zhiltsov-max Apr 8, 2024
bda889a
add outside attribute when it is coco keypoints
Yeek020407 Apr 9, 2024
fdc0033
Merge branch 'switch-outside' of https://github.com/Yeek020407/cvat i…
Yeek020407 Apr 9, 2024
d2f2ac1
test for coco keypoints
Yeek020407 Apr 9, 2024
bc727aa
added test for coco keypoints with outside true
Yeek020407 Apr 10, 2024
bb71350
use ending_tracks to track last frame with outside true
Yeek020407 Apr 11, 2024
bb77078
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 11, 2024
9a70214
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 11, 2024
8772fbc
change to_boolean to to_bool
Yeek020407 Apr 11, 2024
b6c509e
rearrage tracks into finalized_tracks
Yeek020407 Apr 11, 2024
3ed07a1
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 12, 2024
7000254
remove finalized_tracks
Yeek020407 Apr 12, 2024
fc3a225
Merge branch 'switch-outside' of github.com:Yeek020407/cvat into swit…
Yeek020407 Apr 12, 2024
9232455
remove element_keyframe
Yeek020407 Apr 12, 2024
397b6cc
only add keyframe true to non-outside first shape
Yeek020407 Apr 13, 2024
ff55b20
handle last track with outside true or keyframe true
Yeek020407 Apr 15, 2024
feddc51
minor fix
Yeek020407 Apr 15, 2024
667f2ac
only append shapes with keyframe equals to true
Yeek020407 Apr 15, 2024
67920da
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 15, 2024
37eb05d
refactor code
Yeek020407 Apr 15, 2024
8c5662b
fix minor bug
Yeek020407 Apr 15, 2024
3cd7dcc
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 15, 2024
a8d5c72
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 15, 2024
7a4d8de
add corner tests
Yeek020407 Apr 15, 2024
f9cbe17
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 15, 2024
c471921
remove delete_method before import annotations
Yeek020407 Apr 15, 2024
68ed992
fix bug
Yeek020407 Apr 15, 2024
4e5dcbe
use DeepDiff
Yeek020407 Apr 15, 2024
45ec847
format code
Yeek020407 Apr 15, 2024
9d1651a
more corner tests
Yeek020407 Apr 16, 2024
f91c2eb
add test for outside true
Yeek020407 Apr 16, 2024
dcf6b59
check for outside true
Yeek020407 Apr 16, 2024
18a5b2d
add more complex tests
Yeek020407 Apr 16, 2024
d13b23f
Merge branch 'develop' into switch-outside
zhiltsov-max Apr 17, 2024
ac65678
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 18, 2024
034cd12
add test for element with gap for coco keypoints
Yeek020407 Apr 18, 2024
408b68b
Update cvat/apps/dataset_manager/bindings.py
Yeek020407 Apr 18, 2024
8ae6cce
add complex coco annotations test
Yeek020407 Apr 18, 2024
f06f560
Move shape sorting into the shape validation function
zhiltsov-max Apr 19, 2024
d4388c5
Refactor element shapes update
zhiltsov-max Apr 19, 2024
07ebdd6
Merge branch 'develop' into switch-outside
zhiltsov-max Apr 19, 2024
525d489
Remove whitespace
zhiltsov-max Apr 19, 2024
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
4 changes: 4 additions & 0 deletions changelog.d/20240403_135916_yeek020407_switch_outside.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Formats with the custom `track_id` attribute should import `outside`track shapes properly (e.g. `COCO`, `Datumaro`, `PASCAL VOC`)
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
(<https://github.com/opencv/cvat/pull/7669>)
28 changes: 25 additions & 3 deletions cvat/apps/dataset_manager/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1953,7 +1953,8 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[ProjectDa
'sly_pointcloud',
'coco',
'coco_instances',
'coco_person_keypoints'
'coco_person_keypoints',
'voc'
]

label_cat = dm_dataset.categories()[dm.AnnotationType.label]
Expand Down Expand Up @@ -2080,7 +2081,7 @@ def reduce_fn(acc, v):
))
continue

if keyframe or outside:
if keyframe or not outside:
if track_id not in tracks:
tracks[track_id] = {
'label': label_cat.items[ann.label].name,
Expand All @@ -2105,7 +2106,7 @@ def reduce_fn(acc, v):

tracks[track_id]['shapes'].append(track)

if ann.type == dm.AnnotationType.skeleton:
if ann.type == dm.AnnotationType.skeleton and (keyframe or outside):
for element in ann.elements:
element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool, True)
element_occluded = element.visibility[0] == dm.Points.Visibility.hidden
Expand Down Expand Up @@ -2151,6 +2152,27 @@ def reduce_fn(acc, v):
raise CvatImportError("Image {}: can't import annotation "
"#{} ({}): {}".format(item.id, idx, ann.type.name, e)) from e

for track in tracks.values():
track['shapes'].sort(key=lambda t: t.frame)
prev_shape_idx = 0
prev_shape = track['shapes'][0]
for shape in track['shapes'][1:]:
Yeek020407 marked this conversation as resolved.
Show resolved Hide resolved
has_skip = instance_data.frame_step < shape.frame - prev_shape.frame
if has_skip and not prev_shape.outside:
prev_shape = prev_shape._replace(outside=True,
frame=prev_shape.frame + instance_data.frame_step)
prev_shape_idx += 1
track['shapes'].insert(prev_shape_idx, prev_shape)
prev_shape = shape
prev_shape_idx += 1

Yeek020407 marked this conversation as resolved.
Show resolved Hide resolved
# if the last shape 'outside' is False, we need to add to stop the tracking
if not prev_shape.outside and prev_shape.frame+instance_data.frame_step <= frame_number:
prev_shape = prev_shape._replace(outside=True,
frame=prev_shape.frame + instance_data.frame_step)
prev_shape_idx += 1
track['shapes'].insert(prev_shape_idx, prev_shape)

for track in tracks.values():
track['elements'] = list(track['elements'].values())
instance_data.add_track(instance_data.Track(**track))
Expand Down
46 changes: 46 additions & 0 deletions cvat/apps/dataset_manager/tests/assets/annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,52 @@
],
"tracks": []
},
"Datumaro 1.0 outside true": {
"version": 0,
"tags": [
{
"frame": 0,
"label_id": null,
"group": 0,
"source": "manual",
"attributes": []
}
],
"shapes": [],
"tracks": [
{
"frame": 0,
"label_id": null,
"group": 0,
"source": "manual",
"shapes": [
{
"type": "rectangle",
"occluded": false,
"z_order": 0,
"points": [5.54, 3.5, 19.64, 11.19],
"frame": 0,
"keyframe": true,
"outside": false,
"rotation":0,
"attributes": []
},
{
"type": "rectangle",
"occluded": false,
"z_order": 0,
"points": [5.54, 3.5, 19.64, 11.19],
"frame": 1,
"keyframe": false,
"outside": true,
"rotation":0,
"attributes": []
}
],
"attributes": []
}
]
},
"CVAT for images 1.1 many jobs": {
"version": 0,
"tags": [
Expand Down
41 changes: 41 additions & 0 deletions cvat/apps/dataset_manager/tests/test_rest_api_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,47 @@ def test_api_v2_tasks_annotations_dump_and_upload_many_jobs_with_datumaro(self):
data_from_task_after_upload = self._get_data_from_task(task_id, include_images)
compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload)

def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro_outside_true(self):
zhiltsov-max marked this conversation as resolved.
Show resolved Hide resolved
Yeek020407 marked this conversation as resolved.
Show resolved Hide resolved
test_name = self._testMethodName
import_format = "Datumaro 1.0"
dump_format_name = "Datumaro 1.0 outside true"
include_images_params = (False, True)

for include_images in include_images_params:
with self.subTest(import_format):

# use Datumaro 1.0 annotations that contains outside property with True value
images = self._generate_task_images(3)
task = self._create_task(tasks["main"], images)
self._create_annotations(task, dump_format_name, "default")
task_id = task["id"]
data_from_task_before_upload = self._get_data_from_task(task_id, include_images_params[0])

with TestDir() as test_dir:
# download annotations using track_formats
url = self._generate_url_dump_tasks_annotations(task_id)
file_zip_name = osp.join(test_dir, f'{test_name}_{import_format}.zip')
data = {
"format": import_format,
"action": "download",
}
self._download_file(url, data, self.admin, file_zip_name)
self._check_downloaded_file(file_zip_name)

# remove annotations
self._remove_annotations(url, self.admin)

# upload annotations
upload_format_name = import_format
url = self._generate_url_upload_tasks_annotations(task_id, upload_format_name)
with open(file_zip_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)

# equals annotations
data_from_task_after_upload = self._get_data_from_task(task_id, include_images)
compare_datasets(self, data_from_task_before_upload, data_from_task_after_upload)


def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro(self):
test_name = self._testMethodName
# get formats
Expand Down
94 changes: 94 additions & 0 deletions tests/python/rest_api/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2858,3 +2858,97 @@ def test_can_export_and_import_skeleton_tracks_in_coco_format(self):
for tes in te.shapes
]
)

@pytest.mark.parametrize("format_name", ["Datumaro 1.0", "COCO 1.0", "PASCAL VOC 1.1"])
Yeek020407 marked this conversation as resolved.
Show resolved Hide resolved
def test_export_and_import_tracked_format_with_outside_true(self, format_name):
imageFileNames = [
"1.jpg",
"2.jpg",
"3.jpg",
"4.jpg",
"5.jpg",
]
images = generate_image_files(len(imageFileNames), filenames=imageFileNames)

source_archive_path = self.tmp_dir / "source_data.zip"
with zipfile.ZipFile(source_archive_path, "w") as zip_file:
for image in images:
zip_file.writestr(image.name, image.getvalue())

task = self.client.tasks.create_from_data(
{
"name": "test_tracked_format_with_outside_true_{format_name}",
"labels": [{"name": "cat"}],
},
resources=[source_archive_path],
)

labels = task.get_labels()
task.set_annotations(
models.LabeledDataRequest(
shapes=[
models.LabeledShapeRequest(
frame=0,
label_id=labels[0].id,
type="rectangle",
points=[1, 1, 2, 2],
)
],
tracks=[
models.LabeledTrackRequest(
frame=0,
label_id=labels[0].id,
shapes=[
models.TrackedShapeRequest(
frame=0, type="rectangle", points=[3, 2, 2, 3]
),
models.TrackedShapeRequest(
frame=1, type="rectangle", points=[3, 2, 2, 3]
),
models.TrackedShapeRequest(
frame=2, type="rectangle", points=[3, 2, 2, 3], outside=True
),
],
)
],
)
)

dataset_file = self.tmp_dir / (format_name + "some_file.zip")
task.export_dataset(format_name, dataset_file, include_images=False)

original_annotations = task.get_annotations()
task.remove_annotations()
task.import_annotations(format_name, dataset_file)

imported_annotations = task.get_annotations()

# Number of shapes and tracks hasn't changed
assert len(original_annotations.shapes) == len(imported_annotations.shapes)
assert len(original_annotations.tracks) == len(imported_annotations.tracks)

for i, original_track in enumerate(original_annotations.tracks):
assert len(original_track.shapes) == len(imported_annotations.tracks[i].shapes)

# Frames of shapes, tracks and track elements hasn't changed
assert set([s.frame for s in original_annotations.shapes]) == set(
[s.frame for s in imported_annotations.shapes]
)
assert set([t.frame for t in original_annotations.tracks]) == set(
[t.frame for t in imported_annotations.tracks]
)
assert set(
[
tes.frame
for t in original_annotations.tracks
for te in t.elements
for tes in te.shapes
]
) == set(
[
tes.frame
for t in imported_annotations.tracks
for te in t.elements
for tes in te.shapes
]
)
Loading