Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f7a384e
Add new DICOM reader using Pydicom and nvimgcode GPU accelerated deco…
MMelQin Jun 26, 2026
9c4d813
Clarify why GPU direct loading not compatible with accelerated decomp…
MMelQin Jun 26, 2026
7eb485e
Fix formatting complaints
MMelQin Jun 26, 2026
1aebb73
Fix mypy complaints
MMelQin Jun 27, 2026
77ac541
Fixed and tested doc build warnings and nits
MMelQin Jun 27, 2026
423574f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 27, 2026
6fbac5a
Fix test failures after this branch’s reader registration change,
MMelQin Jun 27, 2026
7fbef1b
Add explicit failure mode test.
MMelQin Jul 1, 2026
03ed82c
Merge branch 'dev' into mq/add_gpu_dcm_decoding
ericspod Jul 3, 2026
0235afe
Merge branch 'dev' into mq/add_gpu_dcm_decoding
MMelQin Jul 3, 2026
edca43f
Addressed review comments, e.g. intent of SUPPORTED_READERS and fallback
MMelQin Jul 26, 2026
ee9f5cd
Address comments on test's skipping conditions
MMelQin Jul 26, 2026
65c1161
Autofix of formatting errors
MMelQin Jul 26, 2026
7ffd22e
Added a test to compare decoded data between CPU and GPU decoding if …
MMelQin Jul 27, 2026
ed6401e
Merge remote-tracking branch 'origin/dev' into mq/add_gpu_dcm_decoding
Copilot Sep 9, 2026
c244fa4
Merge branch 'dev' into mq/add_gpu_dcm_decoding
ericspod Sep 9, 2026
8889bea
Addressed latest review comments and tested code post merge commits.
MMelQin Sep 10, 2026
9fe9721
Merge remote-tracking branch 'origin/dev' into mq/add_gpu_dcm_decoding
MMelQin Sep 10, 2026
f9c4af5
Merge branch 'dev' into mq/add_gpu_dcm_decoding
MMelQin Sep 11, 2026
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
15 changes: 15 additions & 0 deletions docs/source/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ PILReader
.. autoclass:: PILReader
:members:

PydicomReader
~~~~~~~~~~~~~
.. autoclass:: PydicomReader
:members:

NvImgCodecPydicomReader
~~~~~~~~~~~~~~~~~~~~~~~
GPU-accelerated DICOM reader built on :py:class:`PydicomReader` and the nvImageCodec pydicom decoder plugin.
The ``to_gpu`` init argument is accepted for API compatibility but is always ignored so that GPU decompression
is not bypassed by GPU direct loading. The optional dependency ``nvimgcodec`` needs to be installed for this
feature (``pip install 'monai[nvimgcodec]'``).

.. autoclass:: NvImgCodecPydicomReader
:members:

NrrdReader
~~~~~~~~~~
.. autoclass:: NrrdReader
Expand Down
12 changes: 8 additions & 4 deletions docs/source/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,17 @@ pip install monai
MONAI supports the extras syntax such as `pip install 'monai[nibabel]'`. The options are

```text
clearml, cucim, cupy, einops, fire, gdown, h5py, huggingface_hub, hyena, ignite, imagecodecs, itk, jsonschema, lmdb, lpips, matplotlib, metrics_reloaded, mlflow, nibabel, nni, onnx, openslide, optuna, pandas, pillow, polygraphy, psutil, pyamg, pybind11, pydicom, pynrrd, pynvml, pyyaml, requests, segment_anything, scipy, skimage, tensorboard, tensorboardX, tifffile, torchio, torchvision, tqdm, transformers, zarr
clearml, cucim, cupy, einops, fire, gdown, h5py, huggingface_hub, hyena, ignite, imagecodecs, itk, jsonschema, lmdb, lpips, matplotlib, metrics_reloaded, mlflow, nibabel, nni, nvimgcodec, onnx, openslide, optuna, pandas, pillow, polygraphy, psutil, pyamg, pybind11, pydicom, pynrrd, pynvml, pyyaml, requests, segment_anything, scipy, skimage, tensorboard, tensorboardX, tifffile, torchio, torchvision, tqdm, transformers, zarr
```

