Skip to content

Commit e3ff7e1

Browse files
committed
Updated the unit test code and the decoder itself
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 2131b05 commit e3ff7e1

File tree

2 files changed

+172
-111
lines changed

2 files changed

+172
-111
lines changed

monai/deploy/operators/decoder_nvimgcodec.py

Lines changed: 77 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
6464
from typing import Any, Callable, Iterable
6565

6666
import numpy as np
67+
from pydicom.pixels.common import PhotometricInterpretation as PI # noqa: N817
68+
from pydicom.pixels.common import (
69+
RunnerBase,
70+
)
6771
from pydicom.pixels.decoders import (
6872
HTJ2KDecoder,
6973
HTJ2KLosslessDecoder,
@@ -75,11 +79,9 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
7579
JPEGLosslessDecoder,
7680
JPEGLosslessSV1Decoder,
7781
)
78-
7982
from pydicom.pixels.decoders.base import DecodeRunner
8083
from pydicom.pixels.utils import _passes_version_check
81-
from pydicom.uid import UID, JPEG2000TransferSyntaxes, JPEGTransferSyntaxes
82-
from pydicom.pixels.common import PhotometricInterpretation as PI
84+
from pydicom.uid import UID, JPEG2000TransferSyntaxes
8385

8486
try:
8587
import cupy as cp
@@ -140,7 +142,8 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
140142

141143
# Required for decoder plugin
142144
DECODER_DEPENDENCIES = {
143-
x: ("numpy", "cupy", f"{NVIMGCODEC_MODULE_NAME}>={NVIMGCODEC_MIN_VERSION}") for x in SUPPORTED_TRANSFER_SYNTAXES
145+
x: ("numpy", "cupy", f"{NVIMGCODEC_MODULE_NAME}>={NVIMGCODEC_MIN_VERSION}, nvidia-nvjpeg2k-cu12>=0.9.1,")
146+
for x in SUPPORTED_TRANSFER_SYNTAXES
144147
}
145148

146149

@@ -168,11 +171,58 @@ def is_available(uid: UID) -> bool:
168171
return True
169172

170173

174+
# Required for decoder plugin
175+
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes:
176+
"""Return the decoded image data in `src` as a :class:`bytearray` or :class:`bytes`."""
177+
tsyntax = runner.transfer_syntax
178+
_logger.debug(f"transfer_syntax: {tsyntax}")
179+
180+
if not is_available(tsyntax):
181+
raise ValueError(f"Transfer syntax {tsyntax} not supported; see details in the debug log.")
182+
183+
runner.set_frame_option(runner.index, "decoding_plugin", "nvimgcodec") # type: ignore[attr-defined]
184+
185+
is_jpeg2k = tsyntax in JPEG2000TransferSyntaxes
186+
samples_per_pixel = runner.samples_per_pixel
187+
photometric_interpretation = runner.photometric_interpretation
188+
189+
# --- JPEG 2000: Precision/Bit depth ---
190+
if is_jpeg2k:
191+
precision, bits_allocated = _jpeg2k_precision_bits(runner)
192+
runner.set_frame_option(runner.index, "bits_allocated", bits_allocated) # type: ignore[attr-defined]
193+
_logger.debug(f"Set bits_allocated to {bits_allocated} for J2K precision {precision}")
194+
195+
# Check if RGB conversion requested (following Pillow decoder logic)
196+
convert_to_rgb = (
197+
samples_per_pixel > 1 and runner.get_option("as_rgb", False) and "YBR" in photometric_interpretation
198+
)
199+
200+
decoder = _get_decoder_resources()
201+
params = _get_decode_params(runner)
202+
decoded_surface = decoder.decode(src, params=params).cpu()
203+
np_surface = np.ascontiguousarray(np.asarray(decoded_surface))
204+
205+
# Handle JPEG2000-specific postprocessing separately
206+
if is_jpeg2k:
207+
np_surface = _jpeg2k_postprocess(np_surface, runner)
208+
209+
# Update photometric interpretation if we converted to RGB, or JPEG 2000 YBR*
210+
if convert_to_rgb or photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
211+
runner.set_frame_option(runner.index, "photometric_interpretation", PI.RGB) # type: ignore[attr-defined]
212+
_logger.debug(
213+
"Set photometric_interpretation to RGB after conversion"
214+
if convert_to_rgb
215+
else f"Set photometric_interpretation to RGB for {photometric_interpretation}"
216+
)
217+
218+
return np_surface.tobytes()
219+
220+
171221
def _get_decoder_resources() -> Any:
172222
"""Return cached nvimgcodec decoder (parameters are created per decode)."""
173223

