longitudinal/REGISTRATION_BENCHMARK.md
Furen Xiao 4ad972b00f refactor(pipelines): remove shared resample helper and clarify init conventions
Drop the _ct_head_box_resample utility that was only needed for centered
initialization, since centroid alignment is now scoped to local optimizers
(antspyx, impact) rather than all pipelines. Reorder the pipeline table and
module docstring alphabetically and add explicit fixed/moving and centroid-init
columns so the per-toolkit registration convention is unambiguous.
2026-09-29 04:09:55 +08:00

441 lines
No EOL
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 use
**fixed = CT, moving = T1c** except `impact`, which keeps **fixed = T1c,
moving = CT** (swapping its roles breaks the TotalSegmentator-based metric,
which aborts with "Error in metric" or converges to a wrong rotation). The
intensity-metric pipelines register on the same CT-space head box (MI computed
on head tissue, not neck/air); `synthmorph` uses the full volumes (it
normalizes each one to an isotropic network space internally). The local
optimizers that cannot recover the gross inter-session offset from identity
(`antspyx`, `impact`) start from a centroid-aligned translation
(`centroid_offset`); the others start from identity, so the comparison is on
each toolkit's native optimizer / metric / global-search capability.
## The five pipelines
Listed alphabetically. All use **fixed = CT, moving = T1c** except `impact`,
which keeps **fixed = T1c, moving = CT** (swapping breaks its TotalSegmentator
metric). A pipeline that is a local optimizer and cannot recover the gross
inter-session offset from identity (`antspyx`, `impact`) starts from a
centroid-aligned translation; the others start from identity / their own global
search.
| # | pipeline | fixed / moving | centroid init | metric | transform | status in this env |
|---|----------|----------------|---------------|--------|-----------|--------------------|
| 1 | `antspyx` | CT / T1c | **yes** (required) | Mattes MI (multi-res) | ANTs `Rigid` | ✅ ran (main env; stochastic MI subsampling) |
| 2 | `elastix` | CT / T1c | no (identity) | Mattes MI (10 bins) | `EulerTransform` rigid (5.x), ASGD, 3 resolutions | ✅ ran (main env, in-process python API — no CLI) |
| 3 | `impact` | **T1c / CT** | **yes** | **IMPACT semantic** (TotalSegmentator `TS/M730.pt`, L2, Jacobian/GPU) | `EulerTransform` rigid, ASGD, 4 scales | ✅ ran (main env, Docker + GPU) |
| 4 | `simpleitk` | CT / T1c | no (identity) | Mattes MI (32 bins) | `Euler3D` rigid, pyramid `[4,2,1]` | ✅ ran |
| 5 | `synthmorph` | CT / T1c | no (global deep) | **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 | **1/10 (0.10)** | 18.9 / 20.1 | 31.7 / 31.3 | 10.9 | 0.94 | **0.436 / 0.393** |
| **elastix** | ITK-Elastix 0.25.4 | 9 | **3/10 (0.30)** | 16.6 / 17.6 | 16.9 / 18.8 | 2.4 | 0.93 | **0.641 / 0.583** |
| **impact** | Docker Elastix 5.2 + IMPACT | 9 | **3/10 (0.30)** | 13.1 / 16.8 | 13.2 / 18.6 | **576.0** | 0.93 | **0.469 / 0.482** |
| **simpleitk** | SimpleITK 2.5.6 | 9 | **4/10 (0.40)** | 12.7 / 16.0 | 13.5 / 15.7 | 7.6 | 0.93 | **0.645 / 0.632** |
| **synthmorph** | Docker SynthMorph (TensorFlow) | 9 | **2/10 (0.20)** | 16.7 / 21.8 | 22.9 / 21.7 | 26.0 | 0.93 | **0.241 / 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 ~9.6
min/case on GPU, 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 | 18.9 / 43.8 | 27.0 / 29.3 | 8.3 / 13.2 | 26.2 / 29.6 | 10.4 / 23.7 |
| m6_23XIMO4M_20230222 | 22.9 / 24.5 | 22.6 / 23.9 | 23.1 / 25.7 | 22.6 / 24.0 | 20.3 / 23.3 |
| m6_24V62QQM_20230223 | 18.7 / 47.5 | 11.3 / 16.9 | 9.9 / 7.9 **OK** | 10.2 / 16.2 | 10.4 / 22.9 |
| m6_24V62QQM_20231017 | 15.3 / 37.9 | 16.6 / 11.1 **OK** | 6.6 / 6.9 **OK** | 11.9 / 7.7 **OK** | 11.5 / 8.4 **OK** |
| m6_24V62QQM_20240820 | 38.7 / 54.0 | 10.2 / 8.2 **OK** | 37.1 / 45.6 | 10.8 / 9.0 **OK** | 43.4 / 15.8 |
| m6_26TYQI3C_20221027 | 11.2 / 3.7 **OK** | 11.7 / 3.7 **OK** | 23.1 / 13.0 | 11.1 / 2.6 **OK** | 11.8 / 2.9 **OK** |
| m6_2BBN5XA4_20250411 | 20.6 / 24.2 | 25.3 / 30.8 | 10.1 / 6.5 **OK** | 19.1 / 8.8 **OK** | 52.4 / 53.0 |
| m6_2C4TKI6H_20240506 | 15.3 / 14.2 | 14.2 / 15.3 | 13.1 / 17.4 | 12.7 / 13.5 | 16.7 / 28.2 |
| m6_2CDNN7BH_20240102 | 19.4 / 31.7 | 19.5 / 30.3 | 19.6 / 30.9 | 19.3 / 30.1 | 18.9 / 16.8 |
### Interpretation
- **Translation is recovered well by all five** (mid gaps mostly 6–20 mm,
within the `≤ 20 mm` band, except the grossly-drifted cases below).
`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** — `std > 12 mm` on the repositioned cases
(the ~30° inter-session tilt produces local minima that a single start
(identity or centroid) can lock onto). This is exactly why production
`02_build_m6_dataset.py` uses a **multi-start tilt grid**.
- **SimpleITK is the most reliable intensity-metric tool in this draw**
(4/10 accepted, best median mid 12.7), and **Elastix is the fastest**
(2.4 s/case median wall vs 7.6 s SimpleITK / 10.9 s ANTs) with the best
median MI (0.641) but 3/10 accepted. Under the fixed=CT/moving=T1c
convention the two settle into different local minima on the hard tilted
cases: Elastix accepts `m6_24V62QQM_20240820` (std 8.2) and `m6_24V62QQM_20231017`
(std 11.1) but misses `m6_24V62QQM_20230223` (std 16.9) that SimpleITK handles
better, and vice-versa on the drift cases. The fixed/moving direction
materially affects where the ASGD/RSGD optimizers converge on the tilted cases.
- **ANTs' Rigid is the weakest of the intensity-metric tools** (1/10 in this
draw; it is stochastic, so the count varies): it frequently locks onto the
tilted local minimum (median std 31.7, worst of all). It accepts only
`m6_26TYQI3C` (mid 11.2 / std 3.7) and misses `m6_24V62QQM_20230223`
(std 47.5) and `m6_24V62QQM_20231017` (std 37.9) that the other tools accept —
textbook tilted local minima.
- **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) it gets **mid 8.3 / std 13.2** — the best translation of
any pipeline and within ~1 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 several cases, and
two drift grossly (`m6_24V62QQM_20240820` mid 37, `m6_26TYQI3C` std 13.0).
Net 3/10 accepted — between ANTs and SimpleITK — at ~9.6 min/case, two orders
of magnitude slower than the intensity tools.
- **Speed** (median wall, 4 cases in parallel, 24 threads): Elastix ~2.4 s
(warm workers), SimpleITK ~7.6 s, ANTs ~10.9 s (CPU-bound),
**SynthMorph ~26 s** (GPU: a single pretrained-model forward pass per case),
**IMPACT ~576 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.645 (SimpleITK) / 0.567 (Elastix) / 0.469 (IMPACT) / 0.436 (ANTs) /
0.241 (SynthMorph),
and it diverges sharply on the tilted cases — ANTs `m6_24V62QQM_20230223`
drops to ~0.06 nats (rotation ~47 mm off) while SimpleITK holds 0.703. A high
MI means the CT skull/tissue intensities fall on the matching MR structures
after the inverse transform. ANTs' and SynthMorph's low median MI is
consistent with their tilted-case convergence failures.
- **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.241)
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): swapping its roles
breaks the TotalSegmentator metric ("Error in metric" / wrong rotation).
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` 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:
1. **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.
2. **Shared init via `-t0`** — the `centroid_offset` translation is written to a
`TranslationTransform` parameter file and passed with `-t0`; with
`HowToCombineTransforms "Compose"` the final transform is
`T = T_euler(T_init(p))`. (Elastix 5.x removed in-file multi-`Transform`
chains and `UseScalingFactors`, so the initial transform must be a separate
`-t0` file.)
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 box's 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 box/world physical frame; the adapter composes it
with the `-t0` translation and runs the usual `fix_orientation` gate.
## Notes / caveats
- The benchmark compares each toolkit in its **native configuration**: the
local optimizers that cannot recover the gross offset from identity
(`antspyx`, `impact`) get a centroid-aligned start, while `simpleitk`,
`elastix` and `synthmorph` start from identity / their own global search.
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.
- **~26 s/case** (GPU: one pretrained forward pass), between ANTs (~11 s)
and IMPACT (~576 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.
- No initial transform is passed (identity start): the ASGD + 3-resolution
Mattes-MI pyramid recovers the gross inter-session offset on its own, so
the shared `centroid_offset` start is not required. 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 ~9.6 min/case (vs 2–11 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: swapping its roles
breaks the TotalSegmentator metric (elastix aborts with "Error in metric"
or converges to a wrong rotation). The returned transform still satisfies
the contract (T1c→CT).
- **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.3 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 `elastix` pipeline:** the image
bundles 5.2.0, so the in-file multi-`Transform` chain and `UseScalingFactors`
are gone — the shared centered start is a separate `-t0` translation file,
and the result is read back as an `Euler3DTransform` (`.itk.txt`) and
composed with `-t0`. The image's own `CMD.sh` doesn't pass `-t0`, 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).