Expand description
Retrospective motion correction: co-register a series of repeats, runs or echoes onto one of them, in the complex domain, and re-derive B0 for each.
The image-domain half of motion correction — what a library consuming reconstructed NIfTI can
do, as opposed to the navigator-driven prospective methods that need raw k-space. Two things
it exists to get right: wrapped phase only ever moves through the complex domain
(motion::MotionSeries::apply_complex), and a series that rotated has one B0 direction per
volume (motion::VolumeMotion::bdir), not one for the series.
Retrospective motion correction for GRE/QSM: co-register a series of volumes onto one of
them, in the complex domain, and re-derive B0 for each.
This is the image-domain half of motion correction, and it is the half a library that consumes reconstructed NIfTI can actually do. The best QSM motion correction in the literature is prospective — spherical or dual-echo spiral navigators driving a conjugate-phase reconstruction — and it needs raw k-space plus a modified sequence. Nothing here can reach that; the boundary is recorded so it is not mistaken for a gap.
What this module does is what the field does in image space. The most recent published example registers every echo of a UTE acquisition to the first TE as a fixed reference with a rigid algorithm, and reports that it significantly reduced streaking from substantial inter-scan motion — with the honest caveat that a residual boundary effect remains from motion it could not correct. Expect the same shape of result here: a large improvement, not a clean recovery.
§The two things that are easy to get wrong
Phase must move through the complex domain. Wrapped phase cannot be interpolated: halfway
between +3.0 and −3.0 rad a linear interpolator returns 0.0 when the answer is near
±π, so every wrap in the volume becomes a band of wrong values. Registration is therefore
estimated on magnitude and applied to the magnitude/phase pair together through
crate::geometry::resample_complex_onto, which interpolates mag·e^{iφ}.
[MotionSeries::apply_complex] is the only phase-moving entry point here, and
[MotionSeries::apply] is documented as refusing to be the one.
B0 must be re-derived per volume. When the head rotates, B0 does not: the field direction
relative to the head changes, so the dipole kernel orientation changes with it. A series
that moved is a series with one B0 direction per volume, and reusing a single one produces a
plausible-looking wrong susceptibility map rather than an error. This is the same physics that
makes COSMOS ([crate::inversion::cosmos]) work, and the transport lives in
crate::registration::RigidTransform::b0_direction_in_fixed; [VolumeMotion::bdir] is just
that value, carried alongside the volume it belongs to so a caller cannot lose track of which
direction goes with which field map.
§Two ways to finish
Having put the series on one grid there are two things to do with it, and they are not equally safe:
- Pass it downstream per volume ([
correct_motion]), each field map with its ownbdir. Always correct. With enough orientations and enough spread this is COSMOS. - Average it ([
MotionSeries::average_complex]). Correct for magnitude, and the right thing for repeats that barely moved, but it averages phase accumulated under different dipole-kernel orientations, so it is an approximation that degrades with the rotation it is averaging over. [MotionSeries::rotation_spread_deg] is the number to look at before trusting it.
§What is deliberately not here
Detecting a corrupted volume and downweighting or dropping it — a per-echo fit residual as a
corruption detector, feeding a weighted echo combination — is a separate piece of work that
belongs with the weighting machinery in [crate::utils::multi_echo], not here.
[VolumeMotion::suspect] reports when an estimate looks untrustworthy, which is a
registration diagnostic; acting on it is not this module’s job.
§Example
use qsm_core::motion::{correct_motion, MotionParams, MotionReference};
let params = MotionParams {
reference: MotionReference::First, // the first TE, as the UTE work does
..Default::default()
};
let corrected = correct_motion(mags, phases, dims, affine, Some(brain), ¶ms)?;
println!(
"max tissue displacement {:.2} mm over {:.2}° of rotation",
corrected.motion.max_displacement_mm(),
corrected.motion.rotation_spread_deg(),
);
// Each volume on the reference grid, each with the B0 direction it was acquired under.
for (phase, bdir) in corrected.phases.iter().zip(corrected.bdirs.iter()) {
let _ = (phase, bdir);
}Structs§
- Corrected
Series - A series of volumes put onto the reference grid, with the B0 direction each was acquired under. The “pass downstream” half of Tier A.
- Motion
Average - A complex average of a motion-corrected series.
- Motion
Params - Motion-correction parameters.
- Motion
Series - Where every volume of a series was, relative to the reference.
- Series
Volume - One volume of a series, borrowed: the magnitude to register on and the grid it lives on.
- Volume
Motion - Where one volume of a series was, and which way B0 pointed while it was there.
Enums§
- Motion
Reference - Which volume of the series the rest are aligned onto.
Functions§
- correct_
motion - Estimate the motion across a complex series and put every volume onto the reference grid.
- estimate_
motion - Estimate where every volume of a series was, relative to a reference volume.
- implied_
b0 - B0 direction a volume’s own affine implies, in its own voxel frame — re-exported shape of
b0_direction_from_affineso a caller assemblingMotionParams::declared_b0by hand can see the convention it has to match. - max_
displacement_ 🔒mm - Largest
|M·x − x|over the evaluation domain, in mm. - uniform_
series - Borrow a series of volumes that all share one grid — the echoes of one acquisition, or repeats of one prescription.