Skip to content

Commit

Permalink
Add attributes in VOC format (cvat-ai#1792)
Browse files Browse the repository at this point in the history
* Add voc attributes

* Allow any values for voc pose

* update changelog

* Add attribute conversion

* linter

* fix tests
  • Loading branch information
zhiltsov-max authored and Fernando Martínez González committed Aug 3, 2020
1 parent 765a731 commit e79cda9
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Settings page move to the modal. (<https://github.com/opencv/cvat/pull/1705>)
- Implemented import and export of annotations with relative image paths (<https://github.com/opencv/cvat/pull/1463>)
- Using only single click to start editing or remove a point (<https://github.com/opencv/cvat/pull/1571>)
- Added support for attributes in VOC XML format (https://github.com/opencv/cvat/pull/1792)

### Deprecated
-
Expand Down
30 changes: 25 additions & 5 deletions cvat/apps/dataset_manager/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ def __init__(self, annotation_ir, db_task, host='', create_callback=None):
(db_label.id, db_label) for db_label in db_labels)

self._attribute_mapping = {db_label.id: {
'mutable': {}, 'immutable': {}} for db_label in db_labels}
'mutable': {}, 'immutable': {}, 'spec': {}}
for db_label in db_labels}

for db_label in db_labels:
for db_attribute in db_label.attributespec_set.all():
if db_attribute.mutable:
self._attribute_mapping[db_label.id]['mutable'][db_attribute.id] = db_attribute.name
else:
self._attribute_mapping[db_label.id]['immutable'][db_attribute.id] = db_attribute.name
self._attribute_mapping[db_label.id]['spec'][db_attribute.id] = db_attribute

self._attribute_mapping_merged = {}
for label_id, attr_mapping in self._attribute_mapping.items():
Expand Down Expand Up @@ -317,10 +319,28 @@ def _import_tag(self, tag):
return _tag

def _import_attribute(self, label_id, attribute):
return {
'spec_id': self._get_attribute_id(label_id, attribute.name),
'value': attribute.value,
}
spec_id = self._get_attribute_id(label_id, attribute.name)
value = attribute.value

if spec_id:
spec = self._attribute_mapping[label_id]['spec'][spec_id]

try:
if spec.input_type == AttributeType.NUMBER:
pass # no extra processing required
elif spec.input_type == AttributeType.CHECKBOX:
if isinstance(value, str):
value = value.lower()
assert value in {'true', 'false'}
elif isinstance(value, (bool, int, float)):
value = 'true' if value else 'false'
else:
raise ValueError("Unexpected attribute value")
except Exception as e:
raise Exception("Failed to convert attribute '%s'='%s': %s" %
(self._get_label_name(label_id), value, e))

return { 'spec_id': spec_id, 'value': value }

def _import_shape(self, shape):
_shape = shape._asdict()
Expand Down
2 changes: 1 addition & 1 deletion cvat/apps/engine/tests/_test_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1988,7 +1988,7 @@ def _create_task(self, owner, assignee):
"name": "parked",
"mutable": True,
"input_type": "checkbox",
"default_value": False
"default_value": "false"
},
]
},
Expand Down
33 changes: 26 additions & 7 deletions datumaro/datumaro/plugins/voc_format/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ def _write_xml_bbox(bbox, parent_elem):

