From 145fca097894b6a6caf36f01d0d41aebdfcc84af Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:26:59 +0100 Subject: [PATCH 01/19] Add the "meta" prefix to MetaTensor __repr__ and __str__ Signed-off-by: Mathijs de Boer --- monai/data/meta_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/data/meta_tensor.py b/monai/data/meta_tensor.py index 3bbd243b4a..77b532aea5 100644 --- a/monai/data/meta_tensor.py +++ b/monai/data/meta_tensor.py @@ -572,14 +572,14 @@ def __repr__(self): Prints a representation of the tensor identical to ``torch.Tensor.__repr__``. Use ``print_verbose`` for associated metadata. """ - return self.as_tensor().__repr__() + return "meta" + self.as_tensor().__repr__() def __str__(self): """ Prints a representation of the tensor identical to ``torch.Tensor.__str__``. Use ``print_verbose`` for associated metadata. """ - return str(self.as_tensor()) + return "meta" + str(self.as_tensor()) def print_verbose(self) -> None: """Verbose print with meta data.""" From 57c23ee9a44b8738214cc1a4981d45efdeeaf7bc Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:28:18 +0100 Subject: [PATCH 02/19] Add the FolderLayoutBase class to serve as an abstract class to extend for custom implementations Signed-off-by: Mathijs de Boer --- monai/data/__init__.py | 2 +- monai/data/folder_layout.py | 62 ++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/monai/data/__init__.py b/monai/data/__init__.py index 8d8297deaf..0e9759aaf1 100644 --- a/monai/data/__init__.py +++ b/monai/data/__init__.py @@ -46,7 +46,7 @@ load_decathlon_datalist, load_decathlon_properties, ) -from .folder_layout import FolderLayout +from .folder_layout import FolderLayout, FolderLayoutBase from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter, PatchIterd from .image_dataset import ImageDataset from .image_reader import ImageReader, ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader diff --git a/monai/data/folder_layout.py b/monai/data/folder_layout.py index 190a07334d..d966ebab96 100644 --- a/monai/data/folder_layout.py +++ b/monai/data/folder_layout.py @@ -11,14 +11,17 @@ from __future__ import annotations +from abc import ABC, abstractmethod + import monai from monai.config import PathLike from monai.data.utils import create_file_basename +from monai.transforms import Transform -__all__ = ["FolderLayout", "default_name_formatter"] +__all__ = ["FolderLayoutBase", "FolderLayout", "default_name_formatter"] -def default_name_formatter(metadict, saver): +def default_name_formatter(metadict: dict, saver: Transform) -> dict: """Returns a kwargs dict for :py:meth:`FolderLayout.filename`, according to the input metadata and SaveImage transform.""" subject = ( @@ -30,7 +33,58 @@ def default_name_formatter(metadict, saver): return {"subject": f"{subject}", "idx": patch_index} -class FolderLayout: +class FolderLayoutBase(ABC): + """ + Abstract base class to define a common interface for FolderLayout and derived classes + Mainly, defines the ``filename(**kwargs) -> PathLike`` function, which must be defined + by the deriving class. + + Example: + + .. code-block:: python + + from monai.data import FolderLayoutBase + + class MyFolderLayout(FolderLayoutBase): + def __init__( + self, + basepath: Path, + extension: str = "", + makedirs: bool = False + ): + self.basepath = basepath + if not extension: + self.extension = "" + elif extension.startswith("."): + self.extension = extension: + else: + self.extension = f".{extension}" + self.makedirs = makedirs + + def filename(self, patient_no: int, image_name: str, **kwargs) -> Path: + sub_path = self.basepath / patient_no + if not sub_path.exists(): + sub_path.mkdir(parents=True) + + file = image_name + for k, v in kwargs.items(): + file += f"_{k}-{v}" + + file += self.extension + return sub_path / file + + """ + + @abstractmethod + def filename(self, **kwargs) -> PathLike: + """ + Create a filename with path based on the input kwargs. + Abstract method, implement your own. + """ + raise NotImplementedError + + +class FolderLayout(FolderLayoutBase): """ A utility class to create organized filenames within ``output_dir``. The ``filename`` method could be used to create a filename following the folder structure. @@ -81,7 +135,7 @@ def __init__( self.makedirs = makedirs self.data_root_dir = data_root_dir - def filename(self, subject: PathLike = "subject", idx=None, **kwargs): + def filename(self, subject: PathLike = "subject", idx=None, **kwargs) -> PathLike: """ Create a filename based on the input ``subject`` and ``idx``. From 7e72b8506a059fd0fd48d25ea53e315ee73ebc5a Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:31:01 +0100 Subject: [PATCH 03/19] Use replaceable FolderLayout in SaveImage and SaveImaged Add `savepath_in_metadict`, to include the location of the output image in the metadata Signed-off-by: Mathijs de Boer --- monai/transforms/io/array.py | 84 +++++++++++++++++++------------ monai/transforms/io/dictionary.py | 64 ++++++++++++----------- 2 files changed, 88 insertions(+), 60 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index a21a070b15..edcdf7e4a3 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -23,13 +23,14 @@ from collections.abc import Sequence from pathlib import Path from pydoc import locate +from typing import Callable import numpy as np import torch from monai.config import DtypeLike, NdarrayOrTensor, PathLike from monai.data import image_writer -from monai.data.folder_layout import FolderLayout, default_name_formatter +from monai.data.folder_layout import FolderLayout, FolderLayoutBase, default_name_formatter from monai.data.image_reader import ( ImageReader, ITKReader, @@ -319,7 +320,7 @@ class SaveImage(Transform): output_ext: output file extension name. output_dtype: data type (if not None) for saving data. Defaults to ``np.float32``. resample: whether to resample image (if needed) before saving the data array, - based on the `spatial_shape` (and `original_affine`) from metadata. + based on the ``"spatial_shape"`` (and ``"original_affine"``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. Depending on the writers, the possible options are @@ -332,40 +333,47 @@ class SaveImage(Transform): Possible options are {``"zeros"``, ``"border"``, ``"reflection"``} See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample scale: {``255``, ``65535``} postprocess data by clipping to [0, 1] and scaling - [0, 255] (uint8) or [0, 65535] (uint16). Default is `None` (no scaling). + [0, 255] (``uint8``) or [0, 65535] (``uint16``). Default is ``None`` (no scaling). dtype: data type during resampling computation. Defaults to ``np.float64`` for best precision. - if None, use the data type of input data. To set the output data type, use `output_dtype`. - squeeze_end_dims: if True, any trailing singleton dimensions will be removed (after the channel + if ``None``, use the data type of input data. To set the output data type, use ``output_dtype``. + squeeze_end_dims: if ``True``, any trailing singleton dimensions will be removed (after the channel has been moved to the end). So if input is (C,H,W,D), this will be altered to (H,W,D,C), and - then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If `false`, + then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If ``False``, image will always be saved as (H,W,D,C). data_root_dir: if not empty, it specifies the beginning parts of the input file's - absolute path. It's used to compute `input_file_rel_path`, the relative path to the file from - `data_root_dir` to preserve folder structure when saving in case there are files in different + absolute path. It's used to compute ``input_file_rel_path``, the relative path to the file from + ``data_root_dir`` to preserve folder structure when saving in case there are files in different folders with the same file names. For example, with the following inputs: - - input_file_name: `/foo/bar/test1/image.nii` - - output_postfix: `seg` - - output_ext: `.nii.gz` - - output_dir: `/output` - - data_root_dir: `/foo/bar` + - input_file_name: ``/foo/bar/test1/image.nii`` + - output_postfix: ``seg`` + - output_ext: ``.nii.gz`` + - output_dir: ``/output`` + - data_root_dir: ``/foo/bar`` - The output will be: /output/test1/image/image_seg.nii.gz + The output will be: ``/output/test1/image/image_seg.nii.gz`` separate_folder: whether to save every file in a separate folder. For example: for the input filename - `image.nii`, postfix `seg` and folder_path `output`, if `separate_folder=True`, it will be saved as: - `output/image/image_seg.nii`, if `False`, saving as `output/image_seg.nii`. Default to `True`. - print_log: whether to print logs when saving. Default to `True`. + ``image.nii``, postfix ``seg`` and ``folder_path`` ``output``, if ``separate_folder=True``, it will be + saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. + Default to ``True``. + print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string of filename extension to specify the output image writer. - see also: `monai.data.image_writer.SUPPORTED_WRITERS`. - writer: a customised `monai.data.ImageWriter` subclass to save data arrays. - if `None`, use the default writer from `monai.data.image_writer` according to `output_ext`. + see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. + writer: a customised ``monai.data.ImageWriter`` subclass to save data arrays. + if ``None``, use the default writer from ``monai.data.image_writer`` according to ``output_ext``. if it's a string, it's treated as a class name or dotted path (such as ``"monai.data.ITKWriter"``); the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. - channel_dim: the index of the channel dimension. Default to `0`. - `None` to indicate no channel dimension. + channel_dim: the index of the channel dimension. Default to ``0``. + ``None`` to indicate no channel dimension. output_name_formatter: a callable function (returning a kwargs dict) to format the output file name. + If using a custom ``monai.data.FolderLayoutBase`` class in ``folder_layout``, consider providing + your own formatter. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. + folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. + if ``None``, uses the default ``FolderLayout`` + savepath_in_metadict: if ``True``, adds a key ``"saved_to"`` to the metadata, which contains the path + to where the input image has been saved. """ @deprecated_arg_default("resample", True, False, since="1.1", replaced="1.3") @@ -387,16 +395,26 @@ def __init__( output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, channel_dim: int | None = 0, - output_name_formatter=None, + output_name_formatter: Callable[[dict, Transform], dict] = None, + folder_layout: FolderLayoutBase = None, + savepath_in_metadict: bool = False, ) -> None: - self.folder_layout = FolderLayout( - output_dir=output_dir, - postfix=output_postfix, - extension=output_ext, - parent=separate_folder, - makedirs=True, - data_root_dir=data_root_dir, - ) + if folder_layout is None: + self.folder_layout = FolderLayout( + output_dir=output_dir, + postfix=output_postfix, + extension=output_ext, + parent=separate_folder, + makedirs=True, + data_root_dir=data_root_dir, + ) + else: + self.folder_layout = folder_layout + + if output_name_formatter is None: + self.fname_formatter = default_name_formatter + else: + self.fname_formatter = output_name_formatter self.output_ext = output_ext.lower() or output_format.lower() if isinstance(writer, str): @@ -418,8 +436,8 @@ def __init__( self.data_kwargs = {"squeeze_end_dims": squeeze_end_dims, "channel_dim": channel_dim} self.meta_kwargs = {"resample": resample, "mode": mode, "padding_mode": padding_mode, "dtype": dtype} self.write_kwargs = {"verbose": print_log} - self.fname_formatter = default_name_formatter if output_name_formatter is None else output_name_formatter self._data_index = 0 + self.savepath_in_metadict = savepath_in_metadict def set_options(self, init_kwargs=None, data_kwargs=None, meta_kwargs=None, write_kwargs=None): """ @@ -478,6 +496,8 @@ def __call__(self, img: torch.Tensor | np.ndarray, meta_data: dict | None = None ) else: self._data_index += 1 + if self.savepath_in_metadict: + meta_data["saved_to"] = filename return img msg = "\n".join([f"{e}" for e in err]) raise RuntimeError( diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 47dfbf7e28..f4afca9483 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -18,14 +18,15 @@ from __future__ import annotations from pathlib import Path +from typing import Callable import numpy as np from monai.config import DtypeLike, KeysCollection -from monai.data import image_writer +from monai.data import FolderLayoutBase, image_writer from monai.data.image_reader import ImageReader from monai.transforms.io.array import LoadImage, SaveImage -from monai.transforms.transform import MapTransform +from monai.transforms.transform import MapTransform, Transform from monai.utils import GridSamplePadMode, ensure_tuple, ensure_tuple_rep from monai.utils.deprecate_utils import deprecated_arg_default from monai.utils.enums import PostFix @@ -189,16 +190,16 @@ class SaveImaged(MapTransform): keys: keys of the corresponding items to be transformed. See also: :py:class:`monai.transforms.compose.MapTransform` meta_keys: explicitly indicate the key of the corresponding metadata dictionary. - For example, for data with key `image`, the metadata by default is in `image_meta_dict`. - The metadata is a dictionary contains values such as filename, original_shape. - This argument can be a sequence of string, map to the `keys`. - If `None`, will try to construct meta_keys by `key_{meta_key_postfix}`. - meta_key_postfix: if `meta_keys` is `None`, use `key_{meta_key_postfix}` to retrieve the metadict. + For example, for data with key ``image``, the metadata by default is in ``image_meta_dict``. + The metadata is a dictionary contains values such as ``filename``, ``original_shape``. + This argument can be a sequence of strings, mapped to the ``keys``. + If ``None``, will try to construct ``meta_keys`` by ``key_{meta_key_postfix}``. + meta_key_postfix: if ``meta_keys`` is ``None``, use ``key_{meta_key_postfix}`` to retrieve the metadict. output_dir: output image directory. - output_postfix: a string appended to all output file names, default to `trans`. - output_ext: output file extension name, available extensions: `.nii.gz`, `.nii`, `.png`. + output_postfix: a string appended to all output file names, default to ``trans``. + output_ext: output file extension name, available extensions: ``.nii.gz``, ``.nii``, ``.png``. resample: whether to resample image (if needed) before saving the data array, - based on the `spatial_shape` (and `original_affine`) from metadata. + based on the ``spatial_shape`` (and ``original_affine``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. Depending on the writers, the possible options are: @@ -211,9 +212,9 @@ class SaveImaged(MapTransform): Possible options are {``"zeros"``, ``"border"``, ``"reflection"``} See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample scale: {``255``, ``65535``} postprocess data by clipping to [0, 1] and scaling - [0, 255] (uint8) or [0, 65535] (uint16). Default is `None` (no scaling). + [0, 255] (``uint8``) or [0, 65535] (``uint16``). Default is ``None`` (no scaling). dtype: data type during resampling computation. Defaults to ``np.float64`` for best precision. - if None, use the data type of input data. To set the output data type, use `output_dtype`. + if None, use the data type of input data. To set the output data type, use ``output_dtype``. output_dtype: data type for saving data. Defaults to ``np.float32``. allow_missing_keys: don't raise exception if key is missing. squeeze_end_dims: if True, any trailing singleton dimensions will be removed (after the channel @@ -221,31 +222,34 @@ class SaveImaged(MapTransform): then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If `false`, image will always be saved as (H,W,D,C). data_root_dir: if not empty, it specifies the beginning parts of the input file's - absolute path. It's used to compute `input_file_rel_path`, the relative path to the file from - `data_root_dir` to preserve folder structure when saving in case there are files in different + absolute path. It's used to compute ``input_file_rel_path``, the relative path to the file from + ``data_root_dir`` to preserve folder structure when saving in case there are files in different folders with the same file names. For example, with the following inputs: - - input_file_name: `/foo/bar/test1/image.nii` - - output_postfix: `seg` - - output_ext: `.nii.gz` - - output_dir: `/output` - - data_root_dir: `/foo/bar` + - input_file_name: ``/foo/bar/test1/image.nii`` + - output_postfix: ``seg`` + - output_ext: ``.nii.gz`` + - output_dir: ``/output`` + - data_root_dir: ``/foo/bar`` - The output will be: /output/test1/image/image_seg.nii.gz + The output will be: ``/output/test1/image/image_seg.nii.gz`` separate_folder: whether to save every file in a separate folder. For example: for the input filename - `image.nii`, postfix `seg` and folder_path `output`, if `separate_folder=True`, it will be saved as: - `output/image/image_seg.nii`, if `False`, saving as `output/image_seg.nii`. Default to `True`. - print_log: whether to print logs when saving. Default to `True`. + ``image.nii``, postfix ``seg`` and folder_path ``output``, if ``separate_folder=True``, it will be saved as: + ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. + print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string to specify the output image writer. - see also: `monai.data.image_writer.SUPPORTED_WRITERS`. - writer: a customised `monai.data.ImageWriter` subclass to save data arrays. - if `None`, use the default writer from `monai.data.image_writer` according to `output_ext`. + see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. + writer: a customised ``monai.data.ImageWriter`` subclass to save data arrays. + if ``None``, use the default writer from ``monai.data.image_writer`` according to ``output_ext``. if it's a string, it's treated as a class name or dotted path; the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. output_name_formatter: a callable function (returning a kwargs dict) to format the output file name. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. - + folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. + if ``None``, uses the default ``FolderLayout`` + savepath_in_metadict: if ``True``, adds a key ``saved_to`` to the metadata, which contains the path + to where the input image has been saved. """ @deprecated_arg_default("resample", True, False, since="1.1", replaced="1.3") @@ -270,7 +274,9 @@ def __init__( print_log: bool = True, output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, - output_name_formatter=None, + output_name_formatter: Callable[[dict, Transform], dict] = None, + folder_layout: FolderLayoutBase = None, + savepath_in_metadict: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) self.meta_keys = ensure_tuple_rep(meta_keys, len(self.keys)) @@ -292,6 +298,8 @@ def __init__( output_format=output_format, writer=writer, output_name_formatter=output_name_formatter, + folder_layout=folder_layout, + savepath_in_metadict=savepath_in_metadict, ) def set_options(self, init_kwargs=None, data_kwargs=None, meta_kwargs=None, write_kwargs=None): From b6ee3901419a967e7b3b32aa3854d9e061380af4 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:43:06 +0100 Subject: [PATCH 04/19] Fix accidentally introduced circular imports. Signed-off-by: Mathijs de Boer --- monai/data/folder_layout.py | 3 +-- monai/transforms/io/dictionary.py | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monai/data/folder_layout.py b/monai/data/folder_layout.py index d966ebab96..4403855030 100644 --- a/monai/data/folder_layout.py +++ b/monai/data/folder_layout.py @@ -16,12 +16,11 @@ import monai from monai.config import PathLike from monai.data.utils import create_file_basename -from monai.transforms import Transform __all__ = ["FolderLayoutBase", "FolderLayout", "default_name_formatter"] -def default_name_formatter(metadict: dict, saver: Transform) -> dict: +def default_name_formatter(metadict: dict, saver: monai.transforms.Transform) -> dict: """Returns a kwargs dict for :py:meth:`FolderLayout.filename`, according to the input metadata and SaveImage transform.""" subject = ( diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index f4afca9483..09d2113829 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -22,8 +22,9 @@ import numpy as np +import monai from monai.config import DtypeLike, KeysCollection -from monai.data import FolderLayoutBase, image_writer +from monai.data import image_writer from monai.data.image_reader import ImageReader from monai.transforms.io.array import LoadImage, SaveImage from monai.transforms.transform import MapTransform, Transform @@ -275,7 +276,7 @@ def __init__( output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, output_name_formatter: Callable[[dict, Transform], dict] = None, - folder_layout: FolderLayoutBase = None, + folder_layout: monai.data.FolderLayoutBase = None, savepath_in_metadict: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) From 9e1dcd474f0ba2a7435cd87c1fdbdec5242f5c3f Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 14:36:26 +0100 Subject: [PATCH 05/19] Fix MetaTensor tests Signed-off-by: Mathijs de Boer --- tests/test_meta_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_meta_tensor.py b/tests/test_meta_tensor.py index 4f2cb9636a..a6607a3ccd 100644 --- a/tests/test_meta_tensor.py +++ b/tests/test_meta_tensor.py @@ -430,8 +430,8 @@ def test_decollate(self, dtype): def test_str(self): t = MetaTensor([1.0], affine=torch.tensor(1), meta={"fname": "filename"}) - self.assertEqual(str(t), "tensor([1.])") - self.assertEqual(t.__repr__(), "tensor([1.])") + self.assertEqual(str(t), "metatensor([1.])") + self.assertEqual(t.__repr__(), "metatensor([1.])") def test_shape(self): s = MetaTensor([1]) From 0fe40347118ccdbd3377d3ff80e619b340e615b4 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:28:18 +0100 Subject: [PATCH 06/19] Add the FolderLayoutBase class to serve as an abstract class to extend for custom implementations Signed-off-by: Mathijs de Boer --- monai/data/__init__.py | 2 +- monai/data/folder_layout.py | 62 ++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/monai/data/__init__.py b/monai/data/__init__.py index 8d8297deaf..0e9759aaf1 100644 --- a/monai/data/__init__.py +++ b/monai/data/__init__.py @@ -46,7 +46,7 @@ load_decathlon_datalist, load_decathlon_properties, ) -from .folder_layout import FolderLayout +from .folder_layout import FolderLayout, FolderLayoutBase from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter, PatchIterd from .image_dataset import ImageDataset from .image_reader import ImageReader, ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader diff --git a/monai/data/folder_layout.py b/monai/data/folder_layout.py index 190a07334d..d966ebab96 100644 --- a/monai/data/folder_layout.py +++ b/monai/data/folder_layout.py @@ -11,14 +11,17 @@ from __future__ import annotations +from abc import ABC, abstractmethod + import monai from monai.config import PathLike from monai.data.utils import create_file_basename +from monai.transforms import Transform -__all__ = ["FolderLayout", "default_name_formatter"] +__all__ = ["FolderLayoutBase", "FolderLayout", "default_name_formatter"] -def default_name_formatter(metadict, saver): +def default_name_formatter(metadict: dict, saver: Transform) -> dict: """Returns a kwargs dict for :py:meth:`FolderLayout.filename`, according to the input metadata and SaveImage transform.""" subject = ( @@ -30,7 +33,58 @@ def default_name_formatter(metadict, saver): return {"subject": f"{subject}", "idx": patch_index} -class FolderLayout: +class FolderLayoutBase(ABC): + """ + Abstract base class to define a common interface for FolderLayout and derived classes + Mainly, defines the ``filename(**kwargs) -> PathLike`` function, which must be defined + by the deriving class. + + Example: + + .. code-block:: python + + from monai.data import FolderLayoutBase + + class MyFolderLayout(FolderLayoutBase): + def __init__( + self, + basepath: Path, + extension: str = "", + makedirs: bool = False + ): + self.basepath = basepath + if not extension: + self.extension = "" + elif extension.startswith("."): + self.extension = extension: + else: + self.extension = f".{extension}" + self.makedirs = makedirs + + def filename(self, patient_no: int, image_name: str, **kwargs) -> Path: + sub_path = self.basepath / patient_no + if not sub_path.exists(): + sub_path.mkdir(parents=True) + + file = image_name + for k, v in kwargs.items(): + file += f"_{k}-{v}" + + file += self.extension + return sub_path / file + + """ + + @abstractmethod + def filename(self, **kwargs) -> PathLike: + """ + Create a filename with path based on the input kwargs. + Abstract method, implement your own. + """ + raise NotImplementedError + + +class FolderLayout(FolderLayoutBase): """ A utility class to create organized filenames within ``output_dir``. The ``filename`` method could be used to create a filename following the folder structure. @@ -81,7 +135,7 @@ def __init__( self.makedirs = makedirs self.data_root_dir = data_root_dir - def filename(self, subject: PathLike = "subject", idx=None, **kwargs): + def filename(self, subject: PathLike = "subject", idx=None, **kwargs) -> PathLike: """ Create a filename based on the input ``subject`` and ``idx``. From b9787c712f62b0cc3dd4085dca9c17cce63ca430 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:31:01 +0100 Subject: [PATCH 07/19] Use replaceable FolderLayout in SaveImage and SaveImaged Add `savepath_in_metadict`, to include the location of the output image in the metadata Signed-off-by: Mathijs de Boer --- monai/transforms/io/array.py | 84 +++++++++++++++++++------------ monai/transforms/io/dictionary.py | 64 ++++++++++++----------- 2 files changed, 88 insertions(+), 60 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index a21a070b15..edcdf7e4a3 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -23,13 +23,14 @@ from collections.abc import Sequence from pathlib import Path from pydoc import locate +from typing import Callable import numpy as np import torch from monai.config import DtypeLike, NdarrayOrTensor, PathLike from monai.data import image_writer -from monai.data.folder_layout import FolderLayout, default_name_formatter +from monai.data.folder_layout import FolderLayout, FolderLayoutBase, default_name_formatter from monai.data.image_reader import ( ImageReader, ITKReader, @@ -319,7 +320,7 @@ class SaveImage(Transform): output_ext: output file extension name. output_dtype: data type (if not None) for saving data. Defaults to ``np.float32``. resample: whether to resample image (if needed) before saving the data array, - based on the `spatial_shape` (and `original_affine`) from metadata. + based on the ``"spatial_shape"`` (and ``"original_affine"``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. Depending on the writers, the possible options are @@ -332,40 +333,47 @@ class SaveImage(Transform): Possible options are {``"zeros"``, ``"border"``, ``"reflection"``} See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample scale: {``255``, ``65535``} postprocess data by clipping to [0, 1] and scaling - [0, 255] (uint8) or [0, 65535] (uint16). Default is `None` (no scaling). + [0, 255] (``uint8``) or [0, 65535] (``uint16``). Default is ``None`` (no scaling). dtype: data type during resampling computation. Defaults to ``np.float64`` for best precision. - if None, use the data type of input data. To set the output data type, use `output_dtype`. - squeeze_end_dims: if True, any trailing singleton dimensions will be removed (after the channel + if ``None``, use the data type of input data. To set the output data type, use ``output_dtype``. + squeeze_end_dims: if ``True``, any trailing singleton dimensions will be removed (after the channel has been moved to the end). So if input is (C,H,W,D), this will be altered to (H,W,D,C), and - then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If `false`, + then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If ``False``, image will always be saved as (H,W,D,C). data_root_dir: if not empty, it specifies the beginning parts of the input file's - absolute path. It's used to compute `input_file_rel_path`, the relative path to the file from - `data_root_dir` to preserve folder structure when saving in case there are files in different + absolute path. It's used to compute ``input_file_rel_path``, the relative path to the file from + ``data_root_dir`` to preserve folder structure when saving in case there are files in different folders with the same file names. For example, with the following inputs: - - input_file_name: `/foo/bar/test1/image.nii` - - output_postfix: `seg` - - output_ext: `.nii.gz` - - output_dir: `/output` - - data_root_dir: `/foo/bar` + - input_file_name: ``/foo/bar/test1/image.nii`` + - output_postfix: ``seg`` + - output_ext: ``.nii.gz`` + - output_dir: ``/output`` + - data_root_dir: ``/foo/bar`` - The output will be: /output/test1/image/image_seg.nii.gz + The output will be: ``/output/test1/image/image_seg.nii.gz`` separate_folder: whether to save every file in a separate folder. For example: for the input filename - `image.nii`, postfix `seg` and folder_path `output`, if `separate_folder=True`, it will be saved as: - `output/image/image_seg.nii`, if `False`, saving as `output/image_seg.nii`. Default to `True`. - print_log: whether to print logs when saving. Default to `True`. + ``image.nii``, postfix ``seg`` and ``folder_path`` ``output``, if ``separate_folder=True``, it will be + saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. + Default to ``True``. + print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string of filename extension to specify the output image writer. - see also: `monai.data.image_writer.SUPPORTED_WRITERS`. - writer: a customised `monai.data.ImageWriter` subclass to save data arrays. - if `None`, use the default writer from `monai.data.image_writer` according to `output_ext`. + see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. + writer: a customised ``monai.data.ImageWriter`` subclass to save data arrays. + if ``None``, use the default writer from ``monai.data.image_writer`` according to ``output_ext``. if it's a string, it's treated as a class name or dotted path (such as ``"monai.data.ITKWriter"``); the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. - channel_dim: the index of the channel dimension. Default to `0`. - `None` to indicate no channel dimension. + channel_dim: the index of the channel dimension. Default to ``0``. + ``None`` to indicate no channel dimension. output_name_formatter: a callable function (returning a kwargs dict) to format the output file name. + If using a custom ``monai.data.FolderLayoutBase`` class in ``folder_layout``, consider providing + your own formatter. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. + folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. + if ``None``, uses the default ``FolderLayout`` + savepath_in_metadict: if ``True``, adds a key ``"saved_to"`` to the metadata, which contains the path + to where the input image has been saved. """ @deprecated_arg_default("resample", True, False, since="1.1", replaced="1.3") @@ -387,16 +395,26 @@ def __init__( output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, channel_dim: int | None = 0, - output_name_formatter=None, + output_name_formatter: Callable[[dict, Transform], dict] = None, + folder_layout: FolderLayoutBase = None, + savepath_in_metadict: bool = False, ) -> None: - self.folder_layout = FolderLayout( - output_dir=output_dir, - postfix=output_postfix, - extension=output_ext, - parent=separate_folder, - makedirs=True, - data_root_dir=data_root_dir, - ) + if folder_layout is None: + self.folder_layout = FolderLayout( + output_dir=output_dir, + postfix=output_postfix, + extension=output_ext, + parent=separate_folder, + makedirs=True, + data_root_dir=data_root_dir, + ) + else: + self.folder_layout = folder_layout + + if output_name_formatter is None: + self.fname_formatter = default_name_formatter + else: + self.fname_formatter = output_name_formatter self.output_ext = output_ext.lower() or output_format.lower() if isinstance(writer, str): @@ -418,8 +436,8 @@ def __init__( self.data_kwargs = {"squeeze_end_dims": squeeze_end_dims, "channel_dim": channel_dim} self.meta_kwargs = {"resample": resample, "mode": mode, "padding_mode": padding_mode, "dtype": dtype} self.write_kwargs = {"verbose": print_log} - self.fname_formatter = default_name_formatter if output_name_formatter is None else output_name_formatter self._data_index = 0 + self.savepath_in_metadict = savepath_in_metadict def set_options(self, init_kwargs=None, data_kwargs=None, meta_kwargs=None, write_kwargs=None): """ @@ -478,6 +496,8 @@ def __call__(self, img: torch.Tensor | np.ndarray, meta_data: dict | None = None ) else: self._data_index += 1 + if self.savepath_in_metadict: + meta_data["saved_to"] = filename return img msg = "\n".join([f"{e}" for e in err]) raise RuntimeError( diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 47dfbf7e28..f4afca9483 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -18,14 +18,15 @@ from __future__ import annotations from pathlib import Path +from typing import Callable import numpy as np from monai.config import DtypeLike, KeysCollection -from monai.data import image_writer +from monai.data import FolderLayoutBase, image_writer from monai.data.image_reader import ImageReader from monai.transforms.io.array import LoadImage, SaveImage -from monai.transforms.transform import MapTransform +from monai.transforms.transform import MapTransform, Transform from monai.utils import GridSamplePadMode, ensure_tuple, ensure_tuple_rep from monai.utils.deprecate_utils import deprecated_arg_default from monai.utils.enums import PostFix @@ -189,16 +190,16 @@ class SaveImaged(MapTransform): keys: keys of the corresponding items to be transformed. See also: :py:class:`monai.transforms.compose.MapTransform` meta_keys: explicitly indicate the key of the corresponding metadata dictionary. - For example, for data with key `image`, the metadata by default is in `image_meta_dict`. - The metadata is a dictionary contains values such as filename, original_shape. - This argument can be a sequence of string, map to the `keys`. - If `None`, will try to construct meta_keys by `key_{meta_key_postfix}`. - meta_key_postfix: if `meta_keys` is `None`, use `key_{meta_key_postfix}` to retrieve the metadict. + For example, for data with key ``image``, the metadata by default is in ``image_meta_dict``. + The metadata is a dictionary contains values such as ``filename``, ``original_shape``. + This argument can be a sequence of strings, mapped to the ``keys``. + If ``None``, will try to construct ``meta_keys`` by ``key_{meta_key_postfix}``. + meta_key_postfix: if ``meta_keys`` is ``None``, use ``key_{meta_key_postfix}`` to retrieve the metadict. output_dir: output image directory. - output_postfix: a string appended to all output file names, default to `trans`. - output_ext: output file extension name, available extensions: `.nii.gz`, `.nii`, `.png`. + output_postfix: a string appended to all output file names, default to ``trans``. + output_ext: output file extension name, available extensions: ``.nii.gz``, ``.nii``, ``.png``. resample: whether to resample image (if needed) before saving the data array, - based on the `spatial_shape` (and `original_affine`) from metadata. + based on the ``spatial_shape`` (and ``original_affine``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. Depending on the writers, the possible options are: @@ -211,9 +212,9 @@ class SaveImaged(MapTransform): Possible options are {``"zeros"``, ``"border"``, ``"reflection"``} See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample scale: {``255``, ``65535``} postprocess data by clipping to [0, 1] and scaling - [0, 255] (uint8) or [0, 65535] (uint16). Default is `None` (no scaling). + [0, 255] (``uint8``) or [0, 65535] (``uint16``). Default is ``None`` (no scaling). dtype: data type during resampling computation. Defaults to ``np.float64`` for best precision. - if None, use the data type of input data. To set the output data type, use `output_dtype`. + if None, use the data type of input data. To set the output data type, use ``output_dtype``. output_dtype: data type for saving data. Defaults to ``np.float32``. allow_missing_keys: don't raise exception if key is missing. squeeze_end_dims: if True, any trailing singleton dimensions will be removed (after the channel @@ -221,31 +222,34 @@ class SaveImaged(MapTransform): then if C==1, it will be saved as (H,W,D). If D is also 1, it will be saved as (H,W). If `false`, image will always be saved as (H,W,D,C). data_root_dir: if not empty, it specifies the beginning parts of the input file's - absolute path. It's used to compute `input_file_rel_path`, the relative path to the file from - `data_root_dir` to preserve folder structure when saving in case there are files in different + absolute path. It's used to compute ``input_file_rel_path``, the relative path to the file from + ``data_root_dir`` to preserve folder structure when saving in case there are files in different folders with the same file names. For example, with the following inputs: - - input_file_name: `/foo/bar/test1/image.nii` - - output_postfix: `seg` - - output_ext: `.nii.gz` - - output_dir: `/output` - - data_root_dir: `/foo/bar` + - input_file_name: ``/foo/bar/test1/image.nii`` + - output_postfix: ``seg`` + - output_ext: ``.nii.gz`` + - output_dir: ``/output`` + - data_root_dir: ``/foo/bar`` - The output will be: /output/test1/image/image_seg.nii.gz + The output will be: ``/output/test1/image/image_seg.nii.gz`` separate_folder: whether to save every file in a separate folder. For example: for the input filename - `image.nii`, postfix `seg` and folder_path `output`, if `separate_folder=True`, it will be saved as: - `output/image/image_seg.nii`, if `False`, saving as `output/image_seg.nii`. Default to `True`. - print_log: whether to print logs when saving. Default to `True`. + ``image.nii``, postfix ``seg`` and folder_path ``output``, if ``separate_folder=True``, it will be saved as: + ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. + print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string to specify the output image writer. - see also: `monai.data.image_writer.SUPPORTED_WRITERS`. - writer: a customised `monai.data.ImageWriter` subclass to save data arrays. - if `None`, use the default writer from `monai.data.image_writer` according to `output_ext`. + see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. + writer: a customised ``monai.data.ImageWriter`` subclass to save data arrays. + if ``None``, use the default writer from ``monai.data.image_writer`` according to ``output_ext``. if it's a string, it's treated as a class name or dotted path; the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. output_name_formatter: a callable function (returning a kwargs dict) to format the output file name. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. - + folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. + if ``None``, uses the default ``FolderLayout`` + savepath_in_metadict: if ``True``, adds a key ``saved_to`` to the metadata, which contains the path + to where the input image has been saved. """ @deprecated_arg_default("resample", True, False, since="1.1", replaced="1.3") @@ -270,7 +274,9 @@ def __init__( print_log: bool = True, output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, - output_name_formatter=None, + output_name_formatter: Callable[[dict, Transform], dict] = None, + folder_layout: FolderLayoutBase = None, + savepath_in_metadict: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) self.meta_keys = ensure_tuple_rep(meta_keys, len(self.keys)) @@ -292,6 +298,8 @@ def __init__( output_format=output_format, writer=writer, output_name_formatter=output_name_formatter, + folder_layout=folder_layout, + savepath_in_metadict=savepath_in_metadict, ) def set_options(self, init_kwargs=None, data_kwargs=None, meta_kwargs=None, write_kwargs=None): From 6e4b7579973b3c5f371f5b8a0725077ff0761bba Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 13:43:06 +0100 Subject: [PATCH 08/19] Fix accidentally introduced circular imports. Signed-off-by: Mathijs de Boer --- monai/data/folder_layout.py | 3 +-- monai/transforms/io/dictionary.py | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monai/data/folder_layout.py b/monai/data/folder_layout.py index d966ebab96..4403855030 100644 --- a/monai/data/folder_layout.py +++ b/monai/data/folder_layout.py @@ -16,12 +16,11 @@ import monai from monai.config import PathLike from monai.data.utils import create_file_basename -from monai.transforms import Transform __all__ = ["FolderLayoutBase", "FolderLayout", "default_name_formatter"] -def default_name_formatter(metadict: dict, saver: Transform) -> dict: +def default_name_formatter(metadict: dict, saver: monai.transforms.Transform) -> dict: """Returns a kwargs dict for :py:meth:`FolderLayout.filename`, according to the input metadata and SaveImage transform.""" subject = ( diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index f4afca9483..09d2113829 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -22,8 +22,9 @@ import numpy as np +import monai from monai.config import DtypeLike, KeysCollection -from monai.data import FolderLayoutBase, image_writer +from monai.data import image_writer from monai.data.image_reader import ImageReader from monai.transforms.io.array import LoadImage, SaveImage from monai.transforms.transform import MapTransform, Transform @@ -275,7 +276,7 @@ def __init__( output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, output_name_formatter: Callable[[dict, Transform], dict] = None, - folder_layout: FolderLayoutBase = None, + folder_layout: monai.data.FolderLayoutBase = None, savepath_in_metadict: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) From 744bbd11ffad54867d18f767f09f73006e48b8c2 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 14:57:09 +0100 Subject: [PATCH 09/19] Revert "Add the "meta" prefix to MetaTensor __repr__ and __str__" This reverts commit 145fca097894b6a6caf36f01d0d41aebdfcc84af. --- monai/data/meta_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/data/meta_tensor.py b/monai/data/meta_tensor.py index 77b532aea5..3bbd243b4a 100644 --- a/monai/data/meta_tensor.py +++ b/monai/data/meta_tensor.py @@ -572,14 +572,14 @@ def __repr__(self): Prints a representation of the tensor identical to ``torch.Tensor.__repr__``. Use ``print_verbose`` for associated metadata. """ - return "meta" + self.as_tensor().__repr__() + return self.as_tensor().__repr__() def __str__(self): """ Prints a representation of the tensor identical to ``torch.Tensor.__str__``. Use ``print_verbose`` for associated metadata. """ - return "meta" + str(self.as_tensor()) + return str(self.as_tensor()) def print_verbose(self) -> None: """Verbose print with meta data.""" From 2e058f6aa5b7e8d455bb50656f8ad967a702f581 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 14:57:16 +0100 Subject: [PATCH 10/19] Revert "Fix MetaTensor tests" This reverts commit 9e1dcd474f0ba2a7435cd87c1fdbdec5242f5c3f. --- tests/test_meta_tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_meta_tensor.py b/tests/test_meta_tensor.py index a6607a3ccd..4f2cb9636a 100644 --- a/tests/test_meta_tensor.py +++ b/tests/test_meta_tensor.py @@ -430,8 +430,8 @@ def test_decollate(self, dtype): def test_str(self): t = MetaTensor([1.0], affine=torch.tensor(1), meta={"fname": "filename"}) - self.assertEqual(str(t), "metatensor([1.])") - self.assertEqual(t.__repr__(), "metatensor([1.])") + self.assertEqual(str(t), "tensor([1.])") + self.assertEqual(t.__repr__(), "tensor([1.])") def test_shape(self): s = MetaTensor([1]) From f51fdc573975013c731c95a13e5b40636680d5ec Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Tue, 21 Mar 2023 17:24:08 +0100 Subject: [PATCH 11/19] Pass MyPy checks Signed-off-by: Mathijs de Boer --- monai/transforms/io/array.py | 8 +++++--- monai/transforms/io/dictionary.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index edcdf7e4a3..4ec63f26f1 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -395,10 +395,11 @@ def __init__( output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, channel_dim: int | None = 0, - output_name_formatter: Callable[[dict, Transform], dict] = None, - folder_layout: FolderLayoutBase = None, + output_name_formatter: Callable[[dict, Transform], dict] | None = None, + folder_layout: FolderLayoutBase | None = None, savepath_in_metadict: bool = False, ) -> None: + self.folder_layout: FolderLayoutBase if folder_layout is None: self.folder_layout = FolderLayout( output_dir=output_dir, @@ -411,6 +412,7 @@ def __init__( else: self.folder_layout = folder_layout + self.fname_formatter: Callable if output_name_formatter is None: self.fname_formatter = default_name_formatter else: @@ -496,7 +498,7 @@ def __call__(self, img: torch.Tensor | np.ndarray, meta_data: dict | None = None ) else: self._data_index += 1 - if self.savepath_in_metadict: + if self.savepath_in_metadict and meta_data is not None: meta_data["saved_to"] = filename return img msg = "\n".join([f"{e}" for e in err]) diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 09d2113829..01b1a87d4e 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -275,8 +275,8 @@ def __init__( print_log: bool = True, output_format: str = "", writer: type[image_writer.ImageWriter] | str | None = None, - output_name_formatter: Callable[[dict, Transform], dict] = None, - folder_layout: monai.data.FolderLayoutBase = None, + output_name_formatter: Callable[[dict, Transform], dict] | None = None, + folder_layout: monai.data.FolderLayoutBase | None = None, savepath_in_metadict: bool = False, ) -> None: super().__init__(keys, allow_missing_keys) From 32701a4c795f753ec77496d65e5d81a205f170e0 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Wed, 22 Mar 2023 21:54:24 +0100 Subject: [PATCH 12/19] Small documentation update, to signify effects of including folder_layout Signed-off-by: Mathijs de Boer --- monai/transforms/io/array.py | 5 +++++ monai/transforms/io/dictionary.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 4ec63f26f1..3f22322ea6 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -316,8 +316,11 @@ class SaveImage(Transform): Args: output_dir: output image directory. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_postfix: a string appended to all output file names, default to `trans`. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_ext: output file extension name. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_dtype: data type (if not None) for saving data. Defaults to ``np.float32``. resample: whether to resample image (if needed) before saving the data array, based on the ``"spatial_shape"`` (and ``"original_affine"``) from metadata. @@ -353,10 +356,12 @@ class SaveImage(Transform): The output will be: ``/output/test1/image/image_seg.nii.gz`` + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. separate_folder: whether to save every file in a separate folder. For example: for the input filename ``image.nii``, postfix ``seg`` and ``folder_path`` ``output``, if ``separate_folder=True``, it will be saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string of filename extension to specify the output image writer. see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 01b1a87d4e..a6ca38fe29 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -197,8 +197,11 @@ class SaveImaged(MapTransform): If ``None``, will try to construct ``meta_keys`` by ``key_{meta_key_postfix}``. meta_key_postfix: if ``meta_keys`` is ``None``, use ``key_{meta_key_postfix}`` to retrieve the metadict. output_dir: output image directory. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_postfix: a string appended to all output file names, default to ``trans``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_ext: output file extension name, available extensions: ``.nii.gz``, ``.nii``, ``.png``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. resample: whether to resample image (if needed) before saving the data array, based on the ``spatial_shape`` (and ``original_affine``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. @@ -235,9 +238,11 @@ class SaveImaged(MapTransform): The output will be: ``/output/test1/image/image_seg.nii.gz`` + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. separate_folder: whether to save every file in a separate folder. For example: for the input filename ``image.nii``, postfix ``seg`` and folder_path ``output``, if ``separate_folder=True``, it will be saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string to specify the output image writer. see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. @@ -247,6 +252,7 @@ class SaveImaged(MapTransform): the supported built-in writer classes are ``"NibabelWriter"``, ``"ITKWriter"``, ``"PILWriter"``. output_name_formatter: a callable function (returning a kwargs dict) to format the output file name. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. + If using a custom ``folder_layout``, consider providing your own formatter. folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. if ``None``, uses the default ``FolderLayout`` savepath_in_metadict: if ``True``, adds a key ``saved_to`` to the metadata, which contains the path From 6ad0984071d1d37b5a2e1c20f6f6e02ab71d94e4 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Wed, 22 Mar 2023 21:54:57 +0100 Subject: [PATCH 13/19] Added unit tests for folder_layout and savepath_in_metadict Signed-off-by: Mathijs de Boer --- tests/test_save_imaged.py | 82 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index 676eb74678..bc7bbeb34d 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -14,25 +14,40 @@ import os import tempfile import unittest +from pathlib import Path import torch from parameterized import parameterized +from monai.config import PathLike from monai.data.meta_tensor import MetaTensor +from monai.data.folder_layout import FolderLayoutBase from monai.transforms import SaveImaged from monai.utils import optional_import _, has_itk = optional_import("itk", allow_namespace_pkg=True) TEST_CASE_1 = [ - {"img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nii.gz"})}, + { + "img": MetaTensor( + torch.randint(0, 255, (1, 2, 3, 4)), + meta={ + "filename_or_obj": "testfile0.nii.gz" + } + ) + }, ".nii.gz", False, ] TEST_CASE_2 = [ { - "img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nii.gz"}), + "img": MetaTensor( + torch.randint(0, 255, (1, 2, 3, 4)), + meta={ + "filename_or_obj": "testfile0.nii.gz" + } + ), "patch_index": 6, }, ".nii.gz", @@ -41,7 +56,12 @@ TEST_CASE_3 = [ { - "img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nrrd"}), + "img": MetaTensor( + torch.randint(0, 255, (1, 2, 3, 4)), + meta={ + "filename_or_obj": "testfile0.nrrd" + } + ), "patch_index": 6, }, ".nrrd", @@ -68,6 +88,62 @@ def test_saved_content(self, test_data, output_ext, resample): filepath = os.path.join("testfile0", "testfile0" + "_trans" + patch_index + output_ext) self.assertTrue(os.path.exists(os.path.join(tempdir, filepath))) + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + def test_custom_folderlayout(self, test_data, output_ext, resample): + class TestFolderLayout(FolderLayoutBase): + def __init__(self, basepath: Path, extension: str, makedirs: bool): + self.basepath = basepath + self.ext = extension + self.makedirs = makedirs + + def filename( + self, + subdirectory: str, + filename: str, + ) -> PathLike: + p = self.basepath / subdirectory + if not p.exists() and self.makedirs: + p.mkdir() + + return p / (filename + self.ext) + + def name_formatter(metadict: dict, _) -> dict: + # "[filename].[ext]" + # quick and dirty split on . + base_filename = metadict["filename_or_obj"].split(".")[0] + + return { + "subdirectory": base_filename, + "filename": "image", + } + + with tempfile.TemporaryDirectory() as tempdir: + trans = SaveImaged( + keys=["img", "pred"], + resample=resample, + allow_missing_keys=True, + output_name_formatter=name_formatter, + folder_layout=TestFolderLayout(basepath=Path(tempdir), extension=output_ext, makedirs=True), + ) + trans(test_data) + + filepath = os.path.join("testfile0", "image" + output_ext) + self.assertTrue(os.path.exists(os.path.join(tempdir, filepath))) + + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + def test_includes_metadata(self, test_data, output_ext, resample): + with tempfile.TemporaryDirectory() as tempdir: + trans = SaveImaged( + keys=["img", "pred"], + resample=resample, + allow_missing_keys=True, + savepath_in_metadict=True + ) + trans(test_data) + + self.assertTrue("saved_to" in test_data["img"].meta.keys()) + self.assertTrue(os.path.exists(test_data["img"].meta["saved_to"])) + if __name__ == "__main__": unittest.main() From 0907cdf06a5d604b68d437a0a28424da2c643018 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer <8137653+MathijsdeBoer@users.noreply.github.com> Date: Thu, 23 Mar 2023 11:51:00 +0100 Subject: [PATCH 14/19] Documentation formatting by KumoLiu Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com> Signed-off-by: Mathijs de Boer <8137653+MathijsdeBoer@users.noreply.github.com> --- monai/transforms/io/array.py | 10 +++++----- monai/transforms/io/dictionary.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 3f22322ea6..dbc954a9de 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -316,11 +316,11 @@ class SaveImage(Transform): Args: output_dir: output image directory. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_postfix: a string appended to all output file names, default to `trans`. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_ext: output file extension name. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_dtype: data type (if not None) for saving data. Defaults to ``np.float32``. resample: whether to resample image (if needed) before saving the data array, based on the ``"spatial_shape"`` (and ``"original_affine"``) from metadata. @@ -358,7 +358,7 @@ class SaveImage(Transform): Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. separate_folder: whether to save every file in a separate folder. For example: for the input filename - ``image.nii``, postfix ``seg`` and ``folder_path`` ``output``, if ``separate_folder=True``, it will be + ``image.nii``, postfix ``seg`` and folder_path ``output``, if ``separate_folder=True``, it will be saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. @@ -376,7 +376,7 @@ class SaveImage(Transform): your own formatter. see also: :py:func:`monai.data.folder_layout.default_name_formatter`. folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. - if ``None``, uses the default ``FolderLayout`` + if ``None``, uses the default ``FolderLayout``. savepath_in_metadict: if ``True``, adds a key ``"saved_to"`` to the metadata, which contains the path to where the input image has been saved. """ diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index a6ca38fe29..f22a11f627 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -197,11 +197,11 @@ class SaveImaged(MapTransform): If ``None``, will try to construct ``meta_keys`` by ``key_{meta_key_postfix}``. meta_key_postfix: if ``meta_keys`` is ``None``, use ``key_{meta_key_postfix}`` to retrieve the metadict. output_dir: output image directory. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_postfix: a string appended to all output file names, default to ``trans``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. output_ext: output file extension name, available extensions: ``.nii.gz``, ``.nii``, ``.png``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. resample: whether to resample image (if needed) before saving the data array, based on the ``spatial_shape`` (and ``original_affine``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. @@ -254,7 +254,7 @@ class SaveImaged(MapTransform): see also: :py:func:`monai.data.folder_layout.default_name_formatter`. If using a custom ``folder_layout``, consider providing your own formatter. folder_layout: A customized ``monai.data.FolderLayoutBase`` subclass to define file naming schemes. - if ``None``, uses the default ``FolderLayout`` + if ``None``, uses the default ``FolderLayout``. savepath_in_metadict: if ``True``, adds a key ``saved_to`` to the metadata, which contains the path to where the input image has been saved. """ From 6dcc322b6fb2e1812822d989e126ac71e1e14134 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Thu, 23 Mar 2023 11:55:28 +0100 Subject: [PATCH 15/19] Documentation language fix Formatting fix in test_save_imaged.py Signed-off-by: Mathijs de Boer --- monai/transforms/io/array.py | 10 ++++---- monai/transforms/io/dictionary.py | 10 ++++---- tests/test_save_imaged.py | 41 ++++++------------------------- 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 3f22322ea6..addc2832ba 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -316,11 +316,11 @@ class SaveImage(Transform): Args: output_dir: output image directory. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. output_postfix: a string appended to all output file names, default to `trans`. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. output_ext: output file extension name. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. output_dtype: data type (if not None) for saving data. Defaults to ``np.float32``. resample: whether to resample image (if needed) before saving the data array, based on the ``"spatial_shape"`` (and ``"original_affine"``) from metadata. @@ -356,12 +356,12 @@ class SaveImage(Transform): The output will be: ``/output/test1/image/image_seg.nii.gz`` - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. separate_folder: whether to save every file in a separate folder. For example: for the input filename ``image.nii``, postfix ``seg`` and ``folder_path`` ``output``, if ``separate_folder=True``, it will be saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string of filename extension to specify the output image writer. see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index a6ca38fe29..78643b7f12 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -197,11 +197,11 @@ class SaveImaged(MapTransform): If ``None``, will try to construct ``meta_keys`` by ``key_{meta_key_postfix}``. meta_key_postfix: if ``meta_keys`` is ``None``, use ``key_{meta_key_postfix}`` to retrieve the metadict. output_dir: output image directory. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. output_postfix: a string appended to all output file names, default to ``trans``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. output_ext: output file extension name, available extensions: ``.nii.gz``, ``.nii``, ``.png``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. resample: whether to resample image (if needed) before saving the data array, based on the ``spatial_shape`` (and ``original_affine``) from metadata. mode: This option is used when ``resample=True``. Defaults to ``"nearest"``. @@ -238,11 +238,11 @@ class SaveImaged(MapTransform): The output will be: ``/output/test1/image/image_seg.nii.gz`` - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. separate_folder: whether to save every file in a separate folder. For example: for the input filename ``image.nii``, postfix ``seg`` and folder_path ``output``, if ``separate_folder=True``, it will be saved as: ``output/image/image_seg.nii``, if ``False``, saving as ``output/image_seg.nii``. Default to ``True``. - Handled by ``folder_layout`` instead, if ``folder_layout`` not ``None``. + Handled by ``folder_layout`` instead, if ``folder_layout`` is not ``None``. print_log: whether to print logs when saving. Default to ``True``. output_format: an optional string to specify the output image writer. see also: ``monai.data.image_writer.SUPPORTED_WRITERS``. diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index bc7bbeb34d..0a5087f9e9 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -20,34 +20,22 @@ from parameterized import parameterized from monai.config import PathLike -from monai.data.meta_tensor import MetaTensor from monai.data.folder_layout import FolderLayoutBase +from monai.data.meta_tensor import MetaTensor from monai.transforms import SaveImaged from monai.utils import optional_import _, has_itk = optional_import("itk", allow_namespace_pkg=True) TEST_CASE_1 = [ - { - "img": MetaTensor( - torch.randint(0, 255, (1, 2, 3, 4)), - meta={ - "filename_or_obj": "testfile0.nii.gz" - } - ) - }, + {"img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nii.gz"})}, ".nii.gz", False, ] TEST_CASE_2 = [ { - "img": MetaTensor( - torch.randint(0, 255, (1, 2, 3, 4)), - meta={ - "filename_or_obj": "testfile0.nii.gz" - } - ), + "img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nii.gz"}), "patch_index": 6, }, ".nii.gz", @@ -56,12 +44,7 @@ TEST_CASE_3 = [ { - "img": MetaTensor( - torch.randint(0, 255, (1, 2, 3, 4)), - meta={ - "filename_or_obj": "testfile0.nrrd" - } - ), + "img": MetaTensor(torch.randint(0, 255, (1, 2, 3, 4)), meta={"filename_or_obj": "testfile0.nrrd"}), "patch_index": 6, }, ".nrrd", @@ -96,11 +79,7 @@ def __init__(self, basepath: Path, extension: str, makedirs: bool): self.ext = extension self.makedirs = makedirs - def filename( - self, - subdirectory: str, - filename: str, - ) -> PathLike: + def filename(self, subdirectory: str, filename: str) -> PathLike: p = self.basepath / subdirectory if not p.exists() and self.makedirs: p.mkdir() @@ -112,10 +91,7 @@ def name_formatter(metadict: dict, _) -> dict: # quick and dirty split on . base_filename = metadict["filename_or_obj"].split(".")[0] - return { - "subdirectory": base_filename, - "filename": "image", - } + return {"subdirectory": base_filename, "filename": "image"} with tempfile.TemporaryDirectory() as tempdir: trans = SaveImaged( @@ -134,10 +110,7 @@ def name_formatter(metadict: dict, _) -> dict: def test_includes_metadata(self, test_data, output_ext, resample): with tempfile.TemporaryDirectory() as tempdir: trans = SaveImaged( - keys=["img", "pred"], - resample=resample, - allow_missing_keys=True, - savepath_in_metadict=True + keys=["img", "pred"], resample=resample, allow_missing_keys=True, savepath_in_metadict=True ) trans(test_data) From 9f9a7d20abb82c96d509c6e273f12753e5bfc67c Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Thu, 23 Mar 2023 16:33:15 +0100 Subject: [PATCH 16/19] Use unused variables Signed-off-by: Mathijs de Boer --- tests/test_save_imaged.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index 0a5087f9e9..cec51aaa69 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -110,7 +110,11 @@ def name_formatter(metadict: dict, _) -> dict: def test_includes_metadata(self, test_data, output_ext, resample): with tempfile.TemporaryDirectory() as tempdir: trans = SaveImaged( - keys=["img", "pred"], resample=resample, allow_missing_keys=True, savepath_in_metadict=True + keys=["img", "pred"], + output_dir=tempdir, + output_ext=output_ext, + resample=resample, + allow_missing_keys=True, ) trans(test_data) From 0d1a99cae69f91e71c03b538d121f294aa8c82db Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Thu, 23 Mar 2023 16:33:15 +0100 Subject: [PATCH 17/19] Use unused variables Signed-off-by: Mathijs de Boer --- tests/test_save_imaged.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index 0a5087f9e9..e133388d93 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -110,7 +110,12 @@ def name_formatter(metadict: dict, _) -> dict: def test_includes_metadata(self, test_data, output_ext, resample): with tempfile.TemporaryDirectory() as tempdir: trans = SaveImaged( - keys=["img", "pred"], resample=resample, allow_missing_keys=True, savepath_in_metadict=True + keys=["img", "pred"], + output_dir=tempdir, + output_ext=output_ext, + resample=resample, + allow_missing_keys=True, + savepath_in_metadict=True ) trans(test_data) From 6cb03442a42cdfd9482b6dde3635c019cdbc1fd8 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Thu, 23 Mar 2023 21:25:38 +0100 Subject: [PATCH 18/19] Run autoformat. For missing one comma. Signed-off-by: Mathijs de Boer --- tests/test_save_imaged.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index e133388d93..f2e45dea2c 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -115,7 +115,7 @@ def test_includes_metadata(self, test_data, output_ext, resample): output_ext=output_ext, resample=resample, allow_missing_keys=True, - savepath_in_metadict=True + savepath_in_metadict=True, ) trans(test_data) From 047a62a1d3864b6dc89dab58fd6092836342e805 Mon Sep 17 00:00:00 2001 From: Mathijs de Boer Date: Fri, 24 Mar 2023 00:51:37 +0100 Subject: [PATCH 19/19] Fix mypy error Signed-off-by: Mathijs de Boer --- tests/test_save_imaged.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_save_imaged.py b/tests/test_save_imaged.py index f2e45dea2c..ab0b9c0d9f 100644 --- a/tests/test_save_imaged.py +++ b/tests/test_save_imaged.py @@ -79,12 +79,12 @@ def __init__(self, basepath: Path, extension: str, makedirs: bool): self.ext = extension self.makedirs = makedirs - def filename(self, subdirectory: str, filename: str) -> PathLike: - p = self.basepath / subdirectory + def filename(self, **kwargs) -> PathLike: + p = self.basepath / str(kwargs["subdirectory"]) if not p.exists() and self.makedirs: p.mkdir() - return p / (filename + self.ext) + return p / (str(kwargs["filename"]) + self.ext) def name_formatter(metadict: dict, _) -> dict: # "[filename].[ext]"