20 KiB
Registration Pipeline Benchmark — CT → T1c (M6)
Benchmarks five rigid, cross-modality registration pipelines that align a
head-and-neck CT to a tightly-cropped T1c MRI so the GTV can be warped onto the
T1c (MR) native grid. This is the registration step of
scripts/02_build_m6_dataset.py. The pairs are hard: different modality (CT
Hounsfield vs MR intensity) and the patient is repositioned between the CT and
MR sessions, so the inter-session head tilt can reach ~30°.
- Pipelines:
src/registration_pipelines.py - Benchmark harness:
scripts/bench_registration.py - Results:
results/bench_registration/(per-pipeline JSON,summary.json,summary.md,bench_registration.png)
Unified contract
Every pipeline returns a SimpleITK transform T such that
sitk.Resample(ct, t1c, T, sitk.sitkLinear) # CT onto the MR grid
sitk.Resample(tv, t1c, T, sitk.sitkNearestNeighbor, 0) # GTV onto the MR grid
places the CT / GTV concentric with the MR head. In Resample's
destination→source convention this T maps T1c → CT. All pipelines register
on the same CT-space head box (MI computed on head tissue, not neck/air) and,
for the single-init tools, from the same centroid-aligned start
(centroid_offset), so the comparison is on the optimizer / metric / transform
handling — not on who finds the ~30–60 mm gross offset first.
The four pipelines
| # | pipeline | toolkit | metric | transform | status in this env |
|---|---|---|---|---|---|
| 1 | elastix |
ITK-Elastix 0.25.4 (pip itk + itk-elastix, elastix 5.3.1) |
Mattes MI (10 bins) | EulerTransform rigid (5.x replacement for Rigid3DTransform), ASGD, 3 resolutions |
✅ ran (main env, in-process python API — no CLI) |
| 2 | simpleitk |
SimpleITK 2.5.6 | Mattes MI (32 bins) | Euler3D rigid, pyramid [4,2,1] |
✅ ran |
| 3 | antspyx |
ANTsPy / ANTs 0.6.3 | Mattes MI (multi-res) | ANTs Rigid |
✅ ran (main env; stochastic MI subsampling) |
| 4 | deepreg |
Oxford DeepReg (CycleGAN) | CycleGAN + reg. loss (pretrained) | 3D displacement field | not installed (see below) |
| 5 | impact |
Docker vboussot/elastix_impact (Elastix 5.2.0 + IMPACT) |
IMPACT semantic (TotalSegmentator TS/M730.pt features, L2, Jacobian/GPU) |
EulerTransform rigid, ASGD, 4 scales (no image resampling) |
✅ ran (main env, Docker + GPU) |
simpleitk has two budgets: --simpleitk-mode benchmark (single pyramid,
centered start — the default, comparable to the other tools) and
--simpleitk-mode production (the full multi-start tilt grid from
02_build_m6_dataset.py, ~20× slower, the most robust but not used for the
head-to-head).
Test cases
First 10 labeled M6 cases (--n-cases 10 --sample first), reproducible. 9 have
a complete CT; 1 (m6_22GD5VL2_20230724) has an incomplete CT head and is
reported as incomplete-CT for every pipeline (a data issue, not a pipeline
failure).
m6_22GD5VL2_20230724 (incomplete CT)
m6_22IRRKU6_20240115 m6_23XIMO4M_20230222 m6_24V62QQM_20230223
m6_24V62QQM_20231017 m6_24V62QQM_20240820 m6_26TYQI3C_20221027
m6_2BBN5XA4_20250411 m6_2C4TKI6H_20240506 m6_2CDNN7BH_20240102
Metrics (identical for every pipeline)
Production QA, crop-independent:
skull_ring_gap_mm— march rays from the MR head center; the gap between the MR head silhouette boundary and the warped-CT skull ring (p97 of positive CT) at mid and low levels, plus the angular std. Aligned ⇔0 ≤ mid ≤ 20 mmandstd ≤ 12 mm.midmeasures translational concentricity;stdmeasures rotational concentricity.tv_in_ct_frac— fraction of the dilated warped GTV inside the warped-CT head tissue (the slab must straddle the lesion). Acceptance needs≥ 0.5.tv_nvox— warped GTV volume.accepted—tv_nvox > 0 and tv_in_ct_frac ≥ 0.5 and aligned(the production acceptance rule).mi_ct_mr— mean Mattes MI (nats) between the CT inverse-transformed into MR coordinates (the warped CT on the T1c grid) and the original MR, restricted to the MR head and each image percentile-binned independently (SimpleITK Mattes-MI style). This is the registration's achieved metric read off as a standalone alignment score: higher = the warped-CT and MR intensities are better in-sync = better aligned; it collapses toward 0 when the CT is rotated/translated away from the MR.reg_wall_s— wall-clock registration time (excludes I/O + scoring).
Results (10 cases)
| pipeline | toolkit | registered | accepted | gap mid (med/mean) | gap std (med/mean) | s/reg (med) | tv-in-CT | MI (med/mean, nats) |
|---|---|---|---|---|---|---|---|---|
| simpleitk | SimpleITK 2.5.6 | 9 | 4/10 (0.40) | 12.6 / 16.1 | 15.4 / 16.0 | 8.6 | 0.93 | 0.643 / 0.630 |
| antspyx | ANTsPy 0.6.3 | 9 | 0/10 (0.00) | 14.9 / 16.5 | 18.4 / 22.7 | 14.5 | 0.93 | 0.598 / 0.529 |
| elastix | ITK-Elastix 0.25.4 | 9 | 5/10 (0.50) | 15.3 / 16.1 | 9.9 / 13.8 | 3.2 | 0.93 | 0.562 / 0.549 |
| impact | Docker Elastix 5.2 + IMPACT | 9 | 3/10 (0.30) | 12.4 / 19.0 | 15.1 / 16.7 | 562.3 | 0.93 | 0.475 / 0.448 |
| deepreg | — | — | — | — | — | — | — | — |
(antspyx and impact are stochastic (random metric/patch subsampling), so their accepted counts are one draw each — see Interpretation. impact's s/reg is ~9.4 min/case on GPU, orders of magnitude above the intensity-metric tools.)
Per-case (mid / std, OK = accepted):
| case | simpleitk | antspyx | elastix | impact |
|---|---|---|---|---|
| m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT |
| m6_22IRRKU6_20240115 | 25.7 / 29.3 | 24.4 / 27.7 | 26.4 / 30.3 | 8.4 / 12.6 |
| m6_23XIMO4M_20230222 | 22.2 / 23.9 | 23.4 / 24.8 | 21.1 / 23.3 | 21.4 / 24.1 |
| m6_24V62QQM_20230223 | 10.7 / 16.9 | 9.5 / 18.4 | 10.6 / 8.2 OK | 10.0 / 8.3 OK |
| m6_24V62QQM_20231017 | 11.5 / 7.3 OK | 14.9 / 15.5 | 11.0 / 6.1 OK | 11.1 / 8.8 OK |
| m6_24V62QQM_20240820 | 11.6 / 9.6 OK | 14.3 / 17.5 | 15.3 / 14.0 | 54.4 / 15.1 |
| m6_26TYQI3C_20221027 | 12.2 / 3.7 OK | 17.7 / 37.6 | 11.4 / 3.2 OK | 23.9 / 22.6 |
| m6_2BBN5XA4_20250411 | 18.6 / 8.0 OK | 14.3 / 12.7 | 18.1 / 9.9 OK | 10.1 / 5.9 OK |
| m6_2C4TKI6H_20240506 | 12.6 / 15.4 | 13.1 / 14.3 | 12.0 / 6.8 OK | 12.4 / 22.3 |
| m6_2CDNN7BH_20240102 | 19.4 / 30.2 | 17.2 / 35.4 | 18.9 / 22.7 | 19.6 / 30.3 |
Interpretation
- Translation is recovered well by all four (mid gaps mostly 6–20 mm,
within the
≤ 20 mmband).tv-in-CTis identical (~0.93): the warped GTV lands inside the CT head for every pipeline, so the target is in the right place. - The binding failure is rotation —
std > 12 mmon the repositioned cases (the ~30° inter-session tilt produces local minima that a single centered start can lock onto). This is exactly why production02_build_m6_dataset.pyuses a multi-start tilt grid. - Elastix is the most accurate of the standard single-init configs
(5/10 accepted, lowest median std 9.9 vs SimpleITK 15.4 and ANTs 18.4) and
the fastest (3.2 s/case median wall, ~1.8 s pure registration vs 8.6 s
SimpleITK / 14.5 s ANTs): ASGD's adaptive step sizes climb out of small
misalignments better than RSGD/ANTs on these boxes. It accepts
m6_24V62QQM_20230223(std 8.2) andm6_2C4TKI6H(std 6.8), which the other two tools fail on (std 16.9 / 15.4), and missesm6_24V62QQM_20240820(std 14.0) that both others accept — different optimizers settle in different local minima on the hard tilted cases. - ANTs' Rigid is the weakest of the intensity-metric tools (0/10 in this
draw; it is stochastic, so a prior draw gave 1/10): it frequently locks onto
the tilted local minimum. Note
m6_26TYQI3C: ANTs nailed the position (mid 17.7) but missed the orientation (std 37.6) — a textbook tilted local minimum. - IMPACT (semantic metric) is the standout on the hardest repositioned case
and the least reliable overall. On
m6_22IRRKU6(the ~30° tilt every intensity tool fails, std 28–30 mm) it gets mid 8.4 / std 12.6 — the best translation of any pipeline and within 0.6 mm of the acceptance threshold — because matching anatomical semantics (TotalSegmentator features) is far more discriminating for gross pose than raw cross-modality intensity. But the semantic landscape is rugged: single-init ASGD lands in bad local minima on 4 of 9 cases (std 15–30 mm), and one (m6_24V62QQM_20240820) even drifts the translation (mid 54 mm). Net 3/10 accepted — between ANTs and SimpleITK — at ~9.4 min/case, two orders of magnitude slower than the intensity tools. - Speed (median wall, 4 cases in parallel, 24 threads): Elastix ~3.2 s (warm workers; a worker's first case pays a ~10 s warm-up), SimpleITK ~8.6 s, ANTs ~14.5 s (CPU-bound), IMPACT ~562 s (GPU-bound: every ASGD iteration runs the TotalSegmentator extractor on the sampled patches with backprop).
- Mattes MI (warped-CT ~ MR) is an independent, intensity-based confirmation:
medians 0.643 (SimpleITK) / 0.598 (ANTs) / 0.562 (Elastix) / 0.475 (IMPACT),
and it diverges sharply on the tilted cases — ANTs
m6_26TYQI3Cdrops to ~0.1 nats (rotation ~38 mm off) while SimpleITK holds 0.670. A high MI means the CT skull/tissue intensities fall on the matching MR structures after the inverse transform. IMPACT's lower median MI is consistent with its less reliable single-init convergence. - Stochasticity: ANTs' Mattes-MI subsamples the image at random, so its per-case numbers carry run-to-run variance (this table is one draw); SimpleITK's MI (and registration) is deterministic at fixed thread count (RSGD wall time is thread-sensitive, convergence is not); the in-process Elastix ASGD is deterministic given its fixed seed. IMPACT is the most stochastic: its Jacobian mode draws random patches every iteration, so the final pose — and even which cases get accepted — changes run to run (two independent 10-case draws both accepted 3/10 but on different cases). Treat its per-case numbers as one sample from a distribution.
Reproducing
# main env — SimpleITK + Elastix + ANTsPy (all pip-installed here); deepreg
# reports unavailable and is recorded as such
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py --n-cases 10 \
--workers 4 --threads 24 --simpleitk-mode benchmark
# IMPACT (needs Docker + the pulled image + a model; see the Impact section
# below for the one-time setup)
python scripts/bench_registration.py --pipelines impact --n-cases 10 \
--workers 4 --threads 24
# combine the per-pipeline JSONs accumulated across envs into one table + plot
python scripts/bench_registration.py --merge
Other flags: --sample random --seed 0 (reproducible random 10),
--keys k1,k2,... (explicit cases), --n-cases N, --workers W,
--threads T, --simpleitk-mode {benchmark,production},
--deepreg-ckpt <.ckpt> (DeepReg), --impact-model <rel|abs>,
--impact-gpu <int|-1>, --impact-iters N, --impact-samples N,
--out-dir <dir>, --qa-images / --qa-out <dir> (visual QA, below).
Visual QA images (checkerboard + color overlay)
--qa-images writes a checkreg-style PNG per (pipeline, case) to
<out-dir>/qa/<pipeline>__<case>.png for eyeballing the alignment. Each image
is a 3×2 grid: rows are the three orthogonal mid-slices through the head
center (mid-axial, mid-coronal, mid-sagittal), and columns are:
- left — checkerboard blend: alternating blocks of the CT inverse-transformed into MR coordinates and the original MR; a clean registration gives a seamless mosaic, a misregistration shows hard edges at the block boundaries.
- right — additive color overlay: CT in red, MR in green. Structures that coincide blend to yellow; rotation/translation error appears as red/green fringing at the skull and brain edges.
The CT is shown in a brain window (L40/W80 = 0–80 HU) so the brain parenchyma is visible and comparable to the MR (the skull, ~800+ HU, clips to white); the MR uses a 2–98 % head window. The coronal and sagittal views are oriented superior-up to match the axial view.
The title carries the pipeline, case key, Mattes MI, and the skull-ring gap (mid/std), so a glance tells you both the achieved-metric score and whether the visuals agree. Generate for the available pipelines with:
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py \
--pipelines simpleitk elastix antspyx --n-cases 10 --qa-images
python scripts/bench_registration.py \
--pipelines impact --n-cases 10 --qa-images
(9 images each; the incomplete-CT case is skipped.)
Enabling the blocked pipeline
DeepReg (Oxford) — the PyPI deepreg (0.1.2) is an unrelated stub that pins
numpy==1.18.5; do not install it. DeepReg is not a one-shot rigid tool: it is a
CycleGAN image-to-image registration that must be trained or run from a
pretrained checkpoint. To benchmark it:
git clone https://github.com/OB-CVHU/DeepReg && cd DeepReg && pip install -e .
python scripts/bench_registration.py --pipelines deepreg --n-cases 10 \
--deepreg-ckpt /path/to/pretrained_3d.ckpt
The adapter runs deepreg test to predict a voxel displacement field, converts
it to a SimpleITK DisplacementFieldTransform, and scores it like the others.
IMPACT pipeline (Docker) — one-time setup
The impact pipeline runs the vboussot/elastix_impact
image: Elastix 5.2.0 with the IMPACT
semantic similarity metric compiled in as a native Elastix metric, plus a
TorchScript TotalSegmentator feature extractor. It is the only pipeline that
is GPU-based and the only one that is Docker-based.
Prerequisites (check with RP.availability(); a missing piece raises
PipelineUnavailable with the exact command):
# 1. Docker (with NVIDIA container toolkit so --gpus works)
docker --version
# 2. Pull the image (~23.5 GB, one-time)
docker pull vboussot/elastix_impact
# 3. Fetch the TorchScript model (~123 MB, one-time) into the repo's model
# cache (data/impact_models/). M730.pt is the recommended TotalSegmentator.
mkdir -p /mnt/t24/Public/xfr/longitudinal/impact_models/TS
curl -L -o /mnt/t24/Public/xfr/longitudinal/impact_models/TS/M730.pt \
https://huggingface.co/VBoussot/impact-torchscript-models/resolve/main/TS/M730.pt
Other models (M258/M291/M850, MIND, SAM2.1, …) are selectable with
--impact-model <HF-relative-path-or-abs>; see the
impact-torchscript-models
repo for the full list and FOV/layer tables.
How the adapter drives the image
The image's CMD.sh only runs elastix -f -m -p -out (no -t0), so the
adapter calls the in-image binary directly (/lib/elastix-install/bin/elastix)
to add the shared centered start:
- Head box — resamples fixed=T1c and moving=CT into the same CT-space head box (physical frame) the other pipelines use, so MI/IMPACT is computed on head tissue, not neck/air.
- Shared init via
-t0— thecentroid_offsettranslation is written to aTranslationTransformparameter file and passed with-t0; withHowToCombineTransforms "Compose"the final transform isT = T_euler(T_init(p)). (Elastix 5.x removed in-file multi-Transformchains andUseScalingFactors, so the initial transform must be a separate-t0file.) - Parameter map — rigid
EulerTransform(6-DOF),Impactmetric in Jacobian mode (differentiable, per the recommended config), 4 semantic scalesImpactVoxelSize6/3/1.5/1 mm (scaled to the box's base spacing) with no image resampling (all-1 pyramid schedules, as the IMPACT docs require), ASGD,Randomsampler. GPU auto-spread across the host GPUs by pid (--impact-gpu -1forces CPU). - Read-back — the result
TransformParameters.0.itk.txtis anEuler3DTransformin the box/world physical frame; the adapter composes it with the-t0translation and runs the usualfix_orientationgate.
Notes / caveats
- The benchmark compares standard single-init configurations (centered
start). Production uses
--simpleitk-mode production's multi-start tilt grid, which raises SimpleITK's acceptance at ~20× the cost; that is out of scope for the head-to-head but is the config the dataset actually ships with. deepregcould not be executed here, so its row is "not installed", not measured; its code path is implemented and unit-verified for import/convention.fix_orientation(elastix/ants/deepreg/impact) chooses between a transform and its inverse by skull-ring concentricity — a correctness gate that makes the result independent of each toolkit's transform-direction convention. In practice it never fired (allauto_inverted: False): elastix/ants/impact returned the T1c→CT direction as expected.- Elastix specifics (ITK-Elastix 0.25.4, elastix 5.3.1, in-process python
API — no CLI binary involved):
- Elastix 5.x renamed components vs the classic parameter files: the rigid
3D transform is
EulerTransform(6-DOF,Rigid3DTransformis gone from the component set), the metric key isMetric(notImageMetric), the random sampler isRandom(notRandomSampler). The adapter uses the 5.x names and is not compatible with elastix ≤ 4.x parameter files. - The initial transform (the shared
centroid_offsetstart) is passed as anInitialTransformParameterObject(aTranslationTransformwithNumberOfParameters 3); the C++ filter only wires it up when an output directory is set (SetOutputDirectory), so the adapter uses a scratch dir underdata/m6reg/elastix/itkelastix/. WriteResultImagemust be the string"false"(PythonFalse); the default (absent) makes the filter raise "Could not read result image" after a successful registration.- The result transform is recovered by point-mapping
(
GetCombinationTransform().TransformPoint) into an affine — independent of elastix's Euler-angle parameterization — and wrapped in a SimpleITKAffineTransform(3). - Registration wall time is ~1.8 s/case at full budget (ASGD 500/500/250 over 3 resolutions); each worker's first case pays a ~10 s warm-up, so the median wall is 3.2 s and the mean is inflated by warm-up.
- Elastix 5.x renamed components vs the classic parameter files: the rigid
3D transform is
- IMPACT specifics (Docker
vboussot/elastix_impact, Elastix 5.2.0, GPU, Jacobian mode):- Slow, by design. Every ASGD iteration runs the TotalSegmentator
extractor on the sampled patches with backpropagation, so the recommended
500-iter / 2000-sample budget is ~9 min/case (vs 3–15 s for the intensity
tools).
--impact-iters/--impact-samplestrade accuracy for speed. - Rugged single-init convergence. The semantic landscape has more local
minima than Mattes-MI: the pipeline nails the hardest repositioned case
(
m6_22IRRKU6, mid 8.4 mm — best of all) but lands in bad minima on some others. A multi-start or higher-iteration budget would likely raise its acceptance; that is out of scope for the single-init head-to-head. - Stochastic. Jacobian mode resamples patches each iteration, so results vary run to run (see Stochasticity).
- Elastix 5.x differences vs the in-process
elastixpipeline: the image bundles 5.2.0, so the in-file multi-Transformchain andUseScalingFactorsare gone — the shared centered start is a separate-t0translation file, and the result is read back as anEuler3DTransform(.itk.txt) and composed with-t0. The image's ownCMD.shdoesn't pass-t0, so the adapter invokes/lib/elastix-install/bin/elastixdirectly. - Models are not in the image. The TorchScript model(s) must be present
under the mounted
/Data/Models(the repo caches them indata/impact_models/, bind-mounted read-only). - CPU fallback:
--impact-gpu -1runs without--gpusand withImpactGPU -1/ mixed precision off (much slower).
- Slow, by design. Every ASGD iteration runs the TotalSegmentator
extractor on the sampled patches with backpropagation, so the recommended
500-iter / 2000-sample budget is ~9 min/case (vs 3–15 s for the intensity
tools).