diff --git a/.travis.yml b/.travis.yml index fd76331dae73..9875fc6e3150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,6 @@ before_script: script: - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'python3 manage.py test cvat/apps utils/cli' + - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'python3 manage.py test --pattern="_tests.py" cvat/apps/dataset_manager' - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'python3 manage.py test datumaro/' - docker-compose -f docker-compose.yml -f docker-compose.ci.yml run cvat_ci /bin/bash -c 'cd cvat-data && npm install && cd ../cvat-core && npm install && npm run test && npm run coveralls' diff --git a/cvat/apps/annotation/README.md b/cvat/apps/annotation/README.md index 279d7a4d6068..101a28ac2f26 100644 --- a/cvat/apps/annotation/README.md +++ b/cvat/apps/annotation/README.md @@ -1,3 +1,5 @@ + + ## Description The purpose of this application is to add support for multiple annotation formats for CVAT. @@ -525,10 +527,10 @@ python create_pascal_tf_record.py --data_dir --set train --y │   └── Segmentation/ │   └── default.txt # list of image names without extension ├── SegmentationClass/ # merged class masks - │   └── image1.png + │   ├── image1.png │   └── image2.png └── SegmentationObject/ # merged instance masks - └── image1.png + ├── image1.png └── image2.png ``` Mask is a png image with several (RGB) channels where each pixel has own color which corresponds to a label. @@ -557,11 +559,70 @@ python create_pascal_tf_record.py --data_dir --set train --y │   └── Segmentation/ │   └── .txt ├── SegmentationClass/ - │   └── image1.png + │   ├── image1.png │   └── image2.png └── SegmentationObject/ - └── image.png + ├── image1.png └── image2.png ``` - supported shapes: Polygons -- additional comments: the CVAT task should be created with the full label set that may be in the annotation files \ No newline at end of file +- additional comments: the CVAT task should be created with the full label set that may be in the annotation files + +### [MOT sequence](https://arxiv.org/pdf/1906.04567.pdf) +#### Dumper +- downloaded file: a zip archive of the following structure: + ```bash + taskname.zip/ + ├── img1/ + | ├── imgage1.jpg + | └── imgage2.jpg + └── gt/ + ├── labels.txt + └── gt.txt + + # labels.txt + cat + dog + person + ... + + # gt.txt + # frame_id, track_id, x, y, w, h, "not ignored", class_id, visibility, + 1,1,1363,569,103,241,1,1,0.86014 + ... + + ``` +- supported annotations: Rectangle shapes and tracks +- supported attributes: `visibility` (number), `ignored` (checkbox) + +#### Loader +- uploaded file: a zip archive of the structure above or: + ```bash + taskname.zip/ + ├── labels.txt # optional, mandatory for non-official labels + └── gt.txt + ``` +- supported annotations: Rectangle tracks + +### [LabelMe](http://labelme.csail.mit.edu/Release3.0) +#### Dumper +- downloaded file: a zip archive of the following structure: + ```bash + taskname.zip/ + ├── img1.jpg + └── img1.xml + ``` +- supported annotations: Rectangles, Polygons (with attributes) + +#### Loader +- uploaded file: a zip archive of the following structure: + ```bash + taskname.zip/ + ├── Masks/ + | ├── img1_mask1.png + | └── img1_mask2.png + ├── img1.xml + ├── img2.xml + └── img3.xml + ``` +- supported annotations: Rectangles, Polygons, Masks (as polygons) diff --git a/cvat/apps/annotation/annotation.py b/cvat/apps/annotation/annotation.py index a172066b6a5b..49ababe9ffc1 100644 --- a/cvat/apps/annotation/annotation.py +++ b/cvat/apps/annotation/annotation.py @@ -104,8 +104,8 @@ class Annotation: Attribute = namedtuple('Attribute', 'name, value') LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order') LabeledShape.__new__.__defaults__ = (0, 0) - TrackedShape = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside, keyframe, z_order') - TrackedShape.__new__.__defaults__ = (0, ) + TrackedShape = namedtuple('TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, group, z_order, label, track_id') + TrackedShape.__new__.__defaults__ = (0, 0, None, 0) Track = namedtuple('Track', 'label, group, shapes') Tag = namedtuple('Tag', 'frame, label, attributes, group') Tag.__new__.__defaults__ = (0, ) @@ -272,11 +272,14 @@ def _export_tracked_shape(self, shape): return Annotation.TrackedShape( type=shape["type"], frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step, + label=self._get_label_name(shape["label_id"]), points=shape["points"], occluded=shape["occluded"], + z_order=shape.get("z_order", 0), + group=shape.get("group", 0), outside=shape.get("outside", False), keyframe=shape.get("keyframe", True), - z_order=shape["z_order"], + track_id=shape["track_id"], attributes=self._export_attributes(shape["attributes"]), ) @@ -318,7 +321,11 @@ def _get_frame(annotations, shape): annotations = {} data_manager = DataManager(self._annotation_ir) for shape in sorted(data_manager.to_shapes(self._db_task.data.size), key=lambda shape: shape.get("z_order", 0)): - _get_frame(annotations, shape).labeled_shapes.append(self._export_labeled_shape(shape)) + if 'track_id' in shape: + exported_shape = self._export_tracked_shape(shape) + else: + exported_shape = self._export_labeled_shape(shape) + _get_frame(annotations, shape).labeled_shapes.append(exported_shape) for tag in self._annotation_ir.tags: _get_frame(annotations, tag).tags.append(self._export_tag(tag)) @@ -332,14 +339,17 @@ def shapes(self): @property def tracks(self): - for track in self._annotation_ir.tracks: + for idx, track in enumerate(self._annotation_ir.tracks): tracked_shapes = TrackManager.get_interpolated_shapes(track, 0, self._db_task.data.size) for tracked_shape in tracked_shapes: tracked_shape["attributes"] += track["attributes"] + tracked_shape["track_id"] = idx + tracked_shape["group"] = track["group"] + tracked_shape["label_id"] = track["label_id"] yield Annotation.Track( label=self._get_label_name(track["label_id"]), - group=track['group'], + group=track["group"], shapes=[self._export_tracked_shape(shape) for shape in tracked_shapes], ) diff --git a/cvat/apps/annotation/format.py b/cvat/apps/annotation/format.py index a4b8bb45f3fb..9ac2a00ca1b8 100644 --- a/cvat/apps/annotation/format.py +++ b/cvat/apps/annotation/format.py @@ -11,12 +11,10 @@ def register_format(format_file): source_code = open(format_file, 'r').read() - global_vars = { - "__builtins__": {}, - } + global_vars = {} exec(source_code, global_vars) if "format_spec" not in global_vars or not isinstance(global_vars["format_spec"], dict): - raise Exception("Could not find \'format_spec\' definition in format file specification") + raise Exception("Could not find 'format_spec' definition in format file specification") format_spec = deepcopy(global_vars["format_spec"]) format_spec["handler_file"] = File(open(format_file)) diff --git a/cvat/apps/annotation/labelme.py b/cvat/apps/annotation/labelme.py deleted file mode 100644 index baacb388ef01..000000000000 --- a/cvat/apps/annotation/labelme.py +++ /dev/null @@ -1,307 +0,0 @@ -# Copyright (C) 2019 Intel Corporation -# -# SPDX-License-Identifier: MIT - -format_spec = { - "name": "LabelMe", - "dumpers": [ - { - "display_name": "{name} {format} {version} for images", - "format": "ZIP", - "version": "3.0", - "handler": "dump_as_labelme_annotation" - } - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "ZIP", - "version": "3.0", - "handler": "load", - } - ], -} - - -_DEFAULT_USERNAME = 'cvat' -_MASKS_DIR = 'Masks' - - -def dump_frame_anno(frame_annotation): - from collections import defaultdict - from lxml import etree as ET - - root_elem = ET.Element('annotation') - - ET.SubElement(root_elem, 'filename').text = frame_annotation.name - ET.SubElement(root_elem, 'folder').text = '' - - source_elem = ET.SubElement(root_elem, 'source') - ET.SubElement(source_elem, 'sourceImage').text = '' - ET.SubElement(source_elem, 'sourceAnnotation').text = 'CVAT' - - image_elem = ET.SubElement(root_elem, 'imagesize') - ET.SubElement(image_elem, 'nrows').text = str(frame_annotation.height) - ET.SubElement(image_elem, 'ncols').text = str(frame_annotation.width) - - groups = defaultdict(list) - - for obj_id, shape in enumerate(frame_annotation.labeled_shapes): - obj_elem = ET.SubElement(root_elem, 'object') - ET.SubElement(obj_elem, 'name').text = str(shape.label) - ET.SubElement(obj_elem, 'deleted').text = '0' - ET.SubElement(obj_elem, 'verified').text = '0' - ET.SubElement(obj_elem, 'occluded').text = \ - 'yes' if shape.occluded else 'no' - ET.SubElement(obj_elem, 'date').text = '' - ET.SubElement(obj_elem, 'id').text = str(obj_id) - - parts_elem = ET.SubElement(obj_elem, 'parts') - if shape.group: - groups[shape.group].append((obj_id, parts_elem)) - else: - ET.SubElement(parts_elem, 'hasparts').text = '' - ET.SubElement(parts_elem, 'ispartof').text = '' - - if shape.type == 'rectangle': - ET.SubElement(obj_elem, 'type').text = 'bounding_box' - - poly_elem = ET.SubElement(obj_elem, 'polygon') - x0, y0, x1, y1 = shape.points - points = [ (x0, y0), (x1, y0), (x1, y1), (x0, y1) ] - for x, y in points: - point_elem = ET.SubElement(poly_elem, 'pt') - ET.SubElement(point_elem, 'x').text = '%.2f' % x - ET.SubElement(point_elem, 'y').text = '%.2f' % y - - ET.SubElement(poly_elem, 'username').text = _DEFAULT_USERNAME - elif shape.type == 'polygon': - poly_elem = ET.SubElement(obj_elem, 'polygon') - for x, y in zip(shape.points[::2], shape.points[1::2]): - point_elem = ET.SubElement(poly_elem, 'pt') - ET.SubElement(point_elem, 'x').text = '%.2f' % x - ET.SubElement(point_elem, 'y').text = '%.2f' % y - - ET.SubElement(poly_elem, 'username').text = _DEFAULT_USERNAME - elif shape.type == 'polyline': - pass - elif shape.type == 'points': - pass - else: - raise NotImplementedError("Unknown shape type '%s'" % shape.type) - - attrs = ['%s=%s' % (a.name, a.value) for a in shape.attributes] - ET.SubElement(obj_elem, 'attributes').text = ', '.join(attrs) - - for _, group in groups.items(): - leader_id, leader_parts_elem = group[0] - leader_parts = [str(o_id) for o_id, _ in group[1:]] - ET.SubElement(leader_parts_elem, 'hasparts').text = \ - ','.join(leader_parts) - ET.SubElement(leader_parts_elem, 'ispartof').text = '' - - for obj_id, parts_elem in group[1:]: - ET.SubElement(parts_elem, 'hasparts').text = '' - ET.SubElement(parts_elem, 'ispartof').text = str(leader_id) - - return ET.tostring(root_elem, encoding='unicode', pretty_print=True) - -def dump_as_labelme_annotation(file_object, annotations): - import os.path as osp - from zipfile import ZipFile, ZIP_DEFLATED - - with ZipFile(file_object, 'w', compression=ZIP_DEFLATED) as output_zip: - for frame_annotation in annotations.group_by_frame(): - xml_data = dump_frame_anno(frame_annotation) - filename = osp.splitext(frame_annotation.name)[0] + '.xml' - output_zip.writestr(filename, xml_data) - -def parse_xml_annotations(xml_data, annotations, input_zip): - from datumaro.util.mask_tools import mask_to_polygons - from io import BytesIO - from lxml import etree as ET - import numpy as np - import os.path as osp - from PIL import Image - - def parse_attributes(attributes_string): - parsed = [] - if not attributes_string: - return parsed - - read = attributes_string.split(',') - read = [a.strip() for a in read if a.strip()] - for attr in read: - if '=' in attr: - name, value = attr.split('=', maxsplit=1) - parsed.append(annotations.Attribute(name, value)) - else: - parsed.append(annotations.Attribute(attr, '1')) - - return parsed - - - root_elem = ET.fromstring(xml_data) - - frame_number = annotations.match_frame(root_elem.find('filename').text) - - parsed_annotations = dict() - group_assignments = dict() - root_annotations = set() - for obj_elem in root_elem.iter('object'): - obj_id = int(obj_elem.find('id').text) - - ann_items = [] - - attributes = [] - attributes_elem = obj_elem.find('attributes') - if attributes_elem is not None and attributes_elem.text: - attributes = parse_attributes(attributes_elem.text) - - occluded = False - occluded_elem = obj_elem.find('occluded') - if occluded_elem is not None and occluded_elem.text: - occluded = (occluded_elem.text == 'yes') - - deleted = False - deleted_elem = obj_elem.find('deleted') - if deleted_elem is not None and deleted_elem.text: - deleted = bool(int(deleted_elem.text)) - - poly_elem = obj_elem.find('polygon') - segm_elem = obj_elem.find('segm') - type_elem = obj_elem.find('type') # the only value is 'bounding_box' - if poly_elem is not None: - points = [] - for point_elem in poly_elem.iter('pt'): - x = float(point_elem.find('x').text) - y = float(point_elem.find('y').text) - points.append(x) - points.append(y) - label = obj_elem.find('name').text - if label and attributes: - label_id = annotations._get_label_id(label) - if label_id: - attributes = [a for a in attributes - if annotations._get_attribute_id(label_id, a.name) - ] - else: - attributes = [] - else: - attributes = [] - - if type_elem is not None and type_elem.text == 'bounding_box': - xmin = min(points[::2]) - xmax = max(points[::2]) - ymin = min(points[1::2]) - ymax = max(points[1::2]) - ann_items.append(annotations.LabeledShape( - type='rectangle', - frame=frame_number, - label=label, - points=[xmin, ymin, xmax, ymax], - occluded=occluded, - attributes=attributes, - )) - else: - ann_items.append(annotations.LabeledShape( - type='polygon', - frame=frame_number, - label=label, - points=points, - occluded=occluded, - attributes=attributes, - )) - elif segm_elem is not None: - label = obj_elem.find('name').text - if label and attributes: - label_id = annotations._get_label_id(label) - if label_id: - attributes = [a for a in attributes - if annotations._get_attribute_id(label_id, a.name) - ] - else: - attributes = [] - else: - attributes = [] - - mask_file = segm_elem.find('mask').text - mask = input_zip.read(osp.join(_MASKS_DIR, mask_file)) - mask = np.asarray(Image.open(BytesIO(mask)).convert('L')) - mask = (mask != 0) - polygons = mask_to_polygons(mask) - - for polygon in polygons: - ann_items.append(annotations.LabeledShape( - type='polygon', - frame=frame_number, - label=label, - points=polygon, - occluded=occluded, - attributes=attributes, - )) - - if not deleted: - parsed_annotations[obj_id] = ann_items - - parts_elem = obj_elem.find('parts') - if parts_elem is not None: - children_ids = [] - hasparts_elem = parts_elem.find('hasparts') - if hasparts_elem is not None and hasparts_elem.text: - children_ids = [int(c) for c in hasparts_elem.text.split(',')] - - parent_ids = [] - ispartof_elem = parts_elem.find('ispartof') - if ispartof_elem is not None and ispartof_elem.text: - parent_ids = [int(c) for c in ispartof_elem.text.split(',')] - - if children_ids and not parent_ids and hasparts_elem.text: - root_annotations.add(obj_id) - group_assignments[obj_id] = [None, children_ids] - - # assign a single group to the whole subtree - current_group_id = 0 - annotations_to_visit = list(root_annotations) - while annotations_to_visit: - ann_id = annotations_to_visit.pop() - ann_assignment = group_assignments[ann_id] - group_id, children_ids = ann_assignment - if group_id: - continue - - if ann_id in root_annotations: - current_group_id += 1 # start a new group - - group_id = current_group_id - ann_assignment[0] = group_id - - # continue with children - annotations_to_visit.extend(children_ids) - - assert current_group_id == len(root_annotations) - - for ann_id, ann_items in parsed_annotations.items(): - group_id = 0 - if ann_id in group_assignments: - ann_assignment = group_assignments[ann_id] - group_id = ann_assignment[0] - - for ann_item in ann_items: - if group_id: - ann_item = ann_item._replace(group=group_id) - if isinstance(ann_item, annotations.LabeledShape): - annotations.add_shape(ann_item) - else: - raise NotImplementedError() - -def load(file_object, annotations): - from zipfile import ZipFile - - with ZipFile(file_object, 'r') as input_zip: - for filename in input_zip.namelist(): - if not filename.endswith('.xml'): - continue - - xml_data = input_zip.read(filename) - parse_xml_annotations(xml_data, annotations, input_zip) diff --git a/cvat/apps/annotation/mot.py b/cvat/apps/annotation/mot.py deleted file mode 100644 index b7f63d1e79b0..000000000000 --- a/cvat/apps/annotation/mot.py +++ /dev/null @@ -1,109 +0,0 @@ -# SPDX-License-Identifier: MIT -format_spec = { - "name": "MOT", - "dumpers": [ - { - "display_name": "{name} {format} {version}", - "format": "CSV", - "version": "1.0", - "handler": "dump" - }, - ], - "loaders": [ - { - "display_name": "{name} {format} {version}", - "format": "CSV", - "version": "1.0", - "handler": "load", - } - ], -} - - -MOT = [ - "frame_id", - "track_id", - "xtl", - "ytl", - "width", - "height", - "confidence", - "class_id", - "visibility" -] - - -def dump(file_object, annotations): - """ Export track shapes in MOT CSV format. Due to limitations of the MOT - format, this process only supports rectangular interpolation mode - annotations. - """ - import csv - import io - - # csv requires a text buffer - with io.TextIOWrapper(file_object, encoding="utf-8") as csv_file: - writer = csv.DictWriter(csv_file, fieldnames=MOT) - for i, track in enumerate(annotations.tracks): - for shape in track.shapes: - # MOT doesn't support polygons or 'outside' property - if shape.type != 'rectangle': - continue - writer.writerow({ - "frame_id": shape.frame, - "track_id": i, - "xtl": shape.points[0], - "ytl": shape.points[1], - "width": shape.points[2] - shape.points[0], - "height": shape.points[3] - shape.points[1], - "confidence": 1, - "class_id": track.label, - "visibility": 1 - int(shape.occluded) - }) - - -def load(file_object, annotations): - """ Read MOT CSV format and convert objects to annotated tracks. - """ - import csv - import io - tracks = {} - # csv requires a text buffer - with io.TextIOWrapper(file_object, encoding="utf-8") as csv_file: - reader = csv.DictReader(csv_file, fieldnames=MOT) - for row in reader: - # create one shape per row - xtl = float(row["xtl"]) - ytl = float(row["ytl"]) - xbr = xtl + float(row["width"]) - ybr = ytl + float(row["height"]) - shape = annotations.TrackedShape( - type="rectangle", - points=[xtl, ytl, xbr, ybr], - occluded=float(row["visibility"]) == 0, - outside=False, - keyframe=False, - z_order=0, - frame=int(row["frame_id"]), - attributes=[], - ) - # build trajectories as lists of shapes in track dict - track_id = int(row["track_id"]) - if track_id not in tracks: - tracks[track_id] = annotations.Track(row["class_id"], track_id, []) - tracks[track_id].shapes.append(shape) - for track in tracks.values(): - # Set outside=True for the last shape since MOT has no support - # for this flag - last = annotations.TrackedShape( - type=track.shapes[-1].type, - points=track.shapes[-1].points, - occluded=track.shapes[-1].occluded, - outside=True, - keyframe=track.shapes[-1].keyframe, - z_order=track.shapes[-1].z_order, - frame=track.shapes[-1].frame, - attributes=track.shapes[-1].attributes, - ) - track.shapes[-1] = last - annotations.add_track(track) diff --git a/cvat/apps/annotation/settings.py b/cvat/apps/annotation/settings.py index e1e5f82b42c8..9099a387c05c 100644 --- a/cvat/apps/annotation/settings.py +++ b/cvat/apps/annotation/settings.py @@ -4,7 +4,7 @@ import os -path_prefix = os.path.join('cvat', 'apps', 'annotation') +path_prefix = os.path.join('cvat', 'apps', 'dataset_manager', 'formats') BUILTIN_FORMATS = ( os.path.join(path_prefix, 'cvat.py'), os.path.join(path_prefix, 'pascal_voc.py'), diff --git a/cvat/apps/dataset_manager/_tests.py b/cvat/apps/dataset_manager/_tests.py new file mode 100644 index 000000000000..1a5300756e1f --- /dev/null +++ b/cvat/apps/dataset_manager/_tests.py @@ -0,0 +1,309 @@ + +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +class _GitImportFix: + import sys + former_path = sys.path[:] + + @classmethod + def apply(cls): + # HACK: fix application and module name clash + # 'git' app is found earlier than a library in the path. + # The clash is introduced by unittest discover + import sys + print('apply') + + apps_dir = __file__[:__file__.rfind('/dataset_manager/')] + assert 'apps' in apps_dir + try: + sys.path.remove(apps_dir) + except ValueError: + pass + + for name in list(sys.modules): + if name.startswith('git.') or name == 'git': + m = sys.modules.pop(name, None) + del m + + import git + assert apps_dir not in git.__file__ + + @classmethod + def restore(cls): + import sys + print('restore') + + for name in list(sys.modules): + if name.startswith('git.') or name == 'git': + m = sys.modules.pop(name) + del m + + sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')]) + + import importlib + importlib.invalidate_caches() + +def _setUpModule(): + _GitImportFix.apply() + import cvat.apps.dataset_manager.task as dm + from cvat.apps.engine.models import Task + globals()['dm'] = dm + globals()['Task'] = Task + + import sys + sys.path.insert(0, __file__[:__file__.rfind('/dataset_manager/')]) + +def tearDownModule(): + _GitImportFix.restore() + +from io import BytesIO +import os +import random +import tempfile + +from PIL import Image +from django.contrib.auth.models import User, Group +from rest_framework.test import APITestCase, APIClient +from rest_framework import status + +_setUpModule() + + +def generate_image_file(filename): + f = BytesIO() + width = random.randint(10, 200) + height = random.randint(10, 200) + image = Image.new('RGB', size=(width, height)) + image.save(f, 'jpeg') + f.name = filename + f.seek(0) + + return f + +def create_db_users(cls): + group_user, _ = Group.objects.get_or_create(name="user") + + user_dummy = User.objects.create_superuser(username="test", password="test", email="") + user_dummy.groups.add(group_user) + + cls.user = user_dummy + +class ForceLogin: + def __init__(self, user, client): + self.user = user + self.client = client + + def __enter__(self): + if self.user: + self.client.force_login(self.user, + backend='django.contrib.auth.backends.ModelBackend') + + return self + + def __exit__(self, exception_type, exception_value, traceback): + if self.user: + self.client.logout() + +class TaskExportTest(APITestCase): + def setUp(self): + self.client = APIClient() + + @classmethod + def setUpTestData(cls): + create_db_users(cls) + + def _generate_task(self): + task = { + "name": "my task #1", + "owner": '', + "assignee": '', + "overlap": 0, + "segment_size": 100, + "z_order": False, + "labels": [ + { + "name": "car", + "attributes": [ + { + "name": "model", + "mutable": False, + "input_type": "select", + "default_value": "mazda", + "values": ["bmw", "mazda", "renault"] + }, + { + "name": "parked", + "mutable": True, + "input_type": "checkbox", + "default_value": False + }, + ] + }, + {"name": "person"}, + ] + } + task = self._create_task(task, 3) + + annotations = { + "version": 0, + "tags": [ + { + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": None, + "attributes": [] + } + ], + "shapes": [ + { + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": None, + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][0] + }, + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][0]["default_value"] + } + ], + "points": [1.0, 2.1, 100, 300.222], + "type": "rectangle", + "occluded": False + }, + { + "frame": 1, + "label_id": task["labels"][1]["id"], + "group": None, + "attributes": [], + "points": [2.0, 2.1, 100, 300.222, 400, 500, 1, 3], + "type": "polygon", + "occluded": False + }, + ], + "tracks": [ + { + "frame": 0, + "label_id": task["labels"][0]["id"], + "group": None, + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][0]["id"], + "value": task["labels"][0]["attributes"][0]["values"][0] + }, + ], + "shapes": [ + { + "frame": 0, + "points": [1.0, 2.1, 100, 300.222], + "type": "rectangle", + "occluded": False, + "outside": False, + "attributes": [ + { + "spec_id": task["labels"][0]["attributes"][1]["id"], + "value": task["labels"][0]["attributes"][1]["default_value"] + } + ] + }, + { + "frame": 1, + "attributes": [], + "points": [2.0, 2.1, 100, 300.222], + "type": "rectangle", + "occluded": True, + "outside": True + }, + ] + }, + { + "frame": 1, + "label_id": task["labels"][1]["id"], + "group": None, + "attributes": [], + "shapes": [ + { + "frame": 1, + "attributes": [], + "points": [1.0, 2.1, 100, 300.222], + "type": "rectangle", + "occluded": False, + "outside": False + } + ] + }, + ] + } + self._put_api_v1_task_id_annotations(task["id"], annotations) + + return task, annotations + + def _create_task(self, data, size): + with ForceLogin(self.user, self.client): + response = self.client.post('/api/v1/tasks', data=data, format="json") + assert response.status_code == status.HTTP_201_CREATED, response.status_code + tid = response.data["id"] + + images = { + "client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) + for i in range(size) + } + images["image_quality"] = 75 + response = self.client.post("/api/v1/tasks/{}/data".format(tid), data=images) + assert response.status_code == status.HTTP_202_ACCEPTED, response.status_code + + response = self.client.get("/api/v1/tasks/{}".format(tid)) + task = response.data + + return task + + def _put_api_v1_task_id_annotations(self, tid, data): + with ForceLogin(self.user, self.client): + response = self.client.put("/api/v1/tasks/{}/annotations".format(tid), + data=data, format="json") + + return response + + def _test_export(self, format_name, save_images=False): + self.assertTrue(format_name in [f['tag'] for f in dm.EXPORT_FORMATS]) + + task, _ = self._generate_task() + project = dm.TaskProject.from_task( + Task.objects.get(pk=task["id"]), self.user.username) + + with tempfile.TemporaryDirectory() as test_dir: + project.export(format_name, test_dir, save_images=save_images) + + self.assertTrue(os.listdir(test_dir)) + + def test_datumaro(self): + self._test_export(dm.EXPORT_FORMAT_DATUMARO_PROJECT, save_images=False) + + def test_coco(self): + self._test_export('cvat_coco', save_images=True) + + def test_voc(self): + self._test_export('cvat_voc', save_images=True) + + def test_tf_detection_api(self): + self._test_export('cvat_tfrecord', save_images=True) + + def test_yolo(self): + self._test_export('cvat_yolo', save_images=True) + + def test_mot(self): + self._test_export('cvat_mot', save_images=True) + + def test_labelme(self): + self._test_export('cvat_label_me', save_images=True) + + def test_formats_query(self): + formats = dm.get_export_formats() + + expected = set(f['tag'] for f in dm.EXPORT_FORMATS) + actual = set(f['tag'] for f in formats) + self.assertSetEqual(expected, actual) diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py index 5215e25fd470..7a7b80b27859 100644 --- a/cvat/apps/dataset_manager/bindings.py +++ b/cvat/apps/dataset_manager/bindings.py @@ -140,6 +140,10 @@ def convert_attrs(label, cvat_attrs): anno_attr['occluded'] = shape_obj.occluded anno_attr['z_order'] = shape_obj.z_order + if hasattr(shape_obj, 'track_id'): + anno_attr['track_id'] = shape_obj.track_id + anno_attr['keyframe'] = shape_obj.keyframe + anno_points = shape_obj.points if shape_obj.type == ShapeType.POINTS: anno = datumaro.Points(anno_points, diff --git a/cvat/apps/dataset_manager/formats/__init__.py b/cvat/apps/dataset_manager/formats/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat/apps/annotation/coco.py b/cvat/apps/dataset_manager/formats/coco.py similarity index 86% rename from cvat/apps/annotation/coco.py rename to cvat/apps/dataset_manager/formats/coco.py index 942aa61f6b33..fe323b645db0 100644 --- a/cvat/apps/annotation/coco.py +++ b/cvat/apps/dataset_manager/formats/coco.py @@ -29,15 +29,19 @@ def load(file_object, annotations): dm_dataset = CocoInstancesExtractor(file_object.name) import_dm_annotations(dm_dataset, annotations) +from datumaro.plugins.coco_format.converter import \ + CocoInstancesConverter as _CocoInstancesConverter +class CvatCocoConverter(_CocoInstancesConverter): + NAME = 'cvat_coco' + def dump(file_object, annotations): import os.path as osp import shutil from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor - from datumaro.components.project import Environment from tempfile import TemporaryDirectory + extractor = CvatAnnotationsExtractor('', annotations) - converter = Environment().make_converter('coco_instances', - crop_covered=True) + converter = CvatCocoConverter() with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) diff --git a/cvat/apps/annotation/cvat.py b/cvat/apps/dataset_manager/formats/cvat.py similarity index 100% rename from cvat/apps/annotation/cvat.py rename to cvat/apps/dataset_manager/formats/cvat.py diff --git a/cvat/apps/dataset_manager/formats/labelme.py b/cvat/apps/dataset_manager/formats/labelme.py new file mode 100644 index 000000000000..8cc0d880cc18 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/labelme.py @@ -0,0 +1,68 @@ +# Copyright (C) 2019 Intel Corporation +# +# SPDX-License-Identifier: MIT + +format_spec = { + "name": "LabelMe", + "dumpers": [ + { + "display_name": "{name} {format} {version}", + "format": "ZIP", + "version": "3.0", + "handler": "dump" + } + ], + "loaders": [ + { + "display_name": "{name} {format} {version}", + "format": "ZIP", + "version": "3.0", + "handler": "load", + } + ], +} + + +from datumaro.components.converter import Converter +class CvatLabelMeConverter(Converter): + def __init__(self, save_images=False): + self._save_images = save_images + + def __call__(self, extractor, save_dir): + from datumaro.components.project import Environment, Dataset + + env = Environment() + id_from_image = env.transforms.get('id_from_image_name') + + extractor = extractor.transform(id_from_image) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms + + converter = env.make_converter('label_me', save_images=self._save_images) + converter(extractor, save_dir=save_dir) + +def dump(file_object, annotations): + from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor + from cvat.apps.dataset_manager.util import make_zip_archive + from tempfile import TemporaryDirectory + + extractor = CvatAnnotationsExtractor('', annotations) + converter = CvatLabelMeConverter() + with TemporaryDirectory() as temp_dir: + converter(extractor, save_dir=temp_dir) + make_zip_archive(temp_dir, file_object) + +def load(file_object, annotations): + from pyunpack import Archive + from tempfile import TemporaryDirectory + from datumaro.plugins.labelme_format import LabelMeImporter + from datumaro.components.project import Environment + from cvat.apps.dataset_manager.bindings import import_dm_annotations + + archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") + with TemporaryDirectory() as tmp_dir: + Archive(archive_file).extractall(tmp_dir) + + dm_dataset = LabelMeImporter()(tmp_dir).make_dataset() + masks_to_polygons = Environment().transforms.get('masks_to_polygons') + dm_dataset = dm_dataset.transform(masks_to_polygons) + import_dm_annotations(dm_dataset, annotations) diff --git a/cvat/apps/annotation/mask.py b/cvat/apps/dataset_manager/formats/mask.py similarity index 59% rename from cvat/apps/annotation/mask.py rename to cvat/apps/dataset_manager/formats/mask.py index dbdb48df1c97..c18553b32650 100644 --- a/cvat/apps/annotation/mask.py +++ b/cvat/apps/dataset_manager/formats/mask.py @@ -22,27 +22,38 @@ ], } +from datumaro.components.converter import Converter +class CvatMaskConverter(Converter): + def __init__(self, save_images=False): + self._save_images = save_images + + def __call__(self, extractor, save_dir): + from datumaro.components.project import Environment, Dataset + + env = Environment() + polygons_to_masks = env.transforms.get('polygons_to_masks') + boxes_to_masks = env.transforms.get('boxes_to_masks') + merge_instance_segments = env.transforms.get('merge_instance_segments') + id_from_image = env.transforms.get('id_from_image_name') + + extractor = extractor.transform(polygons_to_masks) + extractor = extractor.transform(boxes_to_masks) + extractor = extractor.transform(merge_instance_segments) + extractor = extractor.transform(id_from_image) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms + + converter = env.make_converter('voc_segmentation', + apply_colormap=True, label_map='source', + save_images=self._save_images) + converter(extractor, save_dir=save_dir) def dump(file_object, annotations): from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.util import make_zip_archive - from datumaro.components.project import Environment, Dataset from tempfile import TemporaryDirectory - env = Environment() - polygons_to_masks = env.transforms.get('polygons_to_masks') - boxes_to_masks = env.transforms.get('boxes_to_masks') - merge_instance_segments = env.transforms.get('merge_instance_segments') - id_from_image = env.transforms.get('id_from_image_name') - extractor = CvatAnnotationsExtractor('', annotations) - extractor = extractor.transform(polygons_to_masks) - extractor = extractor.transform(boxes_to_masks) - extractor = extractor.transform(merge_instance_segments) - extractor = extractor.transform(id_from_image) - extractor = Dataset.from_extractors(extractor) # apply lazy transforms - converter = env.make_converter('voc_segmentation', - apply_colormap=True, label_map='source') + converter = CvatMaskConverter() with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) make_zip_archive(temp_dir, file_object) diff --git a/cvat/apps/dataset_manager/formats/mot.py b/cvat/apps/dataset_manager/formats/mot.py new file mode 100644 index 000000000000..ced2fccabfe8 --- /dev/null +++ b/cvat/apps/dataset_manager/formats/mot.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: MIT +format_spec = { + "name": "MOT", + "dumpers": [ + { + "display_name": "{name} {format} {version}", + "format": "ZIP", + "version": "1.1", + "handler": "dump" + }, + ], + "loaders": [ + { + "display_name": "{name} {format} {version}", + "format": "ZIP", + "version": "1.1", + "handler": "load", + } + ], +} + + +from datumaro.plugins.mot_format import \ + MotSeqGtConverter as _MotConverter +class CvatMotConverter(_MotConverter): + NAME = 'cvat_mot' + +def dump(file_object, annotations): + from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor + from cvat.apps.dataset_manager.util import make_zip_archive + from tempfile import TemporaryDirectory + + extractor = CvatAnnotationsExtractor('', annotations) + converter = CvatMotConverter() + with TemporaryDirectory() as temp_dir: + converter(extractor, save_dir=temp_dir) + make_zip_archive(temp_dir, file_object) + + +def load(file_object, annotations): + from pyunpack import Archive + from tempfile import TemporaryDirectory + from datumaro.plugins.mot_format import MotSeqImporter + import datumaro.components.extractor as datumaro + from cvat.apps.dataset_manager.bindings import match_frame + + archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") + with TemporaryDirectory() as tmp_dir: + Archive(archive_file).extractall(tmp_dir) + + tracks = {} + + dm_dataset = MotSeqImporter()(tmp_dir).make_dataset() + label_cat = dm_dataset.categories()[datumaro.AnnotationType.label] + + for item in dm_dataset: + frame_id = match_frame(item, annotations) + + for ann in item.annotations: + if ann.type != datumaro.AnnotationType.bbox: + continue + + track_id = ann.attributes.get('track_id') + if track_id is None: + continue + + shape = annotations.TrackedShape( + type='rectangle', + points=ann.points, + occluded=ann.attributes.get('occluded') == True, + outside=False, + keyframe=False, + z_order=ann.z_order, + frame=frame_id, + attributes=[], + ) + + # build trajectories as lists of shapes in track dict + if track_id not in tracks: + tracks[track_id] = annotations.Track( + label_cat.items[ann.label].name, 0, []) + tracks[track_id].shapes.append(shape) + + for track in tracks.values(): + # MOT annotations do not require frames to be ordered + track.shapes.sort(key=lambda t: t.frame) + # Set outside=True for the last shape in a track to finish the track + track.shapes[-1] = track.shapes[-1]._replace(outside=True) + annotations.add_track(track) diff --git a/cvat/apps/annotation/pascal_voc.py b/cvat/apps/dataset_manager/formats/pascal_voc.py similarity index 78% rename from cvat/apps/annotation/pascal_voc.py rename to cvat/apps/dataset_manager/formats/pascal_voc.py index b6bcfaa33eeb..a74d14ba3f2c 100644 --- a/cvat/apps/annotation/pascal_voc.py +++ b/cvat/apps/dataset_manager/formats/pascal_voc.py @@ -62,19 +62,30 @@ def load(file_object, annotations): dm_dataset = dm_project.make_dataset() import_dm_annotations(dm_dataset, annotations) +from datumaro.components.converter import Converter +class CvatVocConverter(Converter): + def __init__(self, save_images=False): + self._save_images = save_images + + def __call__(self, extractor, save_dir): + from datumaro.components.project import Environment, Dataset + env = Environment() + id_from_image = env.transforms.get('id_from_image_name') + + extractor = extractor.transform(id_from_image) + extractor = Dataset.from_extractors(extractor) # apply lazy transforms + + converter = env.make_converter('voc', label_map='source', + save_images=self._save_images) + converter(extractor, save_dir=save_dir) + def dump(file_object, annotations): from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.util import make_zip_archive - from datumaro.components.project import Environment, Dataset from tempfile import TemporaryDirectory - env = Environment() - id_from_image = env.transforms.get('id_from_image_name') - extractor = CvatAnnotationsExtractor('', annotations) - extractor = extractor.transform(id_from_image) - extractor = Dataset.from_extractors(extractor) # apply lazy transforms - converter = env.make_converter('voc', label_map='source') + converter = CvatVocConverter() with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) make_zip_archive(temp_dir, file_object) \ No newline at end of file diff --git a/cvat/apps/annotation/tfrecord.py b/cvat/apps/dataset_manager/formats/tfrecord.py similarity index 86% rename from cvat/apps/annotation/tfrecord.py rename to cvat/apps/dataset_manager/formats/tfrecord.py index 647d5c26442f..db6dee6944fc 100644 --- a/cvat/apps/annotation/tfrecord.py +++ b/cvat/apps/dataset_manager/formats/tfrecord.py @@ -22,13 +22,18 @@ ], } +from datumaro.plugins.tf_detection_api_format.converter import \ + TfDetectionApiConverter as _TfDetectionApiConverter +class CvatTfrecordConverter(_TfDetectionApiConverter): + NAME = 'cvat_tfrecord' + def dump(file_object, annotations): from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.util import make_zip_archive - from datumaro.components.project import Environment from tempfile import TemporaryDirectory + extractor = CvatAnnotationsExtractor('', annotations) - converter = Environment().make_converter('tf_detection_api') + converter = CvatTfrecordConverter() with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) make_zip_archive(temp_dir, file_object) diff --git a/cvat/apps/annotation/yolo.py b/cvat/apps/dataset_manager/formats/yolo.py similarity index 75% rename from cvat/apps/annotation/yolo.py rename to cvat/apps/dataset_manager/formats/yolo.py index 379ea45abe3d..f21ebe43634a 100644 --- a/cvat/apps/annotation/yolo.py +++ b/cvat/apps/dataset_manager/formats/yolo.py @@ -27,8 +27,9 @@ def load(file_object, annotations): import os.path as osp from tempfile import TemporaryDirectory from glob import glob + from datumaro.components.extractor import DatasetItem from datumaro.plugins.yolo_format.importer import YoloImporter - from cvat.apps.dataset_manager.bindings import import_dm_annotations + from cvat.apps.dataset_manager.bindings import import_dm_annotations, match_frame archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name") with TemporaryDirectory() as tmp_dir: @@ -37,33 +38,31 @@ def load(file_object, annotations): image_info = {} anno_files = glob(osp.join(tmp_dir, '**', '*.txt'), recursive=True) for filename in anno_files: - filename = osp.basename(filename) + filename = osp.splitext(osp.basename(filename))[0] frame_info = None try: - frame_info = annotations.frame_info[ - int(osp.splitext(filename)[0])] - except Exception: - pass - try: - frame_info = annotations.match_frame(filename) - frame_info = annotations.frame_info[frame_info] + frame_id = match_frame(DatasetItem(id=filename), annotations) + frame_info = annotations.frame_info[frame_id] except Exception: pass if frame_info is not None: - image_info[osp.splitext(filename)[0]] = \ - (frame_info['height'], frame_info['width']) + image_info[filename] = (frame_info['height'], frame_info['width']) dm_project = YoloImporter()(tmp_dir, image_info=image_info) dm_dataset = dm_project.make_dataset() import_dm_annotations(dm_dataset, annotations) +from datumaro.plugins.yolo_format.converter import \ + YoloConverter as _YoloConverter +class CvatYoloConverter(_YoloConverter): + NAME = 'cvat_yolo' + def dump(file_object, annotations): from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.util import make_zip_archive - from datumaro.components.project import Environment from tempfile import TemporaryDirectory extractor = CvatAnnotationsExtractor('', annotations) - converter = Environment().make_converter('yolo') + converter = CvatYoloConverter() with TemporaryDirectory() as temp_dir: converter(extractor, save_dir=temp_dir) make_zip_archive(temp_dir, file_object) diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index 4fdfbee3ec84..688f7792c101 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -8,24 +8,23 @@ import os import os.path as osp import shutil -import sys import tempfile from django.utils import timezone import django_rq +from cvat.settings.base import DATUMARO_PATH as _DATUMARO_REPO_PATH, \ + BASE_DIR as _CVAT_ROOT_DIR from cvat.apps.engine.log import slogger from cvat.apps.engine.models import Task from cvat.apps.engine.frame_provider import FrameProvider from .util import current_function_name, make_zip_archive -_CVAT_ROOT_DIR = __file__[:__file__.rfind('cvat/')] -_DATUMARO_REPO_PATH = osp.join(_CVAT_ROOT_DIR, 'datumaro') -sys.path.append(_DATUMARO_REPO_PATH) from datumaro.components.project import Project, Environment import datumaro.components.extractor as datumaro from .bindings import CvatImagesExtractor, CvatTaskExtractor +_FORMATS_DIR = osp.join(osp.dirname(__file__), 'formats') _MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0] def log_exception(logger=None, exc_info=True): @@ -96,8 +95,10 @@ def _load(self): FrameProvider(self._db_task.data))) def _import_from_task(self, user): - self._project = Project.generate(self._project_dir, - config={'project_name': self._db_task.name}) + self._project = Project.generate(self._project_dir, config={ + 'project_name': self._db_task.name, + 'plugins_dir': _FORMATS_DIR, + }) self._project.add_source('task_%s_images' % self._db_task.id, { 'format': _TASK_IMAGES_EXTRACTOR, @@ -316,28 +317,40 @@ def clear_export_cache(task_id, file_path, file_ctime): }, { 'name': 'PASCAL VOC 2012', - 'tag': 'voc', + 'tag': 'cvat_voc', 'is_default': False, }, { 'name': 'MS COCO', - 'tag': 'coco', + 'tag': 'cvat_coco', 'is_default': False, }, { 'name': 'YOLO', - 'tag': 'yolo', + 'tag': 'cvat_yolo', + 'is_default': False, + }, + { + 'name': 'TF Detection API', + 'tag': 'cvat_tfrecord', + 'is_default': False, + }, + { + 'name': 'MOT', + 'tag': 'cvat_mot', 'is_default': False, }, { - 'name': 'TF Detection API TFrecord', - 'tag': 'tf_detection_api', + 'name': 'LabelMe', + 'tag': 'cvat_label_me', 'is_default': False, }, ] def get_export_formats(): - converters = Environment().converters + converters = Environment(config={ + 'plugins_dir': _FORMATS_DIR + }).converters available_formats = set(converters.items) available_formats.add(EXPORT_FORMAT_DATUMARO_PROJECT) diff --git a/cvat/apps/engine/data_manager.py b/cvat/apps/engine/data_manager.py index bbbd847ea985..d7adc8b8b4a5 100644 --- a/cvat/apps/engine/data_manager.py +++ b/cvat/apps/engine/data_manager.py @@ -226,12 +226,11 @@ def to_shapes(self, end_frame): shapes = [] for idx, track in enumerate(self.objects): for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame): - if not shape["outside"]: - shape["label_id"] = track["label_id"] - shape["group"] = track["group"] - shape["track_id"] = idx - shape["attributes"] += track["attributes"] - shapes.append(shape) + shape["label_id"] = track["label_id"] + shape["group"] = track["group"] + shape["track_id"] = idx + shape["attributes"] += track["attributes"] + shapes.append(shape) return shapes @staticmethod diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index 1c2f7a87173d..1fb9d1b054bf 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -1878,10 +1878,15 @@ def test_api_v1_tasks_id_data_no_auth(self): def compare_objects(self, obj1, obj2, ignore_keys, fp_tolerance=.001): if isinstance(obj1, dict): self.assertTrue(isinstance(obj2, dict), "{} != {}".format(obj1, obj2)) - for k in obj1.keys(): + for k, v1 in obj1.items(): if k in ignore_keys: continue - compare_objects(self, obj1[k], obj2.get(k), ignore_keys) + v2 = obj2[k] + if k == 'attributes': + key = lambda a: a['spec_id'] + v1.sort(key=key) + v2.sort(key=key) + compare_objects(self, v1, v2, ignore_keys) elif isinstance(obj1, list): self.assertTrue(isinstance(obj2, list), "{} != {}".format(obj1, obj2)) self.assertEqual(len(obj1), len(obj2), "{} != {}".format(obj1, obj2)) @@ -2475,7 +2480,12 @@ def _get_annotation_formats(self, user): def _check_response(self, response, data): if not response.status_code in [ status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]: - compare_objects(self, data, response.data, ignore_keys=["id"]) + try: + compare_objects(self, data, response.data, ignore_keys=["id"]) + except AssertionError as e: + print("Objects are not equal: ", data, response.data) + print(e) + raise def _run_api_v1_tasks_id_annotations(self, owner, assignee, annotator): task, _ = self._create_task(owner, assignee) @@ -3049,10 +3059,10 @@ def _get_initial_annotation(annotation_format): annotations["shapes"] = rectangle_shapes_wo_attrs + polygon_shapes_wo_attrs annotations["tracks"] = rectangle_tracks_wo_attrs - elif annotation_format == "MOT CSV 1.0": + elif annotation_format == "MOT ZIP 1.1": annotations["tracks"] = rectangle_tracks_wo_attrs - elif annotation_format == "LabelMe ZIP 3.0 for images": + elif annotation_format == "LabelMe ZIP 3.0": annotations["shapes"] = rectangle_shapes_with_attrs + \ rectangle_shapes_wo_attrs + \ polygon_shapes_wo_attrs + \ diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 68499ff2c1e2..6bf38049e8c7 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -415,3 +415,6 @@ def generate_ssh_keys(): DATA_UPLOAD_MAX_NUMBER_FIELDS = None # this django check disabled LOCAL_LOAD_MAX_FILES_COUNT = 500 LOCAL_LOAD_MAX_FILES_SIZE = 512 * 1024 * 1024 # 512 MB + +DATUMARO_PATH = os.path.join(BASE_DIR, 'datumaro') +sys.path.append(DATUMARO_PATH) \ No newline at end of file diff --git a/datumaro/datumaro/plugins/labelme_format.py b/datumaro/datumaro/plugins/labelme_format.py index d2512ad7a011..0607ba9baea6 100644 --- a/datumaro/datumaro/plugins/labelme_format.py +++ b/datumaro/datumaro/plugins/labelme_format.py @@ -10,7 +10,7 @@ import os import os.path as osp -from datumaro.components.extractor import (SourceExtractor, +from datumaro.components.extractor import (SourceExtractor, DEFAULT_SUBSET_NAME, DatasetItem, AnnotationType, Mask, Bbox, Polygon, LabelCategories ) from datumaro.components.extractor import Importer @@ -81,9 +81,16 @@ def parse_attributes(attr_str): for attr in [a.strip() for a in attr_str.split(',') if a.strip()]: if '=' in attr: name, value = attr.split('=', maxsplit=1) + if value.lower() in {'true', 'false'}: + value = value.lower() == 'true' + else: + try: + value = float(value) + except Exception: + pass parsed.append((name, value)) else: - parsed.append((attr, '1')) + parsed.append((attr, True)) return parsed @@ -426,10 +433,7 @@ def _save_item(self, item, subset_dir): attrs = [] for k, v in ann.attributes.items(): - if isinstance(v, bool): - attrs.append(k) - else: - attrs.append('%s=%s' % (k, v)) + attrs.append('%s=%s' % (k, v)) ET.SubElement(obj_elem, 'attributes').text = ', '.join(attrs) obj_id += 1 diff --git a/datumaro/datumaro/plugins/mot_format.py b/datumaro/datumaro/plugins/mot_format.py index a586645da5d9..b39c607052c8 100644 --- a/datumaro/datumaro/plugins/mot_format.py +++ b/datumaro/datumaro/plugins/mot_format.py @@ -90,9 +90,8 @@ def __init__(self, path, labels=None, occlusion_threshold=0, is_gt=None): self._is_gt = is_gt if labels is None: - if osp.isfile(osp.join(seq_root, MotPath.LABELS_FILE)): - labels = osp.join(seq_root, MotPath.LABELS_FILE) - else: + labels = osp.join(osp.dirname(path), MotPath.LABELS_FILE) + if not osp.isfile(labels): labels = [lbl.name for lbl in MotLabel] if isinstance(labels, str): labels = self._parse_labels(labels) @@ -277,6 +276,8 @@ def __call__(self, extractor, save_dir): anno_file = osp.join(anno_dir, MotPath.GT_FILENAME) with open(anno_file, 'w', encoding="utf-8") as csv_file: writer = csv.DictWriter(csv_file, fieldnames=MotPath.FIELDS) + + track_id_mapping = {-1: -1} for idx, item in enumerate(extractor): log.debug("Converting item '%s'", item.id) @@ -286,9 +287,13 @@ def __call__(self, extractor, save_dir): if anno.type != AnnotationType.bbox: continue + track_id = int(anno.attributes.get('track_id', -1)) + if track_id not in track_id_mapping: + track_id_mapping[track_id] = len(track_id_mapping) + track_id = track_id_mapping[track_id] writer.writerow({ 'frame_id': frame_id, - 'track_id': int(anno.attributes.get('track_id', -1)), + 'track_id': track_id, 'x': anno.x, 'y': anno.y, 'w': anno.w, @@ -310,7 +315,7 @@ def __call__(self, extractor, save_dir): else: log.debug("Item '%s' has no image" % item.id) - labels_file = osp.join(save_dir, MotPath.LABELS_FILE) + labels_file = osp.join(anno_dir, MotPath.LABELS_FILE) with open(labels_file, 'w', encoding='utf-8') as f: f.write('\n'.join(l.name for l in extractor.categories()[AnnotationType.label].items) diff --git a/datumaro/tests/test_labelme_format.py b/datumaro/tests/test_labelme_format.py index 35fa2ca848b4..098fd26392ed 100644 --- a/datumaro/tests/test_labelme_format.py +++ b/datumaro/tests/test_labelme_format.py @@ -36,7 +36,10 @@ def __iter__(self): annotations=[ Bbox(0, 4, 4, 8, label=2, group=2), Polygon([0, 4, 4, 4, 5, 6], label=3, attributes={ - 'occluded': True + 'occluded': True, + 'a1': 'qwe', + 'a2': True, + 'a3': 123, }), Mask(np.array([[0, 1], [1, 0], [1, 1]]), group=2, attributes={ 'username': 'test' }), @@ -70,6 +73,9 @@ def __iter__(self): Polygon([0, 4, 4, 4, 5, 6], label=1, id=1, attributes={ 'occluded': True, 'username': '', + 'a1': 'qwe', + 'a2': True, + 'a3': 123, } ), Mask(np.array([[0, 1], [1, 0], [1, 1]]), group=2, @@ -150,7 +156,7 @@ def __iter__(self): Polygon([30, 12, 42, 21, 24, 26, 15, 22, 18, 14, 22, 12, 27, 12], label=2, group=2, id=2, attributes={ - 'a1': '1', + 'a1': True, 'occluded': True, 'username': 'anonymous' } @@ -158,21 +164,21 @@ def __iter__(self): Polygon([35, 21, 43, 22, 40, 28, 28, 31, 31, 22, 32, 25], label=3, group=2, id=3, attributes={ - 'kj': '1', + 'kj': True, 'occluded': False, 'username': 'anonymous' } ), Bbox(13, 19, 10, 11, label=4, group=2, id=4, attributes={ - 'hg': '1', + 'hg': True, 'occluded': True, 'username': 'anonymous' } ), Mask(mask2, label=5, group=1, id=5, attributes={ - 'd': '1', + 'd': True, 'occluded': False, 'username': 'anonymous' } @@ -180,7 +186,7 @@ def __iter__(self): Polygon([64, 21, 74, 24, 72, 32, 62, 34, 60, 27, 62, 22], label=6, group=1, id=6, attributes={ - 'gfd lkj lkj hi': '1', + 'gfd lkj lkj hi': True, 'occluded': False, 'username': 'anonymous' }