Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
from monai.transforms.utility.array import EnsureChannelFirst
from monai.utils import (
GridSamplePadMode,
ImageMetaKey,
)
from monai.utils import ImageMetaKey
from monai.utils import (
MetaKeys,
OptionalImportError,
convert_to_dst_type,
Expand Down Expand Up @@ -138,6 +140,7 @@ def __init__(
prune_meta_pattern: str | None = None,
prune_meta_sep: str = ".",
expanduser: bool = True,
raise_on_missing_reader: bool = False,
*args,
**kwargs,
) -> None:
Expand All @@ -161,9 +164,21 @@ def __init__(
in the metadata (nested dictionary). default is ".", see also :py:class:`monai.transforms.DeleteItemsd`.
e.g. ``prune_meta_pattern=".*_code$", prune_meta_sep=" "`` removes meta keys that ends with ``"_code"``.
expanduser: if True cast filename to Path and call .expanduser on it, otherwise keep filename as is.
raise_on_missing_reader: if True, raise `OptionalImportError` when a specified reader is not available;
otherwise attempt to use fallback readers. Defaults to False (backward compatibility).
args: additional parameters for reader if providing a reader name.
kwargs: additional parameters for reader if providing a reader name.

Raises:
OptionalImportError: If `raise_on_missing_reader=True` and the specified reader
cannot be found or its optional dependency is not installed.

Accepted reader types:
- str: name of a registered reader (e.g., `"ITKReader"`)
- class: e.g., `ITKReader` or a custom reader class
- instance: e.g., `ITKReader(pixel_type=itk.UC)`
- list/tuple: multiple reader names or classes to try in order

Note:

- The transform returns a MetaTensor, unless `set_track_meta(False)` has been used, in which case, a
Expand All @@ -183,6 +198,7 @@ def __init__(
self.pattern = prune_meta_pattern
self.sep = prune_meta_sep
self.expanduser = expanduser
self.raise_on_missing_reader = raise_on_missing_reader

self.readers: list[ImageReader] = []
for r in SUPPORTED_READERS: # set predefined readers as default
Expand All @@ -206,18 +222,61 @@ def __init__(
if not has_built_in:
the_reader = locate(f"{_r}") # search dotted path
if the_reader is None:
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
try:
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
except ValueError:
# If the reader name is not recognized at all, raise OptionalImportError
msg = f"Cannot find reader '{_r}'. It may not be installed or recognized."
if self.raise_on_missing_reader:
raise OptionalImportError(msg)
else:
warnings.warn(
f"{msg} Will use fallback readers if available.",
category=UserWarning,
stacklevel=2,
)
continue
try:
self.register(the_reader(*args, **kwargs))
except OptionalImportError:
warnings.warn(
f"required package for reader {_r} is not installed, or the version doesn't match requirement."
except OptionalImportError as e:
msg = (
f"Required package for reader {_r} is not installed, or the version doesn't match requirement."
)
if self.raise_on_missing_reader:
raise OptionalImportError(msg) from e
else:
warnings.warn(
f"{msg} Will use fallback readers if available.",
category=UserWarning,
stacklevel=2,
)
except TypeError: # the reader doesn't have the corresponding args/kwargs
warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.")
warnings.warn(
f"{_r} is not supported with the given parameters {args} {kwargs}.",
category=UserWarning,
stacklevel=2,
)
self.register(the_reader())
elif inspect.isclass(_r):
self.register(_r(*args, **kwargs))
try:
self.register(_r(*args, **kwargs))
except OptionalImportError as e:
msg = f"Required package for reader {_r.__name__} is not installed, or the version doesn't match requirement."
if self.raise_on_missing_reader:
raise OptionalImportError(msg) from e
else:
warnings.warn(
f"{msg} Will use fallback readers if available.",
category=UserWarning,
stacklevel=2,
)
except TypeError:
warnings.warn(
f"{_r.__name__} is not supported with the given parameters {args} {kwargs}.",
category=UserWarning,
stacklevel=2,
)
self.register(_r())
else:
self.register(_r) # reader instance, ignoring the constructor args/kwargs
return
Expand Down
32 changes: 31 additions & 1 deletion tests/transforms/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import shutil
import tempfile
import unittest
import warnings
from pathlib import Path

import nibabel as nib
Expand All @@ -28,7 +29,7 @@
from monai.data.meta_obj import set_track_meta
from monai.data.meta_tensor import MetaTensor
from monai.transforms import LoadImage
from monai.utils import optional_import
from monai.utils import OptionalImportError, optional_import
from tests.test_utils import SkipIfNoModule, assert_allclose, skip_if_downloading_fails, testing_data_config

itk, has_itk = optional_import("itk", allow_namespace_pkg=True)
Expand Down Expand Up @@ -436,12 +437,41 @@ def test_my_reader(self):
self.assertEqual(out.meta["name"], "my test")
out = LoadImage(image_only=True, reader=_MiniReader, is_compatible=False)("test")
self.assertEqual(out.meta["name"], "my test")
# Test runtime reader specification
for item in (_MiniReader, _MiniReader(is_compatible=False)):
out = LoadImage(image_only=True, reader=item)("test")
self.assertEqual(out.meta["name"], "my test")
out = LoadImage(image_only=True)("test", reader=_MiniReader(is_compatible=False))
self.assertEqual(out.meta["name"], "my test")

def test_reader_not_installed_exception(self):
"""test if an exception is raised when a specified reader is not installed"""
with self.assertRaises(OptionalImportError):
LoadImage(image_only=True, reader="NonExistentReader", raise_on_missing_reader=True)

def test_raise_on_missing_reader_flag(self):
"""test raise_on_missing_reader flag behavior"""
# Test with flag enabled - should raise exception for unknown reader name
with self.assertRaises(OptionalImportError):
LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=True)

# Test with flag disabled - should warn but not raise exception for unknown reader name
# This should succeed and create the loader with fallback behavior
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
loader_with_fallback = LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=False)
self.assertIsInstance(loader_with_fallback, LoadImage)
# Should have produced a warning about the unknown reader
self.assertTrue(any("Cannot find reader 'UnknownReaderName'" in str(warning.message) for warning in w))

# The flag should work properly with valid readers too
loader_with_flag = LoadImage(image_only=True, reader="ITKReader", raise_on_missing_reader=False)
loader_without_flag = LoadImage(image_only=True, reader="ITKReader")

# Both should work since ITK is available in this test environment
self.assertIsInstance(loader_with_flag, LoadImage)
self.assertIsInstance(loader_without_flag, LoadImage)

def test_itk_meta(self):
"""test metadata from a directory"""
out = LoadImage(image_only=True, reader="ITKReader", pixel_type=itk_uc, series_meta=True)(
Expand Down
Loading