Skip to main content

Module qsmnet

Module qsmnet 

Source
Expand description

QSMnet deep-learning dipole inversion (onnx feature).

QSMnet is a 3D U-Net (SNU-LIST) that maps a local field (ppm) to susceptibility (ppm). The upstream weights are TensorFlow, but we ship a clean PyTorch re-export (scripts/onnx-export/export_qsmnet.py): the plain U-Net rebuilt in PyTorch with the TF weights ported in, giving a tidy NCDHW ONNX ([1, 1, X, Y, Z]) that the pure-Rust tract engine runs — unlike the tf2onnx graph, whose NHWC↔NCHW Reshape/Transpose ops tract can’t analyse. This mirrors the authors’ inference (Code/inference.py): normalize by the dataset mean/std shipped with the checkpoint, centered zero-pad each dim to a multiple of 16 (four pool/deconv levels), run, crop, de-normalize, and mask.

Weights are not bundled; the caller passes the exported qsmnet.onnx bytes (see crate::models).

§Reference pipeline

From the authors’ README (SNU-LIST/QSMnet README.md:34-72) unless noted:

  • Training data: 3T (Siemens), single-echo 3D GRE, 1 mm isotropic, TE = 25 ms; COSMOS labels.
  • Phase processing: FSL BET brain extraction, STI Suite Laplacian unwrapping, STI Suite 3D V-SHARP background removal.
  • Resolution: data at another resolution “need to” be interpolated to 1 mm isotropic (e.g. Fourier zero-padding/truncation) before phase processing. This crate does not resample; the caller must.
  • Units: the V-SHARP tissue phase is converted to ppm as phase / (ΣTE · B0 · γ) (save_input_data_for_QSMnet.m:22), i.e. the field in ppm this module takes.
  • Output: Code/inference.py crops but does not multiply by the mask; this crate does.
  • Orientation: inputs must match the orientation/polarity of the shipped val_input.mat (README.md:86): axial, B0 along the third axis.

§Activation and parity

The two checkpoints differ in more than their weights: each one’s network_info_<name>.npy names its activation, and the authors’ inference builds the U-Net with it. QSMnet_64 uses plain ReLU, QSMnet+_64 LeakyReLU (α 0.1). The first qsmnet.onnx export used LeakyReLU for both, and was checked against a TF graph built the same way, so it went unnoticed: χ came out 1.34× too large, 81 % NRMSE from the original (QSM.rs#330). export_qsmnet.py now takes the activation from network_info.

Measured against QSM-CI’s qsmnet and qsmnet-plus entries (TensorFlow 1.14, SNU-LIST/QSMnet f8782ed, the authors’ checkpoints) on a 36×32×28 in-vivo box of prisma-bridge-run1: qsmnet is 0.0002 % NRMSE from the original (max |Δ| 2.9e-7 ppm; before the re-export, 81 %). Run through ONNX Runtime with the same normalisation and padding, the exported graph is 0.00006 % from it and QSMnet+’s is 0.0001 %. The exported graph matches the TF graph to 4.4e-6 on the export’s own check.

tests/models_onnx/dipole.rs checks both against those entries on that box (QSMxT/QSM-CI#316; the fixture is regenerated in Actions by tests/data/dl_dipole_qsmci_small.py): QSMnet+ NRMSE 3.7e-4 %, max |Δ| 4.3e-7 ppm (float32 rounding).

§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): qsmnet.mask-bet, qsmnet.unwrap-laplacian, qsmnet.echo-combination, qsmnet.echo-weights, qsmnet.bg-vsharp. All are the authors’ setting (Kind::Reference): the README instructs BET, Laplacian unwrapping and V-SHARP as its process flow without saying they are needed or better (reclassified from recommendations 2026-11-04).

Structs§

QsmnetNorm
Dataset normalization constants stored beside a QSMnet checkpoint (norm_factor_<name>.mat): field_n = (field - in_mean)/in_std, and chi = out_std*pred + out_mean.

Functions§

qsmnet
Run QSMnet dipole inversion.
qsmnet_tiled
Memory-bounded QSMnet via overlap-tiling — the fully-convolutional U-Net run patch-by-patch (for 32-bit WASM, where whole-volume qsmnet overflows the heap on clinical data). The dataset normalization is applied per value inside the tile loop and the net’s pool depth requires a /16 patch. Approximates whole-volume up to tile-boundary error; see crate::inversion::tiled.