Move annotation formats to dataset manager (#1256)

* Move formats to dataset manager

* Unify datataset export and anno export implementations

* Add track_id to TrackedShape, export tracked shapes

* Replace MOT format

* Replace LabelMe format

* Add new formats to dm

* Add dm tests

* Extend TrackedShape

* Enable dm test in CI

* Fix tests

* Add import

* Fix tests

* Fix mot track ids

* Fix mot format

* Update attribute logic in labelme tests

* Use common code in yolo

* Put datumaro in path in settings

* Expect labels file in MOT next to annotations file

* Add MOT format description

* Add import

* Add labelme format description

* Linter fix

* Linter fix2

* Compare attributes ordered

* Update docs

* Update tests
main
zhiltsov-max 6 years ago committed by GitHub
parent e87ec38476
commit 887c6f0432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,5 +13,6 @@ before_script:
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 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 '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' - 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'

@ -1,3 +1,5 @@
<!--lint disable list-item-indent-->
<!--lint disable no-duplicate-headings-->
## Description ## Description
The purpose of this application is to add support for multiple annotation formats for CVAT. 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 <path to VOCdevkit> --set train --y
│   └── Segmentation/ │   └── Segmentation/
│   └── default.txt # list of image names without extension │   └── default.txt # list of image names without extension
├── SegmentationClass/ # merged class masks ├── SegmentationClass/ # merged class masks
│   ── image1.png │   ── image1.png
│   └── image2.png │   └── image2.png
└── SegmentationObject/ # merged instance masks └── SegmentationObject/ # merged instance masks
── image1.png ── image1.png
└── image2.png └── image2.png
``` ```
Mask is a png image with several (RGB) channels where each pixel has own color which corresponds to a label. 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 <path to VOCdevkit> --set train --y
│   └── Segmentation/ │   └── Segmentation/
│   └── <any_subset_name>.txt │   └── <any_subset_name>.txt
├── SegmentationClass/ ├── SegmentationClass/
│   ── image1.png │   ── image1.png
│   └── image2.png │   └── image2.png
└── SegmentationObject/ └── SegmentationObject/
└── image.png ├── image1.png
└── image2.png └── image2.png
``` ```
- supported shapes: Polygons - supported shapes: Polygons
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files - 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, <skipped>
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)

@ -104,8 +104,8 @@ class Annotation:
Attribute = namedtuple('Attribute', 'name, value') Attribute = namedtuple('Attribute', 'name, value')
LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order') LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order')
LabeledShape.__new__.__defaults__ = (0, 0) LabeledShape.__new__.__defaults__ = (0, 0)
TrackedShape = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside, keyframe, z_order') TrackedShape = namedtuple('TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, group, z_order, label, track_id')
TrackedShape.__new__.__defaults__ = (0, ) TrackedShape.__new__.__defaults__ = (0, 0, None, 0)
Track = namedtuple('Track', 'label, group, shapes') Track = namedtuple('Track', 'label, group, shapes')
Tag = namedtuple('Tag', 'frame, label, attributes, group') Tag = namedtuple('Tag', 'frame, label, attributes, group')
Tag.__new__.__defaults__ = (0, ) Tag.__new__.__defaults__ = (0, )
@ -272,11 +272,14 @@ class Annotation:
return Annotation.TrackedShape( return Annotation.TrackedShape(
type=shape["type"], type=shape["type"],
frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step, frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step,
label=self._get_label_name(shape["label_id"]),
points=shape["points"], points=shape["points"],
occluded=shape["occluded"], occluded=shape["occluded"],
z_order=shape.get("z_order", 0),
group=shape.get("group", 0),
outside=shape.get("outside", False), outside=shape.get("outside", False),
keyframe=shape.get("keyframe", True), keyframe=shape.get("keyframe", True),
z_order=shape["z_order"], track_id=shape["track_id"],
attributes=self._export_attributes(shape["attributes"]), attributes=self._export_attributes(shape["attributes"]),
) )
@ -318,7 +321,11 @@ class Annotation:
annotations = {} annotations = {}
data_manager = DataManager(self._annotation_ir) 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)): 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: for tag in self._annotation_ir.tags:
_get_frame(annotations, tag).tags.append(self._export_tag(tag)) _get_frame(annotations, tag).tags.append(self._export_tag(tag))
@ -332,14 +339,17 @@ class Annotation:
@property @property
def tracks(self): 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) tracked_shapes = TrackManager.get_interpolated_shapes(track, 0, self._db_task.data.size)
for tracked_shape in tracked_shapes: for tracked_shape in tracked_shapes:
tracked_shape["attributes"] += track["attributes"] 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( yield Annotation.Track(
label=self._get_label_name(track["label_id"]), 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], shapes=[self._export_tracked_shape(shape) for shape in tracked_shapes],
) )

