Replace the unavailable DeepReg benchmark path with the freesurfer/synthmorph Docker pipeline for rigid CT-to-T1c registration. Add a shared CT head-box resample helper for centered initialization and update benchmark docs and CLI for the new SynthMorph options.
423 lines
No EOL
25 KiB
Markdown
423 lines
No EOL
25 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**. 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). All single-init tools start 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 five 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 | `synthmorph` | Docker `freesurfer/synthmorph` (TensorFlow, pretrained) | **SynthMorph** anatomy-aware contrast-invariant (deep, pretrained) | 6-DOF rigid matrix (centroid init) | ✅ ran (main env, Docker + GPU) |
|
||
| 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 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) |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| **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** |
|
||
| **synthmorph** | Docker SynthMorph (TensorFlow) | 9 | **1/10 (0.10)** | 19.8 / 25.1 | 23.0 / 24.1 | 28.2 | 0.93 | **0.215 / 0.309** |
|
||
| **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** |
|
||
|
||
*(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.
|
||
synthmorph is deterministic at fixed threads; its low acceptance is a real
|
||
cross-modality/tilt weakness, not run-to-run variance.)*
|
||
|
||
Per-case (mid / std, `OK` = accepted):
|
||
|
||
| case | simpleitk | antspyx | elastix | synthmorph | impact |
|
||
|---|---|---|---|---|---|
|
||
| m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT |
|
||
| m6_22IRRKU6_20240115 | 25.7 / 29.3 | 24.4 / 27.7 | 26.4 / 30.3 | 20.0 / 25.2 | **8.4 / 12.6** |
|
||
| m6_23XIMO4M_20230222 | 22.2 / 23.9 | 23.4 / 24.8 | 21.1 / 23.3 | 19.8 / 23.2 | 21.4 / 24.1 |
|
||
| m6_24V62QQM_20230223 | 10.7 / 16.9 | 9.5 / 18.4 | 10.6 / 8.2 **OK** | 12.0 / 23.0 | 10.0 / 8.3 **OK** |
|
||
| m6_24V62QQM_20231017 | 11.5 / 7.3 **OK** | 14.9 / 15.5 | 11.0 / 6.1 **OK** | 13.0 / 12.6 | 11.1 / 8.8 **OK** |
|
||
| m6_24V62QQM_20240820 | 11.6 / 9.6 **OK** | 14.3 / 17.5 | 15.3 / 14.0 | 53.8 / 20.2 | 54.4 / 15.1 |
|
||
| m6_26TYQI3C_20221027 | 12.2 / 3.7 **OK** | 17.7 / 37.6 | 11.4 / 3.2 **OK** | 11.3 / 5.0 **OK** | 23.9 / 22.6 |
|
||
| m6_2BBN5XA4_20250411 | 18.6 / 8.0 **OK** | 14.3 / 12.7 | 18.1 / 9.9 **OK** | 49.3 / 58.9 | 10.1 / 5.9 **OK** |
|
||
| m6_2C4TKI6H_20240506 | 12.6 / 15.4 | 13.1 / 14.3 | 12.0 / 6.8 **OK** | 18.7 / 19.3 | 12.4 / 22.3 |
|
||
| m6_2CDNN7BH_20240102 | 19.4 / 30.2 | 17.2 / 35.4 | 18.9 / 22.7 | 27.6 / 29.1 | 19.6 / 30.3 |
|
||
|
||
### 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 centered
|
||
start can lock onto). This is exactly why production `02_build_m6_dataset.py`
|
||
uses 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) and `m6_2C4TKI6H` (std 6.8), which the other two tools fail on
|
||
(std 16.9 / 15.4), and misses `m6_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), **SynthMorph ~28 s** (GPU: a single pretrained-model
|
||
forward pass per case), **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) /
|
||
0.215 (SynthMorph),
|
||
and it diverges sharply on the tilted cases — ANTs `m6_26TYQI3C` drops 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.
|
||
- **SynthMorph (deep, pretrained) is the 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 12–20 mm on most cases) but is repeatedly defeated by
|
||
the ~30° inter-session tilt: 6 of 9 cases have `std > 20 mm` (rotation error)
|
||
and two drift grossly (`m6_2BBN5XA4` mid 49 / std 59, `m6_24V62QQM_20240820`
|
||
mid 54). It accepts only the clean `m6_26TYQI3C` (mid 11.3 / std 5.0, MI
|
||
0.64 — where it is competitive with the intensity tools). Its low median MI
|
||
(0.215) 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. **Shared init** — the `centroid_offset` (CT→T1c head translation) is built
|
||
*inside* the container as an initial LTA (`-i`), because surfa requires the
|
||
init transform's embedded src/dst geometries to match the input images
|
||
exactly. The container also writes the LTA as the host uid so the scratch
|
||
removes cleanly.
|
||
2. **Rigid matrix model** — `register -m rigid -g` on GPU when one is visible
|
||
(auto-spread over the host GPUs by pid; `--synthmorph-gpu -1` forces CPU).
|
||
3. **Read-back** — the `-t` LTA (a `LINEAR_RAS_TO_RAS` 4×4 in shared world
|
||
space) is parsed into a SimpleITK `AffineTransform` and run through the
|
||
usual `fix_orientation` gate.
|
||
4. **Image-build workarounds** — this image's `-i` code path has a
|
||
float64/float32 bug (the dense 256³ mesh is built as `double` and the final
|
||
matrix product mismatches dtypes), so the adapter injects a small wrapper
|
||
that casts the transform operands to float32, and sets
|
||
`TF_FORCE_GPU_ALLOW_GROWTH` so concurrent 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 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):
|
||
|
||
```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 **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.
|
||
- `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 (1/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. It
|
||
rarely fired: elastix/ants/impact returned the T1c→CT direction as expected
|
||
(`auto_inverted: False`) and synthmorph did so on 8/9 cases (its one
|
||
`True` was the grossly-drifted `m6_2BBN5XA4`, where the gate flipped a bad
|
||
pose).
|
||
- **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 (6/9 cases have `std > 20 mm`).
|
||
It is a legitimate data point for how far a pretrained MRI deep model
|
||
generalizes out-of-domain to CT — not a bug.
|
||
- **Shared init is built in-container.** surfa rejects an `-i` transform
|
||
whose src/dst geometries don't match the inputs (tol 1e-3), so the
|
||
adapter has the container write the centroid-translation LTA from the
|
||
mounted images themselves.
|
||
- **Image-build dtype/GPU workarounds.** This image's `-i` path builds the
|
||
dense 256³ mesh as float64 (2× memory → GPU OOM under contention) and
|
||
mismatches dtypes in the final matrix product; the injected wrapper casts
|
||
transform operands to float32, and `TF_FORCE_GPU_ALLOW_GROWTH` lets
|
||
concurrent workers share a GPU.
|
||
- **~28 s/case** (GPU: one pretrained forward pass), between ANTs (~15 s)
|
||
and IMPACT (~562 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.
|
||
- The initial transform (the shared `centroid_offset` start) is passed as an
|
||
`InitialTransformParameterObject` (a `TranslationTransform` with
|
||
`NumberOfParameters 3`); the C++ filter only wires it up when an
|
||
**output directory is set** (`SetOutputDirectory`), so 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.
|
||
- The result transform 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 ~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.
|
||
- **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-samples` trade 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 `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). |