Skip to content

Commit 33db3c5

Browse files
committed
Reorg the code
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 8051e38 commit 33db3c5

File tree

1 file changed

+108
-81
lines changed

1 file changed

+108
-81
lines changed

monai/deploy/operators/decoder_nvimgcodec.py

Lines changed: 108 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,53 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
# This decoder plugin for nvimgcodec <https://github.com/NVIDIA/nvImageCodec> decompresses
13-
# encoded Pixel Data for the following transfer syntaxes:
14-
# JPEGBaseline8Bit, # 1.2.840.10008.1.2.4.50, JPEG Baseline (Process 1)
15-
# JPEGExtended12Bit, # 1.2.840.10008.1.2.4.51, JPEG Extended (Process 2 & 4)
16-
# JPEGLossless, # 1.2.840.10008.1.2.4.57, JPEG Lossless, Non-Hierarchical (Process 14)
17-
# JPEGLosslessSV1, # 1.2.840.10008.1.2.4.70, JPEG Lossless, Non-Hierarchical, First-Order Prediction
18-
# JPEG2000Lossless, # 1.2.840.10008.1.2.4.90, JPEG 2000 Image Compression (Lossless Only)
19-
# JPEG2000, # 1.2.840.10008.1.2.4.91, JPEG 2000 Image Compression
20-
# HTJ2KLossless, # 1.2.840.10008.1.2.4.201, HTJ2K Image Compression (Lossless Only)
21-
# HTJ2KLosslessRPCL, # 1.2.840.10008.1.2.4.202, HTJ2K with RPCL Options Image Compression (Lossless Only)
22-
# HTJ2K, # 1.2.840.10008.1.2.4.203, HTJ2K Image Compression
23-
#
24-
# There are two ways to add a custom decoding plugin to pydicom:
25-
# 1. Using the pixel_data_handlers backend, though pydicom.pixel_data_handlers module is deprecated
26-
# and will be removed in v4.0.
27-
# 2. Using the pixels backend by adding a decoder plugin to existing decoders with the add_plugin method,
28-
# see https://pydicom.github.io/pydicom/stable/guides/decoding/decoder_plugins.html
29-
#
30-
# It is noted that pydicom.dataset.Dataset.pixel_array changed in version 3.0 where the backend used for
31-
# pixel data decoding changed from the pixel_data_handlers module to the pixels module.
32-
#
33-
# So, this implementation uses the pixels backend.
34-
#
35-
# Plugin Requirements:
36-
# A custom decoding plugin must implement three objects within the same module:
37-
# - A function named is_available with the following signature:
38-
# def is_available(uid: pydicom.uid.UID) -> bool:
39-
# Where uid is the Transfer Syntax UID for the corresponding decoder as a UID
40-
# - A dict named DECODER_DEPENDENCIES with the type dict[pydicom.uid.UID, tuple[str, ...], such as:
41-
# DECODER_DEPENDENCIES = {JPEG2000Lossless: ('numpy', 'pillow', 'imagecodecs'),}
42-
# This will be used to provide the user with a list of dependencies required by the plugin.
43-
# - A function that performs the decoding with the following function signature as in Github repo:
44-
# def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
45-
# src is a single frame’s worth of raw compressed data to be decoded, and
46-
# runner is a DecodeRunner instance that manages the decoding process.
47-
#
48-
# Adding plugins to a Decoder:
49-
# Additional plugins can be added to an existing decoder with the add_plugin() method
50-
# ```python
51-
# from pydicom.pixels.decoders import RLELosslessDecoder
52-
# RLELosslessDecoder.add_plugin(
53-
# 'my_decoder', # the plugin's label
54-
# ('my_package.decoders', 'my_decoder_func') # the import paths
55-
# )
56-
# ```
57-
12+
"""
13+
This decoder plugin for nvimgcodec <https://github.com/NVIDIA/nvImageCodec> decompresses
14+
encoded Pixel Data for the following transfer syntaxes:
15+
JPEGBaseline8Bit, 1.2.840.10008.1.2.4.50, JPEG Baseline (Process 1)
16+
JPEGExtended12Bit, 1.2.840.10008.1.2.4.51, JPEG Extended (Process 2 & 4)
17+
JPEGLossless, 1.2.840.10008.1.2.4.57, JPEG Lossless, Non-Hierarchical (Process 14)
18+
JPEGLosslessSV1, 1.2.840.10008.1.2.4.70, JPEG Lossless, Non-Hierarchical, First-Order Prediction
19+
JPEG2000Lossless, 1.2.840.10008.1.2.4.90, JPEG 2000 Image Compression (Lossless Only)
20+
JPEG2000, 1.2.840.10008.1.2.4.91, JPEG 2000 Image Compression
21+
HTJ2KLossless, 1.2.840.10008.1.2.4.201, HTJ2K Image Compression (Lossless Only)
22+
HTJ2KLosslessRPCL, 1.2.840.10008.1.2.4.202, HTJ2K with RPCL Options Image Compression (Lossless Only)
23+
HTJ2K, 1.2.840.10008.1.2.4.203, HTJ2K Image Compression
24+
25+
There are two ways to add a custom decoding plugin to pydicom:
26+
1. Using the pixel_data_handlers backend, though pydicom.pixel_data_handlers module is deprecated
27+
and will be removed in v4.0.
28+
2. Using the pixels backend by adding a decoder plugin to existing decoders with the add_plugin method,
29+
see https://pydicom.github.io/pydicom/stable/guides/decoding/decoder_plugins.html
30+
31+
It is noted that pydicom.dataset.Dataset.pixel_array changed in version 3.0 where the backend used for
32+
pixel data decoding changed from the pixel_data_handlers module to the pixels module.
33+
34+
So, this implementation uses the pixels backend.
35+
36+
Plugin Requirements:
37+
A custom decoding plugin must implement three objects within the same module:
38+
- A function named is_available with the following signature:
39+
def is_available(uid: pydicom.uid.UID) -> bool:
40+
Where uid is the Transfer Syntax UID for the corresponding decoder as a UID
41+
- A dict named DECODER_DEPENDENCIES with the type dict[pydicom.uid.UID, tuple[str, ...], such as:
42+
DECODER_DEPENDENCIES = {JPEG2000Lossless: ('numpy', 'pillow', 'imagecodecs'),}
43+
This will be used to provide the user with a list of dependencies required by the plugin.
44+
- A function that performs the decoding with the following function signature as in Github repo:
45+
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes
46+
src is a single frame’s worth of raw compressed data to be decoded, and
47+
runner is a DecodeRunner instance that manages the decoding process.
48+
49+
Adding plugins to a Decoder:
50+
Additional plugins can be added to an existing decoder with the add_plugin() method
51+
```python
52+
from pydicom.pixels.decoders import RLELosslessDecoder
53+
RLELosslessDecoder.add_plugin(
54+
'my_decoder', the plugin's label
55+
('my_package.decoders', 'my_decoder_func') the import paths
56+
)
57+
```
58+
"""
5859

