Skip to content

Commit 127e823

Browse files
authored
2620 3595 Writer backend selector, deprecating nifti_saver/writer, png_saver/writer (Project-MONAI#3773)
* update saveimage and writer selector Signed-off-by: Wenqi Li <[email protected]> * more tests Signed-off-by: Wenqi Li <[email protected]> * more tests Signed-off-by: Wenqi Li <[email protected]> * adds saving loading tests Signed-off-by: Wenqi Li <[email protected]> * fixes Project-MONAI#3783 Signed-off-by: Wenqi Li <[email protected]> * enhance import checks Signed-off-by: Wenqi Li <[email protected]> * warn to exception; int check Signed-off-by: Wenqi Li <[email protected]> * fixes tests Signed-off-by: Wenqi Li <[email protected]> * update based on comments Signed-off-by: Wenqi Li <[email protected]> * fixes Project-MONAI#3787 Signed-off-by: Wenqi Li <[email protected]> * unit testing Signed-off-by: Wenqi Li <[email protected]>
1 parent 0be3341 commit 127e823

20 files changed

+517
-179
lines changed

docs/source/data.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ WSIReader
153153
Image writer
154154
------------
155155

156+
resolve_writer
157+
~~~~~~~~~~~~~~
158+
.. autofunction:: resolve_writer
159+
160+
register_writer
161+
~~~~~~~~~~~~~~~
162+
.. autofunction:: register_writer
163+
156164
ImageWriter
157165
~~~~~~~~~~~
158166
.. autoclass:: ImageWriter

monai/data/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@
3535
from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter
3636
from .image_dataset import ImageDataset
3737
from .image_reader import ImageReader, ITKReader, NibabelReader, NumpyReader, PILReader, WSIReader
38-
from .image_writer import ImageWriter, ITKWriter, NibabelWriter, PILWriter, logger
38+
from .image_writer import (
39+
SUPPORTED_WRITERS,
40+
ImageWriter,
41+
ITKWriter,
42+
NibabelWriter,
43+
PILWriter,
44+
logger,
45+
register_writer,
46+
resolve_writer,
47+
)
3948
from .iterable_dataset import CSVIterableDataset, IterableDataset, ShuffleBuffer
4049
from .nifti_saver import NiftiSaver
4150
from .nifti_writer import write_nifti

monai/data/folder_layout.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class FolderLayout:
2929
layout = FolderLayout(
3030
output_dir="/test_run_1/",
3131
postfix="seg",
32-
extension=".nii",
32+
extension="nii",
3333
makedirs=False)
3434
layout.filename(subject="Sub-A", idx="00", modality="T1")
3535
# return value: "/test_run_1/Sub-A_seg_00_modality-T1.nii"
@@ -95,5 +95,6 @@ def filename(self, subject: PathLike = "subject", idx=None, **kwargs):
9595
for k, v in kwargs.items():
9696
full_name += f"_{k}-{v}"
9797
if self.ext is not None:
98-
full_name += f"{self.ext}"
98+
ext = f"{self.ext}"
99+
full_name += f".{ext}" if ext and not ext.startswith(".") else f"{ext}"
99100
return full_name

monai/data/image_reader.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@
1818
from torch.utils.data._utils.collate import np_str_obj_array_pattern
1919

2020
from monai.config import DtypeLike, KeysCollection, PathLike
21-
from monai.data.utils import correct_nifti_header_if_necessary
21+
from monai.data.utils import correct_nifti_header_if_necessary, is_supported_format
2222
from monai.transforms.utility.array import EnsureChannelFirst
2323
from monai.utils import ensure_tuple, ensure_tuple_rep, optional_import, require_pkg
2424

25-
from .utils import is_supported_format
26-
2725
if TYPE_CHECKING:
2826
import itk
2927
import nibabel as nib

monai/data/image_writer.py

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
from typing import TYPE_CHECKING, Mapping, Optional, Sequence, Union
12+
from typing import TYPE_CHECKING, Dict, Mapping, Optional, Sequence, Union
1313

1414
import numpy as np
1515

@@ -22,13 +22,15 @@
2222
GridSampleMode,
2323
GridSamplePadMode,
2424
InterpolateMode,
25+
OptionalImportError,
2526
convert_data_type,
2627
look_up_option,
2728
optional_import,
2829
require_pkg,
2930
)
3031

3132
DEFAULT_FMT = "%(asctime)s %(levelname)s %(filename)s:%(lineno)d - %(message)s"
33+
EXT_WILDCARD = "*"
3234
logger = get_logger(module_name=__name__, fmt=DEFAULT_FMT)
3335

3436
if TYPE_CHECKING:
@@ -41,7 +43,76 @@
4143
PILImage, _ = optional_import("PIL.Image")
4244

4345

