Add attributes in VOC format (#1792)

* Add voc attributes

* Allow any values for voc pose

* update changelog

* Add attribute conversion

* linter

* fix tests
main
zhiltsov-max 6 years ago committed by GitHub
parent 49a7ad59ed
commit 962f61fa27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -49,7 +49,8 @@ class TaskData:
(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():
@ -57,6 +58,7 @@ class TaskData:
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():
@ -317,10 +319,28 @@ class TaskData:
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()

@ -1988,7 +1988,7 @@ class JobAnnotationAPITestCase(APITestCase):
"name": "parked",
"mutable": True,
"input_type": "checkbox",
"default_value": False
"default_value": "false"
},
]
},

@ -53,7 +53,8 @@ LabelmapType = Enum('LabelmapType', ['voc', 'source', 'guess'])
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)
@ -66,6 +67,7 @@ class _Converter:
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)
@ -205,9 +207,8 @@ class _Converter:
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)
@ -252,6 +253,21 @@ class _Converter:
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,
@ -565,15 +581,17 @@ class VocConverter(Converter, CliPlugin):
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 = {
@ -581,6 +599,7 @@ class VocConverter(Converter, CliPlugin):
'save_images': save_images,
'apply_colormap': apply_colormap,
'label_map': label_map,
'allow_attributes': allow_attributes,
}
def __call__(self, extractor, save_dir):

@ -198,6 +198,12 @@ class _VocXmlExtractor(_VocExtractor):
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:

@ -395,8 +395,8 @@ class VocConverterTest(TestCase):
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):
@ -679,3 +679,34 @@ class VocConverterTest(TestCase):
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())
Loading…
Cancel
Save