Skip to content

fixing problems with skeleton tracks with missing points #8377

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

Merged
merged 13 commits into from
Sep 5, 2024
2 changes: 1 addition & 1 deletion cvat-core/src/annotations-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2918,7 +2918,7 @@ export class SkeletonTrack extends Track {
parentID: this.clientID,
readOnlyFields: ['group', 'zOrder', 'source', 'rotation'],
});
}).sort((a: Annotation, b: Annotation) => a.label.id - b.label.id);
}).filter(Boolean).sort((a: Annotation, b: Annotation) => a.label.id - b.label.id);
}

public updateFromServerResponse(body: SerializedTrack): void {
Expand Down
4 changes: 3 additions & 1 deletion cvat/apps/dataset_manager/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def has_overlap(a, b):

prev_shape = shape

if not prev_shape['outside'] and prev_shape['frame'] <= stop:
if prev_shape is not None and not prev_shape['outside'] and prev_shape['frame'] <= stop:
return True

return False
Expand Down Expand Up @@ -552,6 +552,8 @@ def _calc_objects_similarity(obj0, obj1, start_frame, overlap, dimension):
return 0

def _modify_unmatched_object(self, obj, end_frame):
if not obj["shapes"]:
return
shape = obj["shapes"][-1]
if not shape["outside"]:
shape = deepcopy(shape)
Expand Down
44 changes: 44 additions & 0 deletions cvat/apps/dataset_manager/tests/assets/annotations.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,50 @@
],
"tracks": []
},
"many jobs skeleton tracks with missing shapes": {
"version": 0,
"tags": [],
"shapes": [],
"tracks": [
{
"frame": 10,
"group": 0,
"source": "file",
"shapes": [
{
"type": "skeleton",
"occluded": false,
"outside": false,
"z_order": 0,
"rotation": 0,
"points": [],
"frame": 10,
"attributes": []
}
],
"attributes": [],
"elements": [
{
"frame": 10,
"group": 0,
"source": "file",
"shapes": [],
"attributes": [],
"label_id": null
},
{
"frame": 10,
"group": 0,
"source": "file",
"shapes": [],
"attributes": [],
"label_id": null
}
],
"label_id": null
}
]
},
"ICDAR Localization 1.0": {
"version": 0,
"tags": [],
Expand Down
41 changes: 41 additions & 0 deletions cvat/apps/dataset_manager/tests/assets/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,47 @@
}
]
},
"many jobs skeleton": {
"name": "many jobs",
"overlap": 5,
"segment_size": 10,
"labels": [
{
"name": "skeleton",
"color": "#2080c0",
"type": "skeleton",
"attributes": [
{
"name": "attr",
"mutable": false,
"input_type": "select",
"values": ["1", "2", "3"]
}
],
"sublabels": [
{
"name": "1",
"color": "#d12345",
"attributes": [],
"type": "points"
},
{
"name": "2",
"color": "#350dea",
"attributes": [],
"type": "points"
},
{
"name": "3",
"color": "#479ffe",
"attributes": [],
"type": "points"
}
],
"svg": "<line x1=\"38.92810821533203\" y1=\"53.31378173828125\" x2=\"80.23341369628906\" y2=\"18.36313819885254\" stroke=\"black\" data-type=\"edge\" data-node-from=\"2\" stroke-width=\"0.5\" data-node-to=\"3\"></line><line x1=\"30.399484634399414\" y1=\"32.74474334716797\" x2=\"38.92810821533203\" y2=\"53.31378173828125\" stroke=\"black\" data-type=\"edge\" data-node-from=\"1\" stroke-width=\"0.5\" data-node-to=\"2\"></line><circle r=\"1.5\" stroke=\"black\" fill=\"#b3b3b3\" cx=\"30.399484634399414\" cy=\"32.74474334716797\" stroke-width=\"0.1\" data-type=\"element node\" data-element-id=\"1\" data-node-id=\"1\" data-label-name=\"1\"></circle><circle r=\"1.5\" stroke=\"black\" fill=\"#b3b3b3\" cx=\"38.92810821533203\" cy=\"53.31378173828125\" stroke-width=\"0.1\" data-type=\"element node\" data-element-id=\"2\" data-node-id=\"2\" data-label-name=\"2\"></circle><circle r=\"1.5\" stroke=\"black\" fill=\"#b3b3b3\" cx=\"80.23341369628906\" cy=\"18.36313819885254\" stroke-width=\"0.1\" data-type=\"element node\" data-element-id=\"3\" data-node-id=\"3\" data-label-name=\"3\"></circle>"
}
]
},
"change overlap and segment size": {
"name": "change overlap and segment size",
"overlap": 3,
Expand Down
186 changes: 90 additions & 96 deletions cvat/apps/dataset_manager/tests/test_rest_api_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,109 +231,64 @@ def _delete_request(self, path, user):
response = self.client.delete(path)
return response

def _create_annotations(self, task, name_ann, key_get_values):
tmp_annotations = copy.deepcopy(annotations[name_ann])
@staticmethod
def _make_annotations_for_task(task, name_ann, key_get_values):
assert key_get_values in ["default", "random"]

def make_attribute_value(attribute, attribute_spec):
value = attribute["default_value"]
if key_get_values == "random":
if attribute["input_type"] == "number":
start = int(attribute["values"][0])
stop = int(attribute["values"][1]) + 1
step = int(attribute["values"][2])
value = str(random.randrange(start, stop, step))
else:
value = random.choice(attribute_spec["values"]) # nosec B311 NOSONAR
return value

def fill_element_attributes(element, label):
element["label_id"] = label["id"]

for index_attribute, attribute in enumerate(label["attributes"]):
spec_id = label["attributes"][index_attribute]["id"]

value = make_attribute_value(attribute, label["attributes"][index_attribute])

if item == "tracks" and attribute["mutable"]:
for index_shape, _ in enumerate(element["shapes"]):
element["shapes"][index_shape]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
else:
element["attributes"].append({
"spec_id": spec_id,
"value": value,
})

tmp_annotations = copy.deepcopy(annotations[name_ann])
# change attributes in all annotations
for item in tmp_annotations:
if item in ["tags", "shapes", "tracks"]:
for index_elem, _ in enumerate(tmp_annotations[item]):
tmp_annotations[item][index_elem]["label_id"] = task["labels"][0]["id"]

for index_attribute, attribute in enumerate(task["labels"][0]["attributes"]):
spec_id = task["labels"][0]["attributes"][index_attribute]["id"]

if key_get_values == "random":
if attribute["input_type"] == "number":
start = int(attribute["values"][0])
stop = int(attribute["values"][1]) + 1
step = int(attribute["values"][2])
value = str(random.randrange(start, stop, step))
else:
value = random.choice(task["labels"][0]["attributes"][index_attribute]["values"])
elif key_get_values == "default":
value = attribute["default_value"]

if item == "tracks" and attribute["mutable"]:
for index_shape, _ in enumerate(tmp_annotations[item][index_elem]["shapes"]):
tmp_annotations[item][index_elem]["shapes"][index_shape]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
else:
tmp_annotations[item][index_elem]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
elements = tmp_annotations[item][index_elem].get("elements", [])
labels = task["labels"][0].get("sublabels", [])
for element, label in zip(elements, labels):
element["label_id"] = label["id"]

for index_attribute, attribute in enumerate(label["attributes"]):
spec_id = label["attributes"][index_attribute]["id"]

if key_get_values == "random":
if attribute["input_type"] == "number":
start = int(attribute["values"][0])
stop = int(attribute["values"][1]) + 1
step = int(attribute["values"][2])
value = str(random.randrange(start, stop, step))
else:
value = random.choice(label["attributes"][index_attribute]["values"])
elif key_get_values == "default":
value = attribute["default_value"]

if item == "tracks" and attribute["mutable"]:
for index_shape, _ in enumerate(element["shapes"]):
element["shapes"][index_shape]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
else:
element["attributes"].append({
"spec_id": spec_id,
"value": value,
})
for item in ["tags", "shapes", "tracks"]:
for element in tmp_annotations.get(item, []):
fill_element_attributes(element, task["labels"][0])

sub_elements = element.get("elements", [])
labels = task["labels"][0].get("sublabels", [])
for sub_element, sub_label in zip(sub_elements, labels):
fill_element_attributes(sub_element, sub_label)

return tmp_annotations

def _create_annotations(self, task, name_ann, key_get_values):
tmp_annotations = self._make_annotations_for_task(task, name_ann, key_get_values)
response = self._put_api_v2_task_id_annotations(task["id"], tmp_annotations)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def _create_annotations_in_job(self, task, job_id, name_ann, key_get_values):
tmp_annotations = copy.deepcopy(annotations[name_ann])

# change attributes in all annotations
for item in tmp_annotations:
if item in ["tags", "shapes", "tracks"]:
for index_elem, _ in enumerate(tmp_annotations[item]):
tmp_annotations[item][index_elem]["label_id"] = task["labels"][0]["id"]

for index_attribute, attribute in enumerate(task["labels"][0]["attributes"]):
spec_id = task["labels"][0]["attributes"][index_attribute]["id"]

if key_get_values == "random":
if attribute["input_type"] == "number":
start = int(attribute["values"][0])
stop = int(attribute["values"][1]) + 1
step = int(attribute["values"][2])
value = str(random.randrange(start, stop, step))
else:
value = random.choice(task["labels"][0]["attributes"][index_attribute]["values"])
elif key_get_values == "default":
value = attribute["default_value"]

if item == "tracks" and attribute["mutable"]:
for index_shape, _ in enumerate(tmp_annotations[item][index_elem]["shapes"]):
tmp_annotations[item][index_elem]["shapes"][index_shape]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
else:
tmp_annotations[item][index_elem]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
tmp_annotations = self._make_annotations_for_task(task, name_ann, key_get_values)
response = self._put_api_v2_job_id_annotations(job_id, tmp_annotations)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_200_OK, msg=response.json())

