This commit introduces a new script, `setup_impact_konfai.sh`, which facilitates a one-time setup for the IMPACT registration pipeline (impact-reg-konfai). The script builds a self-contained shim that includes: - A glibc 2.35 loader and libraries extracted from the niftyreg:2.1.1-ubuntu Docker image. - A self-contained LibTorch 2.8.0 (cu128) library. - A wrapper script that runs the stock elastix-impact binary through the glibc 2.35 loader with the appropriate library paths. The setup ensures compatibility with hosts that do not meet the required glibc and LibTorch versions, allowing the pipeline to function correctly. The script also verifies the installation and provides usage instructions.
43 KiB
Registration Pipeline Benchmark — CT → T1c (M6)
Benchmarks six cross-modality registration pipelines (five rigid;
impact is the native deformable B-spline preset of the ImpactReg package)
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 - Benchmark harness:
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
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. Shared rules for
every pipeline:
- No mask, no crop / pad: registration runs on the full input volumes
exactly as given (no
FixedMask/MovingMask, no head-box resampling, no padding). - The T1c (MR) is always the fixed image (moving = CT) for every
pipeline.
impact's ImpactReg MR_CT convention coincides with this rule: its output displacement field (defined on the fixed grid) is the contract transform T1c→CT directly — see the IMPACT section. (A CT clipped to a non-negative window reads as MR, so the T1c slot decides; both-CT pairs keep the given order.)
All pipelines also 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, niftyreg
(reg_aladin) initializes from the image centres, synthmorph needs none. One
consequence of the no-mask rule: the IMPACT metric only evaluates samples that
map inside the moving buffer. With the ImpactReg MR_CT direction the samples
are drawn from the tight T1c head crop (fixed, ~220×220×150 mm) and map mostly
inside the large CT head-neck FOV (moving, ~300×300×251 mm), so the gate
rarely fires; the pipeline still sets RequiredRatioOfValidSamples 0.1 (vs
the build default 0.25) as a safety override for the FOV mismatch — see the
IMPACT section. The comparison is on each toolkit's native optimizer / metric
/ global-search capability.
The six pipelines
Listed alphabetically. All register on the full volumes (no head box, no
mask, no crop / pad) 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, niftyreg
initializes from the image centres, and synthmorph needs none (global deep
method). All use fixed = T1c, moving = CT (shared rule above;
impact's ImpactReg MR_CT convention coincides with it). niftyreg and
synthmorph clip the CT (the moving image) to [0, 80] HU before
registration (per the CT-clipping A/B below; clip_ct=None disables it).
| # | pipeline | fixed / moving | init | metric | transform | status in this env |
|---|---|---|---|---|---|---|
| 1 | antspyx |
T1c / CT | auto COM | Mattes MI (multi-res) | ANTs Rigid |
✅ ran (main env; stochastic MI subsampling) |
| 2 | elastix |
T1c / CT | auto CenterOfGravity | Mattes MI (32 bins) | EulerTransform rigid (5.x), ASGD, 3 resolutions |
✅ ran (main env, in-process python API — no CLI) |
| 3 | impact |
T1c / CT (MR_CT) | auto CenterOfGravity (override) | IMPACT semantic (MIND R1D2_3D L1 + TotalSegmentator M730 deepest-layer Dice, per-level weighted, Static) + Mattes MI (w 0.4) + bending energy |
B-spline DVF (preset native), ASGD, 4 scales | ✅ ran (impact-reg-konfai + GPU; Elastix_IMPACT_Static preset — see IMPACT section) |
| 4 | niftyreg |
T1c / CT | image centres | NCC block matching + trimmed least squares (symmetric) | reg_aladin -rigOnly, 3-level pyramid |
✅ ran (Docker niftyreg:2.1.1-ubuntu, GPU -platf 1) |
| 5 | simpleitk |
T1c / CT | identity | Mattes MI (32 bins) | Euler3D rigid, pyramid [4,2,1] |
✅ ran |
| 6 | synthmorph |
T1c / CT | none (global) | SynthMorph anatomy-aware contrast-invariant (deep, pretrained) | 6-DOF rigid matrix | ✅ ran (main env, Docker + GPU) |
Benchmark budget (best-results settings): elastix and simpleitk run with
MaximumNumberOfIterations = 2000 (per resolution / per pyramid level),
with the sampler and metric left at their defaults (NumberOfSpatialSamples
5000, NumberOfHistogramBins 32). impact runs the published ImpactReg
preset Elastix_IMPACT_Static (the impact-reg-konfai package) — an
elastix B-spline (4 scales, FinalGridSpacing 14 mm, per-level
MaximumNumberOfIterations 400/300/200/200, NumberOfSpatialSamples
2000) driven by the IMPACT metric: a per-level MIND (L1) +
TotalSegmentator M730 (deepest layer, Dice) combination, layer weights
0.2/0.8 → 0.7/0.3, Static mode, + Mattes MI weight 0.4 + bending energy —
with CenterOfGravity auto-init and RequiredRatioOfValidSamples 0.1 applied
as raw-elastix overrides for the M6 pairs. antspyx, niftyreg and
synthmorph keep their native defaults.
simpleitk has two budgets: --simpleitk-mode benchmark (single pyramid,
identity start — the default, comparable to the other tools) and
--simpleitk-mode production (the full multi-start tilt grid from
02_build_m6_dataset.py, now also on the full volumes per the no-crop rule,
~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)
The headline alignment score is mi_ct_mr — the registration's achieved
Mattes MI, read off as a standalone score:
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). 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. Tables and plots report this (plus registration time and GTV-in-CT coverage).tv_in_ct_frac— fraction of the dilated warped GTV inside the warped-CT head tissue (the slab must straddle the lesion).reg_wall_s— wall-clock registration time (excludes I/O + scoring).
The production geometric QA is still computed per case and kept in the JSON
(it drives dataset acceptance in 02_build_m6_dataset.py, but its summary
numbers were found to correlate poorly with perceived alignment, so they are no
longer reported in the tables/plots):
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.midmeasures translational concentricity;stdmeasures rotational concentricity.tv_nvox— warped GTV volume.accepted—tv_nvox > 0 and tv_in_ct_frac ≥ 0.5 and aligned(the production acceptance rule; aligned ⇔|mid| ≤ 20 mmandstd ≤ 12 mm— a small negativemidis fine: on full-volume registration the CT skull ring can land slightly inside the MR silhouette, which is equally concentric).
Results (10 cases, no clip)
Alignment is scored by the achieved Mattes MI (nats, warped-CT ~ MR over
the MR head; higher = better) with the unclipped CT, so all rows are
comparable on the same score scale. Full volumes, no mask / crop / pad,
fixed = T1c / moving = CT (shared rule; impact's MR_CT convention
coincides with it). niftyreg and synthmorph clip the CT (the moving
image) to [0, 80] HU before registration (per the CT-clipping A/B below;
clip_ct=None disables it); antspyx, elastix and simpleitk register
the CT as-is. niftyreg is the v2.1.1 Docker build on the GPU (-platf 1);
impact runs the impact-reg-konfai package on the GPU (see the IMPACT
section).
| pipeline | toolkit | registered | s/reg (med) | tv-in-CT | MI (med/mean, nats) |
|---|---|---|---|---|---|
| antspyx | ANTsPy 0.63 | 9 | 15.5 | 0.93 | 0.652 / 0.650 |
| elastix | ITK-Elastix 0.25.4 | 9 | 9.8 | 0.93 | 0.646 / 0.638 |
| impact | impact-reg-konfai (Elastix 5.2 + IMPACT, Elastix_IMPACT_Static preset, MIND+M730, deformable) |
9 | 216.4 | 0.93 | 0.578 / 0.558 |
| niftyreg | NiftyReg 2.1.1 (reg_aladin, Docker + CUDA) | 9 | 16.2 | 0.93 | 0.292 / 0.339 |
| simpleitk | SimpleITK 2.5.6 | 9 | 9.6 | 0.93 | 0.647 / 0.642 |
| synthmorph | Docker SynthMorph (TensorFlow) | 9 | 27.9 | 0.93 | 0.277 / 0.328 |
Per-case MI (nats, unclipped score; bold = best for the case). All rows
are re-runs under the fixed = T1c / moving = CT convention (the shared
rule); the CT-fixed-era numbers are in the git history. The impact column
is the impact-reg-konfai deformable Elastix_IMPACT_Static preset (the
retired rigid Docker config is archived in
results/bench_registration/impact_docker_legacy.json).
| case | antspyx | elastix | impact | niftyreg | simpleitk | synthmorph |
|---|---|---|---|---|---|---|
| m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT |
| m6_22IRRKU6_20240115 | 0.648 | 0.644 | 0.542 | 0.292 | 0.647 | 0.277 |
| m6_23XIMO4M_20230222 | 0.654 | 0.652 | 0.543 | 0.632 | 0.654 | 0.616 |
| m6_24V62QQM_20230223 | 0.703 | 0.698 | 0.667 | 0.260 | 0.704 | 0.256 |
| m6_24V62QQM_20231017 | 0.601 | 0.590 | 0.587 | 0.493 | 0.597 | 0.484 |
| m6_24V62QQM_20240820 | 0.652 | 0.646 | 0.612 | 0.118 | 0.638 | 0.111 |
| m6_26TYQI3C_20221027 | 0.718 | 0.650 | 0.717 | 0.609 | 0.670 | 0.590 |
| m6_2BBN5XA4_20250411 | 0.577 | 0.577 | 0.258 | 0.069 | 0.579 | 0.061 |
| m6_2C4TKI6H_20240506 | 0.645 | 0.638 | 0.578 | 0.233 | 0.636 | 0.231 |
| m6_2CDNN7BH_20240102 | 0.655 | 0.650 | 0.523 | 0.340 | 0.652 | 0.323 |
Interpretation
- ANTsPy leads, with SimpleITK and Elastix within 0.006 of it (median MI
0.652 / 0.647 / 0.646): on the well-positioned cases the three cluster at
0.60–0.70 and all now survive the harder repositioned cases (0.58–0.65 on
m6_2BBN5XA4/m6_22IRRKU6). - Switching to fixed = T1c / moving = CT lifted Elastix sharply
(0.517 → 0.646 median, and it is now #3 rather than #4). The Mattes-MI
pipelines sample spatial pairs from the fixed buffer and discard the ones
that map outside the moving image; with the small T1c head crop as fixed and
the large CT head+neck FOV as moving, (nearly) all fixed samples are
in-buffer, versus only a minority when the big CT was fixed. Elastix's
repositioned cases jumped (
m6_2BBN5XA40.287→0.577,m6_2C4TKI6H0.401→0.638,m6_23XIMO4M0.416→0.652). The other pipelines are essentially unchanged by the swap (antspyx 0.653→0.652, simpleitk 0.651→0.647, niftyreg 0.292→0.292, synthmorph 0.277→0.277) — they were not sample-coverage limited. - The grossly repositioned cases still separate the leaders from the
tail. On
m6_2BBN5XA4/m6_24V62QQM_20240820(~30° inter-session tilt) ANTs/SimpleITK/Elastix hold 0.58–0.65, while niftyreg (0.07–0.12) and synthmorph (0.06–0.11) — the single-start block-matching and pretrained-MRI methods — lock onto local minima. This is exactly why production02_build_m6_dataset.pyuses a multi-start tilt grid. - IMPACT now runs the published
Elastix_IMPACT_Staticpreset (impact-reg-konfaipackage; the old Docker backend is retired): per-level MIND + TotalSegmentator M730 combination (L1 + deepest-layer Dice, weights 0.2/0.8 → 0.7/0.3, Static mode, whole-image features), the preset's native deformable B-spline (400/300/200/200 iterations, 2000 samples,- Mattes MI 0.4 + bending energy), registered in the MR_CT direction
(fixed=T1c) with CenterOfGravity auto-init and the 0.1 sample-validity gate
as overrides. It is the best of the IMPACT variants on M6 and now second
overall at median MI 0.578 (behind SimpleITK/ANTsPy 0.65; ahead of
elastix 0.52), a clear step up from the old rigid Docker config (median
0.467, archived in
impact_docker_legacy.json): the deformable refinement - MR_CT direction lifts the repositioned cases hardest (
m6_2C4TKI6H0.334→0.578,m6_23XIMO4M0.362→0.543,m6_22IRRKU60.467→0.542,m6_2CDNN7BH0.332→0.523;m6_26TYQI3C0.717 — now the best tool on that case) at a small cost on two well-aligned ones (m6_24V62QQM_202302230.700→0.667,m6_24V62QQM_202408200.646→0.612, still accepted). Concentricity also improved (median mid 12.0 / std 21.9 mm vs 21.1 / 28.0; 3/9 accepted). It is the slowest of the six (~216 s/case, the deformable optimization + KonfAI wrapper; the old rigid config was ~78 s). Them6_2BBN5XA4~30° tilt case is still its weakest (0.258) — the single CenterOfGravity start remains the limiting factor. Earlier Docker-era variants: MIND+MRSegmentator median 0.417 (~72 s), single-model MIND median 0.406 (~37 s), MRSegmentator-only Jacobian configs 0.631 (500/2000) and 0.591 (2000/5000) at ~5×/~35× the cost (archived:results/bench_registration/impact_marseg_*.json,impact_2000_5000.json,impact_500_2000_explicit_smoothing.json,impact_mind_500_ref.json).
- Mattes MI 0.4 + bending energy), registered in the MR_CT direction
(fixed=T1c) with CenterOfGravity auto-init and the 0.1 sample-validity gate
as overrides. It is the best of the IMPACT variants on M6 and now second
overall at median MI 0.578 (behind SimpleITK/ANTsPy 0.65; ahead of
elastix 0.52), a clear step up from the old rigid Docker config (median
0.467, archived in
- NiftyReg (reg_aladin) is mid-pack: single-start symmetric block matching
is strong on moderate cases (
m6_23XIMO4M0.632, best in class;m6_26TYQI3C0.609) but falls into the same tilt local minima (m6_2BBN5XA40.069,m6_24V62QQM_202408200.118). The v2.1.1 Docker build on the GPU (-platf 1) is ~21 s/case, ~4× slower than elastix (vs ~4 min CPU for the conda 1.5.69 build). It clips the CT to [0,80] HU by default (per the A/B below) — neutral on the unclipped score scale (0.296→0.292 median). - SynthMorph is weakest of the six on these cross-modality CT pairs
(median 0.277 with its default CT clip; 0.241 unclipped): its
rigidmodel is trained on brain MRI, so the ~30° tilt defeats it on the repositioned cases (0.06–0.11) even though it matches the intensity tools on the cleanest (m6_26TYQI3C0.590). The [0,80] brain-window clip of the CT is its only genuine A/B win (+0.036 median, 7/9 cases), so it is the default. It needs no centroid init (global deep method). tv-in-CTis ~0.93 for every pipeline: the warped GTV lands inside the CT head everywhere, so coverage is not the discriminator — MI is.- Speed (median wall, 4 cases in parallel, 24 threads, T1c-fixed re-run):
SimpleITK 9.6 s, elastix 9.8 s, ANTs 15.5 s (down from 36.6 s — the
smaller fixed image also shaves ANTs' per-level work), NiftyReg 16.2 s
(v2.1.1, GPU block matching), SynthMorph 27.9 s (GPU forward pass),
IMPACT 216.4 s (
Elastix_IMPACT_Staticpreset — Static feature mode, no backprop, but the deformable B-spline optimization at 1100 total iterations- the KonfAI wrapper; the retired rigid Docker config was 78.0 s). The earlier MRSegmentator Jacobian configs were far slower still: ~354 s/case at 500/2000, 2534 s/case at 2000/5000 (GPU feature extraction with backprop per iteration).
- 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;
the in-process Elastix ASGD is deterministic given its fixed seed;
SynthMorph is deterministic; NiftyReg is deterministic on the CPU
(conda 1.5.69) but shows small run-to-run variation on the GPU (v2.1.1,
~±0.01–0.05 MI on the weak cases). IMPACT in the Jacobian preset
(
Elastix_IMPACT_Jacobian) is the most stochastic: it draws random patches through the network every iteration, so the final pose changes run to run — treat such numbers as one sample from a distribution. The default Static preset re-samples coordinates every iteration as well, but the feature maps are deterministic given the pose.
CT clipping A/B (--clip-ct 0 80)
Question: does clipping the CT to a brain window ([0, 80] HU — skull and air removed) before registration improve the result? The harness clips the CT before registration AND before scoring (percentile-binned MI is scale-free, so the A/B compares like for like). Same 10 cases, same budgets.
Median achieved MI (nats) and wall-clock (9 complete-CT cases):
| pipeline | no clip | clip [0,80] | Δ (median MI) | clip wins (cases by MI) |
|---|---|---|---|---|
| antspyx | 0.653 / 36.6 s | 0.466 / 34.7 s | −0.186 | 0/9 |
| elastix | 0.517 / 6.0 s | 0.425 / 5.9 s | −0.092 | 1/9 |
| niftyreg | 0.296 / 23.8 s | 0.320 / 20.2 s | +0.024 | 5/9 |
| simpleitk | 0.651 / 10.6 s | 0.436 / 11.1 s | −0.215 | 0/9 |
| synthmorph | 0.241 / 26.0 s | 0.318 / 25.6 s | +0.077 | 5/9 |
| impact | abort | abort | - | - (historical: at the time both aborted on the old 0.25 sample-validity gate; the gate is now 0.1, A/B to be re-done) |
Verdict: no — clipping the CT to [0, 80] HU does not improve the result; do not clip.
- The headline alignment score drops for all three leading intensity pipelines: antspyx 0.653→0.466, simpleitk 0.651→0.436, elastix 0.517→0.425 (median nats; simpleitk and antspyx lose on all 9 cases, elastix on 8/9).
- The two weaker pipelines show small median-MI gains (niftyreg +0.024, synthmorph +0.077) but only mixed case-by-case (5/9), and their geometric QA does not support a real improvement.
- Geometric concentricity (skull-ring gap) is a wash: mean mid/std move by ~1–5 mm in either direction (niftyreg mid 23.6→18.1 the only clear gain; antspyx std 16.4→18.0 the clear loss). Production acceptance even falls (no-clip 4/4/2/4/2 → clip 1/0/1/0/2).
- Mechanism. Clipping is a saturating (non-invertible) intensity map: the skull atom (800+ HU → 80) removes the strongest intensity anchor (skull ↔ MR scalp) and compresses the CT rank distribution, so the percentile-binned MI between the warped CT and the MR is inherently lower for the same pose. The pose itself does not systematically degrade — the head is rigid, so skull-ring concentricity is its fingerprint, and that is unchanged. The MI drop is therefore mostly a score-scale effect, but it is exactly the score the benchmark reports, and there is no geometric benefit to pay it for.
- Scoring note:
_skull_ring_gaptests the ring with>= p97(not>) so the orientation gate and the gap QA also work on a clipped CT (where p97 sits exactly on the clipped-skull atom). - Decision / adopted default. The pipelines were updated:
niftyregandsynthmorphclip the CT to [0, 80] HU before registration (clip_ct=(0.0, 80.0)default,clip_ct=Nonedisables it; MR inputs are never clipped);antspyx/elastix/simpleitkregister the CT as-is. Re-checked on the unclipped score scale (the protocol used by the table above): synthmorph keeps a genuine gain (0.241→0.277 median, 7/9 cases), niftyreg is neutral (0.296→0.292; its apparent +0.024 above is mostly the clip-scoring effect). All rows of the main table are scored on the unclipped warped CT, so pipelines remain comparable.
Reproducing
# main env — SimpleITK + Elastix + ANTsPy + NiftyReg (niftyreg via Docker v2.1.1)
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py --pipelines simpleitk elastix antspyx niftyreg \
--n-cases 10 --workers 4 --threads 24 --simpleitk-mode benchmark
# force the conda 1.5.69 CPU build: --niftyreg-backend conda
# impact (pip; GPU; ~120-220 s/case; one-time elastix shim on hosts too old
# for the stock binary — see the IMPACT section). --impact-preset selects
# another published preset (e.g. Elastix_IMPACT_Jacobian, Generic_Rigid);
# --impact-iters / --impact-samples are no-ops for the default preset's
# per-resolution matrix
python scripts/bench_registration.py --pipelines impact --n-cases 10 \
--workers 3 --threads 24
# synthmorph (Docker, needs the pulled image; see the SynthMorph section)
python scripts/bench_registration.py --pipelines synthmorph --n-cases 10 \
--workers 4 --threads 24
# A/B: does clipping the CT to a brain window help? (--clip-ct clips the CT
# for ALL pipelines, before registration and scoring; niftyreg/synthmorph
# already clip by default, so this only affects antspyx/elastix/simpleitk)
python scripts/bench_registration.py --n-cases 10 --workers 4 --threads 24 \
--out-dir results/bench_reg_noclip
python scripts/bench_registration.py --n-cases 10 --workers 4 --threads 24 \
--clip-ct 0 80 --out-dir results/bench_reg_clip0_80
python scripts/bench_registration.py --merge --out-dir results/bench_reg_noclip
python scripts/bench_registration.py --merge --out-dir results/bench_reg_clip0_80
# 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 (also the OpenMP thread count for reg_aladin),
--simpleitk-mode {benchmark,production},
--synthmorph-model {rigid,affine}, --synthmorph-gpu <int|-1>,
--synthmorph-extent <int>, --impact-preset <name> (default
Elastix_IMPACT_Static), --impact-gpu <int|-1>, --impact-iters N
(preset max_iterations override; no-op for matrix presets),
--impact-samples N (preset spatial_samples override),
--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 and the achieved Mattes MI, so a glance tells you whether the visuals agree with the score. Generate for the available pipelines with:
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py \
--pipelines simpleitk elastix antspyx niftyreg --n-cases 10 --qa-images
python scripts/bench_registration.py \
--pipelines synthmorph --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
image: SynthMorph (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):
# 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
- Rigid matrix model, no init —
register -m rigidon GPU when one is visible (auto-spread over the host GPUs by pid;--synthmorph-gpu -1forces CPU). SynthMorph is a global deep method, so it recovers the gross inter-session offset on its own and needs no initial transform. - Read-back — the model estimates first→second, so the pair is passed as
(fixed=T1c, moving=CT)and the-TLTA (aLINEAR_RAS_TO_RAS4×4 in shared world space) is the T1c→CT contract transform directly; it is parsed into a SimpleITKAffineTransformand run through the usualfix_orientationgate. - GPU sharing — the container is run as the host uid (clean scratch
removal, TF still reaches the GPU) with
TF_FORCE_GPU_ALLOW_GROWTHset so concurrent benchmark workers can share a GPU.
NiftyReg pipeline — one-time setup
The niftyreg pipeline runs reg_aladin from the NiftyReg
suite (KCL-BMEIS, the actively-maintained NiftyReg): symmetric block
matching + trimmed least squares (Modat et al., J. Med. Img. 1(2) 024003,
2014). Instead of a histogram MI objective it matches 4-voxel image blocks by
normalised cross-correlation at each level of a 3-level Gaussian pyramid
and solves a robust (trimmed) least-squares for the rigid transform — a
genuinely different algorithmic family, and cross-modality tolerant because
NCC is invariant to intensity scaling.
Default backend: the official v2.1.1 release in Docker. The v2.x release
binaries need GLIBC_2.34, which this host (Ubuntu 20.04, glibc 2.31) lacks, so
the v2.1.1 Ubuntu zip is packaged in the niftyreg:2.1.1-ubuntu image
(nvidia/cuda ubuntu22.04 base for the CUDA plugin). GPU runs use
docker run --gpus all + -platf 1 -gpuid N and are ~20× faster than CPU;
the v2.1.1 CUDA path supports linear interpolation only (-interp 1), so
GPU runs use linear and CPU/conda runs use cubic (-interp 3) — the poses
they recover on these cases agree to ~4 decimal places. The conda-forge
build (1.5.69, CPU) remains the fallback when the Docker image is absent
(--niftyreg-backend conda).
Prerequisites (check with RP.availability(); a missing piece raises
PipelineUnavailable with the exact command):
# default: build the v2.1.1 Docker image (one-time)
docker build -t niftyreg:2.1.1-ubuntu niftyreg/ # from NiftyReg-Ubuntu-v2.1.1.zip
# fallback: conda-forge build (provides reg_aladin + reg_f3d, NiftyReg 1.5.69)
conda install -c conda-forge niftyreg
How the adapter drives the tool
- Rigid 6-DOF, full volumes —
reg_aladin -ref T1c -flo CT -rigOnly(the T1c is the reference/fixed image, the CT the floating/moving one; the CT is clipped to [0,80] HU first). Symmetric variant on by default, which matters: the non-symmetric mode drifts grossly on these repositioned pairs.-ln 3pyramid,-maxit 15TLS iterations per level (the tool's default of 5 is too coarse),-interp 1(GPU) /-interp 3(CPU) internal warping, default initialisation (image centres). GPU:--gpus all,-platf 1,-gpuid <auto by pid>; CPU/conda:-platf 0(v2.1.1) or no-platf(1.5.69). - Read-back — the output affine (
-aff) is a 4×4 row-major matrix, documented asAffine * Reference = Floating; the adapter parses it into a SimpleITKAffineTransformand runs it through the usualfix_orientationgate (skull-ring concentricity), which resolves the direction empirically rather than trusting the docs. Under-ref T1c -flo CTthe file's affine comes out the other way, so the gate inverts it on the M6 cases (8/9auto_inverted: True) to the contract transform (T1c→CT).
IMPACT pipeline (impact-reg-konfai) — one-time setup
The impact pipeline runs the
impact-reg-konfai package:
IMPACT registration presets from the
VBoussot/ImpactReg Hugging Face
repo, driven through the KonfAI
runtime — no Docker. The default preset is Elastix_IMPACT_Static:
Elastix 5.2 with the IMPACT semantic similarity metric — a per-level
combination of the MIND handcrafted descriptor (MIND/R1D2_3D.pt, L1,
subset 32) and TotalSegmentator TS/M730.pt (deepest layer, Dice, subset
64), layer weights 0.2/0.8 → 0.7/0.3 from coarse to fine, Static mode
(whole-image features, no backprop), a 4-scale B-spline (FinalGridSpacing
14 mm), 400/300/200/200 iterations per level, 2000 spatial samples, + Mattes
MI (w 0.4) + bending energy — the published successor of the MR_CT_TS
reference parameter map this repo used to hand-build (and replace the
vboussot/elastix_impact Docker image). Other presets are selectable with
--impact-preset (Elastix_IMPACT_Jacobian, Generic_Rigid,
Generic_Rigid_BSpline, FireANTs_Anatomix/IMPACT/SyN,
ConvexAdam_Coarse/Composite; the live database is listed by
impact-reg-konfai register --help). It is GPU-based (like synthmorph),
but pip-installed.
Prerequisites (check with RP.availability(); a missing piece raises
PipelineUnavailable with the exact command):
# in the longitudinal-313 env (Python >= 3.11):
pip install impact-reg-konfai # CLI + KonfAI runtime (pulls h5py etc.)
bash scripts/setup_impact_konfai.sh # one-time, this host only: the elastix shim
The CLI then auto-downloads the one-time assets on first register: the
preset app (HF VBoussot/ImpactReg), the TorchScript feature models (HF
VBoussot/impact-torchscript-models: MIND ~15 KB + TS/M730 ~200 MB) and the
elastix-impact 1.0.0 binary (GitHub vboussot/ImpactElastix, ~40 MB)
into ~/.cache (HF cache + ~/.cache/konfai).
Why the elastix shim (this host only)
The stock binary needs glibc ≥ 2.35 and links LibTorch 2.8.0; this
host (Ubuntu 20.04, glibc 2.31, env torch 2.14) satisfies neither, so
scripts/setup_impact_konfai.sh builds a self-contained shim at
~/.cache/konfai/elastix-impact-shim: the glibc 2.35 loader + libs extracted
from the niftyreg:2.1.1-ubuntu image (docker is needed once for that copy
only), the self-contained LibTorch 2.8.0 cu128 from download.pytorch.org
(~3.8 GB zip), and a wrapper bin/elastix that runs the stock binary through
the shim loader with the LibTorch 2.8.0 library path (stripping the inherited
LD_LIBRARY_PATH, so the env's torch 2.14 cannot leak in). The pipeline
selects the shim via KONFAI_ELASTIX_DIR (_impact_elastix_root); a host
new enough to run the stock binary directly needs no shim at all.
How the adapter drives the CLI
- Full volumes, fixed=T1c (MR), moving=CT — the ImpactReg MR_CT
convention, which coincides with the shared rule (the T1c is the fixed
image): the preset's output is a displacement field on the fixed grid
mapping fixed→moving, so the field is the contract transform T1c→CT
directly (displacement fields cannot be inverted with
GetInverse, which a fixed=CT direction would require). It also keeps the metric's sample-validity gate healthy: samples drawn from the tight T1c head crop (~220×220×150 mm) map mostly inside the large CT head-neck FOV (~300×300×251 mm), unlike fixed=CT where only ~14% were in-buffer at the auto-init. - M6 robustness overrides — the preset's map is tuned for well-aligned
pairs, so the adapter forwards raw elastix lines through the package's
parameter_overridesknob:AutomaticTransformInitialization=true+AutomaticTransformInitializationMethod=CenterOfGravity(the preset's GeometricalCenter start leaves the intensity centroids ~50-100 mm apart on the head+neck FOV mismatch) andRequiredRatioOfValidSamples 0.1(build default 0.25; keeps the divergence gate while letting the FOV-mismatched runs proceed). --fields-only— the adapter derives the warp itself (the package's derived "Moved" image would be an extra full-size resample of the same content).- GPU auto-spread across the host GPUs by pid (
--impact-gpu -1forces CPU; the engine then turns mixed precision off). - Read-back — the transform comes from
<out>/P000/Transform.h5(an ITK-form HDF5DisplacementFieldTransformon the T1c grid; the SimpleITK build here has no ITK HDF5 transform reader, so it is parsed with h5py) and direction-checked by_orient_impact: the skull-ring gap of the field vs a sign-flipped (first-order reverse) proxy — thefix_orientationanalogue for a field with noGetInverse. - Intensity normalization happens inside the metric / model (no pipeline-side normalization): the MIND descriptor normalizes each 5-voxel neighbourhood to [0, 1] locally (modality-agnostic by construction); the segmentation models (TS/MRSeg) are standardized to zero mean / unit variance per the model card. Pre-normalizing the volumes would be an exact no-op for the z-scored models and would only corrupt the HU scale used by MI scoring and the QA brain-window images.
Notes / caveats
- The benchmark compares each toolkit in its native configuration: all
register on the full volumes (no head box, no mask, no crop / pad) and use
each toolkit's native automatic transform initialization (no manual
centroid_offset) — elastix / impactAutomaticTransformInitialization(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. synthmorphis deterministic (one fixed pretrained model, no random subsampling), so its per-case numbers are stable run to run — unlike ANTs / IMPACT. Its low MI (median 0.238; 0.06–0.10 on the repositioned cases) is a genuine cross-modality / tilt weakness, not variance.fix_orientation(elastix/ants/niftyreg/synthmorph) 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, so the fixed/moving swap is absorbed by the gate rather than re-derived per toolkit. Under the shared fixed=T1c / moving=CT rule the raw transforms come out in the contract direction for elastix (combination transform = fixed→moving), antspyx (ANTs forward transform) and synthmorph (the-TLTA, model estimates first→second) — completed runs reportauto_inverted: Falsefor those;niftyreg's-afffile comes out the other way and the gate inverts it (8/9auto_inverted: True).impactreturns a displacement field (noGetInversein ITK), so it uses_orient_impact— the same skull-ring decision against a sign-flipped (first-order reverse) proxy field; its MR_CT field is in the contract direction (auto_inverted: False).- SynthMorph specifics (Docker
freesurfer/synthmorph, TensorFlow, GPU):- Cross-modality / tilt weakness. The
rigidmodel 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 havestd > 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
-iinitial-transform path is not used (which also sidesteps this image's float64/float32 bug on the-icode path). - GPU sharing. The container runs as the host uid with
TF_FORCE_GPU_ALLOW_GROWTHso concurrent benchmark workers share a GPU.
- Cross-modality / tilt weakness. The
- ~25 s/case (GPU: one pretrained forward pass), between SimpleITK (~10 s) / ANTs (~37 s) and IMPACT (~216 s, deformable preset).
- NiftyReg specifics (conda-forge
niftyreg1.5.69,reg_aladin, symmetric block matching + TLS, CPU/OpenMP):- Different algorithmic family. No histogram MI objective: 4-voxel
blocks are matched by normalised cross-correlation and the rigid
transform is solved by a trimmed least squares, iterated per pyramid
level (
-ln 3 -maxit 15). NCC's scale invariance is what makes it cross-modality tolerant. - Symmetric mode matters. The default symmetric variant (optimising
forward and backward transforms concurrently) is the robust one on these
repositioned pairs;
-noSymdrifts grossly (gap std 45 mm on a clean case every other tool passes). -maxit 5(the tool default) is too coarse — on the clean casem6_26TYQI3Cit reaches MI 0.41 vs 0.61 at 15 TLS iterations/level; 15 is the benchmark setting.- GPU is the default now (v2.1.1,
-platf 1viadocker run --gpus all): ~20–25 s/case on the full volumes vs ~4 min CPU (120–440 s,-ompthread-bound) for the conda 1.5.69 build.--niftyreg-gpu -1forces the CPU platform (-platf 0). - Version note. The upstream KCL-BMEIS release binaries (v2.x) need
glibc ≥ 2.34; this host (Ubuntu 20.04, glibc 2.31) runs them in the
niftyreg:2.1.1-ubuntuDocker image (nvidia/cuda ubuntu22.04 base; the CUDA plugin needs libcusolver.so.11, provided by the image). The conda-forge 1.5.69 build remains as the no-Docker fallback and gives essentially identical poses (MI matches to ±0.001 on the 10 cases).
- Different algorithmic family. No histogram MI objective: 4-voxel
blocks are matched by normalised cross-correlation and the rigid
transform is solved by a trimmed least squares, iterated per pyramid
level (
- 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,Rigid3DTransformis gone from the component set), the metric key isMetric(notImageMetric), the random sampler isRandom(notRandomSampler). The adapter uses the 5.x names and is not compatible with elastix ≤ 4.x parameter files. AutomaticTransformInitialization "true"(CenterOfGravity) provides the start (no manualcentroid_offset);AutomaticScalesEstimationsets the optimizer scales. An output directory is still set (SetOutputDirectory) for the C++ filter to run; the adapter uses a scratch dir underdata/m6reg/elastix/itkelastix/.WriteResultImagemust be the string"false"(PythonFalse); the default (absent) makes the filter raise "Could not read result image" after a successful registration.
- Elastix 5.x renamed components vs the classic parameter files: the rigid
3D transform is
- With fixed=T1c/moving=CT the combination transform is T1c→CT — the
contract direction — so
fix_orientationkeeps it as-is (auto_inverted: False). The result is recovered by point-mapping (GetCombinationTransform().TransformPoint) into an affine — independent of elastix's Euler-angle parameterization — and wrapped in a SimpleITKAffineTransform(3).- Benchmark budget:
MaximumNumberOfIterations2000 per resolution (ASGD 2000/2000/2000 over 3 resolutions) with the sampler/metric at their defaults (NumberOfSpatialSamples5000,NumberOfHistogramBins32).
- Benchmark budget:
- IMPACT specifics (
impact-reg-konfai1.8.6, KonfAI runtime, elastix-impact 5.2, GPU; presetElastix_IMPACT_Static):- Runs on M6 with the T1c-fixed direction + relaxed sample-validity
gate. Full volumes, no mask, no crop / pad, fixed=T1c/moving=CT (the
shared rule; the ImpactReg MR_CT convention coincides with it): the
preset's output field is on the fixed grid, so that direction is the
contract transform (T1c→CT) directly, and the metric's samples (drawn
from the tight T1c crop) map mostly inside the large CT FOV. The package's generated map still defaults to
RequiredRatioOfValidSamples 0.25, so the adapter overrides it to 0.1 (a genuinely diverging transform, <10% in-buffer, still aborts). In the old Docker era (fixed=CT) the gate aborted the no-mask M6 pairs until the same 0.1 relaxation. The transform contract holds;_orient_impactdecides the direction as usual (completed runs reportauto_inverted: False— the MR_CT field is already the contract). - Auto init.
AutomaticTransformInitialization(CenterOfGravity) is applied as an override (the preset's GeometricalCenter start is too crude for the head+neck FOV mismatch); no manual translation is used. - Deformable, not rigid. The preset's native transform is a
RecursiveBSpline (FinalGridSpacing 14 mm, 4 scales) regularized by the
bending-energy metric; the old pipeline forced a rigid
EulerTransformin place of it. The benchmark contract (warp the CT concentric with the MR head) is transform-agnostic, so this is a native-config comparison point like every other row. - History: single-init convergence. The semantic landscape has more
local minima than Mattes-MI; earlier (masked, fixed=CT) runs no longer
nailed the hardest repositioned case (
m6_22IRRKU6mid 22.7 / std 27.0; the pre-mask head-box flow got it to mid 8.3 / std 13.2). The no-mask MRSegmentator runs showed the same pattern (0.635 MI onm6_22IRRKU6at 2000/5000 but mid 26.1 / std 28.6 there; accepted onm6_2BBN5XA4, mid 15.1 / std 10.7). The single CenterOfGravity start on the ~30° tilt pairs remains IMPACT's limiting factor — a multi-start (or--ttaflips through the preset app) would be the next step. - Stochastic. The Jacobian preset (
Elastix_IMPACT_Jacobian) resamples patches through the network each iteration, so results vary run to run; the default Static preset re-samples coordinates each iteration but the feature maps are deterministic given the pose (see Stochasticity). - The elastix shim (this host). The stock binary needs glibc ≥ 2.35
- LibTorch 2.8.0; the host (glibc 2.31, torch 2.14) runs it through
~/.cache/konfai/elastix-impact-shim(built byscripts/setup_impact_konfai.sh, selected viaKONFAI_ELASTIX_DIR). Same glibc story as the niftyreg v2.x binaries.
- LibTorch 2.8.0; the host (glibc 2.31, torch 2.14) runs it through
- Models / presets are not in the repo. They auto-download from HF
(
VBoussot/ImpactReg,VBoussot/impact-torchscript-models) into~/.cache/huggingfaceon first use; the olddata/impact_models/cache is obsolete. - CPU fallback:
--impact-gpu -1(no--gpuflag; the engine runs the feature models on CPU with mixed precision off — much slower).
- Runs on M6 with the T1c-fixed direction + relaxed sample-validity
gate. Full volumes, no mask, no crop / pad, fixed=T1c/moving=CT (the
shared rule; the ImpactReg MR_CT convention coincides with it): the
preset's output field is on the fixed grid, so that direction is the
contract transform (T1c→CT) directly, and the metric's samples (drawn
from the tight T1c crop) map mostly inside the large CT FOV. The package's generated map still defaults to