diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a81cc7d..79ed9f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () - Basic functions of CVAT works without activated nuclio dashboard +- Fixed a case in which exported masks could have wrong color order () - Fixed error with creating task with labels with the same name () - Django RQ dashboard view () diff --git a/datumaro/datumaro/plugins/voc_format/converter.py b/datumaro/datumaro/plugins/voc_format/converter.py index 0de87dfb..65e586d8 100644 --- a/datumaro/datumaro/plugins/voc_format/converter.py +++ b/datumaro/datumaro/plugins/voc_format/converter.py @@ -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 @@ -102,6 +102,8 @@ class VocConverter(Converter): 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): @@ -446,7 +448,7 @@ class VocConverter(Converter): 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() @@ -456,10 +458,8 @@ class VocConverter(Converter): # 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(): @@ -467,60 +467,45 @@ class VocConverter(Converter): 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): diff --git a/datumaro/datumaro/plugins/voc_format/format.py b/datumaro/datumaro/plugins/voc_format/format.py index 5af79f2d..471866be 100644 --- a/datumaro/datumaro/plugins/voc_format/format.py +++ b/datumaro/datumaro/plugins/voc_format/format.py @@ -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, \ @@ -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() @@ -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 \ No newline at end of file diff --git a/datumaro/tests/test_voc_format.py b/datumaro/tests/test_voc_format.py index 52f9403c..e83a7430 100644 --- a/datumaro/tests/test_voc_format.py +++ b/datumaro/tests/test_voc_format.py @@ -472,53 +472,6 @@ class VocConverterTest(TestCase): 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): @@ -602,8 +555,8 @@ class VocConverterTest(TestCase): 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) @@ -616,11 +569,11 @@ class VocConverterTest(TestCase): 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): @@ -633,14 +586,19 @@ class VocConverterTest(TestCase): 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, @@ -649,12 +607,12 @@ class VocConverterTest(TestCase): '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(),