From c5055b4ebb454c82198e3f4fe9bf41c333890836 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Thu, 28 Nov 2019 17:29:55 +0300 Subject: [PATCH 1/8] Add version file --- datumaro/datumaro/__init__.py | 2 +- datumaro/datumaro/components/config.py | 1 - datumaro/datumaro/version.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 datumaro/datumaro/version.py diff --git a/datumaro/datumaro/__init__.py b/datumaro/datumaro/__init__.py index 48088c40801..fae1306ab66 100644 --- a/datumaro/datumaro/__init__.py +++ b/datumaro/datumaro/__init__.py @@ -23,7 +23,7 @@ stats_command as stats_command_module, explain_command as explain_command_module, ) -from .components.config import VERSION +from .version import VERSION KNOWN_COMMANDS = { diff --git a/datumaro/datumaro/components/config.py b/datumaro/datumaro/components/config.py index 330d01f5bf2..520c6e70bd5 100644 --- a/datumaro/datumaro/components/config.py +++ b/datumaro/datumaro/components/config.py @@ -234,5 +234,4 @@ def set(self, key, value): return super().set(key, value) -VERSION = '0.1.0' DEFAULT_FORMAT = 'datumaro' \ No newline at end of file diff --git a/datumaro/datumaro/version.py b/datumaro/datumaro/version.py new file mode 100644 index 00000000000..8589c063873 --- /dev/null +++ b/datumaro/datumaro/version.py @@ -0,0 +1 @@ +VERSION = '0.1.0' \ No newline at end of file From 1c90835e96c519c25caca52df026121c97f132c0 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 13:44:37 +0300 Subject: [PATCH 2/8] Remove unnecessary dependencies --- datumaro/requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/datumaro/requirements.txt b/datumaro/requirements.txt index 652ec3eab36..f06040c0039 100644 --- a/datumaro/requirements.txt +++ b/datumaro/requirements.txt @@ -6,6 +6,4 @@ opencv-python>=4.1.0.25 Pillow>=6.1.0 pycocotools>=2.0.0 PyYAML>=5.1.1 -requests>=2.20.0 -tensorboard>=1.12.0 tensorboardX>=1.8 From 23e61c28285bdd163a625886283c25c7f48fd5c3 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:14:40 +0300 Subject: [PATCH 3/8] Add lxml use motivation --- datumaro/datumaro/components/dataset_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datumaro/datumaro/components/dataset_filter.py b/datumaro/datumaro/components/dataset_filter.py index 7b27c49914f..157720f3651 100644 --- a/datumaro/datumaro/components/dataset_filter.py +++ b/datumaro/datumaro/components/dataset_filter.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: MIT -from lxml import etree as ET +from lxml import etree as ET # NOTE: lxml has proper XPath implementation from datumaro.components.extractor import (DatasetItem, Annotation, LabelObject, MaskObject, PointsObject, PolygonObject, PolyLineObject, BboxObject, CaptionObject, From a120f121c9d2e8ea4f19c07512c5604ccf381d97 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:14:54 +0300 Subject: [PATCH 4/8] Add pip setup script --- datumaro/setup.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 datumaro/setup.py diff --git a/datumaro/setup.py b/datumaro/setup.py new file mode 100644 index 00000000000..cb90d1b3aba --- /dev/null +++ b/datumaro/setup.py @@ -0,0 +1,66 @@ + +# Copyright (C) 2019 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp +import re +import setuptools + + +def find_version(file_path=None): + if not file_path: + file_path = osp.join(osp.dirname(osp.abspath(__file__)), + 'datumaro', 'version.py') + + with open(file_path, 'r') as version_file: + version_text = version_file.read() + + # PEP440: + # https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + pep_regex = r'([1-9]\d*!)?(0|[1-9]\d*)(\.(0|[1-9]\d*))*((a|b|rc)(0|[1-9]\d*))?(\.post(0|[1-9]\d*))?(\.dev(0|[1-9]\d*))?' + version_regex = r'VERSION\s*=\s*.(' + pep_regex + ').' + match = re.match(version_regex, version_text) + if not match: + raise RuntimeError("Failed to find version string in '%s'" % file_path) + + version = version_text[match.start(1) : match.end(1)] + return version + + +with open('README.md', 'r') as fh: + long_description = fh.read() + +setuptools.setup( + name="datumaro", + version=find_version(), + author="Intel", + author_email="maxim.zhiltsov@intel.com", + description="Dataset Framework", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/opencv/cvat/datumaro", + packages=setuptools.find_packages(exclude=['tests*']), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.5', + install_requires=[ + 'cv2', + 'GitPython', + 'lxml', + 'matplotlib', + 'numpy', + 'Pillow', + 'PyYAML', + 'pycocotools', + 'tensorboardX', + ], + entry_points={ + 'console_scripts': [ + 'datum=datum:main', + ], + }, +) \ No newline at end of file From 1e196201c51f747e6ccf1533abd224b4e0fafe7b Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:17:09 +0300 Subject: [PATCH 5/8] Reduce opencv dependency --- datumaro/datumaro/cli/explain_command.py | 8 ++-- datumaro/datumaro/cli/project/diff.py | 13 ++++-- .../datumaro/components/algorithms/rise.py | 3 +- .../components/converters/datumaro.py | 6 +-- .../datumaro/components/converters/ms_coco.py | 4 +- .../datumaro/components/converters/voc.py | 6 +-- datumaro/datumaro/components/extractor.py | 7 +++- .../datumaro/components/launchers/openvino.py | 3 +- datumaro/datumaro/util/command_targets.py | 7 +++- datumaro/datumaro/util/image.py | 41 +++++++++++++++++-- datumaro/tests/test_image.py | 39 ++++++++++++++++++ 11 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 datumaro/tests/test_image.py diff --git a/datumaro/datumaro/cli/explain_command.py b/datumaro/datumaro/cli/explain_command.py index 195b1026b9a..8a83f7dafa7 100644 --- a/datumaro/datumaro/cli/explain_command.py +++ b/datumaro/datumaro/cli/explain_command.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: MIT import argparse -import cv2 import logging as log import os import os.path as osp @@ -13,7 +12,7 @@ from datumaro.components.algorithms.rise import RISE from datumaro.util.command_targets import (TargetKinds, target_selector, ProjectTarget, SourceTarget, ImageTarget, is_project_path) -from datumaro.util.image import load_image +from datumaro.util.image import load_image, save_image from .util.project import load_project @@ -60,6 +59,7 @@ def build_parser(parser=argparse.ArgumentParser()): return parser def explain_command(args): + import cv2 from matplotlib import cm project = load_project(args.project_dir) @@ -110,7 +110,7 @@ def explain_command(args): for j, heatmap in enumerate(heatmaps): save_path = osp.join(args.save_dir, file_name + '-heatmap-%s.png' % j) - cv2.imwrite(save_path, heatmap * 255.0) + save_image(save_path, heatmap * 255.0) else: for j, heatmap in enumerate(heatmaps): disp = (image + cm.jet(heatmap)[:, :, 2::-1]) / 2 @@ -151,7 +151,7 @@ def explain_command(args): for j, heatmap in enumerate(heatmaps): save_path = osp.join(args.save_dir, file_name + '-heatmap-%s.png' % j) - cv2.imwrite(save_path, heatmap * 255.0) + save_image(save_path, heatmap * 255.0) if args.progressive: for j, heatmap in enumerate(heatmaps): diff --git a/datumaro/datumaro/cli/project/diff.py b/datumaro/datumaro/cli/project/diff.py index 8fdae82bc21..78fdcd51a1f 100644 --- a/datumaro/datumaro/cli/project/diff.py +++ b/datumaro/datumaro/cli/project/diff.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: MIT from collections import Counter -import cv2 from enum import Enum import numpy as np import os @@ -19,6 +18,7 @@ _formats.append('tensorboard') from datumaro.components.extractor import AnnotationType +from datumaro.util.image import save_image Format = Enum('Formats', _formats) @@ -135,8 +135,13 @@ def update_bbox_confusion(self, bbox_diff): @classmethod def draw_text_with_background(cls, frame, text, origin, - font=cv2.FONT_HERSHEY_SIMPLEX, scale=1.0, + font=None, scale=1.0, color=(0, 0, 0), thickness=1, bgcolor=(1, 1, 1)): + import cv2 + + if not font: + font = cv2.FONT_HERSHEY_SIMPLEX + text_size, baseline = cv2.getTextSize(text, font, scale, thickness) cv2.rectangle(frame, tuple((origin + (0, baseline)).astype(int)), @@ -148,6 +153,8 @@ def draw_text_with_background(cls, frame, text, origin, return text_size, baseline def draw_detection_roi(self, frame, x, y, w, h, label, conf, color): + import cv2 + cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2) text = '%s %.2f%%' % (label, 100.0 * conf) @@ -216,7 +223,7 @@ def save_item_bbox_diff(self, item_a, item_b, diff): path = osp.join(self.save_dir, 'diff_%s' % item_a.id) if self.output_format is Format.simple: - cv2.imwrite(path + '.png', img) + save_image(path + '.png', img) elif self.output_format is Format.tensorboard: self.save_as_tensorboard(img, path) diff --git a/datumaro/datumaro/components/algorithms/rise.py b/datumaro/datumaro/components/algorithms/rise.py index e61737d3174..78e936392c2 100644 --- a/datumaro/datumaro/components/algorithms/rise.py +++ b/datumaro/datumaro/components/algorithms/rise.py @@ -5,7 +5,6 @@ # pylint: disable=unused-variable -import cv2 import numpy as np from math import ceil @@ -79,6 +78,8 @@ def normalize_hmaps(self, heatmaps, counts): return np.reshape(mhmaps, heatmaps.shape) def apply(self, image, progressive=False): + import cv2 + assert len(image.shape) == 3, \ "Expected an input image in (H, W, C) format" assert image.shape[2] in [3, 4], \ diff --git a/datumaro/datumaro/components/converters/datumaro.py b/datumaro/datumaro/components/converters/datumaro.py index 9e17f82bd36..e3d4fcd6698 100644 --- a/datumaro/datumaro/components/converters/datumaro.py +++ b/datumaro/datumaro/components/converters/datumaro.py @@ -5,7 +5,6 @@ # pylint: disable=no-self-use -import cv2 import json import os import os.path as osp @@ -19,6 +18,7 @@ LabelCategories, MaskCategories, PointsCategories ) from datumaro.components.formats.datumaro import DatumaroPath +from datumaro.util.image import save_image from datumaro.util.mask_tools import apply_colormap @@ -133,7 +133,7 @@ def _save_mask(self, mask): DatumaroPath.MASKS_DIR) os.makedirs(masks_dir, exist_ok=True) path = osp.join(masks_dir, filename) - cv2.imwrite(path, mask) + save_image(path, mask) return mask_id def _convert_mask_object(self, obj): @@ -279,7 +279,7 @@ def _save_image(self, item): image_path = osp.join(self._images_dir, str(item.id) + DatumaroPath.IMAGE_EXT) - cv2.imwrite(image_path, image) + save_image(image_path, image) class DatumaroConverter(Converter): def __init__(self, save_images=False, apply_colormap=False): diff --git a/datumaro/datumaro/components/converters/ms_coco.py b/datumaro/datumaro/components/converters/ms_coco.py index ed004baa3d6..b91662511df 100644 --- a/datumaro/datumaro/components/converters/ms_coco.py +++ b/datumaro/datumaro/components/converters/ms_coco.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: MIT -import cv2 import json import numpy as np import os @@ -17,6 +16,7 @@ ) from datumaro.components.formats.ms_coco import CocoAnnotationType, CocoPath from datumaro.util import find +from datumaro.util.image import save_image import datumaro.util.mask_tools as mask_tools @@ -374,7 +374,7 @@ def make_task_converters(self): def save_image(self, item, filename): path = osp.join(self._images_dir, filename) - cv2.imwrite(path, item.image) + save_image(path, item.image) return path diff --git a/datumaro/datumaro/components/converters/voc.py b/datumaro/datumaro/components/converters/voc.py index c513150e7f9..034749bb783 100644 --- a/datumaro/datumaro/components/converters/voc.py +++ b/datumaro/datumaro/components/converters/voc.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: MIT -import cv2 from collections import OrderedDict, defaultdict import os import os.path as osp @@ -14,6 +13,7 @@ from datumaro.components.formats.voc import VocLabel, VocAction, \ VocBodyPart, VocPose, VocTask, VocPath, VocColormap, VocInstColormap from datumaro.util import find +from datumaro.util.image import save_image from datumaro.util.mask_tools import apply_colormap @@ -111,7 +111,7 @@ def save_subsets(self): if self._save_images: data = item.image if data is not None: - cv2.imwrite(osp.join(self._images_dir, + save_image(osp.join(self._images_dir, str(item_id) + VocPath.IMAGE_EXT), data) @@ -334,7 +334,7 @@ def save_segm(self, path, annotation, colormap): if colormap is None: colormap = VocColormap data = apply_colormap(data, colormap) - cv2.imwrite(path, data) + save_image(path, data) class VocConverter(Converter): def __init__(self, task=None, save_images=False, apply_colormap=False): diff --git a/datumaro/datumaro/components/extractor.py b/datumaro/datumaro/components/extractor.py index c96acc2be96..bf73a9f05af 100644 --- a/datumaro/datumaro/components/extractor.py +++ b/datumaro/datumaro/components/extractor.py @@ -182,7 +182,8 @@ def __eq__(self, other): return False return \ (self.label == other.label) and \ - (np.all(self.image == other.image)) + (self.image is not None and other.image is not None and \ + np.all(self.image == other.image)) def compute_iou(bbox_a, bbox_b): aX, aY, aW, aH = bbox_a @@ -461,7 +462,9 @@ def __eq__(self, other): (self.id == other.id) and \ (self.subset == other.subset) and \ (self.annotations == other.annotations) and \ - (self.image == other.image) + (self.has_image == other.has_image) and \ + (self.has_image and np.all(self.image == other.image) or \ + not self.has_image) class IExtractor: def __iter__(self): diff --git a/datumaro/datumaro/components/launchers/openvino.py b/datumaro/datumaro/components/launchers/openvino.py index 28155539b98..613203b9ec2 100644 --- a/datumaro/datumaro/components/launchers/openvino.py +++ b/datumaro/datumaro/components/launchers/openvino.py @@ -5,7 +5,6 @@ # pylint: disable=exec-used -import cv2 import os import os.path as osp import numpy as np @@ -142,6 +141,8 @@ def _load_executable_net(self, batch_size=1): self._net = plugin.load(network=network, num_requests=1) def infer(self, inputs): + import cv2 + assert len(inputs.shape) == 4, \ "Expected an input image in (N, H, W, C) format, got %s" % \ (inputs.shape) diff --git a/datumaro/datumaro/util/command_targets.py b/datumaro/datumaro/util/command_targets.py index 8c7c2dd9271..d8035a23da3 100644 --- a/datumaro/datumaro/util/command_targets.py +++ b/datumaro/datumaro/util/command_targets.py @@ -4,10 +4,10 @@ # SPDX-License-Identifier: MIT import argparse -import cv2 from enum import Enum from datumaro.components.project import Project +from datumaro.util.image import load_image TargetKinds = Enum('TargetKinds', @@ -50,7 +50,10 @@ def is_inference_path(value): return False def is_image_path(value): - return cv2.imread(value) is not None + try: + return load_image(value) is not None + except Exception: + return False class Target: diff --git a/datumaro/datumaro/util/image.py b/datumaro/datumaro/util/image.py index de37b4e8913..3eb7d3b26b4 100644 --- a/datumaro/datumaro/util/image.py +++ b/datumaro/datumaro/util/image.py @@ -3,9 +3,18 @@ # # SPDX-License-Identifier: MIT -import cv2 import numpy as np +from enum import Enum +_IMAGE_BACKENDS = Enum('_IMAGE_BACKENDS', ['cv2', 'PIL']) +_IMAGE_BACKEND = None +try: + import cv2 + _IMAGE_BACKEND = _IMAGE_BACKENDS.cv2 +except ModuleNotFoundError: + import PIL + _IMAGE_BACKEND = _IMAGE_BACKENDS.PIL + from datumaro.util.image_cache import ImageCache as _ImageCache @@ -13,13 +22,39 @@ def load_image(path): """ Reads an image in the HWC Grayscale/BGR(A) float [0; 255] format. """ - image = cv2.imread(path) - image = image.astype(np.float32) + + if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2: + import cv2 + image = cv2.imread(path) + image = image.astype(np.float32) + elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: + from PIL import Image + image = Image.open(path) + image = np.asarray(image, dtype=np.float32) + if len(image.shape) == 3 and image.shape[2] in [3, 4]: + image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR + else: + raise NotImplementedError() assert len(image.shape) == 3 assert image.shape[2] in [1, 3, 4] return image +def save_image(path, image): + if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2: + import cv2 + cv2.imwrite(path, image) + elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: + from PIL import Image + image = image.astype(np.uint8) + if len(image.shape) == 3 and image.shape[2] in [3, 4]: + image[:, :, :3] = image[:, :, 2::-1] # BGR to RGB + image = Image.fromarray(image) + image.save(path) + else: + raise NotImplementedError() + + class lazy_image: def __init__(self, path, loader=load_image, cache=None): self.path = path diff --git a/datumaro/tests/test_image.py b/datumaro/tests/test_image.py new file mode 100644 index 00000000000..67e97d20eed --- /dev/null +++ b/datumaro/tests/test_image.py @@ -0,0 +1,39 @@ +from itertools import product +import numpy as np +import os.path as osp + +from unittest import TestCase + +import datumaro.util.image as image_module +from datumaro.util.test_utils import TestDir + + +class ImageTest(TestCase): + def setUp(self): + self.default_backend = image_module._IMAGE_BACKEND + + def tearDown(self): + image_module._IMAGE_BACKEND = self.default_backend + + def _test_can_save_and_load(self, src_image, path, + save_backend=None, load_backend=None): + if save_backend: + image_module._IMAGE_BACKEND = save_backend + image_module.save_image(path, src_image) + + if load_backend: + image_module._IMAGE_BACKEND = load_backend + dst_image = image_module.load_image(path) + + self.assertTrue(np.all(src_image == dst_image), 'save: %s, load: %s' % \ + (save_backend, load_backend)) + + def test_save_and_load_backends(self): + backends = image_module._IMAGE_BACKENDS + for save_backend, load_backend in product(backends, backends): + with TestDir() as test_dir: + src_image = np.random.random_integers(0, 255, (2, 4, 3)) + image_path = osp.join(test_dir.path, 'img.png') + + self._test_can_save_and_load(src_image, image_path, + save_backend, load_backend) \ No newline at end of file From c887a9d30e96b2563f0617d843622c54552f6be9 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:30:28 +0300 Subject: [PATCH 6/8] Fix setup --- datumaro/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datumaro/setup.py b/datumaro/setup.py index cb90d1b3aba..f1556b4ae3f 100644 --- a/datumaro/setup.py +++ b/datumaro/setup.py @@ -48,11 +48,11 @@ def find_version(file_path=None): ], python_requires='>=3.5', install_requires=[ - 'cv2', 'GitPython', 'lxml', 'matplotlib', 'numpy', + 'opencv-python', 'Pillow', 'PyYAML', 'pycocotools', From 14516a0b1976afee9a89535060efbf1b1beed6a2 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:33:46 +0300 Subject: [PATCH 7/8] Fix cli command --- datumaro/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datumaro/setup.py b/datumaro/setup.py index f1556b4ae3f..a250412a0c7 100644 --- a/datumaro/setup.py +++ b/datumaro/setup.py @@ -60,7 +60,7 @@ def find_version(file_path=None): ], entry_points={ 'console_scripts': [ - 'datum=datum:main', + 'datum=datumaro:main', ], }, ) \ No newline at end of file From 07827f3c72c6135bbe1ef4eca3b616a13a4114e0 Mon Sep 17 00:00:00 2001 From: Zhiltsov Max Date: Fri, 29 Nov 2019 17:49:58 +0300 Subject: [PATCH 8/8] Codacy --- datumaro/datumaro/util/image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/datumaro/datumaro/util/image.py b/datumaro/datumaro/util/image.py index 3eb7d3b26b4..e55cce73af9 100644 --- a/datumaro/datumaro/util/image.py +++ b/datumaro/datumaro/util/image.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: MIT +# pylint: disable=unused-import + import numpy as np from enum import Enum