5960
import inspect
6061
import logging
@@ -88,10 +89,11 @@
8889
# Parse version string, extracting only numeric components to handle suffixes like "0.6.0rc1"
8990
try:
9091
import re
92+
9193
version_parts = []
9294
for part in nvimgcodec.__version__.split("."):
9395
# Extract leading digits from each version component
94-
match = re.match(r'^(\d+)', part)
96+
match = re.match(r"^(\d+)", part)
9597
if match:
9698
version_parts.append(int(match.group(1)))
9799
else:
@@ -107,7 +109,6 @@
107109
NVIMGCODEC_MIN_VERSION = "0.6"
108110
NVIMGCODEC_MIN_VERSION_TUPLE = tuple(int(x) for x in NVIMGCODEC_MIN_VERSION.split("."))
109111
NVIMGCODEC_PLUGIN_LABEL = "0.6+nvimgcodec" # to be sorted to first in ascending order of plugins
110-
NVIMGCODEC_PLUGIN_FUNC_NAME = "_decode_frame"
111112

112113
# Supported decoder classes of the corresponding transfer syntaxes by this decoder plugin.
113114
SUPPORTED_DECODER_CLASSES = [
@@ -193,7 +194,20 @@ def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray | bytes:
193194
return bytearray(decoded_data.cpu()) # HWC layout, interleaved format, and contiguous array in C-style
194195

195196

196-
# End of required function for decoder plugin
197+
def _is_nvimgcodec_available() -> bool:
198+
"""Return ``True`` if nvimgcodec is available, ``False`` otherwise."""
199+
200+
if not nvimgcodec or not _passes_version_check(NVIMGCODEC_MODULE_NAME, NVIMGCODEC_MIN_VERSION_TUPLE) or not cp:
201+
_logger.debug(f"nvimgcodec (version >= {NVIMGCODEC_MIN_VERSION}) or CuPy missing.")
202+
return False
203+
if not cp.cuda.is_available():
204+
_logger.debug("CUDA device not found.")
205+
return False
206+
207+
return True
208+
209+
210+
# Helper functions for an application to register this decoder plugin with Pydicom at application startup.
197211

198212

199213
def register_as_decoder_plugin(module_path: str | None = None) -> bool:
@@ -219,10 +233,14 @@ def register_as_decoder_plugin(module_path: str | None = None) -> bool:
219233
"""
220234

221235
if not _is_nvimgcodec_available():
222-
_logger.info(f"Module {NVIMGCODEC_MODULE_NAME} is not available.")
236+
_logger.warning(f"Module {NVIMGCODEC_MODULE_NAME} is not available.")
223237
return False
224238

225-
func_name = NVIMGCODEC_PLUGIN_FUNC_NAME
239+
try:
240+
func_name = getattr(_decode_frame, "__name__", None)
241+
except NameError:
242+
_logger.error("Decoder function `_decode_frame` not found.")
243+
return False
226244

227245
if module_path is None:
228246
module_path = _find_module_path(__name__)
@@ -231,44 +249,66 @@ def register_as_decoder_plugin(module_path: str | None = None) -> bool:
231249
module_path_found, func_name_found = _get_callable_origin(_decode_frame) # get the func's module path.
232250
if module_path_found:
233251
if module_path.casefold() != module_path_found.casefold():
234-
_logger.info(f"Module path {module_path} does not match {module_path_found} for decoder plugin.")
252+
_logger.warning(f"Module path {module_path} does not match {module_path_found} for decoder plugin.")
235253
else:
236-
_logger.info(f"Module path {module_path} not found for decoder plugin.")
254+
_logger.error(f"Module path {module_path} not found for decoder plugin.")
237255
return False
238256

239-
if func_name_found != NVIMGCODEC_PLUGIN_FUNC_NAME:
257+
if func_name != func_name_found:
240258
_logger.warning(
241-
f"Function name {func_name_found} does not match {NVIMGCODEC_PLUGIN_FUNC_NAME} for decoder plugin."
259+
f"Function {func_name_found} in {module_path_found} instead of {func_name} used for decoder plugin."
242260
)
243261

244262
for decoder_class in SUPPORTED_DECODER_CLASSES:
245-
_logger.info(
246-
f"Adding plugin {NVIMGCODEC_PLUGIN_LABEL} with module path {module_path} and func name {func_name} "
247-
f"for transfer syntax {decoder_class.UID}"
263+
_logger.debug(
264+
f"Adding plugin for transfer syntax {decoder_class.UID}: "
265+
f"{NVIMGCODEC_PLUGIN_LABEL} with {func_name} in module path {module_path}."
248266
)
249-
decoder_class.add_plugin(NVIMGCODEC_PLUGIN_LABEL, (module_path, func_name))
267+
decoder_class.add_plugin(NVIMGCODEC_PLUGIN_LABEL, (module_path, str(func_name)))
250268

251269
# Need to sort the plugins to make sure the custom plugin is the first in items() of
252270
# the decoder class search for the plugin to be used.
253271
decoder_class._available = dict(sorted(decoder_class._available.items(), key=lambda item: item[0]))
254-
_logger.info(
255-
f"Registered decoder plugin {NVIMGCODEC_PLUGIN_LABEL} for transfer syntax {decoder_class.UID}: "
256-
f"{decoder_class._available}"
257-
)
258-
_logger.info(f"Registered nvimgcodec decoder plugin with {len(SUPPORTED_DECODER_CLASSES)} decoder classes.")
272+
_logger.debug(f"Sorted plugins for transfer syntax {decoder_class.UID}: {decoder_class._available}")
273+
_logger.info(f"Registered {NVIMGCODEC_MODULE_NAME} with {len(SUPPORTED_DECODER_CLASSES)} decoder classes.")
259274

260275
return True
261276

262277

263278
def _find_module_path(module_name: str | None) -> str:
264-
"""Return the importable module path for this file.
279+
"""Return the importable module path for *module_name* file.
265280
266281
When *module_name* is ``None`` or ``"__main__"``, search the loaded modules
267282
for an entry whose ``__file__`` resolves to the current file.
268-
Likely to be in module paths that start with ``monai.deploy.operators`` or ``monai.data``.
283+
284+
When *module_name* is provided and not ``"__main__"``, validate it exists in
285+
loaded modules and corresponds to the current file, returning it if valid.
286+
287+
When used in MONAI, likely in module paths ``monai.deploy.operators`` or ``monai.data``.
269288
"""
270289

271290
current_file = Path(__file__).resolve()
291+
292+
# If a specific module name is provided (not None or "__main__"), validate it
293+
if module_name and module_name != "__main__":
294+
module = sys.modules.get(module_name)
295+
if module:
296+
module_file = getattr(module, "__file__", None)
297+
if module_file:
298+
try:
299+
if Path(module_file).resolve() == current_file:
300+
return module_name
301+
else:
302+
_logger.warning(f"Module {module_name} found but its file path does not match current file.")
303+
except (OSError, RuntimeError):
304+
_logger.warning(f"Could not resolve file path for module {module_name}.")
305+
else:
306+
_logger.warning(f"Module {module_name} has no __file__ attribute.")
307+
else:
308+
_logger.warning(f"Module {module_name} not found in loaded modules.")
309+
# Fall through to search for the correct module
310+
311+
# Search for modules that correspond to the current file
272312
candidates: list[str] = []
273313

274314
for name, module in sys.modules.items():
@@ -297,9 +337,9 @@ def _find_module_path(module_name: str | None) -> str:
297337

298338

299339
def _get_callable_origin(obj: Callable[..., Any]) -> tuple[str | None, str | None]:
300-
"""Return the importable module path and attribute name for *obj*.
340+
"""Return the importable module path and attribute(function) name for *obj*.
301341
302-
Can be used to get the importable module path and func name of existing loaded functions.
342+
Can be used to get the importable module path and func name of existing callables.
303343
304344
Args:
305345
obj: Callable retrieved via :func:`getattr` or similar.
@@ -332,16 +372,3 @@ def _get_callable_origin(obj: Callable[..., Any]) -> tuple[str | None, str | Non
332372
continue
333373

334374
return module_path, attr_name
335-
336-
337-
def _is_nvimgcodec_available() -> bool:
338-
"""Return ``True`` if nvimgcodec is available, ``False`` otherwise."""
339-
340-
if not nvimgcodec or not _passes_version_check(NVIMGCODEC_MODULE_NAME, NVIMGCODEC_MIN_VERSION_TUPLE) or not cp:
341-
_logger.debug(f"nvimgcodec (version >= {NVIMGCODEC_MIN_VERSION}) or CuPy missing.")
342-
return False
343-
if not cp.cuda.is_available():
344-
_logger.debug("CUDA device not found.")
345-
return False
346-
347-
return True

0 commit comments

Comments
 (0)