diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb7951f..e715b7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-ascii paths while adding files from "Connected file share" (issue #4428) - Removed unnecessary volumes defined in docker-compose.serverless.yml () +- Project import with skeletons () ### Security - TDB diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 602fd4d6..1f1177b8 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -524,9 +524,7 @@ class TaskData(InstanceLabelData): ] shape['attributes'] = [self._import_attribute(label_id, attrib, mutable=True) for attrib in shape['attributes'] - if self._get_mutable_attribute_id(label_id, attrib.name) or ( - self.soft_attribute_import and attrib.name not in CVAT_INTERNAL_ATTRIBUTES - ) + if self._get_mutable_attribute_id(label_id, attrib.name) ] shape['points'] = list(map(float, shape['points'])) @@ -1101,9 +1099,9 @@ class CVATDataExtractorMixin: def _read_cvat_anno(self, cvat_frame_anno: Union[ProjectData.Frame, TaskData.Frame], labels: list): categories = self.categories() label_cat = categories[dm.AnnotationType.label] - def map_label(name): return label_cat.find(name)[0] + def map_label(name, parent=''): return label_cat.find(name, parent)[0] label_attrs = { - label['name']: label['attributes'] + label.get('parent', '') + label['name']: label['attributes'] for _, label in labels } @@ -1198,9 +1196,9 @@ class CvatTaskDataExtractor(dm.SourceExtractor, CVATDataExtractorMixin): def _read_cvat_anno(self, cvat_frame_anno: TaskData.Frame, labels: list): categories = self.categories() label_cat = categories[dm.AnnotationType.label] - def map_label(name, parent=""): return label_cat.find(name, parent)[0] + def map_label(name, parent=''): return label_cat.find(name, parent)[0] label_attrs = { - label.get("parent", "") + label['name']: label['attributes'] + label.get('parent', '') + label['name']: label['attributes'] for _, label in labels } @@ -1562,6 +1560,7 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[TaskData, for n, v in ann.attributes.items() ] + points = [] if ann.type in shapes: points = [] if ann.type == dm.AnnotationType.cuboid_3d: @@ -1647,6 +1646,11 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[TaskData, if ann.type == dm.AnnotationType.skeleton: for element in ann.elements: + element_keyframe = dm.util.cast(element.attributes.get('keyframe', None), bool) is True + element_outside = dm.util.cast(element.attributes.pop('outside', None), bool) is True + if not element_keyframe and not element_outside: + continue + if element.label not in tracks[track_id]['elements']: tracks[track_id]['elements'][element.label] = instance_data.Track( label=label_cat.items[element.label].name, @@ -1659,7 +1663,6 @@ def import_dm_annotations(dm_dataset: dm.Dataset, instance_data: Union[TaskData, for n, v in element.attributes.items() ] element_occluded = dm.util.cast(element.attributes.pop('occluded', None), bool) is True - element_outside = dm.util.cast(element.attributes.pop('outside', None), bool) is True element_source = element.attributes.pop('source').lower() \ if element.attributes.get('source', '').lower() in {'auto', 'manual'} else 'manual' tracks[track_id]['elements'][element.label].shapes.append(instance_data.TrackedShape( diff --git a/cvat/apps/dataset_manager/formats/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py index 50d066b0..ede5b67c 100644 --- a/cvat/apps/dataset_manager/formats/cvat.py +++ b/cvat/apps/dataset_manager/formats/cvat.py @@ -14,7 +14,7 @@ from typing import Callable from datumaro.components.annotation import (AnnotationType, Bbox, Label, LabelCategories, Points, Polygon, - PolyLine) + PolyLine, Skeleton) from datumaro.components.dataset import Dataset, DatasetItem from datumaro.components.extractor import (DEFAULT_SUBSET_NAME, Extractor, Importer) @@ -119,23 +119,34 @@ class CvatExtractor(Extractor): items = OrderedDict() track = None + track_element = None + track_shapes = None shape = None + shape_element = None tag = None attributes = None + element_attributes = None image = None subset = None for ev, el in context: if ev == 'start': if el.tag == 'track': - frame_size = tasks_info[int(el.attrib.get('task_id'))]['frame_size'] if el.attrib.get('task_id') else tuple(tasks_info.values())[0]['frame_size'] - track = { - 'id': el.attrib['id'], - 'label': el.attrib.get('label'), - 'group': int(el.attrib.get('group_id', 0)), - 'height': frame_size[0], - 'width': frame_size[1], - } - subset = el.attrib.get('subset') + if track: + track_element = { + 'id': el.attrib['id'], + 'label': el.attrib.get('label'), + } + else: + frame_size = tasks_info[int(el.attrib.get('task_id'))]['frame_size'] if el.attrib.get('task_id') else tuple(tasks_info.values())[0]['frame_size'] + track = { + 'id': el.attrib['id'], + 'label': el.attrib.get('label'), + 'group': int(el.attrib.get('group_id', 0)), + 'height': frame_size[0], + 'width': frame_size[1], + } + subset = el.attrib.get('subset') + track_shapes = {} elif el.tag == 'image': image = { 'name': el.attrib.get('name'), @@ -145,16 +156,28 @@ class CvatExtractor(Extractor): } subset = el.attrib.get('subset') elif el.tag in cls._SUPPORTED_SHAPES and (track or image): - attributes = {} - shape = { - 'type': None, - 'attributes': attributes, - } - if track: - shape.update(track) - shape['track_id'] = int(track['id']) - if image: - shape.update(image) + if shape and shape['type'] == 'skeleton': + element_attributes = {} + shape_element = { + 'type': 'rectangle' if el.tag == 'box' else el.tag, + 'attributes': element_attributes, + } + shape_element.update(image) + else: + attributes = {} + shape = { + 'type': 'rectangle' if el.tag == 'box' else el.tag, + 'attributes': attributes, + } + shape['elements'] = [] + if track_element: + shape.update(track_element) + shape['track_id'] = int(track_element['id']) + elif track: + shape.update(track) + shape['track_id'] = int(track['id']) + if image: + shape.update(image) elif el.tag == 'tag' and image: attributes = {} tag = { @@ -165,7 +188,19 @@ class CvatExtractor(Extractor): } subset = el.attrib.get('subset') elif ev == 'end': - if el.tag == 'attribute' and attributes is not None: + if el.tag == 'attribute' and element_attributes is not None and shape_element is not None: + attr_value = el.text or '' + attr_type = attribute_types.get(el.attrib['name']) + if el.text in ['true', 'false']: + attr_value = attr_value == 'true' + elif attr_type is not None and attr_type != 'text': + try: + attr_value = float(attr_value) + except ValueError: + pass + element_attributes[el.attrib['name']] = attr_value + + if el.tag == 'attribute' and attributes is not None and shape_element is None: attr_value = el.text or '' attr_type = attribute_types.get(el.attrib['name']) if el.text in ['true', 'false']: @@ -176,6 +211,37 @@ class CvatExtractor(Extractor): except ValueError: pass attributes[el.attrib['name']] = attr_value + + elif el.tag in cls._SUPPORTED_SHAPES and shape["type"] == "skeleton" and el.tag != "skeleton": + shape_element['label'] = el.attrib.get('label') + shape_element['group'] = int(el.attrib.get('group_id', 0)) + + shape_element['type'] = el.tag + shape_element['z_order'] = int(el.attrib.get('z_order', 0)) + + if el.tag == 'box': + shape_element['points'] = list(map(float, [ + el.attrib['xtl'], el.attrib['ytl'], + el.attrib['xbr'], el.attrib['ybr'], + ])) + else: + shape_element['points'] = [] + for pair in el.attrib['points'].split(';'): + shape_element['points'].extend(map(float, pair.split(','))) + + if el.tag == 'points' and el.attrib.get('occluded') == '1': + shape_element['visibility'] = [Points.Visibility.hidden] * (len(shape_element['points']) // 2) + else: + shape_element['occluded'] = (el.attrib.get('occluded') == '1') + + if el.tag == 'points' and el.attrib.get('outside') == '1': + shape_element['visibility'] = [Points.Visibility.absent] * (len(shape_element['points']) // 2) + else: + shape_element['outside'] = (el.attrib.get('outside') == '1') + + shape['elements'].append(shape_element) + shape_element = None + elif el.tag in cls._SUPPORTED_SHAPES: if track is not None: shape['frame'] = el.attrib['frame'] @@ -194,15 +260,22 @@ class CvatExtractor(Extractor): el.attrib['xtl'], el.attrib['ytl'], el.attrib['xbr'], el.attrib['ybr'], ])) + elif el.tag == 'skeleton': + shape['points'] = [] else: shape['points'] = [] for pair in el.attrib['points'].split(';'): shape['points'].extend(map(float, pair.split(','))) + if track_element: + track_shapes[shape['frame']]['elements'].append(shape) + elif track: + track_shapes[shape['frame']] = shape + else: + frame_desc = items.get((subset, shape['frame']), {'annotations': []}) + frame_desc['annotations'].append( + cls._parse_shape_ann(shape, categories)) + items[(subset, shape['frame'])] = frame_desc - frame_desc = items.get((subset, shape['frame']), {'annotations': []}) - frame_desc['annotations'].append( - cls._parse_shape_ann(shape, categories)) - items[(subset, shape['frame'])] = frame_desc shape = None elif el.tag == 'tag': @@ -212,7 +285,15 @@ class CvatExtractor(Extractor): items[(subset, tag['frame'])] = frame_desc tag = None elif el.tag == 'track': - track = None + if track_element: + track_element = None + else: + for track_shape in track_shapes.values(): + frame_desc = items.get((subset, track_shape['frame']), {'annotations': []}) + frame_desc['annotations'].append( + cls._parse_shape_ann(track_shape, categories)) + items[(subset, track_shape['frame'])] = frame_desc + track = None elif el.tag == 'image': frame_desc = items.get((subset, image['frame']), {'annotations': []}) frame_desc.update({ @@ -377,7 +458,8 @@ class CvatExtractor(Extractor): id=ann_id, attributes=attributes, group=group) elif ann_type == 'points': - return Points(points, label=label_id, z_order=z_order, + visibility = ann.get('visibility', None) + return Points(points, visibility, label=label_id, z_order=z_order, id=ann_id, attributes=attributes, group=group) elif ann_type == 'box': @@ -386,6 +468,14 @@ class CvatExtractor(Extractor): return Bbox(x, y, w, h, label=label_id, z_order=z_order, id=ann_id, attributes=attributes, group=group) + elif ann_type == 'skeleton': + elements = [] + for element in ann.get('elements', []): + elements.append(cls._parse_shape_ann(element, categories)) + + return Skeleton(elements, label=label_id, z_order=z_order, + id=ann_id, attributes=attributes, group=group) + else: raise NotImplementedError("Unknown annotation type '%s'" % ann_type) @@ -963,7 +1053,10 @@ def dump_as_cvat_interpolation(dumper, annotations): elements=[], ) for element in shape.elements] } - if isinstance(annotations, ProjectData): track['task_id'] = shape.task_id + if isinstance(annotations, ProjectData): + track['task_id'] = shape.task_id + for element in track['elements']: + element.task_id = shape.task_id dump_track(counter, annotations.Track(**track)) counter += 1 diff --git a/cvat/apps/dataset_manager/tests/assets/annotations.json b/cvat/apps/dataset_manager/tests/assets/annotations.json index cdd9f983..e92ed404 100644 --- a/cvat/apps/dataset_manager/tests/assets/annotations.json +++ b/cvat/apps/dataset_manager/tests/assets/annotations.json @@ -708,43 +708,6 @@ ], "tracks": [] }, - "WiderFace 1.0": { - "version": 0, - "tags": [ - { - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "attributes": [] - } - ], - "shapes": [ - { - "type": "rectangle", - "occluded": false, - "z_order": 0, - "points": [7.55, 9.75, 16.44, 15.85], - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "attributes": [] - }, - { - "type": "rectangle", - "occluded": true, - "z_order": 0, - "points": [3.55, 27.75, 11.33, 33.71], - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "attributes": [] - } - ], - "tracks": [] - }, "VGGFace2 1.0": { "version": 0, "tags": [], @@ -1085,7 +1048,6 @@ "points": [66.45, 147.08, 182.16, 204.56], "frame": 0, "outside": false, - "outside": true, "attributes": [] }, { @@ -1215,50 +1177,6 @@ } ] }, - "CVAT for video 1.1 polygon": { - "version": 0, - "tags": [], - "shapes": [], - "tracks": [ - { - "frame": 0, - "label_id": null, - "group": 1, - "source": "manual", - "shapes": [ - { - "type": "polygon", - "occluded": false, - "z_order": 0, - "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], - "frame": 0, - "outside": false, - "attributes": [] - }, - { - "type": "polygon", - "occluded": false, - "z_order": 0, - "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], - "frame": 1, - "outside": true, - "attributes": [] - }, - { - "type": "polygon", - "occluded": false, - "z_order": 0, - "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], - "frame": 2, - "outside": false, - "keyframe": true, - "attributes": [] - } - ], - "attributes": [] - } - ] - }, "CVAT for video 1.1 attributes in tracks": { "version": 0, "tags": [], diff --git a/cvat/apps/engine/backup.py b/cvat/apps/engine/backup.py index 5cbcb327..b7661d8c 100644 --- a/cvat/apps/engine/backup.py +++ b/cvat/apps/engine/backup.py @@ -173,34 +173,35 @@ class _TaskBackupBase(_BackupBase): source, dest = attribute.pop('spec_id'), 'name' attribute[dest] = label_mapping[label]['attributes'][source] - def _update_label(shape): + def _update_label(shape, parent_label=''): if 'label_id' in shape: - source, dest = shape.pop('label_id'), 'label' + source = shape.pop('label_id') + shape['label'] = label_mapping[source]['value'] elif 'label' in shape: - source, dest = shape.pop('label'), 'label_id' - shape[dest] = label_mapping[source]['value'] + source = parent_label + shape.pop('label') + shape['label_id'] = label_mapping[source]['value'] return source - def _prepare_shapes(shapes): + def _prepare_shapes(shapes, parent_label=''): for shape in shapes: - label = _update_label(shape) + label = _update_label(shape, parent_label) for attr in shape['attributes']: _update_attribute(attr, label) - _prepare_shapes(shape.get('elements', [])) + _prepare_shapes(shape.get('elements', []), label) self._prepare_meta(allowed_fields, shape) - def _prepare_tracks(tracks): + def _prepare_tracks(tracks, parent_label=''): for track in tracks: - label = _update_label(track) + label = _update_label(track, parent_label) for shape in track['shapes']: for attr in shape['attributes']: _update_attribute(attr, label) self._prepare_meta(allowed_fields, shape) - _prepare_tracks(track.get('elements', [])) + _prepare_tracks(track.get('elements', []), label) for attr in track['attributes']: _update_attribute(attr, label) @@ -427,7 +428,7 @@ class _ImporterBase(): sublabels = label.pop('sublabels', []) db_label = models.Label.objects.create(**label_relation, parent=parent_label, **label) - label_mapping[label_name] = { + label_mapping[(parent_label.name if parent_label else '') + label_name] = { 'value': db_label.id, 'attributes': {}, } @@ -444,7 +445,7 @@ class _ImporterBase(): attribute_serializer = AttributeSerializer(data=attribute) attribute_serializer.is_valid(raise_exception=True) db_attribute = attribute_serializer.save(label=db_label) - label_mapping[label_name]['attributes'][attribute_name] = db_attribute.id + label_mapping[(parent_label.name if parent_label else '') + label_name]['attributes'][attribute_name] = db_attribute.id return label_mapping diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 103f9a25..f81991e3 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -816,12 +816,13 @@ class ProjectWriteSerializer(serializers.ModelSerializer): sublabels = label.pop('sublabels', []) svg = label.pop('svg', '') db_label = LabelSerializer.update_instance(label, instance, parent_label) - update_labels(sublabels, parent_label=db_label) + if not label.get('deleted'): + update_labels(sublabels, parent_label=db_label) - if label.get('id') is None and db_label.type == str(models.LabelType.SKELETON): - for db_sublabel in list(db_label.sublabels.all()): - svg = svg.replace(f'data-label-name="{db_sublabel.name}"', f'data-label-id="{db_sublabel.id}"') - models.Skeleton.objects.create(root=db_label, svg=svg) + if label.get('id') is None and db_label.type == str(models.LabelType.SKELETON): + for db_sublabel in list(db_label.sublabels.all()): + svg = svg.replace(f'data-label-name="{db_sublabel.name}"', f'data-label-id="{db_sublabel.id}"') + models.Skeleton.objects.create(root=db_label, svg=svg) update_labels(labels) diff --git a/site/content/en/docs/manual/advanced/formats/format-cvat.md b/site/content/en/docs/manual/advanced/formats/format-cvat.md index 0d9ab7da..f8c28286 100644 --- a/site/content/en/docs/manual/advanced/formats/format-cvat.md +++ b/site/content/en/docs/manual/advanced/formats/format-cvat.md @@ -9,10 +9,10 @@ This is the native CVAT annotation format. It supports all CVAT annotations features, so it can be used to make data backups. - supported annotations CVAT for Images: Rectangles, Polygons, Polylines, - Points, Cuboids, Tags, Tracks + Points, Cuboids, Skeletons, Tags, Tracks - supported annotations CVAT for Videos: Rectangles, Polygons, Polylines, - Points, Cuboids, Tracks + Points, Cuboids, Skeletons, Tracks - attributes are supported diff --git a/site/content/en/docs/manual/advanced/xml_format.md b/site/content/en/docs/manual/advanced/xml_format.md index 36abb409..97a7ddd0 100644 --- a/site/content/en/docs/manual/advanced/xml_format.md +++ b/site/content/en/docs/manual/advanced/xml_format.md @@ -38,6 +38,7 @@ In annotation mode each image tag has `width` and `height` attributes for the sa @@ -81,7 +84,7 @@ On each image it is possible to have many different objects. Each object can hav If an annotation task is created with `z_order` flag then each object will have `z_order` attribute which is used to draw objects properly when they are intersected (if `z_order` is bigger the object is closer to camera). In previous versions of the format only `box` shape was available. -In later releases `polygon`, `polyline`, `points` and `tags` were added. Please see below for more details: +In later releases `polygon`, `polyline`, `points`, `skeletons` and `tags` were added. Please see below for more details: ```xml @@ -117,6 +120,14 @@ In later releases `polygon`, `polyline`, `points` and `tags` were added. Please String: the attribute value ... + + + String: the attribute value + + ... + String: the attribute value + ... + ... ... @@ -161,6 +172,34 @@ Example: + + + + @@ -190,6 +229,14 @@ Example: + + + + + + + + ``` @@ -206,7 +253,7 @@ cloned for each location (a known redundancy). ... - + String: the attribute value ... @@ -222,6 +269,17 @@ cloned for each location (a known redundancy). ... + + + + ... + + + + ... + + ... + ... ``` @@ -254,6 +312,34 @@ Example: + + + + @@ -288,5 +374,61 @@ Example: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ```