Skip to content

Commit

Permalink
[Datumaro] Mandatory sorting for black color in voc masks (#2048)
Browse files Browse the repository at this point in the history
* Remove `guess`, add mandatory sorting and black label

* update changelog

Co-authored-by: Nikita Manovich <[email protected]>
  • Loading branch information
zhiltsov-max and Nikita Manovich authored Aug 26, 2020
1 parent 2510d4d commit 55073fb
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 104 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Issue loading openvino models for semi-automatic and automatic annotation (<https://github.com/opencv/cvat/pull/1996>)
- Basic functions of CVAT works without activated nuclio dashboard
- Fixed a case in which exported masks could have wrong color order (<https://github.com/opencv/cvat/issues/2032>)
- Fixed error with creating task with labels with the same name (<https://github.com/opencv/cvat/pull/2031>)
- Django RQ dashboard view (<https://github.com/opencv/cvat/pull/2069>)

Expand Down
61 changes: 23 additions & 38 deletions datumaro/datumaro/plugins/voc_format/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _write_xml_bbox(bbox, parent_elem):
return bbox_elem


LabelmapType = Enum('LabelmapType', ['voc', 'source', 'guess'])
LabelmapType = Enum('LabelmapType', ['voc', 'source'])

class VocConverter(Converter):
DEFAULT_IMAGE_EXT = VocPath.IMAGE_EXT
Expand Down Expand Up @@ -102,6 +102,8 @@ def __init__(self, extractor, save_dir,
self._apply_colormap = apply_colormap
self._allow_attributes = allow_attributes

if label_map is None:
label_map = LabelmapType.source
self._load_categories(label_map)

def apply(self):
Expand Down Expand Up @@ -446,7 +448,7 @@ def save_label_map(self):
path = osp.join(self._save_dir, VocPath.LABELMAP_FILE)
write_label_map(path, self._label_map)

def _load_categories(self, label_map_source=None):
def _load_categories(self, label_map_source):
if label_map_source == LabelmapType.voc.name:
# use the default VOC colormap
label_map = make_voc_label_map()
Expand All @@ -456,71 +458,54 @@ def _load_categories(self, label_map_source=None):
# generate colormap for input labels
labels = self._extractor.categories() \
.get(AnnotationType.label, LabelCategories())
label_map = OrderedDict()
label_map['background'] = [None, [], []]
for item in labels.items:
label_map[item.name] = [None, [], []]
label_map = OrderedDict((item.name, [None, [], []])
for item in labels.items)

elif label_map_source == LabelmapType.source.name and \
AnnotationType.mask in self._extractor.categories():
# use source colormap
labels = self._extractor.categories()[AnnotationType.label]
colors = self._extractor.categories()[AnnotationType.mask]
label_map = OrderedDict()
has_black = False
for idx, item in enumerate(labels.items):
color = colors.colormap.get(idx)
if idx is not None:
if color == (0, 0, 0):
has_black = True
if color is not None:
label_map[item.name] = [color, [], []]
if not has_black and 'background' not in label_map:
label_map['background'] = [(0, 0, 0), [], []]
label_map.move_to_end('background', last=False)

elif label_map_source in [LabelmapType.guess.name, None]:
# generate colormap for union of VOC and input dataset labels
label_map = make_voc_label_map()

rebuild_colormap = False
source_labels = self._extractor.categories() \
.get(AnnotationType.label, LabelCategories())
for label in source_labels.items:
if label.name not in label_map:
rebuild_colormap = True
if label.attributes or label.name not in label_map:
label_map[label.name] = [None, [], label.attributes]

if rebuild_colormap:
for item in label_map.values():
item[0] = None

elif isinstance(label_map_source, dict):
label_map = label_map_source
label_map = OrderedDict(
sorted(label_map_source.items(), key=lambda e: e[0]))

elif isinstance(label_map_source, str) and osp.isfile(label_map_source):
label_map = parse_label_map(label_map_source)

has_black = find(label_map.items(),
lambda e: e[0] == 'background' or e[1][0] == (0, 0, 0))
if not has_black and 'background' not in label_map:
label_map['background'] = [(0, 0, 0), [], []]
label_map.move_to_end('background', last=False)

else:
raise Exception("Wrong labelmap specified, "
"expected one of %s or a file path" % \
', '.join(t.name for t in LabelmapType))

# There must always be a label with color (0, 0, 0) at index 0
bg_label = find(label_map.items(), lambda x: x[1][0] == (0, 0, 0))
if bg_label is not None:
bg_label = bg_label[0]
else:
bg_label = 'background'
if bg_label not in label_map:
has_colors = any(v[0] is not None for v in label_map.values())
color = (0, 0, 0) if has_colors else None
label_map[bg_label] = [color, [], []]
label_map.move_to_end(bg_label, last=False)

self._categories = make_voc_categories(label_map)

self._label_map = label_map
# Update colors with assigned values
colormap = self._categories[AnnotationType.mask].colormap
for label_id, color in colormap.items():
label_desc = label_map[
self._categories[AnnotationType.label].items[label_id].name]
label_desc[0] = color

self._label_map = label_map
self._label_id_mapping = self._make_label_id_map()

def _is_label(self, s):
Expand Down
15 changes: 8 additions & 7 deletions datumaro/datumaro/plugins/voc_format/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def parse_label_map(path):
label_desc = line.strip().split(':')
name = label_desc[0]

if name in label_map:
raise ValueError("Label '%s' is already defined" % name)

if 1 < len(label_desc) and len(label_desc[1]) != 0:
color = label_desc[1].split(',')
assert len(color) == 3, \
Expand Down Expand Up @@ -173,7 +176,6 @@ def write_label_map(path, label_map):

f.write('%s\n' % ':'.join([label_name, color_rgb, parts, actions]))

# pylint: disable=pointless-statement
def make_voc_categories(label_map=None):
if label_map is None:
label_map = make_voc_label_map()
Expand All @@ -190,16 +192,15 @@ def make_voc_categories(label_map=None):
label_categories.add(part)
categories[AnnotationType.label] = label_categories

has_colors = sum(v[0] is not None for v in label_map.values())
if not has_colors:
has_colors = any(v[0] is not None for v in label_map.values())
if not has_colors: # generate new colors
colormap = generate_colormap(len(label_map))
else:
else: # only copy defined colors
label_id = lambda label: label_categories.find(label)[0]
colormap = { label_id(name): desc[0]
for name, desc in label_map.items() }
for name, desc in label_map.items() if desc[0] is not None }
mask_categories = MaskCategories(colormap)
mask_categories.inverse_colormap # force init
mask_categories.inverse_colormap # pylint: disable=pointless-statement
categories[AnnotationType.mask] = mask_categories

return categories
# pylint: enable=pointless-statement
76 changes: 17 additions & 59 deletions datumaro/tests/test_voc_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,53 +472,6 @@ def categories(self):
partial(VocConverter.convert, label_map='voc'),
test_dir, target_dataset=DstExtractor())

def test_dataset_with_guessed_labelmap(self):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=0, id=1),
Bbox(1, 2, 3, 4, label=1, id=2),
])

def categories(self):
label_cat = LabelCategories()
label_cat.add(VOC.VocLabel(1).name)
label_cat.add('non_voc_label')
return {
AnnotationType.label: label_cat,
}

class DstExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=self._label(VOC.VocLabel(1).name),
id=1, group=1, attributes={
'truncated': False,
'difficult': False,
'occluded': False,
}
),
Bbox(1, 2, 3, 4, label=self._label('non_voc_label'),
id=2, group=2, attributes={
'truncated': False,
'difficult': False,
'occluded': False,
}
),
])

