@@ -64,6 +64,10 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
6464from typing import Any , Callable , Iterable
6565
6666import numpy as np
67+ from pydicom .pixels .common import PhotometricInterpretation as PI # noqa: N817
68+ from pydicom .pixels .common import (
69+ RunnerBase ,
70+ )
6771from 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-
7982from pydicom .pixels .decoders .base import DecodeRunner
8083from 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
8486try :
8587 import cupy as cp
@@ -140,7 +142,8 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
140142
141143# Required for decoder plugin
142144DECODER_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+
171221def _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
352344def _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
372364def register_as_decoder_plugin (module_path : str | None = None ) -> bool :
0 commit comments