Skip to content

Commit fbcf8b2

Browse files
Merge pull request #11 from ChristianHinge/dev/documentation
Change hu_to_mu formula (slight bug)
2 parents 3fff36a + 4c0ecfb commit fbcf8b2

10 files changed

Lines changed: 724 additions & 31 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.old/
22
conversion/
3+
docs_scripts/
34
CLAUDE.md
45
outputs*/
56
docs_scripts/
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[WIP]
2+
13
# PET Imaging Background
24

35
This guide is for participants who are familiar with medical imaging (MRI, CT) but have not worked extensively with PET. It covers the concepts you need to understand the challenge task, the data, and the evaluation metrics.

docs/reconstruction.md

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
1-
# PET Reconstruction Pipeline
1+
# PET Reconstruction
22

3-
The reconstruction pipeline (`src/recon/`) converts a pseudo-CT into an attenuation-corrected PET image using [STIR](http://stir.sourceforge.net/) (Software for Tomographic Image Reconstruction). You do **not** need to understand or modify the pipeline to participate — it is run by the challenge organisers on your submissions. This guide is for participants who want to run it locally for closed-loop training or debugging.
3+
You do **not** need to understand reconstruction or attenuation correction to participate, however, having an intuition of the first reconstruction steps can be a **significant advantage** when designing your pseudo-CT algorithm and loss function.
4+
5+
## Introduction
6+
In PET, the radioactive tracer (FDG, a glucose analogue) emits positrons that immediately annihilate with nearby electrons, releasing two 511 keV photons traveling in exactly opposite directions. The detector rings record both photons simultaneously — a *coincidence event* — telling us which straight line through the patient the emission occurred on, but not where along that line<sup>1</sup>. Tallying all such lines produces a **sinogram**, the raw projection data from which the activity image is reconstructed. However, some photons are absorbed (attenuated) by tissue before reaching the detectors — an effect that is more pronounced for deep structures and dense bone. **Attenuation correction** compensates for this, and it is what your pseudo-CT enables.
7+
8+
The PET reconstruction algorithm is titled Ordinary Poisson Ordered Subsets Expectation Maximization (OP-OSEM). OP-OSEM is simply a maximum likelihood expectation maximization algorithm where the observed data (the sinograms) are partioned and processed in chunks (subsets) to dramatically accelerate the reconstruction speed. The reconstruction for BIC-MAC performs OP-OPSEM for `5 subsets x 4 iterations = 20 subiterations`. Three sinograms are used in OP-OSEM of which the last two must be attenuation corrected: `prompts_rd85[.s/.hs]`, `add_nac_rd85[.s/.hs]`, and `mult_nac_rd85[.s/.hs]` <sup>2</sup>
9+
10+
The reconstruction pipeline for BIC-MAC (`src/recon/`) uses [STIR](http://stir.sourceforge.net/) (Software for Tomographic Image Reconstruction) to perform reconstruction. (see [Further Reading](#further-reading) for a primer)
11+
12+
> <sup>1</sup> A simplification. Modern scanners like the Siemens Quadra used for BIC-MAC can detect the tiny time of flight (TOF) difference between the arriving photons and infer their origin (roughly) on the LOR. Consequently, TOF-enabled sinograms have an extra dimension.
13+
>
14+
> <sup>2</sup> RD85 means maximum ring difference of 85 and defines how oblique the LORs are allowed to be. The Siemens Quadra allows up to RD322, but RD85 was chosen to reduce the sinogram size.
415
516
---
617

7-
## Pipeline Steps
18+
## BIC-MAC reconstruction steps
19+
Given a CT (ground-truth or pseudo-CT) and the subject's sinogram data (`recon/`), the the reconstruction pipeline exectutes the following steps to arrive at a reconstructed attenuation-corrected PET NIfTI image:
20+
21+
1. **Superimpose bed pixelated face** - The pseudo-CT face is replaced by a pre-saved pixelated face. Likewise, everything outside a ~1cm rim (pillows, bed, hair, air) is replaced by the ground truth image (see `ct_face_and_bed.nii.gz` and `face_and_bed_mask.nii.gz`). Consequently, the pseudo-CT algorithm will not benefit from trying to predict these areas. The `prediction_mask.nii.gz` under `ct-label` is the *inverse* mask of `face_and_bed_mask.nii.gz` and may be used to restrict training to the relevant body region. You can inspect the face-swapped intermediate file (`intermediates/ct_face_swapped.nii.gz`)
22+
23+
2. **HU → μ-map** — The pseudo-CT is a volume of Hounsfield units (HU), a relative X-ray density scale where air = −1000 and water = 0. PET reconstruction needs instead the linear attenuation coefficient μ (cm⁻¹) at the 511 keV photon energy of PET annihilation events. The conversion uses a bilinear model (Carney et al. 2006): a steep linear segment maps soft tissue (HU ≤ 47, dominated by water) and a flatter segment maps dense materials like bone (HU > 47). This means that errors in HU are not equally costly in soft tissue and in bone when converting to a mumap. You can inspect the mu_map intermediate file (`intermediates/mu_map.nii.gz`).
24+
25+
3. **Smooth μ-map** — A 4 mm FWHM Gaussian blur is applied to the μ-map before any sinogram operations. This is standard clinical practice to reduce the effect of CT noise and slight patient movement. Consequently, very fine structural detail in your pseudo-CT may be blurred away before it ever influences the PET sinogram, and by extension, the PET-based metrics. You can inspect the smoothed mu_map intermediate file (`intermediates/mumap_smoothed.nii.gz`)
26+
27+
4. **Resample to STIR** — Resamples the μ-map onto STIR's z-axis grid (ring spacing 3.29 mm), snapping the origin to the STIR coordinate system. A technical prerequisite for STIR's forward projection. The intermediate files are (`intermediates/mumap_stir[.hv/.ahv/.v]`)
28+
29+
5. **Compute ACF sinogram** — The μ-map is *forward projected* along every line of response (LOR) in the PET scanner geometry, computing the total integrated attenuation each annihilation photon pair experiences along that path. The result is the **attenuation correction factor (ACF)** sinogram, which has the same shape as the other PET sinograms, except from the fact that it lacks a TOF-dimension. The intermediate files are(`intermediates/acf[.hs/.v]`)
30+
31+
6.–7. **Apply ACF to sinograms** — The ACF sinogram is multiplied into both the *multiplicative* sinogram and the *additive* sinogram. This encodes the predicted attenuation into the reconstruction inputs. The intermediate files are (`intermediates/add[.hs/.s]` and `intermediates/mult[.hs/.s]`)
832

9-
Given a CT (ground-truth or pseudo-CT) and the subject's sinogram data (`recon/`), the pipeline produces a reconstructed PET NIfTI:
33+
8. **OSEM reconstruction** — OP-OSEM is run for 20 subiterations (5 subsets, 4 iterations). The result is smoothed by a 4 mm FWHM Gaussian post-filter to reduce noise. The intermediate files are (`intermediates/pet_20[.ahv/.hv/.v]`).
1034

11-
1. **Validate CT** — checks shape, affine, and HU range against the ground-truth CT
12-
2. **Swap face and bed** — replaces the face and scanner bed region with ground-truth CT values (so face/bed prediction is not penalised)
13-
3. **HU → μ-map** — converts Hounsfield units to linear attenuation coefficients at 511 keV using the Carney et al. (2006) bilinear model at 120 kVp
14-
4. **Smooth μ-map** — applies a 4 mm FWHM Gaussian to match scanner resolution
15-
5. **Resample to STIR** — resamples the μ-map onto the STIR z-axis grid (ring spacing 3.29114 mm)
16-
6. **Compute ACF sinogram** — forward-projects the μ-map to produce the attenuation correction factor (ACF) sinogram
17-
7. **Apply ACF to additive sinogram** — multiplies ACF into the scatter+randoms estimate
18-
8. **Apply ACF to multiplicative sinogram** — multiplies ACF into the detector normalisation sinogram
19-
9. **OSEM reconstruction** — reconstructs PET via ordered-subsets expectation maximisation with a 4 mm post-filter
20-
10. **Convert to NIfTI** — writes the reconstructed PET with the correct bed/gantry offset origin
35+
9. **Convert to NIfTI** — Writes the reconstructed PET volume as a NIfTI file with the correct origin, accounting for the scanner bed position and gantry offset stored in `recon/offset.json`. The final output is `pet.nii.gz`
2136

22-
Intermediate outputs (μ-map, ACF sinogram, STIR-format files) are written to `output_dir/intermediates/`. The pipeline skips steps whose outputs already exist, so it resumes automatically from a partial run.
37+
Intermediate outputs are written to `output_dir/intermediates/`. The pipeline skips steps whose outputs already exist, so it resumes automatically from a partial run.
2338

2439
---
2540

docs/rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This document describes the rules governing participation in the Big Cross-Modal
88

99
**Additional training data is allowed.** as long as it was released, publicly available and accessible to all participants without restrictions prior the start of the challenge (April 1st). Private datasets are NOT allowed. The use of public datasets must be disclosed in the submitted methodology paper. Please see [tips-and-faq.md](tips-and-faq.md) for suggested public datasets. If you are unsure whether a particular dataset fulfills the above criteria, please send an email to bic-mac-challenge@outlook.com.
1010

11-
**Pretrained networks are allowed** if they were publicly available and accessible to all participants without restrictions (e.g. on GitHub, Huggingface, Zenodo, or a comparable platform) *prior to the start of the challenge* (April 1st) . You may use these as initialization, feature extractors or preprocessing, but the fine-tuning data must be limited to the provided dataset.
11+
**Pretrained networks are allowed** if they were publicly available and accessible to all participants without restrictions (e.g. on GitHub, Huggingface, Zenodo, or a comparable platform) *prior to the start of the challenge* (April 1st). You may use these as initialization, feature extractors or preprocessing, but the fine-tuning data must be limited to the provided dataset.
1212

1313
**Any preprocessing, manual labelling or augmentation of the BIC-MAC dataset and public datasets is allowed**, as long as it does not conflict with the other rules or the BIC-MAC Data User Agreement.
1414

docs/tips-and-faq.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
## Suggested public datasets
21

2+
# Tips
33

4+
We strongly suggest you start by reading [data-background.md](data-background.md) and [reconstruction.md](reconstruction.md).
5+
6+
7+
### Suggested public datasets
8+
We recommend the following public datasets if you wish to perform pretraining etc.
49
### PET/CT
510
> Note that all PET images in the following datasets are attenuation corrected (usually by the accompanying CT), which means that the PET may encode some of the CT information.
611
- [Vienna QUADRA_HC](https://zenodo.org/records/16588733) 96 whole-body 18F-FDG PET/CT studies from 48 participants. Like BIC-MAC, the PET/CT is acquired on a Siemens Biograph Vision Quadra and the participants are healthy controls. [citation](https://www.nature.com/articles/s41597-025-05997-4)
@@ -47,3 +52,90 @@
4752

4853
### Chest X-ray (Topogram-like)
4954
- [CheXpert](https://stanfordmlgroup.github.io/competitions/chexpert/) 224,316 chest radiographs of 65,240 patients with 14 pathology labels. [citation](https://arxiv.org/abs/1901.07031)
55+
56+
---
57+
58+
59+
# FAQ
60+
61+
62+
**Do I need to understand PET reconstruction to participate?**
63+
64+
No. You only need to predict a pseudo-CT from the input features. The reconstruction pipeline is provided and run for you. See [reconstruction.md](reconstruction.md) if you want to understand what the reconstruction does and why CT quality matters for PET accuracy.
65+
66+
67+
**What data can my pseudo-CT model use as input?**
68+
69+
Any and all files under the `features/` folder. The baseline uses just the `nacpet.nii.gz`, but you are free to combine modalities and demographic features in any way you see fit.
70+
71+
**Do I need to resample or register any of the images?**
72+
73+
All images under `features/` have been resampled to the dimensions of `ct.nii.gz (512x512x531)`. The topogram is 2D and therefore resampled to `(512x1x531)`. Prior to resampling, the MR have been rigidly translated to PET/CT space. The MRI aligns crudely with the PET/CT/Topogram, and it is up to you to decide whether your model should incoorporate registration as a preprocessing step.
74+
75+
**Can I use other data than the BIC-MAC dataset?**
76+
Additional training data is allowed, as long as it was released, publicly available and accessible to all participants without restrictions prior the start of the challenge (April 1st). Private datasets are NOT allowed.
77+
78+
**Can I use pretrained models?**
79+
80+
Yes, you are allowed to use and finetune pretrained models, as longs as they were publicly available and accessible to all participants without restrictions (e.g. on GitHub, Huggingface, Zenodo, or a comparable platform) *prior to the start of the challenge* (April 1st).
81+
82+
**What subjects have sinogram data for local reconstruction?**
83+
84+
Sinogram data is provided for the following subjects:
85+
- `train/`: `sub-000`, `sub-001`, `sub-002`, `sub-005`, `sub-006`, `sub-008`, `sub-013`, `sub-014`
86+
- `val/`: `sub-004`, `sub-009` ,`sub-010` ,`sub-018` (all of them)
87+
88+
The remaining 67 training subjects have `features/` and `ct-label/` only — you can train and evaluate CT metrics on them, but cannot run closed-loop PET reconstruction locally. We chose to provide sinogram data for only 8 of the 67 training subjects to keep the dataset size managable.
89+
90+
**Why is `prediction_mask.nii.gz` in `ct-label/`?**
91+
92+
It marks the voxels your model is responsible for predicting (body minus face and scanner bed). During training you may want to restrict your loss to this mask so the model is not penalised for face/bed regions that are overwritten anyway during reconstruction.
93+
94+
**The MRI comes in chunks — do I need to stitch them?**
95+
96+
Pre-stitched versions (`mri_combined_in_phase.nii.gz`, `mri_combined_out_phase.nii.gz`) are provided if you want a single whole-body volume. The individual chunks (`mri_chunk_{0-3}_{in/out}_phase.nii.gz`) are available if you prefer to work per bed position.
97+
98+
99+
**What format does my pseudo-CT need to be in?**
100+
101+
A NIfTI file (`.nii.gz`) in Hounsfield units, with the same shape and affine as `features/nacpet.nii.gz`. Copying the header directly from the NAC-PET when saving is the safest approach:
102+
103+
```python
104+
ref = nib.load("features/nacpet.nii.gz")
105+
nib.save(nib.Nifti1Image(pred_hu, ref.affine, ref.header), "ct.nii.gz")
106+
```
107+
108+
**Do I need to install STIR locally?**
109+
110+
No, you do not even need to run reconstruction locally - unless you want to validate using the PET-based challenge metrics. If we you do wish to do reconstruction, we recommend using the Docker image, which includes STIR and all dependencies. The image wraps the python code in (`src/recon`) (see ['reconstruction.md](reconstruction.md) for details). Alternatively, you can run the reconstruction locally if you have a local STIR build. Please see [STIR User Guide](https://stir.sourceforge.net/documentation/STIR-UsersGuide.pdf) for installation instructions. IMPORTANT: Make sure to install STIR from source and not a prepackaged version, since the critical reconstruction bugs related to Quadra Sinograms remain present in version 6.3.
111+
112+
**Reconstruction is slow — how long should I expect it to take?**
113+
114+
Roughly 20–120 minutes per subject on a modern CPU, dominated by the OSEM reconstruction step (step 9). Intermediate outputs are cached, so re-runs resume from where they left off unless `OVERWRITE=1` is set.
115+
116+
**How do I debug a failed reconstruction?**
117+
118+
Check `output_dir/intermediates/recon.log` for the full STIR log. Rerun with `VERBOSE=1` (Docker) or `-v` (Python) to stream STIR output to the terminal in real time.
119+
120+
**Which metrics are reported on the validation leaderboard?**
121+
122+
Four metrics in total: Whole-body SUV MAE, Brain Outlier Score, Organ Bias, and CT μ-MAE. See the evaluation section of the main README for descriptions. The Brain Outlier Score is a dataset-level metric — it cannot be computed for a single subject. The fifth and final metric "TAC Bias" is only computed for the final test set. The metric calculation requires reconstruction using dynamic sinograms, which are unfortunately too large to share.
123+
124+
**Can I evaluate without running reconstruction?**
125+
126+
Yes — CT μ-MAE only requires your pseudo-CT and the ground-truth CT. Pass `--pred_ct` without `--pred_pet` to `eval_subject.py`:
127+
128+
```bash
129+
python src/evaluation/eval_subject.py \
130+
--subject_dir data/sub-000 \
131+
--pred_ct outputs/sub-000/ct.nii.gz
132+
```
133+
134+
**Do I need to submit both a pseudo-CT and a PET?**
135+
For the validation phase, you can submit CT-only, PET-only, or both to CodaBench in a zip file. Please see [Submission Guide](submission-guide.md) for instructions. Submitting both PET and CT unlocks all four metrics. Note that to submit a PET image, you have to run reconstruction locally. For the Final Test phase, the organizers will run reconstruction so you only submit the the Docker image with your pseudo-CT model.
136+
137+
**How can I make sure that my submitted Docker image will work?**
138+
Once the validation phase starts, you can submit your pseudo-CT container for "Dry-Run". The organizers will the run your container on the hardware used for final evaluation and report back the CT-based metrics for the validation set. This way you can check that the container runs successfully and within the 5-minute time limit.
139+
140+
**Does my final container need to be the same as the dry-run container?**
141+
No. But we recommend doing a dry-run for the container you intend to submit for the final validation to ensure that it will not crash or run out of memory.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ dependencies = [
1111

1212
[tool.setuptools.packages.find]
1313
where = ["src"]
14+
15+
[dependency-groups]
16+
dev = [
17+
"matplotlib>=3.10.8",
18+
]

src/evaluation/metrics/ct_whole_body_mae.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,27 @@
88
import numpy as np
99
import nibabel as nib
1010

11-
def hu_to_mu(ct_path, kvp=120):
12-
"""Carney et al. 2006 (Med Phys 33:976-983) bilinear HU to mu at 511 keV."""
13-
bone_slope = {80: 3.84e-5, 100: 4.56e-5, 120: 5.10e-5, 140: 5.64e-5}
11+
def hu_to_mu(ct_path):
12+
"""
13+
Carney et al. 2006 (Med Phys 33:976-983)
14+
bilinear HU to mu at 511 keV for 120 kVp.
15+
"""
16+
# Carney parameters for KVP 120: (slope 'a', intercept 'b', breakpoint 'bp' in HU+1000)
17+
a, b, bp = (5.10e-5, 4.71e-2, 1047) # 1047 corresponds to 47 HU
1418

19+
# Load NIfTI CT image
1520
ct = nib.load(ct_path)
1621
hu = ct.get_fdata(dtype=np.float32)
1722

18-
mu = np.where(hu <= 0,
19-
9.6e-5 * (hu + 1000),
20-
9.6e-5 * 1000 + bone_slope[kvp] * hu)
23+
# Pre-calculate (HU + 1000) to optimize the np.where evaluations
24+
hu1000 = hu + 1000
25+
26+
# Apply the Carney bilinear scaling
27+
mu = np.where(hu1000 < bp,
28+
9.6e-5 * hu1000,
29+
a * hu1000 + b)
30+
31+
# Ensure no negative attenuation values from extreme CT noise
2132
mu = np.clip(mu, 0, None)
2233

2334
return nib.Nifti1Image(mu, ct.affine, ct.header)

src/recon/ct_to_acf.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,27 @@ def swap_face_from_gt(pred_ct_path, ct_face_and_bed_path, face_mask_path, output
100100

101101
return result_img
102102

103-
def hu_to_mu(ct_path, kvp=120):
104-
"""Carney et al. 2006 (Med Phys 33:976-983) bilinear HU to mu at 511 keV."""
105-
bone_slope = {80: 3.84e-5, 100: 4.56e-5, 120: 5.10e-5, 140: 5.64e-5}
106-
103+
def hu_to_mu(ct_path):
104+
"""
105+
Carney et al. 2006 (Med Phys 33:976-983)
106+
bilinear HU to mu at 511 keV for 120 kVp.
107+
"""
108+
# Carney parameters for KVP 120: (slope 'a', intercept 'b', breakpoint 'bp' in HU+1000)
109+
a, b, bp = (5.10e-5, 4.71e-2, 1047) # 1047 corresponds to 47 HU
110+
111+
# Load NIfTI CT image
107112
ct = nib.load(ct_path)
108113
hu = ct.get_fdata(dtype=np.float32)
109114

110-
mu = np.where(hu <= 0,
111-
9.6e-5 * (hu + 1000),
112-
9.6e-5 * 1000 + bone_slope[kvp] * hu)
115+
# Pre-calculate (HU + 1000) to optimize the np.where evaluations
116+
hu1000 = hu + 1000
117+
118+
# Apply the Carney bilinear scaling
119+
mu = np.where(hu1000 < bp,
120+
9.6e-5 * hu1000,
121+
a * hu1000 + b)
122+
123+
# Ensure no negative attenuation values from extreme CT noise
113124
mu = np.clip(mu, 0, None)
114125

115126
return nib.Nifti1Image(mu, ct.affine, ct.header)

src/recon/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def reconstruction_pipeline(
7979
if not os.path.exists(mumap_nifti_path) or overwrite:
8080
t = time.perf_counter()
8181
log.info("[3/10] Converting CT to mu-map...")
82-
mu = hu_to_mu(ct_path, kvp=120)
82+
mu = hu_to_mu(ct_path)
8383
mu.to_filename(mumap_nifti_path)
8484
log.info(f" done ({time.perf_counter()-t:.1f}s)")
8585
else:

0 commit comments

Comments
 (0)