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
77 changes: 59 additions & 18 deletions monai/deploy/operators/decoder_nvimgcodec.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,19 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes:
if not is_available(tsyntax):
raise ValueError(f"Transfer syntax {tsyntax} not supported; see details in the debug log.")

runner.set_frame_option(runner.index, "decoding_plugin", "nvimgcodec") # type: ignore[attr-defined]

# runner.set_frame_option(runner.index, "decoding_plugin", "nvimgcodec") # type: ignore[attr-defined]
# in pydicom v3.1.0 can use the above call
# runner.set_option("decoding_plugin", "nvimgcodec")
is_jpeg2k = tsyntax in JPEG2000TransferSyntaxes
samples_per_pixel = runner.samples_per_pixel
photometric_interpretation = runner.photometric_interpretation

# --- JPEG 2000: Precision/Bit depth ---
if is_jpeg2k:
precision, bits_allocated = _jpeg2k_precision_bits(runner)
runner.set_frame_option(runner.index, "bits_allocated", bits_allocated) # type: ignore[attr-defined]
# runner.set_frame_option(runner.index, "bits_allocated", bits_allocated) # type: ignore[attr-defined]
# in pydicom v3.1.0 can use the abover call
runner.set_option("bits_allocated", bits_allocated)
_logger.debug(f"Set bits_allocated to {bits_allocated} for J2K precision {precision}")

# Check if RGB conversion requested (following Pillow decoder logic)
Expand All @@ -199,16 +202,22 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes:

decoder = _get_decoder_resources()
params = _get_decode_params(runner)
decoded_surface = decoder.decode(src, params=params).cpu()
np_surface = np.ascontiguousarray(np.asarray(decoded_surface))
decoded_data = decoder.decode(src, params=params)
if decoded_data:
decoded_data = decoded_data.cpu()
else:
raise RuntimeError(f"Decoded data is None: {type(decoded_data)}")
np_surface = np.ascontiguousarray(np.asarray(decoded_data))

# Handle JPEG2000-specific postprocessing separately
if is_jpeg2k:
np_surface = _jpeg2k_postprocess(np_surface, runner)

# Update photometric interpretation if we converted to RGB, or JPEG 2000 YBR*
if convert_to_rgb or photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
runner.set_frame_option(runner.index, "photometric_interpretation", PI.RGB) # type: ignore[attr-defined]
# runner.set_frame_option(runner.index, "photometric_interpretation", PI.RGB) # type: ignore[attr-defined]
# in pydicon v3.1.0 can use the above call
runner.set_option("photometric_interpretation", PI.RGB)
_logger.debug(
"Set photometric_interpretation to RGB after conversion"
if convert_to_rgb
Expand Down Expand Up @@ -253,6 +262,18 @@ def _get_decode_params(runner: RunnerBase) -> Any:
# Access DICOM metadata from the runner
samples_per_pixel = runner.samples_per_pixel
photometric_interpretation = runner.photometric_interpretation
transfer_syntax = runner.transfer_syntax
as_rgb = runner.get_option("as_rgb", False)
force_rgb = runner.get_option("force_rgb", False)
force_ybr = runner.get_option("force_ybr", False)

_logger.debug("DecodeRunner options:")
_logger.debug(f"transfer_syntax: {transfer_syntax}")
_logger.debug(f"photometric_interpretation: {photometric_interpretation}")
_logger.debug(f"samples_per_pixel: {samples_per_pixel}")
_logger.debug(f"as_rgb: {as_rgb}")
_logger.debug(f"force_rgb: {force_rgb}")
_logger.debug(f"force_ybr: {force_ybr}")

# Default: keep color space unchanged
color_spec = nvimgcodec.ColorSpec.UNCHANGED
Expand All @@ -261,17 +282,22 @@ def _get_decode_params(runner: RunnerBase) -> Any:
if samples_per_pixel > 1:
# JPEG 2000 color transformations are always returned as RGB (matches Pillow)
if photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
color_spec = nvimgcodec.ColorSpec.RGB
color_spec = nvimgcodec.ColorSpec.SRGB
_logger.debug(
f"Using RGB color spec for JPEG 2000 color transformation " f"(PI: {photometric_interpretation})"
)
elif photometric_interpretation in (PI.RGB) and transfer_syntax in (JPEGBaseline8BitDecoder.UID):
color_spec = nvimgcodec.ColorSpec.SYCC
_logger.debug(
f"Need to decode without YCC->RGB for PI: {photometric_interpretation} of transfer syntanx {transfer_syntax}"
)
else:
# Check the as_rgb option - same as Pillow decoder
convert_to_rgb = runner.get_option("as_rgb", False) and "YBR" in photometric_interpretation
convert_to_rgb = as_rgb or (force_rgb and "YBR" in photometric_interpretation)

if convert_to_rgb:
# Convert YCbCr to RGB as requested
color_spec = nvimgcodec.ColorSpec.RGB
color_spec = nvimgcodec.ColorSpec.SRGB
_logger.debug(f"Using RGB color spec (as_rgb=True, PI: {photometric_interpretation})")
else:
# Keep YCbCr unchanged - matches Pillow's image.draft("YCbCr") behavior
Expand All @@ -280,7 +306,10 @@ def _get_decode_params(runner: RunnerBase) -> Any:
)
else:
# Grayscale image - keep unchanged
_logger.debug(f"Using UNCHANGED color spec for grayscale image " f"(samples_per_pixel: {samples_per_pixel})")
_logger.debug(
f"Using UNCHANGED color spec for grayscale image (samples_per_pixel: {samples_per_pixel},"
f" PI: {photometric_interpretation}, transfer_syntax: {transfer_syntax})"
)

