Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attributes in VOC format #1792

Merged
merged 6 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ClamAV antivirus integration (<https://github.com/opencv/cvat/pull/1712>)
- Supported import and export or single boxes in MOT format (https://github.com/opencv/cvat/pull/1764)
- [Datumaro] Added `stats` command, which shows some dataset statistics like image mean and std (https://github.com/opencv/cvat/pull/1734)
- Add option to upload annotations upon task creation on CLI
- Add option to upload annotations upon task creation on CLI
- Polygon and polylines interpolation (<https://github.com/opencv/cvat/pull/1571>)
- Ability to redraw shape from scratch (Shift + N) for an activated shape (<https://github.com/opencv/cvat/pull/1571>)
- Highlights for the first point of a polygon/polyline and direction (<https://github.com/opencv/cvat/pull/1571>)
Expand All @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update https install manual. Makes it easier and more robust. Includes automatic renewing of lets encrypt certificates.
- 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())