@ -11,12 +11,10 @@ from copy import deepcopy
def register_format(format_file): def register_format(format_file):
source_code = open(format_file, 'r').read() source_code = open(format_file, 'r').read()
global_vars = { global_vars = {}
"__builtins__": {},
}
exec(source_code, global_vars) exec(source_code, global_vars)
if "format_spec" not in global_vars or not isinstance(global_vars["format_spec"], dict): 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 = deepcopy(global_vars["format_spec"])
format_spec["handler_file"] = File(open(format_file)) format_spec["handler_file"] = File(open(format_file))

@ -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)

@ -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)

@ -4,7 +4,7 @@
import os import os
path_prefix = os.path.join('cvat', 'apps', 'annotation') path_prefix = os.path.join('cvat', 'apps', 'dataset_manager', 'formats')
BUILTIN_FORMATS = ( BUILTIN_FORMATS = (
os.path.join(path_prefix, 'cvat.py'), os.path.join(path_prefix, 'cvat.py'),
os.path.join(path_prefix, 'pascal_voc.py'), os.path.join(path_prefix, 'pascal_voc.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)

@ -140,6 +140,10 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
anno_attr['occluded'] = shape_obj.occluded anno_attr['occluded'] = shape_obj.occluded
anno_attr['z_order'] = shape_obj.z_order 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 anno_points = shape_obj.points
if shape_obj.type == ShapeType.POINTS: if shape_obj.type == ShapeType.POINTS:
anno = datumaro.Points(anno_points, anno = datumaro.Points(anno_points,

@ -29,15 +29,19 @@ def load(file_object, annotations):
dm_dataset = CocoInstancesExtractor(file_object.name) dm_dataset = CocoInstancesExtractor(file_object.name)
import_dm_annotations(dm_dataset, annotations) 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): def dump(file_object, annotations):
import os.path as osp import os.path as osp
import shutil import shutil
from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor
from datumaro.components.project import Environment
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
extractor = CvatAnnotationsExtractor('', annotations) extractor = CvatAnnotationsExtractor('', annotations)
converter = Environment().make_converter('coco_instances', converter = CvatCocoConverter()
crop_covered=True)
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)

@ -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)

@ -22,27 +22,38 @@ format_spec = {
], ],
} }
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): def dump(file_object, annotations):
from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from datumaro.components.project import Environment, Dataset
from tempfile import TemporaryDirectory 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 = CvatAnnotationsExtractor('', annotations)
extractor = extractor.transform(polygons_to_masks) converter = CvatMaskConverter()
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')
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object) make_zip_archive(temp_dir, file_object)

@ -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)

@ -62,19 +62,30 @@ def load(file_object, annotations):
dm_dataset = dm_project.make_dataset() dm_dataset = dm_project.make_dataset()
import_dm_annotations(dm_dataset, annotations) 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): def dump(file_object, annotations):
from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from datumaro.components.project import Environment, Dataset
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
env = Environment()
id_from_image = env.transforms.get('id_from_image_name')
extractor = CvatAnnotationsExtractor('', annotations) extractor = CvatAnnotationsExtractor('', annotations)
extractor = extractor.transform(id_from_image) converter = CvatVocConverter()
extractor = Dataset.from_extractors(extractor) # apply lazy transforms
converter = env.make_converter('voc', label_map='source')
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object) make_zip_archive(temp_dir, file_object)

