From bb2dc415ca761f90b024f53b0fefdc9bf39f784e Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 4 Feb 2025 17:13:25 +0400 Subject: [PATCH 01/49] syncing util/mask_tools.py --- src/datumaro/util/mask_tools.py | 101 ++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/datumaro/util/mask_tools.py b/src/datumaro/util/mask_tools.py index 2c0056dd41..7bac21b9ba 100644 --- a/src/datumaro/util/mask_tools.py +++ b/src/datumaro/util/mask_tools.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2021 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -7,7 +7,9 @@ from typing import Dict, List, NamedTuple, NewType, Optional, Sequence, Tuple, TypedDict, Union import numpy as np +from pycocotools import mask as pycocotools_mask +from datumaro._capi import encode from datumaro.util.image import lazy_image, load_image @@ -79,7 +81,7 @@ def check_is_mask(mask: np.ndarray) -> bool: _default_unpaint_colormap = invert_colormap(_default_colormap) -def unpaint_mask(painted_mask: ColorMask, inverse_colormap=None) -> IndexMask: +def unpaint_mask(painted_mask: ColorMask, inverse_colormap=None, default_id=None) -> IndexMask: """ Convert color mask to index mask @@ -104,8 +106,10 @@ def unpaint_mask(painted_mask: ColorMask, inverse_colormap=None) -> IndexMask: palette = [] for v in uvals: class_id = map_fn(v) - if class_id is None: + if class_id is None and default_id is None: raise KeyError(f"Undeclared color {((v >> 16) & 255, (v >> 8) & 255, v & 255)}") + elif class_id is None and default_id is not None: + class_id = default_id palette.append(class_id) palette = np.array(palette, dtype=np.min_scalar_type(len(uvals))) unpainted_mask = palette[unpainted_mask].reshape(painted_mask.shape[:2]) @@ -224,11 +228,11 @@ def index2bgr(id_map): return np.dstack((id_map >> 16, id_map >> 8, id_map)).astype(np.uint8) -def load_mask(path, inverse_colormap=None): +def load_mask(path, inverse_colormap=None, default_id=None): mask = load_image(path, dtype=np.uint8) if inverse_colormap is not None: if len(mask.shape) == 3 and mask.shape[2] != 1: - mask = unpaint_mask(mask, inverse_colormap) + mask = unpaint_mask(mask, inverse_colormap, default_id) return mask @@ -237,6 +241,10 @@ def lazy_mask(path, inverse_colormap=None): def mask_to_rle(binary_mask: BinaryMask) -> CompressedRle: + return encode(binary_mask) + + +def mask_to_rle_py(binary_mask: BinaryMask) -> CompressedRle: # walk in row-major order as COCO format specifies bounded = binary_mask.ravel(order="F") @@ -308,7 +316,19 @@ def is_outside_contour(index): return parent_to_children -def _extract_contours(mask: np.ndarray) -> List[np.ndarray]: +def extract_contours(mask: np.ndarray) -> List[np.ndarray]: + """ + Convert an instance mask to polygons + + Args: + mask: a 2d binary mask + tolerance: maximum distance from original points of + a polygon to the approximated ones + area_threshold: minimal area of generated polygons + + Returns: + A list of polygons like [[x1,y1, x2,y2 ...], [...]] + """ import cv2 contours, hierarchy = cv2.findContours( @@ -355,13 +375,14 @@ def mask_to_polygons(mask: BinaryMask, area_threshold=1) -> List[Polygon]: Returns: A list of polygons like [[x1,y1, x2,y2 ...], [...]] """ - from pycocotools import mask as mask_utils + + contours = extract_contours(mask) polygons = [] - for contour in _extract_contours(mask): + for contour in contours: # Check if the polygon is big enough - rle = mask_utils.frPyObjects([contour], mask.shape[0], mask.shape[1]) - area = sum(mask_utils.area(rle)) + rle = pycocotools_mask.frPyObjects([contour], mask.shape[0], mask.shape[1]) + area = sum(pycocotools_mask.area(rle)) if area_threshold <= area: polygons.append(contour) return polygons @@ -383,9 +404,30 @@ def to_uncompressed_rle(rle: Rle, *, width: int, height: int) -> UncompressedRle if is_uncompressed_rle(rle): return rle - from pycocotools import mask as mask_utils + return pycocotools_mask.frPyObjects(rle, height, width) - return mask_utils.frPyObjects(rle, height, width) + +def mask_to_bboxes(mask): + """ + Convert an instance mask to bboxes + + Args: + mask: a 2d binary mask + + Returns: + A list of bboxes like [[x1,x2,y1,y2], [...]] + """ + + contours = extract_contours(mask) + + bboxes = [] + for contour in contours: + x1, x2 = min(contour[0::2]), max(contour[0::2]) + y1, y2 = min(contour[1::2]), max(contour[1::2]) + + bboxes.append([x1, x2, y1, y2]) + + return bboxes def crop_covered_segments( @@ -427,18 +469,16 @@ def crop_covered_segments( ... ] """ - from pycocotools import mask as mask_utils - # Convert to uncompressed RLEs wrapped_segments = [[s] for s in segments] input_rles = [ - mask_utils.frPyObjects(s, height, width) if not is_uncompressed_rle(s[0]) else s + pycocotools_mask.frPyObjects(s, height, width) if not is_uncompressed_rle(s[0]) else s for s in wrapped_segments ] output_segments = [] for i, rle_bottom in enumerate(input_rles): - area_bottom = sum(mask_utils.area(rle_bottom)) + area_bottom = sum(pycocotools_mask.area(rle_bottom)) if area_bottom < area_threshold: output_segments.append([] if not return_masks else None) continue @@ -446,12 +486,12 @@ def crop_covered_segments( rles_top = [] for j in range(i + 1, len(input_rles)): rle_top = input_rles[j] - iou = sum(mask_utils.iou(rle_bottom, rle_top, [0]))[0] + iou = sum(pycocotools_mask.iou(rle_bottom, rle_top, [0]))[0] if iou <= iou_threshold: continue - area_top = sum(mask_utils.area(rle_top)) + area_top = sum(pycocotools_mask.area(rle_top)) area_ratio = area_top / area_bottom # If the top segment is (almost) fully inside the background one, @@ -466,11 +506,11 @@ def crop_covered_segments( continue rle_bottom = rle_bottom[0] - bottom_mask = mask_utils.decode(rle_bottom).astype(np.uint8) + bottom_mask = pycocotools_mask.decode(rle_bottom).astype(np.uint8) if rles_top: - rle_top = mask_utils.merge(rles_top) - top_mask = mask_utils.decode(rle_top).astype(np.uint8) + rle_top = pycocotools_mask.merge(rles_top) + top_mask = pycocotools_mask.decode(rle_top).astype(np.uint8) bottom_mask -= top_mask bottom_mask[bottom_mask != 1] = 0 @@ -486,14 +526,23 @@ def crop_covered_segments( def rles_to_mask(rles: Sequence[Union[CompressedRle, Polygon]], width, height) -> BinaryMask: - from pycocotools import mask as mask_utils - - rles = mask_utils.frPyObjects(rles, height, width) - rles = mask_utils.merge(rles) - mask = mask_utils.decode(rles) + rles = pycocotools_mask.frPyObjects(rles, height, width) + rles = pycocotools_mask.merge(rles) + mask = pycocotools_mask.decode(rles) return mask +def rle_to_mask(rle_uncompressed: Dict[str, np.ndarray]) -> np.ndarray: + """Decode the uncompressed RLE string to the binary mask (2D np.ndarray) + + The uncompressed RLE string can be obtained by + the datumaro.util.mask_tools.mask_to_rle() function + """ + resulting_mask = pycocotools_mask.frPyObjects(rle_uncompressed, *rle_uncompressed["size"]) + resulting_mask = pycocotools_mask.decode(resulting_mask) + return resulting_mask + + def find_mask_bbox(mask: BinaryMask) -> BboxCoords: cols = np.any(mask, axis=0) rows = np.any(mask, axis=1) From 5919001e80b92e20cf918586698d930bc8173199 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 5 Feb 2025 14:19:23 +0400 Subject: [PATCH 02/49] syncing util/image.py --- src/datumaro/util/image.py | 310 ++++++++++++++------ tests/unit/data_formats/test_yolo_format.py | 10 +- tests/unit/test_image.py | 36 ++- 3 files changed, 262 insertions(+), 94 deletions(-) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index 1fdab39c64..ef671b6c70 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -1,110 +1,219 @@ -# Copyright (C) 2019-2021 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # Copyright (C) 2023 CVAT.ai Corporation # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MIT§ +from __future__ import annotations -import importlib import os import os.path as osp import shlex -import warnings import weakref +from contextlib import contextmanager +from contextvars import ContextVar from enum import Enum, auto -from io import BytesIO -from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Tuple, Union +from functools import partial +from io import BytesIO, IOBase +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Tuple, Union import numpy as np -try: - # Introduced in 1.20 - from numpy.typing import DTypeLike -except ImportError: - DTypeLike = Any +from datumaro.components.crypter import NULL_CRYPTER, Crypter +from datumaro.components.errors import DatumaroError + +if TYPE_CHECKING: + try: + # Introduced in 1.20 + from numpy.typing import DTypeLike + except ImportError: + DTypeLike = Any -class _IMAGE_BACKENDS(Enum): +class ImageBackend(Enum): cv2 = auto() PIL = auto() -_IMAGE_BACKEND = None +IMAGE_BACKEND: ContextVar[ImageBackend] = ContextVar("IMAGE_BACKEND") _image_loading_errors = (FileNotFoundError,) try: - importlib.import_module("cv2") - _IMAGE_BACKEND = _IMAGE_BACKENDS.cv2 + import cv2 + + IMAGE_BACKEND.set(ImageBackend.cv2) except ModuleNotFoundError: import PIL - _IMAGE_BACKEND = _IMAGE_BACKENDS.PIL + IMAGE_BACKEND.set(ImageBackend.PIL) _image_loading_errors = (*_image_loading_errors, PIL.UnidentifiedImageError) from datumaro.util.image_cache import ImageCache from datumaro.util.os_util import find_files -def __getattr__(name: str): - if name in {"Image", "ByteImage"}: - warnings.warn( - f"Using {name} from 'util.image' is deprecated, " - "the class is moved to 'components.media'", - DeprecationWarning, - stacklevel=2, +class ImageColorChannel(Enum): + """Image color channel + + - UNCHANGED: Use the original image's channel (default) + - COLOR_BGR: Use BGR 3 channels + (it can ignore the alpha channel or convert the gray scale image) + - COLOR_RGB: Use RGB 3 channels + (it can ignore the alpha channel or convert the gray scale image) + """ + + UNCHANGED = 0 + COLOR_BGR = 1 + COLOR_RGB = 2 + + def decode_by_cv2( + self, image_bytes: bytes, dtype: DTypeLike = np.uint8, keep_exif: bool = False + ) -> np.ndarray: + """Convert image color channel for OpenCV image (np.ndarray).""" + image_buffer = np.frombuffer(image_bytes, dtype=dtype) + + if self == ImageColorChannel.UNCHANGED: + return cv2.imdecode( + image_buffer, + cv2.IMREAD_UNCHANGED ^ (cv2.IMREAD_IGNORE_ORIENTATION if keep_exif else 0), + ) + + img = cv2.imdecode( + image_buffer, cv2.IMREAD_COLOR ^ (0 if keep_exif else cv2.IMREAD_IGNORE_ORIENTATION) ) - import datumaro.components.media as media_module + if self == ImageColorChannel.COLOR_BGR: + return img + + if self == ImageColorChannel.COLOR_RGB: + return cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + raise ValueError + + def decode_by_pil(self, image_bytes: bytes, keep_exif: bool = False) -> np.ndarray: + """Convert image color channel for PIL Image.""" + from PIL import Image, ImageOps + + img = Image.open(BytesIO(image_bytes)) + + if keep_exif: + img = ImageOps.exif_transpose(img) + + if self == ImageColorChannel.UNCHANGED: + return np.asarray(img) + + if self == ImageColorChannel.COLOR_BGR: + img = np.asarray(img.convert("RGB")) + return cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + + if self == ImageColorChannel.COLOR_RGB: + return np.asarray(img.convert("RGB")) + + raise ValueError + + +IMAGE_COLOR_CHANNEL: ContextVar[ImageColorChannel] = ContextVar( + "IMAGE_COLOR_CHANNEL", default=ImageColorChannel.UNCHANGED +) + + +@contextmanager +def decode_image_context(image_backend: ImageBackend, image_color_channel: ImageColorChannel): + """Change Datumaro image color channel while decoding. + + For model training, it is recommended to use this context manager + to load images in the BGR 3-channel format. For example, + + .. code-block:: python + + import datumaro as dm + with decode_image_context( + image_backend=ImageBackend.cv2, + image_color_channel=ImageColorScale.COLOR, + ): + item: dm.DatasetItem + img_data = item.media_as(dm.Image).data + assert img_data.shape[-1] == 3 # It should be a 3-channel image + """ + + curr_ctx = (IMAGE_BACKEND.get(), IMAGE_COLOR_CHANNEL.get()) + + IMAGE_BACKEND.set(image_backend) + IMAGE_COLOR_CHANNEL.set(image_color_channel) + + yield - return getattr(media_module, name) - raise AttributeError(f"module {__name__} has no attribute {name}") + IMAGE_BACKEND.set(curr_ctx[0]) + IMAGE_COLOR_CHANNEL.set(curr_ctx[1]) -def load_image(path: str, dtype: DTypeLike = np.float32, **kwargs): +def load_image( + path: str, dtype: DTypeLike = np.uint8, crypter: Crypter = NULL_CRYPTER, keep_exif: bool = False +): """ - Reads an image in the HWC Grayscale/BGR(A) float [0; 255] format. + Reads an image in the HWC Grayscale/BGR(A) [0; 255] format (default dtype is uint8). """ - if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2: + if IMAGE_BACKEND.get() == ImageBackend.cv2: # cv2.imread does not support paths that are not representable # in the locale encoding on Windows, so we read the image bytes # ourselves. with open(path, "rb") as f: - image_bytes = f.read() + image_bytes = crypter.decrypt(f.read()) - if kwargs.get("keep_exif"): - return decode_image(image_bytes, dtype=dtype, cv2_read_flag=1) + return decode_image(image_bytes, dtype=dtype, keep_exif=keep_exif) + elif IMAGE_BACKEND.get() == ImageBackend.PIL: + with open(path, "rb") as f: + image_bytes = crypter.decrypt(f.read()) - return decode_image(image_bytes, dtype=dtype) - elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: - from PIL import Image, ImageOps + return decode_image(image_bytes, dtype=dtype, keep_exif=keep_exif) - image = Image.open(path) + raise NotImplementedError(IMAGE_BACKEND) - if kwargs.get("keep_exif"): - image = ImageOps.exif_transpose(image) - image = np.asarray(image, dtype=dtype) - if len(image.shape) == 3 and image.shape[2] in {3, 4}: - image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR - else: - raise NotImplementedError() +def copyto_image( + src: Union[str, IOBase], dst: Union[str, IOBase], src_crypter: Crypter, dst_crypter: Crypter +) -> None: + if src_crypter == dst_crypter and src == dst: + return - assert len(image.shape) in {2, 3} - if len(image.shape) == 3: - assert image.shape[2] in {3, 4} - return image + @contextmanager + def _open(fp, mode): + was_file = False + if not isinstance(fp, IOBase): + was_file = True + fp = open(fp, mode) + yield fp + if was_file: + fp.close() + + with _open(src, "rb") as src_fp: + _bytes = src_crypter.decrypt(src_fp.read()) + + with _open(dst, "wb") as dst_fp: + dst_fp.write(dst_crypter.encrypt(_bytes)) def save_image( - path: str, image: np.ndarray, create_dir: bool = False, dtype: DTypeLike = np.uint8, **kwargs + dst: Union[str, IOBase], + image: np.ndarray, + ext: Optional[str] = None, + create_dir: bool = False, + dtype: DTypeLike = np.uint8, + crypter: Crypter = NULL_CRYPTER, + **kwargs, ) -> None: # NOTE: Check destination path for existence # OpenCV silently fails if target directory does not exist - dst_dir = osp.dirname(path) - if dst_dir: - if create_dir: - os.makedirs(dst_dir, exist_ok=True) - elif not osp.isdir(dst_dir): - raise FileNotFoundError("Directory does not exist: '%s'" % dst_dir) + if isinstance(dst, IOBase): + ext = ext if ext else ".png" + else: + dst_dir = osp.dirname(dst) + if dst_dir: + if create_dir: + os.makedirs(dst_dir, exist_ok=True) + elif not osp.isdir(dst_dir): + raise FileNotFoundError("Directory does not exist: '%s'" % dst_dir) + # file extension and actual encoding can be differ + ext = ext if ext else osp.splitext(dst)[1] if not kwargs: kwargs = {} @@ -112,22 +221,30 @@ def save_image( # NOTE: OpenCV documentation says "If the image format is not supported, # the image will be converted to 8-bit unsigned and saved that way". # Conversion from np.int32 to np.uint8 is not working properly - backend = _IMAGE_BACKEND + backend = IMAGE_BACKEND.get() if dtype == np.int32: - backend = _IMAGE_BACKENDS.PIL - if backend == _IMAGE_BACKENDS.cv2: + backend = ImageBackend.PIL + if backend == ImageBackend.cv2: # cv2.imwrite does not support paths that are not representable # in the locale encoding on Windows, so we write the image bytes # ourselves. - ext = osp.splitext(path)[1] image_bytes = encode_image(image, ext, dtype=dtype, **kwargs) - with open(path, "wb") as f: - f.write(image_bytes) - elif backend == _IMAGE_BACKENDS.PIL: + if isinstance(dst, str): + with open(dst, "wb") as f: + f.write(crypter.encrypt(image_bytes)) + else: + dst.write(crypter.encrypt(image_bytes)) + elif backend == ImageBackend.PIL: from PIL import Image + if ext.startswith("."): + ext = ext[1:] + + if not crypter.is_null_crypter: + raise DatumaroError("PIL backend should have crypter=NullCrypter.") + params = {} params["quality"] = kwargs.get("jpeg_quality") if kwargs.get("jpeg_quality") == 100: @@ -137,7 +254,7 @@ def save_image( 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, **params) + image.save(dst, format=ext, **params) else: raise NotImplementedError() @@ -146,7 +263,7 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa if not kwargs: kwargs = {} - if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2: + if IMAGE_BACKEND.get() == ImageBackend.cv2: import cv2 params = [] @@ -154,7 +271,7 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa if not ext.startswith("."): ext = "." + ext - if ext.upper() == ".JPG": + if ext.upper() in (".JPG", ".JPEG"): params = [int(cv2.IMWRITE_JPEG_QUALITY), kwargs.get("jpeg_quality", 75)] image = image.astype(dtype) @@ -162,7 +279,7 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa if not success: raise Exception("Failed to encode image to '%s' format" % (ext)) return result.tobytes() - elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: + elif IMAGE_BACKEND.get() == ImageBackend.PIL: from PIL import Image if ext.startswith("."): @@ -184,22 +301,30 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa raise NotImplementedError() -def decode_image(image_bytes: bytes, dtype: DTypeLike = np.float32, **kwargs) -> np.ndarray: - if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2: - import cv2 - - image = np.frombuffer(image_bytes, dtype=np.uint8) - image = cv2.imdecode(image, kwargs.get("cv2_read_flag", cv2.IMREAD_UNCHANGED)) - image = image.astype(dtype) - elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: - from PIL import Image - - image = Image.open(BytesIO(image_bytes)) - image = np.asarray(image, dtype=dtype) - if len(image.shape) == 3 and image.shape[2] in {3, 4}: - image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR +def decode_image( + image_bytes: bytes, dtype: np.dtype = np.uint8, keep_exif: bool = False +) -> np.ndarray: + ctx_color_scale = IMAGE_COLOR_CHANNEL.get() + + if np.issubdtype(dtype, np.floating): + # PIL doesn't support floating point image loading + # CV doesn't support floating point image with color channel setting (IMREAD_COLOR) + with decode_image_context( + image_backend=ImageBackend.cv2, image_color_channel=ImageColorChannel.UNCHANGED + ): + image = ctx_color_scale.decode_by_cv2(image_bytes, dtype=dtype, keep_exif=keep_exif) + image = image[..., ::-1] + if ctx_color_scale == ImageColorChannel.COLOR_BGR: + image = image[..., ::-1] else: - raise NotImplementedError() + if IMAGE_BACKEND.get() == ImageBackend.cv2: + image = ctx_color_scale.decode_by_cv2(image_bytes, keep_exif=keep_exif) + elif IMAGE_BACKEND.get() == ImageBackend.PIL: + image = ctx_color_scale.decode_by_pil(image_bytes, keep_exif=keep_exif) + else: + raise NotImplementedError() + + image = image.astype(dtype) assert len(image.shape) in {2, 3} if len(image.shape) == 3: @@ -237,10 +362,15 @@ def find_images( dirpath: str, exts: Union[str, Iterable[str]] = None, recursive: bool = False, - max_depth: int = None, + max_depth: Optional[int] = None, + min_depth: Optional[int] = None, ) -> Iterator[str]: yield from find_files( - dirpath, exts=exts or IMAGE_EXTENSIONS, recursive=recursive, max_depth=max_depth + dirpath, + exts=exts or IMAGE_EXTENSIONS, + recursive=recursive, + max_depth=max_depth, + min_depth=min_depth, ) @@ -255,6 +385,8 @@ def __init__( path: str, loader: Callable[[str], np.ndarray] = None, cache: Union[bool, ImageCache] = True, + crypter: Crypter = NULL_CRYPTER, + dtype: Optional[DTypeLike] = None, ) -> None: """ Cache: @@ -263,13 +395,19 @@ def __init__( - ImageCache instance: an object to be used as cache """ + self._custom_loader = True + if loader is None: - loader = load_image + loader = partial(load_image, dtype=dtype) if dtype else load_image + self._custom_loader = False + self._path = path self._loader = loader assert isinstance(cache, (ImageCache, bool)) self._cache = cache + self._crypter = crypter + self._dtype = dtype def __call__(self) -> np.ndarray: image = None @@ -280,7 +418,11 @@ def __call__(self) -> np.ndarray: image = cache.get(cache_key) if image is None: - image = self._loader(self._path) + image = ( + self._loader(self._path) + if self._custom_loader + else self._loader(self._path, crypter=self._crypter) + ) if cache is not None: cache.push(cache_key, image) return image diff --git a/tests/unit/data_formats/test_yolo_format.py b/tests/unit/data_formats/test_yolo_format.py index b8601b8122..a8690195c4 100644 --- a/tests/unit/data_formats/test_yolo_format.py +++ b/tests/unit/data_formats/test_yolo_format.py @@ -64,13 +64,19 @@ YoloUltralyticsSegmentationImporter, ) from datumaro.util.definitions import DEFAULT_SUBSET_NAME -from datumaro.util.image import save_image +from datumaro.util.image import IMAGE_COLOR_CHANNEL, ImageBackend, decode_image_context, save_image from tests.requirements import Requirements, mark_requirement from tests.utils.assets import get_test_asset_path from tests.utils.test_utils import compare_annotations, compare_datasets, compare_datasets_strict +@pytest.fixture(params=ImageBackend) +def image_backend(request): + with decode_image_context(request.param, IMAGE_COLOR_CHANNEL.get()): + yield + + @pytest.fixture(autouse=True) def seed_random(): random.seed(1234) @@ -1134,7 +1140,7 @@ def test_can_import(self): self.compare_datasets(expected_dataset, dataset) @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_can_import_with_exif_rotated_images(self, test_dir): + def test_can_import_with_exif_rotated_images(self, test_dir, image_backend): expected_dataset = Dataset.from_iterable( [ DatasetItem( diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 81d573cda7..5b5632417c 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -12,14 +12,14 @@ class ImageOperationsTest(TestCase): def setUp(self): - self.default_backend = image_module._IMAGE_BACKEND + self.default_backend = image_module.IMAGE_BACKEND.get() def tearDown(self): - image_module._IMAGE_BACKEND = self.default_backend + image_module.IMAGE_BACKEND.set(self.default_backend) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_save_and_load_backends(self): - backends = image_module._IMAGE_BACKENDS + backends = image_module.ImageBackend for save_backend, load_backend, c in product(backends, backends, [1, 3]): with TestDir() as test_dir: if c == 1: @@ -28,12 +28,22 @@ def test_save_and_load_backends(self): src_image = np.random.randint(0, 255 + 1, (2, 4, c)) path = osp.join(test_dir, "img.png") # lossless - image_module._IMAGE_BACKEND = save_backend + image_module.IMAGE_BACKEND.set(save_backend) image_module.save_image(path, src_image, jpeg_quality=100) - image_module._IMAGE_BACKEND = load_backend + image_module.IMAGE_BACKEND.set(load_backend) dst_image = image_module.load_image(path) + # If image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED + # OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). + if ( + c == 3 + and load_backend == image_module.ImageBackend.PIL + and image_module.IMAGE_COLOR_CHANNEL.get() + == image_module.ImageColorChannel.UNCHANGED + ): + dst_image = np.flip(dst_image, -1) + self.assertTrue( np.array_equal(src_image, dst_image), "save: %s, load: %s" % (save_backend, load_backend), @@ -41,19 +51,29 @@ def test_save_and_load_backends(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_encode_and_decode_backends(self): - backends = image_module._IMAGE_BACKENDS + backends = image_module.ImageBackend for save_backend, load_backend, c in product(backends, backends, [1, 3]): if c == 1: src_image = np.random.randint(0, 255 + 1, (2, 4)) else: src_image = np.random.randint(0, 255 + 1, (2, 4, c)) - image_module._IMAGE_BACKEND = save_backend + image_module.IMAGE_BACKEND.set(save_backend) buffer = image_module.encode_image(src_image, ".png", jpeg_quality=100) # lossless - image_module._IMAGE_BACKEND = load_backend + image_module.IMAGE_BACKEND.set(load_backend) dst_image = image_module.decode_image(buffer) + # If image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED + # OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). + if ( + c == 3 + and load_backend == image_module.ImageBackend.PIL + and image_module.IMAGE_COLOR_CHANNEL.get() + == image_module.ImageColorChannel.UNCHANGED + ): + dst_image = np.flip(dst_image, -1) + self.assertTrue( np.array_equal(src_image, dst_image), "save: %s, load: %s" % (save_backend, load_backend), From e41a9d0849cb1605edcf398f471d7e7353b45ad2 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 5 Feb 2025 16:15:25 +0400 Subject: [PATCH 03/49] keeping exif unconditionally --- .../plugins/data_formats/yolo/base.py | 10 +---- src/datumaro/util/image.py | 37 ++++++------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/datumaro/plugins/data_formats/yolo/base.py b/src/datumaro/plugins/data_formats/yolo/base.py index 9f16f2a687..11834e8884 100644 --- a/src/datumaro/plugins/data_formats/yolo/base.py +++ b/src/datumaro/plugins/data_formats/yolo/base.py @@ -39,7 +39,6 @@ DEFAULT_IMAGE_META_FILE_NAME, ImageMeta, find_images, - load_image, load_image_meta_file, ) from datumaro.util.meta_file_util import get_meta_file, has_meta_file, parse_meta_file @@ -108,10 +107,6 @@ def __init__( subset.items = self._get_lazy_subset_items(subset_name) self._subsets[subset_name] = subset - @classmethod - def _image_loader(cls, *args, **kwargs): - return load_image(*args, **kwargs, keep_exif=True) - def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]: subset = self._subsets[subset_name] item = subset.items[item_id] @@ -121,10 +116,7 @@ def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]: image_size = self._image_info.get(item_id) image_path = osp.join(self._path, item) - if image_size: - image = Image(path=image_path, size=image_size) - else: - image = Image(path=image_path, data=self._image_loader) + image = Image(path=image_path, size=image_size) annotations = self._parse_annotations(image, item_id=(item_id, subset_name)) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index ef671b6c70..ced4a77619 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -63,21 +63,14 @@ class ImageColorChannel(Enum): COLOR_BGR = 1 COLOR_RGB = 2 - def decode_by_cv2( - self, image_bytes: bytes, dtype: DTypeLike = np.uint8, keep_exif: bool = False - ) -> np.ndarray: + def decode_by_cv2(self, image_bytes: bytes, dtype: DTypeLike = np.uint8) -> np.ndarray: """Convert image color channel for OpenCV image (np.ndarray).""" image_buffer = np.frombuffer(image_bytes, dtype=dtype) if self == ImageColorChannel.UNCHANGED: - return cv2.imdecode( - image_buffer, - cv2.IMREAD_UNCHANGED ^ (cv2.IMREAD_IGNORE_ORIENTATION if keep_exif else 0), - ) + return cv2.imdecode(image_buffer, cv2.IMREAD_UNCHANGED ^ cv2.IMREAD_IGNORE_ORIENTATION) - img = cv2.imdecode( - image_buffer, cv2.IMREAD_COLOR ^ (0 if keep_exif else cv2.IMREAD_IGNORE_ORIENTATION) - ) + img = cv2.imdecode(image_buffer, cv2.IMREAD_COLOR) if self == ImageColorChannel.COLOR_BGR: return img @@ -87,14 +80,12 @@ def decode_by_cv2( raise ValueError - def decode_by_pil(self, image_bytes: bytes, keep_exif: bool = False) -> np.ndarray: + def decode_by_pil(self, image_bytes: bytes) -> np.ndarray: """Convert image color channel for PIL Image.""" from PIL import Image, ImageOps img = Image.open(BytesIO(image_bytes)) - - if keep_exif: - img = ImageOps.exif_transpose(img) + img = ImageOps.exif_transpose(img) if self == ImageColorChannel.UNCHANGED: return np.asarray(img) @@ -144,9 +135,7 @@ def decode_image_context(image_backend: ImageBackend, image_color_channel: Image IMAGE_COLOR_CHANNEL.set(curr_ctx[1]) -def load_image( - path: str, dtype: DTypeLike = np.uint8, crypter: Crypter = NULL_CRYPTER, keep_exif: bool = False -): +def load_image(path: str, dtype: DTypeLike = np.uint8, crypter: Crypter = NULL_CRYPTER): """ Reads an image in the HWC Grayscale/BGR(A) [0; 255] format (default dtype is uint8). """ @@ -159,12 +148,12 @@ def load_image( with open(path, "rb") as f: image_bytes = crypter.decrypt(f.read()) - return decode_image(image_bytes, dtype=dtype, keep_exif=keep_exif) + return decode_image(image_bytes, dtype=dtype) elif IMAGE_BACKEND.get() == ImageBackend.PIL: with open(path, "rb") as f: image_bytes = crypter.decrypt(f.read()) - return decode_image(image_bytes, dtype=dtype, keep_exif=keep_exif) + return decode_image(image_bytes, dtype=dtype) raise NotImplementedError(IMAGE_BACKEND) @@ -301,9 +290,7 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa raise NotImplementedError() -def decode_image( - image_bytes: bytes, dtype: np.dtype = np.uint8, keep_exif: bool = False -) -> np.ndarray: +def decode_image(image_bytes: bytes, dtype: np.dtype = np.uint8) -> np.ndarray: ctx_color_scale = IMAGE_COLOR_CHANNEL.get() if np.issubdtype(dtype, np.floating): @@ -312,15 +299,15 @@ def decode_image( with decode_image_context( image_backend=ImageBackend.cv2, image_color_channel=ImageColorChannel.UNCHANGED ): - image = ctx_color_scale.decode_by_cv2(image_bytes, dtype=dtype, keep_exif=keep_exif) + image = ctx_color_scale.decode_by_cv2(image_bytes, dtype=dtype) image = image[..., ::-1] if ctx_color_scale == ImageColorChannel.COLOR_BGR: image = image[..., ::-1] else: if IMAGE_BACKEND.get() == ImageBackend.cv2: - image = ctx_color_scale.decode_by_cv2(image_bytes, keep_exif=keep_exif) + image = ctx_color_scale.decode_by_cv2(image_bytes) elif IMAGE_BACKEND.get() == ImageBackend.PIL: - image = ctx_color_scale.decode_by_pil(image_bytes, keep_exif=keep_exif) + image = ctx_color_scale.decode_by_pil(image_bytes) else: raise NotImplementedError() From 43f53eb19e5e7cdfd751b5955c31b2a7975f8fb8 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 5 Feb 2025 18:27:42 +0400 Subject: [PATCH 04/49] syncing components/media.py --- src/datumaro/__init__.py | 2 +- src/datumaro/components/extractor_tfds.py | 4 +- src/datumaro/components/media.py | 1320 ++++++++++++++--- .../plugins/data_formats/ade20k2017.py | 2 +- .../plugins/data_formats/ade20k2020.py | 2 +- src/datumaro/plugins/data_formats/camvid.py | 2 +- .../data_formats/celeba/align_celeba.py | 6 +- .../plugins/data_formats/celeba/celeba.py | 6 +- src/datumaro/plugins/data_formats/cifar.py | 2 +- .../plugins/data_formats/cityscapes.py | 6 +- .../plugins/data_formats/coco/base.py | 4 +- .../common_semantic_segmentation.py | 2 +- .../data_formats/common_super_resolution.py | 11 +- .../plugins/data_formats/cvat/base.py | 4 +- .../plugins/data_formats/datumaro/base.py | 6 +- .../plugins/data_formats/datumaro/exporter.py | 7 +- .../plugins/data_formats/icdar/base.py | 6 +- .../plugins/data_formats/image_dir.py | 4 +- .../plugins/data_formats/image_zip.py | 11 +- src/datumaro/plugins/data_formats/imagenet.py | 4 +- .../plugins/data_formats/imagenet_txt.py | 2 +- .../plugins/data_formats/kitti/base.py | 6 +- .../plugins/data_formats/kitti_raw/base.py | 12 +- src/datumaro/plugins/data_formats/labelme.py | 2 +- src/datumaro/plugins/data_formats/lfw.py | 10 +- .../data_formats/mapillary_vistas/base.py | 6 +- .../plugins/data_formats/market1501.py | 5 +- src/datumaro/plugins/data_formats/mars.py | 2 +- src/datumaro/plugins/data_formats/mnist.py | 2 +- .../plugins/data_formats/mnist_csv.py | 2 +- src/datumaro/plugins/data_formats/mot.py | 6 +- src/datumaro/plugins/data_formats/mots.py | 2 +- .../plugins/data_formats/mpii/mpii_json.py | 2 +- .../plugins/data_formats/mpii/mpii_mat.py | 2 +- .../plugins/data_formats/nyu_depth_v2.py | 4 +- .../plugins/data_formats/open_images.py | 2 +- .../data_formats/sly_pointcloud/base.py | 6 +- src/datumaro/plugins/data_formats/synthia.py | 4 +- .../data_formats/tf_detection_api/base.py | 4 +- .../data_formats/tf_detection_api/exporter.py | 6 +- .../plugins/data_formats/vgg_face2.py | 4 +- src/datumaro/plugins/data_formats/voc/base.py | 6 +- src/datumaro/plugins/data_formats/vott_csv.py | 2 +- .../plugins/data_formats/vott_json.py | 4 +- .../plugins/data_formats/widerface.py | 2 +- .../plugins/data_formats/yolo/base.py | 2 +- src/datumaro/plugins/transforms.py | 4 +- tests/integration/cli/test_compare.py | 16 +- .../integration/cli/test_image_zip_format.py | 8 +- .../integration/cli/test_kitti_raw_format.py | 12 +- tests/integration/cli/test_merge.py | 28 +- tests/integration/cli/test_patch.py | 22 +- tests/integration/cli/test_project.py | 6 +- .../cli/test_sly_point_cloud_format.py | 12 +- tests/integration/cli/test_utils.py | 2 +- tests/integration/cli/test_voc_format.py | 48 +- tests/integration/cli/test_yolo_format.py | 10 +- .../datumaro/test_datumaro_format.py | 78 +- ...est_common_semantic_segmentation_format.py | 8 +- .../test_mapillary_vistas_format.py | 30 +- .../unit/data_formats/test_synthia_format.py | 20 +- tests/unit/data_formats/test_yolo_format.py | 128 +- tests/unit/test_ade20k2017_format.py | 6 +- tests/unit/test_ade20k2020_format.py | 6 +- tests/unit/test_align_celeba_format.py | 20 +- tests/unit/test_camvid_format.py | 52 +- tests/unit/test_celeba_format.py | 20 +- tests/unit/test_cifar_format.py | 101 +- tests/unit/test_cityscapes_format.py | 76 +- tests/unit/test_coco_format.py | 162 +- .../test_common_super_resolution_format.py | 12 +- tests/unit/test_compare.py | 20 +- tests/unit/test_cvat_format.py | 66 +- tests/unit/test_dataset.py | 58 +- tests/unit/test_extractor_tfds.py | 8 +- tests/unit/test_icdar_format.py | 52 +- tests/unit/test_image_dir_format.py | 20 +- tests/unit/test_image_zip_format.py | 27 +- tests/unit/test_imagenet_format.py | 40 +- tests/unit/test_imagenet_txt_format.py | 20 +- tests/unit/test_images.py | 52 +- tests/unit/test_kitti_format.py | 74 +- tests/unit/test_kitti_raw_format.py | 72 +- tests/unit/test_labelme_format.py | 40 +- tests/unit/test_lfw_format.py | 70 +- tests/unit/test_market1501_format.py | 26 +- tests/unit/test_mars_format.py | 6 +- tests/unit/test_mnist_csv_format.py | 67 +- tests/unit/test_mnist_format.py | 67 +- tests/unit/test_mot_format.py | 28 +- tests/unit/test_mots_format.py | 28 +- tests/unit/test_mpii_format.py | 6 +- tests/unit/test_mpii_json_format.py | 12 +- tests/unit/test_ndr.py | 7 +- tests/unit/test_nyu_depth_v2_format.py | 8 +- tests/unit/test_open_images_format.py | 46 +- tests/unit/test_ops.py | 72 +- tests/unit/test_project.py | 48 +- tests/unit/test_sampler.py | 2 +- tests/unit/test_sly_pointcloud_format.py | 34 +- tests/unit/test_splitter.py | 4 +- tests/unit/test_tfrecord_format.py | 42 +- tests/unit/test_transforms.py | 52 +- tests/unit/test_validator.py | 14 +- tests/unit/test_vgg_face2_format.py | 48 +- tests/unit/test_video.py | 12 +- tests/unit/test_voc_format.py | 98 +- tests/unit/test_vott_csv_format.py | 12 +- tests/unit/test_vott_json_format.py | 12 +- tests/unit/test_widerface_format.py | 48 +- tests/utils/test_utils.py | 6 +- 111 files changed, 2562 insertions(+), 1267 deletions(-) diff --git a/src/datumaro/__init__.py b/src/datumaro/__init__.py index accafbc5f4..e8e07903f0 100644 --- a/src/datumaro/__init__.py +++ b/src/datumaro/__init__.py @@ -49,7 +49,7 @@ ) from .components.importer import Importer from .components.launcher import Launcher -from .components.media import ByteImage, Image, MediaElement, PointCloud, Video, VideoFrame +from .components.media import Image, MediaElement, PointCloud, Video, VideoFrame from .components.media_manager import MediaManager from .components.progress_reporting import NullProgressReporter, ProgressReporter from .components.transformer import ItemTransform, ModelTransform, Transform diff --git a/src/datumaro/components/extractor_tfds.py b/src/datumaro/components/extractor_tfds.py index 83066a89b2..f10172785c 100644 --- a/src/datumaro/components/extractor_tfds.py +++ b/src/datumaro/components/extractor_tfds.py @@ -15,7 +15,7 @@ from datumaro.components.annotation import AnnotationType, Bbox, Label, LabelCategories from datumaro.components.dataset_base import CategoriesInfo, DatasetInfo, DatasetItem, IDataset -from datumaro.components.media import ByteImage, Image, MediaElement +from datumaro.components.media import Image, MediaElement from datumaro.util.tf_util import import_tf try: @@ -143,7 +143,7 @@ def __call__( else: filename = None - item.media = ByteImage(data=tfds_example[self.feature_name].numpy(), path=filename) + item.media = Image.from_bytes(data=tfds_example[self.feature_name].numpy(), path=filename) @frozen diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index 33d66248a9..f38a090ba8 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -1,70 +1,237 @@ -# Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2021-2024 Intel Corporation # # SPDX-License-Identifier: MIT from __future__ import annotations +import errno +import io import os import os.path as osp import shutil -import weakref -from typing import Callable, Iterable, Iterator, List, Optional, Tuple, Union +from copy import deepcopy +from enum import IntEnum +from functools import partial +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + Iterable, + Iterator, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) import cv2 import numpy as np -from datumaro.util.image import _image_loading_errors, decode_image, lazy_image, save_image +from datumaro.components.crypter import NULL_CRYPTER, Crypter +from datumaro.components.errors import DatumaroError, MediaShapeError +from datumaro.util.definitions import BboxIntCoords +from datumaro.util.image import ( + _image_loading_errors, + copyto_image, + decode_image, + lazy_image, + load_image, + save_image, +) + +if TYPE_CHECKING: + import pandas as pd +else: + from datumaro.util.import_util import lazy_import + + pd = lazy_import("pandas") + + +AnyData = TypeVar("AnyData", bytes, np.ndarray) + + +class MediaType(IntEnum): + NONE = 0 + MEDIA_ELEMENT = 1 + IMAGE = 2 + BYTE_IMAGE = 3 + VIDEO_FRAME = 4 + VIDEO = 5 + POINT_CLOUD = 6 + MULTIFRAME_IMAGE = 7 + ROI_IMAGE = 8 + MOSAIC_IMAGE = 9 + TABLE_ROW = 10 + @property + def media(self) -> Optional[Type[MediaElement]]: + if self == self.NONE: + return None + if self == self.MEDIA_ELEMENT: + return MediaElement + if self == self.IMAGE: + return Image + if self == self.VIDEO_FRAME: + return VideoFrame + if self == self.VIDEO: + return Video + if self == self.POINT_CLOUD: + return PointCloud + if self == self.MULTIFRAME_IMAGE: + return MultiframeImage + if self == self.ROI_IMAGE: + return RoIImage + if self == self.MOSAIC_IMAGE: + return MosaicImage + if self == self.TABLE_ROW: + return TableRow + raise NotImplementedError + + +class MediaElement(Generic[AnyData]): + _type = MediaType.MEDIA_ELEMENT + + def __init__(self, crypter: Crypter = NULL_CRYPTER, *args, **kwargs) -> None: + self._crypter = crypter + + def as_dict(self) -> Dict[str, Any]: + # NOTE: + # attributes starting with a single underscore are assumed + # to be arguments of __init__ method and + # attributes starting with double underscores are assuemd + # to be not directly related to __init__ method. + return { + key[1:]: value + for key, value in self.__dict__.items() + if key.startswith("_") and not key.startswith(f"_{self.__class__.__name__}") + } + + def from_self(self, **kwargs): + attrs = deepcopy(self.as_dict()) + attrs.update(kwargs) + return self.__class__(**attrs) + + @property + def is_encrypted(self) -> bool: + return not self._crypter.is_null_crypter + + def set_crypter(self, crypter: Crypter): + self._crypter = crypter + + @property + def type(self) -> MediaType: + return self._type + + @property + def data(self) -> Optional[AnyData]: + return None + + @property + def has_data(self) -> bool: + return False + + @property + def bytes(self) -> Optional[bytes]: + return None -class MediaElement: - def __init__(self, path: str) -> None: + def __eq__(self, other: object) -> bool: + other_type = getattr(other, "type", None) + if self.type != other_type: + return False + return True + + def save( + self, + fp: Union[str, io.IOBase], + crypter: Crypter = NULL_CRYPTER, + ): + raise NotImplementedError + + +class FromFileMixin: + def __init__(self, path: str, *args, **kwargs): + super().__init__(*args, **kwargs) assert path, "Path can't be empty" self._path = path @property def path(self) -> str: """Path to the media file""" - return self._path + # TODO: do we need this replace? + return self._path.replace("\\", "/") @property - def ext(self) -> str: - """Media file extension (with the leading dot)""" - return osp.splitext(osp.basename(self.path))[1] + def bytes(self) -> Optional[bytes]: + if self.has_data: + with open(self._path, "rb") as f: + _bytes = f.read() + return _bytes + return None - def __eq__(self, other: object) -> bool: - # We need to compare exactly with this type - if type(other) is not __class__: # pylint: disable=unidiomatic-typecheck - return False - return self._path == other._path + @property + def has_data(self) -> bool: + return os.path.exists(self.path) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(path={repr(self._path)})" + + +class FromDataMixin(Generic[AnyData]): + def __init__(self, data: Union[Callable[[], AnyData], AnyData], *args, **kwargs): + super().__init__(*args, **kwargs) + self._data = data + + @property + def data(self) -> Optional[AnyData]: + if callable(self._data): + return self._data() + return self._data + + @property + def bytes(self) -> Optional[bytes]: + if self.has_data: + _bytes = self._data() if callable(self._data) else self._data + if isinstance(_bytes, bytes): + return _bytes + return None + @property + def has_data(self) -> bool: + return self._data is not None + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(data=" + repr(self._data)[:20].replace("\n", "") + "...)" + + +class Image(MediaElement[np.ndarray]): + _type = MediaType.IMAGE + + _DEFAULT_EXT = ".png" -class Image(MediaElement): def __init__( self, - data: Union[np.ndarray, Callable[[str], np.ndarray], None] = None, - *, - path: Optional[str] = None, - ext: Optional[str] = None, size: Optional[Tuple[int, int]] = None, + ext: Optional[str] = None, + *args, + **kwargs, ) -> None: - """ - Creates an image. - - Any combination of the `data`, `path` and `size` is possible, - but at least one of these arguments must be provided. - The `ext` parameter cannot be used as a single argument for - construction. + assert self.__class__ != Image, ( + f"Directly initalizing {self.__class__.__name__} is not supported. " + f"Please use one of fractory functions ({self.__class__.__name__}.from_file(), " + f"{self.__class__.__name__}.from_numpy(), {self.__class__.__name__}.from_bytes())." + ) + super().__init__(*args, **kwargs) + self._dtype = np.uint8 - Args: - data: Image pixels or a function to retrieve them. The expected - image shape is (H, W [, C]). If a function is provided, - it must accept image path as the first argument. - path: Image path - ext: Image extension. Cannot be used together with `path`. It can - be used for saving with a custom extension - in that case, - the image need to have the `data` and `ext` fields defined. - size: A pair (H, W), which represents image size. - """ + if ext is not None: + if not ext.startswith("."): + ext = "." + ext + ext = ext.lower() + self._ext = ext if size is not None: assert ( @@ -73,50 +240,32 @@ def __init__( size = tuple(map(int, size)) self._size = size # (H, W) - if path is None: - path = "" - elif path: - path = path.replace("\\", "/") - self._path = path - - if ext: - assert not path, "Can't specify both 'path' and 'ext' for image" - - if not ext.startswith("."): - ext = "." + ext - ext = ext.lower() - else: - ext = None - self._ext = ext - - if not isinstance(data, np.ndarray): - assert path or callable(data) or size, "Image can not be empty" - assert data is None or callable(data), f"Image data has unexpected type '{type(data)}'" - if data or path and osp.isfile(path): - data = lazy_image(path, loader=data) - self._data = data - - @property - def data(self) -> np.ndarray: - """Image data in BGR HWC [0; 255] (float) format""" - - if callable(self._data): - data = self._data() - else: - data = self._data + @classmethod + def from_file(cls, path: str, *args, **kwargs): + return ImageFromFile(path, *args, **kwargs) - if self._size is None and data is not None: - self._size = tuple(map(int, data.shape[:2])) - return data + @classmethod + def from_numpy( + cls, + data: Union[np.ndarray, Callable[[], np.ndarray]], + *args, + **kwargs, + ): + return ImageFromNumpy(data, *args, **kwargs) - @property - def has_data(self) -> bool: - return self._data is not None + @classmethod + def from_bytes( + cls, + data: Union[bytes, Callable[[], bytes]], + *args, + **kwargs, + ): + return ImageFromBytes(data, *args, **kwargs) @property def has_size(self) -> bool: """Indicates that size info is cached and won't require image loading""" - return self._size is not None or isinstance(self._data, np.ndarray) + return self._size is not None @property def size(self) -> Optional[Tuple[int, int]]: @@ -132,38 +281,141 @@ def size(self) -> Optional[Tuple[int, int]]: return self._size @property - def ext(self) -> str: - """Media file extension""" - if self._ext is not None: - return self._ext + def ext(self) -> Optional[str]: + """Media file extension (with the leading dot)""" + return self._ext + + def _get_ext_to_save(self, fp: Union[str, io.IOBase], ext: Optional[str] = None): + if isinstance(fp, str): + assert ext is None, "'ext' must be empty if string is given." + ext = osp.splitext(osp.basename(fp))[1].lower() else: - return osp.splitext(osp.basename(self.path))[1] + ext = ext if ext else self._DEFAULT_EXT + return ext def __eq__(self, other): + # Do not compare `_type` + # sicne Image is subclass of RoIImage and MosaicImage if not isinstance(other, __class__): return False - return ( - (np.array_equal(self.size, other.size)) - and (self.has_data == other.has_data) - and (self.has_data and np.array_equal(self.data, other.data) or not self.has_data) - ) + return (np.array_equal(self.size, other.size)) and (np.array_equal(self.data, other.data)) - def save(self, path): - cur_path = osp.abspath(self.path) - path = osp.abspath(path) + def set_crypter(self, crypter: Crypter): + super().set_crypter(crypter) - cur_ext = self.ext.lower() - new_ext = osp.splitext(osp.basename(path))[1].lower() - os.makedirs(osp.dirname(path), exist_ok=True) - if cur_ext == new_ext and osp.isfile(cur_path): - if cur_path != path: - shutil.copyfile(cur_path, path) +class ImageFromFile(FromFileMixin, Image): + def __init__( + self, + path: str, + *args, + **kwargs, + ) -> None: + super().__init__(path, *args, **kwargs) + self.__data = lazy_image(self.path, crypter=self._crypter) + + # extension from file name and real extension can be differ + self._ext = self._ext if self._ext else osp.splitext(osp.basename(path))[1] + + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" + + if not self.has_data: + return None + + if self.__data._dtype != self._dtype: + self.__data._loader = partial(load_image, dtype=self._dtype) + data = self.__data() + + if self._size is None and data is not None: + if not 2 <= data.ndim <= 3: + raise MediaShapeError("An image should have 2 (gray) or 3 (bgra) dims.") + self._size = tuple(map(int, data.shape[:2])) + return data + + def save( + self, + fp: Union[str, io.IOBase], + ext: Optional[str] = None, + crypter: Crypter = NULL_CRYPTER, + ): + cur_path = osp.abspath(self.path) if self.path else None + cur_ext = self.ext + new_ext = self._get_ext_to_save(fp, ext) + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + + if cur_path is not None and osp.isfile(cur_path): + if cur_ext == new_ext: + copyto_image(src=cur_path, dst=fp, src_crypter=self._crypter, dst_crypter=crypter) + else: + save_image(fp, self.data, ext=new_ext, crypter=crypter) else: - save_image(path, self.data) + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), cur_path) + def set_crypter(self, crypter: Crypter): + super().set_crypter(crypter) + if isinstance(self.__data, lazy_image): + self.__data._crypter = crypter -class ByteImage(Image): + def get_data_as_dtype(self, dtype: Optional[np.dtype] = np.uint8) -> Optional[np.ndarray]: + """Get image data with a specific data type""" + self._dtype = dtype + return self.data + + +class ImageFromData(FromDataMixin, Image): + def save( + self, + fp: Union[str, io.IOBase], + ext: Optional[str] = None, + crypter: Crypter = NULL_CRYPTER, + ): + data = self.data + if data is None: + raise ValueError(f"{self.__class__.__name__} is empty.") + new_ext = self._get_ext_to_save(fp, ext) + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + save_image(fp, data, ext=new_ext, crypter=crypter) + + +class ImageFromNumpy(ImageFromData): + def __init__( + self, + data: Union[Callable[[], bytes], bytes], + *args, + **kwargs, + ): + super().__init__(data=data, *args, **kwargs) + + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" + + data = super().data + + if isinstance(data, np.ndarray) and data.dtype != self._dtype: + data = np.clip(data, 0.0, 255.0).astype(self._dtype) + if self._size is None and data is not None: + if not 2 <= data.ndim <= 3: + raise MediaShapeError("An image should have 2 (gray) or 3 (bgra) dims.") + self._size = tuple(map(int, data.shape[:2])) + return data + + @property + def has_size(self) -> bool: + """Indicates that size info is cached and won't require image loading""" + return self._size is not None or isinstance(self._data, np.ndarray) + + def get_data_as_dtype(self, dtype: Optional[np.dtype] = np.uint8) -> Optional[np.ndarray]: + """Get image data with a specific data type""" + self._dtype = dtype + return self.data + + +class ImageFromBytes(ImageFromData): _FORMAT_MAGICS = ( (b"\x89PNG\r\n\x1a\n", ".png"), (b"\xff\xd8\xff", ".jpg"), @@ -172,32 +424,14 @@ class ByteImage(Image): def __init__( self, - data: Union[bytes, Callable[[str], bytes], None] = None, - *, - path: Optional[str] = None, - ext: Optional[str] = None, - size: Optional[Tuple[int, int]] = None, + data: Union[Callable[[], bytes], bytes], + *args, + **kwargs, ): - if not isinstance(data, bytes): - assert path or callable(data), "Image can not be empty" - assert data is None or callable(data) - if path and osp.isfile(path) or data: - data = lazy_image(path, loader=data) - - self._bytes_data = data + super().__init__(data=data, *args, **kwargs) - if ext is None and path is None and isinstance(data, bytes): - ext = self._guess_ext(data) - - super().__init__( - path=path, ext=ext, size=size, data=lambda _: decode_image(self.get_bytes()) - ) - if data is None: - # We don't expect decoder to produce images from nothing, - # otherwise using this class makes no sense. We undefine - # data to avoid using default image loader for loading binaries - # from the path, when no data is provided. - self._data = None + if self._ext is None and isinstance(data, bytes): + self._ext = self._guess_ext(data) @classmethod def _guess_ext(cls, data: bytes) -> Optional[str]: @@ -206,35 +440,46 @@ def _guess_ext(cls, data: bytes) -> Optional[str]: None, ) - def get_bytes(self): - if callable(self._bytes_data): - return self._bytes_data() - return self._bytes_data + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" - def save(self, path): - cur_path = osp.abspath(self.path) - path = osp.abspath(path) + data = super().data - cur_ext = self.ext.lower() - new_ext = osp.splitext(osp.basename(path))[1].lower() + if isinstance(data, bytes): + data = decode_image(data, dtype=self._dtype) + if self._size is None and data is not None: + if not 2 <= data.ndim <= 3: + raise MediaShapeError("An image should have 2 (gray) or 3 (bgra) dims.") + self._size = tuple(map(int, data.shape[:2])) + return data - os.makedirs(osp.dirname(path), exist_ok=True) - if cur_ext == new_ext and osp.isfile(cur_path): - if cur_path != path: - shutil.copyfile(cur_path, path) - elif cur_ext == new_ext: - with open(path, "wb") as f: - f.write(self.get_bytes()) - else: - save_image(path, self.data) + def get_data_as_dtype(self, dtype: Optional[np.dtype] = np.uint8) -> Optional[np.ndarray]: + """Get image data with a specific data type""" + + if dtype != np.uint8: + raise ValueError("ImageFromBytes only support `dtype=np.uint8`.") + self._dtype = dtype + return self.data -class VideoFrame(Image): +class VideoFrame(ImageFromNumpy): + _type = MediaType.VIDEO_FRAME + + _DEFAULT_EXT = None + def __init__(self, video: Video, index: int): self._video = video self._index = index - super().__init__(lambda _: self._video.get_frame_data(self._index)) + super().__init__(data=lambda: self._video.get_frame_data(self._index)) + + def as_dict(self) -> Dict[str, Any]: + attrs = super().as_dict() + return { + "video": attrs["video"], + "index": attrs["index"], + } @property def size(self) -> Tuple[int, int]: @@ -248,6 +493,30 @@ def index(self) -> int: def video(self) -> Video: return self._video + @property + def path(self) -> str: + return self._video.path + + def from_self(self, **kwargs): + attrs = deepcopy(self.as_dict()) + if "path" in kwargs: + attrs.update({"video": self.video.from_self(**kwargs)}) + kwargs.pop("path") + attrs.update(kwargs) + return self.__class__(**attrs) + + def __getstate__(self): + # Return only the picklable parts of the state. + state = self.__dict__.copy() + del state["_data"] + return state + + def __setstate__(self, state): + # Restore the objects' state. + self.__dict__.update(state) + # Reinitialize unpichlable attributes + self._data = lambda: self._video.get_frame_data(self._index) + class _VideoFrameIterator(Iterator[VideoFrame]): """ @@ -287,6 +556,11 @@ def _decode(self, cap) -> Iterator[VideoFrame]: if self._video._frame_count is None: self._video._frame_count = self._pos + 1 + if self._video._end_frame and self._video._end_frame >= self._video._frame_count: + raise ValueError( + f"The end_frame value({self._video._end_frame}) of the video " + f"must be less than the frame count({self._video._frame_count})." + ) def _make_frame(self, index) -> VideoFrame: return VideoFrame(self._video, index=index) @@ -328,21 +602,33 @@ def _navigate_to(self, idx: int) -> VideoFrame: class Video(MediaElement, Iterable[VideoFrame]): + _type = MediaType.VIDEO + """ Provides random access to the video frames. """ def __init__( - self, path: str, *, step: int = 1, start_frame: int = 0, end_frame: Optional[int] = None + self, + path: str, + step: int = 1, + start_frame: int = 0, + end_frame: Optional[int] = None, + *args, + **kwargs, ) -> None: - super().__init__(path) + super().__init__(*args, **kwargs) + self._path = path - if end_frame: - assert start_frame < end_frame + assert 0 <= start_frame + if end_frame is not None: + assert start_frame <= end_frame + # we can't know the video length here, + # so we cannot validate if the end_frame is valid. assert 0 < step self._step = step self._start_frame = start_frame - self._end_frame = end_frame or None + self._end_frame = end_frame self._reader = None self._iterator: Optional[_VideoFrameIterator] = None @@ -359,10 +645,6 @@ def __init__( self._frame_count = None self._length = None - from .media_manager import MediaManager - - MediaManager.get_instance().push(weakref.ref(self), self) - def close(self): self._iterator = None @@ -391,7 +673,7 @@ def __iter__(self) -> Iterator[VideoFrame]: # Decoding is not necessary to get frame pointers # However, it can be inacurrate end_frame = self._get_end_frame() - for index in range(self._start_frame, end_frame, self._step): + for index in range(self._start_frame, end_frame + 1, self._step): yield VideoFrame(video=self, index=index) else: # Need to decode to iterate over frames @@ -400,7 +682,8 @@ def __iter__(self) -> Iterator[VideoFrame]: @property def length(self) -> Optional[int]: """ - Returns frame count, if video provides such information. + Returns frame count of the closed interval [start_frame, end_frame], + if video provides such information. Note that not all videos provide length / duration metainfo, so the result may be undefined. @@ -416,12 +699,15 @@ def length(self) -> Optional[int]: if self._length is None: end_frame = self._get_end_frame() - length = None if end_frame is not None: - length = (end_frame - self._start_frame) // self._step - assert 0 < length - - self._length = length + length = (end_frame + 1 - self._start_frame) // self._step + if 0 >= length: + raise ValueError( + "There is no valid frame for the closed interval" + f"[start_frame({self._start_frame})," + f" end_frame({end_frame})] with step({self._step})." + ) + self._length = length return self._length @@ -447,18 +733,23 @@ def _get_frame_size(self) -> Tuple[int, int]: return frame_size def _get_end_frame(self): + # Note that end_frame could less than the last frame of the video if self._end_frame is not None and self._frame_count is not None: end_frame = min(self._end_frame, self._frame_count) + elif self._end_frame is not None: + end_frame = self._end_frame + elif self._frame_count is not None: + end_frame = self._frame_count - 1 else: - end_frame = self._end_frame or self._frame_count + end_frame = None return end_frame def _includes_frame(self, i): - end_frame = self._get_end_frame() if self._start_frame <= i: if (i - self._start_frame) % self._step == 0: - if end_frame is None or i < end_frame: + end_frame = self._get_end_frame() + if end_frame is None or i <= end_frame: return True return False @@ -480,36 +771,207 @@ def _reset_reader(self): assert self._reader.isOpened() def __eq__(self, other: object) -> bool: + def _get_frame(obj: Video, idx: int): + try: + return obj[idx] + except IndexError: + return None + if not isinstance(other, __class__): return False + if self._start_frame != other._start_frame or self._step != other._step: + return False - return ( - self.path == other.path - and self._start_frame == other._start_frame - and self._step == other._step - and self._end_frame == other._end_frame - ) + # The video path can vary if a dataset is copied. + # So, we need to check if the video data is the same instead of checking paths. + if self._end_frame is not None and self._end_frame == other._end_frame: + for idx in range(self._start_frame, self._end_frame + 1, self._step): + if self[idx] != other[idx]: + return False + return True + + end_frame = self._end_frame or other._end_frame + if end_frame is None: + last_frame = None + for idx, frame in enumerate(self): + if frame != _get_frame(other, frame.index): + return False + last_frame = frame + # check if the actual last frames are same + try: + other[last_frame.index + self._step if last_frame else self._start_frame] + except IndexError: + return True + return False + + # _end_frame values, only one of the two is valid + for idx in range(self._start_frame, end_frame + 1, self._step): + frame = _get_frame(self, idx) + if frame is None: + return False + if frame != _get_frame(other, idx): + return False + # check if the actual last frames are same + idx_next = end_frame + self._step + return None is (_get_frame(self, idx_next) or _get_frame(other, idx_next)) def __hash__(self): # Required for caching return hash((self._path, self._step, self._start_frame, self._end_frame)) + def save( + self, + fp: Union[str, io.IOBase], + crypter: Crypter = NULL_CRYPTER, + ): + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + if isinstance(fp, str): + if fp != self.path: + shutil.copyfile(self.path, fp) + elif isinstance(fp, io.IOBase): + with open(self.path, "rb") as f_video: + fp.write(f_video.read()) -class PointCloud(MediaElement): - def __init__(self, path: str, extra_images: Optional[List[Image]] = None): - self._path = path + @property + def path(self) -> str: + """Path to the media file""" + return self._path - self.extra_images: List[Image] = extra_images or [] + @property + def ext(self) -> str: + """Media file extension (with the leading dot)""" + return osp.splitext(osp.basename(self.path))[1] + + +class PointCloud(MediaElement[bytes]): + _type = MediaType.POINT_CLOUD + + def __init__( + self, + extra_images: Optional[Union[List[Image], Callable[[], List[Image]]]] = None, + *args, + **kwargs, + ): + assert self.__class__ != PointCloud, ( + f"Directly initalizing {self.__class__.__name__} is not supported. " + f"Please use one of fractory function ({self.__class__.__name__}.from_file(), " + f"{self.__class__.__name__}.from_bytes())." + ) + super().__init__(*args, **kwargs) + self._extra_images = extra_images or [] + + @classmethod + def from_file(cls, path: str, *args, **kwargs): + return PointCloudFromFile(path, *args, **kwargs) + + @classmethod + def from_bytes(cls, data: Union[bytes, Callable[[], bytes]], *args, **kwargs): + return PointCloudFromBytes(data, *args, **kwargs) + + @property + def extra_images(self) -> List[Image]: + if callable(self._extra_images): + extra_images = self._extra_images() + assert isinstance(extra_images, list) and all( + [isinstance(image, Image) for image in extra_images] + ) + return extra_images + return self._extra_images + + def _save_extra_images( + self, + fn: Callable[[int, Image], Dict[str, Any]], + crypter: Optional[Crypter] = None, + ): + crypter = crypter if crypter else self._crypter + for i, img in enumerate(self.extra_images): + if img.has_data: + kwargs: Dict[str, Any] = {"crypter": crypter} + kwargs.update(fn(i, img)) + img.save(**kwargs) def __eq__(self, other: object) -> bool: return ( - isinstance(other, __class__) - and self.path == other.path - and set(self.extra_images) == set(other.extra_images) + super().__eq__(other) + and (self.data == other.data) + and self.extra_images == other.extra_images ) +class PointCloudFromFile(FromFileMixin, PointCloud): + @property + def data(self) -> Optional[bytes]: + if self.has_data: + with open(self.path, "rb") as f: + bytes_data = f.read() + return bytes_data + return None + + def save( + self, + fp: Union[str, io.IOBase], + extra_images_fn: Optional[Callable[[int, Image], Dict[str, Any]]] = None, + crypter: Crypter = NULL_CRYPTER, + ): + if not crypter.is_null_crypter: + raise NotImplementedError( + f"{self.__class__.__name__} does not implement save() with non NullCrypter." + ) + + cur_path = osp.abspath(self.path) if self.path else None + + if cur_path is not None and osp.isfile(cur_path): + with open(cur_path, "rb") as reader: + _bytes = reader.read() + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + with open(fp, "wb") as f: + f.write(_bytes) + else: + fp.write(_bytes) + else: + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), cur_path) + + if extra_images_fn is not None: + self._save_extra_images(extra_images_fn, crypter) + + +class PointCloudFromData(FromDataMixin, PointCloud): + def save( + self, + fp: Union[str, io.IOBase], + extra_images_fn: Optional[Callable[[int, Image], Dict[str, Any]]] = None, + crypter: Crypter = NULL_CRYPTER, + ): + if not crypter.is_null_crypter: + raise NotImplementedError( + f"{self.__class__.__name__} does not implement save() with non NullCrypter." + ) + + _bytes = self.data + if _bytes is None: + raise ValueError(f"{self.__class__.__name__} is empty.") + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + with open(fp, "wb") as f: + f.write(_bytes) + else: + fp.write(_bytes) + + if extra_images_fn is not None: + self._save_extra_images(extra_images_fn, crypter) + + +class PointCloudFromBytes(PointCloudFromData): + @property + def data(self) -> Optional[bytes]: + return super().data + + class MultiframeImage(MediaElement): + _type = MediaType.MULTIFRAME_IMAGE + def __init__( self, images: Optional[Iterable[Union[str, Image, np.ndarray, Callable[[str], np.ndarray]]]], @@ -526,9 +988,9 @@ def __init__( assert isinstance(image, (str, Image, np.ndarray)) or callable(image) if isinstance(image, str): - image = Image(path=image) + image = Image.from_file(path=image) elif isinstance(image, np.ndarray) or callable(image): - image = Image(data=image) + image = Image.from_numpy(data=image) self._images[i] = image @@ -537,3 +999,513 @@ def __init__( @property def data(self) -> List[Image]: return self._images + + @property + def path(self) -> str: + """Path to the media file""" + return self._path + + @property + def ext(self) -> str: + """Media file extension (with the leading dot)""" + return osp.splitext(osp.basename(self.path))[1] + + +class RoIImage(Image): + _type = MediaType.ROI_IMAGE + + def __init__( + self, + roi: BboxIntCoords, + *args, + **kwargs, + ): + assert self.__class__ != RoIImage, ( + f"Directly initalizing {self.__class__.__name__} is not supported. " + f"Please use a fractory function '{self.__class__.__name__}.from_image()'. " + ) + + assert len(roi) == 4 and all(isinstance(v, int) for v in roi) + self._roi = roi + _, _, w, h = self._roi + super().__init__(size=(h, w), *args, **kwargs) + + def as_dict(self) -> Dict[str, Any]: + attrs = super().as_dict() + attrs.pop("size", None) + return attrs + + @classmethod + def from_file(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image'.") + + @classmethod + def from_image(cls, data: Image, roi: BboxIntCoords, *args, **kwargs): + if not isinstance(data, Image): + raise TypeError(f"type(image)={type(data)} should be Image.") + + if isinstance(data, ImageFromFile): + return RoIImageFromFile(path=data.path, roi=roi, ext=data._ext, *args, **kwargs) + if isinstance(data, ImageFromNumpy): + return RoIImageFromNumpy(data=data._data, roi=roi, ext=data._ext, *args, **kwargs) + if isinstance(data, ImageFromBytes): + return RoIImageFromBytes(data=data._data, roi=roi, ext=data._ext, *args, **kwargs) + raise NotImplementedError + + @classmethod + def from_numpy(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image'.") + + @classmethod + def from_bytes(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image'.") + + @property + def roi(self) -> BboxIntCoords: + return self._roi + + def _get_roi_data(self, data: np.ndarray) -> np.ndarray: + x, y, w, h = self._roi + return data[y : y + h, x : x + w] + + def save( + self, + fp: Union[str, io.IOBase], + ext: Optional[str] = None, + crypter: Crypter = NULL_CRYPTER, + ): + if not crypter.is_null_crypter: + raise NotImplementedError( + f"{self.__class__.__name__} does not implement save() with non NullCrypter." + ) + data = self.data + if data is None: + raise ValueError(f"{self.__class__.__name__} is empty.") + new_ext = self._get_ext_to_save(fp, ext) + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + save_image(fp, data, ext=new_ext, crypter=crypter) + + +class RoIImageFromFile(FromFileMixin, RoIImage): + def __init__( + self, + path: str, + roi: BboxIntCoords, + *args, + **kwargs, + ) -> None: + super().__init__(path, roi, *args, **kwargs) + self.__data = lazy_image(self.path, crypter=self._crypter) + + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" + if not self.has_data: + return None + data = self.__data() + return self._get_roi_data(data) + + +class RoIImageFromData(FromDataMixin, RoIImage): + pass + + +class RoIImageFromBytes(RoIImageFromData): + def __init__( + self, + data: Union[bytes, Callable[[], bytes]], + roi: BboxIntCoords, + *args, + **kwargs, + ) -> None: + super().__init__(data, roi, *args, **kwargs) + + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" + data = super().data + if data is None: + return None + if isinstance(data, bytes): + data = decode_image(data) + return self._get_roi_data(data) + + +class RoIImageFromNumpy(RoIImageFromData): + def __init__( + self, + data: Union[np.ndarray, Callable[[], np.ndarray]], + roi: BboxIntCoords, + *args, + **kwargs, + ) -> None: + super().__init__(data, roi, *args, **kwargs) + + @property + def data(self) -> Optional[np.ndarray]: + """Image data in BGRA HWC [0; 255] (uint8) format""" + data = super().data + if data is None: + return None + return self._get_roi_data(data) + + +ImageWithRoI = Tuple[Image, BboxIntCoords] + + +class MosaicImage(Image): + _type = MediaType.MOSAIC_IMAGE + + def __init__( + self, + *args, + **kwargs, + ): + assert self.__class__ != MosaicImage, ( + f"Directly initalizing {self.__class__.__name__} is not supported. " + f"Please use a fractory function '{self.__class__.__name__}.from_image_roi_pairs()'." + ) + super().__init__(*args, **kwargs) + + @classmethod + def from_file(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image_roi_pairs'.") + + @classmethod + def from_image_roi_pairs(cls, data: List[ImageWithRoI], size: Tuple[int, int], *args, **kwargs): + return MosaicImageFromImageRoIPairs(data, size) + + @classmethod + def from_numpy(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image_roi_pairs'.") + + @classmethod + def from_bytes(cls, *args, **kwargs): + raise DatumaroError(f"Please use a factory function '{cls.__name__}.from_image_roi_pairs'.") + + +class MosaicImageFromData(FromDataMixin, MosaicImage): + def save( + self, + fp: Union[str, io.IOBase], + ext: Optional[str] = None, + crypter: Crypter = NULL_CRYPTER, + ): + if not crypter.is_null_crypter: + raise NotImplementedError( + f"{self.__class__.__name__} does not implement save() with non NullCrypter." + ) + data = self.data + if data is None: + raise ValueError(f"{self.__class__.__name__} is empty.") + new_ext = self._get_ext_to_save(fp, ext) + if isinstance(fp, str): + os.makedirs(osp.dirname(fp), exist_ok=True) + save_image(fp, data, ext=new_ext, crypter=crypter) + + +class MosaicImageFromImageRoIPairs(MosaicImageFromData): + def __init__(self, data: List[ImageWithRoI], size: Tuple[int, int]) -> None: + def _get_mosaic_img() -> np.ndarray: + h, w = self.size + mosaic_img = np.zeros(shape=(h, w, 3), dtype=np.uint8) + for img, roi in data: + assert isinstance(img, Image), "MosaicImage can only take a list of Images." + x, y, w, h = roi + mosaic_img[y : y + h, x : x + w] = img.data + return mosaic_img + + super().__init__(data=_get_mosaic_img, size=size) + self._data_in = data + + def as_dict(self) -> Dict[str, Any]: + attrs = super().as_dict() + return { + "data": attrs["data_in"], + "size": attrs["size"], + } + + +TableDtype = TypeVar("TableDtype", str, int, float) + + +class Table: + def __init__( + self, + ) -> None: + """ + Table data with multiple rows and columns. + This provides random access to the table row. + + Initialization must be done in the child class. + """ + assert self.__class__ != Table, ( + f"Directly initalizing {self.__class__.__name__} is not supported. " + f"Please use one of fractory functions ({self.__class__.__name__}.from_csv(), " + f"{self.__class__.__name__}.from_dataframe(), " + f"or ({self.__class__.__name__}.from_list())." + ) + self._shape: Tuple[int, int] = (0, 0) + + @classmethod + def from_csv(cls, path: str, *args, **kwargs) -> Type[Table]: + """ + Returns Table instance creating from a csv file. + + Args: + path (str) : Path to csv file. + """ + return TableFromCSV(path, *args, **kwargs) + + @classmethod + def from_dataframe( + cls, + data: Union[pd.DataFrame, Callable[[], pd.DataFrame]], + *args, + **kwargs, + ) -> Type[Table]: + """ + Returns Table instance creating from a pandas DataFrame. + + Args: + data (DataFrame) : Data in pandas DataFrame format. + """ + return TableFromDataFrame(data, *args, **kwargs) + + @classmethod + def from_list( + cls, + data: List[Dict[str, TableDtype]], + *args, + **kwargs, + ) -> Type[Table]: + """ + Returns Table instance creating from a list of dicts. + + Args: + data (list(dict(str,str|int|float))) : A list of table row data. + """ + return TableFromListOfDict(data, *args, **kwargs) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return False + return self.data.equals(other) + + def __getitem__(self, idx: int) -> TableRow: + """ + Random access to a specific row by index. + """ + if idx >= self.shape[0]: + raise IndexError(f"Table doesn't contain row #{idx}.") + return TableRow(table=self, index=idx) + + def __iter__(self) -> Iterator[TableRow]: + """ + Iterates over rows. + """ + for index in range(self.shape[0]): + yield TableRow(table=self, index=index) + + @property + def shape(self) -> Tuple[int, int]: + """Returns table size as (#rows, #cols)""" + return self._shape + + @property + def columns(self) -> List[str]: + """Returns column names""" + return self.data.columns.to_list() + + def dtype(self, column: str) -> Optional[Type[TableDtype]]: + """Returns native python type for a given column""" + numpy_type = self.data.dtypes[column] + if self.data[column].nunique() / self.shape[0] < 0.1: # TODO + # Convert to CategoricalDtype for efficient storage and categorical analysis + return pd.api.types.CategoricalDtype() + if isinstance(numpy_type, np.dtypes.ObjectDType): + return str + else: + return type(np.zeros(1, numpy_type).tolist()[0]) + + def features(self, column: str, unique: Optional[bool] = False) -> List[TableDtype]: + """Get features for a given column name.""" + if unique: + return list(self.data[column].unique()) + else: + return self.data[column].to_list() + + def save( + self, + path: str, + ): + """ + Save table instance to a '.csv' file. + + Args: + path (str) : Path to the output csv file. + """ + data: pd.DataFrame = self.data + os.makedirs(osp.dirname(path), exist_ok=True) + data.to_csv(path, index=False) + + +class TableFromCSV(FromFileMixin, Table): + def __init__( + self, + path: str, + dtype: Optional[Dict] = None, + sep: Optional[str] = None, + encoding: Optional[str] = None, + *args, + **kwargs, + ) -> None: + """ + Read a '.csv' file and compose a Table instance. + + Args: + path (str) : Path to csv file. + dtype (optional, dict(str,str)) : + Dictionary of column name -> type str ('str', 'int', or 'float'). + sep (optional, str) : Delimiter to use. + encoding (optional, str) : Encoding to use for UTF when reading/writing (ex. 'utf-8'). + """ + super().__init__(path, *args, **kwargs) + + # assumes that the 1st row is a header. + data: pd.DataFrame = pd.read_csv( + path, dtype=dtype, sep=sep, engine="python", encoding=encoding, index_col=False + ) + if data is None: + raise ValueError(f"Can't read csv File from {path}") + if data.shape[1] == 0: + raise MediaShapeError("A table should have 1 or more columns.") + + self.__data = data + self._shape = data.shape + + @property + def data(self) -> Optional[pd.DataFrame]: + """Table data in pandas DataFrame format""" + return self.__data + + def select(self, columns: List[str]): + self.__data = self.__data[columns] + self._shape = self.__data.shape + + +class TableFromDataFrame(FromDataMixin, Table): + def __init__( + self, + data: Union[Callable[[], pd.DataFrame], pd.DataFrame], + *args, + **kwargs, + ): + """ + Read a pandas DataFrame and compose a Table instance. + + Args: + data (DataFrame) : Data in pandas DataFrame format. + """ + super().__init__(data=data, *args, **kwargs) + + if data is None: + raise ValueError("'data' can't be None") + if data.shape[1] == 0: + raise MediaShapeError("A table should have 1 or more columns.") + for col in data.columns: + if not isinstance(col, str): + raise TypeError("A table should have column names as a list of str values") + + self._shape = data.shape + + @property + def data(self) -> Optional[pd.DataFrame]: + """Table data in pandas DataFrame format""" + return super().data + + +class TableFromListOfDict(TableFromDataFrame): + def __init__( + self, + data: List[Dict[str, TableDtype]], + *args, + **kwargs, + ): + """ + Read a list of table row data and compose a Table instance. + The table row data is in dictionary format. + + Args: + data (list(dict(str,str|int|float))) : A list of table row data. + """ + super().__init__(data=pd.DataFrame(data), *args, **kwargs) + + +class TableRow(MediaElement): + _type = MediaType.TABLE_ROW + + def __init__(self, table: Table, index: int): + """ + TableRow media refers to a Table instance and its row index. + + Args: + table (Table) : Table instance. + index (int) : Row index. + """ + if table is None: + raise ValueError("'table' can't be None") + if index < 0 or index >= table.shape[0]: + raise IndexError(f"'index({index})' is out of range.") + self._table = table + self._index = index + + @property + def table(self) -> Table: + """Table instance""" + return self._table + + @property + def index(self) -> int: + """Row index""" + return self._index + + @property + def path(self) -> str: + return self._table.data.path + + @property + def has_data(self) -> bool: + return self.data() is not None + + def data(self, targets: Optional[List[str]] = None) -> Dict: + """ + Row data in dict format. + + Args: + targets (optional, list(str)) : If this is specified, + the values corresponding to target colums will be returned. + Otherwise, whole row data will be returned. + """ + row = self.table.data.iloc[self.index] + if targets: + row = row[targets] + return row.to_dict() + + def __repr__(self): + return f"TableRow(row_idx:{self.index}, data:{self.data()})" + + @classmethod + def from_data(cls, data: Dict, *args, **kwargs): + return TableRowFromData(data, *args, **kwargs) + + +class TableRowFromData(FromDataMixin, TableRow): + def __init__(self, data: Dict, *args, **kwargs): + super().__init__(data=data, *args, **kwargs) + + @property + def data(self) -> Dict: + data = super().data + return data diff --git a/src/datumaro/plugins/data_formats/ade20k2017.py b/src/datumaro/plugins/data_formats/ade20k2017.py index 7deb9bbf96..c8dc4e5beb 100644 --- a/src/datumaro/plugins/data_formats/ade20k2017.py +++ b/src/datumaro/plugins/data_formats/ade20k2017.py @@ -119,7 +119,7 @@ def _load_items(self, subset): DatasetItem( item_id, subset=subset, - media=Image(path=image_path), + media=Image.from_file(path=image_path), annotations=item_annotations, ) ) diff --git a/src/datumaro/plugins/data_formats/ade20k2020.py b/src/datumaro/plugins/data_formats/ade20k2020.py index e960dd68fe..c1eb74ff60 100644 --- a/src/datumaro/plugins/data_formats/ade20k2020.py +++ b/src/datumaro/plugins/data_formats/ade20k2020.py @@ -158,7 +158,7 @@ def _load_items(self, subset): DatasetItem( item_id, subset=subset, - media=Image(path=image_path), + media=Image.from_file(path=image_path), annotations=item_annotations, ) ) diff --git a/src/datumaro/plugins/data_formats/camvid.py b/src/datumaro/plugins/data_formats/camvid.py index d3fa898022..c0b2219a16 100644 --- a/src/datumaro/plugins/data_formats/camvid.py +++ b/src/datumaro/plugins/data_formats/camvid.py @@ -225,7 +225,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=image_path), + media=Image.from_file(path=image_path), annotations=item_annotations, ) diff --git a/src/datumaro/plugins/data_formats/celeba/align_celeba.py b/src/datumaro/plugins/data_formats/celeba/align_celeba.py index 64f02f1f14..9dbce6948a 100644 --- a/src/datumaro/plugins/data_formats/celeba/align_celeba.py +++ b/src/datumaro/plugins/data_formats/celeba/align_celeba.py @@ -80,7 +80,7 @@ def _load_items(self, root_dir): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image, annotations=anno) @@ -145,7 +145,7 @@ def _load_items(self, root_dir): if item_id not in items: image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image) @@ -169,7 +169,7 @@ def _load_items(self, root_dir): if item_id not in items: image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image) items[item_id].subset = subset diff --git a/src/datumaro/plugins/data_formats/celeba/celeba.py b/src/datumaro/plugins/data_formats/celeba/celeba.py index 311fca83e5..c59c1d7b8f 100644 --- a/src/datumaro/plugins/data_formats/celeba/celeba.py +++ b/src/datumaro/plugins/data_formats/celeba/celeba.py @@ -77,7 +77,7 @@ def _load_items(self, root_dir): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image, annotations=anno) @@ -177,7 +177,7 @@ def _load_items(self, root_dir): if item_id not in items: image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image) @@ -201,7 +201,7 @@ def _load_items(self, root_dir): if item_id not in items: image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image) diff --git a/src/datumaro/plugins/data_formats/cifar.py b/src/datumaro/plugins/data_formats/cifar.py index 3b4fc54d2e..6ffdc45dc0 100644 --- a/src/datumaro/plugins/data_formats/cifar.py +++ b/src/datumaro/plugins/data_formats/cifar.py @@ -149,7 +149,7 @@ def _load_items(self, path): image = np.transpose(image, (1, 2, 0)) if image is not None: - image = Image(data=image) + image = Image.from_numpy(data=image) items[item_id] = DatasetItem( id=item_id, subset=self._subset, media=image, annotations=annotations diff --git a/src/datumaro/plugins/data_formats/cityscapes.py b/src/datumaro/plugins/data_formats/cityscapes.py index 19d5c55df5..5a0bdf779b 100644 --- a/src/datumaro/plugins/data_formats/cityscapes.py +++ b/src/datumaro/plugins/data_formats/cityscapes.py @@ -326,14 +326,16 @@ def _load_items(self): image = image_path_by_id.pop(item_id, None) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem( id=item_id, subset=self._subset, media=image, annotations=anns ) for item_id, path in image_path_by_id.items(): - items[item_id] = DatasetItem(id=item_id, subset=self._subset, media=Image(path=path)) + items[item_id] = DatasetItem( + id=item_id, subset=self._subset, media=Image.from_file(path=path) + ) self._categories = self._load_categories( self._path, use_train_label_map=mask_suffix is CityscapesPath.LABEL_TRAIN_IDS_SUFFIX diff --git a/src/datumaro/plugins/data_formats/coco/base.py b/src/datumaro/plugins/data_formats/coco/base.py index 4ca5c21f3c..ff69b453dd 100644 --- a/src/datumaro/plugins/data_formats/coco/base.py +++ b/src/datumaro/plugins/data_formats/coco/base.py @@ -197,7 +197,9 @@ def _load_items(self, json_data): items[img_id] = DatasetItem( id=osp.splitext(file_name)[0], subset=self._subset, - media=Image(path=osp.join(self._images_dir, file_name), size=image_size), + media=Image.from_file( + path=osp.join(self._images_dir, file_name), size=image_size + ), annotations=[], attributes={"id": img_id}, ) diff --git a/src/datumaro/plugins/data_formats/common_semantic_segmentation.py b/src/datumaro/plugins/data_formats/common_semantic_segmentation.py index f9b5dcefb4..b98086229c 100644 --- a/src/datumaro/plugins/data_formats/common_semantic_segmentation.py +++ b/src/datumaro/plugins/data_formats/common_semantic_segmentation.py @@ -100,7 +100,7 @@ def _load_items(self): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) annotations = [] mask = lazy_mask(mask_path, self._categories[AnnotationType.mask].inverse_colormap) diff --git a/src/datumaro/plugins/data_formats/common_super_resolution.py b/src/datumaro/plugins/data_formats/common_super_resolution.py index 475ad3f2e0..28ce952dd9 100644 --- a/src/datumaro/plugins/data_formats/common_super_resolution.py +++ b/src/datumaro/plugins/data_formats/common_super_resolution.py @@ -46,10 +46,13 @@ def _load_items(self, path): attributes = {} upsampled_image = upsampled_images.get(item_id) if upsampled_image: - attributes["upsampled"] = Image(path=upsampled_image) + attributes["upsampled"] = Image.from_file(path=upsampled_image) items[item_id] = DatasetItem( - id=item_id, subset=self._subset, media=Image(path=lr_image), attributes=attributes + id=item_id, + subset=self._subset, + media=Image.from_file(path=lr_image), + attributes=attributes, ) hr_image_dir = osp.join(path, CommonSuperResolutionPath.HR_IMAGES_DIR) @@ -59,11 +62,11 @@ def _load_items(self, path): attributes = {} upsampled_image = upsampled_images.get(item_id) if upsampled_image: - attributes["upsampled"] = Image(path=upsampled_image) + attributes["upsampled"] = Image.from_file(path=upsampled_image) items[item_id] = DatasetItem(id=item_id, subset=self._subset, attributes=attributes) - items[item_id].annotations = [SuperResolutionAnnotation(Image(path=hr_image))] + items[item_id].annotations = [SuperResolutionAnnotation(Image.from_file(path=hr_image))] return items diff --git a/src/datumaro/plugins/data_formats/cvat/base.py b/src/datumaro/plugins/data_formats/cvat/base.py index 5a69c2def9..15d71ba4fc 100644 --- a/src/datumaro/plugins/data_formats/cvat/base.py +++ b/src/datumaro/plugins/data_formats/cvat/base.py @@ -496,9 +496,9 @@ def _load_items(self, parsed): image = osp.join(self._images_dir, name) image_size = (item_desc.get("height"), item_desc.get("width")) if all(image_size): - image = Image(path=image, size=tuple(map(int, image_size))) + image = Image.from_file(path=image, size=tuple(map(int, image_size))) else: - image = Image(path=image) + image = Image.from_file(path=image) parsed[frame_id] = DatasetItem( id=osp.splitext(name)[0], diff --git a/src/datumaro/plugins/data_formats/datumaro/base.py b/src/datumaro/plugins/data_formats/datumaro/base.py index 2c09ac6866..6c3bdc0741 100644 --- a/src/datumaro/plugins/data_formats/datumaro/base.py +++ b/src/datumaro/plugins/data_formats/datumaro/base.py @@ -108,7 +108,7 @@ def _load_items(self, parsed): if osp.isfile(old_image_path): image_path = old_image_path - media = Image(path=image_path, size=image_info.get("size")) + media = Image.from_file(path=image_path, size=image_info.get("size")) self._media_type = Image pcd_info = item_desc.get("point_cloud") @@ -122,7 +122,7 @@ def _load_items(self, parsed): ri_info = item_desc.get("related_images") if ri_info: related_images = [ - Image( + Image.from_file( size=ri.get("size"), path=osp.join( self._related_images_dir, self._subset, item_id, ri.get("path") @@ -131,7 +131,7 @@ def _load_items(self, parsed): for ri in ri_info ] - media = PointCloud(point_cloud, extra_images=related_images) + media = PointCloud.from_file(point_cloud, extra_images=related_images) self._media_type = PointCloud media_desc = item_desc.get("media") diff --git a/src/datumaro/plugins/data_formats/datumaro/exporter.py b/src/datumaro/plugins/data_formats/datumaro/exporter.py index afa07fe28f..a733cba0b8 100644 --- a/src/datumaro/plugins/data_formats/datumaro/exporter.py +++ b/src/datumaro/plugins/data_formats/datumaro/exporter.py @@ -75,7 +75,7 @@ def add_item(self, item: DatasetItem): if isinstance(item.media, Image): image = item.media_as(Image) - path = image.path + path = getattr(image, "path", "") if self._context._save_media: path = self._context._make_image_filename(item) self._context._save_image( @@ -97,10 +97,9 @@ def add_item(self, item: DatasetItem): item_desc["point_cloud"] = {"path": path} - images = sorted(pcd.extra_images, key=lambda v: v.path) if self._context._save_media: related_images = [] - for i, img in enumerate(images): + for i, img in enumerate(pcd.extra_images): ri_desc = {} # Images can have completely the same names or don't @@ -120,7 +119,7 @@ def add_item(self, item: DatasetItem): ri_desc["size"] = img.size related_images.append(ri_desc) else: - related_images = [{"path": img.path} for img in images] + related_images = [{"path": getattr(img, "path", "")} for img in pcd.extra_images] if related_images: item_desc["related_images"] = related_images diff --git a/src/datumaro/plugins/data_formats/icdar/base.py b/src/datumaro/plugins/data_formats/icdar/base.py index f8087c3ef1..4c69454550 100644 --- a/src/datumaro/plugins/data_formats/icdar/base.py +++ b/src/datumaro/plugins/data_formats/icdar/base.py @@ -74,7 +74,7 @@ def _load_recognition_items(self): image_path = osp.join(osp.dirname(self._path), IcdarPath.IMAGES_DIR, image) if item_id not in items: items[item_id] = DatasetItem( - item_id, subset=self._subset, media=Image(path=image_path) + item_id, subset=self._subset, media=Image.from_file(path=image_path) ) annotations = items[item_id].annotations @@ -105,7 +105,7 @@ def _load_localization_items(self): image = None image_path = images.get(item_id) if image_path: - image = Image(path=image_path) + image = Image.from_file(path=image_path) items[item_id] = DatasetItem(item_id, subset=self._subset, media=image) annotations = items[item_id].annotations @@ -176,7 +176,7 @@ def _load_segmentation_items(self): image = None image_path = images.get(item_id) if image_path: - image = Image(path=image_path) + image = Image.from_file(path=image_path) items[item_id] = DatasetItem(item_id, subset=self._subset, media=image) annotations = items[item_id].annotations diff --git a/src/datumaro/plugins/data_formats/image_dir.py b/src/datumaro/plugins/data_formats/image_dir.py index 0b7210d6f0..ed36206e7d 100644 --- a/src/datumaro/plugins/data_formats/image_dir.py +++ b/src/datumaro/plugins/data_formats/image_dir.py @@ -42,7 +42,9 @@ def __init__(self, url, subset=None): for path in find_images(url, recursive=True): item_id = osp.relpath(osp.splitext(path)[0], url) - self._items.append(DatasetItem(id=item_id, subset=self._subset, media=Image(path=path))) + self._items.append( + DatasetItem(id=item_id, subset=self._subset, media=Image.from_file(path=path)) + ) class ImageDirExporter(Exporter): diff --git a/src/datumaro/plugins/data_formats/image_zip.py b/src/datumaro/plugins/data_formats/image_zip.py index 22e0cb905f..3a7631601c 100644 --- a/src/datumaro/plugins/data_formats/image_zip.py +++ b/src/datumaro/plugins/data_formats/image_zip.py @@ -11,7 +11,7 @@ from datumaro.components.dataset_base import DatasetItem, SubsetBase from datumaro.components.exporter import Exporter from datumaro.components.importer import Importer -from datumaro.components.media import ByteImage +from datumaro.components.media import Image from datumaro.util import parse_str_enum_value from datumaro.util.image import IMAGE_EXTENSIONS, encode_image @@ -30,7 +30,7 @@ class ImageZipPath: class ImageZipBase(SubsetBase): def __init__(self, url, subset=None): - super().__init__(subset=subset, media_type=ByteImage) + super().__init__(subset=subset, media_type=Image) assert url.endswith(".zip"), url @@ -39,7 +39,7 @@ def __init__(self, url, subset=None): item_id, extension = osp.splitext(path.filename) if extension.lower() not in IMAGE_EXTENSIONS: continue - image = ByteImage(data=zf.read(path.filename)) + image = Image.from_bytes(data=zf.read(path.filename)) self._items.append(DatasetItem(id=item_id, media=image, subset=self._subset)) @@ -115,9 +115,8 @@ def apply(self): def _archive_image(self, zipfile, item): image_name = self._make_image_filename(item) - if osp.isfile(item.media.path): + path = getattr(item.media, "path", None) + if path is not None and osp.isfile(path): zipfile.write(item.media.path, arcname=image_name) - elif isinstance(item.media, ByteImage): - zipfile.writestr(image_name, item.media.get_bytes()) elif item.media.has_data: zipfile.writestr(image_name, encode_image(item.media.data, osp.splitext(image_name)[1])) diff --git a/src/datumaro/plugins/data_formats/imagenet.py b/src/datumaro/plugins/data_formats/imagenet.py index 303a613be4..ca5bfac138 100644 --- a/src/datumaro/plugins/data_formats/imagenet.py +++ b/src/datumaro/plugins/data_formats/imagenet.py @@ -44,7 +44,9 @@ def _load_items(self, path): item_id = osp.join(label, image_name) item = items.get(item_id) if item is None: - item = DatasetItem(id=item_id, subset=self._subset, media=Image(path=image_path)) + item = DatasetItem( + id=item_id, subset=self._subset, media=Image.from_file(path=image_path) + ) items[item_id] = item annotations = item.annotations diff --git a/src/datumaro/plugins/data_formats/imagenet_txt.py b/src/datumaro/plugins/data_formats/imagenet_txt.py index 722573b177..8d71eaf573 100644 --- a/src/datumaro/plugins/data_formats/imagenet_txt.py +++ b/src/datumaro/plugins/data_formats/imagenet_txt.py @@ -126,7 +126,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=osp.join(self.image_dir, image)), + media=Image.from_file(path=osp.join(self.image_dir, image)), annotations=anno, ) diff --git a/src/datumaro/plugins/data_formats/kitti/base.py b/src/datumaro/plugins/data_formats/kitti/base.py index bdc17ccedd..2fb9dfcf0a 100644 --- a/src/datumaro/plugins/data_formats/kitti/base.py +++ b/src/datumaro/plugins/data_formats/kitti/base.py @@ -89,7 +89,7 @@ def _load_items(self): image = image_path_by_id.pop(item_id, None) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem( id=item_id, annotations=anns, media=image, subset=self._subset @@ -136,7 +136,7 @@ def _load_items(self): image = image_path_by_id.pop(item_id, None) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem( id=item_id, annotations=anns, media=image, subset=self._subset @@ -144,7 +144,7 @@ def _load_items(self): for item_id, image_path in image_path_by_id.items(): items[item_id] = DatasetItem( - id=item_id, subset=self._subset, media=Image(path=image_path) + id=item_id, subset=self._subset, media=Image.from_file(path=image_path) ) return items diff --git a/src/datumaro/plugins/data_formats/kitti_raw/base.py b/src/datumaro/plugins/data_formats/kitti_raw/base.py index 9cf61e912a..ba26705046 100644 --- a/src/datumaro/plugins/data_formats/kitti_raw/base.py +++ b/src/datumaro/plugins/data_formats/kitti_raw/base.py @@ -241,9 +241,11 @@ def _load_items(self, parsed): items[frame_id] = DatasetItem( id=name, subset=self._subset, - media=PointCloud( + media=PointCloud.from_file( osp.join(self._rootdir, KittiRawPath.PCD_DIR, name + ".pcd"), - extra_images=[Image(path=image) for image in sorted(images.get(name, []))], + extra_images=[ + Image.from_file(path=image) for image in sorted(images.get(name, [])) + ], ), annotations=item_desc.get("annotations"), attributes={"frame": int(frame_id)}, @@ -256,9 +258,11 @@ def _load_items(self, parsed): items[frame_id] = DatasetItem( id=name, subset=self._subset, - media=PointCloud( + media=PointCloud.from_file( osp.join(self._rootdir, KittiRawPath.PCD_DIR, name + ".pcd"), - extra_images=[Image(path=image) for image in sorted(images.get(name, []))], + extra_images=[ + Image.from_file(path=image) for image in sorted(images.get(name, [])) + ], ), attributes={"frame": int(frame_id)}, ) diff --git a/src/datumaro/plugins/data_formats/labelme.py b/src/datumaro/plugins/data_formats/labelme.py index 376a6449b7..48759a4d70 100644 --- a/src/datumaro/plugins/data_formats/labelme.py +++ b/src/datumaro/plugins/data_formats/labelme.py @@ -89,7 +89,7 @@ def _parse(self, dataset_root): height_elem = imagesize_elem.find("nrows") image_size = (int(height_elem.text), int(width_elem.text)) - image = Image(path=image_path, size=image_size) + image = Image.from_file(path=image_path, size=image_size) annotations = self._parse_annotations(root, osp.join(dataset_root, subset), categories) diff --git a/src/datumaro/plugins/data_formats/lfw.py b/src/datumaro/plugins/data_formats/lfw.py index 98fe8d8b9e..3757a7569c 100644 --- a/src/datumaro/plugins/data_formats/lfw.py +++ b/src/datumaro/plugins/data_formats/lfw.py @@ -100,7 +100,7 @@ def get_label_id(label_name): if item_id not in items: image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem( id=item_id, subset=self._subset, media=image, annotations=annotations @@ -116,7 +116,7 @@ def get_label_id(label_name): image = images.get(image1) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[id1] = DatasetItem( id=id1, subset=self._subset, media=image, annotations=annotations @@ -127,7 +127,7 @@ def get_label_id(label_name): image = images.get(image2) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[id2] = DatasetItem( id=id2, subset=self._subset, media=image, annotations=annotations @@ -152,7 +152,7 @@ def get_label_id(label_name): image = images.get(image1) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[id1] = DatasetItem( id=id1, subset=self._subset, media=image, annotations=annotations @@ -165,7 +165,7 @@ def get_label_id(label_name): image = images.get(image2) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[id2] = DatasetItem( id=id2, subset=self._subset, media=image, annotations=annotations diff --git a/src/datumaro/plugins/data_formats/mapillary_vistas/base.py b/src/datumaro/plugins/data_formats/mapillary_vistas/base.py index 2508ac7b6e..9818ef7be3 100644 --- a/src/datumaro/plugins/data_formats/mapillary_vistas/base.py +++ b/src/datumaro/plugins/data_formats/mapillary_vistas/base.py @@ -134,7 +134,7 @@ def _load_panoptic_items(self, config): item_id = item_ann["image_id"] image = None if images_info.get(item_id): - image = Image( + image = Image.from_file( path=images_info[item_id]["path"], size=self._get_image_size(images_info[item_id]), ) @@ -219,7 +219,7 @@ def _load_instances_items(self): for image_path in find_images(self._images_dir, recursive=True): item_id = osp.splitext(osp.relpath(image_path, self._images_dir))[0] - image = Image(path=image_path) + image = Image.from_file(path=image_path) if item_id in items: items[item_id].media = image else: @@ -238,7 +238,7 @@ def _load_polygons(self, items): image_size = self._get_image_size(item_info) if image_size and item.has_image: - item.media = Image(path=item.image.path, size=image_size) + item.media = Image.from_file(path=item.image.path, size=image_size) polygons = item_info["objects"] annotations = [] diff --git a/src/datumaro/plugins/data_formats/market1501.py b/src/datumaro/plugins/data_formats/market1501.py index 322f6ec044..4d9370d031 100644 --- a/src/datumaro/plugins/data_formats/market1501.py +++ b/src/datumaro/plugins/data_formats/market1501.py @@ -94,7 +94,10 @@ def _load_items(self, subset, subset_path): item = items.get(item_id) if item is None: item = DatasetItem( - id=item_id, subset=subset, media=Image(path=image_path), attributes=attributes + id=item_id, + subset=subset, + media=Image.from_file(path=image_path), + attributes=attributes, ) items[item_id] = item diff --git a/src/datumaro/plugins/data_formats/mars.py b/src/datumaro/plugins/data_formats/mars.py index 52729419dd..3fb6b902cc 100644 --- a/src/datumaro/plugins/data_formats/mars.py +++ b/src/datumaro/plugins/data_formats/mars.py @@ -90,7 +90,7 @@ def _load_items(self, subset, path): items.append( DatasetItem( id=item_id, - media=Image(path=image_path), + media=Image.from_file(path=image_path), subset=subset, annotations=[Label(label=label_id)], attributes={ diff --git a/src/datumaro/plugins/data_formats/mnist.py b/src/datumaro/plugins/data_formats/mnist.py index 28a8a7578d..8962007a87 100644 --- a/src/datumaro/plugins/data_formats/mnist.py +++ b/src/datumaro/plugins/data_formats/mnist.py @@ -109,7 +109,7 @@ def _load_items(self, path): image = images[i].reshape(MnistPath.IMAGE_SIZE, MnistPath.IMAGE_SIZE) if image is not None: - image = Image(data=image) + image = Image.from_numpy(data=image) if 0 < len(meta) and (len(meta[i]) == 1 or len(meta[i]) == 3): i = meta[i][0] diff --git a/src/datumaro/plugins/data_formats/mnist_csv.py b/src/datumaro/plugins/data_formats/mnist_csv.py index 2e67442937..9ba951ff84 100644 --- a/src/datumaro/plugins/data_formats/mnist_csv.py +++ b/src/datumaro/plugins/data_formats/mnist_csv.py @@ -93,7 +93,7 @@ def _load_items(self, path): image = np.array([int(pix) for pix in data[1:]], dtype="uint8").reshape(28, 28) if image is not None: - image = Image(data=image) + image = Image.from_numpy(data=image) if 0 < len(meta) and len(meta[i]) in [1, 3]: i = meta[i][0] diff --git a/src/datumaro/plugins/data_formats/mot.py b/src/datumaro/plugins/data_formats/mot.py index b18a9dc64d..8c49291923 100644 --- a/src/datumaro/plugins/data_formats/mot.py +++ b/src/datumaro/plugins/data_formats/mot.py @@ -135,7 +135,7 @@ def _load_items(self, path): items[str(frame_id)] = DatasetItem( id=str(frame_id), subset=self._subset, - media=Image( + media=Image.from_file( path=osp.join( self._image_dir, "%06d%s" % (frame_id, self._seq_info["imext"]) ), @@ -145,7 +145,9 @@ def _load_items(self, path): elif osp.isdir(self._image_dir): for p in find_images(self._image_dir): frame_id = osp.splitext(osp.relpath(p, self._image_dir))[0] - items[frame_id] = DatasetItem(id=frame_id, subset=self._subset, media=Image(path=p)) + items[frame_id] = DatasetItem( + id=frame_id, subset=self._subset, media=Image.from_file(path=p) + ) with open(path, newline="", encoding="utf-8") as csv_file: # NOTE: Different MOT files have different count of fields diff --git a/src/datumaro/plugins/data_formats/mots.py b/src/datumaro/plugins/data_formats/mots.py index 77cc611c1c..13da123819 100644 --- a/src/datumaro/plugins/data_formats/mots.py +++ b/src/datumaro/plugins/data_formats/mots.py @@ -88,7 +88,7 @@ def _parse_items(self): item_id = osp.splitext(osp.relpath(p, self._anno_dir))[0] image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items.append( DatasetItem( id=item_id, diff --git a/src/datumaro/plugins/data_formats/mpii/mpii_json.py b/src/datumaro/plugins/data_formats/mpii/mpii_json.py index 75ca910710..42901de100 100644 --- a/src/datumaro/plugins/data_formats/mpii/mpii_json.py +++ b/src/datumaro/plugins/data_formats/mpii/mpii_json.py @@ -144,7 +144,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=osp.join(root_dir, ann.get("img_paths", ""))), + media=Image.from_file(path=osp.join(root_dir, ann.get("img_paths", ""))), annotations=annotations, ) diff --git a/src/datumaro/plugins/data_formats/mpii/mpii_mat.py b/src/datumaro/plugins/data_formats/mpii/mpii_mat.py index 60b287a378..b976b6c1c9 100644 --- a/src/datumaro/plugins/data_formats/mpii/mpii_mat.py +++ b/src/datumaro/plugins/data_formats/mpii/mpii_mat.py @@ -127,7 +127,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=osp.join(root_dir, image)), + media=Image.from_file(path=osp.join(root_dir, image)), annotations=annotations, ) diff --git a/src/datumaro/plugins/data_formats/nyu_depth_v2.py b/src/datumaro/plugins/data_formats/nyu_depth_v2.py index 305f1bfb42..a462d4762c 100644 --- a/src/datumaro/plugins/data_formats/nyu_depth_v2.py +++ b/src/datumaro/plugins/data_formats/nyu_depth_v2.py @@ -36,8 +36,8 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, - media=Image(data=image), - annotations=[DepthAnnotation(image=Image(data=depth))], + media=Image.from_numpy(data=image), + annotations=[DepthAnnotation(image=Image.from_numpy(data=depth))], ) return items diff --git a/src/datumaro/plugins/data_formats/open_images.py b/src/datumaro/plugins/data_formats/open_images.py index 0516da6d21..8d935e1187 100644 --- a/src/datumaro/plugins/data_formats/open_images.py +++ b/src/datumaro/plugins/data_formats/open_images.py @@ -349,7 +349,7 @@ def _add_item(self, item_id, subset): "It should be in the '%s' directory" % (item_id, OpenImagesPath.IMAGES_DIR) ) else: - image = Image(path=image_path, size=self._image_meta.get(item_id)) + image = Image.from_file(path=image_path, size=self._image_meta.get(item_id)) item = DatasetItem(id=item_id, media=image, subset=subset) self._items.append(item) diff --git a/src/datumaro/plugins/data_formats/sly_pointcloud/base.py b/src/datumaro/plugins/data_formats/sly_pointcloud/base.py index a97cb8758a..00f5f08544 100644 --- a/src/datumaro/plugins/data_formats/sly_pointcloud/base.py +++ b/src/datumaro/plugins/data_formats/sly_pointcloud/base.py @@ -161,12 +161,14 @@ def _load_items(self, parsed): ) related_images = None if osp.isdir(related_images_dir): - related_images = [Image(path=image) for image in find_images(related_images_dir)] + related_images = [ + Image.from_file(path=image) for image in find_images(related_images_dir) + ] parsed[frame_id] = DatasetItem( id=name, subset=self._subset, - media=PointCloud(pcd_path, extra_images=related_images), + media=PointCloud.from_file(pcd_path, extra_images=related_images), annotations=frame_desc.get("annotations"), attributes={"frame": int(frame_id), **frame_desc["attributes"]}, ) diff --git a/src/datumaro/plugins/data_formats/synthia.py b/src/datumaro/plugins/data_formats/synthia.py index 403992936c..a9e958d404 100644 --- a/src/datumaro/plugins/data_formats/synthia.py +++ b/src/datumaro/plugins/data_formats/synthia.py @@ -153,7 +153,7 @@ def _load_items(self, root_dir): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image, annotations=anno) @@ -175,7 +175,7 @@ def _load_items(self, root_dir): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, media=image, annotations=anno) diff --git a/src/datumaro/plugins/data_formats/tf_detection_api/base.py b/src/datumaro/plugins/data_formats/tf_detection_api/base.py index b5a8ac2ec1..4592d7152e 100644 --- a/src/datumaro/plugins/data_formats/tf_detection_api/base.py +++ b/src/datumaro/plugins/data_formats/tf_detection_api/base.py @@ -11,7 +11,7 @@ from datumaro.components.annotation import AnnotationType, Bbox, LabelCategories, Mask from datumaro.components.dataset_base import DatasetItem, SubsetBase from datumaro.components.importer import Importer -from datumaro.components.media import ByteImage +from datumaro.components.media import Image from datumaro.util.image import decode_image, lazy_image from datumaro.util.tf_util import import_tf as _import_tf @@ -164,7 +164,7 @@ def _parse_tfrecord_file(cls, filepath, subset, images_dir): image = None if image_params: - image = ByteImage(**image_params, size=image_size) + image = Image.from_bytes(**image_params, size=image_size) dataset_items.append( DatasetItem( diff --git a/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py b/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py index 843bbeb27a..34f979f3bf 100644 --- a/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py +++ b/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py @@ -12,7 +12,7 @@ from datumaro.components.annotation import AnnotationType, LabelCategories from datumaro.components.exporter import Exporter -from datumaro.components.media import ByteImage, Image +from datumaro.components.media import Image, ImageFromBytes from datumaro.util.annotation_util import find_group_leader, find_instances, max_bbox from datumaro.util.image import encode_image from datumaro.util.mask_tools import merge_masks @@ -220,8 +220,8 @@ def _save_image(self, item, path=None): # pylint: disable=arguments-differ "image extension, the corresponding field will be empty." % (item.id, dst_ext) ) - if src_ext == dst_ext and isinstance(item.media, ByteImage): - buffer = item.media.get_bytes() + if src_ext == dst_ext and isinstance(item.media, ImageFromBytes): + buffer = item.media.bytes else: buffer = encode_image(item.media.data, dst_ext) return buffer, fmt diff --git a/src/datumaro/plugins/data_formats/vgg_face2.py b/src/datumaro/plugins/data_formats/vgg_face2.py index 5273242a13..b833940209 100644 --- a/src/datumaro/plugins/data_formats/vgg_face2.py +++ b/src/datumaro/plugins/data_formats/vgg_face2.py @@ -133,7 +133,7 @@ def _get_label(path): if item_id not in items: image = images.get(row["NAME_ID"]) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, subset=subset, media=image) annotations = items[item_id].annotations @@ -166,7 +166,7 @@ def _get_label(path): if item_id not in items: image = images.get(row["NAME_ID"]) if image: - image = Image(path=image) + image = Image.from_file(path=image) items[item_id] = DatasetItem(id=item_id, subset=subset, media=image) annotations = items[item_id].annotations diff --git a/src/datumaro/plugins/data_formats/voc/base.py b/src/datumaro/plugins/data_formats/voc/base.py index a184036179..a3fe0664ab 100644 --- a/src/datumaro/plugins/data_formats/voc/base.py +++ b/src/datumaro/plugins/data_formats/voc/base.py @@ -130,7 +130,7 @@ def __iter__(self): log.debug("Reading item '%s'", item_id) image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) yield DatasetItem( id=item_id, subset=self._subset, media=image, annotations=annotations.get(item_id) ) @@ -214,7 +214,7 @@ def __iter__(self): image = images.pop(item_id, None) if image or size: - image = Image(path=image, size=size) + image = Image.from_file(path=image, size=size) yield DatasetItem(id=item_id, subset=self._subset, media=image, annotations=anns) except ElementTree.ParseError as e: @@ -373,7 +373,7 @@ def __iter__(self): image = images.get(item_id) if image: - image = Image(path=image) + image = Image.from_file(path=image) try: yield DatasetItem( diff --git a/src/datumaro/plugins/data_formats/vott_csv.py b/src/datumaro/plugins/data_formats/vott_csv.py index b64138ed4f..f45105b462 100644 --- a/src/datumaro/plugins/data_formats/vott_csv.py +++ b/src/datumaro/plugins/data_formats/vott_csv.py @@ -46,7 +46,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=osp.join(osp.dirname(path), row["image"])), + media=Image.from_file(path=osp.join(osp.dirname(path), row["image"])), ) annotations = items[item_id].annotations diff --git a/src/datumaro/plugins/data_formats/vott_json.py b/src/datumaro/plugins/data_formats/vott_json.py index d39dc431c7..9245d6f105 100644 --- a/src/datumaro/plugins/data_formats/vott_json.py +++ b/src/datumaro/plugins/data_formats/vott_json.py @@ -85,7 +85,9 @@ def _load_items(self, path): id=item_id, subset=self._subset, attributes={"id": id}, - media=Image(path=osp.join(osp.dirname(path), asset.get("asset", {}).get("path"))), + media=Image.from_file( + path=osp.join(osp.dirname(path), asset.get("asset", {}).get("path")) + ), annotations=annotations, ) diff --git a/src/datumaro/plugins/data_formats/widerface.py b/src/datumaro/plugins/data_formats/widerface.py index 89604a368b..67579fc768 100644 --- a/src/datumaro/plugins/data_formats/widerface.py +++ b/src/datumaro/plugins/data_formats/widerface.py @@ -113,7 +113,7 @@ def _load_items(self, path): items[item_id] = DatasetItem( id=item_id, subset=self._subset, - media=Image(path=image_path), + media=Image.from_file(path=image_path), annotations=annotations, ) diff --git a/src/datumaro/plugins/data_formats/yolo/base.py b/src/datumaro/plugins/data_formats/yolo/base.py index 11834e8884..00632880a5 100644 --- a/src/datumaro/plugins/data_formats/yolo/base.py +++ b/src/datumaro/plugins/data_formats/yolo/base.py @@ -116,7 +116,7 @@ def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]: image_size = self._image_info.get(item_id) image_path = osp.join(self._path, item) - image = Image(path=image_path, size=image_size) + image = Image.from_file(path=image_path, size=image_size) annotations = self._parse_annotations(image, item_id=(item_id, subset_name)) diff --git a/src/datumaro/plugins/transforms.py b/src/datumaro/plugins/transforms.py index 569526b054..d09af4df36 100644 --- a/src/datumaro/plugins/transforms.py +++ b/src/datumaro/plugins/transforms.py @@ -988,7 +988,7 @@ def __init__( @staticmethod def _lazy_resize_image(image: Image, new_size: tuple[int, int]) -> Image: - def _resize_image(_): + def _resize_image(): h, w = image.size yscale = new_size[0] / float(h) xscale = new_size[1] / float(w) @@ -1000,7 +1000,7 @@ def _resize_image(_): resized_image *= 255.0 return resized_image - return Image(_resize_image, ext=image.ext, size=new_size) + return Image.from_numpy(data=_resize_image, ext=image.ext, size=new_size) @staticmethod def _lazy_resize_mask(mask, new_size): diff --git a/tests/integration/cli/test_compare.py b/tests/integration/cli/test_compare.py index 292e60eb77..f311b96ee4 100644 --- a/tests/integration/cli/test_compare.py +++ b/tests/integration/cli/test_compare.py @@ -43,7 +43,7 @@ def test_can_compare_projects(self): # just a smoke test DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Caption("hello", id=1), Caption("world", id=2, group=5), @@ -91,7 +91,7 @@ def test_can_compare_projects(self): # just a smoke test ), DatasetItem(id=42, subset="test", attributes={"a1": 5, "a2": "42"}), DatasetItem(id=42), - DatasetItem(id=43, media=Image(path="1/b/c.qq", size=(2, 4))), + DatasetItem(id=43, media=Image.from_file(path="1/b/c.qq", size=(2, 4))), ], categories={ AnnotationType.label: label_categories1, @@ -112,7 +112,7 @@ def test_can_compare_projects(self): # just a smoke test DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Caption("hello", id=1), Caption("world", id=2, group=5), @@ -160,7 +160,7 @@ def test_can_compare_projects(self): # just a smoke test ), DatasetItem(id=42, subset="test", attributes={"a1": 5, "a2": "42"}), DatasetItem(id=42), - DatasetItem(id=43, media=Image(path="1/b/c.qq", size=(2, 4))), + DatasetItem(id=43, media=Image.from_file(path="1/b/c.qq", size=(2, 4))), ], categories={ AnnotationType.label: label_categories2, @@ -185,7 +185,7 @@ def test_can_run_distance_diff(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=0), ], @@ -199,7 +199,7 @@ def test_can_run_distance_diff(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1), Bbox(5, 6, 7, 8, label=2), @@ -237,7 +237,7 @@ def test_can_run_equality_diff(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=0), ], @@ -251,7 +251,7 @@ def test_can_run_equality_diff(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1), Bbox(5, 6, 7, 8, label=2), diff --git a/tests/integration/cli/test_image_zip_format.py b/tests/integration/cli/test_image_zip_format.py index 10c6036161..e319534d8e 100644 --- a/tests/integration/cli/test_image_zip_format.py +++ b/tests/integration/cli/test_image_zip_format.py @@ -26,8 +26,8 @@ class ImageZipIntegrationScenarios(TestCase): def test_can_save_and_load(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((5, 5, 3)))), - DatasetItem(id="2", media=Image(data=np.ones((2, 8, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((5, 5, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((2, 8, 3)))), ] ) @@ -98,8 +98,8 @@ def test_can_export_zip_images_from_coco_dataset(self): def test_can_change_extension_for_images_in_zip(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((5, 5, 3)))), - DatasetItem(id="2", media=Image(data=np.ones((2, 8, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((5, 5, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((2, 8, 3)))), ] ) diff --git a/tests/integration/cli/test_kitti_raw_format.py b/tests/integration/cli/test_kitti_raw_format.py index 5a3c7550c6..4cd0b549c3 100644 --- a/tests/integration/cli/test_kitti_raw_format.py +++ b/tests/integration/cli/test_kitti_raw_format.py @@ -45,10 +45,10 @@ def test_can_convert_to_kitti_raw(self): attributes={"occluded": False, "track_id": 2}, ), ], - media=PointCloud( + media=PointCloud.from_file( osp.join(export_dir, "ds0", "pointcloud", "0000000000.pcd"), extra_images=[ - Image( + Image.from_file( path=osp.join( export_dir, "ds0", @@ -72,10 +72,10 @@ def test_can_convert_to_kitti_raw(self): attributes={"occluded": True, "track_id": 2}, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(export_dir, "ds0", "pointcloud", "0000000001.pcd"), extra_images=[ - Image( + Image.from_file( path=osp.join( export_dir, "ds0", @@ -98,10 +98,10 @@ def test_can_convert_to_kitti_raw(self): attributes={"occluded": False, "track_id": 3}, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(export_dir, "ds0", "pointcloud", "0000000002.pcd"), extra_images=[ - Image( + Image.from_file( path=osp.join( export_dir, "ds0", diff --git a/tests/integration/cli/test_merge.py b/tests/integration/cli/test_merge.py index 73311588e3..af46ae4e1f 100644 --- a/tests/integration/cli/test_merge.py +++ b/tests/integration/cli/test_merge.py @@ -22,7 +22,7 @@ def test_can_run_self_merge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 3, label=0), ], @@ -36,7 +36,7 @@ def test_can_run_self_merge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1), Bbox(5, 6, 2, 3, label=2), @@ -51,7 +51,7 @@ def test_can_run_self_merge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox( 1, @@ -114,7 +114,15 @@ def test_can_run_self_merge(self): project.import_source("source", dataset2_url, "voc") result_dir = osp.join(test_dir, "result") - run(self, "merge", "-o", result_dir, "-p", proj_dir, dataset1_url + ":coco") + run( + self, + "merge", + "-o", + result_dir, + "-p", + proj_dir, + dataset1_url + ":coco", + ) compare_datasets(self, expected, Dataset.load(result_dir), require_media=True) @@ -125,7 +133,7 @@ def test_can_run_multimerge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 3, label=0), ], @@ -139,7 +147,7 @@ def test_can_run_multimerge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1), Bbox(5, 6, 2, 3, label=2), @@ -154,7 +162,7 @@ def test_can_run_multimerge(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox( 1, @@ -224,7 +232,7 @@ def test_can_save_in_another_format(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 3, label=0), ], @@ -238,7 +246,7 @@ def test_can_save_in_another_format(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1), Bbox(5, 6, 2, 3, label=2), @@ -253,7 +261,7 @@ def test_can_save_in_another_format(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=2), Bbox(5, 6, 2, 3, label=3), diff --git a/tests/integration/cli/test_patch.py b/tests/integration/cli/test_patch.py index 00bc5d90b1..997f4a9c99 100644 --- a/tests/integration/cli/test_patch.py +++ b/tests/integration/cli/test_patch.py @@ -23,7 +23,7 @@ def test_can_run_patch(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 3, label=0), ], @@ -31,7 +31,7 @@ def test_can_run_patch(self): # Must be kept DatasetItem( id=1, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -44,7 +44,7 @@ def test_can_run_patch(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=0), # Label must be remapped Bbox(5, 6, 2, 3, label=1), # Label must be remapped @@ -54,7 +54,7 @@ def test_can_run_patch(self): # Must be added DatasetItem( id=2, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Bbox(1, 2, 3, 2, label=1)], # Label must be remapped ), ], @@ -66,7 +66,7 @@ def test_can_run_patch(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Bbox(1, 2, 3, 4, label=1, id=1, group=1), Bbox(5, 6, 2, 3, label=0, id=2, group=2), @@ -74,12 +74,12 @@ def test_can_run_patch(self): ), DatasetItem( id=1, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Bbox(1, 2, 3, 4, label=1, id=1, group=1)], ), DatasetItem( id=2, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Bbox(1, 2, 3, 2, label=0, id=2, group=2)], ), ], @@ -118,7 +118,7 @@ def test_patch_fails_on_inplace_update_without_overwrite(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((3, 5, 3))), + media=Image.from_numpy(data=np.zeros((3, 5, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -129,7 +129,7 @@ def test_patch_fails_on_inplace_update_without_overwrite(self): [ DatasetItem( id=2, - media=Image(data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3))), annotations=[Bbox(1, 2, 3, 2, label=1)], ), ], @@ -151,7 +151,7 @@ def test_patch_fails_on_inplace_update_of_stage(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((3, 5, 3))), + media=Image.from_numpy(data=np.zeros((3, 5, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -162,7 +162,7 @@ def test_patch_fails_on_inplace_update_of_stage(self): [ DatasetItem( id=2, - media=Image(data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3))), annotations=[Bbox(1, 2, 3, 2, label=1)], ), ], diff --git a/tests/integration/cli/test_project.py b/tests/integration/cli/test_project.py index 1a7a5bba60..2ebef582d8 100644 --- a/tests/integration/cli/test_project.py +++ b/tests/integration/cli/test_project.py @@ -132,7 +132,7 @@ def test_can_use_vcs(self): [ DatasetItem( 0, - media=Image(data=np.ones((1, 2, 3))), + media=Image.from_numpy(data=np.ones((1, 2, 3))), annotations=[ Bbox(1, 1, 1, 1, label=0), Bbox(2, 2, 2, 2, label=1), @@ -192,7 +192,7 @@ def test_can_use_vcs(self): [ DatasetItem( 0, - media=Image(data=np.ones((1, 2, 3))), + media=Image.from_numpy(data=np.ones((1, 2, 3))), annotations=[ Bbox( 2, @@ -235,7 +235,7 @@ def test_can_use_vcs(self): [ DatasetItem( 0, - media=Image(data=np.ones((1, 2, 3))), + media=Image.from_numpy(data=np.ones((1, 2, 3))), annotations=[ Bbox( 1, diff --git a/tests/integration/cli/test_sly_point_cloud_format.py b/tests/integration/cli/test_sly_point_cloud_format.py index 6ab405a798..6f809f7ab9 100644 --- a/tests/integration/cli/test_sly_point_cloud_format.py +++ b/tests/integration/cli/test_sly_point_cloud_format.py @@ -51,10 +51,12 @@ def test_can_convert_to_kitti_raw(self): }, ), ], - media=PointCloud( + media=PointCloud.from_file( osp.join(export_dir, "velodyne_points", "data", "frame1.pcd"), extra_images=[ - Image(path=osp.join(export_dir, "image_00", "data", "frame1.png")) + Image.from_file( + path=osp.join(export_dir, "image_00", "data", "frame1.png") + ) ], ), attributes={"frame": 0}, @@ -73,10 +75,12 @@ def test_can_convert_to_kitti_raw(self): }, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(export_dir, "velodyne_points", "data", "frame2.pcd"), extra_images=[ - Image(path=osp.join(export_dir, "image_00", "data", "frame2.png")) + Image.from_file( + path=osp.join(export_dir, "image_00", "data", "frame2.png") + ) ], ), attributes={"frame": 1}, diff --git a/tests/integration/cli/test_utils.py b/tests/integration/cli/test_utils.py index 3060a8ff4f..524493ccdf 100644 --- a/tests/integration/cli/test_utils.py +++ b/tests/integration/cli/test_utils.py @@ -41,4 +41,4 @@ def test_can_split_video(self): "2", ) - assert set(os.listdir(output_dir)) == {"%06d.jpg" % n for n in range(2, 8, 2)} + assert set(os.listdir(output_dir)) == {"%06d.jpg" % n for n in range(2, 9, 2)} diff --git a/tests/integration/cli/test_voc_format.py b/tests/integration/cli/test_voc_format.py index 1c85277878..4eb1c1e47e 100644 --- a/tests/integration/cli/test_voc_format.py +++ b/tests/integration/cli/test_voc_format.py @@ -163,7 +163,7 @@ def test_export_to_voc_format(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox( 0.0, @@ -232,7 +232,7 @@ def test_convert_to_voc_format(self): DatasetItem( id="000001", subset="default", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0.0, @@ -312,7 +312,7 @@ def test_convert_from_voc_format(self): DatasetItem( id="no_label/2007_000002", subset="default", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), ) ], categories=labels, @@ -346,7 +346,7 @@ def test_can_save_and_load_voc_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[Label(i) for i in range(22) if i % 2 == 1] + [ Bbox( @@ -384,7 +384,9 @@ def test_can_save_and_load_voc_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -401,7 +403,7 @@ def test_can_save_and_load_voc_layout_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 4.0, @@ -422,7 +424,9 @@ def test_can_save_and_load_voc_layout_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -454,11 +458,13 @@ def test_can_save_and_load_voc_classification_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[Label(i) for i in range(22) if i % 2 == 1], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -489,7 +495,7 @@ def test_can_save_and_load_voc_detection_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 4.0, @@ -524,7 +530,9 @@ def test_can_save_and_load_voc_detection_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -555,11 +563,13 @@ def test_can_save_and_load_voc_segmentation_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[Mask(image=np.ones([10, 20]), label=2, group=1)], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -591,7 +601,7 @@ def test_can_save_and_load_voc_action_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 4.0, @@ -611,7 +621,9 @@ def test_can_save_and_load_voc_action_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -643,7 +655,7 @@ def test_label_projection_with_masks(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 1, @@ -663,7 +675,9 @@ def test_label_projection_with_masks(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories( diff --git a/tests/integration/cli/test_yolo_format.py b/tests/integration/cli/test_yolo_format.py index a4820f2fa6..ebd2ca583b 100644 --- a/tests/integration/cli/test_yolo_format.py +++ b/tests/integration/cli/test_yolo_format.py @@ -25,7 +25,7 @@ def test_can_save_and_load_yolo_dataset(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(3.0, 3.0, 2.0, 3.0, label=4), Bbox(0.0, 2.0, 4.0, 2.0, label=2), @@ -110,7 +110,7 @@ def test_can_convert_voc_to_yolo(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox(1.0, 2.0, 2.0, 2.0, label=8), Bbox(4.0, 5.0, 2.0, 2.0, label=15), @@ -118,7 +118,9 @@ def test_can_convert_voc_to_yolo(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=[label.name for label in VOC.make_voc_categories()[AnnotationType.label]], @@ -159,7 +161,7 @@ def test_can_delete_labels_from_yolo_dataset(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[Bbox(0.0, 2.0, 4.0, 2.0, label=0)], ) ], diff --git a/tests/unit/data_formats/datumaro/test_datumaro_format.py b/tests/unit/data_formats/datumaro/test_datumaro_format.py index 6a11aeefcc..24c7066336 100644 --- a/tests/unit/data_formats/datumaro/test_datumaro_format.py +++ b/tests/unit/data_formats/datumaro/test_datumaro_format.py @@ -82,7 +82,7 @@ def test_dataset(self): DatasetItem( id=100, subset="train", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Caption("hello", id=1), Caption("world", id=2, group=5), @@ -174,7 +174,7 @@ def test_dataset(self): ), DatasetItem(id=42, subset="test", attributes={"a1": 5, "a2": "42"}), DatasetItem(id=42), - DatasetItem(id=43, media=Image(path="1/b/c.qq", size=(2, 4))), + DatasetItem(id=43, media=Image.from_file(path="1/b/c.qq", size=(2, 4))), ], categories={ AnnotationType.label: label_categories, @@ -223,21 +223,21 @@ def test_can_import_pcd_dataset(self): attributes={"occluded": False}, ) ], - media=PointCloud( + media=PointCloud.from_file( os.path.join(dataset_path, "point_clouds", "default", "0000000000.pcd") ), attributes={"frame": 0}, ), DatasetItem( id="0000000001", - media=PointCloud( + media=PointCloud.from_file( os.path.join(dataset_path, "point_clouds", "default", "0000000001.pcd") ), attributes={"frame": 1}, ), DatasetItem( id="0000000002", - media=PointCloud( + media=PointCloud.from_file( os.path.join(dataset_path, "point_clouds", "default", "0000000002.pcd") ), attributes={"frame": 2}, @@ -280,7 +280,7 @@ def test_can_import_skeleton_dataset(self): DatasetItem( id="100", subset="default", - media=Image(data=np.ones((10, 6, 3))), + media=Image.from_numpy(data=np.ones((10, 6, 3))), annotations=[ Skeleton( [ @@ -325,9 +325,9 @@ def test_can_detect(self): def test_relative_paths(self): test_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((4, 2, 3)))), + DatasetItem(id="subdir1/1", media=Image.from_numpy(data=np.ones((2, 6, 3)))), + DatasetItem(id="subdir2/1", media=Image.from_numpy(data=np.ones((5, 4, 3)))), ] ) @@ -343,7 +343,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(0, 1, 2, 2, label=0, group=1, id=1, attributes={"is_crowd": False}), ], @@ -352,7 +352,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(1, 0, 2, 2, label=1, group=2, id=2, attributes={"is_crowd": False}), ], @@ -361,7 +361,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=3, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(0, 1, 2, 2, label=2, group=3, id=3, attributes={"is_crowd": False}), ], @@ -379,7 +379,11 @@ def test_can_save_dataset_with_cjk_categories(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): test_dataset = Dataset.from_iterable( - [DatasetItem(id="кириллица с пробелом", media=Image(data=np.ones((4, 2, 3))))] + [ + DatasetItem( + id="кириллица с пробелом", media=Image.from_numpy(data=np.ones((4, 2, 3))) + ) + ] ) with TestDir() as test_dir: @@ -393,12 +397,12 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( id="q/1", - media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), attributes={"frame": 1}, ), DatasetItem( id="a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), attributes={"frame": 2}, ), ] @@ -432,7 +436,7 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): expected = Dataset.from_iterable( [ DatasetItem(1, subset="a"), - DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3)))), DatasetItem(2, subset="b"), ] ) @@ -446,12 +450,12 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): # unmodified subset DatasetItem(2, subset="b"), # removed subset - DatasetItem(3, subset="c", media=Image(data=np.ones((2, 2, 3)))), + DatasetItem(3, subset="c", media=Image.from_numpy(data=np.ones((2, 2, 3)))), ] ) dataset.save(path, save_media=True) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) @@ -465,8 +469,8 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): expected = Dataset.from_iterable( [ DatasetItem(2, subset="test"), - DatasetItem(3, subset="train", media=Image(data=np.ones((2, 2, 3)))), - DatasetItem(4, subset="test", media=Image(data=np.ones((2, 3, 3)))), + DatasetItem(3, subset="train", media=Image.from_numpy(data=np.ones((2, 2, 3)))), + DatasetItem(4, subset="test", media=Image.from_numpy(data=np.ones((2, 3, 3)))), ], media_type=Image, ) @@ -474,8 +478,8 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): [ DatasetItem(1, subset="a"), DatasetItem(2, subset="b"), - DatasetItem(3, subset="c", media=Image(data=np.ones((2, 2, 3)))), - DatasetItem(4, subset="d", media=Image(data=np.ones((2, 3, 3)))), + DatasetItem(3, subset="c", media=Image.from_numpy(data=np.ones((2, 2, 3)))), + DatasetItem(4, subset="d", media=Image.from_numpy(data=np.ones((2, 3, 3)))), ], media_type=Image, ) @@ -504,12 +508,12 @@ def test_can_save_and_load_with_pointcloud(self): DatasetItem( id=1, subset="test", - media=PointCloud( + media=PointCloud.from_file( "1.pcd", extra_images=[ - Image(data=np.ones((5, 5, 3)), path="1/a.jpg"), - Image(data=np.ones((5, 4, 3)), path="1/b.jpg"), - Image(size=(5, 3), path="1/c.jpg"), + Image.from_numpy(data=np.ones((5, 5, 3)), path="1/a.jpg"), + Image.from_numpy(data=np.ones((5, 4, 3)), path="1/b.jpg"), + Image.from_file(size=(5, 3), path="1/c.jpg"), ], ), annotations=[ @@ -535,22 +539,12 @@ def test_can_save_and_load_with_pointcloud(self): DatasetItem( id=1, subset="test", - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "point_clouds", "test", "1.pcd"), extra_images=[ - Image( - data=np.ones((5, 5, 3)), - path=osp.join( - test_dir, "related_images", "test", "1", "image_0.jpg" - ), - ), - Image( - data=np.ones((5, 4, 3)), - path=osp.join( - test_dir, "related_images", "test", "1", "image_1.jpg" - ), - ), - Image( + Image.from_numpy(data=np.ones((5, 5, 3))), + Image.from_numpy(data=np.ones((5, 4, 3))), + Image.from_file( size=(5, 3), path=osp.join( test_dir, "related_images", "test", "1", "image_2.jpg" @@ -615,7 +609,7 @@ def test_can_save_and_load_with_skeleton(self): DatasetItem( id="img1", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Skeleton( source_elements, @@ -638,7 +632,7 @@ def test_can_save_and_load_with_skeleton(self): DatasetItem( id="img1", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Skeleton( target_elements, diff --git a/tests/unit/data_formats/test_common_semantic_segmentation_format.py b/tests/unit/data_formats/test_common_semantic_segmentation_format.py index 904eaccb01..60f107cfa2 100644 --- a/tests/unit/data_formats/test_common_semantic_segmentation_format.py +++ b/tests/unit/data_formats/test_common_semantic_segmentation_format.py @@ -42,7 +42,7 @@ def test_can_import(self): [ DatasetItem( id="0001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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=5), @@ -50,7 +50,7 @@ def test_can_import(self): ), DatasetItem( id="0002", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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=4), @@ -81,7 +81,7 @@ def test_can_import_non_standard_structure(self): [ DatasetItem( id="0001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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=5), @@ -89,7 +89,7 @@ def test_can_import_non_standard_structure(self): ), DatasetItem( id="0002", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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=5), diff --git a/tests/unit/data_formats/test_mapillary_vistas_format.py b/tests/unit/data_formats/test_mapillary_vistas_format.py index 7279c90779..98fe6eb32c 100644 --- a/tests/unit/data_formats/test_mapillary_vistas_format.py +++ b/tests/unit/data_formats/test_mapillary_vistas_format.py @@ -51,7 +51,7 @@ def test_can_import_v1_2(self): Mask(image=np.array([[0, 0, 1, 1, 0]] * 5), label=1), Mask(image=np.array([[0, 0, 0, 0, 1]] * 5), label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="1", @@ -61,7 +61,7 @@ def test_can_import_v1_2(self): Mask(image=np.array([[0, 0, 0, 0, 1]] * 5), label=0, id=1), Mask(image=np.array([[0, 0, 1, 1, 0]] * 5), label=1, id=0), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="2", @@ -70,7 +70,7 @@ def test_can_import_v1_2(self): Mask(image=np.array([[1, 1, 0, 1, 1]] * 5), label=1), Mask(image=np.array([[0, 0, 1, 0, 0]] * 5), label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories={AnnotationType.label: label_cat, AnnotationType.mask: mask_cat}, @@ -92,7 +92,7 @@ def test_can_import_with_original_config(self): Mask(image=np.array([[0, 0, 1, 1, 0]] * 5), label=1), Mask(image=np.array([[0, 0, 0, 0, 1]] * 5), label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories=make_mapillary_instance_categories(MapillaryVistasLabelMaps["v1.2"]), @@ -155,7 +155,7 @@ def test_can_import_v2_0_instances(self): Polygon(points=[0, 0, 1, 0, 2, 0, 2, 4, 0, 4], label=0), Polygon(points=[3, 0, 4, 0, 4, 1, 4, 4, 3, 4], label=1), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="1", @@ -168,7 +168,7 @@ def test_can_import_v2_0_instances(self): Polygon(points=[0, 0, 1, 0, 1, 4, 4, 0, 0, 0], label=2), Polygon(points=[3, 0, 4, 0, 4, 4, 3, 4, 3, 0], label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="2", @@ -185,7 +185,7 @@ def test_can_import_v2_0_instances(self): Polygon(points=[1, 0, 1, 1, 1, 2, 1, 3, 1, 4], label=1), Polygon(points=[3, 0, 3, 1, 3, 2, 3, 3, 3, 4], label=1), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories={AnnotationType.label: label_cat, AnnotationType.mask: mask_cat}, @@ -230,7 +230,7 @@ def test_can_import_v2_0_panoptic(self): Polygon(points=[0, 0, 1, 0, 2, 0, 2, 4, 0, 4], label=0), Polygon(points=[3, 0, 4, 0, 4, 1, 4, 4, 3, 4], label=1), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="1", @@ -261,7 +261,7 @@ def test_can_import_v2_0_panoptic(self): Polygon(points=[0, 0, 1, 0, 1, 4, 4, 0, 0, 0], label=2), Polygon(points=[3, 0, 4, 0, 4, 4, 3, 4, 3, 0], label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="2", @@ -308,7 +308,7 @@ def test_can_import_v2_0_panoptic(self): Polygon(points=[1, 0, 1, 1, 1, 2, 1, 3, 1, 4], label=1), Polygon(points=[3, 0, 3, 1, 3, 2, 3, 3, 3, 4], label=1), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories={AnnotationType.label: label_cat, AnnotationType.mask: mask_cat}, @@ -351,7 +351,7 @@ def test_can_import_v2_0_panoptic_with_keeping_category_ids(self): Polygon(points=[0, 0, 1, 0, 2, 0, 2, 4, 0, 4], label=1), Polygon(points=[3, 0, 4, 0, 4, 1, 4, 4, 3, 4], label=10), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="1", @@ -382,7 +382,7 @@ def test_can_import_v2_0_panoptic_with_keeping_category_ids(self): Polygon(points=[0, 0, 1, 0, 1, 4, 4, 0, 0, 0], label=100), Polygon(points=[3, 0, 4, 0, 4, 4, 3, 4, 3, 0], label=100), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="2", @@ -429,7 +429,7 @@ def test_can_import_v2_0_panoptic_with_keeping_category_ids(self): Polygon(points=[1, 0, 1, 1, 1, 2, 1, 3, 1, 4], label=10), Polygon(points=[3, 0, 3, 1, 3, 2, 3, 3, 3, 4], label=10), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories={AnnotationType.label: label_cat, AnnotationType.mask: mask_cat}, @@ -535,7 +535,7 @@ def test_can_import_with_meta_file(self): Mask(image=np.array([[0, 0, 0, 0, 1]] * 5), label=0, id=1), Mask(image=np.array([[0, 0, 1, 1, 0]] * 5), label=1, id=0), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), DatasetItem( id="2", @@ -544,7 +544,7 @@ def test_can_import_with_meta_file(self): Mask(image=np.array([[1, 1, 0, 1, 1]] * 5), label=1), Mask(image=np.array([[0, 0, 1, 0, 0]] * 5), label=2), ], - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), ), ], categories={AnnotationType.label: label_cat, AnnotationType.mask: mask_cat}, diff --git a/tests/unit/data_formats/test_synthia_format.py b/tests/unit/data_formats/test_synthia_format.py index b520e69d1b..5fc56a8762 100644 --- a/tests/unit/data_formats/test_synthia_format.py +++ b/tests/unit/data_formats/test_synthia_format.py @@ -46,7 +46,7 @@ def test_can_import(self): [ DatasetItem( id="Stereo_Left/Omni_B/000000", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 1, 0, 0, 0]]), @@ -62,7 +62,7 @@ def test_can_import(self): ), DatasetItem( id="Stereo_Left/Omni_B/000001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 0, 0]]), @@ -83,7 +83,7 @@ def test_can_import(self): ), DatasetItem( id="Stereo_Left/Omni_F/000000", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 1, 0, 0, 0]]), @@ -104,7 +104,7 @@ def test_can_import(self): ), DatasetItem( id="Stereo_Left/Omni_F/000001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 0, 0]]), @@ -142,7 +142,7 @@ def test_can_import_with_colored_masks(self): [ DatasetItem( id="Stereo_Left/Omni_F/000000", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=1), Mask(np.array([[0, 0, 1, 1, 0]]), label=2), @@ -151,7 +151,7 @@ def test_can_import_with_colored_masks(self): ), DatasetItem( id="Stereo_Left/Omni_F/000001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 0, 0, 0, 0]]), label=1), Mask(np.array([[0, 1, 0, 0, 0]]), label=2), @@ -173,7 +173,7 @@ def test_can_import_with_custom_labelmap(self): [ DatasetItem( id="Stereo_Left/Omni_F/000000", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 1, 0, 0]]), label=1), Mask(np.array([[0, 0, 0, 1, 1]]), label=4), @@ -181,7 +181,7 @@ def test_can_import_with_custom_labelmap(self): ), DatasetItem( id="Stereo_Left/Omni_F/000001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=2), Mask(np.array([[0, 0, 1, 1, 0]]), label=3), @@ -215,7 +215,7 @@ def test_can_import_with_meta_file(self): [ DatasetItem( id="Stereo_Left/Omni_F/000000", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 1, 0, 0]]), label=1), Mask(np.array([[0, 0, 0, 1, 1]]), label=4), @@ -223,7 +223,7 @@ def test_can_import_with_meta_file(self): ), DatasetItem( id="Stereo_Left/Omni_F/000001", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=2), Mask(np.array([[0, 0, 1, 1, 0]]), label=3), diff --git a/tests/unit/data_formats/test_yolo_format.py b/tests/unit/data_formats/test_yolo_format.py index a8690195c4..1a51ffda4c 100644 --- a/tests/unit/data_formats/test_yolo_format.py +++ b/tests/unit/data_formats/test_yolo_format.py @@ -147,7 +147,7 @@ def _generate_random_dataset(self, recipes, n_of_labels=10): subset=recipe.get("subset", "train"), media=recipe.get( "media", - Image(data=np.ones((randint(8, 10), randint(8, 10), 3))), + Image.from_numpy(data=np.ones((randint(8, 10), randint(8, 10), 3))), ), annotations=[ self._generate_random_annotation(n_of_labels=n_of_labels) @@ -199,7 +199,7 @@ def test_can_save_dataset_with_image_info(self, test_dir): [ { "annotations": 2, - "media": Image(path="1.jpg", size=(10, 15)), + "media": Image.from_file(path="1.jpg", size=(10, 15)), }, ] ) @@ -220,7 +220,7 @@ def test_can_load_dataset_with_exact_image_info(self, test_dir): [ { "annotations": 2, - "media": Image(path="1.jpg", size=(10, 15)), + "media": Image.from_file(path="1.jpg", size=(10, 15)), }, ] ) @@ -251,9 +251,15 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self, test_dir): def test_relative_paths(self, save_media, test_dir): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", subset="train", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", subset="train", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", subset="train", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem( + id="1", subset="train", media=Image.from_numpy(data=np.ones((4, 2, 3))) + ), + DatasetItem( + id="subdir1/1", subset="train", media=Image.from_numpy(data=np.ones((2, 6, 3))) + ), + DatasetItem( + id="subdir2/1", subset="train", media=Image.from_numpy(data=np.ones((5, 4, 3))) + ), ], categories=[], ) @@ -267,12 +273,14 @@ def test_can_save_and_load_image_with_arbitrary_extension(self, test_dir): dataset = Dataset.from_iterable( [ DatasetItem( - "q/1", subset="train", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))) + "q/1", + subset="train", + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), ), DatasetItem( "a/b/c/2", subset="valid", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), ), ], categories=[], @@ -286,23 +294,23 @@ def test_can_save_and_load_image_with_arbitrary_extension(self, test_dir): def test_inplace_save_writes_only_updated_data(self, test_dir): expected = Dataset.from_iterable( [ - DatasetItem(1, subset="train", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(2, subset="train", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(1, subset="train", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem(2, subset="train", media=Image.from_numpy(data=np.ones((3, 2, 3)))), ], categories=[], ) dataset = Dataset.from_iterable( [ - DatasetItem(1, subset="train", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(2, subset="train", media=Image(path="2.jpg", size=(3, 2))), - DatasetItem(3, subset="valid", media=Image(data=np.ones((2, 2, 3)))), + DatasetItem(1, subset="train", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem(2, subset="train", media=Image.from_file(path="2.jpg", size=(3, 2))), + DatasetItem(3, subset="valid", media=Image.from_numpy(data=np.ones((2, 2, 3)))), ], categories=[], ) dataset.export(test_dir, self.CONVERTER.NAME, save_media=True) - dataset.put(DatasetItem(2, subset="train", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="train", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "valid") dataset.save(save_media=True) @@ -362,7 +370,7 @@ def _check_cant_save_with_reserved_subset_name(self, test_dir, subset): DatasetItem( id=3, subset=subset, - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), ), ], categories=["a"], @@ -400,7 +408,7 @@ def test_export_rotated_bbox(self, test_dir): DatasetItem( id=3, subset="valid", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ self._generate_random_bbox(n_of_labels=2), self._generate_random_bbox(n_of_labels=2), @@ -414,7 +422,7 @@ def test_export_rotated_bbox(self, test_dir): DatasetItem( id=3, subset="valid", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=list(expected_dataset)[0].annotations + [ self._generate_random_bbox(n_of_labels=2, rotation=30.0), @@ -488,7 +496,7 @@ def test_can_save_without_creating_annotation_file_and_load(self, test_dir): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ # Mask annotation is not supported by yolo8 formats, so should be omitted Mask(np.array([[0, 1, 1, 1, 0]]), label=0), @@ -502,7 +510,7 @@ def test_can_save_without_creating_annotation_file_and_load(self, test_dir): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), ) ], categories=categories, @@ -535,7 +543,7 @@ def test_saves_only_parentless_labels(self, test_dir): DatasetItem( id=3, subset="valid", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[anno1, anno3], ), ], @@ -564,7 +572,7 @@ def test_saves_only_parentless_labels(self, test_dir): DatasetItem( id=3, subset="valid", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[anno1, anno3], ), ], @@ -647,7 +655,7 @@ def test_export_rotated_bbox(self, test_dir): DatasetItem( id=3, subset="valid", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ self._generate_random_bbox(n_of_labels=2, rotation=30.0), ], @@ -701,7 +709,7 @@ def _generate_random_dataset(self, recipes, n_of_labels=10): subset=recipe.get("subset", "train"), media=recipe.get( "media", - Image(data=np.ones((randint(8, 10), randint(8, 10), 3))), + Image.from_numpy(data=np.ones((randint(8, 10), randint(8, 10), 3))), ), annotations=[ self._generate_random_skeleton_annotation( @@ -745,7 +753,7 @@ def _make_dataset_with_edges_and_point_labels(): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -800,7 +808,7 @@ def test_loses_some_info_on_save_load_without_meta_file(self, test_dir): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -882,7 +890,7 @@ def test_saves_only_parentless_labels(self, test_dir): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -916,7 +924,7 @@ def test_saves_only_parentless_labels(self, test_dir): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -966,7 +974,7 @@ def _generate_random_dataset(self, recipes, n_of_labels=10): subset=recipe.get("subset", "train"), media=recipe.get( "media", - Image(data=np.ones((randint(8, 10), randint(8, 10), 3))), + Image.from_numpy(data=np.ones((randint(8, 10), randint(8, 10), 3))), ), annotations=[Label(label=index)], ) @@ -982,24 +990,28 @@ def _generate_random_dataset(self, recipes, n_of_labels=10): def test_relative_paths(self, test_dir, save_media): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", subset="train", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="2", subset="train", media=Image(data=np.ones((4, 2, 3)))), + DatasetItem( + id="1", subset="train", media=Image.from_numpy(data=np.ones((4, 2, 3))) + ), + DatasetItem( + id="2", subset="train", media=Image.from_numpy(data=np.ones((4, 2, 3))) + ), DatasetItem( id="subdir1/1", subset="train", - media=Image(data=np.ones((2, 6, 3))), + media=Image.from_numpy(data=np.ones((2, 6, 3))), annotations=[Label(label=0)], ), DatasetItem( id="label_1/1", subset="train", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Label(label=1)], ), DatasetItem( id="label_1/subdir2/1", subset="train", - media=Image(data=np.ones((6, 5, 3))), + media=Image.from_numpy(data=np.ones((6, 5, 3))), annotations=[Label(label=1)], ), ], @@ -1015,27 +1027,31 @@ def test_relative_paths(self, test_dir, save_media): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id="no_label/1", subset="train", media=Image(data=np.ones((4, 2, 3))) + id="no_label/1", + subset="train", + media=Image.from_numpy(data=np.ones((4, 2, 3))), ), DatasetItem( - id="no_label/2", subset="train", media=Image(data=np.ones((4, 2, 3))) + id="no_label/2", + subset="train", + media=Image.from_numpy(data=np.ones((4, 2, 3))), ), DatasetItem( id="label_0/1", subset="train", - media=Image(data=np.ones((2, 6, 3))), + media=Image.from_numpy(data=np.ones((2, 6, 3))), annotations=[Label(label=0)], ), DatasetItem( id="label_1/1", subset="train", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Label(label=1)], ), DatasetItem( id="label_1/1.0", subset="train", - media=Image(data=np.ones((6, 5, 3))), + media=Image.from_numpy(data=np.ones((6, 5, 3))), annotations=[Label(label=1)], ), ], @@ -1051,13 +1067,13 @@ def test_can_save_and_load_image_with_arbitrary_extension(self, test_dir): DatasetItem( "label_0/q/1", subset="train", - media=Image(path="label_0/q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), annotations=[Label(label=0)], ), DatasetItem( "label_0/a/b/c/2", subset="valid", - media=Image(path="label_0/a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), annotations=[Label(label=0)], ), ], @@ -1075,13 +1091,13 @@ def test_can_save_and_load_arbitrary_number_of_labels(self, test_dir): DatasetItem( "1", subset="train", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Label(label=0), Label(label=1), Label(label=2)], ), DatasetItem( "2", subset="train", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[], ), ], @@ -1121,7 +1137,7 @@ def _asset_dataset(): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(0, 2, 4, 2, label=2), Bbox(3, 3, 2, 3, label=4), @@ -1146,7 +1162,7 @@ def test_can_import_with_exif_rotated_images(self, test_dir, image_backend): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((15, 10, 3))), + media=Image.from_numpy(data=np.ones((15, 10, 3))), annotations=[ Bbox(0, 3, 2.67, 3.0, label=2), Bbox(2, 4.5, 1.33, 4.5, label=4), @@ -1274,7 +1290,7 @@ def test_can_import_if_names_dict_has_non_sequential_keys(self, test_dir): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(0, 2, 4, 2, label=2), Bbox(3, 3, 2, 3, label=4), @@ -1339,7 +1355,7 @@ def _asset_dataset(): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Polygon([1.5, 1.0, 6.0, 1.0, 6.0, 5.0], label=2), Polygon([3.0, 1.5, 6.0, 1.5, 6.0, 7.5, 4.5, 7.5, 3.75, 3.0], label=4), @@ -1363,7 +1379,7 @@ def _asset_dataset(): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(1, 2, 3, 4, label=2, attributes=dict(rotation=30)), Bbox(3, 2, 6, 2, label=4, attributes=dict(rotation=120)), @@ -1394,7 +1410,7 @@ def _asset_dataset(): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -1449,19 +1465,19 @@ def _asset_dataset(): DatasetItem( id="label_0/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[Label(label=0)], ), DatasetItem( id="label_0/2", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[Label(label=0)], ), DatasetItem( id="label_1/3", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[Label(label=1)], ), ], @@ -1475,7 +1491,7 @@ def test_can_import_with_exif_rotated_images(self, test_dir): DatasetItem( id="label_1/3", subset="train", - media=Image(data=np.ones((15, 10, 3))), + media=Image.from_numpy(data=np.ones((15, 10, 3))), annotations=[Label(label=0)], ), ], @@ -1520,7 +1536,7 @@ def _prepare_dataset(self, path: str, anno=None) -> Dataset: DatasetItem( "a", subset="train", - media=Image(np.ones((5, 10, 3))), + media=Image.from_numpy(np.ones((5, 10, 3))), annotations=[anno], ) ], @@ -1755,7 +1771,7 @@ def _prepare_dataset(self, path: str, anno=None) -> Dataset: DatasetItem( "a", subset="train", - media=Image(np.ones((5, 10, 3))), + media=Image.from_numpy(np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -1793,7 +1809,7 @@ def _prepare_dataset_different_skeletons(self, path: str, anno=None) -> Dataset: DatasetItem( "a", subset="train", - media=Image(np.ones((5, 10, 3))), + media=Image.from_numpy(np.ones((5, 10, 3))), annotations=[ Skeleton( [ @@ -1950,7 +1966,7 @@ def test_can_parse(self, helper_tc, test_dir): DatasetItem( "test_label/a", subset="train", - media=Image(np.ones((5, 10, 3))), + media=Image.from_numpy(np.ones((5, 10, 3))), annotations=[Label(0)], ) ], diff --git a/tests/unit/test_ade20k2017_format.py b/tests/unit/test_ade20k2017_format.py index af38c40edc..19ad7735d3 100644 --- a/tests/unit/test_ade20k2017_format.py +++ b/tests/unit/test_ade20k2017_format.py @@ -34,7 +34,7 @@ def test_can_import(self): DatasetItem( id="street/1", subset="training", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Mask(image=np.array([[0, 1, 0, 0]] * 3), label=0, group=1, z_order=0, id=1), Mask(image=np.array([[0, 0, 0, 1]] * 3), label=2, group=1, z_order=1, id=1), @@ -51,7 +51,7 @@ def test_can_import(self): DatasetItem( id="2", subset="validation", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Mask(image=np.array([[0, 1, 0, 1]] * 3), label=0, id=1, z_order=0, group=1), Mask(image=np.array([[0, 0, 1, 0]] * 3), label=1, id=2, z_order=0, group=2), @@ -77,7 +77,7 @@ def test_can_import_with_meta_file(self): DatasetItem( id="street/1", subset="training", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Mask(image=np.array([[0, 1, 0, 0]] * 3), label=0, group=1, z_order=0, id=1), Mask(image=np.array([[0, 0, 0, 1]] * 3), label=2, group=1, z_order=1, id=1), diff --git a/tests/unit/test_ade20k2020_format.py b/tests/unit/test_ade20k2020_format.py index 3d545ad0d6..5bcc0d824e 100644 --- a/tests/unit/test_ade20k2020_format.py +++ b/tests/unit/test_ade20k2020_format.py @@ -34,7 +34,7 @@ def test_can_import(self): DatasetItem( id="street/1", subset="training", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Polygon( [1, 0, 1, 1, 1, 2, 1, 3, 1, 4], @@ -84,7 +84,7 @@ def test_can_import(self): DatasetItem( id="2", subset="validation", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( image=np.array([[0, 0, 1, 1, 1]] * 5), @@ -151,7 +151,7 @@ def test_can_import_with_meta_file(self): DatasetItem( id="street/1", subset="training", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Polygon( [1, 0, 1, 1, 1, 2, 1, 3, 1, 4], diff --git a/tests/unit/test_align_celeba_format.py b/tests/unit/test_align_celeba_format.py index 5fee734287..066a16419b 100644 --- a/tests/unit/test_align_celeba_format.py +++ b/tests/unit/test_align_celeba_format.py @@ -33,7 +33,7 @@ def test_can_import(self): DatasetItem( id="000001", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Label(12), Points([69, 109, 106, 113, 77, 142, 73, 152, 108, 154], label=12), @@ -52,7 +52,7 @@ def test_can_import(self): DatasetItem( id="000002", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Label(5), Points([69, 110, 107, 112, 81, 135, 70, 151, 108, 153], label=5), @@ -61,7 +61,7 @@ def test_can_import(self): DatasetItem( id="000003", subset="val", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Label(2), Points([76, 112, 104, 106, 108, 128, 74, 156, 98, 158], label=2), @@ -80,7 +80,7 @@ def test_can_import(self): DatasetItem( id="000004", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Label(10), Points([72, 113, 108, 108, 101, 138, 71, 155, 101, 151], label=10), @@ -89,7 +89,7 @@ def test_can_import(self): DatasetItem( id="000005", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Label(7), Points([66, 114, 112, 112, 86, 119, 71, 147, 104, 150], label=7), @@ -128,31 +128,31 @@ def test_can_import_with_meta_file(self): DatasetItem( id="000001", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(1)], ), DatasetItem( id="000002", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(3)], ), DatasetItem( id="000003", subset="val", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(0)], ), DatasetItem( id="000004", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(2)], ), DatasetItem( id="000005", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(6)], ), ], diff --git a/tests/unit/test_camvid_format.py b/tests/unit/test_camvid_format.py index 3cf0396f2b..2ff4c4c9aa 100644 --- a/tests/unit/test_camvid_format.py +++ b/tests/unit/test_camvid_format.py @@ -66,7 +66,7 @@ def test_can_import(self): DatasetItem( id="0001TP_008550", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -76,7 +76,7 @@ def test_can_import(self): DatasetItem( id="0001TP_008580", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -86,7 +86,7 @@ def test_can_import(self): DatasetItem( id="0001TP_006690", subset="train", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -95,7 +95,7 @@ def test_can_import(self): DatasetItem( id="0016E5_07959", subset="val", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -140,7 +140,7 @@ def __iter__(self): DatasetItem( id="a/b/1", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -164,7 +164,7 @@ def __iter__(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -181,7 +181,7 @@ def __iter__(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -207,7 +207,7 @@ def __iter__(self): [ DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -215,7 +215,7 @@ def __iter__(self): ), DatasetItem( id=2, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -237,7 +237,7 @@ def __iter__(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -260,7 +260,7 @@ def __iter__(self): DatasetItem( id="a/b/1", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), ), ] ) @@ -276,7 +276,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -295,7 +295,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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")), @@ -323,7 +323,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -341,7 +341,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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")), @@ -370,11 +370,11 @@ def __iter__(self): return iter( [ DatasetItem( - id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") ), DatasetItem( id="a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3)), ext=".bmp"), annotations=[ Mask(np.array([[0, 0, 0, 1, 0]]), label=self._label("a")), Mask(np.array([[0, 1, 1, 0, 0]]), label=self._label("b")), @@ -394,11 +394,11 @@ def __iter__(self): return iter( [ DatasetItem( - id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") ), DatasetItem( id="a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3)), ext=".bmp"), annotations=[ Mask(np.array([[1, 0, 0, 0, 1]]), label=self._label("background")), Mask(np.array([[0, 0, 0, 1, 0]]), label=self._label("a")), @@ -433,10 +433,10 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[Mask(np.ones((2, 1)), label=2)], ), - DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3)))), DatasetItem(2, subset="b"), ], categories=Camvid.make_camvid_categories( @@ -456,14 +456,14 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[Mask(np.ones((2, 1)), label=1)], ), DatasetItem(2, subset="b"), DatasetItem( 3, subset="c", - media=Image(data=np.ones((2, 2, 3))), + media=Image.from_numpy(data=np.ones((2, 2, 3))), annotations=[Mask(np.ones((2, 2)), label=0)], ), ], @@ -475,7 +475,7 @@ def test_inplace_save_writes_only_updated_data(self): ) dataset.export(path, "camvid", save_media=True) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) @@ -493,7 +493,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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), @@ -511,7 +511,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=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")), diff --git a/tests/unit/test_celeba_format.py b/tests/unit/test_celeba_format.py index 57270328ab..c839081819 100644 --- a/tests/unit/test_celeba_format.py +++ b/tests/unit/test_celeba_format.py @@ -32,7 +32,7 @@ def test_can_import(self): DatasetItem( id="000001", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label(12), Bbox(95, 71, 226, 313, label=12), @@ -52,7 +52,7 @@ def test_can_import(self): DatasetItem( id="000002", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label(5), Bbox(72, 94, 221, 306, label=5), @@ -62,7 +62,7 @@ def test_can_import(self): DatasetItem( id="000003", subset="val", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label(2), Bbox(216, 59, 91, 126, label=2), @@ -82,7 +82,7 @@ def test_can_import(self): DatasetItem( id="000004", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label(10), Bbox(622, 257, 564, 781, label=10), @@ -92,7 +92,7 @@ def test_can_import(self): DatasetItem( id="000005", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label(7), Bbox(236, 109, 120, 166, label=7), @@ -132,31 +132,31 @@ def test_can_import_with_meta_file(self): DatasetItem( id="000001", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(1)], ), DatasetItem( id="000002", subset="train", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(3)], ), DatasetItem( id="000003", subset="val", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(0)], ), DatasetItem( id="000004", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(2)], ), DatasetItem( id="000005", subset="test", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[Label(6)], ), ], diff --git a/tests/unit/test_cifar_format.py b/tests/unit/test_cifar_format.py index 86cfb11d99..186fd79014 100644 --- a/tests/unit/test_cifar_format.py +++ b/tests/unit/test_cifar_format.py @@ -25,14 +25,16 @@ def test_can_save_and_load(self): DatasetItem( id="image_2", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), - DatasetItem(id="image_3", subset="test", media=Image(data=np.ones((32, 32, 3)))), + DatasetItem( + id="image_3", subset="test", media=Image.from_numpy(data=np.ones((32, 32, 3))) + ), DatasetItem( id="image_4", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(1)], ), ], @@ -66,10 +68,14 @@ def test_can_save_and_load_with_different_image_size(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="image_1", media=Image(data=np.ones((10, 8, 3))), annotations=[Label(0)] + id="image_1", + media=Image.from_numpy(data=np.ones((10, 8, 3))), + annotations=[Label(0)], ), DatasetItem( - id="image_2", media=Image(data=np.ones((32, 32, 3))), annotations=[Label(1)] + id="image_2", + media=Image.from_numpy(data=np.ones((32, 32, 3))), + annotations=[Label(1)], ), ], categories=["dog", "cat"], @@ -87,7 +93,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), ], @@ -104,9 +110,11 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((32, 32, 3)))), DatasetItem( - id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((32, 32, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((32, 32, 3)), ext=".JPEG") + ), + DatasetItem( + id="a/b/c/2", media=Image.from_numpy(data=np.zeros((32, 32, 3)), ext=".bmp") ), ], categories=[], @@ -136,13 +144,22 @@ def test_inplace_save_writes_only_updated_data(self): expected = Dataset.from_iterable( [ DatasetItem( - 1, subset="a", media=Image(data=np.ones((2, 1, 3))), annotations=[Label(0)] + 1, + subset="a", + media=Image.from_numpy(data=np.ones((2, 1, 3))), + annotations=[Label(0)], ), DatasetItem( - 2, subset="a", media=Image(data=np.ones((3, 2, 3))), annotations=[Label(1)] + 2, + subset="a", + media=Image.from_numpy(data=np.ones((3, 2, 3))), + annotations=[Label(1)], ), DatasetItem( - 2, subset="b", media=Image(data=np.ones((2, 2, 3))), annotations=[Label(1)] + 2, + subset="b", + media=Image.from_numpy(data=np.ones((2, 2, 3))), + annotations=[Label(1)], ), ], categories=["a", "b", "c", "d"], @@ -151,13 +168,22 @@ def test_inplace_save_writes_only_updated_data(self): dataset = Dataset.from_iterable( [ DatasetItem( - 1, subset="a", media=Image(data=np.ones((2, 1, 3))), annotations=[Label(0)] + 1, + subset="a", + media=Image.from_numpy(data=np.ones((2, 1, 3))), + annotations=[Label(0)], ), DatasetItem( - 2, subset="b", media=Image(data=np.ones((2, 2, 3))), annotations=[Label(1)] + 2, + subset="b", + media=Image.from_numpy(data=np.ones((2, 2, 3))), + annotations=[Label(1)], ), DatasetItem( - 3, subset="c", media=Image(data=np.ones((2, 3, 3))), annotations=[Label(2)] + 3, + subset="c", + media=Image.from_numpy(data=np.ones((2, 3, 3))), + annotations=[Label(2)], ), ], categories=["a", "b", "c", "d"], @@ -168,7 +194,10 @@ def test_inplace_save_writes_only_updated_data(self): dataset.put( DatasetItem( - 2, subset="a", media=Image(data=np.ones((3, 2, 3))), annotations=[Label(1)] + 2, + subset="a", + media=Image.from_numpy(data=np.ones((3, 2, 3))), + annotations=[Label(1)], ) ) dataset.remove(3, "c") @@ -184,14 +213,16 @@ def test_can_save_and_load_cifar100(self): DatasetItem( id="image_2", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), - DatasetItem(id="image_3", subset="test", media=Image(data=np.ones((32, 32, 3)))), + DatasetItem( + id="image_3", subset="test", media=Image.from_numpy(data=np.ones((32, 32, 3))) + ), DatasetItem( id="image_4", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(1)], ), ], @@ -236,14 +267,16 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id="image_2", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), - DatasetItem(id="image_3", subset="test", media=Image(data=np.ones((32, 32, 3)))), + DatasetItem( + id="image_3", subset="test", media=Image.from_numpy(data=np.ones((32, 32, 3))) + ), DatasetItem( id="image_4", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(1)], ), ], @@ -271,31 +304,33 @@ def test_can_import_10(self): DatasetItem( id="image_1", subset="data_batch_1", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), DatasetItem( id="image_2", subset="test_batch", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(1)], ), DatasetItem( id="image_3", subset="test_batch", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(3)], ), DatasetItem( id="image_4", subset="test_batch", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(2)], ), DatasetItem( id="image_5", subset="test_batch", - media=Image(data=np.array([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]])), + media=Image.from_numpy( + data=np.array([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]]) + ), annotations=[Label(3)], ), ], @@ -321,37 +356,39 @@ def test_can_import_100(self): DatasetItem( id="image_1", subset="train", - media=Image(data=np.ones((7, 8, 3))), + media=Image.from_numpy(data=np.ones((7, 8, 3))), annotations=[Label(0)], ), DatasetItem( id="image_2", subset="train", - media=Image(data=np.ones((4, 5, 3))), + media=Image.from_numpy(data=np.ones((4, 5, 3))), annotations=[Label(1)], ), DatasetItem( id="image_3", subset="train", - media=Image(data=np.ones((4, 5, 3))), + media=Image.from_numpy(data=np.ones((4, 5, 3))), annotations=[Label(2)], ), DatasetItem( id="image_1", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(0)], ), DatasetItem( id="image_2", subset="test", - media=Image(data=np.ones((32, 32, 3))), + media=Image.from_numpy(data=np.ones((32, 32, 3))), annotations=[Label(1)], ), DatasetItem( id="image_3", subset="test", - media=Image(data=np.array([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]])), + media=Image.from_numpy( + data=np.array([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]]) + ), annotations=[Label(2)], ), ], diff --git a/tests/unit/test_cityscapes_format.py b/tests/unit/test_cityscapes_format.py index ab4737ecbf..c01e93d7b2 100644 --- a/tests/unit/test_cityscapes_format.py +++ b/tests/unit/test_cityscapes_format.py @@ -64,7 +64,7 @@ def test_can_import(self): DatasetItem( id="defaultcity/defaultcity_000001_000031", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=3, attributes={"is_crowd": True}), Mask( @@ -84,7 +84,7 @@ def test_can_import(self): DatasetItem( id="defaultcity/defaultcity_000001_000032", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 1, 0, 0, 0]]), @@ -99,7 +99,7 @@ def test_can_import(self): DatasetItem( id="defaultcity/defaultcity_000002_000045", subset="train", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 1, 1]]), label=3, attributes={"is_crowd": True}), Mask( @@ -113,7 +113,7 @@ def test_can_import(self): DatasetItem( id="defaultcity/defaultcity_000001_000019", subset="val", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 0, 0, 1, 1]]), label=3, attributes={"is_crowd": True}), Mask( @@ -139,7 +139,7 @@ def test_can_import_with_train_label_map(self): DatasetItem( id="defaultcity/defaultcity_000001_000031", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=19, attributes={"is_crowd": True}), Mask(np.array([[0, 0, 1, 1, 1]]), label=14, attributes={"is_crowd": True}), @@ -148,7 +148,7 @@ def test_can_import_with_train_label_map(self): DatasetItem( id="defaultcity/defaultcity_000001_000032", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=16, attributes={"is_crowd": True}), Mask(np.array([[0, 0, 1, 0, 0]]), label=3, attributes={"is_crowd": True}), @@ -158,7 +158,7 @@ def test_can_import_with_train_label_map(self): DatasetItem( id="defaultcity/defaultcity_000002_000045", subset="train", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 0, 1, 1]]), label=19, attributes={"is_crowd": True}), Mask(np.array([[0, 0, 1, 0, 0]]), label=11, attributes={"is_crowd": True}), @@ -167,7 +167,7 @@ def test_can_import_with_train_label_map(self): DatasetItem( id="defaultcity/defaultcity_000001_000019", subset="val", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 1, 1, 1, 1]]), label=19, attributes={"is_crowd": True}), ], @@ -218,7 +218,7 @@ def __iter__(self): DatasetItem( id="defaultcity_1_2", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[0, 0, 0, 1, 0]]), @@ -241,7 +241,7 @@ def __iter__(self): DatasetItem( id="defaultcity_3", subset="val", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 1, 0, 1, 1]]), @@ -273,7 +273,7 @@ def __iter__(self): [ DatasetItem( id="defaultcity_1_2", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 0]]), @@ -289,7 +289,7 @@ def __iter__(self): ), DatasetItem( id="defaultcity_1_3", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 1, 0, 1, 0]]), @@ -321,7 +321,7 @@ def __iter__(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -355,7 +355,7 @@ def __iter__(self): DatasetItem( id="a/b/1", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -389,7 +389,7 @@ def __iter__(self): DatasetItem( id="city_1_2", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), ), ] ) @@ -407,7 +407,7 @@ def test_dataset_with_source_labelmap_undefined(self): [ DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 0, 0, 1, 1]]), label=0), Mask(np.array([[0, 1, 1, 0, 0]]), label=1), @@ -421,7 +421,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -459,7 +459,7 @@ def test_dataset_with_save_dataset_meta_file(self): [ DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask(np.array([[1, 0, 0, 1, 1]]), label=0), Mask(np.array([[0, 1, 1, 0, 0]]), label=1), @@ -473,7 +473,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -517,7 +517,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -545,7 +545,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -583,7 +583,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -615,7 +615,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -653,7 +653,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -686,7 +686,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -724,7 +724,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -761,7 +761,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[1, 0, 0, 1, 1]]), @@ -799,10 +799,12 @@ class TestExtractor(TestExtractorBase): def __iter__(self): return iter( [ - DatasetItem(id="q", media=Image(path="q.JPEG", data=np.zeros((4, 3, 3)))), + DatasetItem( + id="q", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), DatasetItem( id="w", - media=Image(path="w.bmp", data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3)), ext=".bmp"), annotations=[ Mask( np.array([[1, 0, 0, 1, 0]]), @@ -842,14 +844,14 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[Mask(np.ones((2, 1)), label=2, id=1)], ), - DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3)))), DatasetItem( 2, subset="b", - media=Image(data=np.ones((2, 2, 3))), + media=Image.from_numpy(data=np.ones((2, 2, 3))), annotations=[Mask(np.ones((2, 2)), label=1, id=1)], ), ], @@ -870,19 +872,19 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[Mask(np.ones((2, 1)), label=1)], ), DatasetItem( 2, subset="b", - media=Image(data=np.ones((2, 2, 3))), + media=Image.from_numpy(data=np.ones((2, 2, 3))), annotations=[Mask(np.ones((2, 2)), label=0)], ), DatasetItem( 3, subset="c", - media=Image(data=np.ones((2, 3, 3))), + media=Image.from_numpy(data=np.ones((2, 3, 3))), annotations=[Mask(np.ones((2, 2)), label=0)], ), ], @@ -893,7 +895,7 @@ def test_inplace_save_writes_only_updated_data(self): ) dataset.export(path, "cityscapes", save_media=True) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) @@ -932,7 +934,7 @@ def __iter__(self): DatasetItem( id="a", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( np.array([[0, 1, 1, 1, 0]]), diff --git a/tests/unit/test_coco_format.py b/tests/unit/test_coco_format.py index 5b27cf6333..a92d8e532d 100644 --- a/tests/unit/test_coco_format.py +++ b/tests/unit/test_coco_format.py @@ -67,7 +67,7 @@ def test_can_import_instances(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Bbox(2, 2, 3, 1, label=1, group=1, id=1, attributes={"is_crowd": False}) @@ -76,7 +76,7 @@ def test_can_import_instances(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Polygon( @@ -130,7 +130,7 @@ def test_can_import_instances_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Bbox(2, 2, 3, 1, label=1, group=1, id=1, attributes={"is_crowd": False}) @@ -159,7 +159,7 @@ def test_can_import_instances_with_original_cat_ids(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Bbox(2, 2, 3, 1, label=2, group=1, id=1, attributes={"is_crowd": False}) @@ -183,7 +183,7 @@ def test_can_import_captions(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Caption("hello", id=1, group=1), @@ -192,7 +192,7 @@ def test_can_import_captions(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Caption("world", id=1, group=1), @@ -231,7 +231,7 @@ def test_can_import_captions_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Caption("hello", id=1, group=1), @@ -259,7 +259,7 @@ def test_can_import_labels(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Label(1, id=1, group=1), @@ -268,7 +268,7 @@ def test_can_import_labels(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Label(0, id=1, group=1), @@ -305,7 +305,7 @@ def test_can_import_labels_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Label(1, id=1, group=1), @@ -334,7 +334,7 @@ def test_can_import_keypoints(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Skeleton( @@ -350,7 +350,7 @@ def test_can_import_keypoints(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Skeleton( @@ -437,7 +437,7 @@ def test_can_import_keypoints_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Skeleton( @@ -480,7 +480,7 @@ def test_can_import_keypoints_with_original_cat_ids(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Skeleton( @@ -524,13 +524,13 @@ def test_can_import_image_info(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, ), DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, ), ] @@ -569,7 +569,7 @@ def test_can_import_image_info_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, ), ] @@ -594,7 +594,7 @@ def test_can_import_panoptic(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Mask( @@ -609,7 +609,7 @@ def test_can_import_panoptic(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Mask( @@ -661,7 +661,7 @@ def test_can_import_panoptic_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Mask( @@ -700,7 +700,7 @@ def test_can_import_panoptic_with_original_cat_ids(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Mask( @@ -730,7 +730,7 @@ def test_can_import_stuff(self): DatasetItem( id="a", subset="train", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Mask( @@ -745,7 +745,7 @@ def test_can_import_stuff(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), attributes={"id": 40}, annotations=[ Mask( @@ -784,7 +784,7 @@ def test_can_import_stuff_with_any_annotation_filename(self): DatasetItem( id="a", subset="default", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), attributes={"id": 5}, annotations=[ Mask( @@ -1113,7 +1113,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ # Bbox + single polygon Bbox(0, 1, 2, 2, label=2, group=1, id=1, attributes={"is_crowd": False}), @@ -1130,7 +1130,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ # Mask + bbox Mask( @@ -1149,7 +1149,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=3, subset="val", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ # Bbox + mask Bbox(0, 1, 2, 2, label=4, group=3, id=3, attributes={"is_crowd": True}), @@ -1174,7 +1174,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Polygon( [0, 1, 2, 1, 2, 3, 0, 3], @@ -1189,7 +1189,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( @@ -1206,7 +1206,7 @@ def test_can_save_and_load_instances(self): DatasetItem( id=3, subset="val", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( @@ -1239,7 +1239,7 @@ def test_can_save_and_load_panoptic(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( image=np.array( @@ -1256,7 +1256,7 @@ def test_can_save_and_load_panoptic(self): DatasetItem( id=2, subset="val", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( image=np.array( @@ -1310,7 +1310,7 @@ def test_can_save_and_load_stuff(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( @@ -1327,7 +1327,7 @@ def test_can_save_and_load_stuff(self): DatasetItem( id=2, subset="val", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( @@ -1354,7 +1354,7 @@ def test_can_merge_polygons_on_loading(self): [ DatasetItem( id=1, - media=Image(data=np.ones((6, 10, 3))), + media=Image.from_numpy(data=np.ones((6, 10, 3))), annotations=[ Polygon([0, 0, 4, 0, 4, 4], label=3, id=4, group=4), Polygon([5, 0, 9, 0, 5, 5], label=3, id=4, group=4), @@ -1368,7 +1368,7 @@ def test_can_merge_polygons_on_loading(self): [ DatasetItem( id=1, - media=Image(data=np.ones((6, 10, 3))), + media=Image.from_numpy(data=np.ones((6, 10, 3))), annotations=[ Mask( np.array( @@ -1410,7 +1410,7 @@ def test_can_crop_covered_segments(self): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( np.array( @@ -1437,7 +1437,7 @@ def test_can_crop_covered_segments(self): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( np.array( @@ -1498,7 +1498,7 @@ def test_can_convert_polygons_to_mask(self): [ DatasetItem( id=1, - media=Image(data=np.ones((6, 10, 3))), + media=Image.from_numpy(data=np.ones((6, 10, 3))), annotations=[ Polygon([0, 0, 4, 0, 4, 4], label=3, id=4, group=4), Polygon([5, 0, 9, 0, 5, 5], label=3, id=4, group=4), @@ -1513,7 +1513,7 @@ def test_can_convert_polygons_to_mask(self): [ DatasetItem( id=1, - media=Image(data=np.ones((6, 10, 3))), + media=Image.from_numpy(data=np.ones((6, 10, 3))), annotations=[ Mask( np.array( @@ -1556,7 +1556,7 @@ def test_can_convert_masks_to_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Mask( np.array( @@ -1582,7 +1582,7 @@ def test_can_convert_masks_to_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon( [1, 0, 3, 2, 3, 0, 1, 0], @@ -1636,7 +1636,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(0, 1, 2, 2, label=0, group=1, id=1, attributes={"is_crowd": False}), ], @@ -1645,7 +1645,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(1, 0, 2, 2, label=1, group=2, id=2, attributes={"is_crowd": False}), ], @@ -1654,7 +1654,7 @@ def test_can_save_dataset_with_cjk_categories(self): DatasetItem( id=3, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Bbox(0, 1, 2, 2, label=2, group=3, id=3, attributes={"is_crowd": False}), ], @@ -1705,7 +1705,7 @@ def test_can_save_and_load_keypoints(self): DatasetItem( id=1, subset="train", - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ # Full instance annotations: polygon + keypoints Skeleton( @@ -1746,7 +1746,7 @@ def test_can_save_and_load_keypoints(self): DatasetItem( id=1, subset="train", - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Skeleton( [Points([0, 0], [0]), Points([0, 2], [1]), Points([4, 1], [2])], @@ -1819,7 +1819,7 @@ def test_can_save_keypoints_in_skeleton_order(self): DatasetItem( id=1, subset="train", - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Skeleton( [ @@ -1859,7 +1859,7 @@ def test_can_save_keypoints_in_skeleton_order(self): DatasetItem( id=1, subset="train", - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Skeleton( [ @@ -1922,7 +1922,9 @@ def test_can_save_dataset_with_no_subsets(self): def test_can_save_dataset_with_image_info(self): expected_dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(path="1.jpg", size=(10, 15)), attributes={"id": 1}), + DatasetItem( + id=1, media=Image.from_file(path="1.jpg", size=(10, 15)), attributes={"id": 1} + ), ] ) @@ -1933,12 +1935,18 @@ def test_can_save_dataset_with_image_info(self): def test_relative_paths(self): expected_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3))), attributes={"id": 1}), DatasetItem( - id="subdir1/1", media=Image(data=np.ones((2, 6, 3))), attributes={"id": 2} + id="1", media=Image.from_numpy(data=np.ones((4, 2, 3))), attributes={"id": 1} ), DatasetItem( - id="subdir2/1", media=Image(data=np.ones((5, 4, 3))), attributes={"id": 3} + id="subdir1/1", + media=Image.from_numpy(data=np.ones((2, 6, 3))), + attributes={"id": 2}, + ), + DatasetItem( + id="subdir2/1", + media=Image.from_numpy(data=np.ones((5, 4, 3))), + attributes={"id": 3}, ), ] ) @@ -1957,12 +1965,12 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( id="q/1", - media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), attributes={"id": 1}, ), DatasetItem( id="a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), attributes={"id": 2}, ), ] @@ -1981,7 +1989,9 @@ def test_preserve_coco_ids(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id="some/name1", media=Image(data=np.ones((4, 2, 3))), attributes={"id": 40} + id="some/name1", + media=Image.from_numpy(data=np.ones((4, 2, 3))), + attributes={"id": 40}, ), ] ) @@ -2000,7 +2010,7 @@ def test_annotation_attributes(self): [ DatasetItem( id=1, - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -2025,7 +2035,7 @@ def test_auto_annotation_ids(self): [ DatasetItem( id=2, - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon([0, 0, 4, 0, 4, 4], label=0), ], @@ -2038,7 +2048,7 @@ def test_auto_annotation_ids(self): [ DatasetItem( id=2, - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -2066,7 +2076,7 @@ def test_subset_can_contain_underscore(self): DatasetItem( id=2, subset="subset_1", - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -2091,7 +2101,7 @@ def test_reindex(self): [ DatasetItem( id=2, - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon([0, 0, 4, 0, 4, 4], label=0, id=5), ], @@ -2105,7 +2115,7 @@ def test_reindex(self): [ DatasetItem( id=2, - media=Image(data=np.ones((4, 2, 3))), + media=Image.from_numpy(data=np.ones((4, 2, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -2134,7 +2144,10 @@ def test_can_save_media_in_single_dir(self): dataset = Dataset.from_iterable( [ DatasetItem( - id=1, subset="train", media=Image(data=np.ones((2, 4, 3))), attributes={"id": 1} + id=1, + subset="train", + media=Image.from_numpy(data=np.ones((2, 4, 3))), + attributes={"id": 1}, ), ] ) @@ -2153,7 +2166,10 @@ def test_can_save_media_in_separate_dirs(self): dataset = Dataset.from_iterable( [ DatasetItem( - id=1, subset="train", media=Image(data=np.ones((2, 4, 3))), attributes={"id": 1} + id=1, + subset="train", + media=Image.from_numpy(data=np.ones((2, 4, 3))), + attributes={"id": 1}, ), ] ) @@ -2172,7 +2188,7 @@ def test_inplace_save_writes_only_updated_data(self): expected = Dataset.from_iterable( [ DatasetItem(1, subset="a"), - DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3)))), DatasetItem(2, subset="b"), ] ) @@ -2182,12 +2198,12 @@ def test_inplace_save_writes_only_updated_data(self): [ DatasetItem(1, subset="a"), DatasetItem(2, subset="b"), - DatasetItem(3, subset="c", media=Image(data=np.ones((2, 2, 3)))), + DatasetItem(3, subset="c", media=Image.from_numpy(data=np.ones((2, 2, 3)))), ] ) dataset.export(path, "coco", save_media=True) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) @@ -2211,7 +2227,7 @@ def test_can_save_and_load_grouped_masks_and_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( np.array( @@ -2239,7 +2255,7 @@ def test_can_save_and_load_grouped_masks_and_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( np.array( @@ -2278,7 +2294,7 @@ def test_can_save_and_load_panoptic_with_meta_file(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( image=np.array( @@ -2295,7 +2311,7 @@ def test_can_save_and_load_panoptic_with_meta_file(self): DatasetItem( id=2, subset="val", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( image=np.array( @@ -2335,7 +2351,7 @@ def test_can_save_and_load_stuff_with_meta_file(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( @@ -2352,7 +2368,7 @@ def test_can_save_and_load_stuff_with_meta_file(self): DatasetItem( id=2, subset="val", - media=Image(data=np.ones((4, 4, 3))), + media=Image.from_numpy(data=np.ones((4, 4, 3))), annotations=[ Mask( np.array( diff --git a/tests/unit/test_common_super_resolution_format.py b/tests/unit/test_common_super_resolution_format.py index 48ee943685..87ba8b92a7 100644 --- a/tests/unit/test_common_super_resolution_format.py +++ b/tests/unit/test_common_super_resolution_format.py @@ -28,19 +28,19 @@ def test_can_import(self): [ DatasetItem( id="1", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ - SuperResolutionAnnotation(Image(data=np.ones((10, 20, 3)))), + SuperResolutionAnnotation(Image.from_numpy(data=np.ones((10, 20, 3)))), ], - attributes={"upsampled": Image(data=np.ones((10, 20, 3)))}, + attributes={"upsampled": Image.from_numpy(data=np.ones((10, 20, 3)))}, ), DatasetItem( id="2", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ - SuperResolutionAnnotation(Image(data=np.ones((10, 20, 3)))), + SuperResolutionAnnotation(Image.from_numpy(data=np.ones((10, 20, 3)))), ], - attributes={"upsampled": Image(data=np.ones((10, 20, 3)))}, + attributes={"upsampled": Image.from_numpy(data=np.ones((10, 20, 3)))}, ), ] ) diff --git a/tests/unit/test_compare.py b/tests/unit/test_compare.py index 5ada3488b1..53981c43f6 100644 --- a/tests/unit/test_compare.py +++ b/tests/unit/test_compare.py @@ -433,14 +433,14 @@ def test_image_comparison(self): [ DatasetItem( id=11, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(5, 6, 7, 8), ], ), DatasetItem( id=12, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(1, 2, 3, 4), Bbox(5, 6, 7, 8), @@ -448,14 +448,14 @@ def test_image_comparison(self): ), DatasetItem( id=13, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(9, 10, 11, 12), # mismatch ], ), DatasetItem( id=14, - media=Image(data=np.zeros((5, 4, 3))), + media=Image.from_numpy(data=np.zeros((5, 4, 3))), annotations=[ Bbox(1, 2, 3, 4), Bbox(5, 6, 7, 8), @@ -464,7 +464,7 @@ def test_image_comparison(self): ), DatasetItem( id=15, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Bbox(1, 2, 3, 4), Bbox(5, 6, 7, 8), @@ -478,14 +478,14 @@ def test_image_comparison(self): [ DatasetItem( id=21, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(5, 6, 7, 8), ], ), DatasetItem( id=22, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(1, 2, 3, 4), Bbox(5, 6, 7, 8), @@ -493,14 +493,14 @@ def test_image_comparison(self): ), DatasetItem( id=23, - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox(10, 10, 11, 12), # mismatch ], ), DatasetItem( id=24, - media=Image(data=np.zeros((5, 4, 3))), + media=Image.from_numpy(data=np.zeros((5, 4, 3))), annotations=[ Bbox(6, 6, 7, 8), # 1 ann missing, mismatch ], @@ -508,7 +508,7 @@ def test_image_comparison(self): ), DatasetItem( id=25, - media=Image(data=np.zeros((4, 4, 3))), + media=Image.from_numpy(data=np.zeros((4, 4, 3))), annotations=[ Bbox(6, 6, 7, 8), ], diff --git a/tests/unit/test_cvat_format.py b/tests/unit/test_cvat_format.py index 1e80308790..803da1cc31 100644 --- a/tests/unit/test_cvat_format.py +++ b/tests/unit/test_cvat_format.py @@ -50,7 +50,7 @@ def test_can_load_image(self): DatasetItem( id="img0", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 0, @@ -98,7 +98,7 @@ def test_can_load_image(self): DatasetItem( id="img1", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Polygon([1, 2, 3, 4, 6, 5], z_order=1, attributes={"occluded": False}), Points( @@ -161,7 +161,7 @@ def test_can_load_video(self): DatasetItem( id="frame_000010", subset="annotations", - media=Image(data=255 * np.ones((20, 25, 3))), + media=Image.from_numpy(data=255 * np.ones((20, 25, 3))), annotations=[ Bbox( 3, @@ -247,7 +247,7 @@ def test_can_load_video(self): DatasetItem( id="frame_000013", subset="annotations", - media=Image(data=255 * np.ones((20, 25, 3))), + media=Image.from_numpy(data=255 * np.ones((20, 25, 3))), annotations=[ Bbox( 7, @@ -344,7 +344,7 @@ def test_can_load_video(self): DatasetItem( id="frame_000016", subset="annotations", - media=Image(path="frame_0000016.png", size=(20, 25)), + media=Image.from_file(path="frame_0000016.png", size=(20, 25)), annotations=[ Bbox( 8, @@ -475,7 +475,7 @@ def test_can_save_and_load(self): DatasetItem( id=0, subset="s1", - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -524,7 +524,7 @@ def test_can_save_and_load(self): DatasetItem( id=2, subset="s2", - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -557,7 +557,7 @@ def test_can_save_and_load(self): ), ], ), - DatasetItem(id=3, subset="s3", media=Image(path="3.jpg", size=(2, 4))), + DatasetItem(id=3, subset="s3", media=Image.from_file(path="3.jpg", size=(2, 4))), ], categories={AnnotationType.label: src_label_cat, AnnotationType.points: src_points_cat}, ) @@ -580,7 +580,7 @@ def test_can_save_and_load(self): DatasetItem( id=0, subset="s1", - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -633,7 +633,7 @@ def test_can_save_and_load(self): DatasetItem( id=2, subset="s2", - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon( [0, 0, 4, 0, 4, 4], @@ -669,7 +669,7 @@ def test_can_save_and_load(self): DatasetItem( id=3, subset="s3", - media=Image(path="3.jpg", size=(2, 4)), + media=Image.from_file(path="3.jpg", size=(2, 4)), attributes={"frame": 0}, ), ], @@ -732,20 +732,26 @@ def test_can_allow_undeclared_attrs(self): def test_relative_paths(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((4, 2, 3)))), + DatasetItem(id="subdir1/1", media=Image.from_numpy(data=np.ones((2, 6, 3)))), + DatasetItem(id="subdir2/1", media=Image.from_numpy(data=np.ones((5, 4, 3)))), ] ) target_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3))), attributes={"frame": 0}), DatasetItem( - id="subdir1/1", media=Image(data=np.ones((2, 6, 3))), attributes={"frame": 1} + id="1", media=Image.from_numpy(data=np.ones((4, 2, 3))), attributes={"frame": 0} ), DatasetItem( - id="subdir2/1", media=Image(data=np.ones((5, 4, 3))), attributes={"frame": 2} + id="subdir1/1", + media=Image.from_numpy(data=np.ones((2, 6, 3))), + attributes={"frame": 1}, + ), + DatasetItem( + id="subdir2/1", + media=Image.from_numpy(data=np.ones((5, 4, 3))), + attributes={"frame": 2}, ), ], categories=[], @@ -772,7 +778,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): DatasetItem( id="кириллица с пробелом", subset="s1", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Label(1), ], @@ -788,7 +794,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): DatasetItem( id="кириллица с пробелом", subset="s1", - media=Image(data=np.ones((5, 10, 3))), + media=Image.from_numpy(data=np.ones((5, 10, 3))), annotations=[ Label(1), ], @@ -815,12 +821,12 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( "q/1", - media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), attributes={"frame": 1}, ), DatasetItem( "a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), attributes={"frame": 2}, ), ], @@ -842,7 +848,9 @@ def test_preserve_frame_ids(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id="some/name1", media=Image(data=np.ones((4, 2, 3))), attributes={"frame": 40} + id="some/name1", + media=Image.from_numpy(data=np.ones((4, 2, 3))), + attributes={"frame": 40}, ), ], categories=[], @@ -856,7 +864,9 @@ def test_reindex(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="some/name1", media=Image(data=np.ones((4, 2, 3))), attributes={"frame": 40} + id="some/name1", + media=Image.from_numpy(data=np.ones((4, 2, 3))), + attributes={"frame": 40}, ), ] ) @@ -864,7 +874,9 @@ def test_reindex(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id="some/name1", media=Image(data=np.ones((4, 2, 3))), attributes={"frame": 0} + id="some/name1", + media=Image.from_numpy(data=np.ones((4, 2, 3))), + attributes={"frame": 0}, ), ], categories=[], @@ -883,7 +895,7 @@ def test_inplace_save_writes_only_updated_data(self): expected = Dataset.from_iterable( [ DatasetItem(1, subset="a"), - DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3)))), DatasetItem(2, subset="b"), ], categories=[], @@ -895,12 +907,12 @@ def test_inplace_save_writes_only_updated_data(self): [ DatasetItem(1, subset="a"), DatasetItem(2, subset="b"), - DatasetItem(3, subset="c", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(3, subset="c", media=Image.from_numpy(data=np.ones((3, 2, 3)))), ] ) dataset.export(path, "cvat", save_media=True) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 96f5dd1305..4c8430ab1b 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -262,7 +262,9 @@ def test_can_detect_with_nested_folder(self): def test_can_detect_with_nested_folder_and_multiply_matches(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(data=np.ones((3, 3, 3))), annotations=[Label(2)]), + DatasetItem( + id=1, media=Image.from_numpy(data=np.ones((3, 3, 3))), annotations=[Label(2)] + ), ], categories=["a", "b", "c"], ) @@ -401,7 +403,7 @@ def test_can_export_by_string_format_name(self): def test_can_remember_export_options(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(data=np.ones((1, 2, 3)))), + DatasetItem(id=1, media=Image.from_numpy(data=np.ones((1, 2, 3)))), ], categories=["a"], ) @@ -556,16 +558,20 @@ def test_cant_join_different_categories(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_cant_join_different_image_info(self): - s1 = Dataset.from_iterable([DatasetItem(1, media=Image(path="1.png", size=(2, 4)))]) - s2 = Dataset.from_iterable([DatasetItem(1, media=Image(path="1.png", size=(4, 2)))]) + s1 = Dataset.from_iterable( + [DatasetItem(1, media=Image.from_file(path="1.png", size=(2, 4)))] + ) + s2 = Dataset.from_iterable( + [DatasetItem(1, media=Image.from_file(path="1.png", size=(4, 2)))] + ) with self.assertRaises(MismatchingImageInfoError): Dataset.from_extractors(s1, s2) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_cant_join_different_images(self): - s1 = Dataset.from_iterable([DatasetItem(1, media=Image(path="1.png"))]) - s2 = Dataset.from_iterable([DatasetItem(1, media=Image(path="2.png"))]) + s1 = Dataset.from_iterable([DatasetItem(1, media=Image.from_file(path="1.png"))]) + s2 = Dataset.from_iterable([DatasetItem(1, media=Image.from_file(path="2.png"))]) with self.assertRaises(MismatchingMediaPathError): Dataset.from_extractors(s1, s2) @@ -1524,7 +1530,7 @@ def test_loader(): nonlocal called called = True - dataset = Dataset.from_iterable([DatasetItem(1, media=Image(data=test_loader))]) + dataset = Dataset.from_iterable([DatasetItem(1, media=Image.from_numpy(data=test_loader))]) with TestDir() as test_dir: dataset.save(test_dir) @@ -1543,7 +1549,7 @@ def test_can_transform_labels(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_run_model(self): dataset = Dataset.from_iterable( - [DatasetItem(i, media=Image(data=np.array([i]))) for i in range(5)], + [DatasetItem(i, media=Image.from_numpy(data=np.ones((i, i, 3)))) for i in range(5)], categories=["label"], ) @@ -1553,7 +1559,7 @@ def test_can_run_model(self): [ DatasetItem( i, - media=Image(data=np.array([i])), + media=Image.from_numpy(data=np.ones((i, i, 3))), annotations=[Label(0, attributes={"idx": i % batch_size, "data": i})], ) for i in range(5) @@ -1569,8 +1575,8 @@ def launch(self, batch, stack: bool = True): calls += 1 return [ - [Label(0, attributes={"idx": i, "data": item.media.data[0]})] - for i, item in enumerate(batch) + [Label(0, attributes={"idx": i, "data": inp.media.data.shape[0]})] + for i, inp in enumerate(batch) ] model = TestLauncher() @@ -1687,16 +1693,20 @@ def apply(self): with TestDir() as path: dataset = Dataset.from_iterable( [ - DatasetItem(1, subset="train", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(2, subset="train", media=Image(path="2.jpg", size=(3, 2))), - DatasetItem(3, subset="valid", media=Image(data=np.ones((2, 2, 3)))), + DatasetItem(1, subset="train", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem( + 2, subset="train", media=Image.from_file(path="2.jpg", size=(3, 2)) + ), + DatasetItem(3, subset="valid", media=Image.from_numpy(data=np.ones((2, 2, 3)))), ], categories=[], env=env, ) dataset.export(path, "test", save_media=True) - dataset.put(DatasetItem(2, subset="train", media=Image(data=np.ones((3, 2, 3))))) + dataset.put( + DatasetItem(2, subset="train", media=Image.from_numpy(data=np.ones((3, 2, 3)))) + ) dataset.remove(3, "valid") dataset.save(save_media=True) @@ -1997,7 +2007,7 @@ def test_can_pickle(self): DatasetItem( id=1, subset="subset", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Label(0, attributes={"a1": 1, "a2": "2"}, id=1, group=2), Caption("hello", id=1, group=5), @@ -2024,7 +2034,7 @@ def test_can_pickle(self): @mark_requirement(Requirements.DATUM_GENERIC_MEDIA) def test_can_specify_media_type_in_ctor(self): dataset = Dataset.from_iterable( - [DatasetItem(id=1, media=Image(data=np.ones((5, 4, 3))))], media_type=Video + [DatasetItem(id=1, media=Image.from_numpy(data=np.ones((5, 4, 3))))], media_type=Video ) self.assertTrue(dataset.media_type() is Video) @@ -2034,7 +2044,7 @@ def test_cant_put_item_with_mismatching_media_type(self): dataset = Dataset(media_type=Video) with self.assertRaises(MediaTypeError): - dataset.put(DatasetItem(id=1, media=Image(data=np.ones((5, 4, 3))))) + dataset.put(DatasetItem(id=1, media=Image.from_numpy(data=np.ones((5, 4, 3))))) @mark_requirement(Requirements.DATUM_GENERIC_MEDIA) def test_cant_change_media_type_with_transform(self): @@ -2061,7 +2071,7 @@ def __init__(self, **kwargs): @mark_requirement(Requirements.DATUM_GENERIC_MEDIA) def test_can_check_media_type_on_caching(self): dataset = Dataset.from_iterable( - [DatasetItem(id=1, media=Image(data=np.ones((5, 4, 3))))], media_type=Video + [DatasetItem(id=1, media=Image.from_numpy(data=np.ones((5, 4, 3))))], media_type=Video ) with self.assertRaises(MediaTypeError): @@ -2081,10 +2091,10 @@ def test_ctor_requires_id(self): def test_ctors_with_image(): for args in [ {"id": 0, "media": None}, - {"id": 0, "media": Image(path="path.jpg")}, - {"id": 0, "media": Image(data=np.array([1, 2, 3]))}, - {"id": 0, "media": Image(data=lambda f: np.array([1, 2, 3]))}, - {"id": 0, "media": Image(data=np.array([1, 2, 3]))}, + {"id": 0, "media": Image.from_file(path="path.jpg")}, + {"id": 0, "media": Image.from_numpy(data=np.array([1, 2, 3]))}, + {"id": 0, "media": Image.from_numpy(data=lambda f: np.array([1, 2, 3]))}, + {"id": 0, "media": Image.from_numpy(data=np.array([1, 2, 3]))}, ]: DatasetItem(**args) @@ -2096,7 +2106,7 @@ def test_item_representations(): item = DatasetItem( id=1, subset="subset", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Label(0, attributes={"a1": 1, "a2": "2"}, id=1, group=2), Caption("hello", id=1), diff --git a/tests/unit/test_extractor_tfds.py b/tests/unit/test_extractor_tfds.py index 53e61ec39d..e8c076e876 100644 --- a/tests/unit/test_extractor_tfds.py +++ b/tests/unit/test_extractor_tfds.py @@ -97,7 +97,7 @@ def test_can_extract_mnist(self): DatasetItem( id="0", subset="train", - media=Image(data=tfds_example["image"].numpy().squeeze(axis=2)), + media=Image.from_numpy(data=tfds_example["image"].numpy().squeeze(axis=2)), annotations=[Label(tfds_example["label"].numpy())], ), ], @@ -119,7 +119,7 @@ def _test_can_extract_cifar(self, name): DatasetItem( id=tfds_example["id"].numpy().decode("UTF-8"), subset="train", - media=Image(data=tfds_example["image"].numpy()[..., ::-1]), + media=Image.from_numpy(data=tfds_example["image"].numpy()[..., ::-1]), annotations=[Label(tfds_example["label"].numpy())], ), ], @@ -160,7 +160,7 @@ def test_can_extract_coco(self): DatasetItem( id="test", subset="train", - media=Image(data=np.ones((20, 10))), + media=Image.from_numpy(data=np.ones((20, 10))), annotations=[ Bbox(2, 2, 2, 4, label=5, attributes={"is_crowd": True}), ], @@ -240,7 +240,7 @@ def test_can_extract_voc(self): DatasetItem( id="test", subset="train", - media=Image(data=np.ones((20, 10))), + media=Image.from_numpy(data=np.ones((20, 10))), annotations=[ Bbox( 2, diff --git a/tests/unit/test_icdar_format.py b/tests/unit/test_icdar_format.py index 405400ace4..4eae038789 100644 --- a/tests/unit/test_icdar_format.py +++ b/tests/unit/test_icdar_format.py @@ -56,7 +56,7 @@ def test_can_import_captions(self): DatasetItem( id="word_1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Caption("PROPER"), ], @@ -64,7 +64,7 @@ def test_can_import_captions(self): DatasetItem( id="word_2", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Caption("Canon"), ], @@ -85,7 +85,7 @@ def test_can_import_bboxes(self): DatasetItem( id="img_1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Polygon([0, 0, 3, 1, 4, 6, 1, 7], attributes={"text": "FOOD"}), ], @@ -93,7 +93,7 @@ def test_can_import_bboxes(self): DatasetItem( id="img_2", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(0, 0, 2, 3, attributes={"text": "RED"}), Bbox(3, 3, 2, 3, attributes={"text": "LION"}), @@ -115,7 +115,7 @@ def test_can_import_masks(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Mask( group=0, @@ -188,7 +188,7 @@ def test_can_save_and_load_captions(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Caption("caption 0"), ], @@ -196,7 +196,7 @@ def test_can_save_and_load_captions(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Caption("caption_1"), ], @@ -219,7 +219,7 @@ def test_can_save_and_load_captions_with_no_save_media(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Caption("caption 0"), ], @@ -242,7 +242,7 @@ def test_can_save_and_load_bboxes(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(1, 3, 6, 10), Bbox(0, 1, 3, 5, attributes={"text": "word 0"}), @@ -251,7 +251,7 @@ def test_can_save_and_load_bboxes(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Polygon([0, 0, 3, 0, 4, 7, 1, 8], attributes={"text": "word 1"}), Polygon([1, 2, 5, 3, 6, 8, 0, 7]), @@ -260,7 +260,7 @@ def test_can_save_and_load_bboxes(self): DatasetItem( id=3, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Polygon([2, 2, 8, 3, 7, 10, 2, 9], attributes={"text": "word_2"}), Bbox(0, 2, 5, 9, attributes={"text": "word_3"}), @@ -284,7 +284,7 @@ def test_can_save_and_load_bboxes_with_no_save_media(self): DatasetItem( id=3, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Polygon([2, 2, 8, 3, 7, 10, 2, 9], attributes={"text": "word_2"}), Bbox(0, 2, 5, 9, attributes={"text": "word_3"}), @@ -308,7 +308,7 @@ def test_can_save_and_load_masks(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 1]]), @@ -335,7 +335,7 @@ def test_can_save_and_load_masks(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 0, 0, 1]]), @@ -397,7 +397,7 @@ def test_can_save_and_load_masks_with_no_save_media(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 1]]), @@ -438,7 +438,7 @@ def test_can_save_and_load_with_no_subsets(self): [ DatasetItem( id=1, - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 1, 3, 5), ], @@ -457,7 +457,11 @@ def test_can_save_and_load_with_no_subsets(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): expected_dataset = Dataset.from_iterable( - [DatasetItem(id="кириллица с пробелом", media=Image(data=np.ones((8, 8, 3))))] + [ + DatasetItem( + id="кириллица с пробелом", media=Image.from_numpy(data=np.ones((8, 8, 3))) + ) + ] ) for importer, converter in [ @@ -478,9 +482,11 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): expected = Dataset.from_iterable( [ - DatasetItem(id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3)))), DatasetItem( - id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="a/b/c/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") ), ] ) @@ -504,7 +510,9 @@ def test_can_save_and_load_captions_with_quotes(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id="1", media=Image(data=np.ones((5, 5, 3))), annotations=[Caption('caption"')] + id="1", + media=Image.from_numpy(data=np.ones((5, 5, 3))), + annotations=[Caption('caption"')], ) ] ) @@ -524,7 +532,7 @@ def test_can_save_and_load_segm_wo_color_attribute(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 1]]), @@ -556,7 +564,7 @@ def test_can_save_and_load_segm_wo_color_attribute(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 1]]), diff --git a/tests/unit/test_image_dir_format.py b/tests/unit/test_image_dir_format.py index a95e6633ed..aae58bd71d 100644 --- a/tests/unit/test_image_dir_format.py +++ b/tests/unit/test_image_dir_format.py @@ -16,8 +16,8 @@ class ImageDirFormatTest(TestCase): def test_can_load(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(data=np.ones((10, 6, 3)))), - DatasetItem(id=2, media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id=1, media=Image.from_numpy(data=np.ones((10, 6, 3)))), + DatasetItem(id=2, media=Image.from_numpy(data=np.ones((5, 4, 3)))), ] ) @@ -35,9 +35,9 @@ def test_can_load(self): def test_relative_paths(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((4, 2, 3)))), + DatasetItem(id="subdir1/1", media=Image.from_numpy(data=np.ones((2, 6, 3)))), + DatasetItem(id="subdir2/1", media=Image.from_numpy(data=np.ones((5, 4, 3)))), ] ) @@ -50,7 +50,9 @@ def test_relative_paths(self): def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="кириллица с пробелом", media=Image(data=np.ones((4, 2, 3)))), + DatasetItem( + id="кириллица с пробелом", media=Image.from_numpy(data=np.ones((4, 2, 3))) + ), ] ) @@ -63,9 +65,11 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3)))), DatasetItem( - id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="a/b/c/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") ), ] ) diff --git a/tests/unit/test_image_zip_format.py b/tests/unit/test_image_zip_format.py index 6369d179b9..5308ef212f 100644 --- a/tests/unit/test_image_zip_format.py +++ b/tests/unit/test_image_zip_format.py @@ -26,8 +26,8 @@ def _test_can_save_and_load(self, source_dataset, test_dir, **kwargs): def test_can_save_and_load(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((10, 6, 3)))), - DatasetItem(id="2", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((10, 6, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((5, 4, 3)))), ] ) @@ -38,7 +38,7 @@ def test_can_save_and_load(self): def test_can_save_and_load_with_custom_archive_name(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="img_1", media=Image(data=np.ones((10, 10, 3)))), + DatasetItem(id="img_1", media=Image.from_numpy(data=np.ones((10, 10, 3)))), ] ) @@ -49,9 +49,9 @@ def test_can_save_and_load_with_custom_archive_name(self): def test_relative_paths(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((10, 10, 3)))), - DatasetItem(id="a/2", media=Image(data=np.ones((4, 5, 3)))), - DatasetItem(id="a/b/3", media=Image(data=np.ones((20, 10, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((10, 10, 3)))), + DatasetItem(id="a/2", media=Image.from_numpy(data=np.ones((4, 5, 3)))), + DatasetItem(id="a/b/3", media=Image.from_numpy(data=np.ones((20, 10, 3)))), ] ) @@ -62,8 +62,8 @@ def test_relative_paths(self): def test_can_save_and_load_custom_compresion_method(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((5, 5, 3)))), - DatasetItem(id="2", media=Image(data=np.ones((4, 3, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((5, 5, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((4, 3, 3)))), ] ) @@ -75,9 +75,10 @@ def test_can_save_and_load_with_arbitrary_extensions(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="subset/1", media=Image(data=np.ones((10, 10, 3)), path="subset/1.png") + id="subset/1", + media=Image.from_numpy(data=np.ones((10, 10, 3)), path="subset/1.png"), ), - DatasetItem(id="2", media=Image(data=np.ones((4, 5, 3)), path="2.jpg")), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((4, 5, 3)), path="2.jpg")), ] ) @@ -99,7 +100,7 @@ class ImageZipImporterTest(TestCase): @mark_requirement(Requirements.DATUM_267) def test_can_import(self): source_dataset = Dataset.from_iterable( - [DatasetItem(id="1", media=Image(data=np.ones((10, 10, 3))))] + [DatasetItem(id="1", media=Image.from_numpy(data=np.ones((10, 10, 3))))] ) zip_path = osp.join(DUMMY_DATASET_DIR, "1.zip") @@ -110,8 +111,8 @@ def test_can_import(self): def test_can_import_from_directory(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((10, 10, 3)))), - DatasetItem(id="2", media=Image(data=np.ones((5, 10, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((10, 10, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((5, 10, 3)))), ] ) diff --git a/tests/unit/test_imagenet_format.py b/tests/unit/test_imagenet_format.py index 92bac25721..a8f5bb4012 100644 --- a/tests/unit/test_imagenet_format.py +++ b/tests/unit/test_imagenet_format.py @@ -21,10 +21,14 @@ def test_can_save_and_load(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="label_0/1", media=Image(data=np.ones((8, 8, 3))), annotations=[Label(0)] + id="label_0/1", + media=Image.from_numpy(data=np.ones((8, 8, 3))), + annotations=[Label(0)], ), DatasetItem( - id="label_1/2", media=Image(data=np.ones((10, 10, 3))), annotations=[Label(1)] + id="label_1/2", + media=Image.from_numpy(data=np.ones((10, 10, 3))), + annotations=[Label(1)], ), ], categories={ @@ -46,9 +50,11 @@ def test_can_save_and_load_with_multiple_labels(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="1", media=Image(data=np.ones((8, 8, 3))), annotations=[Label(0), Label(1)] + id="1", + media=Image.from_numpy(data=np.ones((8, 8, 3))), + annotations=[Label(0), Label(1)], ), - DatasetItem(id="2", media=Image(data=np.ones((8, 8, 3)))), + DatasetItem(id="2", media=Image.from_numpy(data=np.ones((8, 8, 3)))), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -60,12 +66,16 @@ def test_can_save_and_load_with_multiple_labels(self): excepted_dataset = Dataset.from_iterable( [ DatasetItem( - id="label_0/1", media=Image(data=np.ones((8, 8, 3))), annotations=[Label(0)] + id="label_0/1", + media=Image.from_numpy(data=np.ones((8, 8, 3))), + annotations=[Label(0)], ), DatasetItem( - id="label_1/1", media=Image(data=np.ones((8, 8, 3))), annotations=[Label(1)] + id="label_1/1", + media=Image.from_numpy(data=np.ones((8, 8, 3))), + annotations=[Label(1)], ), - DatasetItem(id="no_label/2", media=Image(data=np.ones((8, 8, 3)))), + DatasetItem(id="no_label/2", media=Image.from_numpy(data=np.ones((8, 8, 3)))), ], categories=["label_0", "label_1"], ) @@ -83,7 +93,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="label_0/кириллица с пробелом", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[Label(0)], ), ], @@ -101,8 +111,12 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="no_label/a", media=Image(path="a.JPEG", data=np.zeros((4, 3, 3)))), - DatasetItem(id="no_label/b", media=Image(path="b.bmp", data=np.zeros((3, 4, 3)))), + DatasetItem( + id="no_label/a", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="no_label/b", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") + ), ], categories=[], ) @@ -125,17 +139,17 @@ def test_can_import(self): [ DatasetItem( id="label_0/label_0_1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[Label(0)], ), DatasetItem( id="label_0/label_0_2", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[Label(0)], ), DatasetItem( id="label_1/label_1_1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[Label(1)], ), ], diff --git a/tests/unit/test_imagenet_txt_format.py b/tests/unit/test_imagenet_txt_format.py index 0fe8f633bd..19eeba22e0 100644 --- a/tests/unit/test_imagenet_txt_format.py +++ b/tests/unit/test_imagenet_txt_format.py @@ -24,7 +24,7 @@ def test_can_save_and_load(self): DatasetItem( id="2", subset="train", - media=Image(data=np.zeros((8, 8, 3))), + media=Image.from_numpy(data=np.zeros((8, 8, 3))), annotations=[Label(1)], ), ], @@ -84,7 +84,7 @@ def test_can_save_and_load_with_multiple_labels(self): DatasetItem( id="2", subset="train", - media=Image(data=np.zeros((2, 8, 3))), + media=Image.from_numpy(data=np.zeros((2, 8, 3))), ), ], categories={ @@ -106,7 +106,9 @@ def test_can_save_dataset_with_no_subsets(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id="a/b/c", media=Image(data=np.zeros((8, 4, 3))), annotations=[Label(1)] + id="a/b/c", + media=Image.from_numpy(data=np.zeros((8, 4, 3))), + annotations=[Label(1)], ), ], categories={ @@ -129,7 +131,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.zeros((8, 8, 3))), + media=Image.from_numpy(data=np.zeros((8, 8, 3))), annotations=[Label(0), Label(1)], ), ], @@ -151,9 +153,11 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="a/1", media=Image(path="a/1.JPEG", data=np.zeros((4, 3, 3)))), DatasetItem( - id="b/c/d/2", media=Image(path="b/c/d/2.bmp", data=np.zeros((3, 4, 3))) + id="a/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="b/c/d/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") ), ], categories=[], @@ -180,13 +184,13 @@ def test_can_import(self): DatasetItem( id="1", subset="train", - media=Image(data=np.zeros((8, 6, 3))), + media=Image.from_numpy(data=np.zeros((8, 6, 3))), annotations=[Label(0)], ), DatasetItem( id="2", subset="train", - media=Image(data=np.zeros((2, 8, 3))), + media=Image.from_numpy(data=np.zeros((2, 8, 3))), annotations=[Label(5)], ), DatasetItem(id="3", subset="train", annotations=[Label(3)]), diff --git a/tests/unit/test_images.py b/tests/unit/test_images.py index 9a43b2fbb3..8a7c9cf24d 100644 --- a/tests/unit/test_images.py +++ b/tests/unit/test_images.py @@ -3,7 +3,7 @@ import numpy as np -from datumaro.components.media import ByteImage, Image +from datumaro.components.media import Image, ImageFromBytes from datumaro.util.image import ( encode_image, lazy_image, @@ -71,7 +71,7 @@ class ImageTest(TestCase): def test_can_report_cached_size(self): data = np.ones((5, 6, 3)) - image = Image(data=lambda _: data, size=(2, 4)) + image = Image.from_numpy(data=lambda _: data, size=(2, 4)) self.assertEqual((2, 4), image.size) @@ -84,46 +84,42 @@ def test_ctors(self): for args in [ {"data": image}, - {"data": image, "path": path}, - {"data": image, "path": path, "size": (2, 4)}, {"data": image, "ext": "png"}, {"data": image, "ext": "png", "size": (2, 4)}, - {"data": lambda p: image}, - {"data": lambda p: image, "path": "somepath"}, - {"data": lambda p: image, "ext": "jpg"}, + {"data": lambda: image}, + {"data": lambda: image, "ext": "jpg"}, {"path": path}, - {"path": path, "data": load_image}, - {"path": path, "data": load_image, "size": (2, 4)}, {"path": path, "size": (2, 4)}, ]: with self.subTest(**args): - img = Image(**args) + assert "path" not in args or "data" not in args + if "path" in args: + img = Image.from_file(**args) + else: + img = Image.from_numpy(**args) + self.assertTrue(img.has_data) np.testing.assert_array_equal(img.data, image) self.assertEqual(img.size, tuple(image.shape[:2])) with self.subTest(): - img = Image(size=(2, 4)) + img = Image.from_file(path="somepath", size=(2, 4)) self.assertEqual(img.size, (2, 4)) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_ctor_errors(self): with self.subTest("no data specified"): - with self.assertRaisesRegex(Exception, "can not be empty"): + with self.assertRaisesRegex(Exception, "Directly initalizing"): Image(ext="jpg") - with self.subTest("either path or ext"): - with self.assertRaisesRegex(Exception, "both 'path' and 'ext'"): - Image(path="somepath", ext="someext") - class BytesImageTest(TestCase): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_lazy_image_shape(self): data = encode_image(np.ones((5, 6, 3)), "png") - image_lazy = ByteImage(data=data, size=(2, 4)) - image_eager = ByteImage(data=data) + image_lazy = ImageFromBytes(data=data, size=(2, 4)) + image_eager = ImageFromBytes(data=data) self.assertEqual((2, 4), image_lazy.size) self.assertEqual((5, 6), image_eager.size) @@ -137,22 +133,16 @@ def test_ctors(self): for args in [ {"data": image_bytes}, - {"data": lambda _: image_bytes}, - {"data": lambda _: image_bytes, "ext": ".jpg"}, - {"data": image_bytes, "path": path}, - {"data": image_bytes, "path": path, "size": (2, 4)}, - {"data": image_bytes, "path": path, "size": (2, 4)}, - {"path": path}, - {"path": path, "size": (2, 4)}, + {"data": lambda: image_bytes}, + {"data": lambda: image_bytes, "ext": ".jpg"}, ]: with self.subTest(**args): - img = ByteImage(**args) + img = ImageFromBytes(**args) # pylint: disable=pointless-statement self.assertEqual("data" in args, img.has_data) if img.has_data: np.testing.assert_array_equal(img.data, image) - self.assertEqual(img.get_bytes(), image_bytes) - img.size + self.assertEqual(img.bytes, image_bytes) if "size" in args: self.assertEqual(img.size, args["size"]) if "ext" in args or "path" in args: @@ -165,14 +155,14 @@ def test_ext_detection(self): for ext in (".bmp", ".jpg", ".png"): with self.subTest(ext=ext): - image = ByteImage(data=encode_image(image_data, ext)) + image = ImageFromBytes(data=encode_image(image_data, ext)) self.assertEqual(image.ext, ext) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_ext_detection_failure(self): image_bytes = b"\xff" * 10 # invalid image - image = ByteImage(data=image_bytes) - self.assertEqual(image.ext, "") + image = ImageFromBytes(data=image_bytes) + self.assertEqual(image.ext, None) class ImageMetaTest(TestCase): diff --git a/tests/unit/test_kitti_format.py b/tests/unit/test_kitti_format.py index 895f677bb7..2c05532257 100644 --- a/tests/unit/test_kitti_format.py +++ b/tests/unit/test_kitti_format.py @@ -69,7 +69,7 @@ def test_can_import_segmentation(self): DatasetItem( id="000030_10", subset="training", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 1, 0, 0, 0]]), @@ -94,7 +94,7 @@ def test_can_import_segmentation(self): DatasetItem( id="000030_11", subset="training", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 1, 0, 0, 0]]), @@ -133,7 +133,7 @@ def test_can_import_detection(self): DatasetItem( id="000030_10", subset="training", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -158,7 +158,7 @@ def test_can_import_detection(self): DatasetItem( id="000030_11", subset="training", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -257,7 +257,7 @@ def __iter__(self): DatasetItem( id="1_2", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 0]]), @@ -282,7 +282,7 @@ def __iter__(self): DatasetItem( id="3", subset="val", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 1, 0, 1, 1]]), @@ -315,7 +315,7 @@ def test_can_save_kitti_detection(self): DatasetItem( id="1_2", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -331,7 +331,7 @@ def test_can_save_kitti_detection(self): DatasetItem( id="1_3", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -373,7 +373,7 @@ def __iter__(self): DatasetItem( id="1_2", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[0, 0, 0, 1, 0]]), @@ -415,7 +415,7 @@ def __iter__(self): [ DatasetItem( id="1_2", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 0]]), @@ -433,7 +433,7 @@ def __iter__(self): ), DatasetItem( id="1_3", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 1, 0, 1, 0]]), @@ -468,7 +468,7 @@ def __iter__(self): DatasetItem( id="data", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -502,7 +502,7 @@ def __iter__(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -537,7 +537,7 @@ def __iter__(self): DatasetItem( id="a/b/1", subset="test", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -572,7 +572,7 @@ def __iter__(self): DatasetItem( id="city_1_2", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), ), ] ) @@ -590,7 +590,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -620,7 +620,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -658,7 +658,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -686,7 +686,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -724,7 +724,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -756,7 +756,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -794,7 +794,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -827,7 +827,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -865,7 +865,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -902,7 +902,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -941,11 +941,11 @@ def __iter__(self): return iter( [ DatasetItem( - id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") ), DatasetItem( id="a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3)), ext=".bmp"), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 0]]), @@ -993,7 +993,7 @@ def __iter__(self): [ DatasetItem( id="a", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 0, 0]] * 5), @@ -1024,7 +1024,7 @@ def test_can_save_and_load_with_no_save_media_detection(self): DatasetItem( id="b", subset="val", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox( 0, @@ -1059,7 +1059,7 @@ def test_can_save_and_load_segmentation_with_unordered_labels(self): [ DatasetItem( id="a", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 0, 0]]), @@ -1095,7 +1095,7 @@ def test_can_save_and_load_segmentation_with_unordered_labels(self): [ DatasetItem( id="a", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 0, 0]]), @@ -1138,7 +1138,7 @@ def test_can_save_detection_with_score_attribute(self): DatasetItem( id="1_2", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -1154,7 +1154,7 @@ def test_can_save_detection_with_score_attribute(self): DatasetItem( id="1_3", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -1194,7 +1194,7 @@ def test_can_save_detection_with_meta_file(self): DatasetItem( id="1_2", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -1210,7 +1210,7 @@ def test_can_save_detection_with_meta_file(self): DatasetItem( id="1_3", subset="test", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -1254,7 +1254,7 @@ class SrcExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), @@ -1282,7 +1282,7 @@ class DstExtractor(TestExtractorBase): def __iter__(self): yield DatasetItem( id=1, - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[ Mask( image=np.array([[1, 0, 0, 1, 1]]), diff --git a/tests/unit/test_kitti_raw_format.py b/tests/unit/test_kitti_raw_format.py index 7c04055507..34e6c5758d 100644 --- a/tests/unit/test_kitti_raw_format.py +++ b/tests/unit/test_kitti_raw_format.py @@ -30,9 +30,15 @@ def test_can_load(self): pcd2 = osp.join(DUMMY_DATASET_DIR, "velodyne_points", "data", "0000000001.pcd") pcd3 = osp.join(DUMMY_DATASET_DIR, "velodyne_points", "data", "0000000002.pcd") - image1 = Image(path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000000.png")) - image2 = Image(path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000001.png")) - image3 = Image(path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000002.png")) + image1 = Image.from_file( + path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000000.png") + ) + image2 = Image.from_file( + path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000001.png") + ) + image3 = Image.from_file( + path=osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000002.png") + ) expected_label_cat = LabelCategories(attributes={"occluded"}) expected_label_cat.add("bus") @@ -55,7 +61,7 @@ def test_can_load(self): attributes={"occluded": False, "track_id": 2}, ), ], - media=PointCloud(pcd1, extra_images=[image1]), + media=PointCloud.from_file(pcd1, extra_images=[image1]), attributes={"frame": 0}, ), DatasetItem( @@ -69,7 +75,7 @@ def test_can_load(self): attributes={"occluded": True, "track_id": 2}, ) ], - media=PointCloud(pcd2, extra_images=[image2]), + media=PointCloud.from_file(pcd2, extra_images=[image2]), attributes={"frame": 1}, ), DatasetItem( @@ -82,7 +88,7 @@ def test_can_load(self): attributes={"occluded": False, "track_id": 3}, ) ], - media=PointCloud(pcd3, extra_images=[image3]), + media=PointCloud.from_file(pcd3, extra_images=[image3]), attributes={"frame": 2}, ), ], @@ -100,13 +106,13 @@ class KittiRawExporterTest(TestCase): pcd2 = osp.abspath(osp.join(DUMMY_DATASET_DIR, "velodyne_points", "data", "0000000001.pcd")) pcd3 = osp.abspath(osp.join(DUMMY_DATASET_DIR, "velodyne_points", "data", "0000000002.pcd")) - image1 = Image( + image1 = Image.from_file( path=osp.abspath(osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000000.png")) ) - image2 = Image( + image2 = Image.from_file( path=osp.abspath(osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000001.png")) ) - image3 = Image( + image3 = Image.from_file( path=osp.abspath(osp.join(DUMMY_DATASET_DIR, "IMAGE_00", "data", "0000000002.png")) ) @@ -144,7 +150,7 @@ def test_can_save_and_load(self): attributes={"occluded": True, "track_id": 2}, ), ], - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 0}, ), DatasetItem( @@ -169,7 +175,7 @@ def test_can_save_and_load(self): attributes={"track_id": 3}, ), ], - media=PointCloud(self.pcd3), + media=PointCloud.from_file(self.pcd3), attributes={"frame": 2}, ), ], @@ -198,10 +204,12 @@ def test_can_save_and_load(self): attributes={"occluded": True, "track_id": 2}, ), ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "velodyne_points", "data", "0000000000.pcd"), extra_images=[ - Image(path=osp.join(test_dir, "image_00", "data", "0000000000.png")) + Image.from_file( + path=osp.join(test_dir, "image_00", "data", "0000000000.png") + ) ], ), attributes={"frame": 0}, @@ -233,7 +241,7 @@ def test_can_save_and_load(self): attributes={"occluded": False, "track_id": 3}, ), ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "velodyne_points", "data", "0000000002.pcd") ), attributes={"frame": 2}, @@ -449,7 +457,7 @@ def test_can_save_and_load_arbitrary_paths(self): DatasetItem( id="a/d", annotations=[Cuboid3d(position=[1, 2, 3], label=0, attributes={"track_id": 1})], - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 3}, ), ], @@ -471,10 +479,12 @@ def test_can_save_and_load_arbitrary_paths(self): attributes={"track_id": 1, "occluded": False}, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "velodyne_points", "data", "a", "d.pcd"), extra_images=[ - Image(path=osp.join(test_dir, "image_00", "data", "a", "d.png")), + Image.from_file( + path=osp.join(test_dir, "image_00", "data", "a", "d.png") + ), ], ), attributes={"frame": 3}, @@ -500,7 +510,7 @@ def test_can_save_and_load_multiple_related_images(self): DatasetItem( id="a/d", annotations=[Cuboid3d(position=[1, 2, 3], label=0, attributes={"track_id": 1})], - media=PointCloud( + media=PointCloud.from_file( self.pcd1, extra_images=[self.image1, self.image2, self.image3] ), attributes={"frame": 3}, @@ -524,12 +534,18 @@ def test_can_save_and_load_multiple_related_images(self): attributes={"track_id": 1, "occluded": False}, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "velodyne_points", "data", "a", "d.pcd"), extra_images=[ - Image(path=osp.join(test_dir, "image_00", "data", "a", "d.png")), - Image(path=osp.join(test_dir, "image_01", "data", "a", "d.png")), - Image(path=osp.join(test_dir, "image_02", "data", "a", "d.png")), + Image.from_file( + path=osp.join(test_dir, "image_00", "data", "a", "d.png") + ), + Image.from_file( + path=osp.join(test_dir, "image_01", "data", "a", "d.png") + ), + Image.from_file( + path=osp.join(test_dir, "image_02", "data", "a", "d.png") + ), ], ), attributes={"frame": 3}, @@ -560,7 +576,7 @@ def test_inplace_save_writes_only_updated_data(self): annotations=[ Cuboid3d(position=[3.5, 9.8, 0.3], label=0, attributes={"track_id": 1}) ], - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 0}, ) ], @@ -573,7 +589,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( "frame2", annotations=[Cuboid3d(position=[1, 2, 0], label=1, attributes={"track_id": 1})], - media=PointCloud(self.pcd2, extra_images=[self.image2]), + media=PointCloud.from_file(self.pcd2, extra_images=[self.image2]), attributes={"frame": 1}, ) ) @@ -598,7 +614,7 @@ def test_can_save_and_load_with_meta_file(self): attributes={"occluded": False, "track_id": 1}, ) ], - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 0}, ), DatasetItem( @@ -628,10 +644,12 @@ def test_can_save_and_load_with_meta_file(self): attributes={"occluded": False, "track_id": 1}, ) ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "velodyne_points", "data", "0000000000.pcd"), extra_images=[ - Image(path=osp.join(test_dir, "image_00", "data", "0000000000.png")) + Image.from_file( + path=osp.join(test_dir, "image_00", "data", "0000000000.png") + ) ], ), attributes={"frame": 0}, diff --git a/tests/unit/test_labelme_format.py b/tests/unit/test_labelme_format.py index 28f39bac56..14aa37627b 100644 --- a/tests/unit/test_labelme_format.py +++ b/tests/unit/test_labelme_format.py @@ -39,7 +39,7 @@ def test_can_save_and_load(self): DatasetItem( id="dir1/1", subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox(0, 4, 4, 8, label=2, group=2), Polygon( @@ -76,7 +76,7 @@ def test_can_save_and_load(self): DatasetItem( id="dir1/1", subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -148,9 +148,11 @@ def test_can_save_and_load(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="a/1", media=Image(path="a/1.JPEG", data=np.zeros((4, 3, 3)))), DatasetItem( - id="b/c/d/2", media=Image(path="b/c/d/2.bmp", data=np.zeros((3, 4, 3))) + id="a/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="b/c/d/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") ), ], categories=[], @@ -171,7 +173,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): DatasetItem( id="кириллица с пробелом", subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[Polygon([0, 4, 4, 4, 5, 6], label=3)], ), ], @@ -183,7 +185,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): DatasetItem( id="кириллица с пробелом", subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Polygon( [0, 4, 4, 4, 5, 6], @@ -210,12 +212,12 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_relative_paths(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((4, 2, 3)))), + DatasetItem(id="subdir1/1", media=Image.from_numpy(data=np.ones((2, 6, 3)))), + DatasetItem(id="subdir2/1", media=Image.from_numpy(data=np.ones((5, 4, 3)))), DatasetItem( id="sub/dir3/1", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Mask( np.array( @@ -233,14 +235,16 @@ def test_relative_paths(self): DatasetItem( id="subdir3/1", subset="a", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox( 1, 2, 3, 4, label=0, attributes={"occluded": False, "username": "user"} ) ], ), - DatasetItem(id="subdir3/1", subset="b", media=Image(data=np.ones((4, 4, 3)))), + DatasetItem( + id="subdir3/1", subset="b", media=Image.from_numpy(data=np.ones((4, 4, 3))) + ), ], categories=["label1", "label2"], ) @@ -257,7 +261,9 @@ def test_relative_paths(self): def test_can_save_dataset_to_correct_dir_with_correct_filename(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="dir/a", media=Image(path="dir/a.JPEG", data=np.zeros((4, 3, 3)))), + DatasetItem( + id="dir/a", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".jpeg") + ), ], categories=[], ) @@ -272,7 +278,7 @@ def test_can_save_dataset_to_correct_dir_with_correct_filename(self): xml_dirpath = osp.join(test_dir, "default/dir") self.assertEqual(os.listdir(osp.join(test_dir, "default")), ["dir"]) - self.assertEqual(set(os.listdir(xml_dirpath)), {"a.xml", "a.JPEG"}) + self.assertEqual(set(os.listdir(xml_dirpath)), {"a.xml", "a.jpeg"}) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_save_and_load_with_meta_file(self): @@ -280,7 +286,7 @@ def test_save_and_load_with_meta_file(self): [ DatasetItem( id="sub/dir3/1", - media=Image(data=np.ones((3, 4, 3))), + media=Image.from_numpy(data=np.ones((3, 4, 3))), annotations=[ Mask( np.array( @@ -298,7 +304,7 @@ def test_save_and_load_with_meta_file(self): DatasetItem( id="subdir3/1", subset="a", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[ Bbox( 1, 2, 3, 4, label=0, attributes={"occluded": False, "username": "user"} @@ -356,7 +362,7 @@ def test_can_import(self): [ DatasetItem( id="example_folder/img1", - media=Image(data=img1), + media=Image.from_numpy(data=img1), annotations=[ Polygon( [43, 34, 45, 34, 45, 37, 43, 37], diff --git a/tests/unit/test_lfw_format.py b/tests/unit/test_lfw_format.py index 9cb18e2131..f5627c26a8 100644 --- a/tests/unit/test_lfw_format.py +++ b/tests/unit/test_lfw_format.py @@ -25,13 +25,13 @@ def test_can_save_and_load(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(0, attributes={"positive_pairs": ["name0/name0_0002"]})], ), DatasetItem( id="name0_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -45,13 +45,13 @@ def test_can_save_and_load(self): DatasetItem( id="name1_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(1, attributes={"positive_pairs": ["name1/name1_0002"]})], ), DatasetItem( id="name1_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 1, @@ -79,13 +79,13 @@ def test_can_save_and_load_with_no_save_media(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(0, attributes={"positive_pairs": ["name0/name0_0002"]})], ), DatasetItem( id="name0_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -99,7 +99,7 @@ def test_can_save_and_load_with_no_save_media(self): DatasetItem( id="name1_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(1, attributes={})], ), ], @@ -119,7 +119,7 @@ def test_can_save_and_load_with_landmarks(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label(0, attributes={"positive_pairs": ["name0/name0_0002"]}), Points([0, 4, 3, 3, 2, 2, 1, 0, 3, 0], label=0), @@ -128,7 +128,7 @@ def test_can_save_and_load_with_landmarks(self): DatasetItem( id="name0_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label(0), Points([0, 5, 3, 5, 2, 2, 1, 0, 3, 0], label=0), @@ -150,11 +150,13 @@ def test_can_save_and_load_with_no_subsets(self): [ DatasetItem( id="name0_0001", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(0, attributes={"positive_pairs": ["name0/name0_0002"]})], ), DatasetItem( - id="name0_0002", media=Image(data=np.ones((2, 5, 3))), annotations=[Label(0)] + id="name0_0002", + media=Image.from_numpy(data=np.ones((2, 5, 3))), + annotations=[Label(0)], ), ], categories=["name0"], @@ -172,7 +174,7 @@ def test_can_save_and_load_with_no_format_names(self): [ DatasetItem( id="a/1", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -180,11 +182,19 @@ def test_can_save_and_load_with_no_format_names(self): ) ], ), - DatasetItem(id="b/2", media=Image(data=np.ones((2, 5, 3))), annotations=[Label(0)]), - DatasetItem(id="c/3", media=Image(data=np.ones((2, 5, 3))), annotations=[Label(1)]), + DatasetItem( + id="b/2", + media=Image.from_numpy(data=np.ones((2, 5, 3))), + annotations=[Label(0)], + ), + DatasetItem( + id="c/3", + media=Image.from_numpy(data=np.ones((2, 5, 3))), + annotations=[Label(1)], + ), DatasetItem( id="d/4", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), ), ], categories=["name0", "name1"], @@ -200,10 +210,12 @@ def test_can_save_and_load_with_no_format_names(self): def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="кириллица с пробелом", media=Image(data=np.ones((2, 5, 3)))), + DatasetItem( + id="кириллица с пробелом", media=Image.from_numpy(data=np.ones((2, 5, 3))) + ), DatasetItem( id="name0_0002", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(0, attributes={"negative_pairs": ["кириллица с пробелом"]})], ), ], @@ -222,12 +234,12 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( id="a/1", - media=Image(path="a/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), annotations=[Label(0)], ), DatasetItem( id="b/c/d/2", - media=Image(path="b/c/d/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), annotations=[Label(1)], ), ], @@ -247,13 +259,13 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(0, attributes={"positive_pairs": ["name0/name0_0002"]})], ), DatasetItem( id="name0_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -267,13 +279,13 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id="name1_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[Label(1, attributes={"positive_pairs": ["name1/name1_0002"]})], ), DatasetItem( id="name1_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 1, @@ -312,7 +324,7 @@ def test_can_import(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -324,7 +336,7 @@ def test_can_import(self): DatasetItem( id="name1_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 1, @@ -338,7 +350,7 @@ def test_can_import(self): DatasetItem( id="name1_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label(1), Points([0, 5, 3, 5, 2, 2, 1, 0, 3, 0], label=1), @@ -359,7 +371,7 @@ def test_can_import_without_people_file(self): DatasetItem( id="name0_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 0, @@ -371,7 +383,7 @@ def test_can_import_without_people_file(self): DatasetItem( id="name1_0001", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label( 1, @@ -385,7 +397,7 @@ def test_can_import_without_people_file(self): DatasetItem( id="name1_0002", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), annotations=[ Label(1), Points([0, 5, 3, 5, 2, 2, 1, 0, 3, 0], label=1), diff --git a/tests/unit/test_market1501_format.py b/tests/unit/test_market1501_format.py index 34904b1bca..e8a4600e2b 100644 --- a/tests/unit/test_market1501_format.py +++ b/tests/unit/test_market1501_format.py @@ -21,7 +21,7 @@ def test_can_save_and_load(self): DatasetItem( id="0001_c2s3_000001_00", subset="query", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 1, "person_id": "0001", @@ -34,7 +34,7 @@ def test_can_save_and_load(self): DatasetItem( id="0002_c4s2_000002_00", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 3, "person_id": "0002", @@ -47,7 +47,7 @@ def test_can_save_and_load(self): DatasetItem( id="0001_c1s1_000003_00", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 0, "person_id": "0001", @@ -72,7 +72,7 @@ def test_can_save_dataset_with_no_subsets(self): [ DatasetItem( id="0001_c2s3_000001_00", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 1, "person_id": "0001", @@ -97,7 +97,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={"camera_id": 0, "person_id": "0001", "query": False}, ), ] @@ -107,7 +107,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="0001_c1s1_000000_00", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 0, "person_id": "0001", @@ -133,7 +133,7 @@ def test_can_save_dataset_with_no_save_media(self): DatasetItem( id="0001_c2s3_000001_00", subset="query", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 1, "person_id": "0001", @@ -158,7 +158,7 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( id="c/0001_c1s1_000000_00", - media=Image(path="c/0001_c1s1_0000_00.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), attributes={ "camera_id": 0, "person_id": "0001", @@ -170,7 +170,7 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): ), DatasetItem( id="a/b/0002_c2s2_000001_00", - media=Image(path="a/b/0002_c2s2_0001_00.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), attributes={ "camera_id": 1, "person_id": "0002", @@ -196,7 +196,7 @@ def test_can_save_dataset_with_no_attributes(self): DatasetItem( id="test1", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), ), ] ) @@ -224,7 +224,7 @@ def test_can_import(self): DatasetItem( id="0001_c2s3_000111_00", subset="query", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 1, "person_id": "0001", @@ -237,7 +237,7 @@ def test_can_import(self): DatasetItem( id="0001_c1s1_001051_00", subset="test", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 0, "person_id": "0001", @@ -250,7 +250,7 @@ def test_can_import(self): DatasetItem( id="0002_c1s3_000151_00", subset="train", - media=Image(data=np.ones((2, 5, 3))), + media=Image.from_numpy(data=np.ones((2, 5, 3))), attributes={ "camera_id": 0, "person_id": "0002", diff --git a/tests/unit/test_mars_format.py b/tests/unit/test_mars_format.py index 847bc2e39b..bbb2461c70 100644 --- a/tests/unit/test_mars_format.py +++ b/tests/unit/test_mars_format.py @@ -26,14 +26,14 @@ def test_can_import(self): [ DatasetItem( id="0001C1T0001F001", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), subset="train", annotations=[Label(label=2)], attributes={"person_id": "0001", "camera_id": 1, "track_id": 1, "frame_id": 1}, ), DatasetItem( id="0000C6T0101F001", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), subset="train", annotations=[Label(label=1)], attributes={ @@ -45,7 +45,7 @@ def test_can_import(self): ), DatasetItem( id="00-1C2T0081F201", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), subset="test", annotations=[Label(label=0)], attributes={ diff --git a/tests/unit/test_mnist_csv_format.py b/tests/unit/test_mnist_csv_format.py index 812f4a1c41..3e577b7a05 100644 --- a/tests/unit/test_mnist_csv_format.py +++ b/tests/unit/test_mnist_csv_format.py @@ -21,11 +21,17 @@ def test_can_save_and_load(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), - DatasetItem(id=1, subset="test", media=Image(data=np.ones((28, 28)))), + DatasetItem(id=1, subset="test", media=Image.from_numpy(data=np.ones((28, 28)))), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), ], categories={ @@ -65,8 +71,12 @@ def test_can_save_and_load_without_saving_images(self): def test_can_save_and_load_with_different_image_size(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id=0, media=Image(data=np.ones((10, 8))), annotations=[Label(0)]), - DatasetItem(id=1, media=Image(data=np.ones((4, 3))), annotations=[Label(1)]), + DatasetItem( + id=0, media=Image.from_numpy(data=np.ones((10, 8))), annotations=[Label(0)] + ), + DatasetItem( + id=1, media=Image.from_numpy(data=np.ones((4, 3))), annotations=[Label(1)] + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -87,7 +97,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(0)], ), ], @@ -108,8 +118,10 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((28, 28)))), - DatasetItem(id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((28, 28)))), + DatasetItem(id="q/1", media=Image.from_numpy(data=np.zeros((28, 28)), ext=".JPEG")), + DatasetItem( + id="a/b/c/2", media=Image.from_numpy(data=np.zeros((28, 28)), ext=".bmp") + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -145,8 +157,12 @@ def test_can_save_and_load_empty_image(self): def test_can_save_and_load_with_other_labels(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=0, media=Image(data=np.ones((28, 28))), annotations=[Label(0)]), - DatasetItem(id=1, media=Image(data=np.ones((28, 28))), annotations=[Label(1)]), + DatasetItem( + id=0, media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(0)] + ), + DatasetItem( + id=1, media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(1)] + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -166,11 +182,17 @@ def test_can_save_and_load_with_meta_file(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), - DatasetItem(id=1, subset="test", media=Image(data=np.ones((28, 28)))), + DatasetItem(id=1, subset="test", media=Image.from_numpy(data=np.ones((28, 28)))), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), ], categories={ @@ -199,24 +221,33 @@ def test_can_import(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), DatasetItem( - id=1, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(2)] + id=1, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(2)], ), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), DatasetItem( id=0, subset="train", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(5)], ), DatasetItem( id=1, subset="train", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(7)], ), ], diff --git a/tests/unit/test_mnist_format.py b/tests/unit/test_mnist_format.py index b9a93ff426..916f1708ae 100644 --- a/tests/unit/test_mnist_format.py +++ b/tests/unit/test_mnist_format.py @@ -21,11 +21,17 @@ def test_can_save_and_load(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), - DatasetItem(id=1, subset="test", media=Image(data=np.ones((28, 28)))), + DatasetItem(id=1, subset="test", media=Image.from_numpy(data=np.ones((28, 28)))), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), ], categories={ @@ -65,8 +71,12 @@ def test_can_save_and_load_without_saving_images(self): def test_can_save_and_load_with_different_image_size(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id=0, media=Image(data=np.ones((3, 4))), annotations=[Label(0)]), - DatasetItem(id=1, media=Image(data=np.ones((2, 2))), annotations=[Label(1)]), + DatasetItem( + id=0, media=Image.from_numpy(data=np.ones((3, 4))), annotations=[Label(0)] + ), + DatasetItem( + id=1, media=Image.from_numpy(data=np.ones((2, 2))), annotations=[Label(1)] + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -87,7 +97,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(0)], ), ], @@ -108,8 +118,10 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((28, 28)))), - DatasetItem(id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((28, 28)))), + DatasetItem(id="q/1", media=Image.from_numpy(data=np.zeros((28, 28)), ext=".JPEG")), + DatasetItem( + id="a/b/c/2", media=Image.from_numpy(data=np.zeros((28, 28)), ext=".bmp") + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -145,8 +157,12 @@ def test_can_save_and_load_empty_image(self): def test_can_save_and_load_with_other_labels(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=0, media=Image(data=np.ones((28, 28))), annotations=[Label(0)]), - DatasetItem(id=1, media=Image(data=np.ones((28, 28))), annotations=[Label(1)]), + DatasetItem( + id=0, media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(0)] + ), + DatasetItem( + id=1, media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(1)] + ), ], categories={ AnnotationType.label: LabelCategories.from_iterable( @@ -166,11 +182,17 @@ def test_can_save_and_load_with_meta_file(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), - DatasetItem(id=1, subset="test", media=Image(data=np.ones((28, 28)))), + DatasetItem(id=1, subset="test", media=Image.from_numpy(data=np.ones((28, 28)))), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), ], categories={ @@ -197,24 +219,33 @@ def test_can_import(self): expected_dataset = Dataset.from_iterable( [ DatasetItem( - id=0, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(0)] + id=0, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(0)], ), DatasetItem( - id=1, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(2)] + id=1, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(2)], ), DatasetItem( - id=2, subset="test", media=Image(data=np.ones((28, 28))), annotations=[Label(1)] + id=2, + subset="test", + media=Image.from_numpy(data=np.ones((28, 28))), + annotations=[Label(1)], ), DatasetItem( id=0, subset="train", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(5)], ), DatasetItem( id=1, subset="train", - media=Image(data=np.ones((28, 28))), + media=Image.from_numpy(data=np.ones((28, 28))), annotations=[Label(7)], ), ], diff --git a/tests/unit/test_mot_format.py b/tests/unit/test_mot_format.py index 25e06cfa40..1524d04b38 100644 --- a/tests/unit/test_mot_format.py +++ b/tests/unit/test_mot_format.py @@ -38,7 +38,7 @@ def test_can_save_bboxes(self): DatasetItem( id="000001", subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -66,7 +66,7 @@ def test_can_save_bboxes(self): DatasetItem( id="000002", subset="val", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(1, 2, 4, 2, label=3), ], @@ -74,7 +74,7 @@ def test_can_save_bboxes(self): DatasetItem( id="000003", subset="test", - media=Image(data=np.ones((5, 4, 3)) * 3), + media=Image.from_numpy(data=np.ones((5, 4, 3)) * 3), ), ], categories={ @@ -88,7 +88,7 @@ def test_can_save_bboxes(self): [ DatasetItem( id="000001", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -129,7 +129,7 @@ def test_can_save_bboxes(self): ), DatasetItem( id="000002", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 1, @@ -147,7 +147,7 @@ def test_can_save_bboxes(self): ), DatasetItem( id="000003", - media=Image(data=np.ones((5, 4, 3)) * 3), + media=Image.from_numpy(data=np.ones((5, 4, 3)) * 3), ), ], categories={ @@ -172,7 +172,7 @@ def test_can_save_and_load_with_no_save_media(self): [ DatasetItem( id=1, - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -190,7 +190,7 @@ def test_can_save_and_load_with_no_save_media(self): ), DatasetItem( id=2, - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 1, @@ -221,7 +221,7 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( "1", - media=Image(path="1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), annotations=[ Bbox( 0, @@ -239,7 +239,7 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): ), DatasetItem( "2", - media=Image(path="2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), ), ], categories=["a"], @@ -259,7 +259,7 @@ def test_can_save_and_load_with_meta_file(self): [ DatasetItem( id="000001", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -277,7 +277,7 @@ def test_can_save_and_load_with_meta_file(self): ), DatasetItem( id="000002", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 1, @@ -323,7 +323,7 @@ def test_can_import(self): [ DatasetItem( id="000001", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, @@ -353,7 +353,7 @@ def test_can_import_seqinfo(self): [ DatasetItem( id=1, - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox( 0, diff --git a/tests/unit/test_mots_format.py b/tests/unit/test_mots_format.py index a3b34c6925..c8237bc731 100644 --- a/tests/unit/test_mots_format.py +++ b/tests/unit/test_mots_format.py @@ -40,7 +40,7 @@ def test_can_save_masks(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ # overlapping masks, the first should be truncated # the first and third are different instances @@ -67,7 +67,7 @@ def test_can_save_masks(self): DatasetItem( id=2, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 0, 0, 0, 0]]), label=3, attributes={"track_id": 2}), ], @@ -75,7 +75,7 @@ def test_can_save_masks(self): DatasetItem( id=3, subset="b", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[0, 1, 0, 0, 0]]), label=0, attributes={"track_id": 1}), ], @@ -89,7 +89,7 @@ def test_can_save_masks(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[0, 0, 0, 1, 0]]), label=3, attributes={"track_id": 1}), Mask(np.array([[0, 0, 1, 0, 0]]), label=2, attributes={"track_id": 2}), @@ -99,7 +99,7 @@ def test_can_save_masks(self): DatasetItem( id=2, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 0, 0, 0, 0]]), label=3, attributes={"track_id": 2}), ], @@ -107,7 +107,7 @@ def test_can_save_masks(self): DatasetItem( id=3, subset="b", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[0, 1, 0, 0, 0]]), label=0, attributes={"track_id": 1}), ], @@ -131,7 +131,7 @@ def test_can_save_and_load_with_no_save_media(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=0, attributes={"track_id": 3}), Mask(np.array([[0, 0, 1, 1, 1]]), label=1, attributes={"track_id": 3}), @@ -153,7 +153,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): DatasetItem( id="кириллица с пробелом", subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 0, 0, 0, 0]]), label=0, attributes={"track_id": 2}), ], @@ -176,14 +176,14 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): [ DatasetItem( "q/1", - media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), annotations=[ Mask(np.array([[0, 1, 0, 0, 0]]), label=0, attributes={"track_id": 1}), ], ), DatasetItem( "a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), annotations=[ Mask(np.array([[0, 1, 0, 0, 0]]), label=0, attributes={"track_id": 1}), ], @@ -207,7 +207,7 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id=1, subset="a", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 1, 0, 0, 0]]), label=0, attributes={"track_id": 3}), Mask(np.array([[0, 0, 1, 1, 1]]), label=1, attributes={"track_id": 3}), @@ -240,7 +240,7 @@ def test_can_import(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[0, 0, 0, 1, 0]]), label=3, attributes={"track_id": 1}), Mask(np.array([[0, 0, 1, 0, 0]]), label=2, attributes={"track_id": 2}), @@ -250,7 +250,7 @@ def test_can_import(self): DatasetItem( id=2, subset="train", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[1, 0, 0, 0, 0]]), label=3, attributes={"track_id": 2}), ], @@ -258,7 +258,7 @@ def test_can_import(self): DatasetItem( id=3, subset="val", - media=Image(data=np.ones((5, 1))), + media=Image.from_numpy(data=np.ones((5, 1))), annotations=[ Mask(np.array([[0, 1, 0, 0, 0]]), label=0, attributes={"track_id": 1}), ], diff --git a/tests/unit/test_mpii_format.py b/tests/unit/test_mpii_format.py index b79e107846..82b7c33b1d 100644 --- a/tests/unit/test_mpii_format.py +++ b/tests/unit/test_mpii_format.py @@ -27,7 +27,7 @@ def test_can_import(self): [ DatasetItem( id="000000001", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -74,7 +74,7 @@ def test_can_import(self): ), DatasetItem( id="000000002", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -121,7 +121,7 @@ def test_can_import(self): ), DatasetItem( id="000000003", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ diff --git a/tests/unit/test_mpii_json_format.py b/tests/unit/test_mpii_json_format.py index 0873feb10d..16d52d211c 100644 --- a/tests/unit/test_mpii_json_format.py +++ b/tests/unit/test_mpii_json_format.py @@ -32,7 +32,7 @@ def test_can_import_dataset_witn_numpy_files(self): [ DatasetItem( id="000000001", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -79,7 +79,7 @@ def test_can_import_dataset_witn_numpy_files(self): ), DatasetItem( id="000000002", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -126,7 +126,7 @@ def test_can_import_dataset_witn_numpy_files(self): ), DatasetItem( id="000000003", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -270,7 +270,7 @@ def test_can_import_dataset_wo_numpy_files(self): [ DatasetItem( id="000000001", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -316,7 +316,7 @@ def test_can_import_dataset_wo_numpy_files(self): ), DatasetItem( id="000000002", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ @@ -362,7 +362,7 @@ def test_can_import_dataset_wo_numpy_files(self): ), DatasetItem( id="000000003", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Points( [ diff --git a/tests/unit/test_ndr.py b/tests/unit/test_ndr.py index 50cd7e6c89..568beefe64 100644 --- a/tests/unit/test_ndr.py +++ b/tests/unit/test_ndr.py @@ -5,6 +5,7 @@ import datumaro.plugins.ndr as ndr from datumaro.components.annotation import AnnotationType, Label, LabelCategories from datumaro.components.dataset_base import DatasetItem +from datumaro.components.errors import MediaShapeError from datumaro.components.media import Image from datumaro.components.project import Dataset @@ -40,7 +41,7 @@ def _generate_dataset(self, config, num_duplicate, dataset="classification"): idx, subset=subset, annotations=[Label(label_id)], - media=Image(data=dummy_images[idx % num_duplicate]), + media=Image.from_numpy(data=dummy_images[idx % num_duplicate]), ) ) categories = {AnnotationType.label: label_cat} @@ -97,7 +98,9 @@ def test_ndr_with_error(self): result = ndr.NDR(source, working_subset="train") len(result) - with self.assertRaisesRegex(ValueError, "unexpected number of dimensions"): + with self.assertRaisesRegex( + MediaShapeError, "An image should have 2 \(gray\) or 3 \(bgra\) dims" + ): source = self._generate_dataset(config, 10, "invalid_dimension") result = ndr.NDR(source, working_subset="train") len(result) diff --git a/tests/unit/test_nyu_depth_v2_format.py b/tests/unit/test_nyu_depth_v2_format.py index d4da607869..817a0c4751 100644 --- a/tests/unit/test_nyu_depth_v2_format.py +++ b/tests/unit/test_nyu_depth_v2_format.py @@ -28,13 +28,13 @@ def test_can_import(self): [ DatasetItem( id="1", - media=Image(data=np.ones((6, 4, 3))), - annotations=[DepthAnnotation(Image(data=np.ones((6, 4))))], + media=Image.from_numpy(data=np.ones((6, 4, 3))), + annotations=[DepthAnnotation(Image.from_numpy(data=np.ones((6, 4))))], ), DatasetItem( id="2", - media=Image(data=np.ones((4, 3, 3))), - annotations=[DepthAnnotation(Image(data=np.ones((4, 3))))], + media=Image.from_numpy(data=np.ones((4, 3, 3))), + annotations=[DepthAnnotation(Image.from_numpy(data=np.ones((4, 3))))], ), ] ) diff --git a/tests/unit/test_open_images_format.py b/tests/unit/test_open_images_format.py index f686cdaa8b..202bd4d04a 100644 --- a/tests/unit/test_open_images_format.py +++ b/tests/unit/test_open_images_format.py @@ -32,7 +32,7 @@ def test_can_save_and_load(self): DatasetItem( id="b", subset="train", - media=Image(data=np.zeros((8, 8, 3))), + media=Image.from_numpy(data=np.zeros((8, 8, 3))), annotations=[ Label(1), Label(2, attributes={"score": 0}), @@ -82,7 +82,7 @@ def test_can_save_and_load(self): DatasetItem( id="b", subset="train", - media=Image(data=np.zeros((8, 8, 3))), + media=Image.from_numpy(data=np.zeros((8, 8, 3))), annotations=[ # the converter assumes that annotations without a score # have a score of 100% @@ -156,9 +156,11 @@ def test_can_save_and_load_with_no_subsets(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem(id="a/1", media=Image(path="a/1.JPEG", data=np.zeros((4, 3, 3)))), DatasetItem( - id="b/c/d/2", media=Image(path="b/c/d/2.bmp", data=np.zeros((3, 4, 3))) + id="a/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), + DatasetItem( + id="b/c/d/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") ), ], categories=[], @@ -178,7 +180,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( "a", subset="modified", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[ Label(0, attributes={"score": 1}), Bbox(0, 0, 1, 2, label=0), @@ -188,7 +190,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( "b", subset="modified", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[ Label(1, attributes={"score": 1}), ], @@ -196,13 +198,13 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( "c", subset="removed", - media=Image(data=np.ones((3, 2, 3))), + media=Image.from_numpy(data=np.ones((3, 2, 3))), annotations=[Label(2, attributes={"score": 1})], ), DatasetItem( "d", subset="unmodified", - media=Image(data=np.ones((4, 3, 3))), + media=Image.from_numpy(data=np.ones((4, 3, 3))), annotations=[Label(3, attributes={"score": 1})], ), ], @@ -216,7 +218,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( "e", subset="new", - media=Image(data=np.ones((5, 4, 3))), + media=Image.from_numpy(data=np.ones((5, 4, 3))), annotations=[Label(1, attributes={"score": 1})], ) ) @@ -257,7 +259,7 @@ def test_can_save_and_load_without_saving_images(self): [ DatasetItem( id="a", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(1, 2, 3, 4, label=0, group=1, attributes={"score": 1.0}), Mask( @@ -285,7 +287,7 @@ def test_can_save_and_load_with_meta_file(self): [ DatasetItem( id="a", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(1, 2, 3, 4, label=0, group=1, attributes={"score": 1.0}), Mask( @@ -321,13 +323,13 @@ def test_can_import_v6(self): DatasetItem( id="a", subset="train", - media=Image(data=np.zeros((8, 6, 3))), + media=Image.from_numpy(data=np.zeros((8, 6, 3))), annotations=[Label(label=0, attributes={"score": 1})], ), DatasetItem( id="b", subset="train", - media=Image(data=np.zeros((2, 8, 3))), + media=Image.from_numpy(data=np.zeros((2, 8, 3))), annotations=[ Label(label=0, attributes={"score": 0}), Bbox(label=0, x=1.6, y=0.6, w=6.4, h=0.4, group=1, attributes={"score": 1}), @@ -345,7 +347,7 @@ def test_can_import_v6(self): DatasetItem( id="c", subset="test", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), annotations=[ Label(label=1, attributes={"score": 1}), Label(label=3, attributes={"score": 1}), @@ -370,7 +372,7 @@ def test_can_import_v6(self): DatasetItem( id="d", subset="validation", - media=Image(data=np.ones((1, 5, 3))), + media=Image.from_numpy(data=np.ones((1, 5, 3))), annotations=[], ), ], @@ -398,8 +400,12 @@ def test_can_import_v6(self): def test_can_import_v5(self): expected_dataset = Dataset.from_iterable( [ - DatasetItem(id="aa", subset="train", media=Image(data=np.zeros((8, 6, 3)))), - DatasetItem(id="cc", subset="test", media=Image(data=np.ones((10, 5, 3)))), + DatasetItem( + id="aa", subset="train", media=Image.from_numpy(data=np.zeros((8, 6, 3))) + ), + DatasetItem( + id="cc", subset="test", media=Image.from_numpy(data=np.ones((10, 5, 3))) + ), ], categories=[ "/m/0", @@ -418,13 +424,13 @@ def test_can_import_without_image_ids_file(self): DatasetItem( id="a", subset="train", - media=Image(data=np.zeros((8, 6, 3))), + media=Image.from_numpy(data=np.zeros((8, 6, 3))), annotations=[Label(label=0, attributes={"score": 1})], ), DatasetItem( id="b", subset="train", - media=Image(data=np.zeros((2, 8, 3))), + media=Image.from_numpy(data=np.zeros((2, 8, 3))), annotations=[ Label(label=0, attributes={"score": 0}), Bbox(label=0, x=1.6, y=0.6, w=6.4, h=0.4, group=1, attributes={"score": 1}), @@ -442,7 +448,7 @@ def test_can_import_without_image_ids_file(self): DatasetItem( id="c", subset="test", - media=Image(data=np.ones((10, 5, 3))), + media=Image.from_numpy(data=np.ones((10, 5, 3))), annotations=[ Label(label=1, attributes={"score": 1}), Label(label=3, attributes={"score": 1}), diff --git a/tests/unit/test_ops.py b/tests/unit/test_ops.py index 1ddc7cdb61..83789c70c1 100644 --- a/tests/unit/test_ops.py +++ b/tests/unit/test_ops.py @@ -2,6 +2,7 @@ from unittest import TestCase import numpy as np +import pytest from datumaro.components.annotation import ( AnnotationType, @@ -43,14 +44,17 @@ class TestOperations(TestCase): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_mean_std(self): + np.random.seed(3000) expected_mean = [100, 50, 150] - expected_std = [20, 50, 10] + expected_std = [2, 1, 3] dataset = Dataset.from_iterable( [ DatasetItem( id=i, - media=Image(data=np.random.normal(expected_mean, expected_std, size=(h, w, 3))), + media=Image.from_numpy( + data=np.random.normal(expected_mean, expected_std, size=(h, w, 3)) + ), ) for i, (w, h) in enumerate([(3000, 100), (800, 600), (400, 200), (700, 300)]) ] @@ -59,20 +63,22 @@ def test_mean_std(self): actual_mean, actual_std = mean_std(dataset) for em, am in zip(expected_mean, actual_mean): - self.assertAlmostEqual(em, am, places=0) + assert np.allclose(em, am, atol=0.6) for estd, astd in zip(expected_std, actual_std): - self.assertAlmostEqual(estd, astd, places=0) + assert np.allclose(estd, astd, atol=0.1) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_image_stats(self): expected_mean = [100, 50, 150] - expected_std = [20, 50, 10] + expected_std = [2, 1, 3] dataset = Dataset.from_iterable( [ DatasetItem( id=i, - media=Image(data=np.random.normal(expected_mean, expected_std, size=(h, w, 3))), + media=Image.from_numpy( + data=np.random.normal(expected_mean, expected_std, size=(h, w, 3)) + ), ) for i, (w, h) in enumerate([(3000, 100), (800, 600), (400, 200), (700, 300)]) ] @@ -96,16 +102,16 @@ def test_image_stats(self): actual_mean = actual["subsets"]["default"]["image mean"][::-1] actual_std = actual["subsets"]["default"]["image std"][::-1] for em, am in zip(expected_mean, actual_mean): - self.assertAlmostEqual(em, am, places=0) + assert am == pytest.approx(em, 5e-1) for estd, astd in zip(expected_std, actual_std): - self.assertAlmostEqual(estd, astd, places=0) + assert astd == pytest.approx(estd, 1e-1) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_image_stats_with_no_image_infos(self): dataset = Dataset.from_iterable( [ - DatasetItem(id=0, media=Image(size=(10, 10))), - DatasetItem(id=1, media=Image(path="inexistent.path")), + DatasetItem(id=0, media=Image.from_file(path="somepath", size=(10, 10))), + DatasetItem(id=1, media=Image.from_file(path="inexistent.path")), DatasetItem(id=2), ] ) @@ -130,7 +136,7 @@ def test_stats(self): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Caption("hello"), Caption("world"), @@ -179,7 +185,7 @@ def test_stats(self): ), DatasetItem( id=2, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 2, @@ -212,7 +218,7 @@ def test_stats(self): ], ), DatasetItem(id=3), - DatasetItem(id="2.2", media=Image(data=np.ones((2, 4, 3)))), + DatasetItem(id="2.2", media=Image.from_numpy(data=np.ones((2, 4, 3)))), ], categories=["label_%s" % i for i in range(4)], ) @@ -402,11 +408,11 @@ def test_unique_image_count(self): dataset = Dataset.from_iterable( [ # no image data, but the same path - DatasetItem(1, subset="a", media=Image(path="1.jpg")), - DatasetItem(1, subset="b", media=Image(path="1.jpg")), + DatasetItem(1, subset="a", media=Image.from_file(path="1.jpg")), + DatasetItem(1, subset="b", media=Image.from_file(path="1.jpg")), # same images - DatasetItem(2, media=Image(data=np.array([1]))), - DatasetItem(3, media=Image(data=np.array([1]))), + DatasetItem(2, media=Image.from_numpy(data=np.ones((5, 5, 3)))), + DatasetItem(3, media=Image.from_numpy(data=np.ones((5, 5, 3)))), # no image is always a unique image DatasetItem(4), ] @@ -1025,20 +1031,20 @@ def test_can_merge_point_clouds(self): pcd1 = osp.join(dataset_dir, "ds0", "pointcloud", "frame1.pcd") pcd2 = osp.join(dataset_dir, "ds0", "pointcloud", "frame2.pcd") - image1 = Image( + image1 = Image.from_file( path=osp.join(dataset_dir, "ds0", "related_images", "frame1_pcd", "img2.png") ) - image2 = Image( + image2 = Image.from_file( path=osp.join(dataset_dir, "ds0", "related_images", "frame2_pcd", "img1.png") ) source0 = Dataset.from_iterable( [ - DatasetItem(1, media=PointCloud(path=pcd1, extra_images=[image1])), - DatasetItem(2, media=PointCloud(path=pcd1, extra_images=[image1])), - DatasetItem(3, media=PointCloud(path=pcd2)), + DatasetItem(1, media=PointCloud.from_file(path=pcd1, extra_images=[image1])), + DatasetItem(2, media=PointCloud.from_file(path=pcd1, extra_images=[image1])), + DatasetItem(3, media=PointCloud.from_file(path=pcd2)), DatasetItem(4), - DatasetItem(5, media=PointCloud(path=pcd2)), + DatasetItem(5, media=PointCloud.from_file(path=pcd2)), ], categories=[], media_type=PointCloud, @@ -1046,11 +1052,11 @@ def test_can_merge_point_clouds(self): source1 = Dataset.from_iterable( [ - DatasetItem(1, media=PointCloud(path=pcd1, extra_images=[image1])), - DatasetItem(2, media=PointCloud(path=pcd1, extra_images=[image2])), + DatasetItem(1, media=PointCloud.from_file(path=pcd1, extra_images=[image1])), + DatasetItem(2, media=PointCloud.from_file(path=pcd1, extra_images=[image2])), DatasetItem(3), - DatasetItem(4, media=PointCloud(path=pcd2)), - DatasetItem(5, media=PointCloud(path=pcd2, extra_images=[image2])), + DatasetItem(4, media=PointCloud.from_file(path=pcd2)), + DatasetItem(5, media=PointCloud.from_file(path=pcd2, extra_images=[image2])), ], categories=[], media_type=PointCloud, @@ -1058,11 +1064,13 @@ def test_can_merge_point_clouds(self): expected = Dataset.from_iterable( [ - DatasetItem(1, media=PointCloud(path=pcd1, extra_images=[image1])), - DatasetItem(2, media=PointCloud(path=pcd1, extra_images=[image1, image2])), - DatasetItem(3, media=PointCloud(path=pcd2)), - DatasetItem(4, media=PointCloud(path=pcd2)), - DatasetItem(5, media=PointCloud(path=pcd2, extra_images=[image2])), + DatasetItem(1, media=PointCloud.from_file(path=pcd1, extra_images=[image1])), + DatasetItem( + 2, media=PointCloud.from_file(path=pcd1, extra_images=[image1, image2]) + ), + DatasetItem(3, media=PointCloud.from_file(path=pcd2)), + DatasetItem(4, media=PointCloud.from_file(path=pcd2)), + DatasetItem(5, media=PointCloud.from_file(path=pcd2, extra_images=[image2])), ], categories=[], media_type=PointCloud, diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index 2cea2bda8e..a080d969d0 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -92,8 +92,12 @@ def launch(self, batch, stack: bool = True): expected = Dataset.from_iterable( [ - DatasetItem(0, media=Image(data=np.zeros([2, 2, 3])), annotations=[Label(0)]), - DatasetItem(1, media=Image(data=np.ones([2, 2, 3])), annotations=[Label(1)]), + DatasetItem( + 0, media=Image.from_numpy(data=np.zeros([2, 2, 3])), annotations=[Label(0)] + ), + DatasetItem( + 1, media=Image.from_numpy(data=np.ones([2, 2, 3])), annotations=[Label(1)] + ), ], categories=["a", "b"], ) @@ -105,8 +109,8 @@ def launch(self, batch, stack: bool = True): source_url = osp.join(test_dir, "source") source_dataset = Dataset.from_iterable( [ - DatasetItem(0, media=Image(data=np.zeros([2, 2, 3]) * 0)), - DatasetItem(1, media=Image(data=np.ones([2, 2, 3]) * 1)), + DatasetItem(0, media=Image.from_numpy(data=np.zeros([2, 2, 3]) * 0)), + DatasetItem(1, media=Image.from_numpy(data=np.ones([2, 2, 3]) * 1)), ], categories=["a", "b"], ) @@ -178,13 +182,13 @@ def test_can_import_local_source_with_relpath(self): DatasetItem( 0, subset="a", - media=Image(data=np.zeros([2, 2, 3])), + media=Image.from_numpy(data=np.zeros([2, 2, 3])), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="b", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -197,7 +201,7 @@ def test_can_import_local_source_with_relpath(self): DatasetItem( 1, subset="b", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -426,12 +430,14 @@ def test_can_redownload_source_rev_noncached(self): source_dataset = Dataset.from_iterable( [ DatasetItem( - 0, media=Image(data=np.ones((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)] + 0, + media=Image.from_numpy(data=np.ones((2, 3, 3))), + annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -463,13 +469,13 @@ def test_can_redownload_source_and_check_data_hash(self): [ DatasetItem( 0, - media=Image(data=np.zeros((2, 3, 3))), + media=Image.from_numpy(data=np.zeros((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -501,13 +507,13 @@ def test_can_use_source_from_cache_with_working_copy(self): [ DatasetItem( 0, - media=Image(data=np.zeros((2, 3, 3))), + media=Image.from_numpy(data=np.zeros((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -535,13 +541,13 @@ def test_raises_an_error_if_local_data_unknown(self): [ DatasetItem( 0, - media=Image(data=np.zeros((2, 3, 3))), + media=Image.from_numpy(data=np.zeros((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -572,13 +578,13 @@ def test_can_read_working_copy_of_source(self): [ DatasetItem( 0, - media=Image(data=np.zeros((2, 3, 3))), + media=Image.from_numpy(data=np.zeros((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((1, 2, 3))), + media=Image.from_numpy(data=np.zeros((1, 2, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -603,13 +609,13 @@ def test_can_read_current_revision_of_source(self): [ DatasetItem( 0, - media=Image(data=np.zeros((2, 3, 3))), + media=Image.from_numpy(data=np.zeros((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="s", - media=Image(data=np.zeros((1, 2, 3))), + media=Image.from_numpy(data=np.zeros((1, 2, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], @@ -1307,13 +1313,13 @@ def test_can_save_local_source_with_relpath(self): DatasetItem( 0, subset="a", - media=Image(data=np.ones((2, 3, 3))), + media=Image.from_numpy(data=np.ones((2, 3, 3))), annotations=[Bbox(1, 2, 3, 4, label=0)], ), DatasetItem( 1, subset="b", - media=Image(data=np.zeros((10, 20, 3))), + media=Image.from_numpy(data=np.zeros((10, 20, 3))), annotations=[Bbox(1, 2, 3, 4, label=1)], ), ], diff --git a/tests/unit/test_sampler.py b/tests/unit/test_sampler.py index 2476ad2944..291c221fd0 100644 --- a/tests/unit/test_sampler.py +++ b/tests/unit/test_sampler.py @@ -71,7 +71,7 @@ def _generate_classification_dataset( attr = {"scores": scores} if no_attr: attr = {} - img = Image(path=f"test/dataset/{idx}.jpg", size=(90, 90)) + img = Image.from_file(path=f"test/dataset/{idx}.jpg", size=(90, 90)) if no_img: img = None iterable.append( diff --git a/tests/unit/test_sly_pointcloud_format.py b/tests/unit/test_sly_pointcloud_format.py index 3f969c06e5..c7161a1a0a 100644 --- a/tests/unit/test_sly_pointcloud_format.py +++ b/tests/unit/test_sly_pointcloud_format.py @@ -29,10 +29,10 @@ def test_can_load(self): pcd1 = osp.join(DUMMY_DATASET_DIR, "ds0", "pointcloud", "frame1.pcd") pcd2 = osp.join(DUMMY_DATASET_DIR, "ds0", "pointcloud", "frame2.pcd") - image1 = Image( + image1 = Image.from_file( path=osp.join(DUMMY_DATASET_DIR, "ds0", "related_images", "frame1_pcd", "img2.png") ) - image2 = Image( + image2 = Image.from_file( path=osp.join(DUMMY_DATASET_DIR, "ds0", "related_images", "frame2_pcd", "img1.png") ) @@ -60,7 +60,7 @@ def test_can_load(self): attributes={"track_id": 231831, "tag1": "v12", "tag3": ""}, ), ], - media=PointCloud(pcd1, extra_images=[image1]), + media=PointCloud.from_file(pcd1, extra_images=[image1]), attributes={"frame": 0, "description": "", "tag1": "25dsd", "tag2": 65}, ), DatasetItem( @@ -73,7 +73,7 @@ def test_can_load(self): attributes={"track_id": 36, "tag1": "", "tag3": ""}, ) ], - media=PointCloud(pcd2, extra_images=[image2]), + media=PointCloud.from_file(pcd2, extra_images=[image2]), attributes={"frame": 1, "description": ""}, ), ], @@ -90,10 +90,10 @@ class PointCloudExporterTest(TestCase): pcd1 = osp.join(DUMMY_DATASET_DIR, "ds0", "pointcloud", "frame1.pcd") pcd2 = osp.join(DUMMY_DATASET_DIR, "ds0", "pointcloud", "frame2.pcd") - image1 = Image( + image1 = Image.from_file( path=osp.join(DUMMY_DATASET_DIR, "ds0", "related_images", "frame1_pcd", "img2.png") ) - image2 = Image( + image2 = Image.from_file( path=osp.join(DUMMY_DATASET_DIR, "ds0", "related_images", "frame2_pcd", "img1.png") ) @@ -137,7 +137,7 @@ def test_can_save_and_load(self): attributes={"occluded": True, "track_id": 2}, ), ], - media=PointCloud(self.pcd1), + media=PointCloud.from_file(self.pcd1), attributes={"frame": 0, "description": "zzz"}, ), DatasetItem( @@ -150,7 +150,7 @@ def test_can_save_and_load(self): attributes={"occluded": False, "track_id": 2}, ) ], - media=PointCloud(self.pcd2, extra_images=[self.image2]), + media=PointCloud.from_file(self.pcd2, extra_images=[self.image2]), attributes={"frame": 1}, ), ], @@ -181,7 +181,9 @@ def test_can_save_and_load(self): attributes={"occluded": True, "track_id": 2}, ), ], - media=PointCloud(osp.join(test_dir, "ds0", "pointcloud", "frame_1.pcd")), + media=PointCloud.from_file( + osp.join(test_dir, "ds0", "pointcloud", "frame_1.pcd") + ), attributes={"frame": 0, "description": "zzz"}, ), DatasetItem( @@ -194,10 +196,10 @@ def test_can_save_and_load(self): attributes={"occluded": False, "track_id": 2}, ), ], - media=PointCloud( + media=PointCloud.from_file( osp.join(test_dir, "ds0", "pointcloud", "frm2.pcd"), extra_images=[ - Image( + Image.from_file( path=osp.join( test_dir, "ds0", "related_images", "frm2_pcd", "img1.png" ) @@ -357,7 +359,7 @@ def test_have_arbitrary_item_ids(self): [ DatasetItem( id="a/b/c235", - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 20}, ), ], @@ -371,7 +373,9 @@ def test_have_arbitrary_item_ids(self): [ DatasetItem( id="a/b/c235", - media=PointCloud(pcd_path, extra_images=[Image(path=img_path)]), + media=PointCloud.from_file( + pcd_path, extra_images=[Image.from_file(path=img_path)] + ), attributes={"frame": 20}, ), ], @@ -403,7 +407,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( id="frame1", annotations=[Cuboid3d(id=215, position=[320.59, 979.48, 1.03], label=0)], - media=PointCloud(self.pcd1, extra_images=[self.image1]), + media=PointCloud.from_file(self.pcd1, extra_images=[self.image1]), attributes={"frame": 0}, ) ], @@ -416,7 +420,7 @@ def test_inplace_save_writes_only_updated_data(self): DatasetItem( id="frame2", annotations=[Cuboid3d(id=216, position=[0.59, 14.41, -0.61], label=1)], - media=PointCloud(self.pcd2, extra_images=[self.image2]), + media=PointCloud.from_file(self.pcd2, extra_images=[self.image2]), attributes={"frame": 1}, ) ) diff --git a/tests/unit/test_splitter.py b/tests/unit/test_splitter.py index e73495f7d6..31124a9c6b 100644 --- a/tests/unit/test_splitter.py +++ b/tests/unit/test_splitter.py @@ -53,7 +53,7 @@ def _generate_dataset(self, config): idx, subset=self._get_subset(idx), annotations=[Label(label_id, attributes=attributes)], - media=Image(data=np.ones((1, 1, 3))), + media=Image.from_numpy(data=np.ones((1, 1, 3))), ) ) else: @@ -64,7 +64,7 @@ def _generate_dataset(self, config): idx, subset=self._get_subset(idx), annotations=[Label(label_id)], - media=Image(data=np.ones((1, 1, 3))), + media=Image.from_numpy(data=np.ones((1, 1, 3))), ) ) categories = {AnnotationType.label: label_cat} diff --git a/tests/unit/test_tfrecord_format.py b/tests/unit/test_tfrecord_format.py index ba37534dc0..862182e601 100644 --- a/tests/unit/test_tfrecord_format.py +++ b/tests/unit/test_tfrecord_format.py @@ -9,7 +9,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.environment import Environment -from datumaro.components.media import ByteImage, Image +from datumaro.components.media import Image from datumaro.util.image import encode_image from datumaro.util.tf_util import check_import @@ -65,7 +65,7 @@ def test_can_save_bboxes(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox(0, 4, 4, 8, label=2), Bbox(0, 4, 4, 4, label=3), @@ -93,7 +93,7 @@ def test_can_save_masks(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((4, 5, 3))), + media=Image.from_numpy(data=np.ones((4, 5, 3))), annotations=[ Mask( image=np.array( @@ -128,7 +128,7 @@ def test_can_save_dataset_with_no_subsets(self): [ DatasetItem( id=1, - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox(2, 1, 4, 4, label=2), Bbox(4, 2, 8, 4, label=3), @@ -137,14 +137,16 @@ def test_can_save_dataset_with_no_subsets(self): ), DatasetItem( id=2, - media=Image(data=np.ones((8, 8, 3)) * 2), + media=Image.from_numpy(data=np.ones((8, 8, 3)) * 2), annotations=[ Bbox(4, 4, 4, 4, label=3), ], attributes={"source_id": ""}, ), DatasetItem( - id=3, media=Image(data=np.ones((8, 4, 3)) * 3), attributes={"source_id": ""} + id=3, + media=Image.from_numpy(data=np.ones((8, 4, 3)) * 3), + attributes={"source_id": ""}, ), ], categories={ @@ -165,7 +167,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox(2, 1, 4, 4, label=2), Bbox(4, 2, 8, 4, label=3), @@ -191,7 +193,7 @@ def test_can_save_dataset_with_image_info(self): [ DatasetItem( id="1/q.e", - media=Image(path="1/q.e", size=(10, 15)), + media=Image.from_file(path="1/q.e", size=(10, 15)), attributes={"source_id": ""}, ) ], @@ -207,12 +209,14 @@ def test_can_save_dataset_with_unknown_image_formats(self): [ DatasetItem( id=1, - media=ByteImage(data=encode_image(np.ones((5, 4, 3)), "png"), path="1/q.e"), + media=Image.from_bytes( + data=encode_image(np.ones((5, 4, 3)), "png"), path="1/q.e" + ), attributes={"source_id": ""}, ), DatasetItem( id=2, - media=ByteImage(data=encode_image(np.ones((6, 4, 3)), "png"), ext="qwe"), + media=Image.from_bytes(data=encode_image(np.ones((6, 4, 3)), "png"), ext="qwe"), attributes={"source_id": ""}, ), ], @@ -234,13 +238,13 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): DatasetItem( "q/1", subset="train", - media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))), + media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG"), attributes={"source_id": ""}, ), DatasetItem( "a/b/c/2", subset="valid", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), attributes={"source_id": ""}, ), ], @@ -261,9 +265,9 @@ def test_inplace_save_writes_only_updated_data(self): # generate initial dataset dataset = Dataset.from_iterable( [ - DatasetItem(1, subset="a", media=Image(data=np.ones((2, 3, 3)))), - DatasetItem(2, subset="b", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(3, subset="c", media=Image(data=np.ones((2, 5, 3)))), + DatasetItem(1, subset="a", media=Image.from_numpy(data=np.ones((2, 3, 3)))), + DatasetItem(2, subset="b", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem(3, subset="c", media=Image.from_numpy(data=np.ones((2, 5, 3)))), ] ) dataset.export(path, "tf_detection_api", save_media=True) @@ -271,7 +275,7 @@ def test_inplace_save_writes_only_updated_data(self): os.unlink(osp.join(path, "b.tfrecord")) os.unlink(osp.join(path, "c.tfrecord")) - dataset.put(DatasetItem(2, subset="a", media=Image(data=np.ones((3, 2, 3))))) + dataset.put(DatasetItem(2, subset="a", media=Image.from_numpy(data=np.ones((3, 2, 3))))) dataset.remove(3, "c") dataset.save(save_media=True) @@ -324,7 +328,7 @@ def test_can_import(self): DatasetItem( id=1, subset="train", - media=Image(data=np.ones((16, 16, 3))), + media=Image.from_numpy(data=np.ones((16, 16, 3))), annotations=[ Bbox(0, 4, 4, 8, label=2), Bbox(0, 4, 4, 4, label=3), @@ -335,7 +339,7 @@ def test_can_import(self): DatasetItem( id=2, subset="val", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(1, 2, 4, 2, label=3), ], @@ -344,7 +348,7 @@ def test_can_import(self): DatasetItem( id=3, subset="test", - media=Image(data=np.ones((5, 4, 3)) * 3), + media=Image.from_numpy(data=np.ones((5, 4, 3)) * 3), attributes={"source_id": "3"}, ), ], diff --git a/tests/unit/test_transforms.py b/tests/unit/test_transforms.py index 847a095b03..a97b0ac2eb 100644 --- a/tests/unit/test_transforms.py +++ b/tests/unit/test_transforms.py @@ -58,7 +58,7 @@ def test_mask_to_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Mask( np.array( @@ -80,7 +80,7 @@ def test_mask_to_polygons(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon([1, 0, 3, 2, 3, 0, 1, 0]), Polygon([5, 0, 5, 3, 8, 0, 5, 0]), @@ -98,7 +98,7 @@ def test_mask_to_polygons_small_polygons_message(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Mask( np.array( @@ -116,7 +116,7 @@ def test_mask_to_polygons_small_polygons_message(self): target_dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(data=np.zeros((5, 10, 3)))), + DatasetItem(id=1, media=Image.from_numpy(data=np.zeros((5, 10, 3)))), ] ) @@ -132,7 +132,7 @@ def test_polygons_to_masks(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Polygon([0, 0, 4, 0, 4, 4]), Polygon([5, 0, 9, 0, 5, 5]), @@ -145,7 +145,7 @@ def test_polygons_to_masks(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 10, 3))), + media=Image.from_numpy(data=np.zeros((5, 10, 3))), annotations=[ Mask( np.array( @@ -183,7 +183,7 @@ def test_crop_covered_segments(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ # The mask is partially covered by the polygon Mask( @@ -208,7 +208,7 @@ def test_crop_covered_segments(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Mask( np.array( @@ -237,7 +237,7 @@ def test_merge_instance_segments(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Mask( np.array( @@ -263,7 +263,7 @@ def test_merge_instance_segments(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Mask( np.array( @@ -325,7 +325,7 @@ def test_shapes_to_boxes(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Mask( np.array( @@ -351,7 +351,7 @@ def test_shapes_to_boxes(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Bbox(0, 0, 5, 5, id=1), Bbox(1, 1, 3, 3, id=2), @@ -369,13 +369,13 @@ def test_shapes_to_boxes(self): def test_id_from_image(self): source_dataset = Dataset.from_iterable( [ - DatasetItem(id=1, media=Image(path="path.jpg")), + DatasetItem(id=1, media=Image.from_file(path="path.jpg")), DatasetItem(id=2), ] ) target_dataset = Dataset.from_iterable( [ - DatasetItem(id="path", media=Image(path="path.jpg")), + DatasetItem(id="path", media=Image.from_file(path="path.jpg")), DatasetItem(id=2), ] ) @@ -389,7 +389,7 @@ def test_boxes_to_masks(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Bbox(0, 0, 3, 3, z_order=1), Bbox(0, 0, 3, 1, z_order=2), @@ -403,7 +403,7 @@ def test_boxes_to_masks(self): [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Mask( np.array( @@ -802,7 +802,7 @@ def test_can_resize(self): [ DatasetItem( id=i, - media=Image(data=np.ones((4, 4)) * i), + media=Image.from_numpy(data=np.ones((4, 4)) * i), annotations=[ Label(1), Bbox(1, 1, 2, 2, label=2), @@ -848,7 +848,7 @@ def test_can_resize(self): [ DatasetItem( id=i, - media=Image(data=np.ones((8, 8)) * i), + media=Image.from_numpy(data=np.ones((8, 8)) * i), annotations=[ Label(1), Bbox(2, 2, 4, 4, label=2), @@ -908,8 +908,12 @@ def test_can_use_only_1_set_of_resize_parameters(self): absolute_params = {"width": 6, "height": 2} relative_params = {"scale_x": 3, "scale_y": 0.2} - input_dataset = Dataset.from_iterable([DatasetItem(id=1, media=Image(np.ones((10, 2))))]) - expected = Dataset.from_iterable([DatasetItem(id=1, media=Image(np.ones((2, 6))))]) + input_dataset = Dataset.from_iterable( + [DatasetItem(id=1, media=Image.from_numpy(np.ones((10, 2))))] + ) + expected = Dataset.from_iterable( + [DatasetItem(id=1, media=Image.from_numpy(np.ones((2, 6))))] + ) with self.subTest(params=absolute_params): actual = transforms.ResizeTransform(input_dataset, **absolute_params) @@ -926,10 +930,10 @@ def test_can_use_only_1_set_of_resize_parameters(self): @mark_bug(Requirements.DATUM_BUG_606) def test_can_keep_image_ext_on_resize(self): - expected = Image(np.ones((8, 4)), ext="jpg") + expected = Image.from_numpy(np.ones((8, 4)), ext="jpg") dataset = Dataset.from_iterable( - [DatasetItem(id=1, media=Image(np.ones((4, 2)), ext="jpg"))] + [DatasetItem(id=1, media=Image.from_numpy(np.ones((4, 2)), ext="jpg"))] ) dataset.transform("resize", width=4, height=8) @@ -1110,7 +1114,7 @@ def test_crop_covered_segments_can_set_fully_covered_segment_behavior( [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ lower_shape, Polygon([1, 1, 4, 1, 4, 4, 1, 4], z_order=1), @@ -1126,7 +1130,7 @@ def test_crop_covered_segments_can_set_fully_covered_segment_behavior( [ DatasetItem( id=1, - media=Image(data=np.zeros((5, 5, 3))), + media=Image.from_numpy(data=np.zeros((5, 5, 3))), annotations=[ Polygon([1, 1, 4, 1, 4, 4, 1, 4], z_order=1), ], diff --git a/tests/unit/test_validator.py b/tests/unit/test_validator.py index df39649507..bd00d5a887 100644 --- a/tests/unit/test_validator.py +++ b/tests/unit/test_validator.py @@ -52,7 +52,7 @@ def setUpClass(cls): [ DatasetItem( id=1, - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Label( 1, @@ -92,7 +92,7 @@ def setUpClass(cls): ), DatasetItem( id=2, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 2, @@ -125,7 +125,7 @@ def setUpClass(cls): DatasetItem(id=3), DatasetItem( id=4, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 0, @@ -188,7 +188,7 @@ def setUpClass(cls): ), DatasetItem( id=5, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 0, @@ -223,7 +223,7 @@ def setUpClass(cls): ), DatasetItem( id=6, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 1, @@ -263,7 +263,7 @@ def setUpClass(cls): ), DatasetItem( id=7, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 1, @@ -299,7 +299,7 @@ def setUpClass(cls): ), DatasetItem( id=8, - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Label( 2, diff --git a/tests/unit/test_vgg_face2_format.py b/tests/unit/test_vgg_face2_format.py index 24e4189368..8383b2e9c7 100644 --- a/tests/unit/test_vgg_face2_format.py +++ b/tests/unit/test_vgg_face2_format.py @@ -23,7 +23,7 @@ def test_can_save_and_load(self): DatasetItem( id="label_0/1", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Points([3.2, 3.12, 4.11, 3.2, 2.11, 2.5, 3.5, 2.11, 3.8, 2.13], label=0), @@ -32,7 +32,7 @@ def test_can_save_and_load(self): DatasetItem( id="label_1/2", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Points( [4.23, 4.32, 5.34, 4.45, 3.54, 3.56, 4.52, 3.51, 4.78, 3.34], label=1 @@ -42,13 +42,13 @@ def test_can_save_and_load(self): DatasetItem( id="label_2/3", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[Label(2)], ), DatasetItem( id="label_3/4", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox(0, 2, 4, 2, label=3), Points([3.2, 3.12, 4.11, 3.2, 2.11, 2.5, 3.5, 2.11, 3.8, 2.13], label=3), @@ -57,7 +57,7 @@ def test_can_save_and_load(self): DatasetItem( id="no_label/a/5", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(2, 2, 2, 2), ], @@ -65,7 +65,7 @@ def test_can_save_and_load(self): DatasetItem( id="no_label/label_0", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), ), ], categories={ @@ -87,7 +87,7 @@ def test_can_save_dataset_with_no_subsets(self): [ DatasetItem( id="a/b/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Points( @@ -111,7 +111,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="a/кириллица с пробелом", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Points( [4.23, 4.32, 5.34, 4.45, 3.54, 3.56, 4.52, 3.51, 4.78, 3.34], label=0 @@ -134,7 +134,7 @@ def test_can_save_dataset_with_no_save_media(self): [ DatasetItem( id="label_0/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Points( @@ -158,7 +158,7 @@ def test_can_save_dataset_with_no_labels(self): [ DatasetItem( id="no_label/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2), Points([4.23, 4.32, 5.34, 4.45, 3.54, 3.56, 4.52, 3.51, 4.78, 3.34]), @@ -166,7 +166,7 @@ def test_can_save_dataset_with_no_labels(self): ), DatasetItem( id="no_label/2", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(2, 2, 4, 2), ], @@ -187,7 +187,7 @@ def test_can_save_dataset_with_wrong_number_of_points(self): [ DatasetItem( id="no_label/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Points([4.23, 4.32, 5.34, 3.51, 4.78, 3.34]), ], @@ -198,7 +198,9 @@ def test_can_save_dataset_with_wrong_number_of_points(self): target_dataset = Dataset.from_iterable( [ - DatasetItem(id="no_label/1", media=Image(data=np.ones((8, 8, 3))), annotations=[]), + DatasetItem( + id="no_label/1", media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[] + ), ], categories=[], ) @@ -213,10 +215,12 @@ def test_can_save_dataset_with_wrong_number_of_points(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem("no_label/q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3)))), + DatasetItem( + "no_label/q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") + ), DatasetItem( "a/b/c/2", - media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))), + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), annotations=[ Bbox(0, 2, 4, 2, label=0), Points( @@ -241,7 +245,7 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id="class_0/1", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Points([3.2, 3.12, 4.11, 3.2, 2.11, 2.5, 3.5, 2.11, 3.8, 2.13], label=0), @@ -250,7 +254,7 @@ def test_can_save_and_load_with_meta_file(self): DatasetItem( id="class_1/2", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Points( [4.23, 4.32, 5.34, 4.45, 3.54, 3.56, 4.52, 3.51, 4.78, 3.34], label=1 @@ -291,7 +295,7 @@ def test_can_import(self): DatasetItem( id="n000001/0001_01", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(2, 2, 1, 2, label=0), Points( @@ -302,7 +306,7 @@ def test_can_import(self): DatasetItem( id="n000002/0001_01", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(2, 4, 2, 2, label=1), Points([2.3, 4.9, 2.9, 4.93, 2.62, 4.745, 2.54, 4.45, 2.76, 4.43], label=1), @@ -311,7 +315,7 @@ def test_can_import(self): DatasetItem( id="n000002/0002_01", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(1, 3, 1, 1, label=1), Points([1.2, 3.8, 1.8, 3.82, 1.51, 3.634, 1.43, 3.34, 1.65, 3.32], label=1), @@ -320,7 +324,7 @@ def test_can_import(self): DatasetItem( id="n000003/0003_01", subset="test", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(1, 1, 1, 1, label=2), Points([0.2, 2.8, 0.8, 2.9, 0.5, 2.6, 0.4, 2.3, 0.6, 2.3], label=2), @@ -345,7 +349,7 @@ def test_can_import_specific_subset(self): DatasetItem( id="n000003/0003_01", subset="test", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox(1, 1, 1, 1, label=2), Points([0.2, 2.8, 0.8, 2.9, 0.5, 2.6, 0.4, 2.3, 0.6, 2.3], label=2), diff --git a/tests/unit/test_video.py b/tests/unit/test_video.py index 0d5d7d0f2f..82f6a61fcb 100644 --- a/tests/unit/test_video.py +++ b/tests/unit/test_video.py @@ -85,8 +85,8 @@ def test_can_skip_from_end(self, fxt_sample_video): for last_frame in video: pass - assert 2 == video.length - assert 1 == last_frame.index + assert 3 == video.length + assert 2 == last_frame.index @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped @@ -120,7 +120,9 @@ def test_can_read_frames(self, fxt_sample_video): expected = Dataset.from_iterable( [ DatasetItem( - "frame_%03d" % i, subset="train", media=Image(data=np.ones((4, 6, 3)) * i) + "frame_%03d" % i, + subset="train", + media=Image.from_numpy(data=np.ones((4, 6, 3)) * i), ) for i in range(4) ] @@ -140,13 +142,13 @@ def test_can_split_and_load(self, fxt_sample_video): expected = Dataset.from_iterable( [ - DatasetItem("frame_%06d" % i, media=Image(data=np.ones((4, 6, 3)) * i)) + DatasetItem("frame_%06d" % i, media=Image.from_numpy(data=np.ones((4, 6, 3)) * i)) for i in range(4) ] ) dataset = Dataset.import_from( - fxt_sample_video, "video_frames", start_frame=0, end_frame=4, name_pattern="frame_%06d" + fxt_sample_video, "video_frames", start_frame=0, end_frame=3, name_pattern="frame_%06d" ) dataset.export(format="image_dir", save_dir=test_dir, image_ext=".jpg") diff --git a/tests/unit/test_voc_format.py b/tests/unit/test_voc_format.py index e431a71a03..e2b9d17cd7 100644 --- a/tests/unit/test_voc_format.py +++ b/tests/unit/test_voc_format.py @@ -164,7 +164,7 @@ def __iter__(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Label(self._label(l.name)) for l in VOC.VocLabel if l.value % 2 == 1 ] @@ -217,7 +217,9 @@ def __iter__(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ] ) @@ -235,13 +237,15 @@ def __iter__(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Label(self._label(l.name)) for l in VOC.VocLabel if l.value % 2 == 1 ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ] ) @@ -271,7 +275,7 @@ def test_can_import_voc_layout_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 4.0, @@ -292,7 +296,9 @@ def test_can_import_voc_layout_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -322,7 +328,7 @@ def test_can_import_voc_detection_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 1.0, @@ -357,7 +363,9 @@ def test_can_import_voc_detection_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -386,11 +394,13 @@ def test_can_import_voc_segmentation_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[Mask(image=np.ones([10, 20]), label=2, group=1)], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -420,7 +430,7 @@ def test_can_import_voc_action_dataset(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 4.0, @@ -440,7 +450,9 @@ def test_can_import_voc_action_dataset(self): ], ), DatasetItem( - id="2007_000002", subset="test", media=Image(data=np.ones((10, 20, 3))) + id="2007_000002", + subset="test", + media=Image.from_numpy(data=np.ones((10, 20, 3))), ), ], categories=VOC.make_voc_categories(), @@ -479,7 +491,7 @@ def test_can_import_voc_dataset_with_empty_lines_in_subset_lists(self): DatasetItem( id="2007_000001", subset="train", - media=Image(data=np.ones((10, 20, 3))), + media=Image.from_numpy(data=np.ones((10, 20, 3))), annotations=[ Bbox( 1.0, @@ -1273,7 +1285,8 @@ def __iter__(self): [ DatasetItem(id="кириллица с пробелом 1"), DatasetItem( - id="кириллица с пробелом 2", media=Image(data=np.ones([4, 5, 3])) + id="кириллица с пробелом 2", + media=Image.from_numpy(data=np.ones([4, 5, 3])), ), ] ) @@ -1293,9 +1306,15 @@ class TestExtractor(TestExtractorBase): def __iter__(self): return iter( [ - DatasetItem(id=1, subset="a", media=Image(data=np.ones([4, 5, 3]))), - DatasetItem(id=2, subset="a", media=Image(data=np.ones([4, 5, 3]))), - DatasetItem(id=3, subset="b", media=Image(data=np.ones([2, 6, 3]))), + DatasetItem( + id=1, subset="a", media=Image.from_numpy(data=np.ones([4, 5, 3])) + ), + DatasetItem( + id=2, subset="a", media=Image.from_numpy(data=np.ones([4, 5, 3])) + ), + DatasetItem( + id=3, subset="b", media=Image.from_numpy(data=np.ones([2, 6, 3])) + ), ] ) @@ -1648,7 +1667,7 @@ def test_background_masks_dont_introduce_instances_but_cover_others(self): [ DatasetItem( 1, - media=Image(data=np.zeros((4, 1, 1))), + media=Image.from_numpy(data=np.zeros((4, 1, 1))), annotations=[ Mask([1, 1, 1, 1], label=1, attributes={"z_order": 1}), Mask([0, 0, 1, 1], label=2, attributes={"z_order": 2}), @@ -1673,7 +1692,7 @@ def test_can_export_masks_with_non_0_background_color(self): [ DatasetItem( 1, - media=Image(data=np.zeros((4, 1, 1))), + media=Image.from_numpy(data=np.zeros((4, 1, 1))), annotations=[ Mask([[1, 1, 0, 0]], label=0, attributes={"z_order": 1}), ], @@ -1701,7 +1720,7 @@ class TestExtractor(TestExtractorBase): def __iter__(self): return iter( [ - DatasetItem(id=1, media=Image(path="1.jpg", size=(10, 15))), + DatasetItem(id=1, media=Image.from_file(path="1.jpg", size=(10, 15))), ] ) @@ -1720,10 +1739,11 @@ def __iter__(self): return iter( [ DatasetItem( - id="q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3))) + id="q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG") ), DatasetItem( - id="a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3))) + id="a/b/c/2", + media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp"), ), ] ) @@ -1743,9 +1763,13 @@ class TestExtractor(TestExtractorBase): def __iter__(self): return iter( [ - DatasetItem(id="1", media=Image(data=np.ones((4, 2, 3)))), - DatasetItem(id="subdir1/1", media=Image(data=np.ones((2, 6, 3)))), - DatasetItem(id="subdir2/1", media=Image(data=np.ones((5, 4, 3)))), + DatasetItem(id="1", media=Image.from_numpy(data=np.ones((4, 2, 3)))), + DatasetItem( + id="subdir1/1", media=Image.from_numpy(data=np.ones((2, 6, 3))) + ), + DatasetItem( + id="subdir2/1", media=Image.from_numpy(data=np.ones((5, 4, 3))) + ), ] ) @@ -1823,7 +1847,7 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((1, 2, 3))), + media=Image.from_numpy(data=np.ones((1, 2, 3))), annotations=[ # Bbox(0, 0, 0, 0, label=1) # won't find removed anns ], @@ -1831,7 +1855,7 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): DatasetItem( 2, subset="b", - media=Image(data=np.ones((3, 2, 3))), + media=Image.from_numpy(data=np.ones((3, 2, 3))), annotations=[ Bbox( 0, @@ -1863,14 +1887,14 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((1, 2, 3))), + media=Image.from_numpy(data=np.ones((1, 2, 3))), annotations=[Bbox(0, 0, 0, 0, label=1)], ), DatasetItem(2, subset="b", annotations=[Bbox(0, 0, 0, 0, label=2)]), DatasetItem( 3, subset="c", - media=Image(data=np.ones((2, 2, 3))), + media=Image.from_numpy(data=np.ones((2, 2, 3))), annotations=[Bbox(0, 0, 0, 0, label=3), Mask(np.ones((2, 2)), label=1)], ), ], @@ -1887,7 +1911,7 @@ def test_inplace_save_writes_only_updated_data_with_direct_changes(self): DatasetItem( 2, subset="b", - media=Image(data=np.ones((3, 2, 3))), + media=Image.from_numpy(data=np.ones((3, 2, 3))), annotations=[Bbox(0, 0, 0, 0, label=3)], ) ) @@ -1911,7 +1935,7 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): DatasetItem( 3, subset="test", - media=Image(data=np.ones((2, 3, 3))), + media=Image.from_numpy(data=np.ones((2, 3, 3))), annotations=[ Bbox( 0, @@ -1932,7 +1956,7 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): DatasetItem( 4, subset="train", - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[ Bbox( 1, @@ -1965,13 +1989,13 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): DatasetItem( 1, subset="a", - media=Image(data=np.ones((2, 1, 3))), + media=Image.from_numpy(data=np.ones((2, 1, 3))), annotations=[Bbox(0, 0, 0, 1, label=1)], ), DatasetItem( 2, subset="b", - media=Image(data=np.ones((2, 2, 3))), + media=Image.from_numpy(data=np.ones((2, 2, 3))), annotations=[ Bbox(0, 0, 1, 0, label=2), Mask(np.ones((2, 2)), label=1), @@ -1980,13 +2004,13 @@ def test_inplace_save_writes_only_updated_data_with_transforms(self): DatasetItem( 3, subset="b", - media=Image(data=np.ones((2, 3, 3))), + media=Image.from_numpy(data=np.ones((2, 3, 3))), annotations=[Bbox(0, 1, 0, 0, label=3)], ), DatasetItem( 4, subset="c", - media=Image(data=np.ones((2, 4, 3))), + media=Image.from_numpy(data=np.ones((2, 4, 3))), annotations=[Bbox(1, 0, 0, 0, label=3), Mask(np.ones((2, 2)), label=1)], ), ], @@ -2021,7 +2045,7 @@ def __iter__(self): DatasetItem( id="frame1", subset="test", - media=Image(path="frame1.jpg"), + media=Image.from_file(path="frame1.jpg"), annotations=[ Bbox( 1.0, diff --git a/tests/unit/test_vott_csv_format.py b/tests/unit/test_vott_csv_format.py index 5b09eb3ee0..338c3ee8f5 100644 --- a/tests/unit/test_vott_csv_format.py +++ b/tests/unit/test_vott_csv_format.py @@ -25,13 +25,13 @@ def test_can_import(self): DatasetItem( id="img0001", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[Bbox(10, 5, 10, 2, label=0)], ), DatasetItem( id="img0002", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(11.5, 12, 10.2, 20.5, label=1), ], @@ -39,7 +39,7 @@ def test_can_import(self): DatasetItem( id="img0003", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(6.7, 10.3, 3.3, 4.7, label=0), Bbox(13.7, 20.2, 31.9, 43.4, label=1), @@ -48,7 +48,7 @@ def test_can_import(self): DatasetItem( id="img0004", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(1, 2, 1, 2, label=0), ], @@ -68,13 +68,13 @@ def test_can_import_with_meta_file(self): DatasetItem( id="img0001", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[Bbox(10, 5, 10, 2, label=0)], ), DatasetItem( id="img0002", subset="test", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), annotations=[ Bbox(11.5, 12, 10.2, 20.5, label=1), ], diff --git a/tests/unit/test_vott_json_format.py b/tests/unit/test_vott_json_format.py index 2677a786fd..00f48ca30c 100644 --- a/tests/unit/test_vott_json_format.py +++ b/tests/unit/test_vott_json_format.py @@ -27,14 +27,14 @@ def test_can_import(self): DatasetItem( id="img0001", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "0d3de147f"}, annotations=[Bbox(5, 10, 10, 2, label=0, attributes={"id": "BsO3zj9bn"})], ), DatasetItem( id="img0002", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "b482849bc"}, annotations=[ Bbox(11.5, 12, 10.2, 20.5, label=0, attributes={"id": "mosw0b97K"}), @@ -44,7 +44,7 @@ def test_can_import(self): DatasetItem( id="img0003", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "50fef05a8"}, annotations=[ Bbox(6.7, 10.3, 3.3, 4.7, attributes={"id": "35t9mf-Zr"}), @@ -66,14 +66,14 @@ def test_can_import_with_meta_file(self): DatasetItem( id="img0001", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "0d3de147f"}, annotations=[Bbox(5, 10, 10, 2, label=0, attributes={"id": "BsO3zj9bn"})], ), DatasetItem( id="img0002", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "b482849bc"}, annotations=[ Bbox(11.5, 12, 10.2, 20.5, label=1, attributes={"id": "mosw0b97K"}) @@ -82,7 +82,7 @@ def test_can_import_with_meta_file(self): DatasetItem( id="img0003", subset="train", - media=Image(data=np.ones((5, 5, 3))), + media=Image.from_numpy(data=np.ones((5, 5, 3))), attributes={"id": "50fef05a8"}, annotations=[ Bbox(6.7, 10.3, 3.3, 4.7, attributes={"id": "35t9mf-Zr"}), diff --git a/tests/unit/test_widerface_format.py b/tests/unit/test_widerface_format.py index fc3de9ae13..ab4b267878 100644 --- a/tests/unit/test_widerface_format.py +++ b/tests/unit/test_widerface_format.py @@ -24,7 +24,7 @@ def test_can_save_and_load(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Bbox( @@ -48,7 +48,7 @@ def test_can_save_and_load(self): DatasetItem( id="2", subset="train", - media=Image(data=np.ones((10, 10, 3))), + media=Image.from_numpy(data=np.ones((10, 10, 3))), annotations=[ Bbox( 0, @@ -101,7 +101,7 @@ def test_can_save_and_load(self): DatasetItem( id="3", subset="val", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 0, @@ -138,7 +138,7 @@ def test_can_save_and_load(self): ), ], ), - DatasetItem(id="4", subset="val", media=Image(data=np.ones((8, 8, 3)))), + DatasetItem(id="4", subset="val", media=Image.from_numpy(data=np.ones((8, 8, 3)))), ], categories=["face", "label_0", "label_1"], ) @@ -156,7 +156,7 @@ def test_can_save_and_load_with_no_save_media(self): DatasetItem( id="1", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=1), Bbox( @@ -193,7 +193,7 @@ def test_can_save_dataset_with_no_subsets(self): [ DatasetItem( id="a/b/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=2), Bbox( @@ -230,7 +230,7 @@ def test_dataset_with_save_dataset_meta_file(self): DatasetItem( id="a/b/1", subset="train", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=2), Bbox( @@ -269,7 +269,7 @@ def test_can_save_dataset_with_cyrillic_and_spaces_in_filename(self): [ DatasetItem( id="кириллица с пробелом", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox( 0, @@ -304,7 +304,7 @@ def test_can_save_dataset_with_non_widerface_attributes(self): [ DatasetItem( id="a/b/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Bbox( @@ -326,7 +326,7 @@ def test_can_save_dataset_with_non_widerface_attributes(self): [ DatasetItem( id="a/b/1", - media=Image(data=np.ones((8, 8, 3))), + media=Image.from_numpy(data=np.ones((8, 8, 3))), annotations=[ Bbox(0, 2, 4, 2, label=0), Bbox(0, 1, 2, 3, label=0, attributes={"blur": "1", "invalid": "1"}), @@ -347,8 +347,10 @@ def test_can_save_dataset_with_non_widerface_attributes(self): def test_can_save_and_load_image_with_arbitrary_extension(self): dataset = Dataset.from_iterable( [ - DatasetItem("q/1", media=Image(path="q/1.JPEG", data=np.zeros((4, 3, 3)))), - DatasetItem("a/b/c/2", media=Image(path="a/b/c/2.bmp", data=np.zeros((3, 4, 3)))), + DatasetItem("q/1", media=Image.from_numpy(data=np.zeros((4, 3, 3)), ext=".JPEG")), + DatasetItem( + "a/b/c/2", media=Image.from_numpy(data=np.zeros((3, 4, 3)), ext=".bmp") + ), ], categories=[], ) @@ -363,8 +365,8 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): def test_inplace_save_writes_only_updated_data(self): expected = Dataset.from_iterable( [ - DatasetItem(1, subset="train", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(2, subset="train", media=Image(data=np.ones((3, 2, 3)))), + DatasetItem(1, subset="train", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem(2, subset="train", media=Image.from_numpy(data=np.ones((3, 2, 3)))), ], categories=[], ) @@ -372,15 +374,19 @@ def test_inplace_save_writes_only_updated_data(self): with TestDir() as path: dataset = Dataset.from_iterable( [ - DatasetItem(1, subset="train", media=Image(data=np.ones((2, 4, 3)))), - DatasetItem(2, subset="train", media=Image(path="2.jpg", size=(3, 2))), - DatasetItem(3, subset="valid", media=Image(data=np.ones((2, 2, 3)))), + DatasetItem(1, subset="train", media=Image.from_numpy(data=np.ones((2, 4, 3)))), + DatasetItem( + 2, subset="train", media=Image.from_file(path="2.jpg", size=(3, 2)) + ), + DatasetItem(3, subset="valid", media=Image.from_numpy(data=np.ones((2, 2, 3)))), ], categories=[], ) dataset.export(path, "wider_face", save_media=True) - dataset.put(DatasetItem(2, subset="train", media=Image(data=np.ones((3, 2, 3))))) + dataset.put( + DatasetItem(2, subset="train", media=Image.from_numpy(data=np.ones((3, 2, 3)))) + ) dataset.remove(3, "valid") dataset.save(save_media=True) @@ -416,7 +422,7 @@ def test_can_import(self): DatasetItem( id="0_Parade_image_01", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox( 1, @@ -438,7 +444,7 @@ def test_can_import(self): DatasetItem( id="1_Handshaking_image_02", subset="train", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox( 1, @@ -475,7 +481,7 @@ def test_can_import(self): DatasetItem( id="0_Parade_image_03", subset="val", - media=Image(data=np.ones((10, 15, 3))), + media=Image.from_numpy(data=np.ones((10, 15, 3))), annotations=[ Bbox( 0, diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index ff8ad2fb30..da0d3fb47a 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -249,11 +249,7 @@ def compare_datasets_3d( if (require_point_cloud and item_a.media) or (item_a.media and item_b.media): test.assertEqual(item_a.media.path, item_b.media.path, item_a.id) - test.assertEqual( - set(img.path for img in item_a.media.extra_images), - set(img.path for img in item_b.media.extra_images), - item_a.id, - ) + test.assertEqual(item_a.media.extra_images, item_b.media.extra_images, item_a.id) test.assertEqual(len(item_a.annotations), len(item_b.annotations)) for ann_a in item_a.annotations: # We might find few corresponding items, so check them all From eb4288a9d861c878e73f1bcaac6080173b2ef09c Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 6 Feb 2025 15:55:26 +0400 Subject: [PATCH 05/49] syncing components/importer.py --- src/datumaro/components/importer.py | 157 ++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 9 deletions(-) diff --git a/src/datumaro/components/importer.py b/src/datumaro/components/importer.py index 688f3e4cd1..84e46c6643 100644 --- a/src/datumaro/components/importer.py +++ b/src/datumaro/components/importer.py @@ -1,35 +1,68 @@ +# Copyright (C) 2019-2022 Intel Corporation +# +# SPDX-License-Identifier: MIT + from __future__ import annotations import os +import os.path as osp +from contextlib import contextmanager +from functools import wraps from glob import iglob -from os import path as osp -from typing import Callable, Dict, List, Optional +from typing import Callable, Dict, List, Optional, Type, TypeVar from datumaro.components.cli_plugin import CliPlugin -from datumaro.components.errors import DatasetNotFoundError +from datumaro.components.contexts.importer import ( + FailingImportErrorPolicy, + ImportContext, + ImportErrorPolicy, + NullImportContext, + _ImportFail, +) +from datumaro.components.errors import DatasetImportError, DatasetNotFoundError from datumaro.components.format_detection import FormatDetectionConfidence, FormatDetectionContext +from datumaro.components.merge.extractor_merger import ExtractorMerger +from datumaro.util.definitions import SUBSET_NAME_WHITELIST + +T = TypeVar("T") + +__all__ = [ + "ImportContext", + "NullImportContext", + "_ImportFail", + "Importer", + "with_subset_dirs", + "ImportErrorPolicy", + "FailingImportErrorPolicy", +] class Importer(CliPlugin): + DETECT_CONFIDENCE = FormatDetectionConfidence.LOW + @classmethod def detect( cls, context: FormatDetectionContext, - ) -> Optional[FormatDetectionConfidence]: + ) -> FormatDetectionConfidence: if not cls.find_sources_with_params(context.root_path): context.fail("specific requirement information unavailable") - return FormatDetectionConfidence.LOW + return cls.DETECT_CONFIDENCE @classmethod - def find_sources(cls, path) -> List[Dict]: + def get_file_extensions(cls) -> List[str]: raise NotImplementedError() @classmethod - def find_sources_with_params(cls, path, **extra_params) -> List[Dict]: + def find_sources(cls, path: str) -> List[Dict]: + raise NotImplementedError() + + @classmethod + def find_sources_with_params(cls, path: str, **extra_params) -> List[Dict]: return cls.find_sources(path) - def __call__(self, path, **extra_params): + def __call__(self, path, stream: bool = False, **extra_params): if not path or not osp.exists(path): raise DatasetNotFoundError(path, self.NAME) @@ -41,6 +74,14 @@ def __call__(self, path, **extra_params): for desc in found_sources: params = dict(extra_params) params.update(desc.get("options", {})) + + if stream and self.can_stream: + params.update({"stream": True}) + elif stream and not self.can_stream: + raise DatasetImportError( + f"{self.__class__.__name__} cannot stream, but stream=True." + ) + desc["options"] = params sources.append(desc) @@ -56,6 +97,7 @@ def _find_sources_recursive( dirname: str = "", file_filter: Optional[Callable[[str], bool]] = None, max_depth: int = 3, + recursive: bool = False, ): """ Finds sources in the specified location, using the matching pattern @@ -72,6 +114,8 @@ def _find_sources_recursive( dirname: a glob pattern for filename prefixes file_filter: a callable (abspath: str) -> bool, to filter paths found max_depth: the maximum depth for recursive search. + recursive: If recursive is true, the pattern '**' will match any files and + zero or more directories and subdirectories. Returns: a list of source configurations (i.e. Extractor type names and c-tor parameters) @@ -95,9 +139,104 @@ def _find_sources_recursive( for d in range(max_depth + 1): sources.extend( {"url": p, "format": extractor_name} - for p in iglob(osp.join(path, *("*" * d), dirname, filename + ext)) + for p in iglob( + osp.join(path, *("*" * d), dirname, filename + ext), recursive=recursive + ) if (callable(file_filter) and file_filter(p)) or (not callable(file_filter)) ) if sources: break return sources + + @property + def can_stream(self) -> bool: + """Flag to indicate whether the importer can stream the dataset item or not.""" + return False + + def get_extractor_merger(self) -> Optional[Type[ExtractorMerger]]: + """Extractor merger dedicated for the data format + + Datumaro import process spawns multiple `DatasetBase` for the detected sources. + We can find a bunch of the detected sources from the given directory path. + It is usually each detected source is corresponded to the subset of dataset + in many data formats. + + Parameters: + stream: There can exist a branch according to `stream` flag + + Returns: + If None, use `Dataset.from_extractors()` to merge the extractors, + Otherwise, use the return type to merge the extractors. + """ + return None + + +def with_subset_dirs(input_cls: Importer): + @wraps(input_cls, updated=()) + class WrappedImporter(input_cls): + NAME = input_cls.NAME + + @classmethod + def detect( + cls, + context: FormatDetectionContext, + ) -> Optional[FormatDetectionConfidence]: + @contextmanager + def _change_context_root_path(context: FormatDetectionContext, path: str): + tmp = context.root_path + context._root_path = path + yield + context._root_path = tmp + + confs = [] + path = context.root_path + + if not osp.isdir(path): + context.fail( + f"{input_cls.NAME} should require an input as a directory path. " + f"However, {path} is not a directory path." + ) + + for sub_dir in os.listdir(path): + if sub_dir.lower() not in SUBSET_NAME_WHITELIST: + continue + + sub_path = osp.join(path, sub_dir) + if osp.isdir(sub_path): + with _change_context_root_path(context, sub_path): + conf = input_cls.detect(context) + if conf is not None: + confs += [conf] + + if len(confs) == 0: + context.fail(f"{input_cls.NAME} cannot find its subdirectory structure.") + + return max(confs) + + def __call__(self, path, **extra_params): + sources = [] + for sub_dir in os.listdir(path): + sub_path = osp.join(path, sub_dir) + if osp.isdir(sub_path): + source = input_cls.__call__(self, sub_path, **extra_params) + + if len(source) != 1: + raise DatasetImportError( + f"@with_subset_dirs only allows one source format from {sub_path}." + ) + + if "subset" in source[0]: + raise DatasetImportError( + f"@with_subset_dirs does not allows " + f"a subset key in source: {source[0]}." + ) + + source[0]["options"]["subset"] = sub_dir + sources += source + + return sources + + def __reduce__(self): + return (input_cls.__class__, ()) + + return WrappedImporter From b3e9d8712893c9328b46340d8f151285d0bca0de Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 6 Feb 2025 16:06:20 +0400 Subject: [PATCH 06/49] syncing util/meta_file_util.py --- src/datumaro/util/meta_file_util.py | 73 ++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/src/datumaro/util/meta_file_util.py b/src/datumaro/util/meta_file_util.py index 8e0aba99f6..c5f638701e 100644 --- a/src/datumaro/util/meta_file_util.py +++ b/src/datumaro/util/meta_file_util.py @@ -1,15 +1,20 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022-2023 Intel Corporation # Copyright (C) 2024 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +import os import os.path as osp from collections import OrderedDict -from datumaro.components.annotation import AnnotationType +import numpy as np + +from datumaro.components.annotation import AnnotationType, HashKey from datumaro.util import dump_json_file, find, parse_json_file DATASET_META_FILE = "dataset_meta.json" +DATASET_HASHKEY_FILE = "hash_keys.json" +DATASET_HASHKEY_FOLDER = "hash_key_meta" def is_meta_file(path): @@ -20,10 +25,19 @@ def has_meta_file(path): return osp.isfile(get_meta_file(path)) +def has_hashkey_file(path): + return osp.isfile(get_hashkey_file(path)) + + def get_meta_file(path): return osp.join(path, DATASET_META_FILE) +def get_hashkey_file(path): + hashkey_folder_path = osp.join(path, DATASET_HASHKEY_FOLDER) + return osp.join(hashkey_folder_path, DATASET_HASHKEY_FILE) + + def parse_meta_file(path): meta_file = path if osp.isdir(path): @@ -81,3 +95,58 @@ def save_meta_file(path, categories): meta_file = get_meta_file(path) dump_json_file(meta_file, dataset_meta, indent=True) + + +def parse_hashkey_file(path): + meta_file = path + if osp.isdir(path): + meta_file = get_hashkey_file(path) + + if not osp.exists(meta_file): + return None + + dataset_meta = parse_json_file(meta_file) + + hashkey_dict = OrderedDict() + for id_, hashkey in dataset_meta.get("hashkey", {}).items(): + hashkey_dict[id_] = hashkey + + return hashkey_dict + + +def save_hashkey_file(path, item_list): + dataset_hashkey = {} + + if osp.isdir(path): + meta_file = get_hashkey_file(path) + hashkey_folder_path = osp.join(path, DATASET_HASHKEY_FOLDER) + if not osp.exists(hashkey_folder_path): + os.makedirs(hashkey_folder_path) + + hashkey_dict = parse_hashkey_file(path) + if not hashkey_dict: + hashkey_dict = {} + + for item in item_list: + item_id = item.id + item_subset = item.subset + for annotation in item.annotations: + if isinstance(annotation, HashKey): + hashkey = annotation.hash_key + break + hashkey_dict.update({item_subset + "/" + item_id: hashkey.tolist()}) + + dataset_hashkey["hashkey"] = hashkey_dict + + dump_json_file(meta_file, dataset_hashkey, indent=True) + + +def load_hash_key(path, dataset): + if not os.path.isdir(path) or not has_hashkey_file(path): + return dataset + + hashkey_dict = parse_hashkey_file(path) + for item in dataset: + hash_key = hashkey_dict[item.subset + "/" + item.id] + item.annotations.append(HashKey(hash_key=np.asarray(hash_key, dtype=np.uint8))) + return dataset From af1c28591abfd01be637380b3df187bc94589d7d Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 6 Feb 2025 16:41:03 +0400 Subject: [PATCH 07/49] moving cli/contexts/project/diff.py to cli/util/compare.py --- src/datumaro/cli/commands/diff.py | 2 +- src/datumaro/cli/{contexts/project/diff.py => util/compare.py} | 0 tests/integration/cli/test_compare.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/datumaro/cli/{contexts/project/diff.py => util/compare.py} (100%) diff --git a/src/datumaro/cli/commands/diff.py b/src/datumaro/cli/commands/diff.py index 20a5d7f437..dc4a89e971 100644 --- a/src/datumaro/cli/commands/diff.py +++ b/src/datumaro/cli/commands/diff.py @@ -8,13 +8,13 @@ import os.path as osp from enum import Enum, auto +from datumaro.cli.util.compare import DiffVisualizer from datumaro.components.comparator import DistanceComparator, EqualityComparator from datumaro.components.errors import ProjectNotFoundError from datumaro.util import dump_json_file from datumaro.util.os_util import rmtree from datumaro.util.scope import on_error_do, scope_add, scoped -from ..contexts.project.diff import DiffVisualizer from ..util import MultilineFormatter from ..util.errors import CliException from ..util.project import generate_next_file_name, load_project, parse_full_revpath diff --git a/src/datumaro/cli/contexts/project/diff.py b/src/datumaro/cli/util/compare.py similarity index 100% rename from src/datumaro/cli/contexts/project/diff.py rename to src/datumaro/cli/util/compare.py diff --git a/tests/integration/cli/test_compare.py b/tests/integration/cli/test_compare.py index f311b96ee4..4ec90473ec 100644 --- a/tests/integration/cli/test_compare.py +++ b/tests/integration/cli/test_compare.py @@ -4,7 +4,7 @@ import numpy as np -from datumaro.cli.contexts.project.diff import DiffVisualizer +from datumaro.cli.util.compare import DiffVisualizer from datumaro.components.annotation import ( AnnotationType, Bbox, From 7089ba6b3752468cb88e0f69e10a543540ffb463 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 6 Feb 2025 17:22:06 +0400 Subject: [PATCH 08/49] moving Registry and PluginRegistry to components/registry.py --- src/datumaro/__init__.py | 3 +- src/datumaro/components/environment.py | 47 ++------------------------ src/datumaro/components/registry.py | 45 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 src/datumaro/components/registry.py diff --git a/src/datumaro/__init__.py b/src/datumaro/__init__.py index e8e07903f0..d1ea89fdb2 100644 --- a/src/datumaro/__init__.py +++ b/src/datumaro/__init__.py @@ -37,7 +37,7 @@ from .components.dataset_base import CategoriesInfo, DatasetBase, DatasetItem, SubsetBase from .components.dataset_item_storage import ItemStatus from .components.dataset_storage import DatasetPatch -from .components.environment import Environment, PluginRegistry +from .components.environment import Environment from .components.exporter import Exporter, ExportErrorPolicy, FailingExportErrorPolicy from .components.hl_ops import ( # pylint: disable=redefined-builtin export, @@ -52,6 +52,7 @@ from .components.media import Image, MediaElement, PointCloud, Video, VideoFrame from .components.media_manager import MediaManager from .components.progress_reporting import NullProgressReporter, ProgressReporter +from .components.registry import PluginRegistry from .components.transformer import ItemTransform, ModelTransform, Transform from .components.validator import Validator from .util.definitions import DEFAULT_SUBSET_NAME diff --git a/src/datumaro/components/environment.py b/src/datumaro/components/environment.py index 73987da308..eb819177dd 100644 --- a/src/datumaro/components/environment.py +++ b/src/datumaro/components/environment.py @@ -8,54 +8,13 @@ import os.path as osp from functools import partial from inspect import isclass -from typing import Callable, Dict, Generic, Iterable, Iterator, List, Optional, Type, TypeVar +from typing import Callable, List, Optional -from datumaro.components.cli_plugin import CliPlugin, plugin_types +from datumaro.components.cli_plugin import plugin_types from datumaro.components.format_detection import RejectionReason, detect_dataset_format +from datumaro.components.registry import PluginRegistry from datumaro.util.os_util import import_foreign_module, split_path -T = TypeVar("T") - - -class Registry(Generic[T]): - def __init__(self): - self.items: Dict[str, T] = {} - - def register(self, name: str, value: T) -> T: - self.items[name] = value - return value - - def unregister(self, name: str) -> Optional[T]: - return self.items.pop(name, None) - - def get(self, key: str): - """Returns a class or a factory function""" - return self.items[key] - - def __getitem__(self, key: str) -> T: - return self.get(key) - - def __contains__(self, key) -> bool: - return key in self.items - - def __iter__(self) -> Iterator[T]: - return iter(self.items) - - -class PluginRegistry(Registry[Type[CliPlugin]]): - def __init__( - self, filter: Callable[[Type[CliPlugin]], bool] = None - ): # pylint: disable=redefined-builtin - super().__init__() - self._filter = filter - - def batch_register(self, values: Iterable[CliPlugin]): - for v in values: - if self._filter and not self._filter(v): - continue - - self.register(v.NAME, v) - class Environment: _builtin_plugins = None diff --git a/src/datumaro/components/registry.py b/src/datumaro/components/registry.py new file mode 100644 index 0000000000..69f3640fd4 --- /dev/null +++ b/src/datumaro/components/registry.py @@ -0,0 +1,45 @@ +from typing import Callable, Dict, Generic, Iterable, Iterator, Optional, Type, TypeVar + +from datumaro.components.cli_plugin import CliPlugin + +T = TypeVar("T") + + +class Registry(Generic[T]): + def __init__(self): + self.items: Dict[str, T] = {} + + def register(self, name: str, value: T) -> T: + self.items[name] = value + return value + + def unregister(self, name: str) -> Optional[T]: + return self.items.pop(name, None) + + def get(self, key: str): + """Returns a class or a factory function""" + return self.items[key] + + def __getitem__(self, key: str) -> T: + return self.get(key) + + def __contains__(self, key) -> bool: + return key in self.items + + def __iter__(self) -> Iterator[T]: + return iter(self.items) + + +class PluginRegistry(Registry[Type[CliPlugin]]): + def __init__( + self, filter: Callable[[Type[CliPlugin]], bool] = None + ): # pylint: disable=redefined-builtin + super().__init__() + self._filter = filter + + def batch_register(self, values: Iterable[CliPlugin]): + for v in values: + if self._filter and not self._filter(v): + continue + + self.register(v.NAME, v) From ac4f89f0b1b585a5a52797f1228318718607c4b2 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 6 Feb 2025 21:39:44 +0400 Subject: [PATCH 09/49] syncing components/exporter.py --- src/datumaro/components/exporter.py | 255 +++++++++++++++--- .../integration/cli/test_kitti_raw_format.py | 2 +- tests/integration/cli/test_patch.py | 2 +- tests/integration/cli/test_project.py | 10 +- .../cli/test_sly_point_cloud_format.py | 2 +- tests/integration/cli/test_voc_format.py | 10 +- tests/integration/cli/test_yolo_format.py | 2 +- .../datumaro/test_datumaro_format.py | 8 +- 8 files changed, 238 insertions(+), 53 deletions(-) diff --git a/src/datumaro/components/exporter.py b/src/datumaro/components/exporter.py index 8a7702e71c..651d3ee39f 100644 --- a/src/datumaro/components/exporter.py +++ b/src/datumaro/components/exporter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2022 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -6,14 +6,15 @@ import os import os.path as osp import shutil -import warnings from tempfile import mkdtemp from typing import NoReturn, Optional, Tuple, TypeVar, Union import attr from attrs import define, field +from datumaro.components.annotation import HashKey from datumaro.components.cli_plugin import CliPlugin +from datumaro.components.crypter import NULL_CRYPTER, Crypter from datumaro.components.dataset_base import DatasetItem, IDataset from datumaro.components.errors import ( AnnotationExportError, @@ -21,9 +22,9 @@ DatumaroError, ItemExportError, ) -from datumaro.components.media import Image, PointCloud +from datumaro.components.media import Image, PointCloud, Video, VideoFrame from datumaro.components.progress_reporting import NullProgressReporter, ProgressReporter -from datumaro.util.meta_file_util import save_meta_file +from datumaro.util.meta_file_util import save_hashkey_file, save_meta_file from datumaro.util.os_util import rmtree from datumaro.util.scope import on_error_do, scoped @@ -98,18 +99,9 @@ class Exporter(CliPlugin): def build_cmdline_parser(cls, **kwargs): parser = super().build_cmdline_parser(**kwargs) - # Deprecated - parser.add_argument( - "--save-images", - action="store_true", - default=None, - help="(Deprecated. Use --save-media instead) " "Save images (default: False)", - ) - parser.add_argument( "--save-media", action="store_true", - default=None, # TODO: remove default once save-images is removed help="Save media (default: False)", ) parser.add_argument( @@ -161,6 +153,12 @@ def patch(cls, dataset, patch, save_dir, **options): return retval def apply(self): + """Execute the data-format conversion""" + if self._save_hashkey_meta: + self._save_hashkey_file(self._save_dir) + return self._apply_impl() + + def _apply_impl(self): raise NotImplementedError("Should be implemented in a subclass") def __init__( @@ -168,49 +166,42 @@ def __init__( extractor: IDataset, save_dir: str, *, - save_images=None, # Deprecated save_media: Optional[bool] = None, image_ext: Optional[str] = None, default_image_ext: Optional[str] = None, save_dataset_meta: bool = False, + save_hashkey_meta: bool = False, + stream: bool = False, ctx: Optional[ExportContext] = None, ): default_image_ext = default_image_ext or self.DEFAULT_IMAGE_EXT assert default_image_ext self._default_image_ext = default_image_ext - if save_images is not None and save_media is not None: - raise DatasetExportError("Can't use both 'save-media' and " "'save-images'") - - if save_media is not None: - self._save_media = save_media - elif save_images is not None: - self._save_media = save_images - warnings.warn( - "'save-images' is deprecated and will be " - "removed in future. Use 'save-media' instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - self._save_media = False - + self._save_media = save_media self._image_ext = image_ext self._extractor = extractor self._save_dir = save_dir self._save_dataset_meta = save_dataset_meta + self._save_hashkey_meta = save_hashkey_meta # TODO: refactor this variable. # Can be used by a subclass to store the current patch info - from datumaro.components.dataset_storage import DatasetPatch + from datumaro.components.dataset import DatasetPatch if isinstance(extractor, DatasetPatch.DatasetPatchWrapper): self._patch = extractor.patch else: self._patch = None + if stream and not self.can_stream: + raise DatasetExportError( + f"{self.__class__.__name__} cannot export a dataset in a stream manner" + ) + self._stream = stream + self._ctx: ExportContext = ctx or NullExportContext() def _find_image_ext(self, item: Union[DatasetItem, Image]): @@ -234,7 +225,16 @@ def _make_image_filename(self, item, *, name=None, subdir=None): def _make_pcd_filename(self, item, *, name=None, subdir=None): return self._make_item_filename(item, name=name, subdir=subdir) + ".pcd" - def _save_image(self, item, path=None, *, name=None, subdir=None, basedir=None): + def _save_image( + self, + item, + path=None, + *, + name=None, + subdir=None, + basedir=None, + crypter: Crypter = NULL_CRYPTER, + ): assert not ( (subdir or name or basedir) and path ), "Can't use both subdir or name or basedir and path arguments" @@ -247,7 +247,7 @@ def _save_image(self, item, path=None, *, name=None, subdir=None, basedir=None): path = path or osp.join(basedir, self._make_image_filename(item, name=name, subdir=subdir)) path = osp.abspath(path) - item.media.save(path) + item.media.save(path, crypter=crypter) def _save_point_cloud(self, item=None, path=None, *, name=None, subdir=None, basedir=None): assert not ( @@ -263,9 +263,192 @@ def _save_point_cloud(self, item=None, path=None, *, name=None, subdir=None, bas path = osp.abspath(path) os.makedirs(osp.dirname(path), exist_ok=True) - if item.media and osp.isfile(item.media.path): - if item.media.path != path: - shutil.copyfile(item.media.path, path) + item.media.save(path, crypter=NULL_CRYPTER) def _save_meta_file(self, path): save_meta_file(path, self._extractor.categories()) + + def _save_hashkey_file(self, path): + save_hashkey_file(path, self._extractor) + + def _check_hash_key_existence(self, item): + if self._save_hashkey_meta: + return + for annotation in item.annotations: + if isinstance(annotation, HashKey): + self._save_hashkey_meta = True + return + + @property + def can_stream(self) -> bool: + """ + Flag to indicate whether the exporter can export the dataset in a stream manner or not. + """ + return False + + +# TODO: Currently, ExportContextComponent is introduced only for Datumaro and DatumaroBinary format +# for multi-processing. We need to propagate this to everywhere in Datumaro 1.2.0 + + +class ExportContextComponent: + def __init__( + self, + save_dir: str, + save_media: bool, + images_dir: str, + pcd_dir: str, + video_dir: str, + crypter: Crypter = NULL_CRYPTER, + image_ext: Optional[str] = None, + default_image_ext: Optional[str] = None, + source_path: Optional[str] = None, + ): + self._save_dir = save_dir + self._save_media = save_media + self._images_dir = images_dir + self._pcd_dir = pcd_dir + self._video_dir = video_dir + self._crypter = crypter + self._image_ext = image_ext + self._default_image_ext = default_image_ext + self._source_path = source_path + + def find_image_ext(self, item: Union[DatasetItem, Image]): + src_ext = None + + if isinstance(item, DatasetItem) and isinstance(item.media, Image): + src_ext = item.media.ext + elif isinstance(item, Image): + src_ext = item.ext + + return self._image_ext or src_ext or self._default_image_ext + + def _make_item_filename(self, item, *, name=None, subdir=None): + name = name or item.id + subdir = subdir or "" + return osp.join(subdir, name) + + def make_image_filename(self, item, *, name=None, subdir=None): + return self._make_item_filename(item, name=name, subdir=subdir) + self.find_image_ext(item) + + def make_pcd_filename(self, item, *, name=None, subdir=None): + return self._make_item_filename(item, name=name, subdir=subdir) + ".pcd" + + def make_pcd_extra_image_filename(self, item, idx, image, *, name=None, subdir=None): + return self._make_item_filename( + item, name=name if name else f"{item.id}/extra_image_{idx}", subdir=subdir + ) + self.find_image_ext(image) + + def make_video_filename(self, item, *, name=None): + STR_WRONG_MEDIA_TYPE = "Video item's media type should be Video or VideoFrame" + assert isinstance(item, DatasetItem), STR_WRONG_MEDIA_TYPE + + if isinstance(item.media, VideoFrame): + video_file_name = osp.basename(item.media.video.path) + elif isinstance(item.media, Video): + video_file_name = osp.basename(item.media.path) + else: + assert False, STR_WRONG_MEDIA_TYPE + + return video_file_name + + def save_image( + self, + item: DatasetItem, + *, + encryption: bool = False, + basedir: Optional[str] = None, + subdir: Optional[str] = None, + fname: Optional[str] = None, + ): + if not isinstance(item.media, Image) or not item.media.has_data: + log.warning("Item '%s' has no image", item.id) + return + + basedir = self._images_dir if basedir is None else basedir + basedir = osp.join(basedir, subdir) if subdir is not None else basedir + fname = self.make_image_filename(item) if fname is None else fname + path = osp.join(basedir, fname) + path = osp.abspath(path) + + os.makedirs(osp.dirname(path), exist_ok=True) + item.media.save(path, crypter=self._crypter if encryption else NULL_CRYPTER) + + def save_point_cloud( + self, + item: DatasetItem, + *, + basedir: Optional[str] = None, + subdir: Optional[str] = None, + fname: Optional[str] = None, + ): + if not item.media or not isinstance(item.media, PointCloud): + log.warning("Item '%s' has no pcd", item.id) + return + + basedir = self._pcd_dir if basedir is None else basedir + basedir = osp.join(basedir, subdir) if subdir is not None else basedir + fname = self.make_pcd_filename(item) if fname is None else fname + path = osp.join(basedir, fname) + path = osp.abspath(path) + + os.makedirs(osp.dirname(path), exist_ok=True) + + def helper(i, image): + basedir = self._images_dir + basedir = osp.join(basedir, subdir) if subdir is not None else basedir + return {"fp": osp.join(basedir, self.make_pcd_extra_image_filename(item, i, image))} + + item.media.save(path, helper, crypter=NULL_CRYPTER) + + def save_video( + self, + item: DatasetItem, + *, + basedir: Optional[str] = None, + subdir: Optional[str] = None, + fname: Optional[str] = None, + ): + if not item.media or not isinstance(item.media, (Video, VideoFrame)): + log.warning("Item '%s' has no video", item.id) + return + basedir = self._video_dir if basedir is None else basedir + basedir = osp.join(basedir, subdir) if subdir is not None else basedir + fname = self.make_video_filename(item) if fname is None else fname + + path = osp.join(basedir, fname) + path = osp.abspath(path) + + # To prevent the video from being overwritten + # (A video can have same path but different start/end frames) + if not osp.exists(path): + os.makedirs(osp.dirname(path), exist_ok=True) + if isinstance(item.media, VideoFrame): + item.media.video.save(path, crypter=NULL_CRYPTER) + else: # Video + item.media.save(path, crypter=NULL_CRYPTER) + + @property + def images_dir(self) -> str: + return self._images_dir + + @property + def pcd_dir(self) -> str: + return self._pcd_dir + + @property + def save_dir(self) -> str: + return self._save_dir + + @property + def save_media(self) -> bool: + return self._save_media + + @property + def crypter(self) -> Crypter: + return self._crypter + + @property + def source_path(self) -> str: + return self._source_path if self._source_path else "" diff --git a/tests/integration/cli/test_kitti_raw_format.py b/tests/integration/cli/test_kitti_raw_format.py index 4cd0b549c3..05957b32a8 100644 --- a/tests/integration/cli/test_kitti_raw_format.py +++ b/tests/integration/cli/test_kitti_raw_format.py @@ -131,7 +131,7 @@ def test_can_convert_to_kitti_raw(self): "-o", export_dir, "--", - "--save-images", + "--save-media", ) parsed_dataset = Dataset.import_from(export_dir, format="sly_pointcloud") diff --git a/tests/integration/cli/test_patch.py b/tests/integration/cli/test_patch.py index 997f4a9c99..b56fa9c8ad 100644 --- a/tests/integration/cli/test_patch.py +++ b/tests/integration/cli/test_patch.py @@ -101,7 +101,7 @@ def test_can_run_patch(self): patch_url + ":voc", "--", "--reindex=1", - "--save-images", + "--save-media", ) compare_datasets( diff --git a/tests/integration/cli/test_project.py b/tests/integration/cli/test_project.py index 2ebef582d8..f0115e91d1 100644 --- a/tests/integration/cli/test_project.py +++ b/tests/integration/cli/test_project.py @@ -42,7 +42,7 @@ def test_can_convert_voc_as_coco(self): "-o", result_dir, "--", - "--save-images", + "--save-media", "--reindex", "1", ) @@ -65,9 +65,7 @@ def test_can_export_coco_as_voc(self): run(self, "import", "-f", "coco", "-p", test_dir, coco_dir) result_dir = osp.join(test_dir, "voc_export") - run( - self, "export", "-f", "voc", "-p", test_dir, "-o", result_dir, "--", "--save-images" - ) + run(self, "export", "-f", "voc", "-p", test_dir, "-o", result_dir, "--", "--save-media") self.assertTrue(osp.isdir(result_dir)) @@ -183,7 +181,7 @@ def test_can_use_vcs(self): result_dir, "source-1", "--", - "--save-images", + "--save-media", ) parsed = Dataset.import_from(result_dir, "coco") compare_datasets( @@ -226,7 +224,7 @@ def test_can_use_vcs(self): "-o", result_dir, "--", - "--save-images", + "--save-media", ) parsed = Dataset.import_from(result_dir, "coco") compare_datasets( diff --git a/tests/integration/cli/test_sly_point_cloud_format.py b/tests/integration/cli/test_sly_point_cloud_format.py index 6f809f7ab9..ec61dc8fa5 100644 --- a/tests/integration/cli/test_sly_point_cloud_format.py +++ b/tests/integration/cli/test_sly_point_cloud_format.py @@ -102,7 +102,7 @@ def test_can_convert_to_kitti_raw(self): "-o", export_dir, "--", - "--save-images", + "--save-media", "--allow-attrs", ) diff --git a/tests/integration/cli/test_voc_format.py b/tests/integration/cli/test_voc_format.py index 4eb1c1e47e..2941b9ac03 100644 --- a/tests/integration/cli/test_voc_format.py +++ b/tests/integration/cli/test_voc_format.py @@ -36,7 +36,7 @@ def _test_can_save_and_load( run(self, "import", "-p", project_path, "-f", dataset_format, *extra_args, source_path) result_dir = osp.join(project_path, "result") - extra_args = ["--", "--save-images"] + extra_args = ["--", "--save-media"] if label_map: extra_args += ["--label-map", label_map] run(self, "export", "-f", dataset_format, "-p", project_path, "-o", result_dir, *extra_args) @@ -200,9 +200,7 @@ def test_export_to_voc_format(self): run(self, "import", "-p", test_dir, "-f", "yolo", yolo_dir) voc_export = osp.join(test_dir, "voc_export") - run( - self, "export", "-p", test_dir, "-f", "voc", "-o", voc_export, "--", "--save-images" - ) + run(self, "export", "-p", test_dir, "-f", "voc", "-o", voc_export, "--", "--save-media") parsed_dataset = Dataset.import_from(voc_export, format="voc") compare_datasets(self, expected_dataset, parsed_dataset, require_media=True) @@ -277,7 +275,7 @@ def test_convert_to_voc_format(self): "-o", voc_dir, "--", - "--save-images", + "--save-media", ) target_dataset = Dataset.import_from(voc_dir, format="voc") @@ -333,7 +331,7 @@ def test_convert_from_voc_format(self): "-o", imagenet_dir, "--", - "--save-image", + "--save-media", ) target_dataset = Dataset.import_from(imagenet_dir, format="imagenet") diff --git a/tests/integration/cli/test_yolo_format.py b/tests/integration/cli/test_yolo_format.py index ebd2ca583b..0cadca3c97 100644 --- a/tests/integration/cli/test_yolo_format.py +++ b/tests/integration/cli/test_yolo_format.py @@ -215,7 +215,7 @@ def test_can_delete_labels_from_yolo_dataset(self): "-f", self.FORMAT_NAME, "--", - "--save-image", + "--save-media", ) parsed_dataset = Dataset.import_from(export_dir, format=self.FORMAT_NAME) diff --git a/tests/unit/data_formats/datumaro/test_datumaro_format.py b/tests/unit/data_formats/datumaro/test_datumaro_format.py index 24c7066336..3cc9448173 100644 --- a/tests/unit/data_formats/datumaro/test_datumaro_format.py +++ b/tests/unit/data_formats/datumaro/test_datumaro_format.py @@ -509,7 +509,13 @@ def test_can_save_and_load_with_pointcloud(self): id=1, subset="test", media=PointCloud.from_file( - "1.pcd", + path=get_test_asset_path( + "datumaro_dataset", + "with_pcd", + "point_clouds", + "default", + "0000000000.pcd", + ), extra_images=[ Image.from_numpy(data=np.ones((5, 5, 3)), path="1/a.jpg"), Image.from_numpy(data=np.ones((5, 4, 3)), path="1/b.jpg"), From b8b3b619e60bccc5a50a3e5c42c07ffbe01c5d12 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 11:37:01 +0400 Subject: [PATCH 10/49] syncing components/hl_ops.py --- src/datumaro/__init__.py | 9 +- src/datumaro/cli/commands/convert.py | 4 +- src/datumaro/cli/commands/diff.py | 10 +- src/datumaro/cli/commands/download.py | 4 +- src/datumaro/cli/commands/merge.py | 2 +- src/datumaro/cli/commands/patch.py | 2 +- src/datumaro/cli/contexts/project/__init__.py | 6 +- src/datumaro/cli/util/compare.py | 2 +- src/datumaro/components/dataset.py | 2 +- src/datumaro/components/environment.py | 32 +- src/datumaro/components/hl_ops/__init__.py | 595 ++++++++++++------ tests/integration/cli/test_compare.py | 4 +- tests/unit/test_dataset.py | 16 +- tests/unit/test_extractor_tfds.py | 2 +- 14 files changed, 464 insertions(+), 226 deletions(-) diff --git a/src/datumaro/__init__.py b/src/datumaro/__init__.py index d1ea89fdb2..c10942c884 100644 --- a/src/datumaro/__init__.py +++ b/src/datumaro/__init__.py @@ -39,14 +39,7 @@ from .components.dataset_storage import DatasetPatch from .components.environment import Environment from .components.exporter import Exporter, ExportErrorPolicy, FailingExportErrorPolicy -from .components.hl_ops import ( # pylint: disable=redefined-builtin - export, - filter, - merge, - run_model, - transform, - validate, -) +from .components.hl_ops import HLOps from .components.importer import Importer from .components.launcher import Launcher from .components.media import Image, MediaElement, PointCloud, Video, VideoFrame diff --git a/src/datumaro/cli/commands/convert.py b/src/datumaro/cli/commands/convert.py index f3b0b1a93e..6b51a1d9ba 100644 --- a/src/datumaro/cli/commands/convert.py +++ b/src/datumaro/cli/commands/convert.py @@ -19,7 +19,7 @@ def build_parser(parser_ctor=argparse.ArgumentParser): builtin_readers = sorted(set(Environment().importers) | set(Environment().extractors)) - builtin_writers = sorted(Environment().converters) + builtin_writers = sorted(Environment().exporters) parser = parser_ctor( help="Convert an existing dataset to another format", @@ -102,7 +102,7 @@ def convert_command(args): env = Environment() try: - converter = env.converters[args.output_format] + converter = env.exporters[args.output_format] except KeyError: raise CliException("Converter for format '%s' is not found" % args.output_format) extra_args = converter.parse_cmdline(args.extra_args) diff --git a/src/datumaro/cli/commands/diff.py b/src/datumaro/cli/commands/diff.py index dc4a89e971..aa5e2a4eaf 100644 --- a/src/datumaro/cli/commands/diff.py +++ b/src/datumaro/cli/commands/diff.py @@ -8,7 +8,7 @@ import os.path as osp from enum import Enum, auto -from datumaro.cli.util.compare import DiffVisualizer +from datumaro.cli.util.compare import DistanceCompareVisualizer from datumaro.components.comparator import DistanceComparator, EqualityComparator from datumaro.components.errors import ProjectNotFoundError from datumaro.util import dump_json_file @@ -78,12 +78,12 @@ def build_parser(parser_ctor=argparse.ArgumentParser): formatter_class=MultilineFormatter, ) - formats = ", ".join(f.name for f in DiffVisualizer.OutputFormat) + formats = ", ".join(f.name for f in DistanceCompareVisualizer.OutputFormat) comp_methods = ", ".join(m.name for m in ComparisonMethod) def _parse_output_format(s): try: - return DiffVisualizer.OutputFormat[s.lower()] + return DistanceCompareVisualizer.OutputFormat[s.lower()] except KeyError: raise argparse.ArgumentError( "format", @@ -140,7 +140,7 @@ def _parse_comparison_method(s): "-f", "--format", type=_parse_output_format, - default=DiffVisualizer.DEFAULT_FORMAT.name, + default=DistanceCompareVisualizer.DEFAULT_FORMAT.name, help="Output format, one of {} (default: %(default)s)".format(formats), ) @@ -239,7 +239,7 @@ def diff_command(args): elif args.method is ComparisonMethod.distance: comparator = DistanceComparator(iou_threshold=args.iou_thresh) - with DiffVisualizer( + with DistanceCompareVisualizer( save_dir=dst_dir, comparator=comparator, output_format=args.format ) as visualizer: log.info("Saving diff to '%s'" % dst_dir) diff --git a/src/datumaro/cli/commands/download.py b/src/datumaro/cli/commands/download.py index 9316c14add..d37751dea5 100644 --- a/src/datumaro/cli/commands/download.py +++ b/src/datumaro/cli/commands/download.py @@ -17,7 +17,7 @@ def build_parser(parser_ctor=argparse.ArgumentParser): - builtin_writers = sorted(Environment().converters) + builtin_writers = sorted(Environment().exporters) if TFDS_EXTRACTOR_AVAILABLE: available_datasets = ", ".join(f"tfds:{name}" for name in AVAILABLE_TFDS_DATASETS) else: @@ -110,7 +110,7 @@ def download_command(args): output_format = args.output_format or default_output_format try: - converter = env.converters[output_format] + converter = env.exporters[output_format] except KeyError: raise CliException("Converter for format '%s' is not found" % output_format) extra_args = converter.parse_cmdline(args.extra_args) diff --git a/src/datumaro/cli/commands/merge.py b/src/datumaro/cli/commands/merge.py index 5188fb5825..d63c7224a4 100644 --- a/src/datumaro/cli/commands/merge.py +++ b/src/datumaro/cli/commands/merge.py @@ -203,7 +203,7 @@ def merge_command(args): env = Environment() try: - converter = env.converters[args.format] + converter = env.exporters[args.format] except KeyError: raise CliException("Converter for format '%s' is not found" % args.format) diff --git a/src/datumaro/cli/commands/patch.py b/src/datumaro/cli/commands/patch.py index 5c4090ac43..9073fef62e 100644 --- a/src/datumaro/cli/commands/patch.py +++ b/src/datumaro/cli/commands/patch.py @@ -138,7 +138,7 @@ def patch_command(args): scope_add(_project) try: - converter = env.converters[target_dataset.format] + converter = env.exporters[target_dataset.format] except KeyError: raise CliException("Converter for format '%s' is not found" % args.format) diff --git a/src/datumaro/cli/contexts/project/__init__.py b/src/datumaro/cli/contexts/project/__init__.py index aa561c8cba..d5a1411709 100644 --- a/src/datumaro/cli/contexts/project/__init__.py +++ b/src/datumaro/cli/contexts/project/__init__.py @@ -62,7 +62,7 @@ def list_options(cls): def build_export_parser(parser_ctor=argparse.ArgumentParser): - builtins = sorted(Environment().converters) + builtins = sorted(Environment().exporters) parser = parser_ctor( help="Export project", @@ -184,7 +184,7 @@ def export_command(args): env = Environment() try: - converter = env.converters[args.format] + converter = env.exporters[args.format] except KeyError: raise CliException("Converter for format '%s' is not found" % args.format) @@ -798,7 +798,7 @@ def info_command(args): print(" location:", project._root_dir) print("Plugins:") print(" extractors:", ", ".join(sorted(set(env.extractors) | set(env.importers)))) - print(" converters:", ", ".join(env.converters)) + print(" converters:", ", ".join(env.exporters)) print(" launchers:", ", ".join(env.launchers)) print("Models:") diff --git a/src/datumaro/cli/util/compare.py b/src/datumaro/cli/util/compare.py index c6e81ad9ee..d562a05163 100644 --- a/src/datumaro/cli/util/compare.py +++ b/src/datumaro/cli/util/compare.py @@ -26,7 +26,7 @@ from datumaro.util.image import save_image -class DiffVisualizer: +class DistanceCompareVisualizer: class OutputFormat(Enum): simple = auto() tensorboard = auto() diff --git a/src/datumaro/components/dataset.py b/src/datumaro/components/dataset.py index d35cb009ba..51998765c6 100644 --- a/src/datumaro/components/dataset.py +++ b/src/datumaro/components/dataset.py @@ -459,7 +459,7 @@ def export( inplace = save_dir == self._source_path and format == self._format if isinstance(format, str): - converter = self.env.converters[format] + converter = self.env.exporters[format] else: converter = format diff --git a/src/datumaro/components/environment.py b/src/datumaro/components/environment.py index eb819177dd..eadab3cf67 100644 --- a/src/datumaro/components/environment.py +++ b/src/datumaro/components/environment.py @@ -8,7 +8,7 @@ import os.path as osp from functools import partial from inspect import isclass -from typing import Callable, List, Optional +from typing import Callable, List, Optional, Sequence from datumaro.components.cli_plugin import plugin_types from datumaro.components.format_detection import RejectionReason, detect_dataset_format @@ -68,7 +68,7 @@ def launchers(self) -> PluginRegistry: return self._get_plugin_registry("_launchers") @property - def converters(self) -> PluginRegistry: + def exporters(self) -> PluginRegistry: return self._get_plugin_registry("_converters") @property @@ -176,7 +176,7 @@ def _register_plugins(self, plugins): self.extractors.batch_register(plugins) self.importers.batch_register(plugins) self.launchers.batch_register(plugins) - self.converters.batch_register(plugins) + self.exporters.batch_register(plugins) self.generators.batch_register(plugins) self.transforms.batch_register(plugins) self.validators.batch_register(plugins) @@ -191,7 +191,7 @@ def make_launcher(self, name, *args, **kwargs): return self.launchers.get(name)(*args, **kwargs) def make_converter(self, name, *args, **kwargs): - result = self.converters.get(name) + result = self.exporters.get(name) if isclass(result): result = result.convert return partial(result, *args, **kwargs) @@ -231,3 +231,27 @@ def detect_dataset( break return [format.name for format in matched_formats] + + @classmethod + def merge(cls, envs: Sequence["Environment"]) -> "Environment": + if all([env == DEFAULT_ENVIRONMENT for env in envs]): + return DEFAULT_ENVIRONMENT + + merged = Environment() + + def _register(registry: PluginRegistry): + merged._register_plugins(list(registry.items.values())) + + for env in envs: + _register(env.extractors) + _register(env.importers) + _register(env.launchers) + _register(env.exporters) + _register(env.generators) + _register(env.transforms) + _register(env.validators) + + return merged + + +DEFAULT_ENVIRONMENT = Environment() diff --git a/src/datumaro/components/hl_ops/__init__.py b/src/datumaro/components/hl_ops/__init__.py index 19f5536e25..628bfa27a5 100644 --- a/src/datumaro/components/hl_ops/__init__.py +++ b/src/datumaro/components/hl_ops/__init__.py @@ -1,211 +1,432 @@ -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2023 Intel Corporation # # SPDX-License-Identifier: MIT +from __future__ import annotations import inspect import os import os.path as osp import shutil -from typing import Dict, Optional, Type, Union +from typing import TYPE_CHECKING, Callable, Dict, Iterable, Optional, Type, Union, overload +from datumaro.cli.util.compare import DistanceCompareVisualizer +from datumaro.components.comparator import DistanceComparator, EqualityComparator, TableComparator from datumaro.components.dataset import Dataset, IDataset -from datumaro.components.dataset_item_storage import DatasetItemStorageDatasetView from datumaro.components.environment import Environment +from datumaro.components.errors import DatasetError from datumaro.components.exporter import Exporter -from datumaro.components.filter import XPathAnnotationsFilter, XPathDatasetFilter +from datumaro.components.filter import ( + UserFunctionAnnotationsFilter, + UserFunctionDatasetFilter, + XPathAnnotationsFilter, + XPathDatasetFilter, +) from datumaro.components.launcher import Launcher -from datumaro.components.merge.exact_merge import ExactMerge +from datumaro.components.merge import DEFAULT_MERGE_POLICY, get_merger from datumaro.components.transformer import ModelTransform, Transform from datumaro.components.validator import TaskType, Validator from datumaro.util import parse_str_enum_value from datumaro.util.scope import on_error_do, scoped +if TYPE_CHECKING: + from datumaro.components.annotation import Annotation + from datumaro.components.dataset_base import DatasetItem + +__all__ = ["HLOps"] + + +class HLOps: + """High-level dataset operations for Python API.""" + + @staticmethod + def compare( + first_dataset: IDataset, + second_dataset: IDataset, + report_dir: Optional[str] = None, + method: str = "table", + **kwargs, + ) -> IDataset: + """ + Compare two datasets and optionally save a comparison report. + + Args: + first_dataset (IDataset): The first dataset to compare. + second_dataset (IDataset): The second dataset to compare. + report_dir (Optional[str], optional): The directory path to save the comparison report. + Defaults to None. + method (str, optional): The comparison method to use. + Possible values are "table", "equality", "distance". Defaults to "table". + **kwargs: Additional keyword arguments that can be passed to the comparison method. + + Returns: + IDataset: The result of the comparison. + + Raises: + ValueError: If the method is "distance" and report_dir is not specified. + + Example: + comparator = Comparator() + result = comparator.compare( + first_dataset, second_dataset, report_dir="./comparison_report" + ) + print(result) + """ + if method == "table": + comparator = TableComparator() + h_table, m_table, l_table, result_dict = comparator.compare_datasets( + first_dataset, second_dataset + ) + if report_dir: + comparator.save_compare_report(h_table, m_table, l_table, result_dict, report_dir) + + elif method == "equality": + comparator = EqualityComparator(**kwargs) + output = comparator.compare_datasets(first_dataset, second_dataset) + if report_dir: + comparator.save_compare_report(output, report_dir) + + elif method == "distance": + if not report_dir: + raise ValueError( + "Please specify report_dir to save comparision result for DistanceComparator." + ) + output_format = kwargs.pop("output_format", "simple") + comparator = DistanceComparator(**kwargs) + with DistanceCompareVisualizer( + save_dir=report_dir, + comparator=comparator, + output_format=output_format, + ) as visualizer: + visualizer.save(first_dataset, second_dataset) + + return 0 + + @staticmethod + def transform( + dataset: IDataset, + method: Union[str, Type[Transform]], + *, + env: Optional[Environment] = None, + **kwargs, + ) -> IDataset: + """ + Applies some function to dataset items. + + Results are computed lazily, if the transform supports this. + + Args: + dataset: The dataset to be transformed + method: The transformation to be applied to the dataset. + If a string is passed, it is treated as a plugin name, + which is searched for in the environment + set by the 'env' argument + env: A plugin collection. If not set, the built-in plugins are used + **kwargs: Parameters for the transformation + + Returns: a wrapper around the input dataset + """ + + if isinstance(method, str): + if env is None: + env = Environment() + method = env.transforms[method] + + if not (inspect.isclass(method) and issubclass(method, Transform)): + raise TypeError(f"Unexpected 'method' argument type: {type(method)}") + + produced = method(dataset, **kwargs) + + return Dataset(source=produced, env=env) + + @overload + @staticmethod + def filter( + dataset: IDataset, + expr: str, + *, # pylint: disable=redefined-builtin + filter_annotations: bool = False, + remove_empty: bool = False, + ) -> IDataset: + """ + Filters out some dataset items or annotations, using a custom filter + expression. + + Args: + dataset: The dataset to be filtered + expr: XPath-formatted filter expression + (e.g. `/item[subset = 'train']`, `/item/annotation[label = 'cat']`) + filter_annotations: Indicates if the filter should be + applied to items or annotations + remove_empty: When filtering annotations, allows to + exclude empty items from the resulting dataset + + Returns: a wrapper around the input dataset, which is computed lazily + during iteration + """ + ... + + @overload + @staticmethod + def filter( + dataset: IDataset, + filter_func: Union[ + Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] + ], + *, # pylint: disable=redefined-builtin + filter_annotations: bool = False, + remove_empty: bool = False, + ) -> IDataset: + """ + Filters out some dataset items or annotations, using a user-provided filter + Python function. + + Results are stored in-place. Modifications are applied lazily. + + Args: + filter_func: User-provided Python function for filtering + filter_annotations: Indicates if the filter should be + applied to items or annotations + remove_empty: When filtering annotations, allows to + exclude empty items from the resulting dataset + + Returns: a wrapper around the input dataset, which is computed lazily + during iteration + + Example: + - (`filter_annotations=False`) This is an example of filtering + dataset items with images larger than 1024 pixels:: + + from datumaro.components.media import Image + + def filter_func(item: DatasetItem) -> bool: + h, w = item.media_as(Image).size + return h > 1024 or w > 1024 + + filtered = HLOps.filter( + dataset=dataset, + filter_func=filter_func, + filter_annotations=False, + ) + # No items with an image height or width greater than 1024 + filtered_items = [item for item in filtered] + + - (`filter_annotations=True`) This is an example of removing bounding boxes + sized greater than 50% of the image size:: + + from datumaro.components.media import Image + from datumaro.components.annotation import Annotation, Bbox + + def filter_func(item: DatasetItem, ann: Annotation) -> bool: + # If the annotation is not a Bbox, do not filter + if not isinstance(ann, Bbox): + return False + + h, w = item.media_as(Image).size + image_size = h * w + bbox_size = ann.h * ann.w + + # Accept Bboxes smaller than 50% of the image size + return bbox_size < 0.5 * image_size + + filtered = HLOps.filter( + dataset=dataset, + filter_func=filter_func, + filter_annotations=True, + ) + # No bounding boxes with a size greater than 50% of their image + filtered_items = [item for item in filtered] + """ + + def filter( + dataset: IDataset, + expr_or_filter_func: Union[ + str, Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] + ], + *, # pylint: disable=redefined-builtin + filter_annotations: bool = False, + remove_empty: bool = False, + ): + if isinstance(expr_or_filter_func, str): + expr = expr_or_filter_func + return ( + HLOps.transform( + dataset, XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty + ) + if filter_annotations + else HLOps.transform(dataset, XPathDatasetFilter, xpath=expr) + ) + elif callable(expr_or_filter_func): + filter_func = expr_or_filter_func + return ( + HLOps.transform( + dataset, + UserFunctionAnnotationsFilter, + filter_func=filter_func, + remove_empty=remove_empty, + ) + if filter_annotations + else HLOps.transform(dataset, UserFunctionDatasetFilter, filter_func=filter_func) + ) + raise TypeError(expr_or_filter_func) + + @staticmethod + def merge( + *datasets: Dataset, + merge_policy: str = DEFAULT_MERGE_POLICY, + report_path: Optional[str] = None, + **kwargs, + ) -> Dataset: + """ + Merge `datasets` according to `merge_policy`. + You have to choose an appropriate `merge_policy` for your purpose. + The available merge policies are "union", "intersect", and "exact". + For more details about the merge policies, please refer to :func:`get_merger`. + """ + + merger = get_merger(merge_policy, **kwargs) + merged = merger(*datasets) + env = Environment.merge( + [ + dataset.env + for dataset in datasets + if hasattr(dataset, "env") + # TODO: Sometimes, there is dataset which is not exactly "Dataset", + # e.g., VocClassificationBase. this should be fixed and every object from + # Dataset.import_from should have "Dataset" type. + ] + ) + if report_path: + merger.save_merge_report(report_path) + return Dataset(source=merged, env=env) + + @staticmethod + def run_model( + dataset: IDataset, + model: Union[Launcher, Type[ModelTransform]], + *, + batch_size: int = 1, + append_annotation: bool = False, + num_workers: int = 0, + **kwargs, + ) -> IDataset: + """ + Run the model on the dataset item media entities, such as images, + to obtain pseudo labels and add them as dataset annotations. + + Args: + dataset: The dataset to be transformed + model: The model to be applied to the dataset + batch_size: The number of dataset items processed + simultaneously by the model + append_annotation: Whether append new annotation to existed annotations + num_workers: The number of worker threads to use for parallel inference. + Set to 0 for single-process mode. Default is 0. + **kwargs: Parameters for the model + + Returns: a wrapper around the input dataset, which is computed lazily + during iteration + """ + + if isinstance(model, Launcher): + return HLOps.transform( + dataset, + ModelTransform, + launcher=model, + batch_size=batch_size, + append_annotation=append_annotation, + num_workers=num_workers, + **kwargs, + ) + elif inspect.isclass(model) and issubclass(model, ModelTransform): + return HLOps.transform( + dataset, + model, + batch_size=batch_size, + append_annotation=append_annotation, + num_workers=num_workers, + **kwargs, + ) + else: + raise TypeError(f"Unexpected model argument type: {type(model)}") + + @staticmethod + @scoped + def export( + dataset: IDataset, + path: str, + format: Union[str, Type[Exporter]], + *, + env: Optional[Environment] = None, + **kwargs, + ) -> None: + """ + Saves the input dataset in some format. + + Args: + dataset: The dataset to be saved + path: The output directory + format: The desired output format for the dataset. + If a string is passed, it is treated as a plugin name, + which is searched for in the environment set by the 'env' argument + env: A plugin collection. If not set, the built-in plugins are used + **kwargs: Parameters for the export format + """ + + if isinstance(format, str): + if env is None: + env = Environment() + exporter = env.exporters[format] + else: + exporter = format + + if not (inspect.isclass(exporter) and issubclass(exporter, Exporter)): + raise TypeError(f"Unexpected 'format' argument type: {type(exporter)}") + + path = osp.abspath(path) + if not osp.exists(path): + on_error_do(shutil.rmtree, path, ignore_errors=True) + os.makedirs(path, exist_ok=True) + + exporter.convert(dataset, save_dir=path, **kwargs) + + @staticmethod + def validate( + dataset: IDataset, + task: Union[str, TaskType], + *, + env: Optional[Environment] = None, + **kwargs, + ) -> Dict: + """ + Checks dataset annotations for correctness relatively to a task type. + + Args: + dataset: The dataset to check + task: Target task type - classification, detection etc. + env: A plugin collection. If not set, the built-in plugins are used + **kwargs: Parameters for the validator + + Returns: a dictionary with validation results + """ + + task = parse_str_enum_value(task, TaskType).name -def transform( - dataset: IDataset, - method: Union[str, Type[Transform]], - *, - env: Optional[Environment] = None, - **kwargs, -) -> IDataset: - """ - Applies some function to dataset items. - - Results are computed lazily, if the transform supports this. - - Args: - dataset: The dataset to be transformed - method: The transformation to be applied to the dataset. - If a string is passed, it is treated as a plugin name, - which is searched for in the environment - set by the 'env' argument - env: A plugin collection. If not set, the built-in plugins are used - **kwargs: Parameters for the transformation - - Returns: a wrapper around the input dataset - """ - - if isinstance(method, str): - if env is None: - env = Environment() - method = env.transforms[method] - - if not (inspect.isclass(method) and issubclass(method, Transform)): - raise TypeError("Unexpected 'method' argument type: %s" % type(method)) - - produced = method(dataset, **kwargs) - - return Dataset(source=produced, env=env) - - -def filter( - dataset: IDataset, - expr: str, - *, # pylint: disable=redefined-builtin - filter_annotations: bool = False, - remove_empty: bool = False, -) -> IDataset: - """ - Filters out some dataset items or annotations, using a custom filter - expression. - - Args: - dataset: The dataset to be filtered - expr: XPath-formatted filter expression - (e.g. `/item[subset = 'train']`, `/item/annotation[label = 'cat']`) - filter_annotations: Indicates if the filter should be - applied to items or annotations - remove_empty: When filtering annotations, allows to - exclude empty items from the resulting dataset - - Returns: a wrapper around the input dataset, which is computed lazily - during iteration - """ - - if filter_annotations: - return transform(dataset, XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty) - else: - if not expr: - return dataset - return transform(dataset, XPathDatasetFilter, xpath=expr) - - -def merge(*datasets: IDataset) -> IDataset: - """ - Merges several datasets using the "simple" (exact matching) algorithm: - - - items are matched by (id, subset) pairs - - matching items share the fields available - - - nothing + nothing = nothing, - - nothing + something = something - - something A + something B = conflict - - annotations are matched by value and shared - - in case of conflicts, throws an error - - Returns: a wrapper around the input datasets - """ - - categories = ExactMerge.merge_categories(d.categories() for d in datasets) - media_type = ExactMerge.merge_media_types(datasets) - return DatasetItemStorageDatasetView( - parent=ExactMerge.merge(datasets), - infos={}, - categories=categories, - media_type=media_type, - ann_types=None, - ) - - -def run_model( - dataset: IDataset, - model: Union[Launcher, Type[ModelTransform]], - *, - batch_size: int = 1, - **kwargs, -) -> IDataset: - """ - Applies a model to dataset items' media and produces a dataset with - media and annotations. - - Args: - dataset: The dataset to be transformed - model: The model to be applied to the dataset - batch_size: The number of dataset items processed - simultaneously by the model - **kwargs: Parameters for the model - - Returns: a wrapper around the input dataset, which is computed lazily - during iteration - """ - - if isinstance(model, Launcher): - return transform(dataset, ModelTransform, launcher=model, batch_size=batch_size, **kwargs) - elif inspect.isclass(model) and issubclass(model, ModelTransform): - return transform(dataset, model, batch_size=batch_size, **kwargs) - else: - raise TypeError("Unexpected model argument type: %s" % type(model)) - - -@scoped -def export( - dataset: IDataset, - path: str, - format: Union[str, Type[Exporter]], - *, - env: Optional[Environment] = None, - **kwargs, -) -> None: - """ - Saves the input dataset in some format. - - Args: - dataset: The dataset to be saved - path: The output directory - format: The desired output format for the dataset. - If a string is passed, it is treated as a plugin name, - which is searched for in the environment set by the 'env' argument - env: A plugin collection. If not set, the built-in plugins are used - **kwargs: Parameters for the export format - """ - - if isinstance(format, str): if env is None: env = Environment() - converter = env.converters[format] - else: - converter = format - - if not (inspect.isclass(converter) and issubclass(converter, Exporter)): - raise TypeError("Unexpected 'format' argument type: %s" % type(converter)) - - path = osp.abspath(path) - if not osp.exists(path): - on_error_do(shutil.rmtree, path, ignore_errors=True) - os.makedirs(path, exist_ok=True) - - converter.convert(dataset, save_dir=path, **kwargs) - - -def validate( - dataset: IDataset, task: Union[str, TaskType], *, env: Optional[Environment] = None, **kwargs -) -> Dict: - """ - Checks dataset annotations for correctness relatively to a task type. - - Args: - dataset: The dataset to check - task: Target task type - classification, detection etc. - env: A plugin collection. If not set, the built-in plugins are used - **kwargs: Parameters for the validator - Returns: a dictionary with validation results - """ + validator: Validator = env.validators[task](**kwargs) + return validator.validate(dataset) - task = parse_str_enum_value(task, TaskType).name + @staticmethod + def aggregate(dataset: Dataset, from_subsets: Iterable[str], to_subset: str) -> IDataset: + subset_names = set(dataset.subsets().keys()) - if env is None: - env = Environment() + for subset in from_subsets: + if subset not in subset_names: + raise DatasetError( + f"{subset} is not found in the subset names ({subset_names}) in the dataset." + ) - validator: Validator = env.validators[task](**kwargs) - return validator.validate(dataset) + return HLOps.transform( + dataset, "map_subsets", mapping={subset: to_subset for subset in from_subsets} + ) diff --git a/tests/integration/cli/test_compare.py b/tests/integration/cli/test_compare.py index 4ec90473ec..4aba75f2e6 100644 --- a/tests/integration/cli/test_compare.py +++ b/tests/integration/cli/test_compare.py @@ -4,7 +4,7 @@ import numpy as np -from datumaro.cli.util.compare import DiffVisualizer +from datumaro.cli.util.compare import DistanceCompareVisualizer from datumaro.components.annotation import ( AnnotationType, Bbox, @@ -170,7 +170,7 @@ def test_can_compare_projects(self): # just a smoke test ) with TestDir() as test_dir: - with DiffVisualizer( + with DistanceCompareVisualizer( save_dir=test_dir, comparator=DistanceComparator(iou_threshold=0.8), ) as visualizer: diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 4c8430ab1b..145b66c049 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -5,7 +5,6 @@ import numpy as np -import datumaro.components.hl_ops as hl_ops from datumaro.components.annotation import ( AnnotationType, Bbox, @@ -45,6 +44,7 @@ XPathAnnotationsFilter, XPathDatasetFilter, ) +from datumaro.components.hl_ops import HLOps from datumaro.components.launcher import Launcher from datumaro.components.media import Image, MediaElement, Video from datumaro.components.progress_reporting import NullProgressReporter @@ -386,7 +386,7 @@ def test_can_report_unknown_format_requested(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_export_by_string_format_name(self): env = Environment() - env.converters.items = {"qq": env.converters[DEFAULT_FORMAT]} + env.exporters.items = {"qq": env.exporters[DEFAULT_FORMAT]} dataset = Dataset.from_iterable( [ @@ -1688,7 +1688,7 @@ def apply(self): self._save_image(item, name=name) env = Environment() - env.converters.items = {"test": CustomExporter} + env.exporters.items = {"test": CustomExporter} with TestDir() as path: dataset = Dataset.from_iterable( @@ -2235,7 +2235,7 @@ def test_can_transform(self): [DatasetItem(10, subset="train")], categories=["cat", "dog"] ) - actual = hl_ops.transform(dataset, "reindex", start=0) + actual = HLOps.transform(dataset, "reindex", start=0) compare_datasets(self, expected, actual) @@ -2249,7 +2249,7 @@ def test_can_filter_items(self): categories=["cat", "dog"], ) - actual = hl_ops.filter(dataset, "/item[id=0]") + actual = HLOps.filter(dataset, "/item[id=0]") compare_datasets(self, expected, actual) @@ -2274,7 +2274,7 @@ def test_can_filter_annotations(self): categories=["cat", "dog"], ) - actual = hl_ops.filter( + actual = HLOps.filter( dataset, "/item/annotation[id=1]", filter_annotations=True, remove_empty=True ) @@ -2297,7 +2297,7 @@ def test_can_merge(self): [DatasetItem(1, subset="train")], categories=["cat", "dog"] ) - actual = hl_ops.merge(dataset_a, dataset_b) + actual = HLOps.merge(dataset_a, dataset_b) compare_datasets(self, expected, actual) @@ -2313,7 +2313,7 @@ def test_can_export(self): ) with TestDir() as test_dir: - hl_ops.export(dataset, test_dir, "datumaro") + HLOps.export(dataset, test_dir, "datumaro") actual = Dataset.load(test_dir) compare_datasets(self, expected, actual) diff --git a/tests/unit/test_extractor_tfds.py b/tests/unit/test_extractor_tfds.py index e8c076e876..46598356ea 100644 --- a/tests/unit/test_extractor_tfds.py +++ b/tests/unit/test_extractor_tfds.py @@ -28,7 +28,7 @@ def test_metadata(self): assert isinstance(dataset.metadata.human_name, str) assert dataset.metadata.human_name != "" - assert dataset.metadata.default_output_format in env.converters + assert dataset.metadata.default_output_format in env.exporters assert issubclass(dataset.metadata.media_type, MediaElement) From 5b191dabe8a3debaa637202ad327b7bdc9ce2e42 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 13:21:46 +0400 Subject: [PATCH 11/49] syncing components/dataset.py --- src/datumaro/components/dataset.py | 506 +++++++++++++++++--- src/datumaro/components/project.py | 2 +- tests/unit/data_formats/test_yolo_format.py | 20 +- tests/unit/test_cifar_format.py | 5 +- tests/unit/test_coco_format.py | 81 ++-- tests/unit/test_splitter.py | 2 +- tests/unit/test_voc_format.py | 9 +- 7 files changed, 517 insertions(+), 108 deletions(-) diff --git a/src/datumaro/components/dataset.py b/src/datumaro/components/dataset.py index 51998765c6..bb3fc76173 100644 --- a/src/datumaro/components/dataset.py +++ b/src/datumaro/components/dataset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2020-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -10,40 +10,70 @@ import os.path as osp import warnings from contextlib import contextmanager -from copy import copy -from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union +from copy import copy, deepcopy +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + Union, + overload, +) -from datumaro.components.annotation import AnnotationType, LabelCategories +from datumaro.components.annotation import ( + Annotation, + AnnotationType, + LabelCategories, + TabularCategories, +) from datumaro.components.config_model import Source -from datumaro.components.contexts.importer import ImportErrorPolicy, _ImportFail from datumaro.components.dataset_base import ( + DEFAULT_SUBSET_NAME, CategoriesInfo, DatasetBase, DatasetInfo, DatasetItem, IDataset, - ImportContext, ) -from datumaro.components.dataset_storage import DatasetPatch, DatasetStorage -from datumaro.components.environment import Environment +from datumaro.components.dataset_item_storage import DatasetItemStorageDatasetView +from datumaro.components.dataset_storage import DatasetPatch, DatasetStorage, StreamDatasetStorage +from datumaro.components.environment import DEFAULT_ENVIRONMENT, Environment from datumaro.components.errors import ( + DatasetImportError, + DatumaroError, MultipleFormatsMatchError, NoMatchingFormatsError, + StreamedItemError, UnknownFormatError, ) from datumaro.components.exporter import ExportContext, Exporter, ExportErrorPolicy, _ExportFail -from datumaro.components.filter import XPathAnnotationsFilter, XPathDatasetFilter +from datumaro.components.filter import ( + UserFunctionAnnotationsFilter, + UserFunctionDatasetFilter, + XPathAnnotationsFilter, + XPathDatasetFilter, +) +from datumaro.components.importer import ImportContext, ImportErrorPolicy, _ImportFail from datumaro.components.launcher import Launcher from datumaro.components.media import Image, MediaElement +from datumaro.components.merge import DEFAULT_MERGE_POLICY from datumaro.components.progress_reporting import NullProgressReporter, ProgressReporter from datumaro.components.transformer import ItemTransform, ModelTransform, Transform -from datumaro.util.definitions import DEFAULT_SUBSET_NAME from datumaro.util.log_utils import logging_disabled +from datumaro.util.meta_file_util import load_hash_key from datumaro.util.os_util import rmtree from datumaro.util.scope import on_error_do, scoped DEFAULT_FORMAT = "datumaro" +__all__ = ["Dataset", "eager_mode"] + class DatasetSubset(IDataset): # non-owning view def __init__(self, parent: Dataset, name: str): @@ -55,7 +85,9 @@ def __iter__(self): yield from self.parent._data.get_subset(self.name) def __len__(self): - return len(self.parent._data.get_subset(self.name)) + subset: DatasetItemStorageDatasetView.Subset = self.parent._data.get_subset(self.name) + + return len(subset) def put(self, item): return self.parent.put(item, subset=self.name) @@ -77,8 +109,8 @@ def subsets(self): return self.parent.subsets() return {self.name: self} - def infos(self) -> DatasetInfo: - return {} + def infos(self): + return self.parent.infos() def categories(self): return self.parent.categories() @@ -87,10 +119,23 @@ def media_type(self): return self.parent.media_type() def ann_types(self): - return set() + return self.parent.ann_types() + + def get_annotated_items(self): + return sum(bool(s.annotations) for s in self.parent._data.get_subset(self.name)) + + def get_annotations(self): + annotations_by_type = {t.name: {"count": 0} for t in AnnotationType} + for item in self.parent._data.get_subset(self.name): + for ann in item.annotations: + annotations_by_type[ann.type.name]["count"] += 1 + return sum(t["count"] for t in annotations_by_type.values()) def as_dataset(self) -> Dataset: - return Dataset.from_extractors(self, env=self.parent.env) + dataset = Dataset.from_extractors(self, env=self.parent.env) + dataset._format = self.parent._format + dataset._source_path = self.parent._source_path + return dataset class Dataset(IDataset): @@ -108,15 +153,18 @@ class Dataset(IDataset): """ _global_eager: bool = False + _stream = False @classmethod def from_iterable( cls, iterable: Iterable[DatasetItem], + infos: Optional[DatasetInfo] = None, categories: Union[CategoriesInfo, List[str], None] = None, *, env: Optional[Environment] = None, media_type: Type[MediaElement] = Image, + ann_types: Optional[Set[AnnotationType]] = [], ) -> Dataset: """ Creates a new dataset from an iterable object producing dataset items - @@ -125,6 +173,7 @@ def from_iterable( Parameters: iterable: An iterable which returns dataset items + infos: A dictionary of the dataset specific information categories: A simple list of labels or complete information about labels. If not specified, an empty list of labels is assumed. @@ -138,6 +187,9 @@ def from_iterable( dataset: A new dataset with specified contents """ + if infos is None: + infos = {} + # TODO: remove the default value for media_type # https://github.com/openvinotoolkit/datumaro/issues/675 @@ -152,18 +204,27 @@ def __init__(self): super().__init__( length=len(iterable) if hasattr(iterable, "__len__") else None, media_type=media_type, + ann_types=ann_types, ) def __iter__(self): return iter(iterable) + def infos(self): + return infos + def categories(self): return categories return cls.from_extractors(_extractor(), env=env) - @staticmethod - def from_extractors(*sources: IDataset, env: Optional[Environment] = None) -> Dataset: + @classmethod + def from_extractors( + cls, + *sources: IDataset, + env: Optional[Environment] = None, + merge_policy: str = DEFAULT_MERGE_POLICY, + ) -> Dataset: """ Creates a new dataset from one or several `Extractor`s. @@ -175,6 +236,8 @@ def from_extractors(*sources: IDataset, env: Optional[Environment] = None) -> Da sources: one or many input extractors env: A context for plugins, which will be used for this dataset. If not specified, the builtin plugins will be used. + merge_policy: Policy on how to merge multiple datasets. + Possible options are "exact", "intersect", and "union". Returns: dataset: A new dataset with contents produced by input extractors @@ -182,22 +245,22 @@ def from_extractors(*sources: IDataset, env: Optional[Environment] = None) -> Da if len(sources) == 1: source = sources[0] - dataset = Dataset(source=source, env=env) + dataset = cls(source=source, env=env) else: - from datumaro.components.merge.exact_merge import ExactMerge + from datumaro.components.hl_ops import HLOps + + return HLOps.merge(*sources, merge_policy=merge_policy) - media_type = ExactMerge.merge_media_types(sources) - source = ExactMerge.merge(sources) - categories = ExactMerge.merge_categories(s.categories() for s in sources) - dataset = Dataset(source=source, categories=categories, media_type=media_type, env=env) return dataset def __init__( self, source: Optional[IDataset] = None, *, + infos: Optional[DatasetInfo] = None, categories: Optional[CategoriesInfo] = None, media_type: Optional[Type[MediaElement]] = None, + ann_types: Optional[Set[AnnotationType]] = None, env: Optional[Environment] = None, ) -> None: super().__init__() @@ -206,7 +269,13 @@ def __init__( self._env = env self.eager = None - self._data = DatasetStorage(source, categories=categories, media_type=media_type) + self._data = DatasetStorage( + source=source, + infos=infos, + categories=categories, + media_type=media_type, + ann_types=ann_types, + ) if self.is_eager: self.init_cache() @@ -214,6 +283,27 @@ def __init__( self._source_path = None self._options = {} + def __repr__(self) -> str: + separator = "\t" + return ( + f"Dataset\n" + f"\tsize={len(self._data)}\n" + f"\tsource_path={self._source_path}\n" + f"\tmedia_type={self.media_type()}\n" + f"\tann_types={self.ann_types()}\n" + f"\tannotated_items_count={self.get_annotated_items()}\n" + f"\tannotations_count={self.get_annotations()}\n" + f"subsets\n" + f"\t{separator.join(self.get_subset_info())}" + f"infos\n" + f"\t{separator.join(self.get_infos())}" + f"categories\n" + f"\t{separator.join(self.get_categories_info())}" + ) + + def define_infos(self, infos: DatasetInfo) -> None: + self._data.define_infos(infos) + def define_categories(self, categories: CategoriesInfo) -> None: self._data.define_categories(categories) @@ -233,20 +323,68 @@ def subsets(self) -> Dict[str, DatasetSubset]: return {k: self.get_subset(k) for k in self._data.subsets()} def infos(self) -> DatasetInfo: - return {} + return self._data.infos() def categories(self) -> CategoriesInfo: return self._data.categories() - def media_type(self): + def media_type(self) -> Type[MediaElement]: return self._data.media_type() - def ann_types(self): - return set() + def ann_types(self) -> Set[AnnotationType]: + return self._data.ann_types() def get(self, id: str, subset: Optional[str] = None) -> Optional[DatasetItem]: return self._data.get(id, subset) + def get_annotated_items(self): + return self._data.get_annotated_items() + + def get_annotations(self): + return self._data.get_annotations() + + def get_datasetitem_by_path(self, path): + if self._source_path not in path: + path = osp.join(self._source_path, path) + return self._data.get_datasetitem_by_path(path) + + def get_label_cat_names(self): + return [ + label.name + for label in self._data.categories().get(AnnotationType.label, LabelCategories()) + ] + + def get_subset_info(self) -> str: + return ( + f"{subset_name}: # of items={len(self.get_subset(subset_name))}, " + f"# of annotated items={self.get_subset(subset_name).get_annotated_items()}, " + f"# of annotations={self.get_subset(subset_name).get_annotations()}\n" + for subset_name in sorted(self.subsets().keys()) + ) + + def get_infos(self) -> Tuple[str]: + if self.infos() is not None: + return (f"{k}: {v}\n" for k, v in self.infos().items()) + else: + return ("\n",) + + def get_categories_info(self) -> Tuple[str]: + category_dict = {} + for annotation_type, category in self.categories().items(): + if isinstance(category, LabelCategories): + category_names = list(category._indices.keys()) + category_dict[annotation_type] = category_names + if isinstance(category, TabularCategories): + category_names = list(category._indices_by_name.keys()) + category_dict[annotation_type] = category_names + return ( + ( + f"{str(annotation_type).split('.')[-1]}: " + f"{list(category_dict.get(annotation_type, []))}\n" + ) + for annotation_type in self.categories().keys() + ) + def __contains__(self, x: Union[DatasetItem, str, Tuple[str, str]]) -> bool: if isinstance(x, DatasetItem): x = (x.id, x.subset) @@ -270,8 +408,13 @@ def put( def remove(self, id: str, subset: Optional[str] = None) -> None: self._data.remove(id, subset) + @overload def filter( - self, expr: str, filter_annotations: bool = False, remove_empty: bool = False + self, + expr: str, + *, + filter_annotations: bool = False, + remove_empty: bool = False, ) -> Dataset: """ Filters out some dataset items or annotations, using a custom filter @@ -290,11 +433,99 @@ def filter( Returns: self """ + ... - if filter_annotations: - return self.transform(XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty) - else: - return self.transform(XPathDatasetFilter, xpath=expr) + @overload + def filter( + self, + filter_func: Union[ + Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] + ], + *, + filter_annotations: bool = False, + remove_empty: bool = False, + ) -> Dataset: + """ + Filters out some dataset items or annotations, using a user-provided filter + Python function. + + Results are stored in-place. Modifications are applied lazily. + + Args: + filter_func: User-provided Python function for filtering + filter_annotations: Indicates if the filter should be + applied to items or annotations + remove_empty: When filtering annotations, allows to + exclude empty items from the resulting dataset + + Returns: self + + Example: + - (`filter_annotations=False`) This is an example of filtering + dataset items with images larger than 1024 pixels:: + + from datumaro.components.media import Image + + def filter_func(item: DatasetItem) -> bool: + h, w = item.media_as(Image).size + return h > 1024 or w > 1024 + + filtered = dataset.filter(filter_func=filter_func, filter_annotations=False) + # No items with an image height or width greater than 1024 + filtered_items = [item for item in filtered] + + - (`filter_annotations=True`) This is an example of removing bounding boxes + sized greater than 50% of the image size:: + + from datumaro.components.media import Image + from datumaro.components.annotation import Annotation, Bbox + + def filter_func(item: DatasetItem, ann: Annotation) -> bool: + # If the annotation is not a Bbox, do not filter + if not isinstance(ann, Bbox): + return False + + h, w = item.media_as(Image).size + image_size = h * w + bbox_size = ann.h * ann.w + + # Accept Bboxes smaller than 50% of the image size + return bbox_size < 0.5 * image_size + + filtered = dataset.filter(filter_func=filter_func, filter_annotations=True) + # No bounding boxes with a size greater than 50% of their image + filtered_items = [item for item in filtered] + """ + ... + + def filter( + self, + expr_or_filter_func: Union[ + str, Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] + ], + *, + filter_annotations: bool = False, + remove_empty: bool = False, + ) -> Dataset: + if isinstance(expr_or_filter_func, str): + expr = expr_or_filter_func + return ( + self.transform(XPathAnnotationsFilter, xpath=expr, remove_empty=remove_empty) + if filter_annotations + else self.transform(XPathDatasetFilter, xpath=expr) + ) + elif callable(expr_or_filter_func): + filter_func = expr_or_filter_func + return ( + self.transform( + UserFunctionAnnotationsFilter, + filter_func=filter_func, + remove_empty=remove_empty, + ) + if filter_annotations + else self.transform(UserFunctionDatasetFilter, filter_func=filter_func) + ) + raise TypeError(expr_or_filter_func) def update(self, source: Union[DatasetPatch, IDataset, Iterable[DatasetItem]]) -> Dataset: """ @@ -344,7 +575,13 @@ def transform(self, method: Union[str, Type[Transform]], **kwargs) -> Dataset: return self def run_model( - self, model: Union[Launcher, Type[ModelTransform]], *, batch_size: int = 1, **kwargs + self, + model: Union[Launcher, Type[ModelTransform]], + *, + batch_size: int = 1, + append_annotation: bool = False, + num_workers: int = 0, + **kwargs, ) -> Dataset: """ Applies a model to dataset items' media and produces a dataset with @@ -354,15 +591,31 @@ def run_model( model: The model to be applied to the dataset batch_size: The number of dataset items processed simultaneously by the model + append_annotation: Whether append new annotation to existed annotations + num_workers: The number of worker threads to use for parallel inference. + Set to 0 for single-process mode. Default is 0. **kwargs: Parameters for the model Returns: self """ if isinstance(model, Launcher): - return self.transform(ModelTransform, launcher=model, batch_size=batch_size, **kwargs) + return self.transform( + ModelTransform, + launcher=model, + batch_size=batch_size, + append_annotation=append_annotation, + num_workers=num_workers, + **kwargs, + ) elif inspect.isclass(model) and isinstance(model, ModelTransform): - return self.transform(model, batch_size=batch_size, **kwargs) + return self.transform( + model, + batch_size=batch_size, + append_annotation=append_annotation, + num_workers=num_workers, + **kwargs, + ) else: raise TypeError("Unexpected 'model' argument type: %s" % type(model)) @@ -396,8 +649,8 @@ def get_patch(self) -> DatasetPatch: @property def env(self) -> Environment: - if not self._env: - self._env = Environment() + if self._env is None: + return DEFAULT_ENVIRONMENT return self._env @property @@ -459,12 +712,12 @@ def export( inplace = save_dir == self._source_path and format == self._format if isinstance(format, str): - converter = self.env.exporters[format] + exporter = self.env.exporters[format] else: - converter = format + exporter = format - if not (inspect.isclass(converter) and issubclass(converter, Exporter)): - raise TypeError("Unexpected 'format' argument type: %s" % type(converter)) + if not (inspect.isclass(exporter) and issubclass(exporter, Exporter)): + raise TypeError("Unexpected 'format' argument type: %s" % type(exporter)) save_dir = osp.abspath(save_dir) if not osp.exists(save_dir): @@ -478,15 +731,16 @@ def export( progress_reporter = NullProgressReporter() assert "ctx" not in kwargs - converter_kwargs = copy(kwargs) - converter_kwargs["ctx"] = ExportContext( + exporter_kwargs = copy(kwargs) + exporter_kwargs["stream"] = self._stream + exporter_kwargs["ctx"] = ExportContext( progress_reporter=progress_reporter, error_policy=error_policy ) try: if not inplace: try: - converter.convert(self, save_dir=save_dir, **converter_kwargs) + exporter.convert(self, save_dir=save_dir, **exporter_kwargs) except TypeError as e: # TODO: for backward compatibility. To be removed after 0.3 if "unexpected keyword argument 'ctx'" not in str(e): @@ -494,17 +748,17 @@ def export( if has_ctx_args: warnings.warn( - "It seems that '%s' converter " + f"It seems that '{format}' exporter " "does not support progress and error reporting, " - "it will be disabled" % format, + "It will be disabled in datumaro==1.5.0.", DeprecationWarning, ) - converter_kwargs.pop("ctx") + exporter_kwargs.pop("ctx") - converter.convert(self, save_dir=save_dir, **converter_kwargs) + exporter.convert(self, save_dir=save_dir, **exporter_kwargs) else: try: - converter.patch(self, self.get_patch(), save_dir=save_dir, **converter_kwargs) + exporter.patch(self, self.get_patch(), save_dir=save_dir, **exporter_kwargs) except TypeError as e: # TODO: for backward compatibility. To be removed after 0.3 if "unexpected keyword argument 'ctx'" not in str(e): @@ -512,19 +766,20 @@ def export( if has_ctx_args: warnings.warn( - "It seems that '%s' converter " + f"It seems that '{format}' exporter " "does not support progress and error reporting, " - "it will be disabled" % format, + "It will be disabled in datumaro==1.5.0.", DeprecationWarning, ) - converter_kwargs.pop("ctx") + exporter_kwargs.pop("ctx") - converter.patch(self, self.get_patch(), save_dir=save_dir, **converter_kwargs) + exporter.patch(self, self.get_patch(), save_dir=save_dir, **exporter_kwargs) except _ExportFail as e: raise e.__cause__ self.bind(save_dir, format, options=copy(kwargs)) - self.flush_changes() + if not self._stream: + self.flush_changes() def save(self, save_dir: Optional[str] = None, **kwargs) -> None: options = dict(self._options) @@ -566,16 +821,22 @@ def import_from( """ if env is None: - env = Environment() + env = DEFAULT_ENVIRONMENT if not format: format = cls.detect(path, env=env) + extractor_merger = None # TODO: remove importers, put this logic into extractors if format in env.importers: importer = env.make_importer(format) with logging_disabled(log.INFO): - detected_sources = importer(path, **kwargs) + detected_sources = ( + importer(path, stream=cls._stream, **kwargs) + if importer.can_stream + else importer(path, **kwargs) + ) + extractor_merger = importer.get_extractor_merger() elif format in env.extractors: detected_sources = [{"url": path, "format": format, "options": kwargs}] else: @@ -605,6 +866,8 @@ def import_from( progress_reporter=pbar, error_policy=error_policy ) + merge_policy = extractor_kwargs.get("merge_policy", DEFAULT_MERGE_POLICY) + try: extractors.append( env.make_extractor(src_conf.format, src_conf.url, **extractor_kwargs) @@ -616,9 +879,9 @@ def import_from( if has_ctx_args: warnings.warn( - "It seems that '%s' extractor " - "does not support progress and error reporting, " - "it will be disabled" % src_conf.format, + f"It seems that '{src_conf.format}' extractor " + "does not support progress and error reporting. " + "It will be disabled in datumaro==1.5.0.", DeprecationWarning, ) extractor_kwargs.pop("ctx") @@ -626,16 +889,28 @@ def import_from( extractors.append( env.make_extractor(src_conf.format, src_conf.url, **extractor_kwargs) ) - - dataset = cls.from_extractors(*extractors, env=env) + dataset = ( + cls(source=extractor_merger(extractors), env=env) + if extractor_merger is not None + else cls.from_extractors(*extractors, env=env, merge_policy=merge_policy) + ) if eager: dataset.init_cache() except _ImportFail as e: - raise e.__cause__ + cause = e.__cause__ if getattr(e, "__cause__", None) is not None else e + cause.__traceback__ = e.__traceback__ + raise DatasetImportError(f"Failed to import dataset '{format}' at '{path}'.") from cause + except DatumaroError as e: + cause = e.__cause__ if getattr(e, "__cause__", None) is not None else e + cause.__traceback__ = e.__traceback__ + raise DatasetImportError(f"Failed to import dataset '{format}' at '{path}'.") from cause + except Exception as e: + raise DatasetImportError(f"Failed to import dataset '{format}' at '{path}'.") from e dataset._source_path = path dataset._format = format + dataset = load_hash_key(path, dataset) return dataset @staticmethod @@ -667,6 +942,111 @@ def detect(path: str, *, env: Optional[Environment] = None, depth: int = 2) -> s else: return matches[0] + @property + def is_stream(self) -> bool: + return self._data.is_stream + + def clone(self) -> "Dataset": + """Create a deep copy of this dataset. + + Returns: + A cloned instance of the `Dataset`. + """ + return deepcopy(self) + + def __getitem__(self, idx: int) -> DatasetItem: + if not self._data.is_stream: + return self._data[idx] + raise StreamedItemError() + + +class StreamDataset(Dataset): + _stream = True + + def __init__( + self, + source: Optional[IDataset] = None, + *, + infos: Optional[DatasetInfo] = None, + categories: Optional[CategoriesInfo] = None, + media_type: Optional[Type[MediaElement]] = None, + ann_types: Optional[Set[AnnotationType]] = None, + env: Optional[Environment] = None, + ) -> None: + assert env is None or isinstance(env, Environment), env + self._env = env + + self._data = StreamDatasetStorage( + source, + infos=infos, + categories=categories, + media_type=media_type, + ann_types=ann_types, + ) + + self._format = DEFAULT_FORMAT + self._source_path = None + self._options = {} + + @property + def is_eager(self) -> bool: + return False + + @classmethod + def from_extractors( + cls, + *sources: IDataset, + env: Optional[Environment] = None, + merge_policy: str = DEFAULT_MERGE_POLICY, + ) -> Dataset: + """ + Creates a new dataset from one or several `Extractor`s. + + In case of a single input, creates a lazy wrapper around the input. + In case of several inputs, unifies them and caches the resulting + dataset. We cannot apply regular dataset merge, since items list cannot be accessed. + + Parameters: + sources: one or many input extractors + env: A context for plugins, which will be used for this dataset. + If not specified, the builtin plugins will be used. + merge_policy: Policy on how to merge multiple datasets. + Possible options are "exact", "intersect", and "union". + + Returns: + dataset: A new dataset with contents produced by input extractors + """ + + if len(sources) == 1: + source = sources[0] + dataset = cls(source=source, env=env) + else: + + class _MergedStreamDataset(cls): + def __init__(self, *sources: IDataset): + from datumaro.components.hl_ops import HLOps + + self._merged = HLOps.merge(*sources, merge_policy=merge_policy) + self._data = self._merged._data + self._env = env + self._format = DEFAULT_FORMAT + self._source_path = None + self._options = {} + + def __iter__(self): + yield from self._merged + + @property + def is_stream(self): + return True + + def subsets(self) -> Dict[str, DatasetSubset]: + return self._merged.subsets() + + return _MergedStreamDataset(*sources) + + return dataset + @contextmanager def eager_mode(new_mode: bool = True, dataset: Optional[Dataset] = None) -> None: diff --git a/src/datumaro/components/project.py b/src/datumaro/components/project.py index c1408937ec..4a9abfc337 100644 --- a/src/datumaro/components/project.py +++ b/src/datumaro/components/project.py @@ -893,7 +893,7 @@ def add_filter_stage( self, target: str, expr: str, params: Optional[Dict] = None, name: Optional[str] = None ): params = params or {} - params["expr"] = expr + params["expr_or_filter_func"] = expr return self.add_stage( target, { diff --git a/tests/unit/data_formats/test_yolo_format.py b/tests/unit/data_formats/test_yolo_format.py index 1a51ffda4c..6665e95d51 100644 --- a/tests/unit/data_formats/test_yolo_format.py +++ b/tests/unit/data_formats/test_yolo_format.py @@ -1641,8 +1641,10 @@ def test_can_report_missing_subset_info(self, test_dir): self._prepare_dataset(test_dir) os.remove(osp.join(test_dir, "train.txt")) - with pytest.raises(InvalidAnnotationError, match="subset list file"): + with pytest.raises(DatasetImportError) as capture: Dataset.import_from(test_dir, self.IMPORTER.NAME).init_cache() + assert isinstance(capture.value.__cause__, InvalidAnnotationError) + assert "subset list file" in str(capture.value.__cause__) class YoloUltralyticsDetectionExtractorTest(YoloExtractorTest): @@ -1663,8 +1665,10 @@ def test_can_report_missing_subset_folder(self, test_dir): shutil.copytree(get_test_asset_path("yolo_dataset", self.IMPORTER.NAME), dataset_path) shutil.rmtree(osp.join(dataset_path, "images", "train")) - with pytest.raises(InvalidAnnotationError, match="subset image folder"): + with pytest.raises(DatasetImportError) as capture: Dataset.import_from(dataset_path, self.IMPORTER.NAME).init_cache() + assert isinstance(capture.value.__cause__, InvalidAnnotationError) + assert "subset image folder" in str(capture.value.__cause__) def test_can_report_missing_ann_file(self, test_dir): # YoloUltralytics does not require annotation files @@ -1915,9 +1919,7 @@ def test_can_use_sub_labels_hint(self, test_dir, helper_tc): def test_can_report_too_many_sub_labels_in_hint(self, test_dir): self._prepare_dataset_different_skeletons(test_dir) - with pytest.raises( - InvalidAnnotationError, match="Number of points in skeletons according to config file" - ): + with pytest.raises(DatasetImportError) as capture: Dataset.import_from( test_dir, self.IMPORTER.NAME, @@ -1932,10 +1934,14 @@ def test_can_report_too_many_sub_labels_in_hint(self, test_dir): "test2": ["sub_label_1", "sub_label_2"], }, ) + assert isinstance(capture.value.__cause__, InvalidAnnotationError) + assert "Number of points in skeletons according to config file" in str( + capture.value.__cause__ + ) def test_can_report_the_lack_of_skeleton_label_in_hint(self, test_dir): self._prepare_dataset_different_skeletons(test_dir) - with pytest.raises(InvalidAnnotationError, match="Labels from config file are absent"): + with pytest.raises(DatasetImportError) as capture: Dataset.import_from( test_dir, self.IMPORTER.NAME, @@ -1943,6 +1949,8 @@ def test_can_report_the_lack_of_skeleton_label_in_hint(self, test_dir): "test2": ["sub_label_1", "sub_label_2"], }, ) + assert isinstance(capture.value.__cause__, InvalidAnnotationError) + assert "Labels from config file are absent" in str(capture.value.__cause__) def test_can_import_if_sub_label_hint_has_extra_skeletons(self, test_dir, helper_tc): source_dataset = self._prepare_dataset_different_skeletons(test_dir) diff --git a/tests/unit/test_cifar_format.py b/tests/unit/test_cifar_format.py index 186fd79014..125c2ee941 100644 --- a/tests/unit/test_cifar_format.py +++ b/tests/unit/test_cifar_format.py @@ -9,6 +9,7 @@ from datumaro.components.dataset import Dataset from datumaro.components.dataset_base import DatasetItem from datumaro.components.environment import Environment +from datumaro.components.errors import DatasetImportError from datumaro.components.media import Image from datumaro.plugins.data_formats.cifar import CifarExporter, CifarImporter @@ -257,8 +258,10 @@ def test_can_catch_pickle_exception(self): anno_file = osp.join(test_dir, "test") with open(anno_file, "wb") as file: pickle.dump(enumerate([1, 2, 3]), file) - with self.assertRaisesRegex(pickle.UnpicklingError, "Global"): + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(test_dir, "cifar") + assert isinstance(capture.exception.__cause__, pickle.UnpicklingError) + assert "Global" in str(capture.exception.__cause__) @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_save_and_load_with_meta_file(self): diff --git a/tests/unit/test_coco_format.py b/tests/unit/test_coco_format.py index a92d8e532d..b3ae58e121 100644 --- a/tests/unit/test_coco_format.py +++ b/tests/unit/test_coco_format.py @@ -902,10 +902,11 @@ def test_can_report_missing_item_field(self): anns["images"][0].pop(field) dump_json_file(ann_path, anns) - with self.assertRaises(ItemImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, MissingFieldError) - self.assertEqual(capture.exception.__cause__.name, field) + self.assertIsInstance(capture.exception.__cause__, ItemImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, MissingFieldError) + self.assertEqual(capture.exception.__cause__.__cause__.name, field) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_missing_ann_field(self): @@ -917,10 +918,11 @@ def test_can_report_missing_ann_field(self): anns["annotations"][0].pop(field) dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, MissingFieldError) - self.assertEqual(capture.exception.__cause__.name, field) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, MissingFieldError) + self.assertEqual(capture.exception.__cause__.__cause__.name, field) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_missing_global_field(self): @@ -932,9 +934,10 @@ def test_can_report_missing_global_field(self): anns.pop(field) dump_json_file(ann_path, anns) - with self.assertRaises(MissingFieldError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertEqual(capture.exception.name, field) + self.assertIsInstance(capture.exception.__cause__, MissingFieldError) + self.assertEqual(capture.exception.__cause__.name, field) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_missing_category_field(self): @@ -946,9 +949,10 @@ def test_can_report_missing_category_field(self): anns["categories"][0].pop(field) dump_json_file(ann_path, anns) - with self.assertRaises(MissingFieldError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertEqual(capture.exception.name, field) + self.assertIsInstance(capture.exception.__cause__, MissingFieldError) + self.assertEqual(capture.exception.__cause__.name, field) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_undeclared_label(self): @@ -958,10 +962,11 @@ def test_can_report_undeclared_label(self): anns["annotations"][0]["category_id"] = 2 dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, UndeclaredLabelError) - self.assertEqual(capture.exception.__cause__.id, "2") + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, UndeclaredLabelError) + self.assertEqual(capture.exception.__cause__.__cause__.id, "2") @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_bbox(self): @@ -971,10 +976,11 @@ def test_can_report_invalid_bbox(self): anns["annotations"][0]["bbox"] = [1, 2, 3, 4, 5] dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidAnnotationError) - self.assertIn("Bbox has wrong value count", str(capture.exception.__cause__)) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, InvalidAnnotationError) + self.assertIn("Bbox has wrong value count", str(capture.exception.__cause__.__cause__)) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_polygon_odd_points(self): @@ -984,10 +990,11 @@ def test_can_report_invalid_polygon_odd_points(self): anns["annotations"][0]["segmentation"] = [[1, 2, 3]] dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidAnnotationError) - self.assertIn("not divisible by 2", str(capture.exception.__cause__)) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, InvalidAnnotationError) + self.assertIn("not divisible by 2", str(capture.exception.__cause__.__cause__)) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_polygon_less_than_3_points(self): @@ -997,10 +1004,11 @@ def test_can_report_invalid_polygon_less_than_3_points(self): anns["annotations"][0]["segmentation"] = [[1, 2, 3, 4]] dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidAnnotationError) - self.assertIn("at least 3 (x, y) pairs", str(capture.exception.__cause__)) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, InvalidAnnotationError) + self.assertIn("at least 3 (x, y) pairs", str(capture.exception.__cause__.__cause__)) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_image_id(self): @@ -1010,10 +1018,11 @@ def test_can_report_invalid_image_id(self): anns["annotations"][0]["image_id"] = 10 dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidAnnotationError) - self.assertIn("Unknown image id", str(capture.exception.__cause__)) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance(capture.exception.__cause__.__cause__, InvalidAnnotationError) + self.assertIn("Unknown image id", str(capture.exception.__cause__.__cause__)) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_item_field_type(self): @@ -1025,11 +1034,14 @@ def test_can_report_invalid_item_field_type(self): anns["images"][0][field] = value dump_json_file(ann_path, anns) - with self.assertRaises(ItemImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidFieldTypeError) - self.assertEqual(capture.exception.__cause__.name, field) - self.assertEqual(capture.exception.__cause__.actual, str(type(value))) + self.assertIsInstance(capture.exception.__cause__, ItemImportError) + self.assertIsInstance( + capture.exception.__cause__.__cause__, InvalidFieldTypeError + ) + self.assertEqual(capture.exception.__cause__.__cause__.name, field) + self.assertEqual(capture.exception.__cause__.__cause__.actual, str(type(value))) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_ann_field_type(self): @@ -1049,11 +1061,14 @@ def test_can_report_invalid_ann_field_type(self): anns["annotations"][0][field] = value dump_json_file(ann_path, anns) - with self.assertRaises(AnnotationImportError) as capture: + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(ann_path, "coco_instances") - self.assertIsInstance(capture.exception.__cause__, InvalidFieldTypeError) - self.assertEqual(capture.exception.__cause__.name, field) - self.assertEqual(capture.exception.__cause__.actual, str(type(value))) + self.assertIsInstance(capture.exception.__cause__, AnnotationImportError) + self.assertIsInstance( + capture.exception.__cause__.__cause__, InvalidFieldTypeError + ) + self.assertEqual(capture.exception.__cause__.__cause__.name, field) + self.assertEqual(capture.exception.__cause__.__cause__.actual, str(type(value))) class CocoExporterTest(TestCase): diff --git a/tests/unit/test_splitter.py b/tests/unit/test_splitter.py index 31124a9c6b..8dabc41ad3 100644 --- a/tests/unit/test_splitter.py +++ b/tests/unit/test_splitter.py @@ -68,7 +68,7 @@ def _generate_dataset(self, config): ) ) categories = {AnnotationType.label: label_cat} - dataset = Dataset.from_iterable(iterable, categories) + dataset = Dataset.from_iterable(iterable, categories=categories) return dataset @mark_requirement(Requirements.DATUM_GENERAL_REQ) diff --git a/tests/unit/test_voc_format.py b/tests/unit/test_voc_format.py index e2b9d17cd7..b03fd10ebb 100644 --- a/tests/unit/test_voc_format.py +++ b/tests/unit/test_voc_format.py @@ -22,6 +22,7 @@ from datumaro.components.environment import Environment from datumaro.components.errors import ( AnnotationImportError, + DatasetImportError, InvalidAnnotationError, InvalidFieldError, ItemImportError, @@ -614,10 +615,12 @@ def test_can_report_invalid_quotes_in_lists_of_layout_task(self): with open(subset_file, "w") as f: f.write('"qwe 1\n') - with self.assertRaisesRegex( - InvalidAnnotationError, "unexpected number of quotes in filename" - ): + with self.assertRaises(DatasetImportError) as capture: Dataset.import_from(test_dir, format="voc_layout") + self.assertIsInstance(capture.exception.__cause__, InvalidAnnotationError) + self.assertIn( + "unexpected number of quotes in filename", str(capture.exception.__cause__) + ) @mark_requirement(Requirements.DATUM_ERROR_REPORTING) def test_can_report_invalid_label_in_xml(self): From 4bae04557eae61846e718ab372a0ab5647388f41 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 14:57:53 +0400 Subject: [PATCH 12/49] limiting opencv version (due to https://github.com/opencv/opencv/pull/25809) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3915468bf3..daaed2f56b 100644 --- a/setup.py +++ b/setup.py @@ -46,9 +46,9 @@ def parse_requirements(filename=CORE_REQUIREMENTS_FILE): CORE_REQUIREMENTS = parse_requirements(CORE_REQUIREMENTS_FILE) if strtobool(os.getenv("DATUMARO_HEADLESS", "0").lower()): - CORE_REQUIREMENTS.append("opencv-python-headless") + CORE_REQUIREMENTS.append("opencv-python-headless<4.11") else: - CORE_REQUIREMENTS.append("opencv-python") + CORE_REQUIREMENTS.append("opencv-python<4.11") DEFAULT_REQUIREMENTS = parse_requirements(DEFAULT_REQUIREMENTS_FILE) From 414d47a1199ece3aa3ea99a90717fbdd6e639c55 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 15:18:34 +0400 Subject: [PATCH 13/49] fixes --- .../plugins/data_formats/tf_detection_api/base.py | 15 +++++++-------- .../data_formats/tf_detection_api/exporter.py | 2 +- tests/unit/test_extractor_tfds.py | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/datumaro/plugins/data_formats/tf_detection_api/base.py b/src/datumaro/plugins/data_formats/tf_detection_api/base.py index 4592d7152e..a8e0ed3b63 100644 --- a/src/datumaro/plugins/data_formats/tf_detection_api/base.py +++ b/src/datumaro/plugins/data_formats/tf_detection_api/base.py @@ -156,15 +156,14 @@ def _parse_tfrecord_file(cls, filepath, subset, images_dir): if frame_height and frame_width: image_size = (frame_height, frame_width) - image_params = {} - if frame_image: - image_params["data"] = frame_image - if frame_filename: - image_params["path"] = osp.join(images_dir, frame_filename) - image = None - if image_params: - image = Image.from_bytes(**image_params, size=image_size) + if frame_image: + if isinstance(frame_image, np.ndarray): + image = Image.from_numpy(data=frame_image, size=image_size) + else: + image = Image.from_bytes(data=frame_image, size=image_size) + elif frame_filename: + image = Image.from_file(path=osp.join(images_dir, frame_filename), size=image_size) dataset_items.append( DatasetItem( diff --git a/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py b/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py index 34f979f3bf..3303211547 100644 --- a/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py +++ b/src/datumaro/plugins/data_formats/tf_detection_api/exporter.py @@ -211,7 +211,7 @@ def _make_tf_example(self, item): return tf_example def _save_image(self, item, path=None): # pylint: disable=arguments-differ - src_ext = item.media.ext.lower() + src_ext = item.media.ext.lower() if item.media.ext else item.media.ext dst_ext = osp.splitext(osp.basename(path))[1].lower() fmt = DetectionApiPath.IMAGE_EXT_FORMAT.get(dst_ext, "") if not fmt: diff --git a/tests/unit/test_extractor_tfds.py b/tests/unit/test_extractor_tfds.py index 46598356ea..9331259e72 100644 --- a/tests/unit/test_extractor_tfds.py +++ b/tests/unit/test_extractor_tfds.py @@ -199,9 +199,9 @@ def test_can_extract_imagenet_v2(self): DatasetItem( id=osp.splitext(example_file_name)[0], subset="train", - media=Image( + media=Image.from_numpy( data=decode_image(tfds_example["image"].numpy()), - path=example_file_name, + ext=osp.splitext(example_file_name)[1], ), annotations=[Label(tfds_example["label"].numpy())], ), From 240196ff3a91a723b4d27a236d9c94fbc79da980 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 15:41:39 +0400 Subject: [PATCH 14/49] upper case extension fix --- tests/unit/test_cvat_format.py | 2 +- tests/unit/test_kitti_format.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_cvat_format.py b/tests/unit/test_cvat_format.py index 803da1cc31..5ed4c6e7b4 100644 --- a/tests/unit/test_cvat_format.py +++ b/tests/unit/test_cvat_format.py @@ -840,7 +840,7 @@ def test_can_save_and_load_image_with_arbitrary_extension(self): test_dir, require_media=True, ) - self.assertTrue(osp.isfile(osp.join(test_dir, "images", "q", "1.JPEG"))) + self.assertTrue(osp.isfile(osp.join(test_dir, "images", "q", "1.jpeg"))) self.assertTrue(osp.isfile(osp.join(test_dir, "images", "a", "b", "c", "2.bmp"))) @mark_requirement(Requirements.DATUM_GENERAL_REQ) diff --git a/tests/unit/test_kitti_format.py b/tests/unit/test_kitti_format.py index 2c05532257..2743457f7a 100644 --- a/tests/unit/test_kitti_format.py +++ b/tests/unit/test_kitti_format.py @@ -982,7 +982,7 @@ def categories(self): osp.isfile(osp.join(test_dir, "default", KittiPath.IMAGES_DIR, "a/b/c/2.bmp")) ) self.assertTrue( - osp.isfile(osp.join(test_dir, "default", KittiPath.IMAGES_DIR, "q/1.JPEG")) + osp.isfile(osp.join(test_dir, "default", KittiPath.IMAGES_DIR, "q/1.jpeg")) ) @mark_requirement(Requirements.DATUM_GENERAL_REQ) From df5d69d8f136e2a9a1966a42f258cbc518a0815e Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Sat, 8 Feb 2025 00:00:05 +0400 Subject: [PATCH 15/49] fixes --- src/datumaro/components/dataset.py | 2 +- src/datumaro/components/media.py | 5 +++++ tests/unit/test_video.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/datumaro/components/dataset.py b/src/datumaro/components/dataset.py index bb3fc76173..a7d33c3305 100644 --- a/src/datumaro/components/dataset.py +++ b/src/datumaro/components/dataset.py @@ -109,7 +109,7 @@ def subsets(self): return self.parent.subsets() return {self.name: self} - def infos(self): + def infos(self) -> DatasetInfo: return self.parent.infos() def categories(self): diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index f38a090ba8..5f46ca1300 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -9,6 +9,7 @@ import os import os.path as osp import shutil +import weakref from copy import deepcopy from enum import IntEnum from functools import partial @@ -645,6 +646,10 @@ def __init__( self._frame_count = None self._length = None + from .media_manager import MediaManager + + MediaManager.get_instance().push(weakref.ref(self), self) + def close(self): self._iterator = None diff --git a/tests/unit/test_video.py b/tests/unit/test_video.py index 82f6a61fcb..f0314fa2cb 100644 --- a/tests/unit/test_video.py +++ b/tests/unit/test_video.py @@ -16,7 +16,7 @@ from tests.utils.video import make_sample_video -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def fxt_sample_video(): with TestDir() as test_dir: video_path = osp.join(test_dir, "video.avi") From 80ba8ade9174cc6271636dad06b192544dc84cc8 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 17:32:12 +0400 Subject: [PATCH 16/49] always keeping exif info --- src/datumaro/plugins/data_formats/yolo/base.py | 10 +--------- src/datumaro/util/image.py | 8 ++------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/datumaro/plugins/data_formats/yolo/base.py b/src/datumaro/plugins/data_formats/yolo/base.py index 9f16f2a687..11834e8884 100644 --- a/src/datumaro/plugins/data_formats/yolo/base.py +++ b/src/datumaro/plugins/data_formats/yolo/base.py @@ -39,7 +39,6 @@ DEFAULT_IMAGE_META_FILE_NAME, ImageMeta, find_images, - load_image, load_image_meta_file, ) from datumaro.util.meta_file_util import get_meta_file, has_meta_file, parse_meta_file @@ -108,10 +107,6 @@ def __init__( subset.items = self._get_lazy_subset_items(subset_name) self._subsets[subset_name] = subset - @classmethod - def _image_loader(cls, *args, **kwargs): - return load_image(*args, **kwargs, keep_exif=True) - def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]: subset = self._subsets[subset_name] item = subset.items[item_id] @@ -121,10 +116,7 @@ def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]: image_size = self._image_info.get(item_id) image_path = osp.join(self._path, item) - if image_size: - image = Image(path=image_path, size=image_size) - else: - image = Image(path=image_path, data=self._image_loader) + image = Image(path=image_path, size=image_size) annotations = self._parse_annotations(image, item_id=(item_id, subset_name)) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index 1fdab39c64..ea11f0cf4e 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -70,17 +70,13 @@ def load_image(path: str, dtype: DTypeLike = np.float32, **kwargs): with open(path, "rb") as f: image_bytes = f.read() - if kwargs.get("keep_exif"): - return decode_image(image_bytes, dtype=dtype, cv2_read_flag=1) - return decode_image(image_bytes, dtype=dtype) elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: from PIL import Image, ImageOps image = Image.open(path) - if kwargs.get("keep_exif"): - image = ImageOps.exif_transpose(image) + image = ImageOps.exif_transpose(image) image = np.asarray(image, dtype=dtype) if len(image.shape) == 3 and image.shape[2] in {3, 4}: @@ -189,7 +185,7 @@ def decode_image(image_bytes: bytes, dtype: DTypeLike = np.float32, **kwargs) -> import cv2 image = np.frombuffer(image_bytes, dtype=np.uint8) - image = cv2.imdecode(image, kwargs.get("cv2_read_flag", cv2.IMREAD_UNCHANGED)) + image = cv2.imdecode(image, cv2.IMREAD_UNCHANGED ^ cv2.IMREAD_IGNORE_ORIENTATION) image = image.astype(dtype) elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL: from PIL import Image From 0952381aa260c50bef6c316bcb57c4bc2ac7f457 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Fri, 7 Feb 2025 14:57:53 +0400 Subject: [PATCH 17/49] limiting opencv version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3915468bf3..daaed2f56b 100644 --- a/setup.py +++ b/setup.py @@ -46,9 +46,9 @@ def parse_requirements(filename=CORE_REQUIREMENTS_FILE): CORE_REQUIREMENTS = parse_requirements(CORE_REQUIREMENTS_FILE) if strtobool(os.getenv("DATUMARO_HEADLESS", "0").lower()): - CORE_REQUIREMENTS.append("opencv-python-headless") + CORE_REQUIREMENTS.append("opencv-python-headless<4.11") else: - CORE_REQUIREMENTS.append("opencv-python") + CORE_REQUIREMENTS.append("opencv-python<4.11") DEFAULT_REQUIREMENTS = parse_requirements(DEFAULT_REQUIREMENTS_FILE) From 349edd0235473b6fbb4e08882ee8e88d40ddab53 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 18:16:15 +0400 Subject: [PATCH 18/49] Update src/datumaro/components/media.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index 5f46ca1300..02edc7d22f 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -296,7 +296,7 @@ def _get_ext_to_save(self, fp: Union[str, io.IOBase], ext: Optional[str] = None) def __eq__(self, other): # Do not compare `_type` - # sicne Image is subclass of RoIImage and MosaicImage + # since Image is subclass of RoIImage and MosaicImage if not isinstance(other, __class__): return False return (np.array_equal(self.size, other.size)) and (np.array_equal(self.data, other.data)) From b3fcd83782b98f6293e319bcc38dca45a1ae2129 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 19:07:11 +0400 Subject: [PATCH 19/49] test for reading exif orientation --- tests/unit/test_image.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 81d573cda7..73d56c989d 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -5,6 +5,7 @@ import numpy as np import datumaro.util.image as image_module +import PIL from tests.requirements import Requirements, mark_requirement from tests.utils.test_utils import TestDir @@ -70,3 +71,19 @@ def test_save_image_can_create_dir(self): path = osp.join(test_dir, "some", "path.jpg") image_module.save_image(path, np.ones((5, 4, 3)), create_dir=True) self.assertTrue(osp.isfile(path)) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_load_image_with_exif_info(self): + with TestDir() as test_dir: + image_path = osp.join(test_dir, "1.jpg") + image_module.save_image(image_path, np.ones((15, 10, 3))) + + img = PIL.Image.open(image_path) + exif = img.getexif() + exif.update([(PIL.ExifTags.Base.Orientation, 6)]) + img.save(image_path, exif=exif) + + for load_backend in image_module._IMAGE_BACKENDS: + image_module._IMAGE_BACKEND = load_backend + img = image_module.load_image(image_path) + assert img.shape == (10, 15, 3) From bd3e6212a1379dd21552f02f7bdaf900b111bf41 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 19:08:45 +0400 Subject: [PATCH 20/49] changelog entry --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed3a4ee66d..614d78bc9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - Support for tracks in Ultralytics YOLO formats () +- Support for tracks in Ultralytics YOLO formats + () ### Changed - `env.detect_dataset()` now returns a list of detected formats at all recursion levels @@ -109,8 +111,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - `Dataset.get()` could ignore existing transforms in the dataset () -- Failing `resize` transform for RLE masks - () +- Always use exif orientation info when loading images + () ### Security - TBD From ec9aeda8ff7c816286405e742b9d5c475ffe46b5 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 19:16:02 +0400 Subject: [PATCH 21/49] fixed isort --- tests/unit/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 73d56c989d..45bfe1f6a7 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -3,9 +3,9 @@ from unittest import TestCase import numpy as np +import PIL import datumaro.util.image as image_module -import PIL from tests.requirements import Requirements, mark_requirement from tests.utils.test_utils import TestDir From 5a08884eb16e86d079fa7297248882601b7aaedc Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 19:30:53 +0400 Subject: [PATCH 22/49] fixed test --- tests/unit/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index d3f47e4750..cd7dced132 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -103,7 +103,7 @@ def test_load_image_with_exif_info(self): exif.update([(PIL.ExifTags.Base.Orientation, 6)]) img.save(image_path, exif=exif) - for load_backend in image_module._IMAGE_BACKENDS: - image_module._IMAGE_BACKEND = load_backend + for load_backend in image_module.ImageBackend: + image_module.IMAGE_BACKEND.set(load_backend) img = image_module.load_image(image_path) assert img.shape == (10, 15, 3) From ab794e72e087fda18a403b4f6b0b7c4d7dfcd1cf Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 19:39:52 +0400 Subject: [PATCH 23/49] fixed changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614d78bc9b..946bac4020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - Support for tracks in Ultralytics YOLO formats () -- Support for tracks in Ultralytics YOLO formats - () ### Changed - `env.detect_dataset()` now returns a list of detected formats at all recursion levels @@ -79,6 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `segment_iou()`, added a separate function argument (turned off by default) () +- Always use exif orientation info when loading images + () ### Deprecated - `--save-images` is replaced with `--save-media` in CLI and converter API @@ -111,8 +111,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - `Dataset.get()` could ignore existing transforms in the dataset () -- Always use exif orientation info when loading images - () +- Failing `resize` transform for RLE masks + () ### Security - TBD From 7730a516c00fab4f06090f18fb08b3d88f2d5294 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 23:04:55 +0400 Subject: [PATCH 24/49] Update src/datumaro/components/hl_ops/__init__.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/hl_ops/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/datumaro/components/hl_ops/__init__.py b/src/datumaro/components/hl_ops/__init__.py index 628bfa27a5..0e8704135c 100644 --- a/src/datumaro/components/hl_ops/__init__.py +++ b/src/datumaro/components/hl_ops/__init__.py @@ -65,8 +65,7 @@ def compare( ValueError: If the method is "distance" and report_dir is not specified. Example: - comparator = Comparator() - result = comparator.compare( + result = HLOps.compare( first_dataset, second_dataset, report_dir="./comparison_report" ) print(result) From 8d59db7cf9c1c10febba3ca9b630c3dda68fb28f Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 23:05:55 +0400 Subject: [PATCH 25/49] fixing filter examples --- src/datumaro/components/hl_ops/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/datumaro/components/hl_ops/__init__.py b/src/datumaro/components/hl_ops/__init__.py index 0e8704135c..051d3a222c 100644 --- a/src/datumaro/components/hl_ops/__init__.py +++ b/src/datumaro/components/hl_ops/__init__.py @@ -142,7 +142,7 @@ def transform( def filter( dataset: IDataset, expr: str, - *, # pylint: disable=redefined-builtin + /, filter_annotations: bool = False, remove_empty: bool = False, ) -> IDataset: @@ -171,7 +171,7 @@ def filter( filter_func: Union[ Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] ], - *, # pylint: disable=redefined-builtin + /, filter_annotations: bool = False, remove_empty: bool = False, ) -> IDataset: @@ -202,8 +202,8 @@ def filter_func(item: DatasetItem) -> bool: return h > 1024 or w > 1024 filtered = HLOps.filter( - dataset=dataset, - filter_func=filter_func, + dataset, + filter_func, filter_annotations=False, ) # No items with an image height or width greater than 1024 @@ -228,8 +228,8 @@ def filter_func(item: DatasetItem, ann: Annotation) -> bool: return bbox_size < 0.5 * image_size filtered = HLOps.filter( - dataset=dataset, - filter_func=filter_func, + dataset, + filter_func, filter_annotations=True, ) # No bounding boxes with a size greater than 50% of their image @@ -241,7 +241,7 @@ def filter( expr_or_filter_func: Union[ str, Callable[[DatasetItem], bool], Callable[[DatasetItem, Annotation], bool] ], - *, # pylint: disable=redefined-builtin + /, filter_annotations: bool = False, remove_empty: bool = False, ): From 5dc029044a8079b2787769518b30da43d8c37515 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Mon, 10 Feb 2025 23:15:33 +0400 Subject: [PATCH 26/49] hl_ops tests --- tests/unit/test_hl_ops.py | 178 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/unit/test_hl_ops.py diff --git a/tests/unit/test_hl_ops.py b/tests/unit/test_hl_ops.py new file mode 100644 index 0000000000..ccff61ca5d --- /dev/null +++ b/tests/unit/test_hl_ops.py @@ -0,0 +1,178 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import pytest + +from datumaro import Dataset, DatasetItem, HLOps +from datumaro.components.annotation import Bbox, Ellipse, Label, Polygon + +from tests.requirements import Requirements, mark_requirement +from tests.utils.test_utils import TestCaseHelper, TestDir +from tests.utils.test_utils import compare_datasets as _compare_datasets + + +def compare_datasets(_, expected, actual): + _compare_datasets(TestCaseHelper(), expected, actual) + + +class HLOpsTest: + def test_can_transform(self): + expected = Dataset.from_iterable( + [DatasetItem(0, subset="train")], categories=["cat", "dog"] + ) + + dataset = Dataset.from_iterable( + [DatasetItem(10, subset="train")], categories=["cat", "dog"] + ) + + actual = HLOps.transform(dataset, "reindex", start=0) + + compare_datasets(self, expected, actual) + + @pytest.mark.parametrize( + "expr_or_filter_func", + ["/item[id=0]", lambda item: str(item.id) == "0"], + ids=["xpath", "pyfunc"], + ) + def test_can_filter_items(self, expr_or_filter_func): + expected = Dataset.from_iterable( + [DatasetItem(0, subset="train")], categories=["cat", "dog"] + ) + + dataset = Dataset.from_iterable( + [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")], + categories=["cat", "dog"], + ) + + actual = HLOps.filter(dataset, expr_or_filter_func) + + compare_datasets(self, expected, actual) + + @pytest.mark.parametrize( + "expr_or_filter_func", + ["/item/annotation[id=1]", lambda item, ann: str(ann.id) == "1"], + ids=["xpath", "pyfunc"], + ) + def test_can_filter_annotations(self, expr_or_filter_func): + expected = Dataset.from_iterable( + [DatasetItem(0, subset="train", annotations=[Label(0, id=1)])], + categories=["cat", "dog"], + ) + + dataset = Dataset.from_iterable( + [ + DatasetItem( + 0, + subset="train", + annotations=[ + Label(0, id=0), + Label(0, id=1), + ], + ), + DatasetItem(1, subset="train"), + ], + categories=["cat", "dog"], + ) + + actual = HLOps.filter( + dataset, expr_or_filter_func, filter_annotations=True, remove_empty=True + ) + + compare_datasets(self, expected, actual) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_can_filter_by_annotation_types(self): + annotations = [ + Label(0, id=0), + Bbox(0, 0, 1, 1, id=1), + Polygon([0, 0, 0, 1, 1, 1], id=2), + Ellipse(0, 0, 1, 1, id=3), + ] + + dataset = Dataset.from_iterable( + [ + DatasetItem( + 0, + subset="train", + annotations=annotations, + ) + ], + ) + + types = {ann.type.name for ann in annotations} + + for t in types: + allowed_types = types - {t} + cmd = " or ".join([f"type='{allowed_type}'" for allowed_type in allowed_types]) + actual = HLOps.filter( + dataset, + f"/item/annotation[{cmd}]", + filter_annotations=True, + remove_empty=True, + ) + actual_anns = [item for item in actual][0].annotations + assert len(actual_anns) == len(allowed_types) + + def test_can_merge(self): + expected = Dataset.from_iterable( + [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")], + categories=["cat", "dog"], + ) + + dataset_a = Dataset.from_iterable( + [ + DatasetItem(0, subset="train"), + ], + categories=["cat", "dog"], + ) + + dataset_b = Dataset.from_iterable( + [DatasetItem(1, subset="train")], categories=["cat", "dog"] + ) + + actual = HLOps.merge(dataset_a, dataset_b) + + compare_datasets(self, expected, actual) + + def test_can_export(self): + expected = Dataset.from_iterable( + [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")], + categories=["cat", "dog"], + ) + + dataset = Dataset.from_iterable( + [DatasetItem(0, subset="train"), DatasetItem(1, subset="train")], + categories=["cat", "dog"], + ) + + with TestDir() as test_dir: + HLOps.export(dataset, test_dir, "datumaro") + actual = Dataset.load(test_dir) + + compare_datasets(self, expected, actual) + + def test_aggregate(self): + expected = Dataset.from_iterable( + [ + DatasetItem(0, subset="default"), + DatasetItem(1, subset="default"), + DatasetItem(2, subset="default"), + ], + categories=["cat", "dog"], + ) + + dataset = Dataset.from_iterable( + [ + DatasetItem(0, subset="train"), + DatasetItem(1, subset="val"), + DatasetItem(2, subset="test"), + ], + categories=["cat", "dog"], + ) + + actual = HLOps.aggregate( + dataset, from_subsets=["train", "val", "test"], to_subset="default" + ) + + compare_datasets(self, expected, actual) From d27b0b53b68ede5ef0cac6c7764594b0cae269a1 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 15:56:03 +0400 Subject: [PATCH 27/49] Update src/datumaro/components/media.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index 02edc7d22f..bccd50c1cf 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -515,7 +515,7 @@ def __getstate__(self): def __setstate__(self, state): # Restore the objects' state. self.__dict__.update(state) - # Reinitialize unpichlable attributes + # Reinitialize unpicklable attributes self._data = lambda: self._video.get_frame_data(self._index) From f71623b61abbe698cb493785a7792f50bf05425f Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 16:25:52 +0400 Subject: [PATCH 28/49] Update src/datumaro/components/media.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/media.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index bccd50c1cf..2ddd90659b 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -618,6 +618,15 @@ def __init__( *args, **kwargs, ) -> None: + """ + Parameters: + path: the video file path + step: frame step + start_frame: the first included frame index + end_frame: the last included frame index. If 'step' is also specified, + 'end_frame' is only included if it belongs to the sequence, + starting from the 'start_frame'. + """ super().__init__(*args, **kwargs) self._path = path From 0dd000217bfb0439739d19fd0a8cfd9489a8d540 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 16:26:07 +0400 Subject: [PATCH 29/49] Update src/datumaro/components/media.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index 2ddd90659b..1ae9882bf9 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -747,7 +747,7 @@ def _get_frame_size(self) -> Tuple[int, int]: return frame_size def _get_end_frame(self): - # Note that end_frame could less than the last frame of the video + # Note that end_frame could be less than the last frame of the video if self._end_frame is not None and self._frame_count is not None: end_frame = min(self._end_frame, self._frame_count) elif self._end_frame is not None: From 911660773781aa817bf23c32e39ac6452bfb0257 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 16:26:24 +0400 Subject: [PATCH 30/49] Update tests/unit/test_video.py Co-authored-by: Maxim Zhiltsov --- tests/unit/test_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_video.py b/tests/unit/test_video.py index f0314fa2cb..2dca820649 100644 --- a/tests/unit/test_video.py +++ b/tests/unit/test_video.py @@ -16,7 +16,7 @@ from tests.utils.video import make_sample_video -@pytest.fixture(scope="function") +@pytest.fixture def fxt_sample_video(): with TestDir() as test_dir: video_path = osp.join(test_dir, "video.avi") From 2f74586c072ff416f29c8e9a80acb202fb00d77a Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 17:24:29 +0400 Subject: [PATCH 31/49] Update src/datumaro/components/registry.py Co-authored-by: Maxim Zhiltsov --- src/datumaro/components/registry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/datumaro/components/registry.py b/src/datumaro/components/registry.py index 69f3640fd4..97c9fbe62a 100644 --- a/src/datumaro/components/registry.py +++ b/src/datumaro/components/registry.py @@ -1,3 +1,7 @@ +# Copyright (C) 2022 Intel Corporation +# +# SPDX-License-Identifier: MIT + from typing import Callable, Dict, Generic, Iterable, Iterator, Optional, Type, TypeVar from datumaro.components.cli_plugin import CliPlugin From 41a4a1488e74bd46d2b101aafac44b50503fae7e Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Tue, 11 Feb 2025 23:20:48 +0400 Subject: [PATCH 32/49] fixes --- src/datumaro/components/importer.py | 72 ----------------------------- src/datumaro/util/mask_tools.py | 21 +++++---- 2 files changed, 11 insertions(+), 82 deletions(-) diff --git a/src/datumaro/components/importer.py b/src/datumaro/components/importer.py index 84e46c6643..530c498464 100644 --- a/src/datumaro/components/importer.py +++ b/src/datumaro/components/importer.py @@ -31,7 +31,6 @@ "NullImportContext", "_ImportFail", "Importer", - "with_subset_dirs", "ImportErrorPolicy", "FailingImportErrorPolicy", ] @@ -169,74 +168,3 @@ def get_extractor_merger(self) -> Optional[Type[ExtractorMerger]]: Otherwise, use the return type to merge the extractors. """ return None - - -def with_subset_dirs(input_cls: Importer): - @wraps(input_cls, updated=()) - class WrappedImporter(input_cls): - NAME = input_cls.NAME - - @classmethod - def detect( - cls, - context: FormatDetectionContext, - ) -> Optional[FormatDetectionConfidence]: - @contextmanager - def _change_context_root_path(context: FormatDetectionContext, path: str): - tmp = context.root_path - context._root_path = path - yield - context._root_path = tmp - - confs = [] - path = context.root_path - - if not osp.isdir(path): - context.fail( - f"{input_cls.NAME} should require an input as a directory path. " - f"However, {path} is not a directory path." - ) - - for sub_dir in os.listdir(path): - if sub_dir.lower() not in SUBSET_NAME_WHITELIST: - continue - - sub_path = osp.join(path, sub_dir) - if osp.isdir(sub_path): - with _change_context_root_path(context, sub_path): - conf = input_cls.detect(context) - if conf is not None: - confs += [conf] - - if len(confs) == 0: - context.fail(f"{input_cls.NAME} cannot find its subdirectory structure.") - - return max(confs) - - def __call__(self, path, **extra_params): - sources = [] - for sub_dir in os.listdir(path): - sub_path = osp.join(path, sub_dir) - if osp.isdir(sub_path): - source = input_cls.__call__(self, sub_path, **extra_params) - - if len(source) != 1: - raise DatasetImportError( - f"@with_subset_dirs only allows one source format from {sub_path}." - ) - - if "subset" in source[0]: - raise DatasetImportError( - f"@with_subset_dirs does not allows " - f"a subset key in source: {source[0]}." - ) - - source[0]["options"]["subset"] = sub_dir - sources += source - - return sources - - def __reduce__(self): - return (input_cls.__class__, ()) - - return WrappedImporter diff --git a/src/datumaro/util/mask_tools.py b/src/datumaro/util/mask_tools.py index 7bac21b9ba..86bd878bfe 100644 --- a/src/datumaro/util/mask_tools.py +++ b/src/datumaro/util/mask_tools.py @@ -4,14 +4,20 @@ from functools import partial from itertools import chain, repeat -from typing import Dict, List, NamedTuple, NewType, Optional, Sequence, Tuple, TypedDict, Union +from typing import TYPE_CHECKING, Dict, List, NamedTuple, NewType, Optional, Sequence, Tuple, TypedDict, Union import numpy as np -from pycocotools import mask as pycocotools_mask from datumaro._capi import encode from datumaro.util.image import lazy_image, load_image +if TYPE_CHECKING: + from pycocotools import mask as pycocotools_mask +else: + from datumaro.util.import_util import lazy_import + + pycocotools_mask = lazy_import("pycocotools.mask") + class UncompressedRle(TypedDict): size: Sequence[int] @@ -316,15 +322,12 @@ def is_outside_contour(index): return parent_to_children -def extract_contours(mask: np.ndarray) -> List[np.ndarray]: +def extract_contours(mask: BinaryMask) -> List[Polygon]: """ Convert an instance mask to polygons Args: mask: a 2d binary mask - tolerance: maximum distance from original points of - a polygon to the approximated ones - area_threshold: minimal area of generated polygons Returns: A list of polygons like [[x1,y1, x2,y2 ...], [...]] @@ -368,8 +371,6 @@ def mask_to_polygons(mask: BinaryMask, area_threshold=1) -> List[Polygon]: Args: mask: a 2d binary mask - tolerance: maximum distance from original points of - a polygon to the approximated ones area_threshold: minimal area of generated polygons Returns: @@ -407,7 +408,7 @@ def to_uncompressed_rle(rle: Rle, *, width: int, height: int) -> UncompressedRle return pycocotools_mask.frPyObjects(rle, height, width) -def mask_to_bboxes(mask): +def mask_to_bboxes(mask: BinaryMask) -> List[List[int]]: """ Convert an instance mask to bboxes @@ -532,7 +533,7 @@ def rles_to_mask(rles: Sequence[Union[CompressedRle, Polygon]], width, height) - return mask -def rle_to_mask(rle_uncompressed: Dict[str, np.ndarray]) -> np.ndarray: +def rle_to_mask(rle_uncompressed: UncompressedRle) -> BinaryMask: """Decode the uncompressed RLE string to the binary mask (2D np.ndarray) The uncompressed RLE string can be obtained by From dc31a96aa36f02867bc23589ba65af42907d9042 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 12 Feb 2025 00:16:38 +0400 Subject: [PATCH 33/49] fix linters --- src/datumaro/components/importer.py | 3 --- src/datumaro/util/mask_tools.py | 13 ++++++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/datumaro/components/importer.py b/src/datumaro/components/importer.py index 530c498464..73f09fe373 100644 --- a/src/datumaro/components/importer.py +++ b/src/datumaro/components/importer.py @@ -6,8 +6,6 @@ import os import os.path as osp -from contextlib import contextmanager -from functools import wraps from glob import iglob from typing import Callable, Dict, List, Optional, Type, TypeVar @@ -22,7 +20,6 @@ from datumaro.components.errors import DatasetImportError, DatasetNotFoundError from datumaro.components.format_detection import FormatDetectionConfidence, FormatDetectionContext from datumaro.components.merge.extractor_merger import ExtractorMerger -from datumaro.util.definitions import SUBSET_NAME_WHITELIST T = TypeVar("T") diff --git a/src/datumaro/util/mask_tools.py b/src/datumaro/util/mask_tools.py index 86bd878bfe..ec1e85bba9 100644 --- a/src/datumaro/util/mask_tools.py +++ b/src/datumaro/util/mask_tools.py @@ -4,7 +4,18 @@ from functools import partial from itertools import chain, repeat -from typing import TYPE_CHECKING, Dict, List, NamedTuple, NewType, Optional, Sequence, Tuple, TypedDict, Union +from typing import ( + TYPE_CHECKING, + Dict, + List, + NamedTuple, + NewType, + Optional, + Sequence, + Tuple, + TypedDict, + Union, +) import numpy as np From 6ae1bce9a1ee11c85b9b4ffefc29dbb91dbe3681 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 12 Feb 2025 02:24:03 +0400 Subject: [PATCH 34/49] tests for HLOps.compare --- tests/unit/test_hl_ops.py | 94 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_hl_ops.py b/tests/unit/test_hl_ops.py index ccff61ca5d..1b53b204e4 100644 --- a/tests/unit/test_hl_ops.py +++ b/tests/unit/test_hl_ops.py @@ -1,10 +1,13 @@ # Copyright (C) 2023 Intel Corporation # # SPDX-License-Identifier: MIT +import json +import os +import numpy as np import pytest -from datumaro import Dataset, DatasetItem, HLOps +from datumaro import Dataset, DatasetItem, HLOps, Image from datumaro.components.annotation import Bbox, Ellipse, Label, Polygon from tests.requirements import Requirements, mark_requirement @@ -176,3 +179,92 @@ def test_aggregate(self): ) compare_datasets(self, expected, actual) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_compare_equality(self): + dataset1 = Dataset.from_iterable( + [ + DatasetItem( + id=100, + subset="train", + annotations=[ + Bbox(1, 2, 3, 4, label=0), + ], + ), + DatasetItem(id=200, subset="train"), + ], + categories=["a", "b"], + ) + + dataset2 = Dataset.from_iterable( + [ + DatasetItem( + id=100, + subset="train", + annotations=[ + Bbox(1, 2, 3, 4, label=1), + Bbox(5, 6, 7, 8, label=2), + ], + ), + ], + categories=["a", "b", "c"], + ) + with TestDir() as test_dir: + HLOps.compare(dataset1, dataset2, method="equality", report_dir=test_dir) + report_file = os.path.join(test_dir, "equality_compare.json") + assert os.path.exists(report_file) + with open(report_file, "r") as f: + report = json.load(f) + assert report["a_extra_items"] == [["200", "train"]] + assert report["b_extra_items"] == [] + assert report["mismatches"] == [] + assert len(report["errors"]) == 1 + assert report["errors"][0]["type"] == "labels" + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_compare_table(self): + dataset1 = Dataset.from_iterable( + [ + DatasetItem( + id=100, + subset="train", + media=Image.from_numpy(np.ones((10, 15, 3))), + annotations=[ + Bbox(1, 2, 3, 4, label=0), + ], + ), + DatasetItem(id=200, media=Image.from_numpy(np.ones((10, 15, 3))), subset="train"), + ], + categories=["a", "b"], + ) + + dataset2 = Dataset.from_iterable( + [ + DatasetItem( + id=100, + subset="train", + media=Image.from_numpy(np.ones((10, 15, 3))), + annotations=[ + Bbox(1, 2, 3, 4, label=1), + Bbox(5, 6, 7, 8, label=2), + ], + ), + ], + categories=["a", "b", "c"], + ) + with TestDir() as test_dir: + HLOps.compare(dataset1, dataset2, method="table", report_dir=test_dir) + assert os.path.exists(os.path.join(test_dir, "table_compare.json")) + assert os.path.exists(os.path.join(test_dir, "table_compare.txt")) + with open(os.path.join(test_dir, "table_compare.json"), "r") as f: + report = json.load(f) + assert report["high_level"] == { + "Number of classes": ["2", "3"], + "Common classes": ["a, b", "a, b"], + "Classes": ["a, b", "a, b, c"], + "Images count": ["2", "1"], + "Unique images count": ["1", "1"], + "Repeated images count": ["1", "0"], + "Annotations count": ["1", "2"], + "Unannotated images count": ["1", "0"], + } From d1b3036180bd047935f89fbd9fc1caa6eebb1039 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 12 Feb 2025 10:54:59 +0400 Subject: [PATCH 35/49] syncing tests/unit/test_image.py --- tests/unit/test_image.py | 57 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index cd7dced132..6f5f2193e3 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -1,13 +1,19 @@ +# Copyright (C) 2019-2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + import os.path as osp from itertools import product from unittest import TestCase import numpy as np import PIL +import pytest import datumaro.util.image as image_module -from tests.requirements import Requirements, mark_requirement +from ..requirements import Requirements, mark_requirement + from tests.utils.test_utils import TestDir @@ -107,3 +113,52 @@ def test_load_image_with_exif_info(self): image_module.IMAGE_BACKEND.set(load_backend) img = image_module.load_image(image_path) assert img.shape == (10, 15, 3) + + +class ImageDecodeTest: + @pytest.fixture + def fxt_img_four_channels(self) -> np.ndarray: + return np.random.randint(low=0, high=256, size=(5, 4, 4), dtype=np.uint8) + + @pytest.mark.parametrize( + "image_backend", [image_module.ImageBackend.cv2, image_module.ImageBackend.PIL] + ) + def test_decode_image_context( + self, fxt_img_four_channels: np.ndarray, image_backend: image_module.ImageBackend + ): + img_bytes = image_module.encode_image(fxt_img_four_channels, ".png") + + # 3 channels from ImageColorScale.COLOR_BGR + with image_module.decode_image_context( + image_backend, image_module.ImageColorChannel.COLOR_BGR + ): + img_decoded = image_module.decode_image(img_bytes) + assert img_decoded.shape[-1] == 3 + assert np.allclose(fxt_img_four_channels[:, :, :3], img_decoded) + + # 3 channels from ImageColorScale.COLOR_RGB + with image_module.decode_image_context( + image_backend, image_module.ImageColorChannel.COLOR_RGB + ): + img_decoded = image_module.decode_image(img_bytes) + assert img_decoded.shape[-1] == 3 + assert np.allclose( + fxt_img_four_channels[:, :, :3][:, :, ::-1], # Flip color channels of the fixture + img_decoded, + ) + + # 4 channels from ImageColorScale.UNCHANGED + with image_module.decode_image_context( + image_backend, image_module.ImageColorChannel.UNCHANGED + ): + img_decoded = image_module.decode_image(img_bytes) + assert img_decoded.shape[-1] == 4 + + if image_backend == image_module.ImageBackend.cv2: + assert np.allclose(fxt_img_four_channels, img_decoded) + else: + # PIL will return RGBA, thus we need to correct the fixture + to_rgb = fxt_img_four_channels[:, :, :3][:, :, ::-1] + fxt_img_four_channels[:, :, :3] = to_rgb + + assert np.allclose(fxt_img_four_channels, img_decoded) From 255f499c2920100a7de6dea227a8de9c57d75d5e Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Wed, 12 Feb 2025 10:56:50 +0400 Subject: [PATCH 36/49] accounting for the new flag in cv2 --- setup.py | 4 ++-- src/datumaro/util/image.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index daaed2f56b..3915468bf3 100644 --- a/setup.py +++ b/setup.py @@ -46,9 +46,9 @@ def parse_requirements(filename=CORE_REQUIREMENTS_FILE): CORE_REQUIREMENTS = parse_requirements(CORE_REQUIREMENTS_FILE) if strtobool(os.getenv("DATUMARO_HEADLESS", "0").lower()): - CORE_REQUIREMENTS.append("opencv-python-headless<4.11") + CORE_REQUIREMENTS.append("opencv-python-headless") else: - CORE_REQUIREMENTS.append("opencv-python<4.11") + CORE_REQUIREMENTS.append("opencv-python") DEFAULT_REQUIREMENTS = parse_requirements(DEFAULT_REQUIREMENTS_FILE) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index ced4a77619..1d7da69c59 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -67,8 +67,19 @@ def decode_by_cv2(self, image_bytes: bytes, dtype: DTypeLike = np.uint8) -> np.n """Convert image color channel for OpenCV image (np.ndarray).""" image_buffer = np.frombuffer(image_bytes, dtype=dtype) + try: + from cv2 import IMREAD_COLOR_RGB + except ImportError: + IMREAD_COLOR_RGB = 0 + if self == ImageColorChannel.UNCHANGED: - return cv2.imdecode(image_buffer, cv2.IMREAD_UNCHANGED ^ cv2.IMREAD_IGNORE_ORIENTATION) + return cv2.imdecode( + image_buffer, + cv2.IMREAD_UNCHANGED ^ cv2.IMREAD_IGNORE_ORIENTATION ^ IMREAD_COLOR_RGB, + ) + + if IMREAD_COLOR_RGB and self == ImageColorChannel.COLOR_RGB: + return cv2.imdecode(image_buffer, IMREAD_COLOR_RGB) img = cv2.imdecode(image_buffer, cv2.IMREAD_COLOR) From 50e8901320c05ea9ae86c73f62bb1cbec0fb205a Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 11:41:00 +0400 Subject: [PATCH 37/49] fixes --- src/datumaro/util/image.py | 5 +---- tests/unit/test_image.py | 38 ++++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index 1d7da69c59..5735f056b7 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -67,10 +67,7 @@ def decode_by_cv2(self, image_bytes: bytes, dtype: DTypeLike = np.uint8) -> np.n """Convert image color channel for OpenCV image (np.ndarray).""" image_buffer = np.frombuffer(image_bytes, dtype=dtype) - try: - from cv2 import IMREAD_COLOR_RGB - except ImportError: - IMREAD_COLOR_RGB = 0 + IMREAD_COLOR_RGB = getattr(cv2, "IMREAD_COLOR_RGB", 0) if self == ImageColorChannel.UNCHANGED: return cv2.imdecode( diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 6f5f2193e3..279a306b12 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -116,17 +116,20 @@ def test_load_image_with_exif_info(self): class ImageDecodeTest: - @pytest.fixture - def fxt_img_four_channels(self) -> np.ndarray: - return np.random.randint(low=0, high=256, size=(5, 4, 4), dtype=np.uint8) + def generate_test_img(self, channels) -> np.ndarray: + return np.random.randint(low=0, high=256, size=(5, 4, channels), dtype=np.uint8) @pytest.mark.parametrize( "image_backend", [image_module.ImageBackend.cv2, image_module.ImageBackend.PIL] ) - def test_decode_image_context( - self, fxt_img_four_channels: np.ndarray, image_backend: image_module.ImageBackend - ): - img_bytes = image_module.encode_image(fxt_img_four_channels, ".png") + @pytest.mark.parametrize("channels", [1, 3, 4]) + def test_decode_image_context(self, image_backend: image_module.ImageBackend, channels: int): + original_image = self.generate_test_img(channels) + img_bytes = image_module.encode_image(original_image, ".png") + + expected_bgr_image = ( + original_image[:, :, :3] if channels >= 3 else np.repeat(original_image, 3, axis=2) + ) # 3 channels from ImageColorScale.COLOR_BGR with image_module.decode_image_context( @@ -134,7 +137,7 @@ def test_decode_image_context( ): img_decoded = image_module.decode_image(img_bytes) assert img_decoded.shape[-1] == 3 - assert np.allclose(fxt_img_four_channels[:, :, :3], img_decoded) + assert np.allclose(expected_bgr_image, img_decoded) # 3 channels from ImageColorScale.COLOR_RGB with image_module.decode_image_context( @@ -142,23 +145,22 @@ def test_decode_image_context( ): img_decoded = image_module.decode_image(img_bytes) assert img_decoded.shape[-1] == 3 - assert np.allclose( - fxt_img_four_channels[:, :, :3][:, :, ::-1], # Flip color channels of the fixture - img_decoded, - ) + assert np.allclose(expected_bgr_image[:, :, ::-1], img_decoded) # 4 channels from ImageColorScale.UNCHANGED with image_module.decode_image_context( image_backend, image_module.ImageColorChannel.UNCHANGED ): img_decoded = image_module.decode_image(img_bytes) - assert img_decoded.shape[-1] == 4 + if len(img_decoded.shape) == 2: + img_decoded = img_decoded[:, :, np.newaxis] + assert img_decoded.shape[-1] == channels - if image_backend == image_module.ImageBackend.cv2: - assert np.allclose(fxt_img_four_channels, img_decoded) + if image_backend == image_module.ImageBackend.cv2 or channels == 1: + assert np.allclose(original_image, img_decoded) else: # PIL will return RGBA, thus we need to correct the fixture - to_rgb = fxt_img_four_channels[:, :, :3][:, :, ::-1] - fxt_img_four_channels[:, :, :3] = to_rgb + to_rgb = original_image[:, :, :3][:, :, ::-1] + original_image[:, :, :3] = to_rgb - assert np.allclose(fxt_img_four_channels, img_decoded) + assert np.allclose(original_image, img_decoded) From 38b113e3288bf1e4ebdbd025683865d0ba608196 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 12:11:37 +0400 Subject: [PATCH 38/49] fixes --- src/datumaro/util/mask_tools.py | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/datumaro/util/mask_tools.py b/src/datumaro/util/mask_tools.py index ec1e85bba9..e3ff381866 100644 --- a/src/datumaro/util/mask_tools.py +++ b/src/datumaro/util/mask_tools.py @@ -20,6 +20,7 @@ import numpy as np from datumaro._capi import encode +from datumaro.util.definitions import BboxIntCoords from datumaro.util.image import lazy_image, load_image if TYPE_CHECKING: @@ -261,24 +262,6 @@ def mask_to_rle(binary_mask: BinaryMask) -> CompressedRle: return encode(binary_mask) -def mask_to_rle_py(binary_mask: BinaryMask) -> CompressedRle: - # walk in row-major order as COCO format specifies - bounded = binary_mask.ravel(order="F") - - # add borders to sequence - # find boundary positions for sequences and compute their lengths - difs = np.diff(bounded, prepend=[1 - bounded[0]], append=[1 - bounded[-1]]) - (counts,) = np.where(difs != 0) - - # start RLE encoding from 0 as COCO format specifies - if bounded[0] != 0: - counts = np.diff(counts, prepend=[0]) - else: - counts = np.diff(counts) - - return {"counts": counts, "size": list(binary_mask.shape)} - - def _is_contour_clockwise(contour: np.ndarray) -> bool: area = sum( (p2[0] - p1[0]) * (p2[1] + p1[1]) # doubled area under the line, (x2-x1)*((y2+y1)/2) @@ -419,7 +402,7 @@ def to_uncompressed_rle(rle: Rle, *, width: int, height: int) -> UncompressedRle return pycocotools_mask.frPyObjects(rle, height, width) -def mask_to_bboxes(mask: BinaryMask) -> List[List[int]]: +def mask_to_bboxes(mask: BinaryMask) -> List[BboxIntCoords]: """ Convert an instance mask to bboxes @@ -544,17 +527,6 @@ def rles_to_mask(rles: Sequence[Union[CompressedRle, Polygon]], width, height) - return mask -def rle_to_mask(rle_uncompressed: UncompressedRle) -> BinaryMask: - """Decode the uncompressed RLE string to the binary mask (2D np.ndarray) - - The uncompressed RLE string can be obtained by - the datumaro.util.mask_tools.mask_to_rle() function - """ - resulting_mask = pycocotools_mask.frPyObjects(rle_uncompressed, *rle_uncompressed["size"]) - resulting_mask = pycocotools_mask.decode(resulting_mask) - return resulting_mask - - def find_mask_bbox(mask: BinaryMask) -> BboxCoords: cols = np.any(mask, axis=0) rows = np.any(mask, axis=1) From a6d977f41b6ee51e0764b105fee4a66b55b21742 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 14:22:37 +0400 Subject: [PATCH 39/49] fixes --- src/datumaro/util/image.py | 1 + tests/unit/test_image.py | 35 ++++++++++++++--------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index 5735f056b7..7d503482b6 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -67,6 +67,7 @@ def decode_by_cv2(self, image_bytes: bytes, dtype: DTypeLike = np.uint8) -> np.n """Convert image color channel for OpenCV image (np.ndarray).""" image_buffer = np.frombuffer(image_bytes, dtype=dtype) + # OpenCV 4.11 added the new imread flag, so we will use it if available IMREAD_COLOR_RGB = getattr(cv2, "IMREAD_COLOR_RGB", 0) if self == ImageColorChannel.UNCHANGED: diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 279a306b12..4a616a1e38 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -17,6 +17,11 @@ from tests.utils.test_utils import TestDir +def generate_test_img(channels: int) -> np.ndarray: + size = (5, 4, channels) if channels > 1 else (5, 4) + return np.random.randint(low=0, high=256, size=size, dtype=np.uint8) + + class ImageOperationsTest(TestCase): def setUp(self): self.default_backend = image_module.IMAGE_BACKEND.get() @@ -29,10 +34,7 @@ def test_save_and_load_backends(self): backends = image_module.ImageBackend for save_backend, load_backend, c in product(backends, backends, [1, 3]): with TestDir() as test_dir: - if c == 1: - src_image = np.random.randint(0, 255 + 1, (2, 4)) - else: - src_image = np.random.randint(0, 255 + 1, (2, 4, c)) + src_image = generate_test_img(c) path = osp.join(test_dir, "img.png") # lossless image_module.IMAGE_BACKEND.set(save_backend) @@ -60,10 +62,7 @@ def test_save_and_load_backends(self): def test_encode_and_decode_backends(self): backends = image_module.ImageBackend for save_backend, load_backend, c in product(backends, backends, [1, 3]): - if c == 1: - src_image = np.random.randint(0, 255 + 1, (2, 4)) - else: - src_image = np.random.randint(0, 255 + 1, (2, 4, c)) + src_image = generate_test_img(c) image_module.IMAGE_BACKEND.set(save_backend) buffer = image_module.encode_image(src_image, ".png", jpeg_quality=100) # lossless @@ -116,20 +115,16 @@ def test_load_image_with_exif_info(self): class ImageDecodeTest: - def generate_test_img(self, channels) -> np.ndarray: - return np.random.randint(low=0, high=256, size=(5, 4, channels), dtype=np.uint8) - - @pytest.mark.parametrize( - "image_backend", [image_module.ImageBackend.cv2, image_module.ImageBackend.PIL] - ) + @pytest.mark.parametrize("image_backend", image_module.ImageBackend) @pytest.mark.parametrize("channels", [1, 3, 4]) def test_decode_image_context(self, image_backend: image_module.ImageBackend, channels: int): - original_image = self.generate_test_img(channels) + original_image = generate_test_img(channels) img_bytes = image_module.encode_image(original_image, ".png") - expected_bgr_image = ( - original_image[:, :, :3] if channels >= 3 else np.repeat(original_image, 3, axis=2) - ) + if channels == 1: + expected_bgr_image = np.repeat(original_image[:, :, np.newaxis], 3, axis=2) + else: + expected_bgr_image = original_image[:, :, :3] # 3 channels from ImageColorScale.COLOR_BGR with image_module.decode_image_context( @@ -152,9 +147,7 @@ def test_decode_image_context(self, image_backend: image_module.ImageBackend, ch image_backend, image_module.ImageColorChannel.UNCHANGED ): img_decoded = image_module.decode_image(img_bytes) - if len(img_decoded.shape) == 2: - img_decoded = img_decoded[:, :, np.newaxis] - assert img_decoded.shape[-1] == channels + assert img_decoded.shape == original_image.shape if image_backend == image_module.ImageBackend.cv2 or channels == 1: assert np.allclose(original_image, img_decoded) From 9fe1e806796e6b6ae292547bf64db03ab35d16b2 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 15:15:45 +0400 Subject: [PATCH 40/49] fixes --- src/datumaro/util/annotation_util.py | 20 ++++++++---------- src/datumaro/util/definitions.py | 4 ++-- src/datumaro/util/image.py | 31 ++++++++++++++++------------ src/datumaro/util/mask_tools.py | 21 ++++--------------- tests/unit/test_masks.py | 4 ++-- 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/src/datumaro/util/annotation_util.py b/src/datumaro/util/annotation_util.py index f9bbb2b71a..d89d2542a1 100644 --- a/src/datumaro/util/annotation_util.py +++ b/src/datumaro/util/annotation_util.py @@ -17,11 +17,9 @@ RleMask, Shape, ) +from datumaro.util.definitions import BboxIntCoords from datumaro.util.mask_tools import mask_to_rle -BboxCoords = Tuple[float, float, float, float] -"A tuple of bounding box coordinates, (x, y, w, h)" - SpatialAnnotation = Union[Shape, Mask] @@ -41,7 +39,7 @@ def find_group_leader(group: Sequence[SpatialAnnotation]) -> SpatialAnnotation: return max(group, key=lambda x: x.get_area()) -def get_bbox(ann: Union[Sequence, BboxCoords, SpatialAnnotation]) -> BboxCoords: +def get_bbox(ann: Union[Sequence, BboxIntCoords, SpatialAnnotation]) -> BboxIntCoords: "An utility function to get a bbox of the bbox-like annotation" if hasattr(ann, "get_bbox"): @@ -49,12 +47,12 @@ def get_bbox(ann: Union[Sequence, BboxCoords, SpatialAnnotation]) -> BboxCoords: elif hasattr(ann, "__len__") and len(ann) == 4: return ann elif hasattr(ann, "__len__") and len(ann) == 0: - return [0, 0, 0, 0] + return BboxIntCoords(0, 0, 0, 0) else: raise ValueError("The value of type '%s' can't be treated as a bounding box" % type(ann)) -def max_bbox(annotations: Iterable[Union[BboxCoords, SpatialAnnotation]]) -> BboxCoords: +def max_bbox(annotations: Iterable[Union[BboxIntCoords, SpatialAnnotation]]) -> BboxIntCoords: """ Computes the maximum bbox for the set of spatial annotations and boxes. @@ -67,10 +65,10 @@ def max_bbox(annotations: Iterable[Union[BboxCoords, SpatialAnnotation]]) -> Bbo y0 = min((b[1] for b in boxes), default=0) x1 = max((b[0] + b[2] for b in boxes), default=0) y1 = max((b[1] + b[3] for b in boxes), default=0) - return [x0, y0, x1 - x0, y1 - y0] + return BboxIntCoords(x0, y0, x1 - x0, y1 - y0) -def mean_bbox(annotations: Iterable[Union[BboxCoords, SpatialAnnotation]]) -> BboxCoords: +def mean_bbox(annotations: Iterable[Union[BboxIntCoords, SpatialAnnotation]]) -> BboxIntCoords: """ Computes the mean bbox for the set of spatial annotations and boxes. @@ -84,7 +82,7 @@ def mean_bbox(annotations: Iterable[Union[BboxCoords, SpatialAnnotation]]) -> Bb mtb = sum(b[1] for b in boxes) / le mrb = sum(b[0] + b[2] for b in boxes) / le mbb = sum(b[1] + b[3] for b in boxes) / le - return [mlb, mtb, mrb - mlb, mbb - mtb] + return BboxIntCoords(mlb, mtb, mrb - mlb, mbb - mtb) def softmax(x): @@ -114,8 +112,8 @@ def nms(segments, iou_thresh=0.5): def bbox_iou( - a: Union[SpatialAnnotation, BboxCoords], - b: Union[SpatialAnnotation, BboxCoords], + a: Union[SpatialAnnotation, BboxIntCoords], + b: Union[SpatialAnnotation, BboxIntCoords], ) -> Union[Literal[-1], float]: """ IoU computations for simple cases with bounding boxes diff --git a/src/datumaro/util/definitions.py b/src/datumaro/util/definitions.py index 9882ead8f5..41ffdd6faa 100644 --- a/src/datumaro/util/definitions.py +++ b/src/datumaro/util/definitions.py @@ -5,10 +5,10 @@ import logging as log import os import os.path as osp -from typing import Tuple +from typing import NamedTuple DEFAULT_SUBSET_NAME = "default" -BboxIntCoords = Tuple[int, int, int, int] # (x, y, w, h) +BboxIntCoords = NamedTuple("BboxIntCoords", [("x", int), ("y", int), ("w", int), ("h", int)]) SUBSET_NAME_BLACKLIST = {"labels", "images", "annotations", "instances"} SUBSET_NAME_WHITELIST = {"train", "test", "val"} diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index ced4a77619..da253cd5d5 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -8,7 +8,7 @@ import os.path as osp import shlex import weakref -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from contextvars import ContextVar from enum import Enum, auto from functools import partial @@ -52,7 +52,7 @@ class ImageBackend(Enum): class ImageColorChannel(Enum): """Image color channel - - UNCHANGED: Use the original image's channel (default) + - UNCHANGED: Use the original image's channel (default). - COLOR_BGR: Use BGR 3 channels (it can ignore the alpha channel or convert the gray scale image) - COLOR_RGB: Use RGB 3 channels @@ -129,15 +129,19 @@ def decode_image_context(image_backend: ImageBackend, image_color_channel: Image IMAGE_BACKEND.set(image_backend) IMAGE_COLOR_CHANNEL.set(image_color_channel) - yield - - IMAGE_BACKEND.set(curr_ctx[0]) - IMAGE_COLOR_CHANNEL.set(curr_ctx[1]) + try: + yield + finally: + IMAGE_BACKEND.set(curr_ctx[0]) + IMAGE_COLOR_CHANNEL.set(curr_ctx[1]) def load_image(path: str, dtype: DTypeLike = np.uint8, crypter: Crypter = NULL_CRYPTER): """ Reads an image in the HWC Grayscale/BGR(A) [0; 255] format (default dtype is uint8). + + A context manager decode_image_context can be used + to specify the color scheme and the image backend with which to read the image. """ if IMAGE_BACKEND.get() == ImageBackend.cv2: @@ -166,13 +170,11 @@ def copyto_image( @contextmanager def _open(fp, mode): - was_file = False - if not isinstance(fp, IOBase): - was_file = True - fp = open(fp, mode) - yield fp - if was_file: - fp.close() + with ExitStack() as es: + if not isinstance(fp, IOBase): + fp = es.enter_context(open(fp, mode)) + + yield fp with _open(src, "rb") as src_fp: _bytes = src_crypter.decrypt(src_fp.read()) @@ -291,6 +293,9 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa def decode_image(image_bytes: bytes, dtype: np.dtype = np.uint8) -> np.ndarray: + """ + Reads an image from bytes in the HWC Grayscale/BGR(A) [0; 255] format (default dtype is uint8). + """ ctx_color_scale = IMAGE_COLOR_CHANNEL.get() if np.issubdtype(dtype, np.floating): diff --git a/src/datumaro/util/mask_tools.py b/src/datumaro/util/mask_tools.py index e3ff381866..f0f3264e0e 100644 --- a/src/datumaro/util/mask_tools.py +++ b/src/datumaro/util/mask_tools.py @@ -4,18 +4,7 @@ from functools import partial from itertools import chain, repeat -from typing import ( - TYPE_CHECKING, - Dict, - List, - NamedTuple, - NewType, - Optional, - Sequence, - Tuple, - TypedDict, - Union, -) +from typing import TYPE_CHECKING, Dict, List, NewType, Optional, Sequence, Tuple, TypedDict, Union import numpy as np @@ -49,8 +38,6 @@ class CompressedRle(TypedDict): PolygonGroup = List[Polygon] "A group of polygons, describing a single object" -BboxCoords = NamedTuple("BboxCoords", [("x", int), ("y", int), ("w", int), ("h", int)]) - Segment = Union[PolygonGroup, Rle] BinaryMask = NewType("BinaryMask", np.ndarray) @@ -527,16 +514,16 @@ def rles_to_mask(rles: Sequence[Union[CompressedRle, Polygon]], width, height) - return mask -def find_mask_bbox(mask: BinaryMask) -> BboxCoords: +def find_mask_bbox(mask: BinaryMask) -> BboxIntCoords: cols = np.any(mask, axis=0) rows = np.any(mask, axis=1) has_pixels = np.any(cols) if not has_pixels: - return BboxCoords(0, 0, 0, 0) + return BboxIntCoords(0, 0, 0, 0) x0, x1 = np.where(cols)[0][[0, -1]] y0, y1 = np.where(rows)[0][[0, -1]] - return BboxCoords(x0, y0, x1 - x0 + 1, y1 - y0 + 1) + return BboxIntCoords(x0, y0, x1 - x0 + 1, y1 - y0 + 1) def merge_masks( diff --git a/tests/unit/test_masks.py b/tests/unit/test_masks.py index ee47edeb84..c7d1c3b46a 100644 --- a/tests/unit/test_masks.py +++ b/tests/unit/test_masks.py @@ -5,7 +5,7 @@ import datumaro.util.mask_tools as mask_tools from datumaro.components.annotation import CompiledMask -from datumaro.util.annotation_util import BboxCoords +from datumaro.util.definitions import BboxIntCoords from tests.requirements import Requirements, mark_requirement @@ -499,5 +499,5 @@ class MaskTest: ], ) @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_find_mask_bbox(self, mask: mask_tools.BinaryMask, expected_bbox: BboxCoords): + def test_find_mask_bbox(self, mask: mask_tools.BinaryMask, expected_bbox: BboxIntCoords): assert tuple(expected_bbox) == mask_tools.find_mask_bbox(mask) From 6db94c90954ff8ea8dc12c0070188394d4e3f1ea Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 15:19:35 +0400 Subject: [PATCH 41/49] tests in test_masks.py from upstream --- tests/unit/test_masks.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_masks.py b/tests/unit/test_masks.py index c7d1c3b46a..09741561e6 100644 --- a/tests/unit/test_masks.py +++ b/tests/unit/test_masks.py @@ -490,7 +490,45 @@ def test_can_decode_compiled_mask(self): self.assertEqual({instance_idx: class_idx}, labels) -class MaskTest: +class MaskToolsTest: + """New test implementation based on PyTest framework. + + The other tests in this file should be also migrated into this test class. + """ + + def test_make_index_mask(self): + binary_mask = np.eye(2, dtype=np.bool_) + + def _test(expected, actual): + assert np.allclose(expected, actual) and actual.dtype == expected.dtype + + _test( + np.array([[10, 0], [0, 10]], dtype=np.uint8), + mask_tools.make_index_mask(binary_mask=binary_mask, index=10, ignore_index=0), + ) + + _test( + np.array([[10, 255], [255, 10]], dtype=np.uint8), + mask_tools.make_index_mask(binary_mask=binary_mask, index=10, ignore_index=255), + ) + + _test( + np.array([[10, 100], [100, 10]], dtype=np.uint8), + mask_tools.make_index_mask(binary_mask=binary_mask, index=10, ignore_index=100), + ) + + _test( + np.array([[200, 100], [100, 200]], dtype=np.uint8), + mask_tools.make_index_mask(binary_mask=binary_mask, index=200, ignore_index=100), + ) + + _test( + np.array([[10, 65535], [65535, 10]], dtype=np.uint16), + mask_tools.make_index_mask( + binary_mask=binary_mask, index=10, ignore_index=65535, dtype=np.uint16 + ), + ) + @pytest.mark.parametrize( "mask, expected_bbox", [ From fcb8021b075249b38be5e7aa0073776173fbc3d2 Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 15:26:05 +0400 Subject: [PATCH 42/49] a bit of info on ImageColorChannel.UNCHANGED --- src/datumaro/util/image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index da253cd5d5..00ac73889b 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -53,6 +53,8 @@ class ImageColorChannel(Enum): """Image color channel - UNCHANGED: Use the original image's channel (default). + be aware that if image is not 1-channel (grayscale) + OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). - COLOR_BGR: Use BGR 3 channels (it can ignore the alpha channel or convert the gray scale image) - COLOR_RGB: Use RGB 3 channels From 1d06dbb2c9607f75fa88e9cbb217b1d3eb227e26 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 15:38:20 +0300 Subject: [PATCH 43/49] Apply suggestions from code review --- tests/unit/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 4a616a1e38..c5518558f5 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -142,7 +142,7 @@ def test_decode_image_context(self, image_backend: image_module.ImageBackend, ch assert img_decoded.shape[-1] == 3 assert np.allclose(expected_bgr_image[:, :, ::-1], img_decoded) - # 4 channels from ImageColorScale.UNCHANGED + # 1 (without an extra dim), 3 or 4 channels from ImageColorScale.UNCHANGED with image_module.decode_image_context( image_backend, image_module.ImageColorChannel.UNCHANGED ): @@ -152,7 +152,7 @@ def test_decode_image_context(self, image_backend: image_module.ImageBackend, ch if image_backend == image_module.ImageBackend.cv2 or channels == 1: assert np.allclose(original_image, img_decoded) else: - # PIL will return RGBA, thus we need to correct the fixture + # PIL will return RGB(A), thus we need to correct the fixture to_rgb = original_image[:, :, :3][:, :, ::-1] original_image[:, :, :3] = to_rgb From caddce7183eb2f7fe2a1e4635acb852ace1bb5e0 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 15:36:21 +0200 Subject: [PATCH 44/49] Extend old image saving and loading tests --- tests/unit/test_image.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index c5518558f5..2bc365b554 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -32,26 +32,27 @@ def tearDown(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_save_and_load_backends(self): backends = image_module.ImageBackend - for save_backend, load_backend, c in product(backends, backends, [1, 3]): + for save_backend, load_backend, c in product(backends, backends, [1, 3, 4]): with TestDir() as test_dir: src_image = generate_test_img(c) path = osp.join(test_dir, "img.png") # lossless image_module.IMAGE_BACKEND.set(save_backend) - image_module.save_image(path, src_image, jpeg_quality=100) + image_module.save_image(path, src_image) image_module.IMAGE_BACKEND.set(load_backend) dst_image = image_module.load_image(path) # If image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED # OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). - if ( - c == 3 - and load_backend == image_module.ImageBackend.PIL + if c in [3, 4] and ( + load_backend == image_module.ImageBackend.PIL and image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED + or image_module.IMAGE_COLOR_CHANNEL.get() + == image_module.ImageColorChannel.COLOR_RGB ): - dst_image = np.flip(dst_image, -1) + dst_image[..., :3] = dst_image[..., 2::-1] # to bgr self.assertTrue( np.array_equal(src_image, dst_image), @@ -61,24 +62,25 @@ def test_save_and_load_backends(self): @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_encode_and_decode_backends(self): backends = image_module.ImageBackend - for save_backend, load_backend, c in product(backends, backends, [1, 3]): + for save_backend, load_backend, c in product(backends, backends, [1, 3, 4]): src_image = generate_test_img(c) image_module.IMAGE_BACKEND.set(save_backend) - buffer = image_module.encode_image(src_image, ".png", jpeg_quality=100) # lossless + buffer = image_module.encode_image(src_image, ".png") # lossless image_module.IMAGE_BACKEND.set(load_backend) dst_image = image_module.decode_image(buffer) # If image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED # OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). - if ( - c == 3 - and load_backend == image_module.ImageBackend.PIL + if c in [3, 4] and ( + load_backend == image_module.ImageBackend.PIL and image_module.IMAGE_COLOR_CHANNEL.get() == image_module.ImageColorChannel.UNCHANGED + or image_module.IMAGE_COLOR_CHANNEL.get() + == image_module.ImageColorChannel.COLOR_RGB ): - dst_image = np.flip(dst_image, -1) + dst_image[..., :3] = dst_image[..., 2::-1] # to bgr self.assertTrue( np.array_equal(src_image, dst_image), From 1936697a6b5e242227668f80201d28c730ec48e1 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 15:45:31 +0200 Subject: [PATCH 45/49] Refactor code --- tests/unit/test_image.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 2bc365b554..69d0c04f32 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -151,11 +151,8 @@ def test_decode_image_context(self, image_backend: image_module.ImageBackend, ch img_decoded = image_module.decode_image(img_bytes) assert img_decoded.shape == original_image.shape - if image_backend == image_module.ImageBackend.cv2 or channels == 1: - assert np.allclose(original_image, img_decoded) - else: - # PIL will return RGB(A), thus we need to correct the fixture - to_rgb = original_image[:, :, :3][:, :, ::-1] - original_image[:, :, :3] = to_rgb - - assert np.allclose(original_image, img_decoded) + if image_backend == image_module.ImageBackend.PIL and channels != 1: + # PIL returns RGB(A) + img_decoded[:, :, :3] = img_decoded[:, :, 2::-1] # to bgr + + assert np.allclose(original_image, img_decoded) From 944878eaa1640d2d362b3c2738778e6108d4a9e5 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 15:47:42 +0200 Subject: [PATCH 46/49] Fix formatting --- tests/unit/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 69d0c04f32..841a53c92e 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -153,6 +153,6 @@ def test_decode_image_context(self, image_backend: image_module.ImageBackend, ch if image_backend == image_module.ImageBackend.PIL and channels != 1: # PIL returns RGB(A) - img_decoded[:, :, :3] = img_decoded[:, :, 2::-1] # to bgr + img_decoded[:, :, :3] = img_decoded[:, :, 2::-1] # to bgr assert np.allclose(original_image, img_decoded) From 672ff019a8ec5f6388e0084aa1491f61daf176ca Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 17:25:28 +0300 Subject: [PATCH 47/49] Update src/datumaro/util/image.py --- src/datumaro/util/image.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index cec70626c3..3611e90dfd 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -53,8 +53,9 @@ class ImageColorChannel(Enum): """Image color channel - UNCHANGED: Use the original image's channel (default). - be aware that if image is not 1-channel (grayscale) - OpenCV will read an image as BGR(A), but PIL will read an image as RGB(A). + Note that image reading by different backends can result in different results. + For instance, if an image has more than 1-channel (grayscale), + OpenCV will read it as BGR(A), while PIL will read it as RGB(A). - COLOR_BGR: Use BGR 3 channels (it can ignore the alpha channel or convert the gray scale image) - COLOR_RGB: Use RGB 3 channels From 03cd9a5a2ee35ffda6e17700b0fa3ce1fbd7a465 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 13 Feb 2025 17:18:29 +0200 Subject: [PATCH 48/49] Fix formatting --- src/datumaro/util/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datumaro/util/image.py b/src/datumaro/util/image.py index 3611e90dfd..c0fdb0d679 100644 --- a/src/datumaro/util/image.py +++ b/src/datumaro/util/image.py @@ -53,7 +53,7 @@ class ImageColorChannel(Enum): """Image color channel - UNCHANGED: Use the original image's channel (default). - Note that image reading by different backends can result in different results. + Note that image reading by different backends can result in different results. For instance, if an image has more than 1-channel (grayscale), OpenCV will read it as BGR(A), while PIL will read it as RGB(A). - COLOR_BGR: Use BGR 3 channels From 9cb2b565a92f139f178e29ee14bfad6e29a1208d Mon Sep 17 00:00:00 2001 From: Dmitrii Lavrukhin Date: Thu, 13 Feb 2025 19:45:03 +0400 Subject: [PATCH 49/49] tests for crypter --- tests/unit/test_crypter.py | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/unit/test_crypter.py diff --git a/tests/unit/test_crypter.py b/tests/unit/test_crypter.py new file mode 100644 index 0000000000..adbc78f91d --- /dev/null +++ b/tests/unit/test_crypter.py @@ -0,0 +1,89 @@ +# Copyright (C) 2023 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os.path as osp + +import numpy as np +import pytest + +from datumaro.components.crypter import NULL_CRYPTER, Crypter +from datumaro.components.media import Image +from datumaro.util.image import IMAGE_BACKEND, ImageBackend + + +@pytest.fixture(scope="class") +def fxt_crypter(): + return Crypter(Crypter.gen_key()) + + +@pytest.fixture() +def fxt_image_file(test_dir): + img = Image.from_numpy(data=np.random.randint(0, 256, size=(10, 10, 3), dtype=np.uint8)) + path = osp.join(test_dir, "test_crypter", "test.png") + img.save(path) + + return path + + +@pytest.fixture() +def fxt_encrypted_image_file(test_dir, fxt_image_file, fxt_crypter): + img = Image.from_file(path=fxt_image_file) + path = osp.join(test_dir, "test_crypter", "test_encrypted.png") + img.save(path, crypter=fxt_crypter) + + return path + + +class CrypterTest: + @pytest.fixture(scope="class", params=[ImageBackend.cv2, ImageBackend.PIL], autouse=True) + def fxt_image_backend(self, request): + curr_backend = IMAGE_BACKEND.get() + IMAGE_BACKEND.set(request.param) + yield + IMAGE_BACKEND.set(curr_backend) + + def test_load_encrypted_image(self, fxt_image_file, fxt_encrypted_image_file, fxt_crypter): + img = Image.from_file(path=fxt_image_file) + encrypted_img = Image.from_file(path=fxt_encrypted_image_file, crypter=fxt_crypter) + + assert img == encrypted_img + + def _test_save_and_load( + self, fxt_encrypted_image_file, fxt_crypter, test_dir, fname, new_crypter + ): + src_img = Image.from_file(path=fxt_encrypted_image_file, crypter=fxt_crypter) + src_img_data = src_img.data # Get data first until it is changed + + new_path = osp.join(test_dir, "test_crypter", fname) + + src_img.save(new_path, crypter=new_crypter) + dst_img = Image.from_file(path=new_path, crypter=new_crypter) + + assert np.array_equal(src_img_data, dst_img.data) + + @pytest.mark.parametrize( + "fname", ["new_encrypted.png", "test_encrypted.png"], ids=["new-path", "overwrite"] + ) + def test_save_and_load_image_with_new_crypter( + self, fxt_encrypted_image_file, fxt_crypter, test_dir, fname + ): + new_crypter = fxt_crypter + while new_crypter == fxt_crypter: + new_crypter = Crypter(Crypter.gen_key()) + + self._test_save_and_load( + fxt_encrypted_image_file, fxt_crypter, test_dir, fname, new_crypter + ) + + @pytest.mark.parametrize( + "fname", ["new_encrypted.png", "test_encrypted.png"], ids=["new-path", "overwrite"] + ) + def test_save_and_load_image_with_null_crypter( + self, fxt_encrypted_image_file, fxt_crypter, test_dir, fname + ): + new_crypter = NULL_CRYPTER + + self._test_save_and_load( + fxt_encrypted_image_file, fxt_crypter, test_dir, fname, new_crypter + )