class _Converter:
def __init__(self, extractor, save_dir,
tasks=None, apply_colormap=True, save_images=False, label_map=None):
tasks=None, apply_colormap=True, save_images=False, label_map=None,
allow_attributes=True):
assert tasks is None or isinstance(tasks, (VocTask, list, set))
if tasks is None:
tasks = set(VocTask)
Expand All @@ -66,6 +67,7 @@ def __init__(self, extractor, save_dir,
self._extractor = extractor
self._save_dir = save_dir
self._apply_colormap = apply_colormap
self._allow_attributes = allow_attributes
self._save_images = save_images

self._load_categories(label_map)
Expand Down Expand Up @@ -205,9 +207,8 @@ def save_subsets(self):
ET.SubElement(obj_elem, 'name').text = obj_label

if 'pose' in attr:
pose = _convert_attr('pose', attr,
lambda v: VocPose[v], VocPose.Unspecified)
ET.SubElement(obj_elem, 'pose').text = pose.name
ET.SubElement(obj_elem, 'pose').text = \
str(attr['pose'])

if 'truncated' in attr:
truncated = _convert_attr('truncated', attr, int, 0)
Expand Down Expand Up @@ -252,6 +253,21 @@ def save_subsets(self):
if len(actions_elem) != 0:
obj_elem.append(actions_elem)

if self._allow_attributes:
native_attrs = {'difficult', 'pose',
'truncated', 'occluded' }
native_attrs.update(label_actions)

attrs_elem = ET.Element('attributes')
for k, v in attr.items():
if k in native_attrs:
continue
attr_elem = ET.SubElement(attrs_elem, 'attribute')
ET.SubElement(attr_elem, 'name').text = str(k)
ET.SubElement(attr_elem, 'value').text = str(v)
if len(attrs_elem):
obj_elem.append(attrs_elem)

if self._tasks & {None,
VocTask.detection,
VocTask.person_layout,
Expand Down Expand Up @@ -565,22 +581,25 @@ def build_cmdline_parser(cls, **kwargs):
parser.add_argument('--label-map', type=cls._get_labelmap, default=None,
help="Labelmap file path or one of %s" % \
', '.join(t.name for t in LabelmapType))
parser.add_argument('--allow-attributes',
type=str_to_bool, default=True,
help="Allow export of attributes (default: %(default)s)")
parser.add_argument('--tasks', type=cls._split_tasks_string,
default=None,
help="VOC task filter, comma-separated list of {%s} "
"(default: all)" % ', '.join([t.name for t in VocTask]))
"(default: all)" % ', '.join(t.name for t in VocTask))

return parser

def __init__(self, tasks=None, save_images=False,
apply_colormap=False, label_map=None):
apply_colormap=False, label_map=None, allow_attributes=True):
super().__init__()

self._options = {
'tasks': tasks,
'save_images': save_images,
'apply_colormap': apply_colormap,
'label_map': label_map,
'allow_attributes': allow_attributes,
}

def __call__(self, extractor, save_dir):
Expand Down
6 changes: 6 additions & 0 deletions datumaro/datumaro/plugins/voc_format/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ def _parse_annotations(self, root_elem):
item_annotations.append(Bbox(*part_bbox, label=part_label_id,
group=group))

attributes_elem = object_elem.find('attributes')
if attributes_elem is not None:
for attr_elem in attributes_elem.iter('attribute'):
attributes[attr_elem.find('name').text] = \
attr_elem.find('value').text

if self._task is VocTask.person_layout and not has_parts:
continue
if self._task is VocTask.action_classification and not actions:
Expand Down
35 changes: 33 additions & 2 deletions datumaro/tests/test_voc_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,8 @@ def __iter__(self):

with TestDir() as test_dir:
self._test_save_and_load(TestExtractor(),
VocActionConverter(label_map='voc'), test_dir,
target_dataset=DstExtractor())
VocActionConverter(label_map='voc', allow_attributes=False),
test_dir, target_dataset=DstExtractor())

def test_can_save_dataset_with_no_subsets(self):
class TestExtractor(TestExtractorBase):
Expand Down Expand Up @@ -679,3 +679,34 @@ def __iter__(self):
with TestDir() as test_dir:
self._test_save_and_load(TestExtractor(),
VocConverter(label_map='voc', save_images=True), test_dir)

def test_can_save_attributes(self):
class TestExtractor(TestExtractorBase):
def __iter__(self):
return iter([
DatasetItem(id='a', annotations=[
Bbox(2, 3, 4, 5, label=2,
attributes={ 'occluded': True, 'x': 1, 'y': '2' }
),
]),
])

class DstExtractor(TestExtractorBase):
def __iter__(self):
return iter([
DatasetItem(id='a', annotations=[
Bbox(2, 3, 4, 5, label=2, id=1, group=1,
attributes={
'truncated': False,
'difficult': False,
'occluded': True,
'x': '1', 'y': '2', # can only read strings
}
),
]),
])

with TestDir() as test_dir:
self._test_save_and_load(TestExtractor(),
VocDetectionConverter(label_map='voc'), test_dir,
target_dataset=DstExtractor())

0 comments on commit e79cda9

Please sign in to comment.