@ -22,13 +22,18 @@ format_spec = {
], ],
} }
from datumaro.plugins.tf_detection_api_format.converter import \
TfDetectionApiConverter as _TfDetectionApiConverter
class CvatTfrecordConverter(_TfDetectionApiConverter):
NAME = 'cvat_tfrecord'
def dump(file_object, annotations): def dump(file_object, annotations):
from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from datumaro.components.project import Environment
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
extractor = CvatAnnotationsExtractor('', annotations) extractor = CvatAnnotationsExtractor('', annotations)
converter = Environment().make_converter('tf_detection_api') converter = CvatTfrecordConverter()
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object) make_zip_archive(temp_dir, file_object)

@ -27,8 +27,9 @@ def load(file_object, annotations):
import os.path as osp import os.path as osp
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from glob import glob from glob import glob
from datumaro.components.extractor import DatasetItem
from datumaro.plugins.yolo_format.importer import YoloImporter 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") archive_file = file_object if isinstance(file_object, str) else getattr(file_object, "name")
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
@ -37,33 +38,31 @@ def load(file_object, annotations):
image_info = {} image_info = {}
anno_files = glob(osp.join(tmp_dir, '**', '*.txt'), recursive=True) anno_files = glob(osp.join(tmp_dir, '**', '*.txt'), recursive=True)
for filename in anno_files: for filename in anno_files:
filename = osp.basename(filename) filename = osp.splitext(osp.basename(filename))[0]
frame_info = None frame_info = None
try: try:
frame_info = annotations.frame_info[ frame_id = match_frame(DatasetItem(id=filename), annotations)
int(osp.splitext(filename)[0])] frame_info = annotations.frame_info[frame_id]
except Exception:
pass
try:
frame_info = annotations.match_frame(filename)
frame_info = annotations.frame_info[frame_info]
except Exception: except Exception:
pass pass
if frame_info is not None: if frame_info is not None:
image_info[osp.splitext(filename)[0]] = \ image_info[filename] = (frame_info['height'], frame_info['width'])
(frame_info['height'], frame_info['width'])
dm_project = YoloImporter()(tmp_dir, image_info=image_info) dm_project = YoloImporter()(tmp_dir, image_info=image_info)
dm_dataset = dm_project.make_dataset() dm_dataset = dm_project.make_dataset()
import_dm_annotations(dm_dataset, annotations) 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): def dump(file_object, annotations):
from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor from cvat.apps.dataset_manager.bindings import CvatAnnotationsExtractor
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from datumaro.components.project import Environment
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
extractor = CvatAnnotationsExtractor('', annotations) extractor = CvatAnnotationsExtractor('', annotations)
converter = Environment().make_converter('yolo') converter = CvatYoloConverter()
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
converter(extractor, save_dir=temp_dir) converter(extractor, save_dir=temp_dir)
make_zip_archive(temp_dir, file_object) make_zip_archive(temp_dir, file_object)