44-
__all__ = ["ImageWriter", "ITKWriter", "NibabelWriter", "PILWriter", "logger"]
46+
__all__ = [
47+
"ImageWriter",
48+
"ITKWriter",
49+
"NibabelWriter",
50+
"PILWriter",
51+
"SUPPORTED_WRITERS",
52+
"register_writer",
53+
"resolve_writer",
54+
"logger",
55+
]
56+
57+
SUPPORTED_WRITERS: Dict = {}
58+
59+
60+
def register_writer(ext_name, *im_writers):
61+
"""
62+
Register ``ImageWriter``, so that writing a file with filename extension ``ext_name``
63+
could be resolved to a tuple of potentially appropriate ``ImageWriter``.
64+
The customised writers could be registered by:
65+
66+
.. code-block:: python
67+
68+
from monai.data import register_writer
69+
# `MyWriter` must implement `ImageWriter` interface
70+
register_writer("nii", MyWriter)
71+
72+
Args:
73+
ext_name: the filename extension of the image.
74+
As an indexing key, it will be converted to a lower case string.
75+
im_writers: one or multiple ImageWriter classes with high priority ones first.
76+
"""
77+
fmt = f"{ext_name}".lower()
78+
if fmt.startswith("."):
79+
fmt = fmt[1:]
80+
existing = look_up_option(fmt, SUPPORTED_WRITERS, default=())
81+
all_writers = im_writers + existing
82+
SUPPORTED_WRITERS[fmt] = all_writers
83+
84+
85+
def resolve_writer(ext_name, error_if_not_found=True) -> Sequence:
86+
"""
87+
Resolves to a tuple of available ``ImageWriter`` in ``SUPPORTED_WRITERS``
88+
according to the filename extension key ``ext_name``.
89+
90+
Args:
91+
ext_name: the filename extension of the image.
92+
As an indexing key it will be converted to a lower case string.
93+
error_if_not_found: whether to raise an error if no suitable image writer is found.
94+
if True , raise an ``OptionalImportError``, otherwise return an empty tuple. Default is ``True``.
95+
"""
96+
if not SUPPORTED_WRITERS:
97+
init()
98+
fmt = f"{ext_name}".lower()
99+
if fmt.startswith("."):
100+
fmt = fmt[1:]
101+
avail_writers = []
102+
default_writers = SUPPORTED_WRITERS.get(EXT_WILDCARD, ())
103+
for _writer in look_up_option(fmt, SUPPORTED_WRITERS, default=default_writers):
104+
try:
105+
_writer() # this triggers `monai.utils.module.require_pkg` to check the system availability
106+
avail_writers.append(_writer)
107+
except OptionalImportError:
108+
continue
109+
except Exception: # other writer init errors indicating it exists
110+
avail_writers.append(_writer)
111+
if not avail_writers and error_if_not_found:
112+
raise OptionalImportError(f"No ImageWriter backend found for {fmt}.")
113+
writer_tuple = ensure_tuple(avail_writers)
114+
SUPPORTED_WRITERS[fmt] = writer_tuple
115+
return writer_tuple
45116

46117