174-
if nvimgcodec is None:
175-
raise ImportError("nvimgcodec package is not available.")
224+
if not _is_nvimgcodec_available():
225+
raise RuntimeError("nvimgcodec package is not available.")
176226

177227
global _NVIMGCODEC_DECODER
178228

@@ -182,74 +232,64 @@ def _get_decoder_resources() -> Any:
182232
return _NVIMGCODEC_DECODER
183233

184234

185-
def _get_decode_params(runner: DecodeRunner) -> Any:
235+
def _get_decode_params(runner: RunnerBase) -> Any:
186236
"""Create decode parameters based on DICOM image characteristics.
187-
237+
188238
Mimics the behavior of pydicom's Pillow decoder:
189239
- By default, keeps JPEG data in YCbCr format (no conversion)
190240
- If as_rgb option is True and photometric interpretation is YBR*, converts to RGB
191-
241+
192242
This matches the logic in pydicom.pixels.decoders.pillow._decode_frame()
193-
243+
194244
Args:
195-
runner: The DecodeRunner instance with access to DICOM metadata.
196-
245+
runner: The DecodeRunner or RunnerBase instance with access to DICOM metadata.
246+
197247
Returns:
198248
nvimgcodec.DecodeParams: Configured decode parameters.
199249
"""
200-
if nvimgcodec is None:
201-
raise ImportError("nvimgcodec package is not available.")
202-
250+
if not _is_nvimgcodec_available():
251+
raise RuntimeError("nvimgcodec package is not available.")
252+
203253
# Access DICOM metadata from the runner
204254
samples_per_pixel = runner.samples_per_pixel
205255
photometric_interpretation = runner.photometric_interpretation
206-
256+
207257
# Default: keep color space unchanged
208258
color_spec = nvimgcodec.ColorSpec.UNCHANGED
209-
210-
# Import PhotometricInterpretation enum for JPEG 2000 color transformations
211-
from pydicom.pixels.common import PhotometricInterpretation as PI
212-
259+
213260
# For multi-sample (color) images, check if RGB conversion is requested
214261
if samples_per_pixel > 1:
215262
# JPEG 2000 color transformations are always returned as RGB (matches Pillow)
216263
if photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
217264
color_spec = nvimgcodec.ColorSpec.RGB
218265
_logger.debug(
219-
f"Using RGB color spec for JPEG 2000 color transformation "
220-
f"(PI: {photometric_interpretation})"
266+
f"Using RGB color spec for JPEG 2000 color transformation " f"(PI: {photometric_interpretation})"
221267
)
222268
else:
223269
# Check the as_rgb option - same as Pillow decoder
224270
convert_to_rgb = runner.get_option("as_rgb", False) and "YBR" in photometric_interpretation
225-
271+
226272
if convert_to_rgb:
227273
# Convert YCbCr to RGB as requested
228274
color_spec = nvimgcodec.ColorSpec.RGB
229-
_logger.debug(
230-
f"Using RGB color spec (as_rgb=True, PI: {photometric_interpretation})"
231-
)
275+
_logger.debug(f"Using RGB color spec (as_rgb=True, PI: {photometric_interpretation})")
232276
else:
233277
# Keep YCbCr unchanged - matches Pillow's image.draft("YCbCr") behavior
234278
_logger.debug(
235-
f"Using UNCHANGED color spec to preserve YCbCr "
236-
f"(as_rgb=False, PI: {photometric_interpretation})"
279+
f"Using UNCHANGED color spec to preserve YCbCr " f"(as_rgb=False, PI: {photometric_interpretation})"
237280
)
238281
else:
239282
# Grayscale image - keep unchanged
240-
_logger.debug(
241-
f"Using UNCHANGED color spec for grayscale image "
242-
f"(samples_per_pixel: {samples_per_pixel})"
243-
)
244-
283+
_logger.debug(f"Using UNCHANGED color spec for grayscale image " f"(samples_per_pixel: {samples_per_pixel})")
284+
245285
return nvimgcodec.DecodeParams(
246286
allow_any_depth=True,
247287
color_spec=color_spec,
248288
)
249289

