@@ -217,6 +217,46 @@ def mask_has_voxels(mask, sitk_module) -> bool:
217217 return bool (sitk_module .GetArrayViewFromImage (mask ).sum () > 0 )
218218
219219
220+ def _extract_original_features (
221+ result : Dict [str , Any ],
222+ * ,
223+ prefix : str ,
224+ include_shape : bool = True ,
225+ include_non_shape : bool = True ,
226+ ) -> Dict [str , Any ]:
227+ features : Dict [str , Any ] = {}
228+ for key , value in result .items ():
229+ skey = str (key )
230+ if not skey .startswith ("original" ):
231+ continue
232+ is_shape = skey .startswith ("original_shape_" )
233+ if (is_shape and not include_shape ) or ((not is_shape ) and not include_non_shape ):
234+ continue
235+ features [f"{ prefix } _{ skey } " ] = value
236+ return features
237+
238+
239+ def _resample_to_reference_if_needed (mask_image , reference_image , sitk_module ):
240+ if (
241+ mask_image .GetSize (),
242+ mask_image .GetSpacing (),
243+ mask_image .GetOrigin (),
244+ mask_image .GetDirection (),
245+ ) == (
246+ reference_image .GetSize (),
247+ reference_image .GetSpacing (),
248+ reference_image .GetOrigin (),
249+ reference_image .GetDirection (),
250+ ):
251+ return mask_image
252+
253+ rs = sitk_module .ResampleImageFilter ()
254+ rs .SetReferenceImage (reference_image )
255+ rs .SetInterpolator (sitk_module .sitkNearestNeighbor )
256+ rs .SetDefaultPixelValue (0 )
257+ return rs .Execute (mask_image )
258+
259+
220260def extract_radiomics_safe (
221261 image_path : str ,
222262 mask_path : Optional [str ],
@@ -235,17 +275,13 @@ def extract_radiomics_safe(
235275
236276 image = sitk_module .ReadImage (image_path )
237277 result = extractor .execute (image , mask_image )
238- features = {
239- f"{ prefix } _{ k } " : v
240- for k , v in result .items ()
241- if str (k ).startswith ("original" )
242- }
278+ features = _extract_original_features (result , prefix = prefix )
243279 return features , None
244280 except Exception as exc :
245281 return {}, f"Error extracting { prefix } features: { exc } "
246282
247283
248- def extract_radiomics_liver_minus_tumor (
284+ def extract_radiomics_organ_minus_tumor (
249285 image_path : str ,
250286 liver_mask_path : Optional [str ],
251287 tumor_mask_path : Optional [str ],
@@ -255,59 +291,109 @@ def extract_radiomics_liver_minus_tumor(
255291 prefix : str = "liver" ,
256292) -> Tuple [Dict [str , Any ], Optional [str ]]:
257293 if not liver_mask_path or not Path (liver_mask_path ).exists ():
258- return {}, "missing liver mask"
294+ return {}, f "missing { prefix } mask"
259295
260296 try :
261297 img = sitk_module .ReadImage (image_path )
262- liver = sitk_module .ReadImage (liver_mask_path )
298+ organ = sitk_module .ReadImage (liver_mask_path )
263299
264- if sitk_module .GetArrayViewFromImage (liver ).sum () == 0 :
265- return {}, "empty liver mask"
300+ if sitk_module .GetArrayViewFromImage (organ ).sum () == 0 :
301+ return {}, f "empty { prefix } mask"
266302
267- liver_bin = sitk_module .Cast (
268- sitk_module .NotEqual (liver , 0 ), sitk_module .sitkUInt8
303+ organ_bin = sitk_module .Cast (
304+ sitk_module .NotEqual (organ , 0 ), sitk_module .sitkUInt8
305+ )
306+ has_tumor = bool (tumor_mask_path and Path (tumor_mask_path ).exists ())
307+
308+ if not has_tumor :
309+ result = extractor .execute (img , organ_bin )
310+ return _extract_original_features (result , prefix = prefix ), None
311+
312+ tumor = sitk_module .ReadImage (tumor_mask_path )
313+ if sitk_module .GetArrayViewFromImage (tumor ).sum () == 0 :
314+ result = extractor .execute (img , organ_bin )
315+ return _extract_original_features (result , prefix = prefix ), None
316+
317+ tumor = _resample_to_reference_if_needed (tumor , organ , sitk_module )
318+ tumor_bin = sitk_module .Cast (sitk_module .NotEqual (tumor , 0 ), sitk_module .sitkUInt8 )
319+ organ_minus_tumor = sitk_module .And (
320+ organ_bin ,
321+ sitk_module .Cast (sitk_module .Not (tumor_bin ), sitk_module .sitkUInt8 ),
269322 )
270323
271- if tumor_mask_path and Path (tumor_mask_path ).exists ():
272- tumor = sitk_module .ReadImage (tumor_mask_path )
273- if sitk_module .GetArrayViewFromImage (tumor ).sum () > 0 :
274- if (
275- tumor .GetSize (),
276- tumor .GetSpacing (),
277- tumor .GetOrigin (),
278- tumor .GetDirection (),
279- ) != (
280- liver .GetSize (),
281- liver .GetSpacing (),
282- liver .GetOrigin (),
283- liver .GetDirection (),
284- ):
285- rs = sitk_module .ResampleImageFilter ()
286- rs .SetReferenceImage (liver )
287- rs .SetInterpolator (sitk_module .sitkNearestNeighbor )
288- rs .SetDefaultPixelValue (0 )
289- tumor = rs .Execute (tumor )
290-
291- tumor_bin = sitk_module .Cast (
292- sitk_module .NotEqual (tumor , 0 ), sitk_module .sitkUInt8
293- )
294- liver_bin = sitk_module .And (
295- liver_bin ,
296- sitk_module .Cast (sitk_module .Not (tumor_bin ), sitk_module .sitkUInt8 ),
297- )
298-
299- if sitk_module .GetArrayViewFromImage (liver_bin ).sum () == 0 :
300- return {}, "liver_minus_tumor mask is empty"
301-
302- result = extractor .execute (img , liver_bin )
303- features = {
304- f"{ prefix } _{ k } " : v
305- for k , v in result .items ()
306- if str (k ).startswith ("original" )
307- }
324+ shape_result = extractor .execute (img , organ_bin )
325+ features = _extract_original_features (
326+ shape_result ,
327+ prefix = prefix ,
328+ include_shape = True ,
329+ include_non_shape = False ,
330+ )
331+
332+ if sitk_module .GetArrayViewFromImage (organ_minus_tumor ).sum () == 0 :
333+ return features , f"{ prefix } _minus_tumor mask is empty"
334+
335+ non_shape_result = extractor .execute (img , organ_minus_tumor )
336+ features .update (
337+ _extract_original_features (
338+ non_shape_result ,
339+ prefix = prefix ,
340+ include_shape = False ,
341+ include_non_shape = True ,
342+ )
343+ )
308344 return features , None
309345 except Exception as exc :
310- return {}, f"Error extracting liver_minus_tumor: { exc } "
346+ return {}, f"Error extracting { prefix } _minus_tumor: { exc } "
347+
348+
349+ def _get_mask_columns (df : pd .DataFrame ) -> list [str ]:
350+ return [col for col in df .columns if col .startswith ("mask_" )]
351+
352+
353+ def _extract_row_features (
354+ row : pd .Series ,
355+ mask_columns : list [str ],
356+ * ,
357+ extractor ,
358+ sitk_module ,
359+ ) -> Tuple [Dict [str , Any ], list [str ]]:
360+ image_path = row .get ("nifti_path" )
361+ features : Dict [str , Any ] = {}
362+ messages : list [str ] = []
363+
364+ if not isinstance (image_path , str ) or not Path (image_path ).exists ():
365+ return {}, [f"CT image path is missing or invalid: { image_path } " ]
366+
367+ mask_columns_set = set (mask_columns )
368+ for mask_col in mask_columns :
369+ prefix = mask_col .replace ("mask_" , "" , 1 )
370+ mask_path = row .get (mask_col )
371+
372+ if prefix .endswith ("_tumor" ):
373+ roi_features , roi_msg = extract_radiomics_safe (
374+ image_path ,
375+ mask_path ,
376+ prefix ,
377+ extractor = extractor ,
378+ sitk_module = sitk_module ,
379+ )
380+ else :
381+ tumor_col = f"{ mask_col } _tumor"
382+ tumor_path = row .get (tumor_col ) if tumor_col in mask_columns_set else None
383+ roi_features , roi_msg = extract_radiomics_organ_minus_tumor (
384+ image_path ,
385+ mask_path ,
386+ tumor_path ,
387+ extractor = extractor ,
388+ sitk_module = sitk_module ,
389+ prefix = prefix ,
390+ )
391+
392+ features .update (roi_features )
393+ if roi_msg :
394+ messages .append (roi_msg )
395+
396+ return features , messages
311397
312398
313399def extract_radiomics_from_dataframe (
@@ -319,51 +405,27 @@ def extract_radiomics_from_dataframe(
319405) -> Tuple [pd .DataFrame , pd .DataFrame ]:
320406 all_features = []
321407 errors = []
408+ mask_columns = _get_mask_columns (df )
322409
323410 iterator = df .iterrows ()
324411 if verbose :
325412 iterator = tqdm (iterator , total = len (df ), desc = "Radiomics" )
326413
327414 for idx , row in iterator :
328- image_path = row .get ("nifti_path" )
329- liver_mask_path = row .get ("mask_liver" ) # row.get("liver_path")
330- tumor_mask_path = row .get ("mask_liver_tumor" ) # row.get("liver_tumor_path")
415+ features , messages = _extract_row_features (
416+ row ,
417+ mask_columns ,
418+ extractor = extractor ,
419+ sitk_module = sitk_module ,
420+ )
331421
332- if not isinstance ( image_path , str ) or not Path ( image_path ). exists () :
422+ if messages and "CT image path is missing or invalid" in messages [ 0 ] :
333423 error_row = row .to_dict ()
334- error_row ["error_message" ] = (
335- f"CT image path is missing or invalid: { image_path } "
336- )
424+ error_row ["error_message" ] = messages [0 ]
337425 errors .append (error_row )
338426 all_features .append ({})
339427 continue
340428
341- features = {}
342- messages = []
343-
344- liver_features , liver_msg = extract_radiomics_liver_minus_tumor (
345- image_path ,
346- liver_mask_path ,
347- tumor_mask_path ,
348- extractor = extractor ,
349- sitk_module = sitk_module ,
350- prefix = "liver" ,
351- )
352- features .update (liver_features )
353- if liver_msg :
354- messages .append (liver_msg )
355-
356- tumor_features , tumor_msg = extract_radiomics_safe (
357- image_path ,
358- tumor_mask_path ,
359- "tumor" ,
360- extractor = extractor ,
361- sitk_module = sitk_module ,
362- )
363- features .update (tumor_features )
364- if tumor_msg :
365- messages .append (tumor_msg )
366-
367429 all_features .append (features )
368430 if not features :
369431 error_row = row .to_dict ()
@@ -422,6 +484,7 @@ def main(args: argparse.Namespace) -> None:
422484 raise KeyError ("column 'nifti_path' missing" )
423485 if not args .skip_filter :
424486 df = filter_df (df )
487+ mask_columns = _get_mask_columns (df )
425488
426489 logger .info ("Extracting radiomics from %d rows" , len (df ))
427490 completed_indices : set [int ] = set ()
@@ -461,45 +524,23 @@ def _checkpoint_write(*, force: bool = False) -> None:
461524 if src_idx in completed_indices :
462525 continue
463526 row = df .loc [idx ]
464- image_path = row .get ("nifti_path" )
465- liver_mask_path = row .get ("mask_liver" )
466- tumor_mask_path = row .get ("mask_liver_tumor" )
467527
468- if not isinstance (image_path , str ) or not Path (image_path ).exists ():
528+ features , messages = _extract_row_features (
529+ row ,
530+ mask_columns ,
531+ extractor = extractor ,
532+ sitk_module = sitk_module ,
533+ )
534+
535+ if messages and "CT image path is missing or invalid" in messages [0 ]:
469536 error_row = row .to_dict ()
470- error_row ["error_message" ] = (
471- f"CT image path is missing or invalid: { image_path } "
472- )
537+ error_row ["error_message" ] = messages [0 ]
473538 errors_by_idx [src_idx ] = error_row
474539 completed_indices .add (src_idx )
475540 ckpt .mark_processed ()
476541 _checkpoint_write (force = False )
477542 continue
478543
479- features = {}
480- messages = []
481- liver_features , liver_msg = extract_radiomics_liver_minus_tumor (
482- image_path ,
483- liver_mask_path ,
484- tumor_mask_path ,
485- extractor = extractor ,
486- sitk_module = sitk_module ,
487- prefix = "liver" ,
488- )
489- features .update (liver_features )
490- if liver_msg :
491- messages .append (liver_msg )
492- tumor_features , tumor_msg = extract_radiomics_safe (
493- image_path ,
494- tumor_mask_path ,
495- "tumor" ,
496- extractor = extractor ,
497- sitk_module = sitk_module ,
498- )
499- features .update (tumor_features )
500- if tumor_msg :
501- messages .append (tumor_msg )
502-
503544 for key , value in features .items ():
504545 df .at [idx , key ] = value
505546 if features :
0 commit comments