qsm_core/separation/
chisepnet.rs1use crate::grid::Grid;
19use crate::models::onnx::{OnnxError, OnnxModel, Tensor};
20
21const PD: usize = 192; const PH: usize = 192; const PW: usize = 128; #[derive(Clone, Copy, Debug)]
30pub struct ChiSepNetNorm {
31 pub qsm: (f64, f64),
32 pub field: (f64, f64),
33 pub r2prime: (f64, f64),
34 pub chi_pos: (f64, f64),
35 pub chi_neg: (f64, f64),
36 pub dr: f64,
37}
38
39impl Default for ChiSepNetNorm {
40 fn default() -> Self {
42 Self {
43 qsm: (-0.0013402086915448308, 0.031504031270742416),
44 field: (-0.0026045702397823334, 0.010849659331142902),
45 r2prime: (0.05141879618167877, 0.06977531313896179),
46 chi_pos: (0.025912819430232048, 0.03526417911052704),
47 chi_neg: (0.026700690388679504, 0.027936099097132683),
48 dr: 114.0,
49 }
50 }
51}
52
53#[allow(clippy::too_many_arguments)]
63pub fn chisepnet(
64 local_field_ppm: &[f64],
65 qsm: &[f64],
66 r2prime: &[f64],
67 mask: &[u8],
68 grid: &Grid,
69 model_onnx: &[u8],
70 norm: &ChiSepNetNorm,
71) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), OnnxError> {
72 let (nx, ny, nz) = grid.dims;
73 let n = nx * ny * nz;
74 for (name, v) in [("field", local_field_ppm), ("qsm", qsm), ("r2prime", r2prime)] {
75 assert_eq!(v.len(), n, "{name} length must match grid");
76 }
77 assert_eq!(mask.len(), n, "mask length must match grid");
78
79 let (px, py, pz) = (nx.max(PD), ny.max(PH), nz.max(PW));
81 let mut ch = [vec![0.0f32; px * py * pz], vec![0.0f32; px * py * pz], vec![0.0f32; px * py * pz]];
82 for z in 0..nz {
83 for y in 0..ny {
84 for x in 0..nx {
85 let i = x + nx * (y + ny * z);
86 if mask[i] == 0 {
87 continue;
88 }
89 let p = x + px * (y + py * z);
90 ch[0][p] = ((qsm[i] - norm.qsm.0) / norm.qsm.1) as f32;
91 ch[1][p] = ((local_field_ppm[i] - norm.field.0) / norm.field.1) as f32;
92 ch[2][p] = ((r2prime[i] / norm.dr - norm.r2prime.0) / norm.r2prime.1) as f32;
93 }
94 }
95 }
96
97 let starts = |size: usize, patch: usize| -> Vec<usize> {
99 if size <= patch {
100 return vec![0];
101 }
102 let step = (patch as f64 * 0.75) as usize;
103 let mut s: Vec<usize> = (0..=size - patch).step_by(step.max(1)).collect();
104 if *s.last().unwrap() != size - patch {
105 s.push(size - patch);
106 }
107 s
108 };
109
110 let model = OnnxModel::load(model_onnx)?;
111 let plane = PD * PH * PW;
112 let mut acc0 = vec![0.0f64; px * py * pz];
113 let mut acc1 = vec![0.0f64; px * py * pz];
114 let mut wsum = vec![0.0f64; px * py * pz];
115 for &x0 in &starts(px, PD) {
116 for &y0 in &starts(py, PH) {
117 for &z0 in &starts(pz, PW) {
118 let mut buf = vec![0.0f32; 3 * plane];
120 for i in 0..PD {
121 for j in 0..PH {
122 for k in 0..PW {
123 let p = (x0 + i) + px * ((y0 + j) + py * (z0 + k));
124 let o = (i * PH + j) * PW + k;
125 buf[o] = ch[0][p];
126 buf[plane + o] = ch[1][p];
127 buf[2 * plane + o] = ch[2][p];
128 }
129 }
130 }
131 let out = model.run_single(&Tensor::new(vec![1, 3, PD, PH, PW], buf))?;
132 if out.data.len() < 2 * plane {
133 return Err(OnnxError::Run("expected 2-channel output".into()));
134 }
135 for i in 0..PD {
136 for j in 0..PH {
137 for k in 0..PW {
138 let p = (x0 + i) + px * ((y0 + j) + py * (z0 + k));
139 let o = (i * PH + j) * PW + k;
140 acc0[p] += out.data[o] as f64;
141 acc1[p] += out.data[plane + o] as f64;
142 wsum[p] += 1.0;
143 }
144 }
145 }
146 }
147 }
148 }
149
150 let mut chi_pos = vec![0.0f64; n];
152 let mut chi_neg = vec![0.0f64; n];
153 let mut chi_total = vec![0.0f64; n];
154 for z in 0..nz {
155 for y in 0..ny {
156 for x in 0..nx {
157 let i = x + nx * (y + ny * z);
158 if mask[i] == 0 {
159 continue;
160 }
161 let p = x + px * (y + py * z);
162 let w = wsum[p].max(1.0);
163 let pos = (acc0[p] / w) * norm.chi_pos.1 + norm.chi_pos.0;
164 let neg = (acc1[p] / w) * norm.chi_neg.1 + norm.chi_neg.0;
165 chi_pos[i] = pos;
166 chi_neg[i] = -neg;
167 chi_total[i] = pos - neg;
168 }
169 }
170 }
171 Ok((chi_pos, chi_neg, chi_total))
172}