@ -8,24 +8,23 @@ import json
import os import os
import os.path as osp import os.path as osp
import shutil import shutil
import sys
import tempfile import tempfile
from django.utils import timezone from django.utils import timezone
import django_rq 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.log import slogger
from cvat.apps.engine.models import Task from cvat.apps.engine.models import Task
from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.frame_provider import FrameProvider
from .util import current_function_name, make_zip_archive 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 from datumaro.components.project import Project, Environment
import datumaro.components.extractor as datumaro import datumaro.components.extractor as datumaro
from .bindings import CvatImagesExtractor, CvatTaskExtractor from .bindings import CvatImagesExtractor, CvatTaskExtractor
_FORMATS_DIR = osp.join(osp.dirname(__file__), 'formats')
_MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0] _MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0]
def log_exception(logger=None, exc_info=True): def log_exception(logger=None, exc_info=True):
@ -96,8 +95,10 @@ class TaskProject:
FrameProvider(self._db_task.data))) FrameProvider(self._db_task.data)))
def _import_from_task(self, user): def _import_from_task(self, user):
self._project = Project.generate(self._project_dir, self._project = Project.generate(self._project_dir, config={
config={'project_name': self._db_task.name}) 'project_name': self._db_task.name,
'plugins_dir': _FORMATS_DIR,
})
self._project.add_source('task_%s_images' % self._db_task.id, { self._project.add_source('task_%s_images' % self._db_task.id, {
'format': _TASK_IMAGES_EXTRACTOR, 'format': _TASK_IMAGES_EXTRACTOR,
@ -316,28 +317,40 @@ EXPORT_FORMATS = [
}, },
{ {
'name': 'PASCAL VOC 2012', 'name': 'PASCAL VOC 2012',
'tag': 'voc', 'tag': 'cvat_voc',
'is_default': False, 'is_default': False,
}, },
{ {
'name': 'MS COCO', 'name': 'MS COCO',
'tag': 'coco', 'tag': 'cvat_coco',
'is_default': False, 'is_default': False,
}, },
{ {
'name': 'YOLO', '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, 'is_default': False,
}, },
{ {
'name': 'TF Detection API TFrecord', 'name': 'LabelMe',
'tag': 'tf_detection_api', 'tag': 'cvat_label_me',
'is_default': False, 'is_default': False,
}, },
] ]
def get_export_formats(): def get_export_formats():
converters = Environment().converters converters = Environment(config={
'plugins_dir': _FORMATS_DIR
}).converters
available_formats = set(converters.items) available_formats = set(converters.items)
available_formats.add(EXPORT_FORMAT_DATUMARO_PROJECT) available_formats.add(EXPORT_FORMAT_DATUMARO_PROJECT)

@ -226,12 +226,11 @@ class TrackManager(ObjectManager):
shapes = [] shapes = []
for idx, track in enumerate(self.objects): for idx, track in enumerate(self.objects):
for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame): for shape in TrackManager.get_interpolated_shapes(track, 0, end_frame):
if not shape["outside"]: shape["label_id"] = track["label_id"]
shape["label_id"] = track["label_id"] shape["group"] = track["group"]
shape["group"] = track["group"] shape["track_id"] = idx
shape["track_id"] = idx shape["attributes"] += track["attributes"]
shape["attributes"] += track["attributes"] shapes.append(shape)
shapes.append(shape)
return shapes return shapes
@staticmethod @staticmethod

@ -1878,10 +1878,15 @@ class TaskDataAPITestCase(APITestCase):
def compare_objects(self, obj1, obj2, ignore_keys, fp_tolerance=.001): def compare_objects(self, obj1, obj2, ignore_keys, fp_tolerance=.001):
if isinstance(obj1, dict): if isinstance(obj1, dict):
self.assertTrue(isinstance(obj2, dict), "{} != {}".format(obj1, obj2)) self.assertTrue(isinstance(obj2, dict), "{} != {}".format(obj1, obj2))
for k in obj1.keys(): for k, v1 in obj1.items():
if k in ignore_keys: if k in ignore_keys:
continue 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): elif isinstance(obj1, list):
self.assertTrue(isinstance(obj2, list), "{} != {}".format(obj1, obj2)) self.assertTrue(isinstance(obj2, list), "{} != {}".format(obj1, obj2))
self.assertEqual(len(obj1), len(obj2), "{} != {}".format(obj1, obj2)) self.assertEqual(len(obj1), len(obj2), "{} != {}".format(obj1, obj2))
@ -2475,7 +2480,12 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
def _check_response(self, response, data): def _check_response(self, response, data):
if not response.status_code in [ if not response.status_code in [
status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]: 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): def _run_api_v1_tasks_id_annotations(self, owner, assignee, annotator):
task, _ = self._create_task(owner, assignee) task, _ = self._create_task(owner, assignee)
@ -3049,10 +3059,10 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
annotations["shapes"] = rectangle_shapes_wo_attrs + polygon_shapes_wo_attrs annotations["shapes"] = rectangle_shapes_wo_attrs + polygon_shapes_wo_attrs
annotations["tracks"] = rectangle_tracks_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 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 + \ annotations["shapes"] = rectangle_shapes_with_attrs + \
rectangle_shapes_wo_attrs + \ rectangle_shapes_wo_attrs + \
polygon_shapes_wo_attrs + \ polygon_shapes_wo_attrs + \