def categories(self):
label_map = VOC.make_voc_label_map()
label_map['non_voc_label'] = [None, [], []]
for label_desc in label_map.values():
label_desc[0] = None # rebuild colormap
return VOC.make_voc_categories(label_map)

with TestDir() as test_dir:
self._test_save_and_load(SrcExtractor(),
partial(VocConverter.convert, label_map='guess'),
test_dir, target_dataset=DstExtractor())

def test_dataset_with_source_labelmap_undefined(self):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
Expand Down Expand Up @@ -602,8 +555,8 @@ def __iter__(self):

def categories(self):
label_map = OrderedDict()
label_map['label_1'] = [(1, 2, 3), [], []]
label_map['background'] = [(0, 0, 0), [], []]
label_map['label_1'] = [(1, 2, 3), [], []]
label_map['label_2'] = [(3, 2, 1), [], []]
return VOC.make_voc_categories(label_map)

Expand All @@ -616,11 +569,11 @@ def test_dataset_with_fixed_labelmap(self):
class SrcExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(2, 3, 4, 5, label=0, id=1),
Bbox(1, 2, 3, 4, label=1, id=2, group=2,
Bbox(2, 3, 4, 5, label=self._label('foreign_label'), id=1),
Bbox(1, 2, 3, 4, label=self._label('label'), id=2, group=2,
attributes={'act1': True}),
Bbox(2, 3, 4, 5, label=2, id=3, group=2),
Bbox(2, 3, 4, 6, label=3, id=4, group=2),
Bbox(2, 3, 4, 5, label=self._label('label_part1'), group=2),
Bbox(2, 3, 4, 6, label=self._label('label_part2'), group=2),
])

def categories(self):
Expand All @@ -633,14 +586,19 @@ def categories(self):
AnnotationType.label: label_cat,
}

label_map = {
'label': [None, ['label_part1', 'label_part2'], ['act1', 'act2']]
}
label_map = OrderedDict([
('label', [None, ['label_part1', 'label_part2'], ['act1', 'act2']])
])

dst_label_map = OrderedDict([
('background', [None, [], []]),
('label', [None, ['label_part1', 'label_part2'], ['act1', 'act2']])
])

class DstExtractor(TestExtractorBase):
def __iter__(self):
yield DatasetItem(id=1, annotations=[
Bbox(1, 2, 3, 4, label=0, id=1, group=1,
Bbox(1, 2, 3, 4, label=self._label('label'), id=1, group=1,
attributes={
'act1': True,
'act2': False,
Expand All @@ -649,12 +607,12 @@ def __iter__(self):
'occluded': False,
}
),
Bbox(2, 3, 4, 5, label=1, group=1),
Bbox(2, 3, 4, 6, label=2, group=1),
Bbox(2, 3, 4, 5, label=self._label('label_part1'), group=1),
Bbox(2, 3, 4, 6, label=self._label('label_part2'), group=1),
])

def categories(self):
return VOC.make_voc_categories(label_map)
return VOC.make_voc_categories(dst_label_map)

with TestDir() as test_dir:
self._test_save_and_load(SrcExtractor(),
Expand Down

0 comments on commit 55073fb

Please sign in to comment.