# 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 `, `--synthmorph-extent `, `--impact-model `, `--impact-gpu `, `--impact-iters N`, `--impact-samples N`, `--out-dir `, `--qa-images` / `--qa-out ` (visual QA, below). ## Visual QA images (checkerboard + color overlay) `--qa-images` writes a `checkreg`-style PNG per (pipeline, case) to `/qa/__.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 `; 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).