This repository accompanies our paper, which took second place in the EMBC 2023 student paper competition:
"ASTRA: Atomic Surface Transformations for Radiotherapy Quality Assurance" Amith Kamath, Robert Poel, Jonas Willmann, Ekin Ermiş, Nicolaus Andratschke, Mauricio Reyes. 45th IEEE Engineering in Medicine and Biology Conference (EMBC), 2023.
Data, model weights and the pre-computed sensitivity maps are archived outside this repository and shared on request — see Configuring where the data lives.
See a short video description of this work here:
Radiotherapy planning for glioblastoma starts with manual contours of the tumour target and the surrounding organs at risk. These are reviewed without information about the dosimetric consequence of any given correction, so review effort is spread uniformly over a surface where the consequences are not uniform.
ASTRA estimates that consequence point by point. A ball of radius 3 voxels is added to one point of one organ's surface — an atomic surface transformation, modelling local over-segmentation at roughly the scale of inter-observer disagreement — the dose is predicted again, and the mean absolute change over the brain is recorded at that point. Repeating this over the surface gives a sensitivity map: a per-point estimate of how much a local contour change would alter the predicted dose.
The dose predictor is the cascaded 3D U-Net from deepdosesens, used unmodified; only the input contours change between predictions. The cost is one forward pass per surface point — roughly a thousand dose estimates per organ, about 3 000 per patient, and 30 589 across the experiment reported here. This is tractable only because the predictor runs in seconds where a treatment planning system takes hours.
docs/videos/ holds one clip per experiment, rendered natively in Python — see
README-videos.md for the layout and the
appearance choices.
| Video | Content |
|---|---|
transformation_walk_DLDP_081.mp4 |
The method: 50 consecutive transformations on the brainstem, one at a time, with the map accumulating as the walk proceeds |
sensitivity_DLDP_090.mp4 |
Contours alone beside the sensitivity map, orbiting. A complex target shape |
sensitivity_DLDP_084.mp4 |
The same, for a target close to the hippocampi and brainstem |
sensitivity_DLDP_082.mp4 |
The same, for the smallest target of the test set, distant from the organs at risk |
sensitivity_DLDP_088.mp4 |
The same, for a larger target |
radius_sweep_DLDP_081.mp4 |
Figure 4: the same brainstem at transformation radius 3, 5 and 7 |
Each frame puts a near-transparent brain behind the structures for context, the target volume in red, and the organs at risk either uncoloured or coloured by the sensitivity map in Gy, with a colour bar. The four sensitivity clips are the four cases of the paper's Table I, which the reproduction script identifies — the paper labels them (a) to (d) and never names them.
Previously MATLAB. The paper's figures came from MATLAB's Medical Imaging Toolbox,
driven by live scripts whose scene state lived in fifteen binary .mat files, so they
could not be regenerated without a licence and those files. Everything is
PyVista now: same reading of the scene, same camera orbit, and
the appearance choices are code in
astra/visualization/scene.py. The original scripts are
kept under astra/visualization/matlab/ for provenance.
Rebuild them with:
scripts/fetch_artifacts.sh
scripts/make_videos.shscripts/reproduce.sh recomputes every published number from the archived sensitivity
maps and prints it beside the paper's value. 66 of 68 reproduce.
| Quantity | Paper | Reproduced |
|---|---|---|
| Table I, mean absolute dose difference per organ (20 values) | — | all match to ≤ 0.0001 Gy |
| Figure 3, the four colour-bar maxima | 0.168, 0.609, 0.772, 0.622 | 0.1689, 0.6094, 0.7726, 0.6220 |
| Table II, organ sizes (13 values) | — | all match to ≤ 0.01 voxels |
| Table II, correlations (26 values) | — | 24 match; the right optic nerve differs |
| Figure 4, radius 3 vs 5 / 5 vs 7 | 0.949, 0.939 | 0.9497, 0.9398 |
| Transformations, total / per subject | > 20 000 / > 2 000 | 30 589 / 3 059 |
The archived maps are checked against the archived weights, not only restated:
python -m astra.analyze.verify_inference re-runs transformations through the network
and agrees with the stored map to 2.3 × 10⁻⁶ Gy on a 0–70 Gy scale.
Two things the paper does not say, which the artifacts settle:
- Table I's four cases are DLDP_082, DLDP_088, DLDP_090 and DLDP_084, in that order. Matching the 20 values identifies them uniquely, and Figure 3's colour-bar maxima confirm the same order independently.
- Table II's size column was computed over seven cases and its correlations over ten. The sizes reproduce exactly from DLDP_081–087 and not from any ten-case set; the correlations reproduce from DLDP_081–090 and not from any seven-case set.
The right-optic-nerve row of Table II could not be reproduced: published −0.15 and
+0.05, recomputed +0.25 and −0.11, both signs reversed, and no subset of cases or method
of averaging comes closer than 0.23. results/embc_verification.csv records every claim
individually, and the project page carries an
errata with the corrected values.
- Python 3.10+
- PyTorch (CUDA, MPS or CPU) — only for the inference check and for training
- SimpleITK, NumPy, SciPy, scikit-image, pandas, PyVista
- ffmpeg, for the videos
git clone https://github.com/amithjkamath/astra.git
cd astra
uv venv .venv
source .venv/bin/activate
uv pip install -r pyproject.tomlNo paths are hardcoded. Copy .env.example to .env and point it at your copy of the
artifacts (or set the same variable in the environment):
ASTRA_EMBC_ARCHIVE=/path/to/2023-02-EMBC/artifacts
#ASTRA_DATA=/mnt/big-disk/astra/data
#ASTRA_CHECKPOINTS=/mnt/big-disk/astra/checkpointsEverything falls back to directories inside the repository, so the defaults work once
scripts/fetch_artifacts.sh has unpacked the archive. Check what is in effect with:
python -m astra.configOnly code and the demonstration videos are committed here. Planning CTs, contours, reference plans, sensitivity maps and model weights live in the artifact archive and are shared on request; the archive carries its own manifest describing the layout. Fetch from it with:
WHAT=core scripts/fetch_artifacts.sh # cohort, radius-3 maps, weights (~960 MB)
scripts/fetch_artifacts.sh # adds radius 5 and 7, and the transformation walkThe sensitivity maps are archived, so nothing needs a GPU. Reproducing every table
and rendering every video reads the maps; only verify_inference runs the network.
An archived Perturbed_<organ>.nii.gz is sparse: at each sampled surface point it holds
the absolute dose difference summed over the brain, and zero everywhere else. The
paper reports the mean over the brain, so the conversion is one division:
from astra.analyze.maps import brain_voxels, read
from astra.config import sensitivity_path
sensitivity = read(sensitivity_path("DLDP_082", "Perturbed_Hippocampus_L.nii.gz"))
gy = sensitivity / brain_voxels("DLDP_082") # mean absolute dose difference, Gy
points = gy[gy > 0]
print(points.mean(), points.max()) # 0.1041, 0.1689 -- Table I (a), Figure 3 (a)Read as a dose it is meaningless, and nothing in the file says so — so
astra/analyze/maps.py does the reading for you and every script goes through it.
Only needed to verify the method or to run it on a new case; the paper's maps are archived.
from astra.config import case_path, checkpoint_path
from astra.inference import DosePredictor
from astra.sensitivity import sensitivity_map
predictor = DosePredictor(checkpoint_path("dose-predictor", "weights.pt"))
smap, baseline, points = sensitivity_map(predictor, case_path("DLDP_081"), "Pituitary")One forward pass per surface point, about 25 s each on Apple silicon, so a whole organ
is hours. points= takes a subset, which is how verify_inference checks a handful
cheaply.
The model and its training belong to deepdosesens; the code is here so the repository is self-contained.
uv pip install -r pyproject.toml --extra train
python astra/train.py| Path | Contents |
|---|---|
astra/config.py |
every path the project uses, from env vars or .env |
astra/sensitivity.py |
atomic surface transformations, and the map they produce |
astra/inference.py |
DosePredictor — load once, predict many |
astra/analyze/maps.py |
reading archived maps and deriving the paper's quantities |
astra/analyze/reproduce_embc_tables.py |
every published number, recomputed and checked |
astra/analyze/verify_inference.py |
the archived maps against the archived weights |
astra/visualization/ |
the PyVista scene, the three video builders, and the superseded MATLAB scripts |
astra/model/, astra/data/, astra/train/ |
the cascaded 3D U-Net, its data pipeline and its trainer |
scripts/ |
fetch_artifacts.sh, reproduce.sh, make_videos.sh |
docs/ |
the project website, served at amithjkamath.github.io/astra |
examples/ |
notebooks and scripts from the original analysis |
If this is useful in your research, please consider citing:
@inproceedings{kamath2023astra,
title={ASTRA: Atomic Surface Transformations for Radiotherapy quality Assurance},
author={Kamath, Amith and Poel, Robert and Willmann, Jonas and Ermis, Ekin and Andratschke, Nicolaus and Reyes, Mauricio},
booktitle={45th IEEE Engineering in Medicine and Biology Conference (EMBC)},
year={2023}
}
Major props to the code and organization in https://github.com/LSL000UD/RTDosePrediction, which is what this model is based on (looks like this repo is not maintained/available anymore!)
