[Datumaro] Add cvat format export (#1034)
* Add cvat format export * Remove wrong items in testmain
parent
43c5fd0088
commit
8da20b38d5
@ -0,0 +1,337 @@
|
||||
|
||||
# Copyright (C) 2019 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from collections import OrderedDict
|
||||
import os
|
||||
import os.path as osp
|
||||
from xml.sax.saxutils import XMLGenerator
|
||||
|
||||
from datumaro.components.converter import Converter
|
||||
from datumaro.components.extractor import DEFAULT_SUBSET_NAME, AnnotationType
|
||||
from datumaro.components.formats.cvat import CvatPath
|
||||
from datumaro.util.image import save_image
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
a = iter(iterable)
|
||||
return zip(a, a)
|
||||
|
||||
class XmlAnnotationWriter:
|
||||
VERSION = '1.1'
|
||||
|
||||
def __init__(self, f):
|
||||
self.xmlgen = XMLGenerator(f, 'utf-8')
|
||||
self._level = 0
|
||||
|
||||
def _indent(self, newline = True):
|
||||
if newline:
|
||||
self.xmlgen.ignorableWhitespace('\n')
|
||||
self.xmlgen.ignorableWhitespace(' ' * self._level)
|
||||
|
||||
def _add_version(self):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('version', {})
|
||||
self.xmlgen.characters(self.VERSION)
|
||||
self.xmlgen.endElement('version')
|
||||
|
||||
def open_root(self):
|
||||
self.xmlgen.startDocument()
|
||||
self.xmlgen.startElement('annotations', {})
|
||||
self._level += 1
|
||||
self._add_version()
|
||||
|
||||
def _add_meta(self, meta):
|
||||
self._level += 1
|
||||
for k, v in meta.items():
|
||||
if isinstance(v, OrderedDict):
|
||||
self._indent()
|
||||
self.xmlgen.startElement(k, {})
|
||||
self._add_meta(v)
|
||||
self._indent()
|
||||
self.xmlgen.endElement(k)
|
||||
elif isinstance(v, list):
|
||||
self._indent()
|
||||
self.xmlgen.startElement(k, {})
|
||||
for tup in v:
|
||||
self._add_meta(OrderedDict([tup]))
|
||||
self._indent()
|
||||
self.xmlgen.endElement(k)
|
||||
else:
|
||||
self._indent()
|
||||
self.xmlgen.startElement(k, {})
|
||||
self.xmlgen.characters(v)
|
||||
self.xmlgen.endElement(k)
|
||||
self._level -= 1
|
||||
|
||||
def write_meta(self, meta):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('meta', {})
|
||||
self._add_meta(meta)
|
||||
self._indent()
|
||||
self.xmlgen.endElement('meta')
|
||||
|
||||
def open_track(self, track):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('track', track)
|
||||
self._level += 1
|
||||
|
||||
def open_image(self, image):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('image', image)
|
||||
self._level += 1
|
||||
|
||||
def open_box(self, box):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('box', box)
|
||||
self._level += 1
|
||||
|
||||
def open_polygon(self, polygon):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('polygon', polygon)
|
||||
self._level += 1
|
||||
|
||||
def open_polyline(self, polyline):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('polyline', polyline)
|
||||
self._level += 1
|
||||
|
||||
def open_points(self, points):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('points', points)
|
||||
self._level += 1
|
||||
|
||||
def add_attribute(self, attribute):
|
||||
self._indent()
|
||||
self.xmlgen.startElement('attribute', {'name': attribute['name']})
|
||||
self.xmlgen.characters(attribute['value'])
|
||||
self.xmlgen.endElement('attribute')
|
||||
|
||||
def _close_element(self, element):
|
||||
self._level -= 1
|
||||
self._indent()
|
||||
self.xmlgen.endElement(element)
|
||||
|
||||
def close_box(self):
|
||||
self._close_element('box')
|
||||
|
||||
def close_polygon(self):
|
||||
self._close_element('polygon')
|
||||
|
||||
def close_polyline(self):
|
||||
self._close_element('polyline')
|
||||
|
||||
def close_points(self):
|
||||
self._close_element('points')
|
||||
|
||||
def close_image(self):
|
||||
self._close_element('image')
|
||||
|
||||
def close_track(self):
|
||||
self._close_element('track')
|
||||
|
||||
def close_root(self):
|
||||
self._close_element('annotations')
|
||||
self.xmlgen.endDocument()
|
||||
|
||||
class _SubsetWriter:
|
||||
def __init__(self, file, name, extractor, context):
|
||||
self._writer = XmlAnnotationWriter(file)
|
||||
self._name = name
|
||||
self._extractor = extractor
|
||||
self._context = context
|
||||
|
||||
def write(self):
|
||||
self._writer.open_root()
|
||||
self._write_meta()
|
||||
|
||||
for item in self._extractor:
|
||||
if self._context._save_images:
|
||||
self._save_image(item)
|
||||
self._write_item(item)
|
||||
|
||||
self._writer.close_root()
|
||||
|
||||
def _save_image(self, item):
|
||||
image = item.image
|
||||
if image is None:
|
||||
return
|
||||
|
||||
image_path = osp.join(self._context._images_dir,
|
||||
str(item.id) + CvatPath.IMAGE_EXT)
|
||||
save_image(image_path, image)
|
||||
|
||||
def _write_item(self, item):
|
||||
h, w = 0, 0
|
||||
if item.has_image:
|
||||
h, w = item.image.shape[:2]
|
||||
self._writer.open_image(OrderedDict([
|
||||
("id", str(item.id)),
|
||||
("name", str(item.id)),
|
||||
("width", str(w)),
|
||||
("height", str(h))
|
||||
]))
|
||||
|
||||
for ann in item.annotations:
|
||||
if ann.type in {AnnotationType.points, AnnotationType.polyline,
|
||||
AnnotationType.polygon, AnnotationType.bbox}:
|
||||
self._write_shape(ann)
|
||||
else:
|
||||
continue
|
||||
|
||||
self._writer.close_image()
|
||||
|
||||
def _write_meta(self):
|
||||
label_cat = self._extractor.categories()[AnnotationType.label]
|
||||
meta = OrderedDict([
|
||||
("task", OrderedDict([
|
||||
("id", ""),
|
||||
("name", self._name),
|
||||
("size", str(len(self._extractor))),
|
||||
("mode", "annotation"),
|
||||
("overlap", ""),
|
||||
("start_frame", "0"),
|
||||
("stop_frame", str(len(self._extractor))),
|
||||
("frame_filter", ""),
|
||||
("z_order", "True"),
|
||||
|
||||
("labels", [
|
||||
("label", OrderedDict([
|
||||
("name", label.name),
|
||||
("attributes", [
|
||||
("attribute", OrderedDict([
|
||||
("name", attr),
|
||||
("mutable", "True"),
|
||||
("input_type", "text"),
|
||||
("default_value", ""),
|
||||
("values", ""),
|
||||
])) for attr in label.attributes
|
||||
])
|
||||
])) for label in label_cat.items
|
||||
]),
|
||||
])),
|
||||
])
|
||||
self._writer.write_meta(meta)
|
||||
|
||||
def _get_label(self, label_id):
|
||||
label_cat = self._extractor.categories()[AnnotationType.label]
|
||||
return label_cat.items[label_id]
|
||||
|
||||
def _write_shape(self, shape):
|
||||
if shape.label is None:
|
||||
return
|
||||
|
||||
shape_data = OrderedDict([
|
||||
("label", self._get_label(shape.label).name),
|
||||
("occluded", str(int(shape.attributes.get('occluded', False)))),
|
||||
])
|
||||
|
||||
points = shape.get_points()
|
||||
if shape.type == AnnotationType.bbox:
|
||||
shape_data.update(OrderedDict([
|
||||
("xtl", "{:.2f}".format(points[0])),
|
||||
("ytl", "{:.2f}".format(points[1])),
|
||||
("xbr", "{:.2f}".format(points[2])),
|
||||
("ybr", "{:.2f}".format(points[3]))
|
||||
]))
|
||||
else:
|
||||
shape_data.update(OrderedDict([
|
||||
("points", ';'.join((
|
||||
','.join((
|
||||
"{:.2f}".format(x),
|
||||
"{:.2f}".format(y)
|
||||
)) for x, y in pairwise(points))
|
||||
)),
|
||||
]))
|
||||
|
||||
shape_data['z_order'] = str(int(shape.attributes.get('z_order', 0)))
|
||||
if shape.group is not None:
|
||||
shape_data['group_id'] = str(shape.group)
|
||||
|
||||
if shape.type == AnnotationType.bbox:
|
||||
self._writer.open_box(shape_data)
|
||||
elif shape.type == AnnotationType.polygon:
|
||||
self._writer.open_polygon(shape_data)
|
||||
elif shape.type == AnnotationType.polyline:
|
||||
self._writer.open_polyline(shape_data)
|
||||
elif shape.type == AnnotationType.points:
|
||||
self._writer.open_points(shape_data)
|
||||
else:
|
||||
raise NotImplementedError("unknown shape type")
|
||||
|
||||
for attr_name, attr_value in shape.attributes.items():
|
||||
if attr_name in self._get_label(shape.label).attributes:
|
||||
self._writer.add_attribute(OrderedDict([
|
||||
("name", str(attr_name)),
|
||||
("value", str(attr_value)),
|
||||
]))
|
||||
|
||||
if shape.type == AnnotationType.bbox:
|
||||
self._writer.close_box()
|
||||
elif shape.type == AnnotationType.polygon:
|
||||
self._writer.close_polygon()
|
||||
elif shape.type == AnnotationType.polyline:
|
||||
self._writer.close_polyline()
|
||||
elif shape.type == AnnotationType.points:
|
||||
self._writer.close_points()
|
||||
else:
|
||||
raise NotImplementedError("unknown shape type")
|
||||
|
||||
class _Converter:
|
||||
def __init__(self, extractor, save_dir, save_images=False):
|
||||
self._extractor = extractor
|
||||
self._save_dir = save_dir
|
||||
self._save_images = save_images
|
||||
|
||||
def convert(self):
|
||||
os.makedirs(self._save_dir, exist_ok=True)
|
||||
|
||||
images_dir = osp.join(self._save_dir, CvatPath.IMAGES_DIR)
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
self._images_dir = images_dir
|
||||
|
||||
annotations_dir = osp.join(self._save_dir, CvatPath.ANNOTATIONS_DIR)
|
||||
os.makedirs(annotations_dir, exist_ok=True)
|
||||
self._annotations_dir = annotations_dir
|
||||
|
||||
subsets = self._extractor.subsets()
|
||||
if len(subsets) == 0:
|
||||
subsets = [ None ]
|
||||
|
||||
for subset_name in subsets:
|
||||
if subset_name:
|
||||
subset = self._extractor.get_subset(subset_name)
|
||||
else:
|
||||
subset_name = DEFAULT_SUBSET_NAME
|
||||
subset = self._extractor
|
||||
|
||||
with open(osp.join(annotations_dir, '%s.xml' % subset_name), 'w') as f:
|
||||
writer = _SubsetWriter(f, subset_name, subset, self)
|
||||
writer.write()
|
||||
|
||||
class CvatConverter(Converter):
|
||||
def __init__(self, save_images=False, cmdline_args=None):
|
||||
super().__init__()
|
||||
|
||||
self._options = {
|
||||
'save_images': save_images,
|
||||
}
|
||||
|
||||
if cmdline_args is not None:
|
||||
self._options.update(self._parse_cmdline(cmdline_args))
|
||||
|
||||
@classmethod
|
||||
def build_cmdline_parser(cls, parser=None):
|
||||
import argparse
|
||||
if not parser:
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument('--save-images', action='store_true',
|
||||
help="Save images (default: %(default)s)")
|
||||
|
||||
return parser
|
||||
|
||||
def __call__(self, extractor, save_dir):
|
||||
converter = _Converter(extractor, save_dir, **self._options)
|
||||
converter.convert()
|
||||
Loading…
Reference in New Issue