1+ import time
2+ from pathlib import Path
3+
4+ import numpy as np
5+ import pytest
6+ from pydicom import dcmread
7+ from pydicom .data import get_testdata_files
8+
9+ from monai .deploy .operators .decoder_nvimgcodec import (
10+ SUPPORTED_TRANSFER_SYNTAXES ,
11+ _is_nvimgcodec_available ,
12+ register_as_decoder_plugin ,
13+ unregister_as_decoder_plugin ,
14+ )
15+
16+
17+ # The JPEG 8-bit standard allows a maximum 1 bit of difference for each pixel component.
18+ # It is normal for slight differences to exist due to varying internal precision in the
19+ # decoders' inverse Discrete Cosine Transform (IDCT) implementations
20+ # So, need to more closely inspect the pixel values for these transfer syntaxes.
21+ TRANSFER_SYNTAXES_WITH_UNEQUAL_PIXEL_VALUES = [
22+ "1.2.840.10008.1.2.4.50" ,
23+ "1.2.840.10008.1.2.4.91" ,
24+ "1.2.840.10008.1.2.4.203" ,
25+ ]
26+
27+ # Files that cannot be decoded with the default decoders
28+ SKIPPING_DEFAULT_ERRORED_FILES = {
29+ 'UN_sequence.dcm' : '1.2.840.10008.1.2.4.70' ,
30+ 'JPEG-lossy.dcm' : '1.2.840.10008.1.2.4.51' ,
31+ 'JPEG2000-embedded-sequence-delimiter.dcm' : '1.2.840.10008.1.2.4.91' ,
32+ 'emri_small_jpeg_2k_lossless_too_short.dcm' : '1.2.840.10008.1.2.4.90' ,
33+ }
34+
35+ # Files that have unequal pixel values between default and nvimgcodec decoders
36+ SKIPPING_UNEQUAL_PIXEL_FILES = {
37+ 'SC_rgb_jpeg_lossy_gdcm.dcm' : '1.2.840.10008.1.2.4.50' ,
38+ 'SC_rgb_dcmtk_+eb+cr.dcm' : '1.2.840.10008.1.2.4.50' ,
39+ 'SC_rgb_jpeg_dcmtk.dcm' : '1.2.840.10008.1.2.4.50' ,
40+ 'SC_rgb_dcmtk_+eb+cy+n1.dcm' : '1.2.840.10008.1.2.4.50' ,
41+ 'SC_rgb_dcmtk_+eb+cy+s2.dcm' : '1.2.840.10008.1.2.4.50' ,
42+ 'examples_ybr_color.dcm' : '1.2.840.10008.1.2.4.50' ,
43+ 'SC_rgb_dcmtk_+eb+cy+n2.dcm' : '1.2.840.10008.1.2.4.50' ,
44+ 'SC_rgb_dcmtk_+eb+cy+s4.dcm' : '1.2.840.10008.1.2.4.50' ,
45+ 'SC_rgb_jpeg.dcm' : '1.2.840.10008.1.2.4.50' ,
46+ 'SC_rgb_jpeg_app14_dcmd.dcm' : '1.2.840.10008.1.2.4.50' ,
47+ 'SC_jpeg_no_color_transform.dcm' : '1.2.840.10008.1.2.4.50' ,
48+ 'SC_jpeg_no_color_transform_2.dcm' : '1.2.840.10008.1.2.4.50' ,
49+ 'SC_rgb_small_odd_jpeg.dcm' : '1.2.840.10008.1.2.4.50' ,
50+ 'SC_rgb_dcmtk_+eb+cy+np.dcm' : '1.2.840.10008.1.2.4.50' ,
51+ 'color3d_jpeg_baseline.dcm' : '1.2.840.10008.1.2.4.50' ,
52+ 'MR2_J2KI.dcm' : '1.2.840.10008.1.2.4.91' ,
53+ 'RG3_J2KI.dcm' : '1.2.840.10008.1.2.4.91' ,
54+ 'US1_J2KI.dcm' : '1.2.840.10008.1.2.4.91'
55+ }
56+
57+ CONFIRMED_EQUAL_PIXEL_FILES = {
58+ 'JPGExtended.dcm' : '1.2.840.10008.1.2.4.51' ,
59+ 'examples_jpeg2k.dcm' : '1.2.840.10008.1.2.4.90' ,
60+ 'J2K_pixelrep_mismatch.dcm' : '1.2.840.10008.1.2.4.90' ,
61+ 'SC_rgb_gdcm_KY.dcm' : '1.2.840.10008.1.2.4.91' ,
62+ 'GDCMJ2K_TextGBR.dcm' : '1.2.840.10008.1.2.4.90' ,
63+ 'SC_rgb_jpeg_gdcm.dcm' : '1.2.840.10008.1.2.4.70' ,
64+ 'MR_small_jp2klossless.dcm' : '1.2.840.10008.1.2.4.90' ,
65+ 'JPEG2000.dcm' : '1.2.840.10008.1.2.4.91' ,
66+ '693_J2KI.dcm' : '1.2.840.10008.1.2.4.91' ,
67+ '693_J2KR.dcm' : '1.2.840.10008.1.2.4.90' ,
68+ 'bad_sequence.dcm' : '1.2.840.10008.1.2.4.70' ,
69+ 'emri_small_jpeg_2k_lossless.dcm' : '1.2.840.10008.1.2.4.90' ,
70+ 'explicit_VR-UN.dcm' : '1.2.840.10008.1.2.4.90' ,
71+ 'JPEG-LL.dcm' : '1.2.840.10008.1.2.4.70' ,
72+ 'JPGLosslessP14SV1_1s_1f_8b.dcm' : '1.2.840.10008.1.2.4.70' ,
73+ 'MR2_J2KR.dcm' : '1.2.840.10008.1.2.4.90' ,
74+ 'RG1_J2KI.dcm' : '1.2.840.10008.1.2.4.91' ,
75+ 'RG1_J2KR.dcm' : '1.2.840.10008.1.2.4.90' ,
76+ 'RG3_J2KR.dcm' : '1.2.840.10008.1.2.4.90' ,
77+ 'US1_J2KR.dcm' : '1.2.840.10008.1.2.4.90'
78+ }
79+
80+
81+ @pytest .mark .skipif (not _is_nvimgcodec_available (), reason = "nvimgcodec dependencies unavailable" )
82+ def test_nvimgcodec_decoder_matches_default ():
83+ """Ensure nvimgcodec decoder matches default decoding for supported transfer syntaxes."""
84+
85+ test_files = get_testdata_files ("*.dcm" )
86+ baseline_total = 0.0
87+ nvimgcodec_total = 0.0
88+ compared = 0
89+ _rtol = 0.01 # The relative tolerance parameter
90+ _atol = 1.00 # The absolute tolerance parameter
91+
92+ default_errored_files = dict ()
93+ nvimgcodec_errored_files = dict ()
94+ unequal_pixel_files = dict ()
95+ inspected_unequal_files = dict ()
96+ confirmed_equal_pixel_files = dict ()
97+
98+ for path in test_files :
99+ default_errored = False
100+ nvimgcodec_errored = False
101+
102+ try :
103+ dataset = dcmread (path , stop_before_pixels = True , force = True )
104+ transfer_syntax = dataset .file_meta .TransferSyntaxUID
105+ except Exception as e :
106+ print (f"Skipping: error reading DICOM file { path } : { e } " )
107+ continue
108+
109+ if transfer_syntax not in SUPPORTED_TRANSFER_SYNTAXES :
110+ print (f"Skipping: unsupported transfer syntax DICOM file { path } : { transfer_syntax } " )
111+ continue
112+
113+ try :
114+ ds_default = dcmread (path , force = True )
115+ start = time .perf_counter ()
116+ baseline_pixels = ds_default .pixel_array
117+ baseline_total += time .perf_counter () - start
118+ except Exception as e :
119+ # Skip files that cannot be decoded with the default backend
120+ print (f"Skipping: default backends cannot decode DICOM file { path } : { e } " )
121+ default_errored_files [Path (path ).name ] = transfer_syntax
122+ default_errored = True
123+ # Let's see if nvimgcodec can decode it.
124+
125+ # Register the nvimgcodec decoder plugin and unregister it after each use.
126+ register_as_decoder_plugin ()
127+ try :
128+ ds_custom = dcmread (path , force = True )
129+ start = time .perf_counter ()
130+ nv_pixels = ds_custom .pixel_array
131+ nvimgcodec_total += time .perf_counter () - start
132+ except Exception as e :
133+ print (f"Skipping: nvimgcodec cannot decode DICOM file { path } : { e } " )
134+ nvimgcodec_errored_files [Path (path ).name ] = transfer_syntax
135+ nvimgcodec_errored = True
136+ finally :
137+ unregister_as_decoder_plugin ()
138+
139+ if default_errored or nvimgcodec_errored :
140+ print (f"Skipping: either default or nvimgcodec decoder or both failed to decode DICOM file { path } " )
141+ continue
142+
143+ assert baseline_pixels .shape == nv_pixels .shape , f"Shape mismatch for { Path (path ).name } "
144+
145+ if baseline_pixels .dtype != nv_pixels .dtype :
146+ baseline_compare = baseline_pixels .astype (np .float32 )
147+ nv_compare = nv_pixels .astype (np .float32 )
148+ else :
149+ baseline_compare = baseline_pixels
150+ nv_compare = nv_pixels
151+
152+ if not np .allclose (
153+ baseline_compare ,
154+ nv_compare ,
155+ rtol = _rtol ,
156+ atol = _atol ,
157+ ):
158+ if transfer_syntax in TRANSFER_SYNTAXES_WITH_UNEQUAL_PIXEL_VALUES :
159+ diff = baseline_compare .astype (np .float32 ) - nv_compare .astype (np .float32 )
160+ peak_absolute_error = float (np .max (np .abs (diff )))
161+ mean_squared_error = float (np .mean (diff ** 2 ))
162+ inspected_unequal_files [Path (path ).name ] = {
163+ "transfer_syntax" : transfer_syntax ,
164+ "peak_absolute_error" : peak_absolute_error ,
165+ "mean_squared_error" : mean_squared_error ,
166+ }
167+ else :
168+ unequal_pixel_files [Path (path ).name ] = {"transfer_syntax" : transfer_syntax }
169+ else :
170+ confirmed_equal_pixel_files [Path (path ).name ] = transfer_syntax
171+
172+ compared += 1
173+
174+ print (f"Default decoder total time: { baseline_total :.4f} s" )
175+ print (f"nvimgcodec decoder total time: { nvimgcodec_total :.4f} s" )
176+ print (f"Total tested DICOM files: { compared } " )
177+ print (f"Default errored files: { default_errored_files } " )
178+ print (f"nvimgcodec errored files: { nvimgcodec_errored_files } " )
179+ print (f"Unequal files (tolerance: { _rtol } , { _atol } ): { unequal_pixel_files } " )
180+ print (f"Inspected unequal files (tolerance: { _rtol } , { _atol } ): { inspected_unequal_files } " )
181+ print (f"Confirmed tested files: { confirmed_equal_pixel_files } " )
182+
183+ assert compared > 0 , "No compatible DICOM files found for nvimgcodec decoder test."
184+ assert (x in default_errored_files .keys () for x in (nvimgcodec_errored_files .keys ())), "nvimgcodec decoder errored files found."
185+ assert len (unequal_pixel_files ) == 0 , "Unequal files found."
186+ assert len (confirmed_equal_pixel_files ) > 0 , "No files with equal pixel values after decoding with both decoders."
187+
188+ if __name__ == "__main__" :
189+ test_nvimgcodec_decoder_matches_default ()
0 commit comments