@ -415,3 +415,6 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 100 * 1024 * 1024 # 100 MB
DATA_UPLOAD_MAX_NUMBER_FIELDS = None # this django check disabled DATA_UPLOAD_MAX_NUMBER_FIELDS = None # this django check disabled
LOCAL_LOAD_MAX_FILES_COUNT = 500 LOCAL_LOAD_MAX_FILES_COUNT = 500
LOCAL_LOAD_MAX_FILES_SIZE = 512 * 1024 * 1024 # 512 MB LOCAL_LOAD_MAX_FILES_SIZE = 512 * 1024 * 1024 # 512 MB
DATUMARO_PATH = os.path.join(BASE_DIR, 'datumaro')
sys.path.append(DATUMARO_PATH)

@ -10,7 +10,7 @@ import numpy as np
import os import os
import os.path as osp 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 DatasetItem, AnnotationType, Mask, Bbox, Polygon, LabelCategories
) )
from datumaro.components.extractor import Importer from datumaro.components.extractor import Importer
@ -81,9 +81,16 @@ class LabelMeExtractor(SourceExtractor):
for attr in [a.strip() for a in attr_str.split(',') if a.strip()]: for attr in [a.strip() for a in attr_str.split(',') if a.strip()]:
if '=' in attr: if '=' in attr:
name, value = attr.split('=', maxsplit=1) 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)) parsed.append((name, value))
else: else:
parsed.append((attr, '1')) parsed.append((attr, True))
return parsed return parsed
@ -426,10 +433,7 @@ class LabelMeConverter(Converter, CliPlugin):
attrs = [] attrs = []
for k, v in ann.attributes.items(): for k, v in ann.attributes.items():
if isinstance(v, bool): attrs.append('%s=%s' % (k, v))
attrs.append(k)
else:
attrs.append('%s=%s' % (k, v))
ET.SubElement(obj_elem, 'attributes').text = ', '.join(attrs) ET.SubElement(obj_elem, 'attributes').text = ', '.join(attrs)
obj_id += 1 obj_id += 1

