# Registration Pipeline Benchmark — CT → T1c (M6) Benchmarks **four 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 same CT-space head box (MI computed on head tissue, not neck/air) and, for the single-init tools, from the **same centroid-aligned start** (`centroid_offset`), so the comparison is on the optimizer / metric / transform handling — not on who finds the ~30–60 mm gross offset first. ## The four pipelines | # | pipeline | toolkit | metric | transform | status in this env | |---|----------|---------|--------|-----------|--------------------| | 1 | `elastix` | ITK-Elastix 0.25.4 (pip `itk` + `itk-elastix`, elastix 5.3.1) | Mattes MI (10 bins) | `EulerTransform` rigid (5.x replacement for `Rigid3DTransform`), ASGD, 3 resolutions | ✅ ran (main env, in-process python API — no CLI) | | 2 | `simpleitk` | SimpleITK 2.5.6 | Mattes MI (32 bins) | `Euler3D` rigid, pyramid `[4,2,1]` | ✅ ran | | 3 | `antspyx` | ANTsPy / ANTs 0.6.3 | Mattes MI (multi-res) | ANTs `Rigid` | ✅ ran (isolated py3.12 env) | | 4 | `deepreg` | Oxford DeepReg (CycleGAN) | CycleGAN + reg. loss (pretrained) | 3D displacement field | **not installed** (see below) | `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 | **1/10 (0.10)** | 14.2 / 17.3 | 24.8 / 22.4 | 12.7 | 0.93 | **0.600 / 0.523** | | **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** | | deepreg | — | — | — | — | — | — | — | — | Per-case (mid / std, `OK` = accepted): | case | simpleitk | antspyx | elastix | |---|---|---|---| | m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | | m6_22IRRKU6_20240115 | 25.7 / 29.3 | 26.3 / 29.2 | 26.4 / 30.3 | | m6_23XIMO4M_20230222 | 22.2 / 23.9 | 23.8 / 25.0 | 21.1 / 23.3 | | m6_24V62QQM_20230223 | 10.7 / 16.9 | 9.4 / 18.2 | 10.6 / 8.2 **OK** | | m6_24V62QQM_20231017 | 11.5 / 7.3 **OK** | 12.5 / 12.8 | 11.0 / 6.1 **OK** | | m6_24V62QQM_20240820 | 11.6 / 9.6 **OK** | 14.8 / 11.7 **OK** | 15.3 / 14.0 | | m6_26TYQI3C_20221027 | 12.2 / 3.7 **OK** | 6.1 / 37.2 | 11.4 / 3.2 **OK** | | m6_2BBN5XA4_20250411 | 18.6 / 8.0 **OK** | 14.3 / 12.7 | 18.1 / 9.9 **OK** | | m6_2C4TKI6H_20240506 | 12.6 / 15.4 | 13.6 / 14.7 | 12.0 / 6.8 **OK** | | m6_2CDNN7BH_20240102 | 19.4 / 30.2 | 18.2 / 33.6 | 18.9 / 22.7 | ### Interpretation - **Translation is recovered well by all three** (mid gaps mostly 6–20 mm, within the `≤ 20 mm` band). `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 MI 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 three standard single-init configs** (5/10 accepted, lowest median std 9.9 vs SimpleITK 15.4 and ANTs 24.8) and the fastest (3.2 s/case median wall, ~1.8 s pure registration vs 8.6 s SimpleITK / 12.7 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** (1/10 accepted): it frequently locks onto the tilted local minimum. Note `m6_26TYQI3C`: ANTs nailed the position (mid 6.1) but missed the orientation (std 37.2) — a textbook tilted local minimum. - **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 ~12.7 s (includes the pre-shift resample + its affine-MI init stages). All CPU-bound. - **Mattes MI (warped-CT ~ MR) is an independent, intensity-based confirmation**: medians 0.643 (SimpleITK) / 0.600 (ANTs) / 0.562 (Elastix), and it diverges sharply on the tilted cases — ANTs `m6_26TYQI3C` drops to 0.094 nats (rotation ~37 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. - **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); Elastix's ASGD is deterministic given its fixed seed. ## Reproducing ```bash # main env — SimpleITK + Elastix (itk-elastix is pip-installed here; # antspyx/deepreg report unavailable and are recorded as such) source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal python scripts/bench_registration.py --n-cases 10 \ --workers 4 --threads 24 --simpleitk-mode benchmark # isolated py3.12 env with antspyx — ANTs (+ optional SimpleITK cross-check) /home/xfr/.conda/envs/ants312/bin/python scripts/bench_registration.py \ --pipelines antspyx --n-cases 10 --workers 4 --threads 24 # combine the per-pipeline JSONs accumulated across envs into one table + plot python scripts/bench_registration.py --merge ``` Other flags: `--sample random --seed 0` (reproducible random 10), `--keys k1,k2,...` (explicit cases), `--n-cases N`, `--workers W`, `--threads T`, `--simpleitk-mode {benchmark,production}`, `--deepreg-ckpt <.ckpt>` (DeepReg), `--out-dir