def _download_file(self, url, data, user, file_name):
response = self._get_request_with_data(url, data, user)
Expand Down Expand Up @@ -1285,6 +1240,45 @@ def test_api_v2_check_attribute_import_in_tracks(self):
data_from_task_after_upload = self._get_data_from_task(task_id, include_images)
compare_datasets(data_from_task_before_upload, data_from_task_after_upload)

def test_api_v2_check_skeleton_tracks_with_missing_shapes(self):
test_name = self._testMethodName
format_name = "COCO Keypoints 1.0"
name_ann = 'many jobs skeleton tracks with missing shapes'

# create task with annotations
for whole_task in (False, True):
with self.subTest():
images = self._generate_task_images(25)
task = self._create_task(tasks['many jobs skeleton'], images)
task_id = task["id"]

if whole_task:
self._create_annotations(task, name_ann, "default")
else:
job_id = next(
job["id"]
for job in self._get_jobs(task_id)
if job["start_frame"] == annotations[name_ann]["tracks"][0]["frame"]
)
self._create_annotations_in_job(task, job_id, name_ann, "default")

# dump annotations
url = self._generate_url_dump_tasks_annotations(task_id)
data = {"format": format_name}
with TestDir() as test_dir:
file_zip_name = osp.join(test_dir, f'{test_name}_{format_name}.zip')
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
url = self._generate_url_upload_tasks_annotations(task_id, format_name)
with open(file_zip_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)


class ExportBehaviorTest(_DbTestBase):
@define
class SharedBase:
Expand Down
Loading