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
5960import inspect
6061import logging
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 :
107109NVIMGCODEC_MIN_VERSION = "0.6"
108110NVIMGCODEC_MIN_VERSION_TUPLE = tuple (int (x ) for x in NVIMGCODEC_MIN_VERSION .split ("." ))
109111NVIMGCODEC_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.
113114SUPPORTED_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
199213def 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
263278def _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
299339def _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