3333class TestDicomSegmentation (unittest .TestCase ):
3434 """
3535 Test direct MONAI Label inference on DICOM series without server.
36-
36+
3737 This test demonstrates serverless usage of MONAILabel for DICOM segmentation,
3838 loading DICOM series from test data directories and running inference directly
3939 through the app instance.
4040 """
41-
41+
4242 app = None
4343 base_dir = os .path .realpath (os .path .dirname (os .path .dirname (os .path .dirname (os .path .dirname (__file__ )))))
4444 data_dir = os .path .join (base_dir , "tests" , "data" )
45-
45+
4646 app_dir = os .path .join (base_dir , "sample-apps" , "radiology" )
4747 studies = os .path .join (data_dir , "dataset" , "local" , "spleen" )
48-
48+
4949 # DICOM test data directories
5050 dicomweb_dir = os .path .join (data_dir , "dataset" , "dicomweb" )
5151 dicomweb_htj2k_dir = os .path .join (data_dir , "dataset" , "dicomweb_htj2k" )
52-
52+
5353 # Specific DICOM series for testing
5454 dicomweb_series = os .path .join (
55- data_dir ,
56- "dataset" ,
57- "dicomweb" ,
55+ data_dir ,
56+ "dataset" ,
57+ "dicomweb" ,
5858 "e7567e0a064f0c334226a0658de23afd" ,
59- "1.2.826.0.1.3680043.8.274.1.1.8323329.686521.1629744176.620266"
59+ "1.2.826.0.1.3680043.8.274.1.1.8323329.686521.1629744176.620266" ,
6060 )
6161 dicomweb_htj2k_series = os .path .join (
6262 data_dir ,
6363 "dataset" ,
6464 "dicomweb_htj2k" ,
6565 "e7567e0a064f0c334226a0658de23afd" ,
66- "1.2.826.0.1.3680043.8.274.1.1.8323329.686521.1629744176.620266"
66+ "1.2.826.0.1.3680043.8.274.1.1.8323329.686521.1629744176.620266" ,
6767 )
68-
68+
6969 @classmethod
7070 def setUpClass (cls ) -> None :
7171 """Initialize MONAI Label app for direct usage without server."""
7272 settings .MONAI_LABEL_APP_DIR = cls .app_dir
7373 settings .MONAI_LABEL_STUDIES = cls .studies
7474 settings .MONAI_LABEL_DATASTORE_AUTO_RELOAD = False
75-
75+
7676 if torch .cuda .is_available ():
7777 logger .info (f"Initializing MONAI Label app from: { cls .app_dir } " )
7878 logger .info (f"Studies directory: { cls .studies } " )
79-
79+
8080 cls .app : MONAILabelApp = app_instance (
8181 app_dir = cls .app_dir ,
8282 studies = cls .studies ,
@@ -85,28 +85,28 @@ def setUpClass(cls) -> None:
8585 "models" : "segmentation_spleen" ,
8686 },
8787 )
88-
88+
8989 logger .info ("App initialized successfully" )
90-
90+
9191 @classmethod
9292 def tearDownClass (cls ) -> None :
9393 """Clean up after tests."""
9494 pass
95-
95+
9696 def _run_inference (self , image_path : str , model_name : str = "segmentation_spleen" ) -> tuple :
9797 """
9898 Run segmentation inference on an image (DICOM series directory or NIfTI file).
99-
99+
100100 Args:
101101 image_path: Path to DICOM series directory or NIfTI file
102102 model_name: Name of the segmentation model to use
103-
103+
104104 Returns:
105105 Tuple of (label_data, label_json, inference_time)
106106 """
107107 logger .info (f"Running inference on: { image_path } " )
108108 logger .info (f"Model: { model_name } " )
109-
109+
110110 # Prepare inference request
111111 request = {
112112 "model" : model_name ,
@@ -115,202 +115,202 @@ def _run_inference(self, image_path: str, model_name: str = "segmentation_spleen
115115 "result_extension" : ".nii.gz" , # Force NIfTI output format
116116 "result_dtype" : "uint8" , # Set output data type
117117 }
118-
118+
119119 # Get the inference task directly
120120 task = self .app ._infers [model_name ]
121-
121+
122122 # Run inference
123123 inference_start = time .time ()
124124 label_data , label_json = task (request )
125125 inference_time = time .time () - inference_start
126-
126+
127127 logger .info (f"Inference completed in { inference_time :.3f} seconds" )
128-
128+
129129 return label_data , label_json , inference_time
130-
130+
131131 def _validate_segmentation_output (self , label_data , label_json ):
132132 """
133133 Validate that the segmentation output is correct.
134-
134+
135135 Args:
136136 label_data: The segmentation result (file path or numpy array)
137137 label_json: Metadata about the segmentation
138138 """
139139 self .assertIsNotNone (label_data , "Label data should not be None" )
140140 self .assertIsNotNone (label_json , "Label JSON should not be None" )
141-
141+
142142 # Check if it's a file path or numpy array
143143 if isinstance (label_data , str ):
144144 self .assertTrue (os .path .exists (label_data ), f"Output file should exist: { label_data } " )
145145 logger .info (f"Segmentation saved to: { label_data } " )
146-
146+
147147 # Try to load and verify the file
148148 try :
149149 import nibabel as nib
150+
150151 nii = nib .load (label_data )
151152 array = nii .get_fdata ()
152153 self .assertGreater (array .size , 0 , "Segmentation array should not be empty" )
153154 logger .info (f"Segmentation shape: { array .shape } , dtype: { array .dtype } " )
154155 logger .info (f"Unique labels: { np .unique (array )} " )
155156 except Exception as e :
156157 logger .warning (f"Could not load segmentation file: { e } " )
157-
158+
158159 elif isinstance (label_data , np .ndarray ):
159160 self .assertGreater (label_data .size , 0 , "Segmentation array should not be empty" )
160161 logger .info (f"Segmentation shape: { label_data .shape } , dtype: { label_data .dtype } " )
161162 logger .info (f"Unique labels: { np .unique (label_data )} " )
162163 else :
163164 self .fail (f"Unexpected label data type: { type (label_data )} " )
164-
165+
165166 # Validate metadata
166167 self .assertIsInstance (label_json , dict , "Label JSON should be a dictionary" )
167168 logger .info (f"Label metadata keys: { list (label_json .keys ())} " )
168-
169+
169170 def test_01_app_initialized (self ):
170171 """Test that the app is properly initialized."""
171172 if not torch .cuda .is_available ():
172173 self .skipTest ("CUDA not available" )
173-
174+
174175 self .assertIsNotNone (self .app , "App should be initialized" )
175176 self .assertIn ("segmentation_spleen" , self .app ._infers , "segmentation_spleen model should be available" )
176177 logger .info (f"Available models: { list (self .app ._infers .keys ())} " )
177-
178+
178179 def test_02_dicom_inference_dicomweb (self ):
179180 """Test inference on DICOM series from dicomweb directory."""
180181 if not torch .cuda .is_available ():
181182 self .skipTest ("CUDA not available" )
182-
183+
183184 if not self .app :
184185 self .skipTest ("App not initialized" )
185-
186+
186187 # Use specific DICOM series
187188 if not os .path .exists (self .dicomweb_series ):
188189 self .skipTest (f"DICOM series not found: { self .dicomweb_series } " )
189-
190+
190191 logger .info (f"Testing on DICOM series: { self .dicomweb_series } " )
191-
192+
192193 # Run inference
193194 label_data , label_json , inference_time = self ._run_inference (self .dicomweb_series )
194-
195+
195196 # Validate output
196197 self ._validate_segmentation_output (label_data , label_json )
197-
198+
198199 # Performance check
199200 self .assertLess (inference_time , 60.0 , "Inference should complete within 60 seconds" )
200201 logger .info (f"✓ DICOM inference test passed (dicomweb) in { inference_time :.3f} s" )
201-
202+
202203 def test_03_dicom_inference_dicomweb_htj2k (self ):
203204 """Test inference on DICOM series from dicomweb_htj2k directory (HTJ2K compressed)."""
204205 if not torch .cuda .is_available ():
205206 self .skipTest ("CUDA not available" )
206-
207+
207208 if not self .app :
208209 self .skipTest ("App not initialized" )
209-
210+
210211 # Use specific HTJ2K DICOM series
211212 if not os .path .exists (self .dicomweb_htj2k_series ):
212213 self .skipTest (f"HTJ2K DICOM series not found: { self .dicomweb_htj2k_series } " )
213-
214+
214215 logger .info (f"Testing on HTJ2K compressed DICOM series: { self .dicomweb_htj2k_series } " )
215-
216+
216217 # Run inference
217218 label_data , label_json , inference_time = self ._run_inference (self .dicomweb_htj2k_series )
218-
219+
219220 # Validate output
220221 self ._validate_segmentation_output (label_data , label_json )
221-
222+
222223 # Performance check
223224 self .assertLess (inference_time , 60.0 , "Inference should complete within 60 seconds" )
224225 logger .info (f"✓ DICOM inference test passed (HTJ2K) in { inference_time :.3f} s" )
225-
226+
226227 def test_04_dicom_inference_both_formats (self ):
227228 """Test inference on both standard and HTJ2K compressed DICOM series."""
228229 if not torch .cuda .is_available ():
229230 self .skipTest ("CUDA not available" )
230-
231+
231232 if not self .app :
232233 self .skipTest ("App not initialized" )
233-
234+
234235 # Test both series types
235236 test_series = [
236237 ("Standard DICOM" , self .dicomweb_series ),
237238 ("HTJ2K DICOM" , self .dicomweb_htj2k_series ),
238239 ]
239-
240+
240241 total_time = 0
241242 successful = 0
242-
243+
243244 for series_type , dicom_dir in test_series :
244245 if not os .path .exists (dicom_dir ):
245246 logger .warning (f"Skipping { series_type } : { dicom_dir } not found" )
246247 continue
247-
248+
248249 logger .info (f"\n Processing { series_type } : { dicom_dir } " )
249-
250+
250251 try :
251252 label_data , label_json , inference_time = self ._run_inference (dicom_dir )
252253 self ._validate_segmentation_output (label_data , label_json )
253-
254+
254255 total_time += inference_time
255256 successful += 1
256257 logger .info (f"✓ { series_type } success in { inference_time :.3f} s" )
257-
258+
258259 except Exception as e :
259260 logger .error (f"✗ { series_type } failed: { e } " , exc_info = True )
260-
261+
261262 logger .info (f"\n { '=' * 60 } " )
262263 logger .info (f"Summary: { successful } /{ len (test_series )} series processed successfully" )
263264 if successful > 0 :
264265 logger .info (f"Total inference time: { total_time :.3f} s" )
265266 logger .info (f"Average time per series: { total_time / successful :.3f} s" )
266267 logger .info (f"{ '=' * 60 } " )
267-
268+
268269 # At least one should succeed
269270 self .assertGreater (successful , 0 , "At least one DICOM series should be processed successfully" )
270-
271+
271272 def test_05_compare_dicom_vs_nifti (self ):
272273 """Compare inference results between DICOM series and pre-converted NIfTI files."""
273274 if not torch .cuda .is_available ():
274275 self .skipTest ("CUDA not available" )
275-
276+
276277 if not self .app :
277278 self .skipTest ("App not initialized" )
278-
279+
279280 # Use specific DICOM series and its NIfTI equivalent
280281 dicom_dir = self .dicomweb_series
281282 nifti_file = f"{ dicom_dir } .nii.gz"
282-
283+
283284 if not os .path .exists (dicom_dir ):
284285 self .skipTest (f"DICOM series not found: { dicom_dir } " )
285-
286+
286287 if not os .path .exists (nifti_file ):
287288 self .skipTest (f"Corresponding NIfTI file not found: { nifti_file } " )
288-
289+
289290 logger .info (f"Comparing DICOM vs NIfTI inference:" )
290291 logger .info (f" DICOM: { dicom_dir } " )
291292 logger .info (f" NIfTI: { nifti_file } " )
292-
293+
293294 # Run inference on DICOM
294295 logger .info ("\n --- Running inference on DICOM series ---" )
295296 dicom_label , dicom_json , dicom_time = self ._run_inference (dicom_dir )
296-
297+
297298 # Run inference on NIfTI
298299 logger .info ("\n --- Running inference on NIfTI file ---" )
299300 nifti_label , nifti_json , nifti_time = self ._run_inference (nifti_file )
300-
301+
301302 # Validate both
302303 self ._validate_segmentation_output (dicom_label , dicom_json )
303304 self ._validate_segmentation_output (nifti_label , nifti_json )
304-
305+
305306 logger .info (f"\n Performance comparison:" )
306307 logger .info (f" DICOM inference time: { dicom_time :.3f} s" )
307308 logger .info (f" NIfTI inference time: { nifti_time :.3f} s" )
308-
309+
309310 # Both should complete successfully
310311 self .assertIsNotNone (dicom_label , "DICOM inference should succeed" )
311312 self .assertIsNotNone (nifti_label , "NIfTI inference should succeed" )
312313
313314
314315if __name__ == "__main__" :
315316 unittest .main ()
316-
0 commit comments