44Runs quantitative evaluation metrics for the PET attenuation
55correction challenge.
66
7- Usage (from project root):
8-
9- python -m evaluation_tool.eval \
10- --subject sub-000 \
11- --root data \
12- -all \
13- --pet_unit kBq \
14- --debug
7+ Usage:
8+ python eval.py <subject_path> <pred_pet> <pred_ct> [-all | -specific_metric <name>]
9+
10+ Example:
11+ python eval.py /data/sub-000 /results/pet.nii.gz /results/ct.nii.gz -all
1512"""
1613
1714import argparse
1815import os
19- import json
2016import numpy as np
2117import nibabel as nib
2218
2521 compute_brain_outlier_score ,
2622 compute_organ_bias_from_totalseg ,
2723 compute_tac_bias ,
24+ compute_whole_body_ct_mae ,
2825)
2926
3027
31- # =========================================================
32- # Helper: Simulate 4D PET (for testing only)
33- # =========================================================
34-
35- def expand_to_4d (pet_path , num_frames = 8 ):
36- """
37- Expand 3D PET into 4D by repeating volume.
38- Used only for local testing when dynamic PET
39- is not available.
40- """
41-
42- img = nib .load (pet_path )
43- data = img .get_fdata ()
44-
45- if data .ndim == 4 :
46- return pet_path # already dynamic
47-
48- data_4d = np .stack ([data ] * num_frames , axis = - 1 )
49-
50- temp_path = pet_path .replace (".nii" , "_4d.nii" )
51- nib .save (nib .Nifti1Image (data_4d , img .affine ), temp_path )
52-
53- return temp_path
54-
55-
56- # =========================================================
57- # Main
58- # =========================================================
59-
6028def main ():
6129
6230 parser = argparse .ArgumentParser (
63- description = "PET Evaluation Tool"
31+ description = "PET Attenuation Correction Challenge — Evaluation"
32+ )
33+
34+ parser .add_argument (
35+ "subject_path" ,
36+ help = "Path to subject directory"
6437 )
6538
6639 parser .add_argument (
67- "--subject" ,
68- required = True ,
69- help = "Subject identifier (e.g., sub-000)"
40+ "pred_pet" ,
41+ help = "Path to predicted PET NIfTI"
7042 )
7143
7244 parser .add_argument (
73- "--root" ,
74- required = True ,
75- help = "Root data directory"
45+ "pred_ct" ,
46+ help = "Path to predicted CT NIfTI"
7647 )
7748
7849 parser .add_argument (
@@ -87,75 +58,26 @@ def main():
8758 "whole_body_mae" ,
8859 "brain_outlier" ,
8960 "organ_bias" ,
90- "tac_bias"
61+ "tac_bias" ,
62+ "ct_mae" ,
9163 ],
9264 help = "Run specific metric only"
9365 )
9466
95- parser .add_argument (
96- "--pet_unit" ,
97- default = "kBq" ,
98- choices = ["kBq" , "Bq" ],
99- help = "Unit of PET images (default: kBq)"
100- )
101-
102- parser .add_argument (
103- "--test_4d" ,
104- action = "store_true" ,
105- help = "Simulate dynamic PET by repeating 3D volume"
106- )
107-
108- parser .add_argument (
109- "--debug" ,
110- action = "store_true" ,
111- help = "Enable debug output (SUV sanity check)"
112- )
113-
11467 args = parser .parse_args ()
11568
116- subject_path = os .path .join (args .root , args .subject )
117-
118- features_path = os .path .join (subject_path , "features" )
119- labels_path = os .path .join (subject_path , "labels" )
120-
121- # -----------------------------------------------------
122- # Define file paths (adjust if naming changes)
123- # -----------------------------------------------------
124-
125- pred_pet = os .path .join (
126- features_path ,
127- f"{ args .subject } _ses-quadra_trc-18FFDG_rec-nacstatOSEM_pet.nii.gz"
128- )
129-
130- gt_pet = os .path .join (
131- labels_path ,
132- f"{ args .subject } _ses-quadra_trc-18FFDG_rec-acstatOSEM_pet.nii.gz"
133- )
134-
135- ts_body = os .path .join (
136- labels_path ,
137- f"{ args .subject } _ses-quadra_acq-LOWDOSE_ce-none_rec-ac_seg-body_space-individual_dseg.nii.gz"
138- )
139-
140- ts_total = os .path .join (
141- labels_path ,
142- f"{ args .subject } _ses-quadra_acq-LOWDOSE_ce-none_rec-ac_seg-total_space-individual_dseg.nii.gz"
143- )
144-
145- synthseg = os .path .join (
146- labels_path ,
147- f"{ args .subject } _ses-vida_task-rest_acq-MPRAGE_seg-synthsegparc_space-individual_dseg.nii.gz"
148- )
69+ subject_path = args .subject_path
70+ ct_label_dir = os .path .join (subject_path , "ct-label" )
71+ pet_label_dir = os .path .join (subject_path , "pet-label" )
72+ features_dir = os .path .join (subject_path , "features" )
14973
150- meta_json = os .path .join (features_path , "constants.json" )
151-
152- # -----------------------------------------------------
153- # Optionally simulate dynamic PET
154- # -----------------------------------------------------
155-
156- if args .test_4d :
157- pred_pet = expand_to_4d (pred_pet )
158- gt_pet = expand_to_4d (gt_pet )
74+ gt_pet = os .path .join (pet_label_dir , "acpet.nii.gz" )
75+ gt_ct = os .path .join (ct_label_dir , "ct.nii.gz" )
76+ body_seg_pet = os .path .join (pet_label_dir , "body_seg.nii.gz" )
77+ organ_seg_pet = os .path .join (pet_label_dir , "organ_seg.nii.gz" )
78+ body_seg_ct = os .path .join (ct_label_dir , "body_seg.nii.gz" )
79+ synthseg = os .path .join (pet_label_dir , "brain_seg.nii.gz" )
80+ meta_json = os .path .join (features_dir , "metadata.json" )
15981
16082 results = {}
16183
@@ -166,13 +88,11 @@ def main():
16688 if args .all or args .specific_metric == "whole_body_mae" :
16789
16890 results ["Whole-body SUV MAE" ] = compute_whole_body_suv_mae (
169- pred_pet_path = pred_pet ,
91+ pred_pet_path = args . pred_pet ,
17092 gt_pet_path = gt_pet ,
171- body_mask_path = ts_body ,
172- liver_mask_path = ts_total ,
93+ body_mask_path = body_seg_pet ,
94+ liver_mask_path = organ_seg_pet ,
17395 json_path = meta_json ,
174- pet_unit = args .pet_unit ,
175- debug = args .debug
17696 )
17797
17898 # =====================================================
@@ -182,7 +102,7 @@ def main():
182102 if args .all or args .specific_metric == "brain_outlier" :
183103
184104 results ["Brain Outlier Score" ] = compute_brain_outlier_score (
185- pred_paths = [pred_pet ],
105+ pred_paths = [args . pred_pet ],
186106 gt_paths = [gt_pet ],
187107 brain_mask_paths = [synthseg ]
188108 )
@@ -205,12 +125,11 @@ def main():
205125 }
206126
207127 results ["Organ Bias" ] = compute_organ_bias_from_totalseg (
208- pred_path = pred_pet ,
128+ pred_path = args . pred_pet ,
209129 gt_path = gt_pet ,
210- totalseg_path = ts_total ,
130+ totalseg_path = organ_seg_pet ,
211131 organ_label_dict = organ_labels ,
212132 json_path = meta_json ,
213- pet_unit = args .pet_unit
214133 )
215134
216135 # =====================================================
@@ -219,29 +138,41 @@ def main():
219138
220139 if args .all or args .specific_metric == "tac_bias" :
221140
222- pet_data = nib .load (pred_pet ).get_fdata ()
141+ pet_data = nib .load (args . pred_pet ).get_fdata ()
223142
224143 if pet_data .ndim != 4 :
225144 print ("TAC Bias skipped: PET is not dynamic (4D)." )
226145 else :
227146 frame_durations = np .array ([4.0 ] * pet_data .shape [- 1 ])
228147
229148 results ["TAC Bias" ] = compute_tac_bias (
230- pred_path = pred_pet ,
149+ pred_path = args . pred_pet ,
231150 gt_path = gt_pet ,
232- totalseg_path = ts_total ,
151+ totalseg_path = organ_seg_pet ,
233152 synthseg_path = synthseg ,
234153 frame_durations = frame_durations ,
235154 aorta_label = 52 ,
236155 brain_label_ids = [3 , 42 , 10 , 49 , 8 , 47 ]
237156 )
238157
158+ # =====================================================
159+ # 5. CT MAE
160+ # =====================================================
161+
162+ if args .all or args .specific_metric == "ct_mae" :
163+
164+ results ["CT MAE" ] = compute_whole_body_ct_mae (
165+ pred_ct_path = args .pred_ct ,
166+ gt_ct_path = gt_ct ,
167+ body_mask_path = body_seg_ct ,
168+ )
169+
239170 # =====================================================
240171 # Print Results
241172 # =====================================================
242173
243174 print ("\n ================ Evaluation Results ================" )
244- print (f"Subject: { args . subject } " )
175+ print (f"Subject: { os . path . basename ( subject_path ) } " )
245176 print ("----------------------------------------------------" )
246177
247178 if not results :
@@ -254,4 +185,4 @@ def main():
254185
255186
256187if __name__ == "__main__" :
257- main ()
188+ main ()
0 commit comments