Skip to main content

qsm_core/bgremove/
vsharp.rs

1//! V-SHARP background field removal
2//!
3//! Variable kernel SHARP uses multiple SMV kernel radii
4//! to preserve more brain tissue at edges while still
5//! removing background fields.
6//!
7//! Reference:
8//! Wu, B., Li, W., Guidon, A., Liu, C. (2012).
9//! "Whole brain susceptibility mapping using compressed sensing."
10//! Magnetic Resonance in Medicine, 67(1):137-147. https://doi.org/10.1002/mrm.23000
11//!
12//! Reference implementation: https://github.com/kamesy/QSM.jl
13
14use num_complex::Complex64;
15use crate::Grid;
16use crate::fft::{fft3d, ifft3d};
17use crate::kernels::smv::{smv_kernel, erode_mask_smv};
18
19/// V-SHARP algorithm parameters
20#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
21#[derive(Clone, Debug)]
22pub struct VsharpParams {
23    /// Deconvolution (TSVD) threshold: k-space frequencies where |1 - S| < threshold are dropped
24    /// rather than divided through. Higher = more regularisation, more robust to a noisy input field
25    /// (default: 0.2).
26    pub threshold: f64,
27    /// Maximum (starting) SMV kernel radius in mm (default: 12.0)
28    pub max_radius: f64,
29    /// Minimum SMV kernel radius in mm — also the step between successive radii (default: 1.0)
30    pub min_radius: f64,
31}
32
33impl Default for VsharpParams {
34    fn default() -> Self {
35        Self {
36            // 0.2 (was 0.05): the deconvolution amplifies structured error in a noisy estimated field,
37            // and real pipelines always feed V-SHARP an estimated (not ground-truth) field. A stronger
38            // regularisation is robust to that; 0.05 under-regularises and only wins on a clean field
39            // (which BFR is never run on in practice). See the PR for the benchmark.
40            threshold: 0.2,
41            max_radius: 12.0,
42            min_radius: 1.0,
43        }
44    }
45}
46
47/// V-SHARP background field removal
48///
49/// Uses multiple SMV kernel radii, starting from largest and decreasing.
50/// At each voxel, uses the smallest radius that doesn't touch the boundary.
51///
52/// # Arguments
53/// * `field` - Unwrapped total field (nx * ny * nz)
54/// * `mask` - Binary mask (nx * ny * nz), 1 = inside ROI
55/// * `grid` - Volume dimensions and voxel sizes
56/// * `params` - V-SHARP parameters (threshold, min/max radius in mm)
57/// * `progress` - Progress callback (radius_index, total_radii)
58///
59/// # Returns
60/// (local_field, eroded_mask)
61pub fn vsharp(
62    field: &[f64],
63    mask: &[u8],
64    grid: &Grid,
65    params: &VsharpParams,
66    progress: impl FnMut(usize, usize),
67) -> (Vec<f64>, Vec<u8>) {
68    let radii = vsharp_radii(params);
69    vsharp_with_radii(field, mask, grid, &radii, params.threshold, progress)
70}
71
72/// Build the descending list of SMV kernel radii (mm) from the params.
73///
74/// Starts at `max_radius` (mm) and steps down by `min_radius` (mm), which is
75/// also the smallest kernel used.
76fn vsharp_radii(params: &VsharpParams) -> Vec<f64> {
77    let mut radii = Vec::new();
78    let mut r = params.max_radius;
79    let step = params.min_radius;
80    while r >= step {
81        radii.push(r);
82        r -= step;
83    }
84    if radii.is_empty() {
85        radii.push(params.max_radius);
86    }
87    radii
88}
89
90/// V-SHARP with an explicit list of SMV kernel radii (mm).
91///
92/// Internal entry point; the public [`vsharp`] wrapper derives the radii
93/// from [`VsharpParams`].
94pub(crate) fn vsharp_with_radii(
95    field: &[f64],
96    mask: &[u8],
97    grid: &Grid,
98    radii: &[f64],
99    threshold: f64,
100    mut progress: impl FnMut(usize, usize),
101) -> (Vec<f64>, Vec<u8>) {
102    let (nx, ny, nz) = grid.dims;
103
104    if radii.is_empty() {
105        return (vec![0.0; nx * ny * nz], mask.to_vec());
106    }
107
108    // If only one radius, use regular SHARP
109    if radii.len() == 1 {
110        progress(1, 1);
111        return crate::bgremove::sharp::sharp_core(
112            field, mask, grid, threshold, radii[0]
113        );
114    }
115
116    let n_total = nx * ny * nz;
117    let n_radii = radii.len();
118
119    // Sort radii from largest to smallest
120    let mut sorted_radii = radii.to_vec();
121    sorted_radii.sort_by(|a, b| b.partial_cmp(a).unwrap());
122
123    // FFT of field
124    let mut field_complex: Vec<Complex64> = field.iter()
125        .map(|&x| Complex64::new(x, 0.0))
126        .collect();
127    fft3d(&mut field_complex, nx, ny, nz);
128    let field_fft = field_complex.clone();
129
130    // Track which voxels have been processed and final mask
131    let mut processed = vec![false; n_total];
132    let mut local_field = vec![0.0; n_total];
133    let mut final_mask = vec![0u8; n_total];
134
135    let delta = 1.0 - 1e-7_f64.sqrt();
136    let mut inverse_kernel: Option<Vec<f64>> = None;
137
138    for (idx, &radius) in sorted_radii.iter().enumerate() {
139        // Report progress
140        progress(idx + 1, n_radii);
141
142        // Generate SMV kernel
143        let s_kernel = smv_kernel(grid, radius);
144
145        // FFT of SMV kernel
146        let mut s_complex: Vec<Complex64> = s_kernel.iter()
147            .map(|&x| Complex64::new(x, 0.0))
148            .collect();
149        fft3d(&mut s_complex, nx, ny, nz);
150        let s_fft: Vec<f64> = s_complex.iter().map(|c| c.re).collect();
151
152        // Store inverse of first (largest) kernel
153        if inverse_kernel.is_none() {
154            inverse_kernel = Some(s_fft.iter().map(|&s| {
155                let one_minus_s = 1.0 - s;
156                if one_minus_s.abs() < threshold {
157                    0.0
158                } else {
159                    1.0 / one_minus_s
160                }
161            }).collect());
162        }
163
164        // Erode mask for this radius
165        let eroded = erode_mask_smv(mask, &s_fft, grid, delta);
166        let current_mask: Vec<bool> = eroded.iter().map(|&m| m == 1).collect();
167
168        // Apply high-pass filter
169        let mut filtered = field_fft.clone();
170        for i in 0..n_total {
171            filtered[i] *= 1.0 - s_fft[i];
172        }
173
174        ifft3d(&mut filtered, nx, ny, nz);
175
176        for i in 0..n_total {
177            if current_mask[i] && !processed[i] {
178                local_field[i] = filtered[i].re;
179                processed[i] = true;
180                final_mask[i] = 1;
181            }
182        }
183    }
184
185    // Deconvolution
186    if let Some(inv_kernel) = inverse_kernel {
187        let mut local_complex: Vec<Complex64> = local_field.iter()
188            .map(|&x| Complex64::new(x, 0.0))
189            .collect();
190
191        fft3d(&mut local_complex, nx, ny, nz);
192
193        for i in 0..n_total {
194            local_complex[i] *= inv_kernel[i];
195        }
196
197        ifft3d(&mut local_complex, nx, ny, nz);
198
199        for i in 0..n_total {
200            local_field[i] = if final_mask[i] == 1 { local_complex[i].re } else { 0.0 };
201        }
202    }
203
204    (local_field, final_mask)
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_vsharp_zero_field() {
213        let n = 8;
214        let field = vec![0.0; n * n * n];
215        let mask = vec![1u8; n * n * n];
216        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
217
218        let radii = vec![4.0, 3.0, 2.0];
219        let (local, _) = vsharp_with_radii(&field, &mask, &grid, &radii, 0.05, |_, _| {});
220
221        for &val in local.iter() {
222            assert!(val.abs() < 1e-10);
223        }
224    }
225
226    #[test]
227    fn test_vsharp_preserves_more_than_sharp() {
228        let n = 16;
229        let field = vec![0.0; n * n * n];
230        let mask = vec![1u8; n * n * n];
231        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
232
233        // V-SHARP with multiple radii
234        let radii = vec![5.0, 4.0, 3.0, 2.0];
235        let (_, vsharp_mask) = vsharp_with_radii(&field, &mask, &grid, &radii, 0.05, |_, _| {});
236
237        // SHARP with single large radius
238        let (_, sharp_mask) = crate::bgremove::sharp::sharp_core(
239            &field, &mask, &grid, 0.05, 5.0
240        );
241
242        let vsharp_count: usize = vsharp_mask.iter().map(|&m| m as usize).sum();
243        let sharp_count: usize = sharp_mask.iter().map(|&m| m as usize).sum();
244
245        // V-SHARP should preserve at least as many voxels as SHARP
246        assert!(vsharp_count >= sharp_count,
247            "V-SHARP {} should preserve at least as many as SHARP {}",
248            vsharp_count, sharp_count);
249    }
250
251    #[test]
252    fn test_vsharp_nonuniform_voxels() {
253        let n = 8;
254        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
255        let mask = vec![1u8; n * n * n];
256        let grid = Grid::new(n, n, n, 0.5, 1.0, 2.0);
257
258        // Anisotropic voxel sizes
259        let radii = vec![4.0, 3.0, 2.0];
260        let (local, final_mask) = vsharp_with_radii(
261            &field, &mask, &grid, &radii, 0.05, |_, _| {}
262        );
263
264        // All values should be finite
265        for (i, &val) in local.iter().enumerate() {
266            assert!(val.is_finite(), "V-SHARP nonuniform voxels: finite at index {}", i);
267        }
268
269        // Final mask should have some voxels
270        let mask_count: usize = final_mask.iter().map(|&m| m as usize).sum();
271        assert!(mask_count > 0, "V-SHARP nonuniform: final mask should have some voxels");
272    }
273
274    #[test]
275    fn test_vsharp_single_radius() {
276        let n = 8;
277        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
278        let mask = vec![1u8; n * n * n];
279        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
280
281        // Single radius should delegate to SHARP
282        let radii = vec![3.0];
283        let (local, final_mask) = vsharp_with_radii(
284            &field, &mask, &grid, &radii, 0.05, |_, _| {}
285        );
286
287        // All values should be finite
288        for (i, &val) in local.iter().enumerate() {
289            assert!(val.is_finite(), "V-SHARP single radius: finite at index {}", i);
290        }
291
292        // Result should match SHARP with same radius
293        let (sharp_local, sharp_mask) = crate::bgremove::sharp::sharp_core(
294            &field, &mask, &grid, 0.05, 3.0
295        );
296
297        for i in 0..n*n*n {
298            assert!(
299                (local[i] - sharp_local[i]).abs() < 1e-10,
300                "Single-radius V-SHARP should match SHARP at index {}", i
301            );
302        }
303
304        assert_eq!(final_mask, sharp_mask, "Single-radius V-SHARP mask should match SHARP mask");
305    }
306
307    #[test]
308    fn test_vsharp_empty_radii() {
309        // Empty radii should return zeros and the original mask
310        let n = 8;
311        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
312        let mask = vec![1u8; n * n * n];
313        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
314
315        let (local, returned_mask) = vsharp_with_radii(
316            &field, &mask, &grid, &[], 0.05, |_, _| {}
317        );
318
319        for &val in &local {
320            assert_eq!(val, 0.0, "Empty radii should return zero local field");
321        }
322        assert_eq!(returned_mask, mask, "Empty radii should return original mask");
323    }
324
325    #[test]
326    fn test_vsharp_larger_volume() {
327        // 16x16x16 volume with spherical mask
328        let n = 16;
329        let mut field = vec![0.0; n * n * n];
330        // Linear background field in z
331        for z in 0..n {
332            for y in 0..n {
333                for x in 0..n {
334                    field[x + y * n + z * n * n] = (z as f64) * 0.1;
335                }
336            }
337        }
338
339        // Spherical mask
340        let mut mask = vec![0u8; n * n * n];
341        let center = n / 2;
342        let radius = n / 3;
343        for z in 0..n {
344            for y in 0..n {
345                for x in 0..n {
346                    let dx = (x as i32) - (center as i32);
347                    let dy = (y as i32) - (center as i32);
348                    let dz = (z as i32) - (center as i32);
349                    if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
350                        mask[x + y * n + z * n * n] = 1;
351                    }
352                }
353            }
354        }
355
356        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
357        let radii = vec![6.0, 4.0, 3.0, 2.0];
358        let (local, final_mask) = vsharp_with_radii(
359            &field, &mask, &grid, &radii, 0.05, |_, _| {}
360        );
361
362        assert_eq!(local.len(), n * n * n);
363        for &val in &local {
364            assert!(val.is_finite(), "V-SHARP larger volume values should be finite");
365        }
366
367        let mask_count: usize = final_mask.iter().map(|&m| m as usize).sum();
368        assert!(mask_count > 0, "V-SHARP larger volume should have voxels in final mask");
369
370        // Voxels outside the final mask should be zero
371        for i in 0..n * n * n {
372            if final_mask[i] == 0 {
373                assert_eq!(local[i], 0.0, "Outside final mask should be zero");
374            }
375        }
376    }
377
378    #[test]
379    fn test_vsharp_with_progress() {
380        let n = 8;
381        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
382        let mask = vec![1u8; n * n * n];
383        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
384
385        let radii = vec![4.0, 3.0, 2.0];
386        let mut progress_calls = Vec::new();
387        let (local, _) = vsharp_with_radii(
388            &field, &mask, &grid, &radii, 0.05,
389            |idx, total| { progress_calls.push((idx, total)); }
390        );
391
392        assert_eq!(local.len(), n * n * n);
393        assert!(!progress_calls.is_empty(), "Progress should be called");
394        for &val in &local {
395            assert!(val.is_finite());
396        }
397    }
398
399    #[test]
400    fn test_vsharp_with_progress_single_radius() {
401        let n = 8;
402        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
403        let mask = vec![1u8; n * n * n];
404        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
405
406        let radii = vec![3.0];
407        let mut progress_calls = Vec::new();
408        let (local, _) = vsharp_with_radii(
409            &field, &mask, &grid, &radii, 0.05,
410            |idx, total| { progress_calls.push((idx, total)); }
411        );
412
413        assert_eq!(local.len(), n * n * n);
414        assert!(!progress_calls.is_empty(), "Progress should be called for single radius");
415        for &val in &local {
416            assert!(val.is_finite());
417        }
418    }
419
420    #[test]
421    fn test_vsharp_with_progress_empty_radii() {
422        let n = 8;
423        let field = vec![0.0; n * n * n];
424        let mask = vec![1u8; n * n * n];
425        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
426
427        let mut progress_calls = Vec::new();
428        let (local, returned_mask) = vsharp_with_radii(
429            &field, &mask, &grid, &[], 0.05,
430            |idx, total| { progress_calls.push((idx, total)); }
431        );
432
433        for &val in &local {
434            assert_eq!(val, 0.0);
435        }
436        assert_eq!(returned_mask, mask);
437    }
438
439    #[test]
440    fn test_vsharp_unsorted_radii() {
441        // Radii given in arbitrary order - should be sorted internally
442        let n = 8;
443        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
444        let mask = vec![1u8; n * n * n];
445        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
446
447        let radii_sorted = vec![4.0, 3.0, 2.0];
448        let radii_unsorted = vec![2.0, 4.0, 3.0];
449
450        let (local_sorted, mask_sorted) = vsharp_with_radii(
451            &field, &mask, &grid, &radii_sorted, 0.05, |_, _| {}
452        );
453        let (local_unsorted, mask_unsorted) = vsharp_with_radii(
454            &field, &mask, &grid, &radii_unsorted, 0.05, |_, _| {}
455        );
456
457        // Results should be the same regardless of input order
458        assert_eq!(mask_sorted, mask_unsorted, "Sorted and unsorted radii should give same mask");
459        for i in 0..n * n * n {
460            assert!(
461                (local_sorted[i] - local_unsorted[i]).abs() < 1e-10,
462                "Results should match at index {}",
463                i
464            );
465        }
466    }
467}