Expand description
Rigid-body (6-DOF) registration between two volumes: recovering the transform that
geometry can already apply.
Multi-orientation QSM needs two things from it — the orientations on one common grid, and
the B0 direction each one contributes to that grid. The second comes out of the recovered
rotation, because the object rotates and B0 does not; see
registration::RigidTransform::b0_direction_in_fixed.
Rigid-body (6-DOF) volume registration: estimating the transform between two volumes.
crate::geometry can already apply an arbitrary transform — crate::geometry::GridMap
maps one voxel grid onto another given two affines, and crate::geometry::resample_onto,
crate::geometry::resample_mask_onto and crate::geometry::resample_complex_onto move
continuous data, labels and wrapped phase across it. What was missing is finding the
transform in the first place, which is what this module does.
§Why QSM needs it
Multi-orientation QSM (COSMOS, [crate::inversion::cosmos]) and susceptibility tensor
imaging ([crate::inversion::sti]) both take N local field maps on one common grid plus
one B0 direction per orientation in that grid’s voxel frame. Registration produces both,
and the second is not a by-product:
The object rotates and B0 does not. So the rigid rotation that aligns orientation t to the reference is where B0 pointed during orientation t, relative to the common grid.
That matters most for the dataset shape where the affine is useless: identically prescribed
slabs, where every orientation has the same affine and the anatomy moved in voxel space
instead. crate::geometry::b0_direction_from_affine returns the same direction for all of
them, and N identical directions quietly collapse COSMOS to an unregularized
single-orientation inversion. Registration is the only thing that can recover the real answer
there — see [RigidTransform::b0_direction_in_fixed], which is where that convention lives
so no caller has to re-derive it.
§Register on magnitude, resample phase through the complex domain
Wrapped phase cannot be interpolated directly (halfway between +3.0 and -3.0 rad a linear
interpolator returns 0.0; see the crate::geometry module docs), so it must not be used
as the registration input either. Register magnitude against magnitude, then move phase with
[RigidTransform::resample_complex], which goes through mag·e^{iφ}.
§Metric: normalised cross-correlation, not mutual information
Every use case here is mono-modal — GRE magnitude against GRE magnitude of the same subject, same sequence. NCC is invariant to an affine intensity change (gain and offset), which is what actually differs between two orientations of one acquisition, and it needs no joint histogram, no binning choice and no parzen window. Mutual information buys nothing on same-contrast data and is noisier at the small sample counts a coarse pyramid level uses. Sum-of-squared-differences would be cheaper still but is not gain-invariant, so a receive-gain difference between runs would bias it.
The one thing NCC does not absorb is spatially varying shading: the receive field is fixed
to the coil, so when the head rotates the bias field rotates with the coil rather than with the
anatomy. That is a multiplicative field, not a scalar gain, and no global metric (MI
included) is invariant to it. Two mitigations, both available here: pass a brain mask so the
metric ignores everything outside the head, and/or hand in magnitude that has already been
bias-corrected (crate::homogeneity).
§Optimiser, pyramid and cost
Powell’s direction-set method with a bracketed Brent line search, over a coarse-to-fine
Gaussian pyramid. Powell needs no derivatives of the metric — convenient, because the metric’s
derivative runs through a trilinear interpolator — and it is what the established intensity
registration tools use for 6-DOF problems. No linear-algebra crate is involved; the whole
module is f64 arrays, consistent with the rest of qsm-core.
The pyramid is deep on purpose. It is what sets the capture range — how far apart two
volumes can start and still converge — because smoothing and decimating flattens the cost
surface’s ripples, and the ripples are what a search walks into instead of the true minimum.
See [RigidParams::levels] for the measured numbers; the short version is that three levels
converge to 40° and five to beyond 70°, and 45° of repositioning is ordinary for STI.
Work per metric evaluation is bounded independently of volume size by
[RigidParams::max_samples]: each level picks the smallest integer voxel stride whose sample
count fits the cap. Coarse levels therefore see smoothed, decimated data (a smooth cost
landscape, which is the point of the pyramid), while the finest level strides the unsmoothed
full-resolution volume (full spatial detail at the same cost, which is what gives the final
precision). Peak extra memory is the pyramid above the finest level — about 1/7 of one input
volume — plus the sample list, so a 32-bit WASM host with a 4 GB heap is not at risk.
Structs§
- Acc 🔒
- Running sums for one chunk of samples, so the reduction is order-independent.
- CostFn 🔒
- The cost being minimised:
1 - NCCover the overlapping, masked samples. - Rigid
Params - Registration parameters.
- Rigid
Transform - A recovered rigid transform, together with everything needed to use it.
- Sample 🔒
- One fixed-grid sample the metric is evaluated on: its voxel coordinate and its intensity.
- Volume 🔒
- One pyramid level: either a borrowed view of the caller’s data (level 0) or a decimated copy.
Constants§
- COST_
DEGENERATE 🔒 - Costs assigned where NCC is not a meaningful number. Both are above the worst real cost
(
1 - (-1) = 2), so a degenerate candidate can never win, and the overlap case keeps a gradient that pushes the search back into the valid region. - COST_
NO_ 🔒OVERLAP - IDENTIT
Y4 🔒 - SAMPLE_
CHUNK 🔒 - Fixed samples per parallel chunk. Fixed size, so the reduction combines in index order and the metric is bit-identical regardless of thread count.
Functions§
- binomial_
blur3 🔒 - Separable
[1 4 6 4 1]/16blur with edge replication. - decimate_
mask 🔒 - Decimate a mask the same way
Volume::halveddecimates data: keep the even samples, with no blur, so the coarse mask stays binary and sits on the coarse sample positions exactly. - direction_
cosines 🔒 - The direction-cosine matrix of an affine: its 3×3 with the voxel sizes divided out, so what is left is the pure rotation taking voxel axes to world axes.
- euler_
rotation 🔒 Rz(rz)·Ry(ry)·Rx(rx), angles in radians.- grid_
center_ 🔒world - World coordinate of a grid’s centre voxel.
- line_
minimise 🔒 - Minimise
costalongp + x·dir: bracket the minimum, then Brent. - mat3_
transpose_ 🔒vec - mat3_
vec 🔒 - mat4_
mul 🔒 - matrix_
from_ 🔒params - The rigid 4×4 for a parameter vector:
x_moving = center + R·(x_fixed - center) + t. - params_
from_ 🔒matrix - Recover a parameter vector from a rigid 4×4, about
center. The inverse ofmatrix_from_params, used byRigidTransform::inverse. - powell 🔒
- Powell’s direction-set method.
- register_
rigid - Estimate the rigid transform that aligns
movingtofixed. - rigid_
inverse 🔒 - Inverse of a rigid 4×4: transpose the rotation, and carry the translation back through it.
- rotation_
of 🔒 - The 3×3 block of a 4×4.
- sample_
set 🔒 - Pick the fixed voxels the metric looks at: the smallest integer stride whose masked sample
count fits
max_samples. - to_mat4 🔒