which correspond to the packages: `clearml`, `cucim` (`cucim-cu12` or `cucim-cu13`), `cupy-cuda13x`, `einops`, `fire`, `gdown`, `h5py`, `huggingface_hub`, `nvsubquadratic`, `omegaconf`, `pytorch-ignite`, `imagecodecs`, `itk`, `jsonschema`, `lmdb`, `lpips`, `matplotlib`, `MetricsReloaded`, `mlflow`, `nibabel`, `nni`, `filelock`, `onnx`, `onnxruntime`, `onnx_graphsurgeon`, `onnxscript`, `openslide-python`, `openslide-bin`, `optuna`, `pandas`, `pillow`, `polygraphy`, `psutil`, `pyamg`, `pybind11`, `pydicom`, `pynrrd`, `nvidia-ml-py`, `pyyaml`, `requests`, `segment_anything`, `scipy`, `scikit-image`, `tensorboard`, `tensorboardX`, `tifffile`, `torchio`, `torchvision`, `tqdm`, `transformers`, `zarr`.
which correspond to the packages: `clearml`, `cucim` (`cucim-cu12` or `cucim-cu13`), `cupy-cuda13x`, `einops`, `fire`, `gdown`, `h5py`, `huggingface_hub`, `nvsubquadratic`, `omegaconf`, `pytorch-ignite`, `imagecodecs`, `itk`, `jsonschema`, `lmdb`, `lpips`, `matplotlib`, `MetricsReloaded`, `mlflow`, `nibabel`, `nni`, `filelock`, `nvidia-nvimgcodec-cu13`, `onnx`, `onnxruntime`, `onnx_graphsurgeon`, `onnxscript`, `openslide-python`, `openslide-bin`, `optuna`, `pandas`, `pillow`, `polygraphy`, `psutil`, `pyamg`, `pybind11`, `pydicom`, `pynrrd`, `nvidia-ml-py`, `pyyaml`, `requests`, `segment_anything`, `scipy`, `scikit-image`, `tensorboard`, `tensorboardX`, `tifffile`, `torchio`, `torchvision`, `tqdm`, `transformers`, `zarr`.

Almost all of these can be installed together with the `all` option. For development on MONAI, this should be accompanied by `testing` which will install the testing static checking packages. Cupy is omitted from `all` since the choice between
Cuda 12 and 13 versions of the library can't be resolved when installing and must be manually installed.
Almost all of these can be installed together with the `all` option. For development on MONAI, this should be accompanied by `testing` which will install the testing static checking packages. Cupy and `nvimgcodec` are omitted from `all` since the choice between
Cuda 12 and 13 versions of the libraries can't be resolved when installing and must be manually installed.

The `nvimgcodec` extra installs GPU-accelerated DICOM decoding for `NvImgCodecPydicomReader`
(`pip install 'monai[nvimgcodec]'`). It is Linux-only in the extra definition; CUDA 13 is the
default. CUDA 12 users should install matching `cupy-cuda12x` and `nvidia-nvimgcodec-cu12` wheels.

