Skip to main content

qsm_core/bgremove/
smv.rs

1//! Simple Spherical Mean Value (SMV) background field removal
2//!
3//! Basic SMV filtering: subtracts the spherical mean of the field.
4//! Simpler than SHARP (no deconvolution step).
5//!
6//! local_field = field - SMV(field)
7//!
8//! # Reference pipeline
9//! Not offered as a pipeline background removal. In the MEDI toolbox the same high-pass is
10//! applied inside `MEDI_L1` with `'smv', radius` (radius 5 mm by default, `parse_QSM_input.m:13`),
11//! together with the SMV-filtered dipole kernel `(1 − S)·D` and the mask eroded to where the
12//! SMV of the mask exceeds 0.999 (`MEDI_L1.m:57-67`), and on the PDF local field, not the total
13//! field (MEDI `README.m:124,144`). That is `MediParams::smv`, not this function.
14//!
15//! Reference:
16//! Schweser, F., Deistung, A., Lehr, B.W., Reichenbach, J.R. (2011).
17//! "Quantitative imaging of intrinsic magnetic tissue properties using MRI signal phase."
18//! NeuroImage, 54(4):2789-2807. https://doi.org/10.1016/j.neuroimage.2010.10.070
19//!
20//! Reference implementation: https://github.com/kamesy/QSM.jl
21
22use crate::Grid;
23use crate::fft::fft_real_kernel;
24use crate::kernels::smv::{smv_kernel, erode_mask_smv};
25
26/// Simple SMV background field removal
27///
28/// Computes: local_field = field - SMV(field)
29///
30/// # Arguments
31/// * `field` - Unwrapped total field (nx * ny * nz)
32/// * `mask` - Binary mask (nx * ny * nz), 1 = inside ROI
33/// * `grid` - Volume dimensions and voxel sizes
34/// * `radius` - SMV kernel radius in mm
35///
36/// # Returns
37/// (local_field, eroded_mask)
38pub fn smv(
39    field: &[f64],
40    mask: &[u8],
41    grid: &Grid,
42    radius: f64,
43) -> (Vec<f64>, Vec<u8>) {
44    let (nx, ny, nz) = grid.dims;
45
46    // Generate SMV kernel and FFT it
47    let s_kernel = smv_kernel(grid, radius);
48    let s_fft = fft_real_kernel(&s_kernel, nx, ny, nz);
49
50    // Erode mask via SMV convolution
51    let eroded_mask = erode_mask_smv(mask, &s_fft, grid, 1.0 - 1e-10);
52
53    // Compute SMV(field) = background field estimate
54    let smv_field = crate::fft::apply_real_kernel(field, &s_fft, nx, ny, nz);
55
56    // Local field = field - SMV(field), within eroded mask
57    let local_field: Vec<f64> = field.iter()
58        .zip(smv_field.iter())
59        .enumerate()
60        .map(|(i, (&f, &smv_f))| {
61            if eroded_mask[i] == 1 { f - smv_f } else { 0.0 }
62        })
63        .collect();
64
65    (local_field, eroded_mask)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_smv_zero_field() {
74        let n = 16;
75        let field = vec![0.0; n * n * n];
76        let mask = vec![1u8; n * n * n];
77        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
78
79        let (local, _) = smv(&field, &mask, &grid, 2.0);
80
81        for &val in local.iter() {
82            assert!(val.abs() < 1e-8, "Zero field should give zero local field, got {}", val);
83        }
84    }
85
86    #[test]
87    fn test_smv_finite() {
88        let n = 16;
89        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.01).collect();
90        let mask = vec![1u8; n * n * n];
91        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
92
93        let (local, eroded) = smv(&field, &mask, &grid, 2.0);
94
95        for (i, &val) in local.iter().enumerate() {
96            assert!(val.is_finite(), "Local field should be finite at index {}", i);
97        }
98
99        let eroded_count: usize = eroded.iter().map(|&m| m as usize).sum();
100        assert!(eroded_count > 0, "Eroded mask should have some voxels");
101    }
102}