Skip to contents

Magnitude MR data follows a Rician distribution. At moderate SNR, the noise variance depends on the underlying signal, which complicates denoising methods designed for additive Gaussian noise. A simple strategy is to apply a variance–stabilizing transform (VST), denoise under a Gaussian assumption, then invert the transform.

Forward and inverse transforms

The forward transform maps magnitude x to z = sqrt(max(x^2 - 2*sigma^2, 0)). The inverse maps back via x = sqrt(max(z^2 + 2*sigma^2, 0)). The parameter sigma is the noise standard deviation in magnitude units.

x <- array(rexp(100, rate = 1/100), dim = c(5,5,4))
z <- vst_forward(x, sigma = 2)
all.equal(dim(z), dim(x))
#> [1] TRUE
xi <- vst_inverse(z, sigma = 2)
all.equal(dim(xi), dim(x))
#> [1] TRUE

Wrapping a denoiser (how to use with this package)

vst_denoise(x, sigma, denoise_fun, ...) applies the forward transform, calls a denoiser, then inverts. The denoise_fun can be any function in this package that accepts the data as its first argument and returns an array of the same shape. Extra arguments are forwarded via ....

The most convenient usage is to pass a function directly and supply its parameters in ....

d4 <- c(8, 8, 8, 12)
clean <- array(100, dim = d4)
noisy <- sqrt((clean + array(rnorm(prod(d4), sd = 2), dim = d4))^2)  # Rician-like

# Use joint bilateral in the Gaussianized (VST) domain
out_bilat <- vst_denoise(
  noisy,
  denoise_fun = bilat_lattice4d,
  sigma_sp = 2.0, sigma_t = 0.4, sigma_r = 8
)

# Or use space–time TV in the VST domain
out_tv <- vst_denoise(
  noisy,
  denoise_fun = tv_denoise4d,
  lambda_s = 0.6, lambda_t = 0.2, iters = 20L
)

c(var_noisy = var(as.vector(noisy)),
  var_bilat = var(as.vector(out_bilat)),
  var_tv    = var(as.vector(out_tv)))
#>  var_noisy  var_bilat     var_tv 
#> 3.97367690 0.03625491 0.20270265

zmid <- ceiling(d4[3]/2); tmid <- ceiling(d4[4]/2)
viz <- rbind(
  slice_df4d(noisy, zmid, tmid, "noisy (magnitude)"),
  slice_df4d(out_bilat, zmid, tmid, "VST + bilat_lattice4d"),
  slice_df4d(out_tv, zmid, tmid, "VST + tv_denoise4d")
)
ggplot(viz, aes(x, y, fill = val)) + geom_raster() + coord_fixed() + scale_y_reverse() +
  scale_fill_gradient(low = "black", high = "white") + facet_wrap(~method) + theme_minimal(base_size = 10) +
  labs(title = "VST-based denoising: mid-slice/frame", x = NULL, y = NULL, fill = "intensity")

You can also pass an anonymous function if you prefer to pre‑bind certain arguments:

out_custom <- vst_denoise(
  noisy,
  denoise_fun = function(z) bilat_lattice4d(z, sigma_sp = 2.5, sigma_t = 0.5, sigma_r = 10)
)
dim(out_custom)
#> [1]  8  8  8 12

Using with pipelines

You can wrap end‑to‑end pipelines, but avoid configurations that internally estimate a Rician noise sigma from the (VST‑transformed) data. For example, prefer explicit parameters over auto‑parameter estimation when wrapping pipelines:

# OK: wrap a pipeline you configure explicitly
out_ok <- vst_denoise(
  noisy,
  denoise_fun = bilat_lattice4d,  # core smoother used in smooth_auto()
  sigma_sp = 2.5, sigma_t = 0.5, sigma_r = 12
)

# Avoid: wrapping smooth_auto() with auto_params=TRUE will try to
# estimate a Rician sigma on VST data. If you do wrap it, set auto_params=FALSE
# and pass explicit parameters.

3D usage

For 3D volumes there is no temporal dimension to estimate sigma from; you must supply it:

d3 <- c(10, 10, 10)
vol <- array(100, dim = d3) + array(rnorm(prod(d3), sd = 2), dim = d3)
out3d <- vst_denoise(vol, sigma = 2.0, denoise_fun = bilat_lattice3d, sigma_sp = 2.0, sigma_r = 8)
all.equal(dim(out3d), d3)
#> [1] TRUE

# visualize 3D mid-slice
zmid <- ceiling(d3[3]/2)
viz3 <- rbind(
  transform(expand.grid(x = seq_len(d3[1]), y = seq_len(d3[2])), val = as.vector(vol[,,zmid]), method = "noisy"),
  transform(expand.grid(x = seq_len(d3[1]), y = seq_len(d3[2])), val = as.vector(out3d[,,zmid]), method = "VST + bilat_lattice3d")
)
ggplot(viz3, aes(x, y, fill = val)) + geom_raster() + coord_fixed() + scale_y_reverse() +
  scale_fill_gradient(low = "black", high = "white") + facet_wrap(~method) + theme_minimal(base_size = 10) +
  labs(title = "VST-based denoising (3D)", x = NULL, y = NULL, fill = "intensity")

When to use VST

VST is especially helpful when the noise level varies with signal intensity (magnitude MR). It can make downstream parameter choices more stable and bring the noise closer to additive Gaussian, which is what bilateral, guided, TV and MP–PCA assume. For complex‑valued reconstructions or already Gaussianized data, you may skip VST and denoise directly.