feat(docs): update README and benchmark documentation for IMPACT pipeline details

This commit is contained in:
Furen Xiao 2026-09-28 22:09:03 +08:00
parent 7ac9979c62
commit 6b85e950e5
4 changed files with 175 additions and 67 deletions

View file

@ -44,6 +44,8 @@ data/
pseudo/roundK/ in-house pseudo-labels (rows.jsonl, masks, summary.json) pseudo/roundK/ in-house pseudo-labels (rows.jsonl, masks, summary.json)
pseudo_nnu/roundK/ nnU-Net pseudo-labels (rows.jsonl, masks, summary.json) pseudo_nnu/roundK/ nnU-Net pseudo-labels (rows.jsonl, masks, summary.json)
pseudo_monai/roundK/ MONAI pseudo-labels (rows.jsonl, masks, summary.json) pseudo_monai/roundK/ MONAI pseudo-labels (rows.jsonl, masks, summary.json)
impact_models/ IMPACT TorchScript feature-extractor cache (registration
`impact` pipeline, see REGISTRATION_BENCHMARK.md)
src/ U-Net, dataset, losses, training/eval helpers src/ U-Net, dataset, losses, training/eval helpers
scripts/ Pipeline A (in-house 3D U-Net) scripts/ Pipeline A (in-house 3D U-Net)
scripts_nnu/ Pipeline B (nnU-Net) scripts_nnu/ Pipeline B (nnU-Net)

View file