250290

251-
def _jpeg2k_precision_bits(runner):
252-
precision = runner.get_frame_option(runner.index, "j2k_precision", runner.bits_stored)
291+
def _jpeg2k_precision_bits(runner: DecodeRunner) -> tuple[int, int]:
292+
precision = runner.get_frame_option(runner.index, "j2k_precision", runner.bits_stored) # type: ignore[attr-defined]
253293
if 0 < precision <= 8:
254294
return precision, 8
255295
elif 8 < precision <= 16:
@@ -300,54 +340,6 @@ def _jpeg2k_postprocess(np_surface, runner):
300340

301341
return np_surface
302342

303-
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes:
304-
"""Return the decoded image data in `src` as a :class:`bytearray` or :class:`bytes`."""
305-
tsyntax = runner.transfer_syntax
306-
_logger.debug(f"transfer_syntax: {tsyntax}")
307-
308-
if not is_available(tsyntax):
309-
raise ValueError(f"Transfer syntax {tsyntax} not supported; see details in the debug log.")
310-
311-
runner.set_frame_option(runner.index, "decoding_plugin", "nvimgcodec")
312-
313-
is_jpeg2k = tsyntax in JPEG2000TransferSyntaxes
314-
samples_per_pixel = runner.samples_per_pixel
315-
photometric_interpretation = runner.photometric_interpretation
316-
317-
# --- JPEG 2000: Precision/Bit depth ---
318-
if is_jpeg2k:
319-
precision, bits_allocated = _jpeg2k_precision_bits(runner)
320-
runner.set_frame_option(runner.index, "bits_allocated", bits_allocated)
321-
_logger.debug(f"Set bits_allocated to {bits_allocated} for J2K precision {precision}")
322-
323-
# Check if RGB conversion requested (following Pillow decoder logic)
324-
convert_to_rgb = (
325-
samples_per_pixel > 1
326-
and runner.get_option("as_rgb", False)
327-
and "YBR" in photometric_interpretation
328-
)
329-
330-
decoder = _get_decoder_resources()
331-
params = _get_decode_params(runner)
332-
decoded_surface = decoder.decode(src, params=params).cpu()
333-
np_surface = np.ascontiguousarray(np.asarray(decoded_surface))
334-
335-
# Handle JPEG2000-specific postprocessing separately
336-
if is_jpeg2k:
337-
np_surface = _jpeg2k_postprocess(np_surface, runner)
338-
339-
# Update photometric interpretation if we converted to RGB, or JPEG 2000 YBR*
340-
if convert_to_rgb or photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
341-
runner.set_frame_option(runner.index, "photometric_interpretation", PI.RGB)
342-
_logger.debug(
343-
"Set photometric_interpretation to RGB after conversion"
344-
if convert_to_rgb
345-
else f"Set photometric_interpretation to RGB for {photometric_interpretation}"
346-
)
347-
348-
return np_surface.tobytes()
349-
350-
351343

352344
def _is_nvimgcodec_available() -> bool:
353345
"""Return ``True`` if nvimgcodec is available, ``False`` otherwise."""
@@ -360,13 +352,13 @@ def _is_nvimgcodec_available() -> bool:
360352
_logger.debug("CUDA device not found.")
361353
return False
362354
except Exception as exc: # pragma: no cover - environment specific
363-
_logger.debug("CUDA availability check failed: %s", exc)
355+
_logger.debug(f"CUDA availability check failed: {exc}")
364356
return False
365357

366358
return True
367359

368360

369-
# Helper functions for an application to register this decoder plugin with Pydicom at application startup.
361+
# Helper functions for an application to register/unregister this decoder plugin with Pydicom at application startup.
370362

371363

372364
def register_as_decoder_plugin(module_path: str | None = None) -> bool:

0 commit comments

Comments
 (0)