diff --git a/monai/__init__.py b/monai/__init__.py index 2d6f5dfc37..db13712b35 100644 --- a/monai/__init__.py +++ b/monai/__init__.py @@ -86,6 +86,7 @@ def filter(self, record): # *.so files and folder "_C" may not exist when the cpp extensions are not compiled excludes = "|".join( [ + "(.*(nvimgcodec_pydicom_plugin)$)", "(^(monai.handlers))", "(^(monai.bundle))", "(^(monai.fl))", diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 0a853372f4..f6ed1858f4 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1159,6 +1159,14 @@ def __init__( to_gpu=False, **kwargs, ) + + self._nvimgcodec_available = False + self._setup_done = False + + def _ensure_setup(self) -> None: + """Import and register the nvImageCodec plugin on first use only.""" + if self._setup_done: + return from monai.data.nvimgcodec_pydicom_plugin import is_nvimgcodec_available, register_as_decoder_plugin self._nvimgcodec_available = is_nvimgcodec_available() @@ -1167,15 +1175,22 @@ def __init__( "NvImgCodecPydicomReader: nvImageCodec decoder plugin did not register successfully. " "Falling back to default pydicom decoders." ) + self._setup_done = True def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool: """ Verify whether the specified file or files are DICOM and nvImageCodec is available. """ + self._ensure_setup() if not has_pydicom or not self._nvimgcodec_available: return False return is_dicom_path(filename) + def read(self, data: Sequence[PathLike] | PathLike, **kwargs): + """Set up the nvImageCodec plugin on first use, then read as PydicomReader does.""" + self._ensure_setup() + return super().read(data, **kwargs) + @require_pkg(pkg_name="nibabel") class NibabelReader(ImageReader): diff --git a/monai/data/nvimgcodec_pydicom_plugin.py b/monai/data/nvimgcodec_pydicom_plugin.py index 573dc715d4..07ba5ca726 100644 --- a/monai/data/nvimgcodec_pydicom_plugin.py +++ b/monai/data/nvimgcodec_pydicom_plugin.py @@ -66,7 +66,7 @@ def is_nvimgcodec_available() -> bool: def register_as_decoder_plugin(module_path: str | None = None) -> bool: """Register the nvImageCodec pydicom decoder plugin.""" if not is_nvimgcodec_available(): - _logger.warning("nvImageCodec is not available; skipping pydicom decoder plugin registration.") + _logger.debug("nvImageCodec is not available; skipping pydicom decoder plugin registration.") return False if not has_pydicom_plugin: return False diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 4cd9ac42c4..39347c9c47 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -12,8 +12,11 @@ from __future__ import annotations import os +import subprocess +import sys import tempfile import unittest +from importlib.machinery import PathFinder import numpy as np @@ -65,6 +68,45 @@ def test_load_image(self): inst = LoadImaged("image", reader="NvImgCodecPydicomReader") self.assertIsInstance(inst, LoadImaged) + def test_nvimgcodec_not_imported_on_monai_import(self): + """Verify importing MONAI does not eagerly load the optional NVIDIA package.""" + nvidia_spec = PathFinder.find_spec("nvidia") + if nvidia_spec is None or nvidia_spec.submodule_search_locations is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + nvimgcodec_spec = PathFinder.find_spec("nvimgcodec", nvidia_spec.submodule_search_locations) + if nvimgcodec_spec is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + code = "import monai, sys; print('nvidia.nvimgcodec' in sys.modules)" + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "False") + self.assertEqual(result.stderr, "") + + def test_nvimgcodec_not_imported_on_loadimage_init(self): + """Verify initializing LoadImage does not eagerly load nvimgcodec.""" + nvidia_spec = PathFinder.find_spec("nvidia") + if nvidia_spec is None or nvidia_spec.submodule_search_locations is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + nvimgcodec_spec = PathFinder.find_spec("nvimgcodec", nvidia_spec.submodule_search_locations) + if nvimgcodec_spec is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + code = ( + "import monai, sys; " + "from monai.transforms import LoadImage; " + "LoadImage(); " + "print('nvidia.nvimgcodec' in sys.modules)" + ) + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "False") + self.assertEqual(result.stderr, "") + @SkipIfNoModule("nibabel") @SkipIfNoModule("cupy") @SkipIfNoModule("kvikio") diff --git a/tests/data/test_nvimgcodec_pydicom_reader.py b/tests/data/test_nvimgcodec_pydicom_reader.py index 46520dc320..8e6e41f919 100644 --- a/tests/data/test_nvimgcodec_pydicom_reader.py +++ b/tests/data/test_nvimgcodec_pydicom_reader.py @@ -100,11 +100,16 @@ class TestNvImgCodecPydicomReader(unittest.TestCase): @SkipIfNoModule("pydicom") @patch("monai.data.nvimgcodec_pydicom_plugin.register_as_decoder_plugin", return_value=True) @patch("monai.data.nvimgcodec_pydicom_plugin.is_nvimgcodec_available", return_value=True) - def test_reader_init_registers_plugin(self, _mock_available, mock_register): + def test_reader_registers_plugin_lazily(self, _mock_available, mock_register): from monai.data import NvImgCodecPydicomReader reader = NvImgCodecPydicomReader() self.assertIsInstance(reader, NvImgCodecPydicomReader) + + mock_register.assert_not_called() + + reader.verify_suffix("x.dcm") + mock_register.assert_called_once() @SkipIfNoModule("pydicom")