Expand description
V-SHARP background field removal
Variable kernel SHARP uses multiple SMV kernel radii to preserve more brain tissue at edges while still removing background fields.
Each voxel takes the SMV high-pass φ − S_r∗φ of the largest radius r
whose ball fits inside the mask. By default that high-passed field is returned
as is, which is what the authors’ code, STI Suite’s V_SHARP, does. With
VsharpParams::deconvolve on it is then deconvolved by the largest kernel,
1/(1 − Ŝ) with TSVD at threshold (as QSM.jl does). The deconvolution restores low
spatial frequencies; on real data those are dominated by residual background and
field-map error, so leaving it off gives a more reproducible local field at the cost
of attenuating genuinely low-frequency tissue contrast.
§Defaults
The defaults are STI Suite 3.0’s V_SHARP (Liu lab, the authors of the V-SHARP papers),
which is P-code and was probed black-box (ukb-qsmxt docs/08_vsharp_sti.md): crop to the
mask’s bounding box + 12 voxels, linear resampling to a 1 mm grid, balls |x| ≤ r + 1/6
(centre excluded) for r = 1…smvsize mm, the largest fitting ball’s background per voxel,
no deconvolution, the background interpolated back; smvsize defaults to 12 mm. Here
that is deconvolve: false, an Isotropic 1 mm grid (same 12-voxel
crop margin, CROP_MARGIN), max_radius 12 and STI’s radius schedule, 12 → 2 mm in 1 mm
steps (VsharpParams::radius_step), and STI’s ball, VsharpBall::StiSuite (|x| ≤ r + 1/6
without its centre voxel, a ball fitting only where all its voxels are in the mask), and STI’s
crop: its MaskBoundingBox makes the bounding box even along every axis before the margin
(sti_even_box).
Measured (QSM-CI parity, astewartau/QSM.rs#329): on the KKI travelling head (256×256×128,
1 mm, STI Suite V_SHARP in the verified vsharp-sti v2 container, the same total field and
mask), the STI ball gives the same eroded mask and NRMSE 2e-6 relative (STI’s float32 output),
where the earlier ball (radius r, centre included, VsharpBall::QsmJl) gave 4.4 %.
tests/vsharp_sti_parity.rs holds a small case (40×44×36, smvsize 12 and 6, from the same
container, regenerated in Actions by tests/data/vsharp_sti_small.py): the same eroded mask
and max |Δ| < 5e-8 on a field of up to 0.15, STI’s single-precision filtering.
The papers and the code disagree on the deconvolution:
Li et al. 2011 (NeuroImage 55:1645, Image Analysis and Results) and Wu et al. 2012
(Background phase removal: “a deconvolution is carried out to restore the local phase”,
with the kernel of the largest diameter used) both deconvolve the SMV-filtered phase,
without giving the kernel’s truncation; STI Suite’s V_SHARP does not (its impulse
response is exactly δ − S₁₂). The code is followed. On 150 UK Biobank subjects × 2
visits (ukb-qsmxt docs/08_vsharp_sti.md, PR #160) deconvolving lowered deep-grey-matter
test-retest ICC (0.922 → 0.915) and removed the detectable longitudinal change (SRM
putamen + caudate 0.161 → −0.019); on the noise-free CI phantom it is closer to the true
local field, since there the low frequencies are tissue. Measured with the pipeline’s ROMEO
field and RTS against the previous defaults (deconvolution, native grid, radii 12 → 1 mm),
on the voxels both keep: qsm-forward phantom χ XSIM 0.261 → 0.256, NRMSE 84.4 → 85.7 %,
r 0.546 → 0.521; in-vivo 3T Philips scan, agreement with the UK Biobank pipeline’s χ
(STI Suite V-SHARP + iLSQR) XSIM 0.641 → 0.714, NRMSE 75.2 → 69.4 %, r 0.781 → 0.790. On
these 1 mm isotropic inputs the 1 mm grid only adds the crop.
Anisotropic voxels (astewartau/QSM.rs#329, measured 2026-10-30): on UK Biobank’s
1×1×1.5 mm (a 32×32×20 phantom through UKBiobank_QSM_core.m in the verified ukb-qsm v2
container, identical to licensed MATLAB R2023b on it) the eroded mask was STI’s but the local
field 4.1 % NRMSE from it. The cause was the crop, not the resampling: STI’s
MaskBoundingBox turns the mask’s 11 slices into 12, and that slice’s phase (outside the mask)
reaches the 1 mm working grid through the z interpolation. Spied in licensed MATLAB, STI’s
resampling is exactly this module’s (interp3 linear, native samples at (1:N)·vs mm, working
samples at 1:ceil(N·vs) mm, the mask > 0.5 both ways, the background interpolated back in
float32), and its working-grid background is the phase outside the eroded mask and the
largest fitting ball’s S_r∗φ inside it. With the even box that phantom agrees to 1.2e-6. A
second, smaller cause showed on non-integer resamplings (1.2×0.8×1.5 mm): STI interpolates the
mask in single precision, and exact ties at 0.5 fall outside its > 0.5 test where f64 put
some just inside (trilinear_mask_f32; 0.96 % and 13 output voxels before). With both,
tests/vsharp_sti_aniso_parity.rs (four phantoms from 1×1×1.5 to 0.7×0.7×3 mm, the verified
vsharp-sti container) agrees to 7e-7–1.1e-6 NRMSE (STI’s float32) with the same masks; before,
6e-4 to 2 % and up to 22 mask voxels apart.
§QSM.jl’s vsharp
The filtering (the per-voxel choice of radius and the deconvolution, and with
VsharpBall::QsmJl the balls and the edge test) is a port of QSM.jl’s vsharp (kamesy/QSM.jl fde4b92, _sharp! for a radius
vector in src/bgremove/sharp.jl), including its working box: the mask’s bounding box
zero-padded to QSM.jl’s fast FFT size for the largest radius, so the deconvolution is periodic
on that box (as in [super::sharp]), and its edge test S_r ∗ mask > 1 − √eps(Float64).
QSM.jl’s own defaults are reachable with
use qsm_core::bgremove::{VsharpBall, VsharpGrid, VsharpParams};
let vsz = (1.0_f64, 1.0_f64, 1.0_f64);
let (lo, hi) = (vsz.0.min(vsz.1).min(vsz.2), vsz.0.max(vsz.1).max(vsz.2));
let qsmjl = VsharpParams {
max_radius: 18.0 * lo, // QSM.jl: r = 18*minimum(vsz):-2*maximum(vsz):2*maximum(vsz)
min_radius: 2.0 * hi,
radius_step: 2.0 * hi,
threshold: 0.05, // QSM.jl's thr (also this module's default)
deconvolve: true, // QSM.jl always deconvolves
ball: VsharpBall::QsmJl, // QSM.jl's ball and edge test
grid: VsharpGrid::Native,
};The deconvolution threshold default, 0.05, is QSM.jl’s vsharp thr and QSM.m’s
vsharp.m thr (kamesy/QSM.m f22dc40, src/bgremove/vsharp.m:15,32): STI Suite’s
V_SHARP (the original authors’ code, whose other defaults this module keeps) does not
deconvolve, so the threshold has no STI value. QSM.rs defaulted to 0.2 from PR #72
(2026-09-07), a QSM.rs tuning: on the QSM Challenge 2.0 phantom (ROMEO field, V-SHARP,
several dipole inversions) 0.2 gave a lower NRMSE on every inversion tested, and was
identical to 0.05 on a noise-free field, because the higher threshold leaves out more of the
low-frequency band where field-mapping error is amplified. It remains available as an
explicit threshold.
On the QSM-CI in-vivo travelling-head scan (Siemens Prisma 2.89 T, 1 mm, 184×256×128, ROMEO
total field and brain mask) these settings and QSM.jl’s vsharp in the QSM-CI julia-qsm
image (sha256:b3c048ac…) agree to float rounding: NRMSE 2.6e-10 %, max |Δ| 5.8e-11 ppm,
identical masks (both outputs saved as float32); with radii 12 → 1 mm in 1 mm steps, NRMSE
2.1e-10 %. Before the working box and edge test were QSM.jl’s (the deconvolution was
periodic on the whole field of view, and the edge test was > 1 − √1e-7), the 12 → 1 mm
comparison gave NRMSE 15.5 %. tests/sharp_qsmjl_parity.rs checks the port against
QSM.jl’s output on small phantoms (max |Δ| < 1e-15 ppm).
The working box depends on the voxel size at the float rounding level: its padding is
⌈r_max / min(vsz)⌉, and NIfTI headers often store 1 mm as 0.99999994 (one float32 ulp
below), for which a 12 mm radius pads 13 voxels, not 12. On the QSM-CI KKI travelling head
(Philips 3 T, 256×256×128, header z spacing 0.99999994, params.json 1.0) that alone gave
NRMSE 1.19 % between QSM.rs (header voxel size) and QSM.jl (params.json’s) with radii
12 → 1 mm (QSMxT/QSM-CI parity run 41029942914); given the same voxel size the outputs are
identical in float32. The port keeps QSM.jl’s rule (no rounding of the voxel size), so the
two agree whenever they are given the same voxel size; tests/sharp_qsmjl_parity.rs has a
case at that spacing (0.75 % from QSM.jl if 1.0 is used instead).
The old edge test 1 − √1e-7 also let a ball count as fitting when it missed a few of its
voxels (up to 2 of the 7,153 voxels of a 12 mm ball at 1 mm), so near the rim some voxels took a larger
radius than fits. The QSM.jl test fixes that for every setting, including the STI Suite
defaults: on the travelling-head scan the default output moved by NRMSE 1.8 % (same
output mask), all of it from the edge test (the working box alone changes it by 2e-7 %).
§Working grid
The SMV balls live on a voxel grid. On strongly anisotropic voxels (e.g. 0.8 × 0.8 × 3 mm)
the native grid makes them crude in the coarse direction: a 1–2 mm ball is a single-slice
disk. VsharpParams::grid can instead estimate the background on an isotropic working
grid, at the finest input voxel size (VsharpGrid::IsotropicFinest) or at a fixed spacing
(VsharpGrid::Isotropic):
- crop the field and mask to the mask’s bounding box, made even along each axis as STI’s
MaskBoundingBoxdoes (sti_even_box), with a zero margin ofCROP_MARGINnative voxels per side (this also fixes where the working grid sits relative to the anatomy, so it lands the same way on repeat scans); - resample both trilinearly onto the isotropic grid (mask: interpolated value > 0.5), axes whose voxel size already equals the spacing are left as they are;
- run V-SHARP there (same balls, radii, edge test and deconvolution as on the native grid);
- interpolate the background (field − local field on the working grid) back to the native grid trilinearly, and return local = native field − background, on the voxels where the back-interpolated working-grid support is > 0.5.
The local field thus keeps native resolution; only the smooth background estimate is made on
the working grid. The default is VsharpGrid::Isotropic at 1 mm (STI Suite’s grid; see Defaults). VsharpGrid::IsotropicFinest
makes the balls spherical without coarsening any axis, at the cost of a working grid that
is larger by the anisotropy ratio. A fixed VsharpGrid::Isotropic spacing gives the same
working grid whatever the acquisition resolution, but coarsens axes finer than it,
including the mask edge the background is estimated from.
§Reference pipeline
STI Suite 3.0’s V_SHARP(φ, mask, 'voxelsize', vs, 'smvsize', R) (Liu lab, the authors of
the V-SHARP papers) is P-code; black-box probing (ukb-qsmxt docs/08_vsharp_sti.md, scripts
in vsharp_probe/) found: crop to the mask’s bounding box, zero-pad 12 voxels per side,
resample linearly to a 1 mm grid, ball |x| ≤ r + 1/6 without its centre for
r = 1…R mm, each voxel takes the background S_r∗φ of the largest ball that fits,
no deconvolution, background interpolated back and subtracted from the native field.
Voxels where only r = 1 fits are dropped, so the radii in effect are 2…R mm and the output
mask is the voxels a 2 mm (+1/6) ball fits around, back-interpolated > 0.5; this module’s
mask is the union down to VsharpParams::min_radius, which by default is the same 2 mm
(see Defaults), and the ball is STI’s (VsharpBall::StiSuite).
smvsize defaults to 12 (probed: bitwise equal to an explicit 12), which is also what STI
Suite’s GUI passes (its “Radius” field) and what QSMART’s paper used for its V-SHARP
comparison (Yaghmaie et al. 2021, §3.4).
In STI Suite’s GUI (probed headless; see crate::inversion::star), V-SHARP’s input is the
sum of the echoes’ unwrapped phases from MRPhaseUnwrap on the raw, unmasked phase, its
mask is the GUI’s magnitude-threshold mask (not BET), and its eroded mask and output go to
QSM_star or QSM_iLSQR with TE = the sum of the echo times.
Reference: Wu, B., Li, W., Guidon, A., Liu, C. (2012). “Whole brain susceptibility mapping using compressed sensing.” Magnetic Resonance in Medicine, 67(1):137-147. https://doi.org/10.1002/mrm.23000
Implementation follows QSM.jl (https://github.com/kamesy/QSM.jl; see QSM.jl’s vsharp);
defaults follow STI Suite 3.0’s V_SHARP (see Defaults), except the deconvolution
threshold, which only QSM.jl has.
§Pipeline rules
The facts above that a configuration can depart from are published as rules in
crate::pipeline::rules::RULES (each with its strength, basis, reason and source): sti.unwrap-laplacian, sti.laplacian-solver, sti.echo-combination, sti.echo-weights, sti.bg-vsharp, sti.vsharp-no-deconvolve, sti.vsharp-radius, sti.vsharp-grid, sti.vsharp-min-radius, sti.vsharp-radius-step.
Structs§
- Vsharp
Params - V-SHARP algorithm parameters
Enums§
- Vsharp
Ball - The SMV ball V-SHARP filters with. See the module docs.
- Vsharp
Grid - Grid V-SHARP estimates the background field on. See the module docs.
Constants§
- CROP_
MARGIN - Zero margin (native voxels per side) around the mask’s bounding box when V-SHARP runs on an isotropic working grid. See the module docs.
- STI_
GRID_ MM - Working-grid spacing of STI Suite’s
V_SHARP, in mm: the default ofVsharpParams::grid. - STI_
MIN_ RADIUS_ MM - Smallest radius in effect in STI Suite’s
V_SHARP, in mm (it drops voxels only r = 1 fits): the default ofVsharpParams::min_radius. - STI_
RADIUS_ STEP_ MM - Step between STI Suite
V_SHARP’s radii, in mm: the default ofVsharpParams::radius_step.
Functions§
- bounding_
box 🔒 - Inclusive bounding box of the nonzero mask voxels, or
Nonefor an empty mask. - sti_
even_ 🔒box - STI Suite’s
MaskBoundingBox(mask, voxelsize)(V_SHARP’s crop): the mask’s bounding box made even along every axis. An odd extent grows by one voxel at its high end, or at its low end when the high end is the array’s last voxel; an odd axis the mask fills entirely loses its last voxel. The voxel size plays no part. Probed black-box in licensed MATLAB R2023b on 17,952 boxes (lengths 8–40, eleven voxel sizes 0.5–3 mm, every position), all reproduced. - trilinear 🔒
- Separable trilinear interpolation (MATLAB
interp3(..., 'linear')):coords[d]are 1-based fractional source indices along axisd, one output sample per entry, output in the same Fortran order. Samples outside[1, n]on any axis are 0. - trilinear_
mask_ 🔒f32 trilinearof a 0/1 mask as MATLAB’sinterp3(..., 'linear')computes it on a single array, which is how STI Suite’sV_SHARPresamples its mask both ways: each fraction rounded to f32, then(1 − f)·v₀ + f·v₁in f32, along axis 1 first (interp3’s X, the second array dimension), then axis 0, then axis 2.- vsharp
- V-SHARP background field removal
- vsharp_
isotropic 🔒 - V-SHARP with the background estimated on an isotropic grid of spacing
hmm anchored at the mask’s bounding box (see the module docs). - vsharp_
padded 🔒 - V-SHARP on QSM.jl’s padded box.
- vsharp_
radii 🔒 - Build the descending list of SMV kernel radii (mm) from the params.
- vsharp_
with_ 🔒radii - V-SHARP with an explicit list of SMV kernel radii (mm).