Skip to content

Commit 6c341ad

Browse files
recon done
1 parent 0f201f1 commit 6c341ad

18 files changed

Lines changed: 326 additions & 226 deletions

recon/Dockerfile

Lines changed: 0 additions & 21 deletions
This file was deleted.
-176 Bytes
Binary file not shown.
-2.46 KB
Binary file not shown.

recon/docker-compose.yaml

Lines changed: 0 additions & 5 deletions
This file was deleted.

recon/postfilter_4mm.par

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/evaluation/eval.py

Lines changed: 52 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44
Runs quantitative evaluation metrics for the PET attenuation
55
correction 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

1714
import argparse
1815
import os
19-
import json
2016
import numpy as np
2117
import nibabel as nib
2218

@@ -25,54 +21,29 @@
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-
6028
def 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

256187
if __name__ == "__main__":
257-
main()
188+
main()

src/evaluation/metrics/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
from .whole_body_mae import compute_whole_body_suv_mae
88
from .brain_outlier import compute_brain_outlier_score
99
from .organ_bias import compute_organ_bias_from_totalseg
10-
from .tac_bias import compute_tac_bias
10+
from .tac_bias import compute_tac_bias
11+
from .ct_mae import compute_whole_body_ct_mae

src/evaluation/metrics/ct_mae.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Whole-body CT MAE metric.
3+
"""
4+
5+
import numpy as np
6+
import nibabel as nib
7+
8+
9+
def compute_whole_body_ct_mae(pred_ct_path, gt_ct_path, body_mask_path):
10+
"""
11+
Compute voxel-wise MAE in HU between predicted and ground-truth CT,
12+
restricted to the body mask.
13+
"""
14+
15+
pred = nib.load(pred_ct_path).get_fdata()
16+
gt = nib.load(gt_ct_path).get_fdata()
17+
body_mask = nib.load(body_mask_path).get_fdata() > 0
18+
19+
return np.mean(np.abs(pred - gt)[body_mask])

src/evaluation/metrics/organ_bias.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
Organ bias metric (SUV-mean MARE).
33
"""
44

5+
import json
56
import numpy as np
67
import nibabel as nib
7-
from .suv_utils import load_pet_as_suv
8+
from .suv_utils import compute_suv_factor
89

910

1011
def compute_organ_bias_from_totalseg(
@@ -13,16 +14,23 @@ def compute_organ_bias_from_totalseg(
1314
totalseg_path,
1415
organ_label_dict,
1516
json_path,
16-
pet_unit="kBq",
1717
epsilon=1e-6,
1818
):
1919
"""
2020
Compute mean absolute relative error (MARE)
2121
of SUV-mean across specified organs.
2222
"""
2323

24-
pred = load_pet_as_suv(pred_path, json_path, pet_unit)
25-
gt = load_pet_as_suv(gt_path, json_path, pet_unit)
24+
with open(json_path, "r") as f:
25+
meta = json.load(f)
26+
27+
weight_kg = meta["weight"]
28+
29+
gt_img = nib.load(gt_path)
30+
suv_factor = compute_suv_factor(weight_kg, gt_img)
31+
32+
pred = nib.load(pred_path).get_fdata() * suv_factor
33+
gt = gt_img.get_fdata() * suv_factor
2634

2735
seg = nib.load(totalseg_path).get_fdata()
2836

@@ -42,4 +50,4 @@ def compute_organ_bias_from_totalseg(
4250
if not mare_values:
4351
raise ValueError("No valid organs found.")
4452

45-
return np.mean(mare_values)
53+
return np.mean(mare_values)

0 commit comments

Comments
 (0)