Replace the shared CT head-box resampling and manual centroid pre-shift with each toolkit's native automatic transform initialization (elastix/impact use CenterOfGravity, antspyx uses center-of-mass, simpleitk uses identity). All pipelines now register on the full volumes instead of a cropped head box. Drop the _ct_head_box_mm helper and update the benchmark table to document per-toolkit init methods in a dedicated "init" column.
440 lines
No EOL
26 KiB
Markdown
440 lines
No EOL
26 KiB
Markdown
# 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`](src/registration_pipelines.py)
|
||
- Benchmark harness: [`scripts/bench_registration.py`](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
|
||
|
||
```python
|
||
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 **full CT / T1c volumes** (no head box) and use each toolkit's
|
||
**native automatic transform initialization** (no manual centroid offset):
|
||
elastix / impact use `AutomaticTransformInitialization` (CenterOfGravity),
|
||
antspyx its default center-of-mass alignment, simpleitk starts from identity,
|
||
synthmorph needs none. All use **fixed = CT, moving = T1c** except `impact`,
|
||
which keeps **fixed = T1c, moving = CT**: the CT FOV (~300×300×251 mm) is much
|
||
larger than the T1c crop (~220×220×150 mm), so with CT as the fixed image too
|
||
many CT samples map outside the T1c buffer and the IMPACT metric aborts
|
||
("too many samples map outside moving image buffer"). The comparison is on
|
||
each toolkit's native optimizer / metric / global-search capability.
|
||
|
||
## The five pipelines
|
||
|
||
Listed alphabetically. All register on the **full volumes** (no head box) and
|
||
use each toolkit's **native automatic transform initialization** (no manual
|
||
centroid offset): elastix / impact use elastix's `AutomaticTransformInitialization`
|
||
(CenterOfGravity), antspyx uses its default center-of-mass alignment, simpleitk
|
||
starts from identity, and synthmorph needs none (global deep method). All use
|
||
**fixed = CT, moving = T1c** except `impact`, which keeps **fixed = T1c, moving
|
||
= CT** (swapping breaks its TotalSegmentator metric — see below).
|
||
|
||
| # | pipeline | fixed / moving | init | metric | transform | status in this env |
|
||
|---|----------|----------------|------|--------|-----------|--------------------|
|
||
| 1 | `antspyx` | CT / T1c | auto COM | Mattes MI (multi-res) | ANTs `Rigid` | ✅ ran (main env; stochastic MI subsampling) |
|
||
| 2 | `elastix` | CT / T1c | auto CenterOfGravity | Mattes MI (10 bins) | `EulerTransform` rigid (5.x), ASGD, 3 resolutions | ✅ ran (main env, in-process python API — no CLI) |
|
||
| 3 | `impact` | **T1c / CT** | auto CenterOfGravity | **IMPACT semantic** (TotalSegmentator `TS/M730.pt`, L2, Jacobian/GPU) | `EulerTransform` rigid, ASGD, 4 scales | ✅ ran (main env, Docker + GPU) |
|
||
| 4 | `simpleitk` | CT / T1c | identity | Mattes MI (32 bins) | `Euler3D` rigid, pyramid `[4,2,1]` | ✅ ran |
|
||
| 5 | `synthmorph` | CT / T1c | none (global) | **SynthMorph** anatomy-aware contrast-invariant (deep, pretrained) | 6-DOF rigid matrix | ✅ 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 mm` and `std ≤ 12 mm`.** `mid` measures translational
|
||
concentricity; `std` measures 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) |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| **antspyx** | ANTsPy 0.6.3 | 9 | **4/10 (0.40)** | 12.8 / 15.6 | 15.6 / 16.5 | 36.7 | 0.93 | **0.652 / 0.644** |
|
||
| **elastix** | ITK-Elastix 0.25.4 | 9 | **3/10 (0.30)** | 13.6 / 15.7 | 14.8 / 17.0 | 2.6 | 0.93 | **0.641 / 0.640** |
|
||
| **impact** | Docker Elastix 5.2 + IMPACT | 9 | **4/10 (0.40)** | 11.6 / 11.2 | 16.2 / 17.3 | **833.5** | 0.93 | **0.511 / 0.493** |
|
||
| **simpleitk** | SimpleITK 2.5.6 | 9 | **4/10 (0.40)** | 14.4 / 16.6 | 17.9 / 17.5 | 9.7 | 0.93 | **0.651 / 0.641** |
|
||
| **synthmorph** | Docker SynthMorph (TensorFlow) | 9 | **2/10 (0.20)** | 16.7 / 21.8 | 21.8 / 21.5 | 25.0 | 0.93 | **0.238 / 0.308** |
|
||
|
||
*(antspyx and impact are stochastic (random metric/patch subsampling), so their
|
||
accepted counts are one draw each — see Interpretation. impact's s/reg is ~14
|
||
min/case on GPU (full volumes), orders of magnitude above the intensity-metric
|
||
tools. elastix, simpleitk and synthmorph are deterministic at fixed threads.)*
|
||
|
||
Per-case (mid / std, `OK` = accepted):
|
||
|
||
| case | antspyx | elastix | impact | simpleitk | synthmorph |
|
||
|---|---|---|---|---|---|
|
||
| m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT |
|
||
| m6_22IRRKU6_20240115 | 24.6 / 28.6 | 24.0 / 28.3 | 22.7 / 27.0 | 26.3 / 29.6 | 10.4 / 23.7 |
|
||
| m6_23XIMO4M_20230222 | 22.2 / 23.9 | 22.3 / 23.5 | 9.1 / 16.2 | 21.9 / 23.2 | 20.3 / 23.3 |
|
||
| m6_24V62QQM_20230223 | 9.5 / 15.6 | 11.6 / 14.8 | 8.6 / 7.2 **OK** | 14.4 / 23.2 | 11.1 / 21.8 |
|
||
| m6_24V62QQM_20231017 | 11.7 / 7.3 **OK** | 12.2 / 7.4 **OK** | 4.6 / 6.6 **OK** | 11.7 / 7.5 **OK** | 11.5 / 8.4 **OK** |
|
||
| m6_24V62QQM_20240820 | 10.8 / 9.1 **OK** | 11.4 / 9.0 **OK** | 11.6 / 4.9 **OK** | 11.5 / 9.1 **OK** | 43.4 / 15.8 |
|
||
| m6_26TYQI3C_20221027 | 12.3 / 4.4 **OK** | 10.6 / 3.1 **OK** | 11.9 / 4.5 **OK** | 11.3 / 4.6 **OK** | 11.8 / 2.9 **OK** |
|
||
| m6_2BBN5XA4_20250411 | 19.1 / 7.7 **OK** | 18.2 / 14.1 | −1.7 / 29.3 | 18.8 / 7.5 **OK** | 52.4 / 53.0 |
|
||
| m6_2C4TKI6H_20240506 | 12.8 / 17.2 | 13.6 / 20.0 | 15.4 / 23.2 | 14.0 / 17.9 | 16.7 / 28.2 |
|
||
| m6_2CDNN7BH_20240102 | 17.5 / 34.6 | 17.6 / 32.8 | 18.9 / 37.1 | 19.3 / 35.0 | 18.9 / 16.8 |
|
||
|
||
### Interpretation
|
||
|
||
- **Translation is recovered well by all five** (mid gaps mostly 4–16 mm,
|
||
within the `≤ 20 mm` band, except the grossly-drifted cases).
|
||
`tv-in-CT` is 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, and removing the head box makes it
|
||
harder.** On the repositioned cases `std > 12 mm` — the ~30° inter-session
|
||
tilt produces local minima a single auto-start locks onto, and without the
|
||
head-box crop the full-volume metric sees more neck/air confounders. Notably,
|
||
the hardest tilt case `m6_22IRRKU6` is now **rejected by all five** (std
|
||
23–30 mm), which the head-box config got through with IMPACT. This is exactly
|
||
why production `02_build_m6_dataset.py` uses a **multi-start tilt grid**.
|
||
- **The easy cases are now accepted by nearly all pipelines.**
|
||
`m6_24V62QQM_20231017`, `m6_24V62QQM_20240820` and `m6_26TYQI3C` are accepted
|
||
by ANTs/Elastix/IMPACT/SimpleITK (SynthMorph accepts the two cleanest). The
|
||
native automatic inits (center-of-gravity / center-of-mass) place the heads
|
||
close enough that the optimizers converge on the well-positioned cases.
|
||
- **SimpleITK, ANTs and IMPACT tie at 4/10; Elastix 3/10; SynthMorph 2/10.**
|
||
ANTs is no longer the weakest — on the full volumes its native center-of-mass
|
||
init works well (median std 15.6, up from the head-box+manual-pre-shift
|
||
31.7). It is stochastic (random MI subsampling), so the count varies by draw.
|
||
- **IMPACT (semantic metric) is 4/10 but the slowest (~14 min/case on full
|
||
volumes) and lost its standout on the hardest tilt** (`m6_22IRRKU6` now
|
||
mid 22.7 / std 27.0 vs mid 8.3 / std 13.2 with the head box). Matching
|
||
*anatomical semantics* still gives it the best median mid (11.6) and a
|
||
competitive MI (0.511), but the full-volume registration is a larger problem
|
||
for the local ASGD. It also drifts grossly on `m6_2BBN5XA4` (mid −1.7, the
|
||
transform flips to the far side).
|
||
- **Speed** (median wall, 4 cases in parallel, 24 threads): Elastix ~2.6 s
|
||
(warm workers), SimpleITK ~9.7 s, ANTs ~36.7 s (CPU-bound, full volumes),
|
||
**SynthMorph ~25 s** (GPU: a single pretrained-model forward pass per case),
|
||
**IMPACT ~834 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.651 (SimpleITK) / 0.641 (Elastix) / 0.511 (IMPACT) / 0.652 (ANTs) /
|
||
0.238 (SynthMorph).
|
||
The intensity-metric tools cluster at ~0.64 (a genuine alignment signal),
|
||
while SynthMorph's 0.238 reflects its tilt failures. A high MI means the CT
|
||
skull/tissue intensities fall on the matching MR structures after the inverse
|
||
transform.
|
||
- **SynthMorph (deep, pretrained) is near-weakest of the five on these
|
||
cross-modality CT pairs.** Its rigid model is trained on brain *MRI*
|
||
(T1/T2, anatomy-aware but intensity-domain MRI), so it recovers gross
|
||
translation well (mid 10–20 mm on most cases) but is repeatedly defeated by
|
||
the ~30° inter-session tilt: 7 of 9 cases have `std > 15 mm` (rotation error)
|
||
and two drift grossly (`m6_2BBN5XA4` mid 52 / std 53, `m6_24V62QQM_20240820`
|
||
mid 43). It accepts the clean `m6_26TYQI3C` (mid 11.8 / std 2.9, MI 0.60) and
|
||
`m6_24V62QQM_20231017` (mid 11.5 / std 8.4) — where it is competitive with the
|
||
intensity tools. Notably it needs **no centroid init** (it is a global deep
|
||
method that recovers the gross offset on its own). Its low median MI (0.238)
|
||
reflects the tilt failures. On same-modality MRI it would likely rank much
|
||
higher; this CT benchmark is an out-of-distribution stress test for it. It is
|
||
deterministic (one fixed pretrained model, no random subsampling), so unlike
|
||
ANTs/IMPACT the per-case numbers are stable run to run.
|
||
- **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
|
||
|
||
```bash
|
||
# main env — SimpleITK + Elastix + ANTsPy (all pip-installed here)
|
||
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
|
||
python scripts/bench_registration.py --pipelines simpleitk elastix antspyx \
|
||
--n-cases 10 --workers 4 --threads 24 --simpleitk-mode benchmark
|
||
|
||
# Docker pipelines (need the pulled image; see the SynthMorph / IMPACT
|
||
# sections below for the one-time setup)
|
||
python scripts/bench_registration.py --pipelines synthmorph --n-cases 10 \
|
||
--workers 4 --threads 24
|
||
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}`,
|
||
`--synthmorph-model {rigid,affine}`, `--synthmorph-gpu <int|-1>`,
|
||
`--synthmorph-extent <int>`, `--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:
|
||
|
||
```bash
|
||
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 synthmorph --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.)
|
||
|
||
## SynthMorph pipeline (Docker) — one-time setup
|
||
|
||
The `synthmorph` pipeline runs the [`freesurfer/synthmorph`](https://hub.docker.com/r/freesurfer/synthmorph)
|
||
image: [SynthMorph](https://synthmorph.io) (Hoffmann et al., IEEE TMI 2022), an
|
||
anatomy-aware, acquisition-agnostic **deep** registration (TensorFlow
|
||
`VxmAffineFeatureDetector` with the pretrained `synthmorph.rigid.1.h5` weights).
|
||
No skull-stripping or intensity preprocessing is needed: each volume is
|
||
normalized to an isotropic 1-mm LIA network space internally. It is the
|
||
second Docker-based pipeline and the only deep-learning one.
|
||
|
||
Prerequisites (check with `RP.availability()`; a missing piece raises
|
||
`PipelineUnavailable` with the exact command):
|
||
|
||
```bash
|
||
# 1. Docker (with NVIDIA container toolkit so --gpus works)
|
||
docker --version
|
||
|
||
# 2. Pull the image (~11 GB, one-time)
|
||
docker pull freesurfer/synthmorph
|
||
```
|
||
|
||
No extra model assets: the rigid weights ship inside the image
|
||
(`/freesurfer/models`).
|
||
|
||
### How the adapter drives the image
|
||
|
||
1. **Rigid matrix model, no init** — `register -m rigid` on GPU when one is
|
||
visible (auto-spread over the host GPUs by pid; `--synthmorph-gpu -1` forces
|
||
CPU). SynthMorph is a global deep method, so it recovers the gross
|
||
inter-session offset on its own and needs **no** initial transform.
|
||
2. **Read-back** — the `-T` LTA (a `LINEAR_RAS_TO_RAS` 4×4 in shared world
|
||
space; with fixed=CT/moving=T1c the `-T` matrix is the T1c→CT contract
|
||
transform) is parsed into a SimpleITK `AffineTransform` and run through the
|
||
usual `fix_orientation` gate.
|
||
3. **GPU sharing** — the container is run as the host uid (clean scratch
|
||
removal, TF still reaches the GPU) with `TF_FORCE_GPU_ALLOW_GROWTH` set so
|
||
concurrent benchmark workers can share a GPU.
|
||
|
||
## IMPACT pipeline (Docker) — one-time setup
|
||
|
||
The `impact` pipeline runs the [`vboussot/elastix_impact`](https://hub.docker.com/r/vboussot/elastix_impact)
|
||
image: **Elastix 5.2.0** with the [IMPACT](https://arxiv.org/abs/2503.24121)
|
||
*semantic* similarity metric compiled in as a native Elastix metric, plus a
|
||
TorchScript **TotalSegmentator** feature extractor. It is GPU-based and
|
||
Docker-based (alongside `synthmorph`). It is also the one pipeline that keeps
|
||
**fixed = T1c, moving = CT** (the reverse of the others): the CT FOV
|
||
(~300×300×251 mm) is much larger than the T1c crop (~220×220×150 mm), so with
|
||
CT as the fixed image too many CT samples map outside the T1c buffer and the
|
||
IMPACT metric aborts ("too many samples map outside moving image buffer").
|
||
|
||
Prerequisites (check with `RP.availability()`; a missing piece raises
|
||
`PipelineUnavailable` with the exact command):
|
||
|
||
```bash
|
||
# 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](https://huggingface.co/VBoussot/impact-torchscript-models)
|
||
repo for the full list and FOV/layer tables.
|
||
|
||
### How the adapter drives the image
|
||
|
||
The image's `CMD.sh` is a generic launcher, so the adapter calls the in-image
|
||
binary directly (`/lib/elastix-install/bin/elastix`):
|
||
|
||
1. **Full volumes** — fixed=T1c and moving=CT are used as-is (no head box).
|
||
T1c must be the fixed image: the CT FOV is much larger, so with CT fixed too
|
||
many CT samples map outside the T1c buffer and the metric aborts.
|
||
2. **Auto init (no `-t0`)** — the parameter map sets
|
||
`AutomaticTransformInitialization "true"` with method `CenterOfGravity`, so
|
||
elastix aligns the intensity centroids; `AutomaticScalesEstimation` sets the
|
||
optimizer scales.
|
||
3. **Parameter map** — rigid `EulerTransform` (6-DOF), `Impact` metric in
|
||
**Jacobian** mode (differentiable, per the recommended config), 4 semantic
|
||
scales `ImpactVoxelSize` 6/3/1.5/1 mm (scaled to the T1c base spacing) with
|
||
**no image resampling** (all-1 pyramid schedules, as the IMPACT docs require),
|
||
ASGD, `Random` sampler. GPU auto-spread across the host GPUs by pid
|
||
(`--impact-gpu -1` forces CPU).
|
||
4. **Read-back** — the result `TransformParameters.0.itk.txt` is an
|
||
`Euler3DTransform` in the world physical frame (the
|
||
`AutomaticTransformInitialization` is folded in by elastix); the adapter
|
||
runs it through the usual `fix_orientation` gate.
|
||
|
||
## Notes / caveats
|
||
|
||
- The benchmark compares each toolkit in its **native configuration**: all
|
||
register on the full volumes (no head box) and use each toolkit's native
|
||
automatic transform initialization (no manual `centroid_offset`) — elastix /
|
||
impact `AutomaticTransformInitialization` (CenterOfGravity), antspyx's
|
||
center-of-mass alignment, simpleitk identity, synthmorph none. 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.
|
||
- `synthmorph` is deterministic (one fixed pretrained model, no random
|
||
subsampling), so its per-case numbers are stable run to run — unlike ANTs /
|
||
IMPACT. Its low acceptance (2/10) is a genuine cross-modality / tilt
|
||
weakness, not variance.
|
||
- `fix_orientation` (elastix/ants/synthmorph/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. With
|
||
the fixed=CT/moving=T1c swap, `elastix`'s combination transform is now the
|
||
inverse of the contract, so it reports `auto_inverted: True` (the gate
|
||
corrects it); antspyx/synthmorph/impact return the contract direction
|
||
directly (`auto_inverted: False`).
|
||
- **SynthMorph specifics** (Docker `freesurfer/synthmorph`, TensorFlow, GPU):
|
||
- **Cross-modality / tilt weakness.** The `rigid` model is trained on brain
|
||
*MRI*, so on these CT↔T1c pairs it recovers translation but is repeatedly
|
||
defeated by the ~30° inter-session tilt (7/9 cases have `std > 15 mm`).
|
||
It is a legitimate data point for how far a pretrained MRI deep model
|
||
generalizes out-of-domain to CT — not a bug.
|
||
- **No centroid init needed.** Being a global deep method it recovers the
|
||
gross offset on its own; the `-i` initial-transform path is not used
|
||
(which also sidesteps this image's float64/float32 bug on the `-i` code
|
||
path).
|
||
- **GPU sharing.** The container runs as the host uid with
|
||
`TF_FORCE_GPU_ALLOW_GROWTH` so concurrent benchmark workers share a GPU.
|
||
- **~25 s/case** (GPU: one pretrained forward pass), between SimpleITK
|
||
(~10 s) / ANTs (~37 s) and IMPACT (~834 s).
|
||
- **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, `Rigid3DTransform` is gone from
|
||
the component set), the metric key is `Metric` (not `ImageMetric`), the
|
||
random sampler is `Random` (not `RandomSampler`). The adapter uses the 5.x
|
||
names and is not compatible with elastix ≤ 4.x parameter files.
|
||
- `AutomaticTransformInitialization "true"` (CenterOfGravity) provides the
|
||
start (no manual `centroid_offset`); `AutomaticScalesEstimation` sets the
|
||
optimizer scales. An **output directory** is still set
|
||
(`SetOutputDirectory`) for the C++ filter to run; the adapter uses a
|
||
scratch dir under `data/m6reg/elastix/itkelastix/`.
|
||
- `WriteResultImage` must be the string `"false"` (Python `False`); the
|
||
default (absent) makes the filter raise "Could not read result image"
|
||
after a *successful* registration.
|
||
- With fixed=CT/moving=T1c the combination transform is CT→T1c (the inverse
|
||
of the contract), so `fix_orientation` reports `auto_inverted: True` and
|
||
inverts it. The result is recovered by point-mapping
|
||
(`GetCombinationTransform().TransformPoint`) into an affine — independent
|
||
of elastix's Euler-angle parameterization — and wrapped in a SimpleITK
|
||
`AffineTransform(3)`.
|
||
- Registration wall time is ~2.4 s/case median at full budget (ASGD
|
||
500/500/250 over 3 resolutions).
|
||
- **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 ~14 min/case on full volumes (vs 2–37 s
|
||
for the other tools). `--impact-iters` / `--impact-samples` trade accuracy
|
||
for speed.
|
||
- **Keeps fixed=T1c/moving=CT.** IMPACT is the one pipeline that does NOT
|
||
follow the shared fixed=CT/moving=T1c convention: the CT FOV (~300×300×251
|
||
mm) is much larger than the T1c crop (~220×220×150 mm), so with CT as the
|
||
fixed image too many CT samples map outside the T1c buffer and the metric
|
||
aborts ("too many samples map outside moving image buffer"). The returned
|
||
transform still satisfies the contract (T1c→CT).
|
||
- **Auto init.** `AutomaticTransformInitialization` (CenterOfGravity) aligns
|
||
the intensity centroids; no manual `-t0` translation is used.
|
||
- **Rugged single-init convergence.** The semantic landscape has more local
|
||
minima than Mattes-MI: on full volumes it no longer nails the hardest
|
||
repositioned case (`m6_22IRRKU6`, now mid 22.7 / std 27.0 — the head box
|
||
used to get it to mid 8.3 / std 13.2). 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 `elastix` pipeline:** the image
|
||
bundles 5.2.0, so the in-file multi-`Transform` chain and `UseScalingFactors`
|
||
are gone. The result is read back as an `Euler3DTransform` (`.itk.txt`).
|
||
The image's own `CMD.sh` is a generic launcher, so the adapter invokes
|
||
`/lib/elastix-install/bin/elastix` directly.
|
||
- **Models are not in the image.** The TorchScript model(s) must be present
|
||
under the mounted `/Data/Models` (the repo caches them in
|
||
`data/impact_models/`, bind-mounted read-only).
|
||
- **CPU fallback:** `--impact-gpu -1` runs without `--gpus` and with
|
||
`ImpactGPU -1` / mixed precision off (much slower). |