@ -1,6 +1,6 @@
# Registration Pipeline Benchmark — CT → T1c (M6) # Registration Pipeline Benchmark — CT → T1c (M6)
Benchmarks **four rigid, cross-modality registration pipelines** that align a 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 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 T1c (MR) native grid. This is the registration step of
`scripts/02_build_m6_dataset.py`. The pairs are hard: different modality (CT `scripts/02_build_m6_dataset.py`. The pairs are hard: different modality (CT
@ -34,8 +34,9 @@ handling — not on who finds the ~30–60 mm gross offset first.
|---|----------|---------|--------|-----------|--------------------| |---|----------|---------|--------|-----------|--------------------|
| 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) | | 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 | | 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) | | 3 | `antspyx` | ANTsPy / ANTs 0.6.3 | Mattes MI (multi-res) | ANTs `Rigid` | ✅ ran (main env; stochastic MI subsampling) |
| 4 | `deepreg` | Oxford DeepReg (CycleGAN) | CycleGAN + reg. loss (pretrained) | 3D displacement field | **not installed** (see below) | | 4 | `deepreg` | Oxford DeepReg (CycleGAN) | CycleGAN + reg. loss (pretrained) | 3D displacement field | **not installed** (see below) |
| 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, `simpleitk` has two budgets: `--simpleitk-mode benchmark` (single pyramid,
centered start — the default, comparable to the other tools) and centered start — the default, comparable to the other tools) and
@ -85,75 +86,97 @@ Production QA, crop-independent:
| pipeline | toolkit | registered | accepted | gap mid (med/mean) | gap std (med/mean) | s/reg (med) | tv-in-CT | MI (med/mean, nats) | | 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** | | **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** | | **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** | | **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** |
| **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** |
| deepreg | — | — | — | — | — | — | — | — | | deepreg | — | — | — | — | — | — | — | — |
*(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.)*
Per-case (mid / std, `OK` = accepted): Per-case (mid / std, `OK` = accepted):
| case | simpleitk | antspyx | elastix | | case | simpleitk | antspyx | elastix | impact |
|---|---|---|---| |---|---|---|---|---|
| m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | | m6_22GD5VL2_20230724 | incomplete-CT | incomplete-CT | incomplete-CT | incomplete-CT |
| m6_22IRRKU6_20240115 | 25.7 / 29.3 | 26.3 / 29.2 | 26.4 / 30.3 | | m6_22IRRKU6_20240115 | 25.7 / 29.3 | 24.4 / 27.7 | 26.4 / 30.3 | **8.4 / 12.6** |
| m6_23XIMO4M_20230222 | 22.2 / 23.9 | 23.8 / 25.0 | 21.1 / 23.3 | | m6_23XIMO4M_20230222 | 22.2 / 23.9 | 23.4 / 24.8 | 21.1 / 23.3 | 21.4 / 24.1 |
| m6_24V62QQM_20230223 | 10.7 / 16.9 | 9.4 / 18.2 | 10.6 / 8.2 **OK** | | m6_24V62QQM_20230223 | 10.7 / 16.9 | 9.5 / 18.4 | 10.6 / 8.2 **OK** | 10.0 / 8.3 **OK** |
| m6_24V62QQM_20231017 | 11.5 / 7.3 **OK** | 12.5 / 12.8 | 11.0 / 6.1 **OK** | | m6_24V62QQM_20231017 | 11.5 / 7.3 **OK** | 14.9 / 15.5 | 11.0 / 6.1 **OK** | 11.1 / 8.8 **OK** |
| m6_24V62QQM_20240820 | 11.6 / 9.6 **OK** | 14.8 / 11.7 **OK** | 15.3 / 14.0 | | m6_24V62QQM_20240820 | 11.6 / 9.6 **OK** | 14.3 / 17.5 | 15.3 / 14.0 | 54.4 / 15.1 |
| m6_26TYQI3C_20221027 | 12.2 / 3.7 **OK** | 6.1 / 37.2 | 11.4 / 3.2 **OK** | | m6_26TYQI3C_20221027 | 12.2 / 3.7 **OK** | 17.7 / 37.6 | 11.4 / 3.2 **OK** | 23.9 / 22.6 |
| m6_2BBN5XA4_20250411 | 18.6 / 8.0 **OK** | 14.3 / 12.7 | 18.1 / 9.9 **OK** | | m6_2BBN5XA4_20250411 | 18.6 / 8.0 **OK** | 14.3 / 12.7 | 18.1 / 9.9 **OK** | 10.1 / 5.9 **OK** |
| m6_2C4TKI6H_20240506 | 12.6 / 15.4 | 13.6 / 14.7 | 12.0 / 6.8 **OK** | | m6_2C4TKI6H_20240506 | 12.6 / 15.4 | 13.1 / 14.3 | 12.0 / 6.8 **OK** | 12.4 / 22.3 |
| m6_2CDNN7BH_20240102 | 19.4 / 30.2 | 18.2 / 33.6 | 18.9 / 22.7 | | m6_2CDNN7BH_20240102 | 19.4 / 30.2 | 17.2 / 35.4 | 18.9 / 22.7 | 19.6 / 30.3 |
### Interpretation ### Interpretation
- **Translation is recovered well by all three** (mid gaps mostly 6–20 mm, - **Translation is recovered well by all four** (mid gaps mostly 6–20 mm,
within the `≤ 20 mm` band). `tv-in-CT` is identical (~0.93): the warped GTV 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 lands inside the CT head for every pipeline, so the target is in the right
place. place.
- **The binding failure is rotation** — `std > 12 mm` on the repositioned cases - **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 (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` start can lock onto). This is exactly why production `02_build_m6_dataset.py`
uses a **multi-start tilt grid**. uses a **multi-start tilt grid**.
- **Elastix is the most accurate of the three standard single-init configs** - **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 24.8) and (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 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 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` 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 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 (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 others accept — different optimizers settle in different local minima on the
hard tilted cases. hard tilted cases.
- **ANTs' Rigid is the weakest** (1/10 accepted): it frequently locks onto the - **ANTs' Rigid is the weakest of the intensity-metric tools** (0/10 in this
tilted local minimum. Note `m6_26TYQI3C`: ANTs nailed the position (mid 6.1) draw; it is stochastic, so a prior draw gave 1/10): it frequently locks onto
but missed the orientation (std 37.2) — a textbook tilted local minimum. 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 - **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, (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). ANTs ~14.5 s (CPU-bound), **IMPACT ~562 s** (GPU-bound: every ASGD iteration
All CPU-bound. runs the TotalSegmentator extractor on the sampled patches with backprop).
- **Mattes MI (warped-CT ~ MR) is an independent, intensity-based confirmation**: - **Mattes MI (warped-CT ~ MR) is an independent, intensity-based confirmation**:
medians 0.643 (SimpleITK) / 0.600 (ANTs) / 0.562 (Elastix), and it diverges medians 0.643 (SimpleITK) / 0.598 (ANTs) / 0.562 (Elastix) / 0.475 (IMPACT),
sharply on the tilted cases — ANTs `m6_26TYQI3C` drops to 0.094 nats and it diverges sharply on the tilted cases — ANTs `m6_26TYQI3C` drops to
(rotation ~37 mm off) while SimpleITK holds 0.670. A high MI means the CT ~0.1 nats (rotation ~38 mm off) while SimpleITK holds 0.670. A high MI means
skull/tissue intensities fall on the matching MR structures after the inverse the CT skull/tissue intensities fall on the matching MR structures after the
transform. inverse transform. IMPACT's lower median MI is consistent with its less
reliable single-init convergence.
- **Stochasticity**: ANTs' Mattes-MI subsamples the image at random, so its - **Stochasticity**: ANTs' Mattes-MI subsamples the image at random, so its
per-case numbers carry run-to-run variance (this table is one draw); per-case numbers carry run-to-run variance (this table is one draw);
SimpleITK's MI (and registration) is deterministic at fixed thread count SimpleITK's MI (and registration) is deterministic at fixed thread count
(RSGD wall time is thread-sensitive, convergence is not); Elastix's ASGD is (RSGD wall time is thread-sensitive, convergence is not); the in-process
deterministic given its fixed seed. 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 ## Reproducing
```bash ```bash
# main env — SimpleITK + Elastix (itk-elastix is pip-installed here; # main env — SimpleITK + Elastix + ANTsPy (all pip-installed here); deepreg
# antspyx/deepreg report unavailable and are recorded as such) # reports unavailable and is recorded as such
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313 source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py --n-cases 10 \ python scripts/bench_registration.py --n-cases 10 \
--workers 4 --threads 24 --simpleitk-mode benchmark --workers 4 --threads 24 --simpleitk-mode benchmark
# isolated py3.12 env with antspyx — ANTs (+ optional SimpleITK cross-check) # IMPACT (needs Docker + the pulled image + a model; see the Impact section
/home/xfr/.conda/envs/ants312/bin/python scripts/bench_registration.py \ # below for the one-time setup)
--pipelines antspyx --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 # combine the per-pipeline JSONs accumulated across envs into one table + plot
python scripts/bench_registration.py --merge python scripts/bench_registration.py --merge
@ -162,8 +185,9 @@ python scripts/bench_registration.py --merge
Other flags: `--sample random --seed 0` (reproducible random 10), Other flags: `--sample random --seed 0` (reproducible random 10),
`--keys k1,k2,...` (explicit cases), `--n-cases N`, `--workers W`, `--keys k1,k2,...` (explicit cases), `--n-cases N`, `--workers W`,
`--threads T`, `--simpleitk-mode {benchmark,production}`, `--threads T`, `--simpleitk-mode {benchmark,production}`,
`--deepreg-ckpt <.ckpt>` (DeepReg), `--out-dir <dir>`, `--deepreg-ckpt <.ckpt>` (DeepReg), `--impact-model <rel|abs>`,
`--qa-images` / `--qa-out <dir>` (visual QA, below). `--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) ## Visual QA images (checkerboard + color overlay)
@ -189,10 +213,11 @@ The title carries the pipeline, case key, Mattes MI, and the skull-ring gap
visuals agree. Generate for the available pipelines with: visuals agree. Generate for the available pipelines with:
```bash ```bash
/home/xfr/.conda/envs/longitudinal-313/bin/python scripts/bench_registration.py \ source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
--pipelines simpleitk elastix --n-cases 10 --qa-images python scripts/bench_registration.py \
/home/xfr/.conda/envs/ants312/bin/python scripts/bench_registration.py \ --pipelines simpleitk elastix antspyx --n-cases 10 --qa-images
--pipelines antspyx --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.) (9 images each; the incomplete-CT case is skipped.)
@ -213,6 +238,61 @@ python scripts/bench_registration.py --pipelines deepreg --n-cases 10 \
The adapter runs `deepreg test` to predict a voxel displacement field, converts The adapter runs `deepreg test` to predict a voxel displacement field, converts
it to a SimpleITK `DisplacementFieldTransform`, and scores it like the others. it to a SimpleITK `DisplacementFieldTransform`, and scores it like the others.
## 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 ## Notes / caveats
- The benchmark compares **standard single-init** configurations (centered - The benchmark compares **standard single-init** configurations (centered
@ -221,11 +301,11 @@ it to a SimpleITK `DisplacementFieldTransform`, and scores it like the others.
the head-to-head but is the config the dataset actually ships with. the head-to-head but is the config the dataset actually ships with.
- `deepreg` could not be executed here, so its row is "not installed", not - `deepreg` could not be executed here, so its row is "not installed", not
measured; its code path is implemented and unit-verified for import/convention. measured; its code path is implemented and unit-verified for import/convention.
- `fix_orientation` (elastix/ants/deepreg) chooses between a transform and its - `fix_orientation` (elastix/ants/deepreg/impact) chooses between a transform
inverse by skull-ring concentricity — a correctness gate that makes the result and its inverse by skull-ring concentricity — a correctness gate that makes
independent of each toolkit's transform-direction convention. In practice it the result independent of each toolkit's transform-direction convention. In
never fired (all `auto_inverted: False`): elastix/ants returned the T1c→CT practice it never fired (all `auto_inverted: False`): elastix/ants/impact
direction as expected. returned the T1c→CT direction as expected.
- **Elastix specifics** (ITK-Elastix 0.25.4, elastix 5.3.1, in-process python - **Elastix specifics** (ITK-Elastix 0.25.4, elastix 5.3.1, in-process python
API — no CLI binary involved): API — no CLI binary involved):
- Elastix 5.x renamed components vs the classic parameter files: the rigid - Elastix 5.x renamed components vs the classic parameter files: the rigid
@ -247,4 +327,28 @@ it to a SimpleITK `DisplacementFieldTransform`, and scores it like the others.
`AffineTransform(3)`. `AffineTransform(3)`.
- Registration wall time is ~1.8 s/case at full budget (ASGD 500/500/250 - 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 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. 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).

View file

@ -28,15 +28,17 @@ see REGISTRATION_BENCHMARK.md). The `--merge` mode then combines
the per-pipeline JSON files (accumulated across envs) into one summary + plot. the per-pipeline JSON files (accumulated across envs) into one summary + plot.
Usage (repo root): Usage (repo root):
# main env (SimpleITK + elastix; impact needs Docker + the image; deepreg unavailable) # main env (SimpleITK + elastix + antspyx; impact needs Docker + the image;
# deepreg unavailable)
source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313 source /opt/conda/etc/profile.d/conda.sh && conda activate longitudinal-313
python scripts/bench_registration.py --pipelines simpleitk elastix impact python scripts/bench_registration.py --pipelines simpleitk elastix antspyx
# isolated env that has antspyx (see REGISTRATION_BENCHMARK.md) # impact (GPU, Docker) — one-time setup in REGISTRATION_BENCHMARK.md
source /opt/conda/etc/profile.d/conda.sh && conda activate <ants312> python scripts/bench_registration.py --pipelines impact
python scripts/bench_registration.py --pipelines antspyx simpleitk
# combine everything produced so far into one table + plot # any pipeline that is not importable in the current env is recorded as
# unavailable, so the script is env-portable; --merge combines the
# per-pipeline JSONs accumulated across envs into one table + plot
python scripts/bench_registration.py --merge python scripts/bench_registration.py --merge
""" """
import argparse import argparse
@ -482,12 +484,12 @@ def parse_args():
ap.add_argument("--impact-gpu", type=int, default=None, ap.add_argument("--impact-gpu", type=int, default=None,
help="GPU device index for IMPACT (default: auto-spread over " help="GPU device index for IMPACT (default: auto-spread over "
"the host GPUs by pid; -1 forces CPU)") "the host GPUs by pid; -1 forces CPU)")
ap.add_argument("--impact-iters", type=int, default=400, ap.add_argument("--impact-iters", type=int, default=500,
help="IMPACT per-resolution max iterations (default 400, a converged " help="IMPACT per-resolution max iterations (default 500, the authors' "
"rigid budget; the authors' full recommended is 500)") "recommended Jacobian budget; lower to trade accuracy for speed)")
ap.add_argument("--impact-samples", type=int, default=1500, ap.add_argument("--impact-samples", type=int, default=2000,
help="IMPACT random spatial samples per iteration (default 1500; " help="IMPACT random spatial samples per iteration (default 2000, "
"full recommended 2000)") "the recommended budget)")
ap.add_argument("--qa-images", action="store_true", ap.add_argument("--qa-images", action="store_true",
help="also write a checkreg-style PNG (checkerboard blend + color " help="also write a checkreg-style PNG (checkerboard blend + color "
"overlay of the inverse-warped CT vs the original MR) per case") "overlay of the inverse-warped CT vs the original MR) per case")

View file

@ -746,7 +746,7 @@ def _impact_param_map(model_cont_path, gpu, iters, samples, voxel_sizes,
return "\n".join(L) + "\n" return "\n".join(L) + "\n"
def register_impact(ct, t1c, model=None, gpu=None, iters=400, samples=1500, def register_impact(ct, t1c, model=None, gpu=None, iters=500, samples=2000,
threads=12, keep_scratch=False, scales=(6.0, 3.0, 1.5, 1.0), threads=12, keep_scratch=False, scales=(6.0, 3.0, 1.5, 1.0),
downsample=False): downsample=False):
"""IMPACT rigid CT->T1c (fixed=T1c, moving=CT) in the Docker image """IMPACT rigid CT->T1c (fixed=T1c, moving=CT) in the Docker image
@ -767,11 +767,11 @@ def register_impact(ct, t1c, model=None, gpu=None, iters=400, samples=1500,
Compose`` the final transform = ``T_main(T_init(p))``), Compose`` the final transform = ``T_main(T_init(p))``),
- uses a rigid 6-DOF ``EulerTransform`` (the 5.x name for the classic - uses a rigid 6-DOF ``EulerTransform`` (the 5.x name for the classic
3D rigid), ASGD, a 4-level scale pyramid (no image resampling). 3D rigid), ASGD, a 4-level scale pyramid (no image resampling).
Budget: the default ``iters=400`` / ``samples=1500`` is a *converged rigid* Budget: the default ``iters=500`` / ``samples=2000`` is the authors'
budget (measured: recovers the inter-session tilt the intensity-metric recommended Jacobian budget. It is *much* slower than the intensity-metric
pipelines miss). The authors' fully-recommended Jacobian budget is pipelines (minutes per case on GPU vs seconds) because every iteration runs
``iters=500`` / ``samples=2000`` (deformable-oriented, ~25% slower); pass the TotalSegmentator feature extractor on the sampled patches with
those for maximum fidelity. Expect minutes per case (GPU), not seconds. backpropagation. Lower ``iters``/``samples`` to trade accuracy for speed.
The result is read back from ``TransformParameters.0.itk.txt`` The result is read back from ``TransformParameters.0.itk.txt``
(an ``Euler3DTransform`` in the box/world physical frame) and composed with (an ``Euler3DTransform`` in the box/world physical frame) and composed with
the ``-t0`` translation, then checked by :func:`fix_orientation`. the ``-t0`` translation, then checked by :func:`fix_orientation`.