The `hyena` extra pulls in [`nvsubquadratic`](https://github.com/NVIDIA-BioNeMo/nvSubquadratic),
required by `HyenaNDUNETR` / `HyenaMixer` / `HyenaTransformerBlock` (subquadratic
Expand Down
11 changes: 10 additions & 1 deletion monai/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,16 @@
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
from .image_reader import (
ImageReader,
ITKReader,
NibabelReader,
NrrdReader,
NumpyReader,
NvImgCodecPydicomReader,
PILReader,
PydicomReader,
)
from .image_writer import (
SUPPORTED_WRITERS,
ImageWriter,
Expand Down
154 changes: 152 additions & 2 deletions monai/data/image_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,22 @@
else:
NdarrayOrCupy: TypeAlias = Any

__all__ = ["ImageReader", "ITKReader", "NibabelReader", "NumpyReader", "PILReader", "PydicomReader", "NrrdReader"]
__all__ = [
"ImageReader",
"ITKReader",
"NibabelReader",
"NumpyReader",
"PILReader",
"PydicomReader",
"NvImgCodecPydicomReader",
"NrrdReader",
"DICOM_READER_ENV_MAP",
"get_preferred_dicom_reader_key",
"is_dicom_path",
]

# Maps ``MONAI_DICOM_READER`` env values to keys in :py:data:`monai.transforms.io.array.SUPPORTED_READERS`.
DICOM_READER_ENV_MAP = {"itk": "itkreader", "pydicom": "pydicomreader", "nvimgcodec": "nvimgcodecpydicomreader"}


class ImageReader(ABC):
Expand Down Expand Up @@ -1027,6 +1042,141 @@ def _get_array_data(self, img, filename):
return data


def is_dicom_path(filename: Sequence[PathLike] | PathLike) -> bool:
"""
Return ``True`` if ``filename`` refers to a DICOM file or a directory that may contain a DICOM series.
"""
for name in ensure_tuple(filename):
name = f"{name}"
path = Path(name)
if path.is_dir():
return True
if path.suffix.lower() == ".dcm":
return True
if has_pydicom:
try:
if pydicom.misc.is_dicom(name):
return True
except Exception:
pass
return False


def get_preferred_dicom_reader_key() -> str:
"""
Return the :py:class:`~monai.transforms.LoadImage` registration key for the preferred DICOM reader.

Controlled by the ``MONAI_DICOM_READER`` environment variable. Supported values are
``itk``, ``pydicom``, and ``nvimgcodec``. Returns an empty string when the variable is
unset or set to an unsupported value (in which case :py:data:`~monai.transforms.io.array.SUPPORTED_READERS`
dict order is used unchanged).
"""
pref = os.environ.get("MONAI_DICOM_READER")
if pref is None:
return ""
pref = pref.lower()
if pref not in DICOM_READER_ENV_MAP:
warnings.warn(f"Unknown MONAI_DICOM_READER='{pref}', ignoring preference.")
return ""
return DICOM_READER_ENV_MAP[pref]


@require_pkg(pkg_name="pydicom")
class NvImgCodecPydicomReader(PydicomReader):
"""
Load DICOM images using Pydicom with GPU-accelerated decompression via nvImageCodec.

This reader extends :py:class:`PydicomReader` and registers the nvImageCodec pydicom
decoder plugin on initialization. The plugin accelerates decoding of compressed pixel data
for JPEG, JPEG 2000, and HTJ2K transfer syntaxes when CUDA, CuPy and ``nvidia-nvimgcodec`` are available.

If nvImageCodec is not available, a warning is issued and the reader falls back to the
default pydicom decoders (same behavior as :py:class:`PydicomReader`).

Requires the optional extra ``nvimgcodec`` (``pip install 'monai[nvimgcodec]'``), which installs ``pydicom``,
CuPy, and ``nvidia-nvimgcodec-cu13`` on Linux. GPU decompression uses
``nvidia.nvimgcodec.tools.dicom.pydicom_plugin`` from the nvImageCodec package. CUDA 13 is strongly
recommended because the nvJPEG library has addressed a known issue with JPEG lossless decoding in
CUDA 13.2.0+. For CUDA 12, install matching ``cupy-cuda12x`` and ``nvidia-nvimgcodec-cu12`` wheels instead.

Set environment variable ``MONAI_DICOM_READER=nvimgcodec`` to use this reader by default
with :py:class:`monai.transforms.LoadImage` without explicit configuration.

Note:
GPU direct loading bypasses Pydicom pixel data interpretation mechanism hence disables GPU decompression
via Pydicom decoder plugin that is used by this reader. So, GPU direct loading (``to_gpu=True``)
cannot be supported by this reader. The ``to_gpu`` init argument is accepted for API compatibility
with :py:class:`PydicomReader` but is always ignored so that GPU-accelerated decompression via nvImageCodec
is not bypassed.

Also noted is that the current implementation of GPU direct loading has a serious flaw as it simply loads
the raw bytes of pixel data into GPU memory and parses them into integers without any required processing,
e.g. applying rescale slope and intercept, `PhotometricInterpretation`, etc., and not processing compressed
pixel data. As such, the resulting data array will not represent the original pixel data faithfully except for
the simplest case of uncompressed pixel data.

This reader only declares ``@require_pkg(pkg_name="pydicom")`` so that :py:class:`monai.transforms.LoadImage`
can register it without hard-failing when GPU dependencies are missing. ``pydicom`` is required to construct
the reader; nvimgcodec, CUDA, and CuPy availability is checked at runtime with a warning issued and fallback to
default pydicom decoders if missing.

Args:
channel_dim: the channel dimension of the input image, default is None.
This is used to set original_channel_dim in the metadata, EnsureChannelFirstD reads this field.
If None, `original_channel_dim` will be either `no_channel` or `-1`.
affine_lps_to_ras: whether to convert the affine matrix from "LPS" to "RAS". Defaults to ``True``.
swap_ij: whether to swap the first two spatial axes. Default to ``True``.
prune_metadata: whether to prune the saved information in metadata. Default to ``True``.
label_dict: label of the dicom data for segmentation loading.
fname_regex: a regular expression to match file names when the input is a folder.
to_gpu: accepted for API compatibility with :py:class:`PydicomReader` but always ignored (always ``False``).
kwargs: additional args for `pydicom.dcmread` API.
"""

def __init__(
self,
channel_dim: str | int | None = None,
affine_lps_to_ras: bool = True,
swap_ij: bool = True,
prune_metadata: bool = True,
label_dict: dict | None = None,
fname_regex: str = "",
to_gpu: bool = False,
**kwargs,
):
if to_gpu:
warnings.warn(
"NvImgCodecPydicomReader ignores to_gpu=True; GPU direct loading is disabled to preserve "
"GPU-accelerated decompression."
)
super().__init__(
channel_dim=channel_dim,
affine_lps_to_ras=affine_lps_to_ras,
swap_ij=swap_ij,
prune_metadata=prune_metadata,
label_dict=label_dict,
fname_regex=fname_regex,
to_gpu=False,
**kwargs,
)
from monai.data.nvimgcodec_pydicom_plugin import is_nvimgcodec_available, register_as_decoder_plugin

self._nvimgcodec_available = is_nvimgcodec_available()
if not register_as_decoder_plugin():
warnings.warn(
"NvImgCodecPydicomReader: nvImageCodec decoder plugin did not register successfully. "
"Falling back to default pydicom decoders."
)

def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool:
"""
Verify whether the specified file or files are DICOM and nvImageCodec is available.
"""
if not has_pydicom or not self._nvimgcodec_available:
return False
return is_dicom_path(filename)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@require_pkg(pkg_name="nibabel")
class NibabelReader(ImageReader):
"""
Expand Down Expand Up @@ -1235,7 +1385,7 @@ def _get_array_data(self, img, filename):
with kvikio.CuFile(filename, "r") as f:
f.read(image)
if filename.endswith(".nii.gz"):
# for compressed data, have to tansfer to CPU to decompress
# for compressed data, have to transfer to CPU to decompress
# and then transfer back to GPU. It is not efficient compared to .nii file
# and may be slower than CPU loading in some cases.
warnings.warn("Loading compressed NIfTI file into GPU may not be efficient.")
Expand Down
80 changes: 80 additions & 0 deletions monai/data/nvimgcodec_pydicom_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
MONAI integration helpers for the nvImageCodec pydicom decoder plugin.

The decoder implementation lives in ``nvidia.nvimgcodec.tools.dicom.pydicom_plugin``
(shipped with ``nvidia-nvimgcodec-cuXX``). This module provides MONAI-facing helpers
and stable aliases for registration and availability checks.
"""

from __future__ import annotations

import logging

from monai.utils import optional_import

cp, has_cp = optional_import("cupy")
pydicom_plugin, has_pydicom_plugin = optional_import("nvidia.nvimgcodec.tools.dicom.pydicom_plugin")

_logger = logging.getLogger(__name__)

if has_pydicom_plugin:
DECODER_DEPENDENCIES = pydicom_plugin.DECODER_DEPENDENCIES
NVIMGCODEC_MIN_VERSION = pydicom_plugin.NVIMGCODEC_MIN_VERSION
NVIMGCODEC_MIN_VERSION_TUPLE = pydicom_plugin.NVIMGCODEC_MIN_VERSION_TUPLE
NVIMGCODEC_PLUGIN_LABEL = pydicom_plugin.NVIMGCODEC_PLUGIN_LABEL
SUPPORTED_DECODER_CLASSES = pydicom_plugin.SUPPORTED_DECODER_CLASSES
SUPPORTED_TRANSFER_SYNTAXES = pydicom_plugin.SUPPORTED_TRANSFER_SYNTAXES
is_available = pydicom_plugin.is_available
else: # pragma: no cover - optional dependency not installed
DECODER_DEPENDENCIES = {}
NVIMGCODEC_MIN_VERSION = "0.8.0"
NVIMGCODEC_MIN_VERSION_TUPLE = (0, 8, 0)
NVIMGCODEC_PLUGIN_LABEL = "0.8.0+nvimgcodec"
SUPPORTED_DECODER_CLASSES = []
SUPPORTED_TRANSFER_SYNTAXES = []

def is_available(uid) -> bool: # type: ignore[no-redef]
return False


def is_nvimgcodec_available() -> bool:
"""Return ``True`` if nvImageCodec with CUDA support is available."""
if not has_pydicom_plugin or getattr(pydicom_plugin, "nvimgcodec", None) is None or not has_cp:
_logger.debug("nvimgcodec pydicom plugin, nvimgcodec module, or CuPy missing.")
return False
try:
if not cp.cuda.is_available():
_logger.debug("CUDA device not found.")
return False
except Exception as exc: # pragma: no cover - environment specific
_logger.debug(f"CUDA availability check failed: {exc}")
return False
return True


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.")
return False
if not has_pydicom_plugin:
return False
return bool(pydicom_plugin.register(module_path))


def unregister_as_decoder_plugin() -> bool:
"""Unregister the nvImageCodec pydicom decoder plugin."""
if not has_pydicom_plugin:
return False
return bool(pydicom_plugin.unregister())
32 changes: 29 additions & 3 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
NibabelReader,
NrrdReader,
NumpyReader,
NvImgCodecPydicomReader,
PILReader,
PydicomReader,
get_preferred_dicom_reader_key,
)
from monai.data.meta_tensor import MetaTensor
from monai.data.utils import is_no_channel
Expand All @@ -59,10 +61,15 @@
nrrd, _ = optional_import("nrrd")
FileLock, has_filelock = optional_import("filelock", name="FileLock")

__all__ = ["LoadImage", "SaveImage", "SUPPORTED_READERS"]
__all__ = ["LoadImage", "SaveImage", "SUPPORTED_READERS", "get_default_reader_registration_order"]

# Default readers for :py:class:`LoadImage`. Dict insertion order is the registration order
# (auto-select tries registered readers from last to first). Users may add custom readers here.
# DICOM readers are listed first so that, by default, ``itkreader`` is tried before the other
# DICOM readers; ``MONAI_DICOM_READER`` can promote a preferred DICOM reader to last (tried first).
SUPPORTED_READERS = {
"pydicomreader": PydicomReader,
"nvimgcodecpydicomreader": NvImgCodecPydicomReader,
"itkreader": ITKReader,
"nrrdreader": NrrdReader,
"numpyreader": NumpyReader,
Expand All @@ -71,6 +78,22 @@
}


def get_default_reader_registration_order() -> list[str]:
"""
Return the default reader registration order for :py:class:`LoadImage`.

Uses :py:data:`SUPPORTED_READERS` insertion order so user-added entries are included.
If ``MONAI_DICOM_READER`` resolves to a non-empty preferred key present in
``SUPPORTED_READERS``, that key is moved to the end of the list so auto-selection
tries it first.
"""
order = list(SUPPORTED_READERS)
preferred = get_preferred_dicom_reader_key()
if preferred and preferred in order:
order = [key for key in order if key != preferred] + [preferred]
return order


def switch_endianness(data, new="<"):
"""
Convert the input `data` endianness to `new`.
Expand Down Expand Up @@ -116,7 +139,10 @@ class LoadImage(Transform):
- User-specified reader in the constructor of `LoadImage`.
- Readers from the last to the first in the registered list.
- Current default readers: (nii, nii.gz -> NibabelReader), (png, jpg, bmp -> PILReader),
(npz, npy -> NumpyReader), (nrrd -> NrrdReader), (DICOM file -> ITKReader).
(npz, npy -> NumpyReader), (nrrd -> NrrdReader),
(DICOM file -> ITKReader first among DICOM readers by default).
- Optionally set ``MONAI_DICOM_READER`` to ``itk``, ``pydicom``, or ``nvimgcodec``
(GPU-accelerated decoding; requires ``pip install 'monai[nvimgcodec]'``) to try that DICOM reader first.

Please note that for png, jpg, bmp, and other 2D formats, readers by default swap axis 0 and 1 after
loading the array with ``reverse_indexing`` set to ``True`` because the spatial axes definition
Expand Down Expand Up @@ -185,7 +211,7 @@ def __init__(
self.expanduser = expanduser

self.readers: list[ImageReader] = []
for r in SUPPORTED_READERS: # set predefined readers as default
for r in get_default_reader_registration_order(): # set predefined readers as default
Comment thread
ericspod marked this conversation as resolved.
try:
self.register(SUPPORTED_READERS[r](*args, **kwargs))
except OptionalImportError:
Expand Down
Loading
Loading