@ -90,9 +90,8 @@ class MotSeqExtractor(SourceExtractor):
self._is_gt = is_gt self._is_gt = is_gt
if labels is None: if labels is None:
if osp.isfile(osp.join(seq_root, MotPath.LABELS_FILE)): labels = osp.join(osp.dirname(path), MotPath.LABELS_FILE)
labels = osp.join(seq_root, MotPath.LABELS_FILE) if not osp.isfile(labels):
else:
labels = [lbl.name for lbl in MotLabel] labels = [lbl.name for lbl in MotLabel]
if isinstance(labels, str): if isinstance(labels, str):
labels = self._parse_labels(labels) labels = self._parse_labels(labels)
@ -277,6 +276,8 @@ class MotSeqGtConverter(Converter, CliPlugin):
anno_file = osp.join(anno_dir, MotPath.GT_FILENAME) anno_file = osp.join(anno_dir, MotPath.GT_FILENAME)
with open(anno_file, 'w', encoding="utf-8") as csv_file: with open(anno_file, 'w', encoding="utf-8") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=MotPath.FIELDS) writer = csv.DictWriter(csv_file, fieldnames=MotPath.FIELDS)
track_id_mapping = {-1: -1}
for idx, item in enumerate(extractor): for idx, item in enumerate(extractor):
log.debug("Converting item '%s'", item.id) log.debug("Converting item '%s'", item.id)
@ -286,9 +287,13 @@ class MotSeqGtConverter(Converter, CliPlugin):
if anno.type != AnnotationType.bbox: if anno.type != AnnotationType.bbox:
continue 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({ writer.writerow({
'frame_id': frame_id, 'frame_id': frame_id,
'track_id': int(anno.attributes.get('track_id', -1)), 'track_id': track_id,
'x': anno.x, 'x': anno.x,
'y': anno.y, 'y': anno.y,
'w': anno.w, 'w': anno.w,
@ -310,7 +315,7 @@ class MotSeqGtConverter(Converter, CliPlugin):
else: else:
log.debug("Item '%s' has no image" % item.id) 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: with open(labels_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(l.name f.write('\n'.join(l.name
for l in extractor.categories()[AnnotationType.label].items) for l in extractor.categories()[AnnotationType.label].items)

@ -36,7 +36,10 @@ class LabelMeConverterTest(TestCase):
annotations=[ annotations=[
Bbox(0, 4, 4, 8, label=2, group=2), Bbox(0, 4, 4, 8, label=2, group=2),
Polygon([0, 4, 4, 4, 5, 6], label=3, attributes={ 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, Mask(np.array([[0, 1], [1, 0], [1, 1]]), group=2,
attributes={ 'username': 'test' }), attributes={ 'username': 'test' }),
@ -70,6 +73,9 @@ class LabelMeConverterTest(TestCase):
Polygon([0, 4, 4, 4, 5, 6], label=1, id=1, Polygon([0, 4, 4, 4, 5, 6], label=1, id=1,
attributes={ attributes={
'occluded': True, 'username': '', 'occluded': True, 'username': '',
'a1': 'qwe',
'a2': True,
'a3': 123,
} }
), ),
Mask(np.array([[0, 1], [1, 0], [1, 1]]), group=2, Mask(np.array([[0, 1], [1, 0], [1, 1]]), group=2,
@ -150,7 +156,7 @@ class LabelMeExtractorTest(TestCase):
Polygon([30, 12, 42, 21, 24, 26, 15, 22, 18, 14, 22, 12, 27, 12], Polygon([30, 12, 42, 21, 24, 26, 15, 22, 18, 14, 22, 12, 27, 12],
label=2, group=2, id=2, label=2, group=2, id=2,
attributes={ attributes={
'a1': '1', 'a1': True,
'occluded': True, 'occluded': True,
'username': 'anonymous' 'username': 'anonymous'
} }
@ -158,21 +164,21 @@ class LabelMeExtractorTest(TestCase):
Polygon([35, 21, 43, 22, 40, 28, 28, 31, 31, 22, 32, 25], Polygon([35, 21, 43, 22, 40, 28, 28, 31, 31, 22, 32, 25],
label=3, group=2, id=3, label=3, group=2, id=3,
attributes={ attributes={
'kj': '1', 'kj': True,
'occluded': False, 'occluded': False,
'username': 'anonymous' 'username': 'anonymous'
} }
), ),
Bbox(13, 19, 10, 11, label=4, group=2, id=4, Bbox(13, 19, 10, 11, label=4, group=2, id=4,
attributes={ attributes={
'hg': '1', 'hg': True,
'occluded': True, 'occluded': True,
'username': 'anonymous' 'username': 'anonymous'
} }
), ),
Mask(mask2, label=5, group=1, id=5, Mask(mask2, label=5, group=1, id=5,
attributes={ attributes={
'd': '1', 'd': True,
'occluded': False, 'occluded': False,
'username': 'anonymous' 'username': 'anonymous'
} }
@ -180,7 +186,7 @@ class LabelMeExtractorTest(TestCase):
Polygon([64, 21, 74, 24, 72, 32, 62, 34, 60, 27, 62, 22], Polygon([64, 21, 74, 24, 72, 32, 62, 34, 60, 27, 62, 22],
label=6, group=1, id=6, label=6, group=1, id=6,
attributes={ attributes={
'gfd lkj lkj hi': '1', 'gfd lkj lkj hi': True,
'occluded': False, 'occluded': False,
'username': 'anonymous' 'username': 'anonymous'
} }

Loading…
Cancel
Save