Skip to main content

Module motion

Module motion 

Source
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 own bdir. 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), &params)?;
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§

CorrectedSeries
A series of volumes put onto the reference grid, with the B0 direction each was acquired under. The “pass downstream” half of Tier A.
MotionAverage
A complex average of a motion-corrected series.
MotionParams
Motion-correction parameters.
MotionSeries
Where every volume of a series was, relative to the reference.
SeriesVolume
One volume of a series, borrowed: the magnitude to register on and the grid it lives on.
VolumeMotion
Where one volume of a series was, and which way B0 pointed while it was there.

Enums§

MotionReference
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_affine so a caller assembling MotionParams::declared_b0 by 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.