From 7407d121b585fff00cbde3088a206abd7f576aa5 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 11 Dec 2020 10:10:10 +0300 Subject: [PATCH] Release 0.1.4 (#64) * Relax importer for Pascal VOC dataset (search in subdirectories) (#50) In some cases developers don't want to specify the exact path to Pascal VOC. Now you have to specify VOCtrainval_11-May-2012/VOCdevkit/VOC2012/. After the patch it will be possible to specify VOCtrainval_11-May-2012/. * Allow missing supercategory in COCO annotations (#54) Now it is possible to load coco_instances dataset even if the annotation file doesn't have supercategory * Add CamVid format support (#55) Co-authored-by: Maxim Zhiltsov * Fix CamVid format (#57) * Fix ImageNet format * Fix CamVid format * ability to install opencv-python-headless instead opencv-python (#62) Allow to choose `opencv=python-headless` as dependency with `DATUMARO_HEADLESS=1` env. variable when installing * Release 0.1.4 (#63) * update version * update changelog Co-authored-by: Nikita Manovich Co-authored-by: Anastasia Yasakova Co-authored-by: Andrey Zhavoronkov --- CHANGELOG.md | 22 ++ README.md | 1 + datumaro/plugins/camvid_format.py | 344 ++++++++++++++++++ datumaro/plugins/coco_format/extractor.py | 2 +- datumaro/plugins/imagenet_format.py | 3 +- datumaro/plugins/voc_format/importer.py | 31 +- datumaro/version.py | 2 +- docs/user_manual.md | 3 + setup.py | 37 +- tests/assets/camvid_dataset/test.txt | 2 + .../camvid_dataset/test/0001TP_008550.png | Bin 0 -> 70 bytes .../camvid_dataset/test/0001TP_008580.png | Bin 0 -> 70 bytes .../testannot/0001TP_008550.png | Bin 0 -> 79 bytes .../testannot/0001TP_008580.png | Bin 0 -> 81 bytes tests/assets/camvid_dataset/train.txt | 1 + .../camvid_dataset/train/0001TP_006690.png | Bin 0 -> 70 bytes .../trainannot/0001TP_006690.png | Bin 0 -> 81 bytes tests/assets/camvid_dataset/val.txt | 1 + .../camvid_dataset/val/0016E5_07959.png | Bin 0 -> 70 bytes .../camvid_dataset/valannot/0016E5_07959.png | Bin 0 -> 75 bytes tests/test_camvid_format.py | 229 ++++++++++++ 21 files changed, 658 insertions(+), 20 deletions(-) create mode 100644 datumaro/plugins/camvid_format.py create mode 100644 tests/assets/camvid_dataset/test.txt create mode 100644 tests/assets/camvid_dataset/test/0001TP_008550.png create mode 100644 tests/assets/camvid_dataset/test/0001TP_008580.png create mode 100644 tests/assets/camvid_dataset/testannot/0001TP_008550.png create mode 100644 tests/assets/camvid_dataset/testannot/0001TP_008580.png create mode 100644 tests/assets/camvid_dataset/train.txt create mode 100644 tests/assets/camvid_dataset/train/0001TP_006690.png create mode 100644 tests/assets/camvid_dataset/trainannot/0001TP_006690.png create mode 100644 tests/assets/camvid_dataset/val.txt create mode 100644 tests/assets/camvid_dataset/val/0016E5_07959.png create mode 100644 tests/assets/camvid_dataset/valannot/0016E5_07959.png create mode 100644 tests/test_camvid_format.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d17138419..f486fccae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - +## 12/10/2020 - Release v0.1.4 +### Added +- `CamVid` dataset format () +- Ability to install `opencv-python-headless` dependency with `DATUMARO_HEADLESS=1` + enviroment variable instead of `opencv-python` () + +### Changed +- Allow empty supercategory in COCO () +- Allow Pascal VOC to search in subdirectories () + +### Deprecated +- + +### Removed +- + +### Fixed +- + +### Security +- + ## 10/28/2020 - Release v0.1.3 ### Added - `ImageNet` and `ImageNetTxt` dataset formats () diff --git a/README.md b/README.md index acfa13754d..c8c153edd9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ CVAT annotations ---> Publication, statistics etc. - [MOT sequences](https://arxiv.org/pdf/1906.04567.pdf) - [MOTS PNG](https://www.vision.rwth-aachen.de/page/mots) - [ImageNet](http://image-net.org/) + - [CamVid](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) - [CVAT](https://github.com/opencv/cvat/blob/develop/cvat/apps/documentation/xml_format.md) - [LabelMe](http://labelme.csail.mit.edu/Release3.0) - Dataset building diff --git a/datumaro/plugins/camvid_format.py b/datumaro/plugins/camvid_format.py new file mode 100644 index 0000000000..6049ce6af4 --- /dev/null +++ b/datumaro/plugins/camvid_format.py @@ -0,0 +1,344 @@ + +# Copyright (C) 2020 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +import os.path as osp +from collections import OrderedDict +from enum import Enum +from glob import glob + +import numpy as np +from datumaro.components.converter import Converter +from datumaro.components.extractor import (AnnotationType, CompiledMask, + DatasetItem, Importer, LabelCategories, Mask, + MaskCategories, SourceExtractor) +from datumaro.util import find, str_to_bool +from datumaro.util.image import save_image +from datumaro.util.mask_tools import lazy_mask, paint_mask, generate_colormap + + +CamvidLabelMap = OrderedDict([ + ('Void', (0, 0, 0)), + ('Animal', (64, 128, 64)), + ('Archway', (192, 0, 128)), + ('Bicyclist', (0, 128, 192)), + ('Bridge', (0, 128, 64)), + ('Building', (128, 0, 0)), + ('Car', (64, 0, 128)), + ('CartLuggagePram', (64, 0, 192)), + ('Child', (192, 128, 64)), + ('Column_Pole', (192, 192, 128)), + ('Fence', (64, 64, 128)), + ('LaneMkgsDriv', (128, 0, 192)), + ('LaneMkgsNonDriv', (192, 0, 64)), + ('Misc_Text', (128, 128, 64)), + ('MotorcycycleScooter', (192, 0, 192)), + ('OtherMoving', (128, 64, 64)), + ('ParkingBlock', (64, 192, 128)), + ('Pedestrian', (64, 64, 0)), + ('Road', (128, 64, 128)), + ('RoadShoulder', (128, 128, 192)), + ('Sidewalk', (0, 0, 192)), + ('SignSymbol', (192, 128, 128)), + ('Sky', (128, 128, 128)), + ('SUVPickupTruck', (64, 128, 192)), + ('TrafficCone', (0, 0, 64)), + ('TrafficLight', (0, 64, 64)), + ('Train', (192, 64, 128)), + ('Tree', (128, 128, 0)), + ('Truck_Bus', (192, 128, 192)), + ('Tunnel', (64, 0, 64)), + ('VegetationMisc', (192, 192, 0)), + ('Wall', (64, 192, 0)) +]) + +class CamvidPath: + LABELMAP_FILE = 'label_colors.txt' + SEGM_DIR = "annot" + IMAGE_EXT = '.png' + + +def parse_label_map(path): + if not path: + return None + + label_map = OrderedDict() + with open(path, 'r') as f: + for line in f: + # skip empty and commented lines + line = line.strip() + if not line or line and line[0] == '#': + continue + + # color, name + label_desc = line.strip().split() + + if 2 < len(label_desc): + name = label_desc[3] + color = tuple([int(c) for c in label_desc[:-1]]) + else: + name = label_desc[0] + color = None + + if name in label_map: + raise ValueError("Label '%s' is already defined" % name) + + label_map[name] = color + return label_map + +def write_label_map(path, label_map): + with open(path, 'w') as f: + for label_name, label_desc in label_map.items(): + if label_desc: + color_rgb = ' '.join(str(c) for c in label_desc) + else: + color_rgb = '' + f.write('%s %s\n' % (color_rgb, label_name)) + +def make_camvid_categories(label_map=None): + if label_map is None: + label_map = CamvidLabelMap + + # 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)) + 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 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) + + categories = {} + label_categories = LabelCategories() + for label, desc in label_map.items(): + label_categories.add(label) + categories[AnnotationType.label] = label_categories + + has_colors = any(v is not None for v in label_map.values()) + if not has_colors: # generate new colors + colormap = generate_colormap(len(label_map)) + else: # only copy defined colors + label_id = lambda label: label_categories.find(label)[0] + colormap = { label_id(name): (desc[0], desc[1], desc[2]) + for name, desc in label_map.items() } + mask_categories = MaskCategories(colormap) + mask_categories.inverse_colormap # pylint: disable=pointless-statement + categories[AnnotationType.mask] = mask_categories + return categories + + +class CamvidExtractor(SourceExtractor): + def __init__(self, path): + assert osp.isfile(path), path + self._path = path + self._dataset_dir = osp.dirname(path) + super().__init__(subset=osp.splitext(osp.basename(path))[0]) + + self._categories = self._load_categories(self._dataset_dir) + self._items = list(self._load_items(path).values()) + + def _load_categories(self, path): + label_map = None + label_map_path = osp.join(path, CamvidPath.LABELMAP_FILE) + if osp.isfile(label_map_path): + label_map = parse_label_map(label_map_path) + else: + label_map = CamvidLabelMap + self._labels = [label for label in label_map] + return make_camvid_categories(label_map) + + def _load_items(self, path): + items = {} + with open(path, encoding='utf-8') as f: + for line in f: + objects = line.split() + image = objects[0] + item_id = ('/'.join(image.split('/')[2:]))[:-len(CamvidPath.IMAGE_EXT)] + image_path = osp.join(self._dataset_dir, + (image, image[1:])[image[0] == '/']) + item_annotations = [] + if 1 < len(objects): + gt = objects[1] + gt_path = osp.join(self._dataset_dir, + (gt, gt[1:]) [gt[0] == '/']) + inverse_cls_colormap = \ + self._categories[AnnotationType.mask].inverse_colormap + mask = lazy_mask(gt_path, inverse_cls_colormap) + # loading mask through cache + mask = mask() + classes = np.unique(mask) + labels = self._categories[AnnotationType.label]._indices + labels = { labels[label_name]: label_name + for label_name in labels } + for label_id in classes: + if labels[label_id] in self._labels: + image = self._lazy_extract_mask(mask, label_id) + item_annotations.append(Mask(image=image, label=label_id)) + items[item_id] = DatasetItem(id=item_id, subset=self._subset, + image=image_path, annotations=item_annotations) + return items + + @staticmethod + def _lazy_extract_mask(mask, c): + return lambda: mask == c + + +class CamvidImporter(Importer): + @classmethod + def find_sources(cls, path): + subset_paths = [p for p in glob(osp.join(path, '**.txt'), recursive=True) + if osp.basename(p) != CamvidPath.LABELMAP_FILE] + sources = [] + for subset_path in subset_paths: + sources += cls._find_sources_recursive( + subset_path, '.txt', 'camvid') + return sources + + +LabelmapType = Enum('LabelmapType', ['camvid', 'source']) + +class CamvidConverter(Converter): + DEFAULT_IMAGE_EXT = '.png' + + @classmethod + def build_cmdline_parser(cls, **kwargs): + parser = super().build_cmdline_parser(**kwargs) + + parser.add_argument('--apply-colormap', type=str_to_bool, default=True, + help="Use colormap for class masks (default: %(default)s)") + 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)) + + def __init__(self, extractor, save_dir, + apply_colormap=True, label_map=None, **kwargs): + super().__init__(extractor, save_dir, **kwargs) + + self._apply_colormap = apply_colormap + + if label_map is None: + label_map = LabelmapType.source.name + self._load_categories(label_map) + + def apply(self): + subset_dir = self._save_dir + os.makedirs(subset_dir, exist_ok=True) + + for subset_name, subset in self._extractor.subsets().items(): + segm_list = {} + for item in subset: + masks = [a for a in item.annotations + if a.type == AnnotationType.mask] + + if masks: + compiled_mask = CompiledMask.from_instance_masks(masks, + instance_labels=[self._label_id_mapping(m.label) + for m in masks]) + + self.save_segm(osp.join(subset_dir, + subset_name + CamvidPath.SEGM_DIR, + item.id + CamvidPath.IMAGE_EXT), + compiled_mask.class_mask) + segm_list[item.id] = True + else: + segm_list[item.id] = False + + if self._save_images: + self._save_image(item, osp.join(subset_dir, subset_name, + item.id + CamvidPath.IMAGE_EXT)) + + self.save_segm_lists(subset_name, segm_list) + self.save_label_map() + + def save_segm(self, path, mask, colormap=None): + if self._apply_colormap: + if colormap is None: + colormap = self._categories[AnnotationType.mask].colormap + mask = paint_mask(mask, colormap) + save_image(path, mask, create_dir=True) + + def save_segm_lists(self, subset_name, segm_list): + if not segm_list: + return + + ann_file = osp.join(self._save_dir, subset_name + '.txt') + with open(ann_file, 'w') as f: + for item in segm_list: + if segm_list[item]: + path_mask = '/%s/%s' % (subset_name + CamvidPath.SEGM_DIR, + item + CamvidPath.IMAGE_EXT) + else: + path_mask = '' + f.write('/%s/%s %s\n' % (subset_name, + item + CamvidPath.IMAGE_EXT, path_mask)) + + def save_label_map(self): + path = osp.join(self._save_dir, CamvidPath.LABELMAP_FILE) + labels = self._extractor.categories()[AnnotationType.label]._indices + if len(self._label_map) > len(labels): + self._label_map.pop('background') + write_label_map(path, self._label_map) + + def _load_categories(self, label_map_source): + if label_map_source == LabelmapType.camvid.name: + # use the default Camvid colormap + label_map = CamvidLabelMap + + elif label_map_source == LabelmapType.source.name and \ + AnnotationType.mask not in self._extractor.categories(): + # generate colormap for input labels + labels = self._extractor.categories() \ + .get(AnnotationType.label, LabelCategories()) + 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() + for idx, item in enumerate(labels.items): + color = colors.colormap.get(idx) + if color is not None: + label_map[item.name] = color + + elif isinstance(label_map_source, dict): + 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) + + else: + raise Exception("Wrong labelmap specified, " + "expected one of %s or a file path" % \ + ', '.join(t.name for t in LabelmapType)) + + self._categories = make_camvid_categories(label_map) + self._label_map = label_map + self._label_id_mapping = self._make_label_id_map() + + def _make_label_id_map(self): + source_labels = { + id: label.name for id, label in + enumerate(self._extractor.categories().get( + AnnotationType.label, LabelCategories()).items) + } + target_labels = { + label.name: id for id, label in + enumerate(self._categories[AnnotationType.label].items) + } + id_mapping = { + src_id: target_labels.get(src_label, 0) + for src_id, src_label in source_labels.items() + } + + def map_id(src_id): + return id_mapping.get(src_id, 0) + return map_id diff --git a/datumaro/plugins/coco_format/extractor.py b/datumaro/plugins/coco_format/extractor.py index d29cf1a606..4f94776b1d 100644 --- a/datumaro/plugins/coco_format/extractor.py +++ b/datumaro/plugins/coco_format/extractor.py @@ -81,7 +81,7 @@ def _load_label_categories(self, loader): label_map = {} for idx, cat in enumerate(cats): label_map[cat['id']] = idx - categories.add(name=cat['name'], parent=cat['supercategory']) + categories.add(name=cat['name'], parent=cat.get('supercategory')) return categories, label_map # pylint: enable=no-self-use diff --git a/datumaro/plugins/imagenet_format.py b/datumaro/plugins/imagenet_format.py index 4aa0548bae..0e0669a9db 100644 --- a/datumaro/plugins/imagenet_format.py +++ b/datumaro/plugins/imagenet_format.py @@ -74,7 +74,8 @@ def apply(self): labels = {} for item in self._extractor: image_name = item.id - labels[image_name] = set(p.label for p in item.annotations) + labels[image_name] = [p.label for p in item.annotations + if p.type == AnnotationType.label] for label in labels[image_name]: label_name = extractor.categories()[AnnotationType.label][label].name self._save_image(item, osp.join(subset_dir, label_name, diff --git a/datumaro/plugins/voc_format/importer.py b/datumaro/plugins/voc_format/importer.py index 1a7e1cd602..7da323249b 100644 --- a/datumaro/plugins/voc_format/importer.py +++ b/datumaro/plugins/voc_format/importer.py @@ -10,6 +10,22 @@ from .format import VocTask, VocPath +def find_path(root_path, path, depth=4): + level, is_found = 0, False + full_path = None + while level < depth and not is_found: + full_path = osp.join(root_path, path) + paths = glob(full_path) + if paths: + full_path = paths[0] # ignore all after the first one + is_found = osp.isdir(full_path) + else: + full_path = None + + level += 1 + root_path = osp.join(root_path, '*') + + return full_path class VocImporter(Importer): _TASKS = [ @@ -41,12 +57,21 @@ def __call__(self, path, **extra_params): @classmethod def find_sources(cls, path): + # find root path for the dataset + root_path = path + for task, extractor_type, task_dir in cls._TASKS: + task_path = find_path(root_path, osp.join(VocPath.SUBSETS_DIR, task_dir)) + if task_path: + root_path = osp.dirname(osp.dirname(task_path)) + break + subset_paths = [] for task, extractor_type, task_dir in cls._TASKS: - task_dir = osp.join(path, VocPath.SUBSETS_DIR, task_dir) - if not osp.isdir(task_dir): + task_path = osp.join(root_path, VocPath.SUBSETS_DIR, task_dir) + + if not osp.isdir(task_path): continue - task_subsets = [p for p in glob(osp.join(task_dir, '*.txt')) + task_subsets = [p for p in glob(osp.join(task_path, '*.txt')) if '_' not in osp.basename(p)] subset_paths += [(task, extractor_type, p) for p in task_subsets] return subset_paths diff --git a/datumaro/version.py b/datumaro/version.py index bac8f3aa76..a0901263b2 100644 --- a/datumaro/version.py +++ b/datumaro/version.py @@ -1 +1 @@ -VERSION = '0.1.3' \ No newline at end of file +VERSION = '0.1.4' \ No newline at end of file diff --git a/docs/user_manual.md b/docs/user_manual.md index 70a6c367de..9afd63e74f 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -101,6 +101,9 @@ List of supported formats: - [Dataset example](../tests/assets/imagenet_dataset) - [Dataset example (txt for classification)](../tests/assets/imagenet_txt_dataset) - Detection format is the same as in PASCAL VOC +- CamVid (`segmentation`) + - [Format specification](http://mi.eng.cam.ac.uk/research/projects/VideoRec/CamVid/) + - [Dataset example](../tests/assets/camvid_dataset) - CVAT - [Format specification](https://github.com/opencv/cvat/blob/develop/cvat/apps/documentation/xml_format.md) - [Dataset example](../tests/assets/cvat_dataset) diff --git a/setup.py b/setup.py index d0ea36e0cc..7e08249839 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: MIT +from distutils.util import strtobool +import os import os.path as osp import re import setuptools @@ -27,6 +29,26 @@ def find_version(file_path=None): version = version_text[match.start(1) : match.end(1)] return version +def get_requirements(): + requirements = [ + 'attrs>=19.3.0', + 'defusedxml', + 'GitPython', + 'lxml', + 'matplotlib', + 'numpy>=1.17.3', + 'Pillow', + 'pycocotools', + 'PyYAML', + 'scikit-image', + 'tensorboardX', + ] + if strtobool(os.getenv('DATUMARO_HEADLESS', '0').lower()): + requirements.append('opencv-python-headless') + else: + requirements.append('opencv-python') + + return requirements with open('README.md', 'r') as fh: long_description = fh.read() @@ -51,20 +73,7 @@ def find_version(file_path=None): "Operating System :: OS Independent", ], python_requires='>=3.5', - install_requires=[ - 'attrs>=19.3.0', - 'defusedxml', - 'GitPython', - 'lxml', - 'matplotlib', - 'numpy>=1.17.3', - 'opencv-python', - 'Pillow', - 'pycocotools', - 'PyYAML', - 'scikit-image', - 'tensorboardX', - ], + install_requires=get_requirements(), extras_require={ 'tf': ['tensorflow'], 'tf-gpu': ['tensorflow-gpu'], diff --git a/tests/assets/camvid_dataset/test.txt b/tests/assets/camvid_dataset/test.txt new file mode 100644 index 0000000000..59fec40c08 --- /dev/null +++ b/tests/assets/camvid_dataset/test.txt @@ -0,0 +1,2 @@ +/test/0001TP_008550.png /testannot/0001TP_008550.png +/test/0001TP_008580.png /testannot/0001TP_008580.png \ No newline at end of file diff --git a/tests/assets/camvid_dataset/test/0001TP_008550.png b/tests/assets/camvid_dataset/test/0001TP_008550.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/test/0001TP_008580.png b/tests/assets/camvid_dataset/test/0001TP_008580.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/testannot/0001TP_008550.png b/tests/assets/camvid_dataset/testannot/0001TP_008550.png new file mode 100644 index 0000000000000000000000000000000000000000..55a54675356d9376c7e4f7d91ff67cfd3e066de9 GIT binary patch literal 79 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qNQrs6IEHY@CYNLsBqSshSUfoJ c-~a;)gJc3zQ&y6~B%lfgPgg&ebxsLQ0EfyGKL7v# literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/testannot/0001TP_008580.png b/tests/assets/camvid_dataset/testannot/0001TP_008580.png new file mode 100644 index 0000000000000000000000000000000000000000..914d0b4720e42d3c974760d6794d0994dc9c3b7d GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qNJ)6QIEHY@CfgjiaNxj!2Nwz! fG(KctW@ctkPGDi0o)G^CsD{DQ)z4*}Q$iB}<>VD+ literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/train.txt b/tests/assets/camvid_dataset/train.txt new file mode 100644 index 0000000000..4383878eb7 --- /dev/null +++ b/tests/assets/camvid_dataset/train.txt @@ -0,0 +1 @@ +/train/0001TP_006690.png /trainannot/0001TP_006690.png \ No newline at end of file diff --git a/tests/assets/camvid_dataset/train/0001TP_006690.png b/tests/assets/camvid_dataset/train/0001TP_006690.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/trainannot/0001TP_006690.png b/tests/assets/camvid_dataset/trainannot/0001TP_006690.png new file mode 100644 index 0000000000000000000000000000000000000000..9aa4b887066dd0fcafe690aef7a91dad6811731f GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qNJ)6QIEHY@CZ}Xr95`^`Lcs%v d=4NIfU{HO);&jlZXdh4wgQu&X%Q~loCIG~f6r=zE literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/val.txt b/tests/assets/camvid_dataset/val.txt new file mode 100644 index 0000000000..b834086d65 --- /dev/null +++ b/tests/assets/camvid_dataset/val.txt @@ -0,0 +1 @@ +/val/0016E5_07959.png /valannot/0016E5_07959.png \ No newline at end of file diff --git a/tests/assets/camvid_dataset/val/0016E5_07959.png b/tests/assets/camvid_dataset/val/0016E5_07959.png new file mode 100644 index 0000000000000000000000000000000000000000..528f10546704be6b339cfe1f577ca4b10ef4f472 GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qEaBo9!XcZ?!o;QmFVdQ&MBb@0GX=|x&QzG literal 0 HcmV?d00001 diff --git a/tests/assets/camvid_dataset/valannot/0016E5_07959.png b/tests/assets/camvid_dataset/valannot/0016E5_07959.png new file mode 100644 index 0000000000000000000000000000000000000000..2023121bfa197ab23353984725ead40168df81b3 GIT binary patch literal 75 zcmeAS@N?(olHy`uVBq!ia0vp^tU%1j!2~2{&iT9qNC|nmIEHY@CYNLsBrr6yu?a9R Y2r4i(RXc2W2$W^;boFyt=akR{0Q%++vj6}9 literal 0 HcmV?d00001 diff --git a/tests/test_camvid_format.py b/tests/test_camvid_format.py new file mode 100644 index 0000000000..12192279b9 --- /dev/null +++ b/tests/test_camvid_format.py @@ -0,0 +1,229 @@ +import os.path as osp +from collections import OrderedDict +from functools import partial +from unittest import TestCase + +import datumaro.plugins.camvid_format as Camvid +import numpy as np +from datumaro.components.extractor import (AnnotationType, DatasetItem, + Extractor, LabelCategories, Mask) +from datumaro.components.project import Dataset, Project +from datumaro.plugins.camvid_format import CamvidConverter, CamvidImporter +from datumaro.util.test_utils import (TestDir, compare_datasets, + test_save_and_load) + + +class CamvidFormatTest(TestCase): + def test_can_write_and_parse_labelmap(self): + src_label_map = Camvid.CamvidLabelMap + + with TestDir() as test_dir: + file_path = osp.join(test_dir, 'label_colors.txt') + Camvid.write_label_map(file_path, src_label_map) + dst_label_map = Camvid.parse_label_map(file_path) + + self.assertEqual(src_label_map, dst_label_map) + +DUMMY_DATASET_DIR = osp.join(osp.dirname(__file__), 'assets', 'camvid_dataset') + +class TestExtractorBase(Extractor): + def _label(self, camvid_label): + return self.categories()[AnnotationType.label].find(camvid_label)[0] + + def categories(self): + return Camvid.make_camvid_categories() + +class CamvidImportTest(TestCase): + def test_can_import(self): + source_dataset = Dataset.from_iterable([ + DatasetItem(id='0001TP_008550', subset='test', + image=np.ones((1, 5, 3)), + annotations=[ + Mask(image=np.array([[1, 1, 0, 0, 0]]), label=1), + Mask(image=np.array([[0, 0, 1, 0, 0]]), label=18), + Mask(image=np.array([[0, 0, 0, 1, 1]]), label=22), + ] + ), + DatasetItem(id='0001TP_008580', subset='test', + image=np.ones((1, 5, 3)), + annotations=[ + Mask(image=np.array([[1, 1, 0, 0, 0]]), label=2), + Mask(image=np.array([[0, 0, 1, 0, 0]]), label=4), + Mask(image=np.array([[0, 0, 0, 1, 1]]), label=27), + ] + ), + DatasetItem(id='0001TP_006690', subset='train', + image=np.ones((1, 5, 3)), + annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 1]]), label=3), + Mask(image=np.array([[0, 0, 1, 0, 0]]), label=18), + ] + ), + DatasetItem(id='0016E5_07959', subset = 'val', + image=np.ones((1, 5, 3)), + annotations=[ + Mask(image=np.array([[1, 1, 1, 0, 0]]), label=1), + Mask(image=np.array([[0, 0, 0, 1, 1]]), label=8), + ] + ), + ], categories=Camvid.make_camvid_categories()) + + parsed_dataset = Project.import_from(DUMMY_DATASET_DIR, 'camvid').make_dataset() + + compare_datasets(self, source_dataset, parsed_dataset) + + def test_can_detect_camvid(self): + self.assertTrue(CamvidImporter.detect(DUMMY_DATASET_DIR)) + +class CamvidConverterTest(TestCase): + def _test_save_and_load(self, source_dataset, converter, test_dir, + target_dataset=None, importer_args=None): + return test_save_and_load(self, source_dataset, converter, test_dir, + importer='camvid', + target_dataset=target_dataset, importer_args=importer_args) + + def test_can_save_camvid_segm(self): + class TestExtractor(TestExtractorBase): + def __iter__(self): + return iter([ + DatasetItem(id='a/b/1', subset='test', + image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[0, 0, 0, 1, 0]]), label=0), + Mask(image=np.array([[0, 1, 1, 0, 0]]), label=3), + Mask(image=np.array([[1, 0, 0, 0, 1]]), label=4), + ]), + ]) + + with TestDir() as test_dir: + self._test_save_and_load(TestExtractor(), + partial(CamvidConverter.convert, label_map='camvid'), + test_dir) + + def test_can_save_camvid_segm_unpainted(self): + class TestExtractor(TestExtractorBase): + def __iter__(self): + return iter([ + DatasetItem(id=1, subset='a', image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[0, 0, 0, 1, 0]]), label=0), + Mask(image=np.array([[0, 1, 1, 0, 0]]), label=3), + Mask(image=np.array([[1, 0, 0, 0, 1]]), label=4), + ]), + ]) + + class DstExtractor(TestExtractorBase): + def __iter__(self): + return iter([ + DatasetItem(id=1, subset='a', image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[0, 0, 0, 1, 0]]), label=0), + Mask(image=np.array([[0, 1, 1, 0, 0]]), label=3), + Mask(image=np.array([[1, 0, 0, 0, 1]]), label=4), + ]), + ]) + + with TestDir() as test_dir: + self._test_save_and_load(TestExtractor(), + partial(CamvidConverter.convert, + label_map='camvid', apply_colormap=False), + test_dir, target_dataset=DstExtractor()) + + def test_can_save_dataset_with_no_subsets(self): + class TestExtractor(TestExtractorBase): + def __iter__(self): + return iter([ + DatasetItem(id=1, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 0, 0, 1, 0]]), label=0), + Mask(image=np.array([[0, 1, 1, 0, 1]]), label=3), + ]), + + DatasetItem(id=2, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 0]]), label=1), + Mask(image=np.array([[0, 0, 1, 0, 1]]), label=2), + ]), + ]) + + with TestDir() as test_dir: + self._test_save_and_load(TestExtractor(), + partial(CamvidConverter.convert, label_map='camvid'), test_dir) + + def test_can_save_with_no_masks(self): + class TestExtractor(TestExtractorBase): + def __iter__(self): + return iter([ + DatasetItem(id='a/b/1', subset='test', + image=np.ones((2, 5, 3)), + ), + ]) + + with TestDir() as test_dir: + self._test_save_and_load(TestExtractor(), + partial(CamvidConverter.convert, label_map='camvid'), + test_dir) + + def test_dataset_with_source_labelmap_undefined(self): + class SrcExtractor(TestExtractorBase): + def __iter__(self): + yield DatasetItem(id=1, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 0]]), label=0), + Mask(image=np.array([[0, 0, 1, 0, 0]]), label=1), + ]) + + def categories(self): + label_cat = LabelCategories() + label_cat.add('Label_1') + label_cat.add('label_2') + return { + AnnotationType.label: label_cat, + } + + class DstExtractor(TestExtractorBase): + def __iter__(self): + yield DatasetItem(id=1, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 0]]), label=self._label('Label_1')), + Mask(image=np.array([[0, 0, 1, 0, 0]]), label=self._label('label_2')), + ]) + + def categories(self): + label_map = OrderedDict() + label_map['background'] = None + label_map['Label_1'] = None + label_map['label_2'] = None + return Camvid.make_camvid_categories(label_map) + + with TestDir() as test_dir: + self._test_save_and_load(SrcExtractor(), + partial(CamvidConverter.convert, label_map='source'), + test_dir, target_dataset=DstExtractor()) + + def test_dataset_with_source_labelmap_defined(self): + class SrcExtractor(TestExtractorBase): + def __iter__(self): + yield DatasetItem(id=1, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 0]]), label=1), + Mask(image=np.array([[0, 0, 1, 0, 1]]), label=2), + ]) + + def categories(self): + label_map = OrderedDict() + label_map['background'] = (0, 0, 0) + label_map['label_1'] = (1, 2, 3) + label_map['label_2'] = (3, 2, 1) + return Camvid.make_camvid_categories(label_map) + + class DstExtractor(TestExtractorBase): + def __iter__(self): + yield DatasetItem(id=1, image=np.ones((1, 5, 3)), annotations=[ + Mask(image=np.array([[1, 1, 0, 1, 0]]), label=self._label('label_1')), + Mask(image=np.array([[0, 0, 1, 0, 1]]), label=self._label('label_2')), + ]) + + def categories(self): + label_map = OrderedDict() + label_map['background'] = (0, 0, 0) + label_map['label_1'] = (1, 2, 3) + label_map['label_2'] = (3, 2, 1) + return Camvid.make_camvid_categories(label_map) + + with TestDir() as test_dir: + self._test_save_and_load(SrcExtractor(), + partial(CamvidConverter.convert, label_map='source'), + test_dir, target_dataset=DstExtractor())