From 4394c6a5448458e6178aa90d7ea53fc703f9a559 Mon Sep 17 00:00:00 2001 From: Jihyeon Yi Date: Fri, 10 May 2024 09:20:30 +0900 Subject: [PATCH] Enabled support for 'Video' media type in the datumaro format (#1491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Enabled support for 'Video' media type in the datumaro format to support annotations by video or video range. Note that the video has a closed interval of [start_frame, end_frame]. ### How to test ### Checklist - [x] I have added unit tests to cover my changes.​ - [x] I have added integration tests to cover my changes.​ - [x] I have added the description of my changes into [CHANGELOG](https://github.com/openvinotoolkit/datumaro/blob/develop/CHANGELOG.md).​ - [x] I have updated the [documentation](https://github.com/openvinotoolkit/datumaro/tree/develop/docs) accordingly ### License - [x] I submit _my code changes_ under the same [MIT License](https://github.com/openvinotoolkit/datumaro/blob/develop/LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. - [x] I have updated the license header for each file (see an example below). ```python # Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT ``` --- CHANGELOG.md | 2 + .../docs/command-reference/context/util.md | 1 + .../docs/data-formats/formats/datumaro.md | 1 + .../data-formats/formats/datumaro_binary.md | 1 + .../source/docs/data-formats/formats/video.md | 1 + src/datumaro/components/dataset.py | 7 +- src/datumaro/components/exporter.py | 20 +- src/datumaro/components/media.py | 119 ++++++++++-- .../plugins/data_formats/datumaro/base.py | 41 +++- .../plugins/data_formats/datumaro/exporter.py | 29 ++- .../data_formats/datumaro_binary/base.py | 9 +- .../datumaro_binary/mapper/media.py | 39 +++- tests/integration/cli/test_utils.py | 6 +- tests/integration/cli/test_video.py | 79 ++++++-- tests/unit/components/test_exporter.py | 70 +++++++ tests/unit/data_formats/datumaro/conftest.py | 54 +++++- .../datumaro/test_datumaro_binary_format.py | 24 ++- .../datumaro/test_datumaro_format.py | 23 ++- tests/unit/test_kinetics_format.py | 105 ++++++---- tests/unit/test_video.py | 183 ++++++++++++------ tests/utils/test_utils.py | 37 ++-- 21 files changed, 660 insertions(+), 191 deletions(-) create mode 100644 tests/unit/components/test_exporter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f777a92c0..59c4b6eaae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Q2 2024 Release 1.7.0 ### New features +- Support 'Video' media type in datumaro format + () - Add ann_types property for dataset (, ) - Add AnnotationType.rotated_bbox for oriented object detection diff --git a/docs/source/docs/command-reference/context/util.md b/docs/source/docs/command-reference/context/util.md index df65f7c0dc..6e59d80642 100644 --- a/docs/source/docs/command-reference/context/util.md +++ b/docs/source/docs/command-reference/context/util.md @@ -17,6 +17,7 @@ the dataset reproducible and stable. This command provides different options like setting the frame step (the `-s/--step` option), file name pattern (`-n/--name-pattern`), starting (`-b/--start-frame`) and finishing (`-e/--end-frame`) frame etc. +Note that starting and finishing frames denote a closed interval [`start-frame`, `end-frame`]. Note that this command is equivalent to the following commands: ```bash diff --git a/docs/source/docs/data-formats/formats/datumaro.md b/docs/source/docs/data-formats/formats/datumaro.md index 60869e55f2..b12f1af6a1 100644 --- a/docs/source/docs/data-formats/formats/datumaro.md +++ b/docs/source/docs/data-formats/formats/datumaro.md @@ -11,6 +11,7 @@ Supported media types: - `Image` - `PointCloud` +- `Video` - `VideoFrame` Supported annotation types: diff --git a/docs/source/docs/data-formats/formats/datumaro_binary.md b/docs/source/docs/data-formats/formats/datumaro_binary.md index 6454eff146..7b724b3734 100644 --- a/docs/source/docs/data-formats/formats/datumaro_binary.md +++ b/docs/source/docs/data-formats/formats/datumaro_binary.md @@ -59,6 +59,7 @@ Supported media types: - `Image` - `PointCloud` +- `Video` - `VideoFrame` Supported annotation types: diff --git a/docs/source/docs/data-formats/formats/video.md b/docs/source/docs/data-formats/formats/video.md index b7f1e0f770..2ba1f6990c 100644 --- a/docs/source/docs/data-formats/formats/video.md +++ b/docs/source/docs/data-formats/formats/video.md @@ -31,6 +31,7 @@ dataset = dm.Dataset.import_from('', format='video_frames') Datumaro has few import options for `video_frames` format, to apply them use the `--` after the main command argument. +Note that a video has a closed interval of [`start-frame`, `end-frame`]. `video_frames` import options: - `--subset` (string) - The name of the subset for the produced diff --git a/src/datumaro/components/dataset.py b/src/datumaro/components/dataset.py index 4023782228..8364b1051e 100644 --- a/src/datumaro/components/dataset.py +++ b/src/datumaro/components/dataset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2023 Intel Corporation +# Copyright (C) 2020-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -41,6 +41,7 @@ from datumaro.components.environment import DEFAULT_ENVIRONMENT, Environment from datumaro.components.errors import ( DatasetImportError, + DatumaroError, MultipleFormatsMatchError, NoMatchingFormatsError, StreamedItemError, @@ -888,6 +889,10 @@ def import_from( 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 diff --git a/src/datumaro/components/exporter.py b/src/datumaro/components/exporter.py index be7492336e..7639947425 100644 --- a/src/datumaro/components/exporter.py +++ b/src/datumaro/components/exporter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2023 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -22,7 +22,7 @@ DatumaroError, ItemExportError, ) -from datumaro.components.media import Image, PointCloud, VideoFrame +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_hashkey_file, save_meta_file from datumaro.util.os_util import rmtree @@ -339,10 +339,15 @@ def make_pcd_extra_image_filename(self, item, idx, image, *, name=None, subdir=N ) + self.find_image_ext(image) def make_video_filename(self, item, *, name=None): - if isinstance(item, DatasetItem) and isinstance(item.media, VideoFrame): + 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 "Video item type should be VideoFrame" + assert False, STR_WRONG_MEDIA_TYPE return video_file_name @@ -403,7 +408,7 @@ def save_video( subdir: Optional[str] = None, fname: Optional[str] = None, ): - if not item.media or not isinstance(item.media, VideoFrame): + 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 @@ -415,7 +420,10 @@ def save_video( os.makedirs(osp.dirname(path), exist_ok=True) - item.media.video.save(path, crypter=NULL_CRYPTER) + 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: diff --git a/src/datumaro/components/media.py b/src/datumaro/components/media.py index 427cbd7766..013fb812be 100644 --- a/src/datumaro/components/media.py +++ b/src/datumaro/components/media.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021-2022 Intel Corporation +# Copyright (C) 2021-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -94,7 +94,7 @@ def media(self) -> Optional[Type[MediaElement]]: class MediaElement(Generic[AnyData]): _type = MediaType.MEDIA_ELEMENT - def __init__(self, crypter: Crypter = NULL_CRYPTER) -> None: + def __init__(self, crypter: Crypter = NULL_CRYPTER, *args, **kwargs) -> None: self._crypter = crypter def as_dict(self) -> Dict[str, Any]: @@ -488,6 +488,26 @@ def video(self) -> Video: 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]): """ @@ -527,6 +547,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) @@ -575,13 +600,22 @@ class Video(MediaElement, Iterable[VideoFrame]): """ 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__() + super().__init__(*args, **kwargs) self._path = path + assert 0 <= start_frame if end_frame: - assert start_frame < end_frame + 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 @@ -630,7 +664,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 @@ -639,7 +673,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. @@ -655,12 +690,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 @@ -686,18 +724,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 @@ -719,15 +762,49 @@ 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 diff --git a/src/datumaro/plugins/data_formats/datumaro/base.py b/src/datumaro/plugins/data_formats/datumaro/base.py index 5095582e9e..ee7a8cdc21 100644 --- a/src/datumaro/plugins/data_formats/datumaro/base.py +++ b/src/datumaro/plugins/data_formats/datumaro/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -132,6 +132,7 @@ def _gen(): items = [] ann_types = set() + actual_media_types = set() for item_desc in pbar.iter( _gen(), desc=f"Importing '{self._subset}'", total=len(item_descs) ): @@ -140,12 +141,28 @@ def _gen(): items.append(item) for ann in item.annotations: ann_types.add(ann.type) + if item.media: + actual_media_types.add(item.media.type) self.ann_types = ann_types + if len(actual_media_types) == 1: + actual_media_type = actual_media_types.pop() + elif len(actual_media_types) > 1: + actual_media_type = MediaType.MEDIA_ELEMENT + else: + actual_media_type = None + + if actual_media_type and not issubclass(actual_media_type.media, self.media_type): + raise MediaTypeError( + f"Unexpected media type of a dataset '{self.media_type}'. " + f"Expected media type is '{actual_media_type.media}." + ) + return items def _parse_item(self, item_desc: Dict) -> Optional[DatasetItem]: + STR_MULTIPLE_MEDIA = "DatasetItem cannot contain multiple media types" try: item_id = item_desc["id"] @@ -161,12 +178,10 @@ def _parse_item(self, item_desc: Dict) -> Optional[DatasetItem]: image_path = old_image_path media = Image.from_file(path=image_path, size=image_info.get("size")) - if self.media_type == MediaElement: - self.media_type = Image pcd_info = item_desc.get("point_cloud") if media and pcd_info: - raise MediaTypeError("Dataset cannot contain multiple media types") + raise MediaTypeError(STR_MULTIPLE_MEDIA) if pcd_info: pcd_path = pcd_info.get("path") point_cloud = osp.join(self._pcd_dir, self._subset, pcd_path) @@ -183,12 +198,10 @@ def _parse_item(self, item_desc: Dict) -> Optional[DatasetItem]: ] media = PointCloud.from_file(path=point_cloud, extra_images=related_images) - if self.media_type == MediaElement: - self.media_type = PointCloud video_frame_info = item_desc.get("video_frame") if media and video_frame_info: - raise MediaTypeError("Dataset cannot contain multiple media types") + raise MediaTypeError(STR_MULTIPLE_MEDIA) if video_frame_info: video_path = osp.join( self._video_dir, self._subset, video_frame_info.get("video_path") @@ -201,6 +214,20 @@ def _parse_item(self, item_desc: Dict) -> Optional[DatasetItem]: media = VideoFrame(video, frame_index) + video_info = item_desc.get("video") + if media and video_info: + raise MediaTypeError(STR_MULTIPLE_MEDIA) + if video_info: + video_path = osp.join(self._video_dir, self._subset, video_info.get("path")) + if video_path not in self._videos: + self._videos[video_path] = Video(video_path) + step = video_info.get("step", 1) + start_frame = video_info.get("start_frame", 0) + end_frame = video_info.get("end_frame", None) + media = Video( + path=video_path, step=step, start_frame=start_frame, end_frame=end_frame + ) + media_desc = item_desc.get("media") if not media and media_desc and media_desc.get("path"): media = MediaElement(path=media_desc.get("path")) diff --git a/src/datumaro/plugins/data_formats/datumaro/exporter.py b/src/datumaro/plugins/data_formats/datumaro/exporter.py index a6e5be71f2..582a42dc46 100644 --- a/src/datumaro/plugins/data_formats/datumaro/exporter.py +++ b/src/datumaro/plugins/data_formats/datumaro/exporter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -179,6 +179,24 @@ def context_save_media( """ if item.media is None: yield + elif isinstance(item.media, Video): + video = item.media_as(Video) + + if context.save_media: + fname = context.make_video_filename(item) + # To prevent the video from being overwritten + # (A video can have same path but different start/end frames) + if not osp.exists(fname): + context.save_video(item, fname=fname, subdir=item.subset) + item.media = Video( + path=video.path, + step=video._step, + start_frame=video._start_frame, + end_frame=video._end_frame, + ) + + yield + item.media = video elif isinstance(item.media, VideoFrame): video_frame = item.media_as(VideoFrame) @@ -245,6 +263,15 @@ def _gen_item_desc(self, item: DatasetItem, *args, **kwargs) -> Dict: "video_path": getattr(video_frame.video, "path", None), "frame_index": getattr(video_frame, "index", -1), } + elif isinstance(item.media, Video): + video = item.media_as(Video) + item_desc["video"] = { + "path": getattr(video, "path", None), + "step": video._step, + "start_frame": video._start_frame, + } + if video._end_frame is not None: + item_desc["video"]["end_frame"] = video._end_frame elif isinstance(item.media, Image): image = item.media_as(Image) item_desc["image"] = {"path": getattr(image, "path", None)} diff --git a/src/datumaro/plugins/data_formats/datumaro_binary/base.py b/src/datumaro/plugins/data_formats/datumaro_binary/base.py index b054a1fe91..a1ef0d74bd 100644 --- a/src/datumaro/plugins/data_formats/datumaro_binary/base.py +++ b/src/datumaro/plugins/data_formats/datumaro_binary/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -12,7 +12,7 @@ from datumaro.components.dataset_base import DatasetItem from datumaro.components.errors import DatasetImportError from datumaro.components.importer import ImportContext -from datumaro.components.media import Image, MediaElement, MediaType, PointCloud, VideoFrame +from datumaro.components.media import Image, MediaElement, MediaType, PointCloud, Video, VideoFrame from datumaro.plugins.data_formats.datumaro_binary.format import DatumaroBinaryPath from datumaro.plugins.data_formats.datumaro_binary.mapper import DictMapper from datumaro.plugins.data_formats.datumaro_binary.mapper.common import IntListMapper @@ -108,6 +108,8 @@ def _read_media_type(self): self._media_type = Image elif media_type == MediaType.POINT_CLOUD: self._media_type = PointCloud + elif media_type == MediaType.VIDEO: + self._media_type = Video elif media_type == MediaType.VIDEO_FRAME: self._media_type = VideoFrame elif media_type == MediaType.MEDIA_ELEMENT: @@ -123,7 +125,8 @@ def _read_items(self) -> None: media_path_prefix = { MediaType.IMAGE: osp.join(self._images_dir, self._subset), MediaType.POINT_CLOUD: osp.join(self._pcd_dir, self._subset), - MediaType.VIDEO_FRAME: self._video_dir, + MediaType.VIDEO: osp.join(self._video_dir, self._subset), + MediaType.VIDEO_FRAME: osp.join(self._video_dir, self._subset), } if self._num_workers > 0: diff --git a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/media.py b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/media.py index 3570cc883c..b832525e9c 100644 --- a/src/datumaro/plugins/data_formats/datumaro_binary/mapper/media.py +++ b/src/datumaro/plugins/data_formats/datumaro_binary/mapper/media.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -21,6 +21,8 @@ def forward(cls, obj: Optional[MediaElement]) -> bytes: return ImageMapper.forward(obj) elif obj._type == MediaType.POINT_CLOUD: return PointCloudMapper.forward(obj) + elif obj._type == MediaType.VIDEO: + return VideoMapper.forward(obj) elif obj._type == MediaType.VIDEO_FRAME: return VideoFrameMapper.forward(obj) elif obj._type == MediaType.MEDIA_ELEMENT: @@ -43,6 +45,8 @@ def backward( return ImageMapper.backward(_bytes, offset, media_path_prefix) elif media_type == MediaType.POINT_CLOUD: return PointCloudMapper.backward(_bytes, offset, media_path_prefix) + elif media_type == MediaType.VIDEO: + return VideoMapper.backward(_bytes, offset, media_path_prefix) elif media_type == MediaType.VIDEO_FRAME: return VideoFrameMapper.backward(_bytes, offset, media_path_prefix) elif media_type == MediaType.MEDIA_ELEMENT: @@ -128,6 +132,39 @@ def backward( ) +class VideoMapper(MediaElementMapper): + MAGIC_END_FRAME_FOR_NONE = 4294967295 # max value of unsigned int32 + MEDIA_TYPE = MediaType.VIDEO + + @classmethod + def forward(cls, obj: Video) -> bytes: + end_frame = obj._end_frame if obj._end_frame else cls.MAGIC_END_FRAME_FOR_NONE + + bytes_arr = bytearray() + bytes_arr.extend(super().forward(obj)) + bytes_arr.extend(struct.pack(" Tuple[Video, int]: + media_dict, offset = cls.backward_dict(_bytes, offset, media_path_prefix) + step, start_frame, end_frame = struct.unpack_from(" Dataset: + video_path = osp.join(test_dir, "video.avi") + make_sample_video(video_path, frame_size=(4, 6), frames=4) + video = Video(video_path) + + kwargs = { + "iterable": [ + DatasetItem( + "0", + media=VideoFrame(video, 0), + annotations=[ + Bbox(1, 1, 1, 1, label=0, object_id=0), + Bbox(2, 2, 2, 2, label=1, object_id=1), + ], + ), + DatasetItem( + "1", + media=Video(video_path, step=1, start_frame=0, end_frame=1), + annotations=[ + Label(0), + ], + ), + DatasetItem( + "2", + media=Video(video_path, step=1, start_frame=2, end_frame=2), + annotations=[ + Label(1), + ], + ), + ], + "categories": ["a", "b"], + } + if media_type: + kwargs["media_type"] = media_type + dataset = Dataset.from_iterable(**kwargs) + + return dataset + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_cannot_create_video_and_videoframe_dataset_with_wrong_media_type(self): + with TestDir() as test_dir: + dataset = self._make_sample_video_dataset(test_dir) + project_dir = osp.join(test_dir, "proj") + run(self, "project", "create", "-o", project_dir) + with self.assertRaises(DatasetImportError): + dataset_dir = osp.join(test_dir, "test_video") + dataset.save(dataset_dir, save_media=True) + run(self, "project", "import", "-p", project_dir, "-f", "datumaro", dataset_dir) + @pytest.mark.new @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_export_video_dataset(self): with TestDir() as test_dir: - video_path = osp.join(test_dir, "video.avi") - make_sample_video(video_path, frame_size=(4, 6), frames=4) - video = Video(video_path) - - expected = Dataset.from_iterable( - [ - DatasetItem( - 0, - media=VideoFrame(video, 0), - annotations=[ - Bbox(1, 1, 1, 1, label=0, object_id=0), - Bbox(2, 2, 2, 2, label=1, object_id=1), - ], - ) - ], - categories=["a", "b"], - ) + expected = self._make_sample_video_dataset(test_dir, media_type=MediaElement) project_dir = osp.join(test_dir, "proj") run(self, "project", "create", "-o", project_dir) diff --git a/tests/unit/components/test_exporter.py b/tests/unit/components/test_exporter.py new file mode 100644 index 0000000000..4820b32eda --- /dev/null +++ b/tests/unit/components/test_exporter.py @@ -0,0 +1,70 @@ +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +import os.path as osp + +import pytest + +from datumaro.components.dataset_base import DatasetItem +from datumaro.components.exporter import ExportContextComponent +from datumaro.components.media import MediaElement, Video, VideoFrame + +from tests.utils.test_utils import TestDir +from tests.utils.video import make_sample_video + + +@pytest.fixture() +def fxt_sample_video(test_dir): + video_path = osp.join(test_dir, "video.avi") + make_sample_video(video_path, frame_size=(4, 6), frames=4) + yield video_path + + +@pytest.fixture +def fxt_export_context_component(test_dir): + return ExportContextComponent( + save_dir=test_dir, + save_media=True, + images_dir="images", + pcd_dir="point_clouds", + video_dir="videos", + ) + + +class ExportContextComponentTest: + def test_make_video_filename(self, fxt_export_context_component, fxt_sample_video): + video = Video(fxt_sample_video) + frame = VideoFrame(video, index=1) + + ecc: ExportContextComponent = fxt_export_context_component + + for media in [video, frame]: + assert "video.avi" == ecc.make_video_filename(DatasetItem(0, media=media)) + + # error cases + for item in [None, DatasetItem(0, media=MediaElement())]: + with pytest.raises(AssertionError): + ecc.make_video_filename(item) + + def test_save_video(self, fxt_export_context_component, fxt_sample_video): + video = Video(fxt_sample_video) + frame = VideoFrame(video, index=1) + ecc: ExportContextComponent = fxt_export_context_component + + with TestDir() as test_dir: + ecc.save_video(DatasetItem(0, media=video), basedir=test_dir) + expected_path = osp.join(test_dir, "video.avi") + assert osp.exists(expected_path) + + with TestDir() as test_dir: + ecc.save_video(DatasetItem(0, media=frame), basedir=test_dir) + expected_path = osp.join(test_dir, "video.avi") + assert osp.exists(expected_path) + + # cannot save items with no media + with TestDir() as test_dir: + ecc.save_video(DatasetItem(0), basedir=test_dir) + files = os.listdir(test_dir) + assert not files diff --git a/tests/unit/data_formats/datumaro/conftest.py b/tests/unit/data_formats/datumaro/conftest.py index 6707c2c637..e600ae957c 100644 --- a/tests/unit/data_formats/datumaro/conftest.py +++ b/tests/unit/data_formats/datumaro/conftest.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -28,11 +28,13 @@ RleMask, ) from datumaro.components.dataset_base import DatasetItem -from datumaro.components.media import Image, PointCloud +from datumaro.components.media import Image, MediaElement, PointCloud, Video, VideoFrame from datumaro.components.project import Dataset from datumaro.plugins.data_formats.datumaro.format import DatumaroPath from datumaro.util.mask_tools import generate_colormap +from tests.utils.video import make_sample_video + @pytest.fixture def fxt_test_datumaro_format_dataset(): @@ -199,6 +201,54 @@ def fxt_test_datumaro_format_dataset(): ) +@pytest.fixture +def fxt_test_datumaro_format_video_dataset(test_dir) -> Dataset: + video_path = osp.join(test_dir, "video.avi") + make_sample_video(video_path, frame_size=(4, 6), frames=4) + video = Video(video_path) + + return Dataset.from_iterable( + iterable=[ + DatasetItem( + "f0", + subset="train", + media=VideoFrame(video, 0), + annotations=[ + Bbox(1, 1, 1, 1, label=0, object_id=0), + Bbox(2, 2, 2, 2, label=1, object_id=1), + ], + ), + DatasetItem( + "f1", + subset="test", + media=VideoFrame(video, 0), + annotations=[ + Bbox(0, 0, 2, 2, label=1, object_id=1), + Bbox(3, 3, 1, 1, label=0, object_id=0), + ], + ), + DatasetItem( + "v0", + subset="train", + media=Video(video_path, step=1, start_frame=0, end_frame=1), + annotations=[ + Label(0), + ], + ), + DatasetItem( + "v1", + subset="test", + media=Video(video_path, step=1, start_frame=2, end_frame=2), + annotations=[ + Bbox(1, 1, 3, 3, label=1, object_id=1), + ], + ), + ], + media_type=MediaElement, + categories=["a", "b"], + ) + + @pytest.fixture def fxt_wrong_version_dir(fxt_test_datumaro_format_dataset, test_dir): dest_dir = osp.join(test_dir, "wrong_version") diff --git a/tests/unit/data_formats/datumaro/test_datumaro_binary_format.py b/tests/unit/data_formats/datumaro/test_datumaro_binary_format.py index 00700bb1e7..73cacaded0 100644 --- a/tests/unit/data_formats/datumaro/test_datumaro_binary_format.py +++ b/tests/unit/data_formats/datumaro/test_datumaro_binary_format.py @@ -1,5 +1,5 @@ # pylint: disable=arguments-differ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -39,10 +39,13 @@ class DatumaroBinaryFormatTest(TestBase): ann_ext = DatumaroBinaryPath.ANNOTATION_EXT @pytest.mark.parametrize( - ["fxt_dataset", "compare", "require_media", "fxt_import_kwargs", "fxt_export_kwargs"], + "fxt_dataset", + ("fxt_test_datumaro_format_dataset", "fxt_test_datumaro_format_video_dataset"), + ) + @pytest.mark.parametrize( + ["compare", "require_media", "fxt_import_kwargs", "fxt_export_kwargs"], [ pytest.param( - "fxt_test_datumaro_format_dataset", compare_datasets_strict, True, {}, @@ -50,7 +53,6 @@ class DatumaroBinaryFormatTest(TestBase): id="test_no_encryption", ), pytest.param( - "fxt_test_datumaro_format_dataset", compare_datasets_strict, True, {"encryption_key": ENCRYPTION_KEY}, @@ -58,7 +60,6 @@ class DatumaroBinaryFormatTest(TestBase): id="test_with_encryption", ), pytest.param( - "fxt_test_datumaro_format_dataset", compare_datasets_strict, True, {"encryption_key": ENCRYPTION_KEY}, @@ -66,7 +67,6 @@ class DatumaroBinaryFormatTest(TestBase): id="test_no_media_encryption", ), pytest.param( - "fxt_test_datumaro_format_dataset", compare_datasets_strict, True, {"encryption_key": ENCRYPTION_KEY}, @@ -74,7 +74,6 @@ class DatumaroBinaryFormatTest(TestBase): id="test_multi_blobs", ), pytest.param( - "fxt_test_datumaro_format_dataset", compare_datasets_strict, True, {"encryption_key": ENCRYPTION_KEY, "num_workers": 2}, @@ -167,10 +166,15 @@ def _get_ann_mapper(ann: Annotation) -> AnnotationMapper: def test_common_mapper(self, mapper: Mapper, expected: Any): self._test(mapper, expected) - def test_annotations_mapper(self, fxt_test_datumaro_format_dataset): - """Test all annotations in fxt_test_datumaro_format_dataset""" + @pytest.mark.parametrize( + "fxt_dataset", + ("fxt_test_datumaro_format_dataset", "fxt_test_datumaro_format_video_dataset"), + ) + def test_annotations_mapper(self, fxt_dataset, request): + """Test all annotations in fxt_dataset""" mapper = DatasetItemMapper - for item in fxt_test_datumaro_format_dataset: + fxt_dataset = request.getfixturevalue(fxt_dataset) + for item in fxt_dataset: for ann in item.annotations: mapper = self._get_ann_mapper(ann) self._test(mapper, ann) diff --git a/tests/unit/data_formats/datumaro/test_datumaro_format.py b/tests/unit/data_formats/datumaro/test_datumaro_format.py index 9a3168df83..1f9caadfbd 100644 --- a/tests/unit/data_formats/datumaro/test_datumaro_format.py +++ b/tests/unit/data_formats/datumaro/test_datumaro_format.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -76,6 +76,18 @@ def _test_save_and_load( False, id="test_can_save_and_load_with_no_save_media", ), + pytest.param( + "fxt_test_datumaro_format_video_dataset", + compare_datasets, + True, + id="test_can_save_and_load_video_dataset", + ), + pytest.param( + "fxt_test_datumaro_format_video_dataset", + None, + False, + id="test_can_save_and_load_video_dataset_with_no_save_media", + ), pytest.param( "fxt_relative_paths", compare_datasets, @@ -176,8 +188,13 @@ def test_source_target_pair( ) @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_can_detect(self, fxt_test_datumaro_format_dataset, test_dir): - self.exporter.convert(fxt_test_datumaro_format_dataset, save_dir=test_dir) + @pytest.mark.parametrize( + "fxt_dataset", + ("fxt_test_datumaro_format_dataset", "fxt_test_datumaro_format_video_dataset"), + ) + def test_can_detect(self, fxt_dataset, test_dir, request): + fxt_dataset = request.getfixturevalue(fxt_dataset) + self.exporter.convert(fxt_dataset, save_dir=test_dir) detected_formats = Environment().detect_dataset(test_dir) assert [self.importer.NAME] == detected_formats diff --git a/tests/unit/test_kinetics_format.py b/tests/unit/test_kinetics_format.py index 586ddbe900..67567e14c5 100644 --- a/tests/unit/test_kinetics_format.py +++ b/tests/unit/test_kinetics_format.py @@ -1,4 +1,11 @@ -from unittest import TestCase +# Copyright (C) 2024 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +import os.path as osp + +import pytest from datumaro.components.annotation import Label from datumaro.components.dataset import Dataset, DatasetItem @@ -11,46 +18,68 @@ from tests.utils.assets import get_test_asset_path from tests.utils.test_utils import compare_datasets -DUMMY_DATASET_DIR = get_test_asset_path("kinetics_dataset") +KINETICS_DATASET_DIR = get_test_asset_path("kinetics_dataset") + + +@pytest.fixture +def fxt_kinetics_dataset(test_dir): + def make_video(fname, frame_size=(4, 6), frames=4): + src_path = osp.join(KINETICS_DATASET_DIR, fname) + dst_path = osp.join(test_dir, fname) + if not osp.exists(osp.dirname(dst_path)): + os.makedirs(osp.dirname(dst_path)) + os.symlink(src_path, dst_path) + return Video(dst_path) + + return Dataset.from_iterable( + [ + DatasetItem( + id="1", + subset="test", + annotations=[Label(0, attributes={"time_start": 0, "time_end": 2})], + media=make_video("video_1.avi"), + ), + DatasetItem( + id="2", + subset="test", + annotations=[Label(0, attributes={"time_start": 5, "time_end": 7})], + ), + DatasetItem( + id="4", + subset="test", + annotations=[Label(1, attributes={"time_start": 10, "time_end": 15})], + ), + DatasetItem( + id="3", + subset="train", + annotations=[Label(2, attributes={"time_start": 0, "time_end": 2})], + media=make_video("train/3.avi"), + ), + ], + categories=["label_0", "label_1", "label_2"], + media_type=Video, + ) -class KineticsImporterTest(TestCase): +class KineticsImporterTest: @mark_requirement(Requirements.DATUM_GENERAL_REQ) def test_can_detect(self): - detected_formats = Environment().detect_dataset(DUMMY_DATASET_DIR) - self.assertEqual([KineticsImporter.NAME], detected_formats) + detected_formats = Environment().detect_dataset(KINETICS_DATASET_DIR) + assert [KineticsImporter.NAME] == detected_formats @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_can_import_with_video(self): - expected_dataset = Dataset.from_iterable( - [ - DatasetItem( - id="1", - subset="test", - annotations=[Label(0, attributes={"time_start": 0, "time_end": 2})], - media=Video("./video_1.avi"), - ), - DatasetItem( - id="2", - subset="test", - annotations=[Label(0, attributes={"time_start": 5, "time_end": 7})], - ), - DatasetItem( - id="4", - subset="test", - annotations=[Label(1, attributes={"time_start": 10, "time_end": 15})], - ), - DatasetItem( - id="3", - subset="train", - annotations=[Label(2, attributes={"time_start": 0, "time_end": 2})], - media=Video("./train/3.avi"), - ), - ], - categories=["label_0", "label_1", "label_2"], - media_type=Video, - ) - - imported_dataset = Dataset.import_from(DUMMY_DATASET_DIR, "kinetics") - - compare_datasets(self, expected_dataset, imported_dataset, require_media=True) + def test_can_import_with_video(self, helper_tc, fxt_kinetics_dataset): + expected_dataset = fxt_kinetics_dataset + imported_dataset = Dataset.import_from(KINETICS_DATASET_DIR, "kinetics") + + compare_datasets(helper_tc, expected_dataset, imported_dataset, require_media=True) + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + def test_can_convert_to_datumaro_and_export_it(self, helper_tc, test_dir): + imported_dataset = Dataset.import_from(KINETICS_DATASET_DIR, "kinetics") + export_dir = osp.join(test_dir, "dst") + imported_dataset.export(export_dir, "datumaro", save_media=True) + + exported_dataset = Dataset.import_from(export_dir, "datumaro") + + compare_datasets(helper_tc, imported_dataset, exported_dataset, require_media=True) diff --git a/tests/unit/test_video.py b/tests/unit/test_video.py index dbbe9d4787..2d44a61ef5 100644 --- a/tests/unit/test_video.py +++ b/tests/unit/test_video.py @@ -1,10 +1,11 @@ -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2024 Intel Corporation # # SPDX-License-Identifier: MIT import filecmp import os.path as osp from unittest import TestCase +from unittest.mock import MagicMock import numpy as np import pytest @@ -22,20 +23,18 @@ from tests.utils.test_utils import TestDir, compare_datasets -@pytest.fixture() -def fxt_sample_video(): - with TestDir() as test_dir: - video_path = osp.join(test_dir, "video.avi") - make_sample_video(video_path, frame_size=(4, 6), frames=4) - - yield video_path +def _make_sample_video(video_dir, fname="video.avi", frame_size=(4, 6), frames=4): + video_path = osp.join(video_dir, fname) + make_sample_video(video_path, frame_size=frame_size, frames=frames) + return video_path class VideoTest: @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_read_video(self, fxt_sample_video): - video = Video(fxt_sample_video) + def test_can_read_video(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) assert None is video.length @@ -43,20 +42,33 @@ def test_can_read_video(self, fxt_sample_video): @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_read_frames_sequentially(self, fxt_sample_video): - video = Video(fxt_sample_video) + def test_can_read_frames_sequentially(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) + assert None == video._frame_count + for idx, frame in enumerate(video): + assert frame.size == video.frame_size + assert frame.index == idx + assert frame.video is video + assert np.array_equal(frame.data, np.ones((*video.frame_size, 3)) * idx) + + assert 4 == video._frame_count for idx, frame in enumerate(video): assert frame.size == video.frame_size assert frame.index == idx assert frame.video is video assert np.array_equal(frame.data, np.ones((*video.frame_size, 3)) * idx) + with pytest.raises(IndexError): + video.get_frame_data(idx + 1) + @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_read_frames_randomly(self, fxt_sample_video): - video = Video(fxt_sample_video) + def test_can_read_frames_randomly(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) for idx in {1, 3, 2, 0, 3}: @@ -64,10 +76,14 @@ def test_can_read_frames_randomly(self, fxt_sample_video): assert frame.index == idx assert np.array_equal(frame.data, np.ones((*video.frame_size, 3)) * idx) + with pytest.raises(IndexError): + frame = video[4] + @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_skip_frames_between(self, fxt_sample_video): - video = Video(fxt_sample_video, step=2) + def test_can_skip_frames_between(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path, step=2) on_exit_do(video.close) for idx, frame in enumerate(video): @@ -75,29 +91,53 @@ def test_can_skip_frames_between(self, fxt_sample_video): @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_skip_from_start(self, fxt_sample_video): - video = Video(fxt_sample_video, start_frame=1) + def test_can_skip_from_start(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path, start_frame=1) on_exit_do(video.close) assert 1 == next(iter(video)).index @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_skip_from_end(self, fxt_sample_video): - video = Video(fxt_sample_video, end_frame=2) + def test_can_skip_from_end(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path, end_frame=2) on_exit_do(video.close) last_frame = None 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 - def test_can_init_frame_count_lazily(self, fxt_sample_video): - video = Video(fxt_sample_video) + def test_check_invalid_length(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path, step=2, start_frame=1, end_frame=1) + on_exit_do(video.close) + + with pytest.raises(ValueError): + video.length + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @scoped + def test_check_invalid_end_frame(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path, end_frame=4) + on_exit_do(video.close) + + with pytest.raises(ValueError): + for _ in video: + pass + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @scoped + def test_can_init_frame_count_lazily(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) assert None is video.length @@ -109,18 +149,35 @@ def test_can_init_frame_count_lazily(self, fxt_sample_video): @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_open_lazily(self): - with TestDir() as test_dir: - video = Video(osp.join(test_dir, "path.mp4")) + def test_can_open_lazily(self, test_dir): + video = Video(osp.join(test_dir, "path.mp4")) + + assert osp.join(test_dir, "path.mp4") == video.path + assert ".mp4" == video.ext + + @mark_requirement(Requirements.DATUM_GENERAL_REQ) + @scoped + def test_can_compare(self, test_dir): + video_path1 = _make_sample_video(test_dir) + video1 = Video(video_path1) + assert video1 != Video(video_path1, step=2) + assert video1 != Video(video_path1, start_frame=1) + assert video1 != Video(video_path1, end_frame=2) + assert video1 == Video(video_path1, end_frame=3) + + video_path2 = _make_sample_video(test_dir, fname="video2.avi") + assert video1 == Video(video_path2) - assert osp.join(test_dir, "path.mp4") == video.path - assert ".mp4" == video.ext + video_path3 = _make_sample_video(test_dir, fname="video3.avi", frames=6) + assert video1 != Video(video_path3) + assert Video(video_path3, end_frame=3) == video1 class VideoExtractorTest: @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_read_frames(self, fxt_sample_video): + def test_can_read_frames(self, test_dir): + video_path = _make_sample_video(test_dir) expected = Dataset.from_iterable( [ DatasetItem( @@ -133,15 +190,16 @@ def test_can_read_frames(self, fxt_sample_video): ) actual = Dataset.import_from( - fxt_sample_video, "video_frames", subset="train", name_pattern="frame_%03d" + video_path, "video_frames", subset="train", name_pattern="frame_%03d" ) compare_datasets(TestCase(), expected, actual) @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_split_and_load(self, fxt_sample_video): - test_dir = scope_add(TestDir()) + def test_can_split_and_load(self, test_dir): + video_path = _make_sample_video(test_dir) + dataset_dir = Scope().add(TestDir()) expected = Dataset.from_iterable( [ @@ -151,62 +209,65 @@ def test_can_split_and_load(self, fxt_sample_video): ) dataset = Dataset.import_from( - fxt_sample_video, "video_frames", start_frame=0, end_frame=4, name_pattern="frame_%06d" + video_path, "video_frames", start_frame=0, end_frame=3, name_pattern="frame_%06d" ) - dataset.export(format="image_dir", save_dir=test_dir, image_ext=".jpg") + dataset.export(format="image_dir", save_dir=dataset_dir, image_ext=".jpg") - actual = Dataset.import_from(test_dir, "image_dir") + actual = Dataset.import_from(dataset_dir, "image_dir") compare_datasets(TestCase(), expected, actual) class ProjectTest: @mark_requirement(Requirements.DATUM_GENERAL_REQ) - def test_can_release_resources_on_exit(self, fxt_sample_video): + def test_can_release_resources_on_exit(self, test_dir): + video_path = _make_sample_video(test_dir) with Scope() as scope: - test_dir = scope.add(TestDir()) + project_dir = scope.add(TestDir()) - project = scope.add(Project.init(test_dir)) + project = scope.add(Project.init(project_dir)) project.import_source( "src", - osp.dirname(fxt_sample_video), + osp.dirname(video_path), "video_frames", - rpath=osp.basename(fxt_sample_video), + rpath=osp.basename(video_path), ) assert len(project.working_tree.make_dataset()) == 4 - assert not osp.exists(test_dir) + assert not osp.exists(project_dir) @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_release_resources_on_remove(self, fxt_sample_video): - test_dir = scope_add(TestDir()) + def test_can_release_resources_on_remove(self, test_dir): + video_path = _make_sample_video(test_dir) + project_dir = scope_add(TestDir()) - project = scope_add(Project.init(test_dir)) + project = scope_add(Project.init(project_dir)) project.import_source( "src", - osp.dirname(fxt_sample_video), + osp.dirname(video_path), "video_frames", - rpath=osp.basename(fxt_sample_video), + rpath=osp.basename(video_path), ) project.commit("commit 1") assert len(project.working_tree.make_dataset()) == 4 - assert osp.isdir(osp.join(test_dir, "src")) + assert osp.isdir(osp.join(project_dir, "src")) project.remove_source("src", keep_data=False) - assert not osp.exists(osp.join(test_dir, "src")) + assert not osp.exists(osp.join(project_dir, "src")) @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_release_resources_on_checkout(self, fxt_sample_video): - test_dir = scope_add(TestDir()) + def test_can_release_resources_on_checkout(self, test_dir): + video_path = _make_sample_video(test_dir) + project_dir = scope_add(TestDir()) - project = scope_add(Project.init(test_dir)) + project = scope_add(Project.init(project_dir)) - src_url = osp.join(test_dir, "src") + src_url = osp.join(project_dir, "src") src = Dataset.from_iterable( [ DatasetItem(1), @@ -221,14 +282,14 @@ def test_can_release_resources_on_checkout(self, fxt_sample_video): project.import_source( "src", - osp.dirname(fxt_sample_video), + osp.dirname(video_path), "video_frames", - rpath=osp.basename(fxt_sample_video), + rpath=osp.basename(video_path), ) project.commit("commit 2") assert len(project.working_tree.make_dataset()) == 4 - assert osp.isdir(osp.join(test_dir, "src")) + assert osp.isdir(osp.join(project_dir, "src")) project.checkout("HEAD~1") @@ -240,8 +301,9 @@ class VideoAnnotationTest: @mark_requirement(Requirements.DATUM_GENERAL_REQ) @pytest.mark.parametrize("dataset_format", ["datumaro", "datumaro_binary"]) @scoped - def test_can_video_annotation_export(self, dataset_format, fxt_sample_video): - video = Video(fxt_sample_video) + def test_can_video_annotation_export(self, dataset_format, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) expected = Dataset.from_iterable( @@ -265,8 +327,9 @@ def test_can_video_annotation_export(self, dataset_format, fxt_sample_video): @mark_requirement(Requirements.DATUM_GENERAL_REQ) @scoped - def test_can_save_video(self, fxt_sample_video): - video = Video(fxt_sample_video) + def test_can_save_video(self, test_dir): + video_path = _make_sample_video(test_dir) + video = Video(video_path) on_exit_do(video.close) with TestDir() as test_dir: diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 9f0cd9e836..64b600d1f8 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2023 Intel Corporation +# Copyright (C) 2019-2024 Intel Corporation # # SPDX-License-Identifier: MIT @@ -21,7 +21,7 @@ from datumaro.components.annotation import AnnotationType from datumaro.components.dataset import Dataset, StreamDataset from datumaro.components.dataset_base import IDataset -from datumaro.components.media import Image, MultiframeImage, PointCloud, VideoFrame +from datumaro.components.media import Image, MultiframeImage, PointCloud, Video, VideoFrame from datumaro.util import filter_dict, find from datumaro.util.os_util import rmfile, rmtree @@ -204,6 +204,8 @@ def compare_datasets( elif isinstance(item_a.media, PointCloud): test.assertEqual(item_a.media.data, item_b.media.data, item_a.id) test.assertEqual(item_a.media.extra_images, item_b.media.extra_images, item_a.id) + elif isinstance(item_a.media, Video): + test.assertEqual(item_a.media, item_b.media, item_a.id) elif isinstance(item_a.media, VideoFrame): test.assertEqual(item_a.media, item_b.media, item_a.id) test.assertEqual(item_a.index, item_b.index, item_a.id) @@ -323,27 +325,28 @@ def check_save_and_load( def _change_path_in_items(dataset, source_path, target_path): for item in dataset: - if item.media and hasattr(item.media, "path"): - path = item.media._path - item.media = item.media.from_self(path=path.replace(source_path, target_path)) - if item.media and isinstance(item.media, PointCloud): - new_images = [] - for image in item.media.extra_images: - if hasattr(image, "path"): - path = image._path - new_images.append( - image.from_self(path=path.replace(source_path, target_path)) - ) - else: - new_images.append(image) - item.media._extra_images = new_images + if item.media: + if hasattr(item.media, "path") and item.media.path: + path = item.media.path.replace(source_path, target_path) + item.media = item.media.from_self(path=path) + if isinstance(item.media, PointCloud): + new_images = [] + for image in item.media.extra_images: + if hasattr(image, "path"): + path = image._path + new_images.append( + image.from_self(path=path.replace(source_path, target_path)) + ) + else: + new_images.append(image) + item.media._extra_images = new_images with TestDir() as tmp_dir: converter(source_dataset, test_dir, stream=stream) if move_save_dir: save_dir = tmp_dir for file in os.listdir(test_dir): - shutil.move(osp.join(test_dir, file), save_dir) + os.symlink(osp.join(test_dir, file), osp.join(save_dir, file)) else: save_dir = test_dir