return nvimgcodec.DecodeParams(
allow_any_depth=True,
Expand All @@ -289,7 +318,9 @@ def _get_decode_params(runner: RunnerBase) -> Any:


def _jpeg2k_precision_bits(runner: DecodeRunner) -> tuple[int, int]:
precision = runner.get_frame_option(runner.index, "j2k_precision", runner.bits_stored) # type: ignore[attr-defined]
# precision = runner.get_frame_option(runner.index, "j2k_precision", runner.bits_stored) # type: ignore[attr-defined]
# in pydicom v3.1.0 can use the above call
precision = runner.get_option("j2k_precision", runner.bits_stored)
if 0 < precision <= 8:
return precision, 8
elif 8 < precision <= 16:
Expand Down Expand Up @@ -317,15 +348,22 @@ def _jpeg2k_bitshift(arr, bit_shift):

def _jpeg2k_postprocess(np_surface, runner):
"""Handle JPEG 2000 postprocessing: sign correction and bit shifts."""
precision = runner.get_frame_option(runner.index, "j2k_precision", runner.bits_stored)
bits_allocated = runner.get_frame_option(runner.index, "bits_allocated", runner.bits_allocated)
# precision = runner.get_frame_option("j2k_precision", runner.bits_stored)
# bits_allocated = runner.get_frame_option(runner.index, "bits_allocated", runner.bits_allocated)
# in pydicom v3.1.0 can use the above calls
precision = runner.get_option("j2k_precision", runner.bits_stored)
bits_allocated = runner.get_option("bits_allocated", runner.bits_allocated)
is_signed = runner.pixel_representation
if runner.get_option("apply_j2k_sign_correction", False):
is_signed = runner.get_frame_option(runner.index, "j2k_is_signed", is_signed)
# is_signed = runner.get_frame_option(runner.index, "j2k_is_signed", is_signed)
# in pydicom v3.1.0 can use the above call
is_signed = runner.get_option("j2k_is_signed", is_signed)

# Sign correction for signed data
if is_signed and runner.pixel_representation == 1:
dtype = runner.frame_dtype(runner.index)
# dtype = runner.frame_dtype(runner.index)
# in pydicomv3.1.0 can use the above call
dtype = runner.pixel_dtype
buffer = bytearray(np_surface.tobytes())
arr = np.frombuffer(buffer, dtype=f"<u{dtype.itemsize}")
np_surface = _jpeg2k_sign_correction(arr, dtype, bits_allocated)
Expand All @@ -334,7 +372,9 @@ def _jpeg2k_postprocess(np_surface, runner):
bit_shift = bits_allocated - precision
if bit_shift:
buffer = bytearray(np_surface.tobytes() if isinstance(np_surface, np.ndarray) else np_surface)
dtype = runner.frame_dtype(runner.index)
# dtype = runner.frame_dtype(runner.index)
# in v3.1.0 can use the above call
dtype = runner.pixel_dtype
arr = np.frombuffer(buffer, dtype=dtype)
np_surface = _jpeg2k_bitshift(arr, bit_shift)

Expand Down Expand Up @@ -416,7 +456,7 @@ def register_as_decoder_plugin(module_path: str | None = None) -> bool:
continue

decoder_class.add_plugin(NVIMGCODEC_PLUGIN_LABEL, (module_path, str(func_name)))
_logger.info(
_logger.debug(
f"Added plugin for transfer syntax {decoder_class.UID}: "
f"{NVIMGCODEC_PLUGIN_LABEL} with {func_name} in module path {module_path}."
)
Expand All @@ -437,7 +477,8 @@ def unregister_as_decoder_plugin() -> bool:
for decoder_class in SUPPORTED_DECODER_CLASSES:
if NVIMGCODEC_PLUGIN_LABEL in decoder_class.available_plugins:
decoder_class.remove_plugin(NVIMGCODEC_PLUGIN_LABEL)
_logger.info(f"Unregistered plugin for transfer syntax {decoder_class.UID}: {NVIMGCODEC_PLUGIN_LABEL}")
_logger.debug(f"Unregistered plugin for transfer syntax {decoder_class.UID}: {NVIMGCODEC_PLUGIN_LABEL}")
_logger.info(f"Unregistered plugin {NVIMGCODEC_PLUGIN_LABEL} for all supported transfer syntaxex.")

return True

Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ ignore =
B026
# B909 editing a loop's mutable iterable often leads to unexpected results/bugs
B909
# F824 global variable is unused: name is never assigned in scope
F824

per_file_ignores =
# e.g. F403 'from holoscan.conditions import *' used; unable to detect undefined names
Expand Down
Loading