47118
class ImageWriter:
@@ -297,7 +368,9 @@ def __init__(self, output_dtype: DtypeLike = np.float32, **kwargs):
297368
"""
298369
super().__init__(output_dtype=output_dtype, affine=None, channel_dim=0, **kwargs)
299370

300-
def set_data_array(self, data_array, channel_dim: Optional[int] = 0, squeeze_end_dims: bool = True, **kwargs):
371+
def set_data_array(
372+
self, data_array: NdarrayOrTensor, channel_dim: Optional[int] = 0, squeeze_end_dims: bool = True, **kwargs
373+
):
301374
"""
302375
Convert ``data_array`` into 'channel-last' numpy ndarray.
303376
@@ -309,14 +382,15 @@ def set_data_array(self, data_array, channel_dim: Optional[int] = 0, squeeze_end
309382
kwargs: keyword arguments passed to ``self.convert_to_channel_last``,
310383
currently support ``spatial_ndim`` and ``contiguous``, defauting to ``3`` and ``False`` respectively.
311384
"""
385+
_r = len(data_array.shape)
312386
self.data_obj = self.convert_to_channel_last(
313387
data=data_array,
314388
channel_dim=channel_dim,
315389
squeeze_end_dims=squeeze_end_dims,
316390
spatial_ndim=kwargs.pop("spatial_ndim", 3),
317391
contiguous=kwargs.pop("contiguous", True),
318392
)
319-
self.channel_dim = channel_dim
393+
self.channel_dim = channel_dim if len(self.data_obj.shape) >= _r else None # channel dim is at the end
320394

321395
def set_metadata(self, meta_dict: Optional[Mapping] = None, resample: bool = True, **options):
322396
"""
@@ -335,7 +409,7 @@ def set_metadata(self, meta_dict: Optional[Mapping] = None, resample: bool = Tru
335409
data_array=self.data_obj,
336410
affine=affine,
337411
target_affine=original_affine if resample else None,
338-
output_spatial_shape=spatial_shape,
412+
output_spatial_shape=spatial_shape if resample else None,
339413
mode=options.pop("mode", GridSampleMode.BILINEAR),
340414
padding_mode=options.pop("padding_mode", GridSamplePadMode.BORDER),
341415
align_corners=options.pop("align_corners", False),
@@ -476,7 +550,7 @@ def set_metadata(self, meta_dict: Optional[Mapping], resample: bool = True, **op
476550
data_array=self.data_obj,
477551
affine=affine,
478552
target_affine=original_affine if resample else None,
479-
output_spatial_shape=spatial_shape,
553+
output_spatial_shape=spatial_shape if resample else None,
480554
mode=options.pop("mode", GridSampleMode.BILINEAR),
481555
padding_mode=options.pop("padding_mode", GridSamplePadMode.BORDER),
482556
align_corners=options.pop("align_corners", False),
@@ -716,3 +790,15 @@ def create_backend_obj(
716790
data = np.moveaxis(data, 0, 1)
717791

718792
return PILImage.fromarray(data, mode=kwargs.pop("image_mode", None))
793+
794+
795+
def init():
796+
"""
797+
Initialize the image writer modules according to the filename extension.
798+
"""
799+
for ext in ("png", "jpg", "jpeg", "bmp", "tiff", "tif"):
800+
register_writer(ext, PILWriter) # TODO: test 16-bit
801+
for ext in ("nii.gz", "nii"):
802+
register_writer(ext, NibabelWriter, ITKWriter)
803+
register_writer("nrrd", ITKWriter, NibabelWriter)
804+
register_writer(EXT_WILDCARD, ITKWriter, NibabelWriter, ITKWriter)

monai/data/nifti_saver.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
from monai.data.utils import create_file_basename
2020
from monai.utils import GridSampleMode, GridSamplePadMode
2121
from monai.utils import ImageMetaKey as Key
22+
from monai.utils import deprecated
2223

2324

25+
@deprecated(since="0.8", msg_suffix="use monai.transforms.SaveImage instead.")
2426
class NiftiSaver:
2527
"""
2628
Save the data as NIfTI file, it can support single data content or a batch of data.
@@ -32,6 +34,9 @@ class NiftiSaver:
3234
3335
Note: image should include channel dimension: [B],C,H,W,[D].
3436
37+
.. deprecated:: 0.8
38+
Use :py:class:`monai.transforms.SaveImage` instead.
39+
3540
"""
3641

3742
def __init__(

monai/data/nifti_writer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
from monai.data.utils import compute_shape_offset, to_affine_nd
2020
from monai.networks.layers import AffineTransform
2121
from monai.transforms.utils_pytorch_numpy_unification import allclose
22-
from monai.utils import GridSampleMode, GridSamplePadMode, optional_import
22+
from monai.utils import GridSampleMode, GridSamplePadMode, deprecated, optional_import
2323
from monai.utils.type_conversion import convert_data_type
2424

2525
nib, _ = optional_import("nibabel")
2626

2727

28+
@deprecated(since="0.8", msg_suffix="use monai.data.NibabelWriter instead.")
2829
def write_nifti(
2930
data: NdarrayOrTensor,
3031
file_name: str,
@@ -98,6 +99,10 @@ def write_nifti(
9899
dtype: data type for resampling computation. Defaults to ``np.float64`` for best precision.
99100
If None, use the data type of input data.
100101
output_dtype: data type for saving data. Defaults to ``np.float32``.
102+
103+
.. deprecated:: 0.8
104+
Use :py:meth:`monai.data.NibabelWriter` instead.
105+
101106
"""
102107
data, *_ = convert_data_type(data, np.ndarray)
103108
affine, *_ = convert_data_type(affine, np.ndarray)

monai/data/png_saver.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
from monai.data.png_writer import write_png
1919
from monai.data.utils import create_file_basename
2020
from monai.utils import ImageMetaKey as Key
21-
from monai.utils import InterpolateMode, look_up_option
21+
from monai.utils import InterpolateMode, deprecated, look_up_option
2222

2323

24+
@deprecated(since="0.8", msg_suffix="use monai.transforms.SaveImage instead.")
2425
class PNGSaver:
2526
"""
2627
Save the data as png file, it can support single data content or a batch of data.
@@ -30,6 +31,9 @@ class PNGSaver:
3031
where the input image name is extracted from the provided meta data dictionary.
3132
If no meta data provided, use index from 0 as the filename prefix.
3233
34+
.. deprecated:: 0.8
35+
Use :py:class:`monai.transforms.SaveImage` instead.
36+
3337
"""
3438

3539
def __init__(

monai/data/png_writer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
import numpy as np
1515

1616
from monai.transforms.spatial.array import Resize
17-
from monai.utils import InterpolateMode, ensure_tuple_rep, look_up_option, optional_import
17+
from monai.utils import InterpolateMode, deprecated, ensure_tuple_rep, look_up_option, optional_import
1818

1919
Image, _ = optional_import("PIL", name="Image")
2020

2121

22+
@deprecated(since="0.8", msg_suffix="use monai.data.PILWriter instead.")
2223
def write_png(
2324
data: np.ndarray,
2425
file_name: str,
@@ -46,6 +47,9 @@ def write_png(
4647
Raises:
4748
ValueError: When ``scale`` is not one of [255, 65535].
4849
50+
.. deprecated:: 0.8
51+
Use :py:meth:`monai.data.PILWriter` instead.
52+
4953
"""
5054
if not isinstance(data, np.ndarray):
5155
raise ValueError("input data must be numpy array.")

0 commit comments

Comments
 (0)