Skip to main content

qsm_core/inversion/
qsmgan.rs

1//! QSMGAN dipole inversion (`onnx` feature).
2//!
3//! QSMGAN (Chen 2020) inverts the dipole with a 3D U-Net **generator** refined by a
4//! Wasserstein GAN. Inference is patch-based with an increased receptive field: a
5//! 64³ local-field patch maps to a 48³ susceptibility patch (a centre crop baked
6//! into the exported graph — the "i64o48" scheme). The network itself is the only
7//! part in ONNX; this glue reproduces the authors' `recon.py` pipeline:
8//!
9//! * sign flip — the QSM-CI runner negates the local field before the net;
10//! * tile by non-overlapping 48³ **output** patches, each fed a 64³ **input** patch
11//!   (same centre, 8-voxel context margin, zero-padded at the volume edges);
12//! * `input_scale = 100`; output head `tanh`, so `χ = atanh(clip(out)) / 10`;
13//! * mask the result.
14//!
15//! The input is the SEPIA-style **local field in ppm** (already background-removed).
16//! Weights are not bundled; the caller passes the exported `qsmgan.onnx` bytes.
17
18use crate::grid::Grid;
19use crate::models::onnx::{OnnxError, OnnxModel, Tensor};
20
21const IPS: usize = 64; // input patch size
22const OPS: usize = 48; // output patch size
23const INPUT_SCALE: f64 = 100.0;
24const OUTPUT_SCALE: f64 = 10.0;
25
26/// Run QSMGAN on a background-removed local field (ppm), column-major `(nx,ny,nz)`.
27/// Returns susceptibility (ppm), masked, in the same layout.
28pub fn qsmgan(
29    local_field_ppm: &[f64],
30    mask: &[u8],
31    grid: &Grid,
32    model_onnx: &[u8],
33) -> Result<Vec<f64>, OnnxError> {
34    let (nx, ny, nz) = grid.dims;
35    let n = nx * ny * nz;
36    assert_eq!(local_field_ppm.len(), n, "field length must match grid");
37    assert_eq!(mask.len(), n, "mask length must match grid");
38
39    let model = OnnxModel::load(model_onnx)?;
40    let (half_i, half_o) = (IPS as i64 / 2, OPS as i64 / 2); // 32, 24
41    let (nxi, nyi, nzi) = (nx as i64, ny as i64, nz as i64);
42    // Negated local field (the runner's sign flip), zero outside the volume.
43    let field = |x: i64, y: i64, z: i64| -> f32 {
44        if x >= 0 && x < nxi && y >= 0 && y < nyi && z >= 0 && z < nzi {
45            (-INPUT_SCALE * local_field_ppm[x as usize + nx * (y as usize + ny * z as usize)]) as f32
46        } else {
47            0.0
48        }
49    };
50
51    let mut predict = vec![0.0f64; n];
52    // Output-patch centres: range(half_o, dim + half_o + 1, OPS) per axis.
53    let mut cx = half_o;
54    while cx <= nxi + half_o {
55        let mut cy = half_o;
56        while cy <= nyi + half_o {
57            let mut cz = half_o;
58            while cz <= nzi + half_o {
59                // In-volume extent of this 48³ output patch.
60                let (x0, y0, z0) = (cx - half_o, cy - half_o, cz - half_o); // ≥ 0
61                let xe = (x0 + OPS as i64).min(nxi);
62                let ye = (y0 + OPS as i64).min(nyi);
63                let ze = (z0 + OPS as i64).min(nzi);
64                let (px, py, pz) = (xe - x0, ye - y0, ze - z0);
65                if px <= 0 || py <= 0 || pz <= 0 {
66                    cz += OPS as i64;
67                    continue;
68                }
69
70                // 64³ input patch centred at (cx,cy,cz), row-major NCDHW.
71                let mut inp = vec![0.0f32; IPS * IPS * IPS];
72                for i in 0..IPS {
73                    for j in 0..IPS {
74                        for k in 0..IPS {
75                            let v = field(
76                                cx - half_i + i as i64,
77                                cy - half_i + j as i64,
78                                cz - half_i + k as i64,
79                            );
80                            inp[(i * IPS + j) * IPS + k] = v;
81                        }
82                    }
83                }
84                let out = model.run_single(&Tensor::new(vec![1, 1, IPS, IPS, IPS], inp))?;
85
86                // Place the in-bounds part of the 48³ output patch: χ = atanh(clip)/10.
87                for oi in 0..px as usize {
88                    for oj in 0..py as usize {
89                        for ok in 0..pz as usize {
90                            let o = (out.data[(oi * OPS + oj) * OPS + ok] as f64)
91                                .clamp(-0.999999, 0.999999);
92                            let dst = (x0 as usize + oi)
93                                + nx * ((y0 as usize + oj) + ny * (z0 as usize + ok));
94                            predict[dst] = o.atanh() / OUTPUT_SCALE;
95                        }
96                    }
97                }
98                cz += OPS as i64;
99            }
100            cy += OPS as i64;
101        }
102        cx += OPS as i64;
103    }
104
105    for i in 0..n {
106        if mask[i] == 0 {
107            predict[i] = 0.0;
108        }
109    }
110    Ok(predict)
111}