1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[allow(clippy::upper_case_acronyms)]
42pub enum AxisCode {
43 R,
45 L,
47 A,
49 P,
51 S,
53 I,
55}
56
57impl AxisCode {
58 const fn world_axis(self) -> usize {
60 match self {
61 AxisCode::R | AxisCode::L => 0,
62 AxisCode::A | AxisCode::P => 1,
63 AxisCode::S | AxisCode::I => 2,
64 }
65 }
66
67 const fn is_negative(self) -> bool {
70 matches!(self, AxisCode::L | AxisCode::P | AxisCode::I)
71 }
72}
73
74pub fn ras_axes(affine: &[f64; 16]) -> [usize; 3] {
80 let Some(inv) = invert_3x3([
81 [affine[0], affine[1], affine[2]],
82 [affine[4], affine[5], affine[6]],
83 [affine[8], affine[9], affine[10]],
84 ]) else {
85 return [0, 1, 2];
86 };
87 let mut axes = [0usize; 3];
101 let norms: [f64; 3] = std::array::from_fn(|i| {
102 let n = (inv[i][0] * inv[i][0] + inv[i][1] * inv[i][1] + inv[i][2] * inv[i][2]).sqrt();
103 if n > 0.0 { n } else { 1.0 }
104 });
105 for (j, a) in axes.iter_mut().enumerate() {
106 *a = (0..3)
107 .max_by(|&x, &y| {
108 (inv[x][j].abs() / norms[x]).total_cmp(&(inv[y][j].abs() / norms[y]))
109 })
110 .unwrap();
111 }
112 for i in 0..3 {
115 if !axes.contains(&i) {
116 let mut counts = [0usize; 3];
117 for &a in &axes {
118 counts[a] += 1;
119 }
120 let dup = (0..3).max_by_key(|&v| counts[v]).unwrap();
121 if let Some(pos) = axes.iter().rposition(|&a| a == dup) {
122 axes[pos] = i;
123 }
124 }
125 }
126 axes
127}
128
129fn invert_3x3(m: [[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
130 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
131 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
132 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
133 if det.abs() < 1e-12 {
134 return None;
135 }
136 let mut inv = [[0.0f64; 3]; 3];
137 for i in 0..3 {
138 for j in 0..3 {
139 let (a, b) = ((i + 1) % 3, (i + 2) % 3);
140 let (c, d) = ((j + 1) % 3, (j + 2) % 3);
141 inv[j][i] = (m[a][c] * m[b][d] - m[a][d] * m[b][c]) / det;
143 }
144 }
145 Some(inv)
146}
147
148pub fn alignment_to(affine: &[f64; 16], target: [AxisCode; 3]) -> ([usize; 3], [bool; 3]) {
158 let ras = ras_axes(affine);
159 let m = [
160 [affine[0], affine[1], affine[2]],
161 [affine[4], affine[5], affine[6]],
162 [affine[8], affine[9], affine[10]],
163 ];
164 let mut perm = [0usize; 3];
165 let mut flip = [false; 3];
166 for i in 0..3 {
167 let w = target[i].world_axis();
168 perm[i] = ras[w];
169 flip[i] = (m[w][ras[w]] < 0.0) != target[i].is_negative();
172 }
173 (perm, flip)
174}
175
176pub fn invert_alignment(perm: [usize; 3], flip: [bool; 3]) -> ([usize; 3], [bool; 3]) {
178 let mut inv_perm = [0usize; 3];
179 for i in 0..3 {
180 inv_perm[perm[i]] = i;
181 }
182 let mut inv_flip = [false; 3];
183 for j in 0..3 {
184 inv_flip[j] = flip[inv_perm[j]];
185 }
186 (inv_perm, inv_flip)
187}
188
189pub fn alignment_is_identity(perm: [usize; 3], flip: [bool; 3]) -> bool {
191 perm == [0, 1, 2] && flip == [false; 3]
192}
193
194pub fn reorient<T: Copy>(
199 data: &[T],
200 dims: (usize, usize, usize),
201 perm: [usize; 3],
202 flip: [bool; 3],
203) -> (Vec<T>, (usize, usize, usize)) {
204 let src = [dims.0, dims.1, dims.2];
205 assert_eq!(data.len(), src[0] * src[1] * src[2], "data length must match dims");
206 let out_dims = [src[perm[0]], src[perm[1]], src[perm[2]]];
207 let strides = [1isize, src[0] as isize, (src[0] * src[1]) as isize];
208
209 let mut base = 0isize;
211 let mut step = [0isize; 3];
212 for i in 0..3 {
213 let s = strides[perm[i]];
214 if flip[i] {
215 base += (out_dims[i] as isize - 1) * s;
216 step[i] = -s;
217 } else {
218 step[i] = s;
219 }
220 }
221
222 let mut out = Vec::with_capacity(out_dims[0] * out_dims[1] * out_dims[2]);
223 for o2 in 0..out_dims[2] {
224 let p2 = base + step[2] * o2 as isize;
225 for o1 in 0..out_dims[1] {
226 let p1 = p2 + step[1] * o1 as isize;
227 for o0 in 0..out_dims[0] {
228 out.push(data[(p1 + step[0] * o0 as isize) as usize]);
229 }
230 }
231 }
232 (out, (out_dims[0], out_dims[1], out_dims[2]))
233}
234
235pub fn voxel_sizes_from_affine(affine: &[f64; 16]) -> (f64, f64, f64) {
237 let col = |j: usize| {
238 (affine[j] * affine[j] + affine[4 + j] * affine[4 + j] + affine[8 + j] * affine[8 + j])
239 .sqrt()
240 };
241 (col(0), col(1), col(2))
242}
243
244pub fn b0_direction_from_affine(affine: &[f64; 16]) -> (f64, f64, f64) {
256 let (sx, sy, sz) = voxel_sizes_from_affine(affine);
257 if sx < 1e-10 || sy < 1e-10 || sz < 1e-10 {
258 return (0.0, 0.0, 1.0);
259 }
260 let (bx, by, bz) = (affine[8] / sx, affine[9] / sy, affine[10] / sz);
262 let norm = (bx * bx + by * by + bz * bz).sqrt();
263 if norm < 1e-10 {
264 return (0.0, 0.0, 1.0);
265 }
266 (bx / norm, by / norm, bz / norm)
267}
268
269pub fn b0_angle_from_affine(affine: &[f64; 16]) -> f64 {
272 let (_, _, bz) = b0_direction_from_affine(affine);
273 bz.clamp(-1.0, 1.0).abs().acos().to_degrees()
274}
275
276pub fn obliquity_axes_from_affine(affine: &[f64; 16]) -> [f64; 3] {
279 let sizes = voxel_sizes_from_affine(affine);
280 let sizes = [sizes.0, sizes.1, sizes.2];
281 let mut out = [0.0f64; 3];
282 for (i, o) in out.iter_mut().enumerate() {
283 let best = (0..3)
285 .map(|j| {
286 if sizes[j] < 1e-10 {
287 0.0
288 } else {
289 (affine[4 * i + j] / sizes[j]).abs()
290 }
291 })
292 .fold(0.0f64, f64::max);
293 *o = best.clamp(0.0, 1.0).acos().to_degrees();
294 }
295 out
296}
297
298pub fn obliquity_from_affine(affine: &[f64; 16]) -> f64 {
305 let a = obliquity_axes_from_affine(affine);
306 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
307}
308
309pub struct GridMap {
317 pub dst_dims: (usize, usize, usize),
318 src_dims: (usize, usize, usize),
319 dst_affine: [f64; 16],
321 inv_src: [[f64; 4]; 3],
323}
324
325pub(crate) fn world_to_voxel(affine: &[f64; 16]) -> Option<[[f64; 4]; 3]> {
327 let r = [
328 [affine[0], affine[1], affine[2]],
329 [affine[4], affine[5], affine[6]],
330 [affine[8], affine[9], affine[10]],
331 ];
332 let t = [affine[3], affine[7], affine[11]];
333 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
334 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
335 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
336 if det.abs() < 1e-12 {
337 return None;
338 }
339 let id = 1.0 / det;
340 let inv = [
341 [
342 (r[1][1] * r[2][2] - r[1][2] * r[2][1]) * id,
343 (r[0][2] * r[2][1] - r[0][1] * r[2][2]) * id,
344 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) * id,
345 ],
346 [
347 (r[1][2] * r[2][0] - r[1][0] * r[2][2]) * id,
348 (r[0][0] * r[2][2] - r[0][2] * r[2][0]) * id,
349 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) * id,
350 ],
351 [
352 (r[1][0] * r[2][1] - r[1][1] * r[2][0]) * id,
353 (r[0][1] * r[2][0] - r[0][0] * r[2][1]) * id,
354 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) * id,
355 ],
356 ];
357 let mut out = [[0.0f64; 4]; 3];
358 for (i, row) in out.iter_mut().enumerate() {
359 row[..3].copy_from_slice(&inv[i]);
360 row[3] = -(inv[i][0] * t[0] + inv[i][1] * t[1] + inv[i][2] * t[2]);
361 }
362 Some(out)
363}
364
365impl GridMap {
366 pub fn new(
368 src_dims: (usize, usize, usize),
369 src_affine: &[f64; 16],
370 dst_dims: (usize, usize, usize),
371 dst_affine: &[f64; 16],
372 ) -> Option<Self> {
373 Some(Self {
374 dst_dims,
375 src_dims,
376 dst_affine: *dst_affine,
377 inv_src: world_to_voxel(src_affine)?,
378 })
379 }
380
381 fn source_coord(&self, i: usize, j: usize, k: usize) -> Option<(f64, f64, f64)> {
383 let (i, j, k) = (i as f64, j as f64, k as f64);
384 let a = &self.dst_affine;
385 let w = [
386 a[0] * i + a[1] * j + a[2] * k + a[3],
387 a[4] * i + a[5] * j + a[6] * k + a[7],
388 a[8] * i + a[9] * j + a[10] * k + a[11],
389 ];
390 let m = &self.inv_src;
391 let o = (
392 m[0][0] * w[0] + m[0][1] * w[1] + m[0][2] * w[2] + m[0][3],
393 m[1][0] * w[0] + m[1][1] * w[1] + m[1][2] * w[2] + m[1][3],
394 m[2][0] * w[0] + m[2][1] * w[1] + m[2][2] * w[2] + m[2][3],
395 );
396 let (nx, ny, nz) = self.src_dims;
397 let inside = o.0 >= -0.5 && o.0 <= nx as f64 - 0.5
398 && o.1 >= -0.5 && o.1 <= ny as f64 - 0.5
399 && o.2 >= -0.5 && o.2 <= nz as f64 - 0.5;
400 if inside { Some(o) } else { None }
401 }
402
403 pub fn sample(&self, data: &[f64]) -> Vec<f64> {
405 let (nx, ny, nz) = self.src_dims;
406 let (dx, dy, dz) = self.dst_dims;
407 let mut out = vec![0.0f64; dx * dy * dz];
408 for k in 0..dz {
409 for j in 0..dy {
410 for i in 0..dx {
411 if let Some((ox, oy, oz)) = self.source_coord(i, j, k) {
412 out[i + j * dx + k * dx * dy] = trilinear_sample(data, nx, ny, nz, ox, oy, oz);
413 }
414 }
415 }
416 }
417 out
418 }
419
420 pub fn sample_nearest(&self, data: &[u8]) -> Vec<u8> {
422 let (nx, ny, nz) = self.src_dims;
423 let (dx, dy, dz) = self.dst_dims;
424 let mut out = vec![0u8; dx * dy * dz];
425 for k in 0..dz {
426 for j in 0..dy {
427 for i in 0..dx {
428 if let Some((ox, oy, oz)) = self.source_coord(i, j, k) {
429 let xi = (ox.round() as isize).clamp(0, nx as isize - 1) as usize;
430 let yi = (oy.round() as isize).clamp(0, ny as isize - 1) as usize;
431 let zi = (oz.round() as isize).clamp(0, nz as isize - 1) as usize;
432 out[i + j * dx + k * dx * dy] = data[xi + yi * nx + zi * nx * ny];
433 }
434 }
435 }
436 }
437 out
438 }
439
440 fn sample_labels<T: Copy + PartialEq + Default>(&self, data: &[T]) -> Vec<T> {
443 let (dx, dy, dz) = self.dst_dims;
444 let mut out = vec![T::default(); dx * dy * dz];
445 for k in 0..dz {
446 for j in 0..dy {
447 for i in 0..dx {
448 if let Some((ox, oy, oz)) = self.source_coord(i, j, k) {
449 out[i + j * dx + k * dx * dy] = majority_label(data, self.src_dims, ox, oy, oz);
450 }
451 }
452 }
453 }
454 out
455 }
456}
457
458fn majority_label<T: Copy + PartialEq>(
462 data: &[T],
463 (nx, ny, nz): (usize, usize, usize),
464 x: f64,
465 y: f64,
466 z: f64,
467) -> T {
468 let axis = |c: f64, n: usize| {
471 let c0 = (c.floor() as isize).clamp(0, n as isize - 1) as usize;
472 let c1 = (c0 + 1).min(n - 1);
473 (c0, c1, (c - c0 as f64).clamp(0.0, 1.0))
474 };
475 let (x0, x1, fx) = axis(x, nx);
476 let (y0, y1, fy) = axis(y, ny);
477 let (z0, z1, fz) = axis(z, nz);
478 let idx = |a: usize, b: usize, c: usize| a + b * nx + c * nx * ny;
479 let nearest = data[idx(
480 if fx < 0.5 { x0 } else { x1 },
481 if fy < 0.5 { y0 } else { y1 },
482 if fz < 0.5 { z0 } else { z1 },
483 )];
484
485 let mut labels: [Option<(T, f64)>; 8] = [None; 8];
486 for (cx, wx) in [(x0, 1.0 - fx), (x1, fx)] {
487 for (cy, wy) in [(y0, 1.0 - fy), (y1, fy)] {
488 for (cz, wz) in [(z0, 1.0 - fz), (z1, fz)] {
489 let w = wx * wy * wz;
490 let label = data[idx(cx, cy, cz)];
491 for slot in labels.iter_mut() {
492 match slot {
493 Some((l, total)) if *l == label => {
494 *total += w;
495 break;
496 }
497 None => {
498 *slot = Some((label, w));
499 break;
500 }
501 Some(_) => {}
502 }
503 }
504 }
505 }
506 }
507
508 const TIE: f64 = 1e-12;
509 let best = labels.iter().flatten().map(|&(_, w)| w).fold(f64::NEG_INFINITY, f64::max);
510 if labels.iter().flatten().any(|&(l, w)| l == nearest && w >= best - TIE) {
511 return nearest;
512 }
513 labels
514 .iter()
515 .flatten()
516 .find(|&&(_, w)| w >= best - TIE)
517 .map_or(nearest, |&(l, _)| l)
518}
519
520pub fn resample_onto(
523 data: &[f64],
524 src_dims: (usize, usize, usize),
525 src_affine: &[f64; 16],
526 dst_dims: (usize, usize, usize),
527 dst_affine: &[f64; 16],
528) -> Option<Vec<f64>> {
529 Some(GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?.sample(data))
530}
531
532pub fn resample_mask_onto(
535 mask: &[u8],
536 src_dims: (usize, usize, usize),
537 src_affine: &[f64; 16],
538 dst_dims: (usize, usize, usize),
539 dst_affine: &[f64; 16],
540) -> Option<Vec<u8>> {
541 Some(GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?.sample_nearest(mask))
542}
543
544pub fn resample_labels_onto<T: Copy + PartialEq + Default>(
566 labels: &[T],
567 src_dims: (usize, usize, usize),
568 src_affine: &[f64; 16],
569 dst_dims: (usize, usize, usize),
570 dst_affine: &[f64; 16],
571) -> Result<Vec<T>, crate::error::InputError> {
572 crate::error::check_len("labels", labels.len(), src_dims.0 * src_dims.1 * src_dims.2)?;
573 let map = GridMap::new(src_dims, src_affine, dst_dims, dst_affine).ok_or_else(|| {
574 crate::error::InputError("the source affine is singular".into())
575 })?;
576 Ok(map.sample_labels(labels))
577}
578
579pub fn resample_complex_onto(
582 magnitude: &[f64],
583 phase: &[f64],
584 src_dims: (usize, usize, usize),
585 src_affine: &[f64; 16],
586 dst_dims: (usize, usize, usize),
587 dst_affine: &[f64; 16],
588) -> Option<(Vec<f64>, Vec<f64>)> {
589 let n = src_dims.0 * src_dims.1 * src_dims.2;
590 assert_eq!(magnitude.len(), n, "magnitude length does not match source dimensions");
591 assert_eq!(phase.len(), n, "phase length does not match source dimensions");
592 let map = GridMap::new(src_dims, src_affine, dst_dims, dst_affine)?;
593 let (real, imag) = to_complex(magnitude, phase);
594 Some(from_complex(&map.sample(&real), &map.sample(&imag)))
595}
596
597fn to_complex(magnitude: &[f64], phase: &[f64]) -> (Vec<f64>, Vec<f64>) {
599 use crate::utils::portable_math::{cos, sin};
600 magnitude.iter().zip(phase).map(|(&m, &p)| (m * cos(p), m * sin(p))).unzip()
601}
602
603fn from_complex(real: &[f64], imag: &[f64]) -> (Vec<f64>, Vec<f64>) {
607 use crate::utils::portable_math::{atan2, hypot};
608 real.iter()
609 .zip(imag)
610 .map(|(&re, &im)| {
611 if re == 0.0 && im == 0.0 {
612 (0.0, 0.0)
613 } else {
614 (hypot(re, im), atan2(im, re))
615 }
616 })
617 .unzip()
618}
619
620pub struct AxialGrid {
624 pub dims: (usize, usize, usize),
625 pub voxel_size: (f64, f64, f64),
626 pub affine: [f64; 16],
628 src_dims: (usize, usize, usize),
630 world_min: [f64; 3],
631 inv_r: [[f64; 3]; 3],
633 t: [f64; 3],
634}
635
636pub fn axial_grid_for(
640 nx: usize,
641 ny: usize,
642 nz: usize,
643 affine: &[f64; 16],
644) -> AxialGrid {
645 let r = [
646 [affine[0], affine[1], affine[2]],
647 [affine[4], affine[5], affine[6]],
648 [affine[8], affine[9], affine[10]],
649 ];
650 let t = [affine[3], affine[7], affine[11]];
651 let vs = voxel_sizes_from_affine(affine);
657 let vs = [vs.0, vs.1, vs.2];
658 let ras = ras_axes(affine);
659 let (vsx, vsy, vsz) = (vs[ras[0]], vs[ras[1]], vs[ras[2]]);
660
661 let mut world_min = [f64::INFINITY; 3];
663 let mut world_max = [f64::NEG_INFINITY; 3];
664 for &(vi, vj, vk) in &[
665 (0.0, 0.0, 0.0),
666 (nx as f64 - 1.0, 0.0, 0.0),
667 (0.0, ny as f64 - 1.0, 0.0),
668 (0.0, 0.0, nz as f64 - 1.0),
669 (nx as f64 - 1.0, ny as f64 - 1.0, 0.0),
670 (nx as f64 - 1.0, 0.0, nz as f64 - 1.0),
671 (0.0, ny as f64 - 1.0, nz as f64 - 1.0),
672 (nx as f64 - 1.0, ny as f64 - 1.0, nz as f64 - 1.0),
673 ] {
674 for d in 0..3 {
675 let w = r[d][0] * vi + r[d][1] * vj + r[d][2] * vk + t[d];
676 world_min[d] = world_min[d].min(w);
677 world_max[d] = world_max[d].max(w);
678 }
679 }
680
681 let dims = (
682 ((world_max[0] - world_min[0]) / vsx).ceil() as usize + 1,
683 ((world_max[1] - world_min[1]) / vsy).ceil() as usize + 1,
684 ((world_max[2] - world_min[2]) / vsz).ceil() as usize + 1,
685 );
686
687 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
688 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
689 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
690 let inv_det = if det.abs() < 1e-12 { 0.0 } else { 1.0 / det };
691 let inv_r = [
692 [
693 (r[1][1] * r[2][2] - r[1][2] * r[2][1]) * inv_det,
694 (r[0][2] * r[2][1] - r[0][1] * r[2][2]) * inv_det,
695 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) * inv_det,
696 ],
697 [
698 (r[1][2] * r[2][0] - r[1][0] * r[2][2]) * inv_det,
699 (r[0][0] * r[2][2] - r[0][2] * r[2][0]) * inv_det,
700 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) * inv_det,
701 ],
702 [
703 (r[1][0] * r[2][1] - r[1][1] * r[2][0]) * inv_det,
704 (r[0][1] * r[2][0] - r[0][0] * r[2][1]) * inv_det,
705 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) * inv_det,
706 ],
707 ];
708
709 AxialGrid {
710 dims,
711 voxel_size: (vsx, vsy, vsz),
712 affine: [
713 vsx, 0.0, 0.0, world_min[0],
714 0.0, vsy, 0.0, world_min[1],
715 0.0, 0.0, vsz, world_min[2],
716 0.0, 0.0, 0.0, 1.0,
717 ],
718 src_dims: (nx, ny, nz),
719 world_min,
720 inv_r,
721 t,
722 }
723}
724
725impl AxialGrid {
726 fn source_coord(&self, ni: usize, nj: usize, nk: usize) -> Option<(f64, f64, f64)> {
728 let (nx, ny, nz) = self.src_dims;
729 let dx = self.world_min[0] + ni as f64 * self.voxel_size.0 - self.t[0];
730 let dy = self.world_min[1] + nj as f64 * self.voxel_size.1 - self.t[1];
731 let dz = self.world_min[2] + nk as f64 * self.voxel_size.2 - self.t[2];
732 let ox = self.inv_r[0][0] * dx + self.inv_r[0][1] * dy + self.inv_r[0][2] * dz;
733 let oy = self.inv_r[1][0] * dx + self.inv_r[1][1] * dy + self.inv_r[1][2] * dz;
734 let oz = self.inv_r[2][0] * dx + self.inv_r[2][1] * dy + self.inv_r[2][2] * dz;
735 let inside = ox >= -0.5
736 && ox <= nx as f64 - 0.5
737 && oy >= -0.5
738 && oy <= ny as f64 - 0.5
739 && oz >= -0.5
740 && oz <= nz as f64 - 0.5;
741 if inside { Some((ox, oy, oz)) } else { None }
742 }
743
744 pub fn sample(&self, data: &[f64]) -> Vec<f64> {
746 let (nx, ny, nz) = self.src_dims;
747 let (dx, dy, dz) = self.dims;
748 let mut out = vec![0.0f64; dx * dy * dz];
749 for nk in 0..dz {
750 for nj in 0..dy {
751 for ni in 0..dx {
752 if let Some((ox, oy, oz)) = self.source_coord(ni, nj, nk) {
753 out[ni + nj * dx + nk * dx * dy] =
754 trilinear_sample(data, nx, ny, nz, ox, oy, oz);
755 }
756 }
757 }
758 }
759 out
760 }
761
762 pub fn sample_nearest(&self, data: &[u8]) -> Vec<u8> {
764 let (nx, ny, nz) = self.src_dims;
765 let (dx, dy, dz) = self.dims;
766 let mut out = vec![0u8; dx * dy * dz];
767 for nk in 0..dz {
768 for nj in 0..dy {
769 for ni in 0..dx {
770 if let Some((ox, oy, oz)) = self.source_coord(ni, nj, nk) {
771 let xi = (ox.round() as isize).clamp(0, nx as isize - 1) as usize;
772 let yi = (oy.round() as isize).clamp(0, ny as isize - 1) as usize;
773 let zi = (oz.round() as isize).clamp(0, nz as isize - 1) as usize;
774 out[ni + nj * dx + nk * dx * dy] = data[xi + yi * nx + zi * nx * ny];
775 }
776 }
777 }
778 }
779 out
780 }
781}
782
783pub(crate) fn trilinear_sample(data: &[f64], nx: usize, ny: usize, nz: usize, x: f64, y: f64, z: f64) -> f64 {
785 let x0 = (x.floor() as isize).clamp(0, nx as isize - 1) as usize;
786 let y0 = (y.floor() as isize).clamp(0, ny as isize - 1) as usize;
787 let z0 = (z.floor() as isize).clamp(0, nz as isize - 1) as usize;
788 let x1 = (x0 + 1).min(nx - 1);
789 let y1 = (y0 + 1).min(ny - 1);
790 let z1 = (z0 + 1).min(nz - 1);
791 let (fx, fy, fz) = (x - x0 as f64, y - y0 as f64, z - z0 as f64);
792 let idx = |a: usize, b: usize, c: usize| a + b * nx + c * nx * ny;
793
794 data[idx(x0, y0, z0)] * (1.0 - fx) * (1.0 - fy) * (1.0 - fz)
795 + data[idx(x1, y0, z0)] * fx * (1.0 - fy) * (1.0 - fz)
796 + data[idx(x0, y1, z0)] * (1.0 - fx) * fy * (1.0 - fz)
797 + data[idx(x1, y1, z0)] * fx * fy * (1.0 - fz)
798 + data[idx(x0, y0, z1)] * (1.0 - fx) * (1.0 - fy) * fz
799 + data[idx(x1, y0, z1)] * fx * (1.0 - fy) * fz
800 + data[idx(x0, y1, z1)] * (1.0 - fx) * fy * fz
801 + data[idx(x1, y1, z1)] * fx * fy * fz
802}
803
804pub struct ResampledVolume {
806 pub data: Vec<f64>,
807 pub dims: (usize, usize, usize),
808 pub voxel_size: (f64, f64, f64),
809 pub affine: [f64; 16],
810}
811
812pub struct ResampledComplex {
814 pub magnitude: Vec<f64>,
815 pub phase: Vec<f64>,
817 pub dims: (usize, usize, usize),
818 pub voxel_size: (f64, f64, f64),
819 pub affine: [f64; 16],
820}
821
822pub const QSMXT8_NOISE_FILL_FRACTION: f64 = 0.1;
825
826#[derive(Debug, Clone, Copy, Default)]
828pub struct AxialResampleParams {
829 pub noise_fill_fraction: Option<f64>,
839}
840
841pub fn resample_to_axial(
845 data: &[f64],
846 nx: usize,
847 ny: usize,
848 nz: usize,
849 affine: &[f64; 16],
850) -> ResampledVolume {
851 let grid = axial_grid_for(nx, ny, nz, affine);
852 ResampledVolume {
853 data: grid.sample(data),
854 dims: grid.dims,
855 voxel_size: grid.voxel_size,
856 affine: grid.affine,
857 }
858}
859
860pub fn resample_mask_to_axial(
862 mask: &[u8],
863 nx: usize,
864 ny: usize,
865 nz: usize,
866 affine: &[f64; 16],
867) -> Vec<u8> {
868 axial_grid_for(nx, ny, nz, affine).sample_nearest(mask)
869}
870
871pub fn resample_complex_to_axial(
881 magnitude: &[f64],
882 phase: &[f64],
883 nx: usize,
884 ny: usize,
885 nz: usize,
886 affine: &[f64; 16],
887 params: &AxialResampleParams,
888) -> ResampledComplex {
889 let n = nx * ny * nz;
890 assert_eq!(magnitude.len(), n, "magnitude length does not match dimensions");
891 assert_eq!(phase.len(), n, "phase length does not match dimensions");
892
893 let (real, imag) = to_complex(magnitude, phase);
894 let grid = axial_grid_for(nx, ny, nz, affine);
895 let (mag_out, mut pha_out) = from_complex(&grid.sample(&real), &grid.sample(&imag));
896 let out_n = pha_out.len();
897
898 if let Some(frac) = params.noise_fill_fraction {
899 let zeros = pha_out.iter().filter(|p| **p == 0.0).count();
900 if out_n > 0 && (zeros as f64 / out_n as f64) >= frac {
901 let mut rng = SplitMix64::new(seed_from_slice(&pha_out));
903 for p in pha_out.iter_mut() {
904 if *p == 0.0 {
905 *p = rng.uniform_signed_pi();
906 }
907 }
908 }
909 }
910
911 ResampledComplex {
912 magnitude: mag_out,
913 phase: pha_out,
914 dims: grid.dims,
915 voxel_size: grid.voxel_size,
916 affine: grid.affine,
917 }
918}
919
920fn seed_from_slice(data: &[f64]) -> u64 {
922 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
923 for v in data {
924 for b in v.to_bits().to_le_bytes() {
925 h ^= b as u64;
926 h = h.wrapping_mul(0x0000_0100_0000_01b3);
927 }
928 }
929 h | 1
930}
931
932struct SplitMix64(u64);
934
935impl SplitMix64 {
936 fn new(seed: u64) -> Self {
937 Self(seed)
938 }
939 fn next_u64(&mut self) -> u64 {
940 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
941 let mut z = self.0;
942 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
943 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
944 z ^ (z >> 31)
945 }
946 fn uniform_signed_pi(&mut self) -> f64 {
948 let u = (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64; (u - 0.5) * 2.0 * std::f64::consts::PI
950 }
951}
952
953#[cfg(test)]
954mod tests {
955
956 use super::{alignment_is_identity, alignment_to, invert_alignment, reorient, AxisCode::*};
957
958 fn affine_for(codes: [super::AxisCode; 3]) -> [f64; 16] {
960 let mut a = [0.0f64; 16];
961 a[15] = 1.0;
962 for (axis, c) in codes.iter().enumerate() {
963 let row = c.world_axis();
964 a[4 * row + axis] = if c.is_negative() { -1.0 } else { 1.0 };
965 }
966 a
967 }
968
969 #[test]
976 fn axial_grid_pairs_each_spacing_with_the_axis_it_samples() {
977 let affine = [
978 0.0, 0.0, 3.0, 0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.8, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
982 ];
983 let g = super::axial_grid_for(64, 64, 20, &affine);
984 assert_eq!(g.voxel_size, (3.0, 0.8, 0.8), "spacings followed position, not geometry");
985 assert_eq!(g.dims.0, 20, "L-R spans 20 slices at 3 mm");
991 assert!((64..=65).contains(&g.dims.1), "A-P was {}", g.dims.1);
992 assert!((64..=65).contains(&g.dims.2), "S-I was {}", g.dims.2);
993 }
994
995 fn tilted(affine: &[f64; 16], world_axis: usize, degrees: f64) -> [f64; 16] {
998 let (s, c) = degrees.to_radians().sin_cos();
999 let rot = match world_axis {
1000 0 => [[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]],
1001 1 => [[c, 0.0, s], [0.0, 1.0, 0.0], [-s, 0.0, c]],
1002 _ => [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]],
1003 };
1004 let mut out = *affine;
1005 for row in 0..3 {
1006 for col in 0..4 {
1007 out[4 * row + col] = (0..3).map(|k| rot[row][k] * affine[4 * k + col]).sum();
1008 }
1009 }
1010 out
1011 }
1012
1013 fn sagittal_affine(slice_mm: f64) -> [f64; 16] {
1016 affine_from(
1017 [[0.0, 0.0, slice_mm], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1018 [0.0, 0.0, 0.0],
1019 )
1020 }
1021
1022 #[test]
1035 fn ras_axes_is_not_fooled_by_anisotropy() {
1036 for slice_mm in [1.0, 2.0, 4.0, 8.0] {
1037 for world_axis in 0..3 {
1038 for degrees in [0.0, 10.0, 18.0, 30.0, -25.0] {
1039 let a = tilted(&sagittal_affine(slice_mm), world_axis, degrees);
1040 assert_eq!(
1041 ras_axes(&a), [2, 0, 1],
1042 "{slice_mm} mm slices tilted {degrees}° about world axis {world_axis}",
1043 );
1044 }
1045 }
1046 }
1047 }
1048
1049 #[test]
1054 fn axial_grid_of_a_double_oblique_sagittal_stack_keeps_its_slice_pitch() {
1055 let a = tilted(&tilted(&sagittal_affine(4.0), 2, 18.0), 1, 12.0);
1057 let g = super::axial_grid_for(64, 64, 20, &a);
1058 let (vx, vy, vz) = g.voxel_size;
1059 assert!((vx - 4.0).abs() < 1e-9, "L-R should carry the 4 mm slice pitch, got {vx}");
1060 assert!((vy - 1.0).abs() < 1e-9, "A-P should stay 1 mm, got {vy}");
1061 assert!((vz - 1.0).abs() < 1e-9, "S-I should stay 1 mm, got {vz}");
1062 assert!((20..=40).contains(&g.dims.0), "L-R dim was {}", g.dims.0);
1065 }
1066
1067 #[test]
1068 fn alignment_to_same_orientation_is_identity() {
1069 for codes in [[R, A, S], [L, P, I], [P, R, S], [A, S, L]] {
1070 let (perm, flip) = alignment_to(&affine_for(codes), codes);
1071 assert!(alignment_is_identity(perm, flip), "{codes:?} -> {perm:?} {flip:?}");
1072 }
1073 }
1074
1075 #[test]
1077 fn ras_to_prs_is_an_in_plane_quarter_turn() {
1078 let (perm, flip) = alignment_to(&affine_for([R, A, S]), [P, R, S]);
1079 assert_eq!(perm, [1, 0, 2]);
1080 assert_eq!(flip, [true, false, false]);
1081 }
1082
1083 #[test]
1084 fn alignment_to_handles_a_flipped_axis() {
1085 let (perm, flip) = alignment_to(&affine_for([L, A, S]), [R, A, S]);
1087 assert_eq!(perm, [0, 1, 2]);
1088 assert_eq!(flip, [true, false, false]);
1089 }
1090
1091 #[test]
1092 fn reorient_moves_the_voxel_it_says_it_does() {
1093 let dims = (2, 3, 4);
1094 let data: Vec<f64> = (0..24).map(|v| v as f64).collect();
1095 let (out, out_dims) = reorient(&data, dims, [1, 0, 2], [true, false, false]);
1096 assert_eq!(out_dims, (3, 2, 4));
1097 for o2 in 0..4 {
1099 for o1 in 0..2 {
1100 for o0 in 0..3 {
1101 let got = out[o0 + 3 * (o1 + 2 * o2)];
1102 let want = data[o1 + 2 * ((3 - 1 - o0) + 3 * o2)];
1103 assert_eq!(got, want, "at {o0},{o1},{o2}");
1104 }
1105 }
1106 }
1107 }
1108
1109 #[test]
1111 fn reorient_then_invert_restores_the_volume() {
1112 let dims = (3, 4, 5);
1113 let data: Vec<f64> = (0..60).map(|v| v as f64).collect();
1114 let axes = [[R, L], [A, P], [S, I]];
1115 for p in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] {
1116 for bits in 0..8u8 {
1117 let target =
1118 [axes[p[0]][(bits & 1) as usize], axes[p[1]][((bits >> 1) & 1) as usize],
1119 axes[p[2]][((bits >> 2) & 1) as usize]];
1120 let (perm, flip) = alignment_to(&affine_for([R, A, S]), target);
1121 let (moved, moved_dims) = reorient(&data, dims, perm, flip);
1122 let (back_perm, back_flip) = invert_alignment(perm, flip);
1123 let (back, back_dims) = reorient(&moved, moved_dims, back_perm, back_flip);
1124 assert_eq!(back_dims, dims, "{target:?}");
1125 assert_eq!(back, data, "{target:?}");
1126 }
1127 }
1128 }
1129
1130 #[test]
1133 fn reorienting_actually_reaches_the_target() {
1134 for src in [[R, A, S], [L, A, S], [P, R, S], [I, P, L]] {
1135 for target in [[R, A, S], [P, R, S], [A, L, I]] {
1136 let a = affine_for(src);
1137 let (perm, flip) = alignment_to(&a, target);
1138 let mut moved = [0.0f64; 16];
1140 moved[15] = 1.0;
1141 for i in 0..3 {
1142 let sign = if flip[i] { -1.0 } else { 1.0 };
1143 for row in 0..3 {
1144 moved[4 * row + i] = sign * a[4 * row + perm[i]];
1145 }
1146 }
1147 let (p2, f2) = alignment_to(&moved, target);
1148 assert!(alignment_is_identity(p2, f2), "{src:?} -> {target:?}: {p2:?} {f2:?}");
1149 }
1150 }
1151 }
1152 use super::*;
1153 use std::f64::consts::PI;
1154
1155 fn affine_from(r: [[f64; 3]; 3], t: [f64; 3]) -> [f64; 16] {
1157 [
1158 r[0][0], r[0][1], r[0][2], t[0],
1159 r[1][0], r[1][1], r[1][2], t[1],
1160 r[2][0], r[2][1], r[2][2], t[2],
1161 0.0, 0.0, 0.0, 1.0,
1162 ]
1163 }
1164
1165 fn identity_affine(vs: (f64, f64, f64)) -> [f64; 16] {
1166 affine_from([[vs.0, 0.0, 0.0], [0.0, vs.1, 0.0], [0.0, 0.0, vs.2]], [0.0, 0.0, 0.0])
1167 }
1168
1169 fn ukb_swi_affine() -> [f64; 16] {
1171 affine_from(
1172 [
1173 [0.7976, -0.0273, -0.1075],
1174 [0.0141, 0.7358, -1.1647],
1175 [0.0370, 0.3092, 2.7626],
1176 ],
1177 [-101.6, -87.4, -60.2],
1178 )
1179 }
1180
1181 #[test]
1182 fn voxel_sizes_are_column_norms() {
1183 let (sx, sy, sz) = voxel_sizes_from_affine(&ukb_swi_affine());
1184 assert!((sx - 0.799).abs() < 0.002, "{sx}");
1185 assert!((sy - 0.799).abs() < 0.002, "{sy}");
1186 assert!((sz - 3.0).abs() < 0.002, "{sz}");
1187 }
1188
1189 #[test]
1190 fn b0_direction_axial_is_z() {
1191 for vs in [(1.0, 1.0, 1.0), (0.8, 0.8, 3.0), (2.0, 0.5, 1.0)] {
1192 let (bx, by, bz) = b0_direction_from_affine(&identity_affine(vs));
1193 assert!(bx.abs() < 1e-9 && by.abs() < 1e-9, "{vs:?} -> {bx} {by}");
1194 assert!((bz - 1.0).abs() < 1e-9, "{vs:?} -> {bz}");
1195 assert!(b0_angle_from_affine(&identity_affine(vs)) < 1e-6);
1196 }
1197 }
1198
1199 #[test]
1202 fn b0_direction_of_a_singular_affine_is_z() {
1203 let mut flat = identity_affine((1.0, 1.0, 1.0));
1204 flat[10] = 0.0;
1205 for a in [[0.0; 16], flat] {
1206 assert_eq!(b0_direction_from_affine(&a), (0.0, 0.0, 1.0), "{a:?}");
1207 }
1208 }
1209
1210 #[test]
1214 fn b0_direction_anisotropic_oblique() {
1215 let a = ukb_swi_affine();
1216 let (bx, by, bz) = b0_direction_from_affine(&a);
1217 assert!((bx - 0.0463).abs() < 1e-3, "bx={bx}");
1219 assert!((by - 0.3871).abs() < 1e-3, "by={by}");
1220 assert!((bz - 0.9209).abs() < 1e-3, "bz={bz}");
1221
1222 let angle = b0_angle_from_affine(&a);
1224 assert!((angle - 22.9).abs() < 0.5, "angle={angle}");
1225
1226 let naive = {
1228 let r = [[a[0], a[1], a[2]], [a[4], a[5], a[6]], [a[8], a[9], a[10]]];
1229 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
1230 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
1231 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
1232 let v = [
1233 (r[0][1] * r[1][2] - r[0][2] * r[1][1]) / det,
1234 (r[0][2] * r[1][0] - r[0][0] * r[1][2]) / det,
1235 (r[0][0] * r[1][1] - r[0][1] * r[1][0]) / det,
1236 ];
1237 let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1238 [v[0] / n, v[1] / n, v[2] / n]
1239 };
1240 let dot = (bx * naive[0] + by * naive[1] + bz * naive[2]).abs().clamp(0.0, 1.0);
1241 let between = dot.acos().to_degrees();
1242 assert!(between > 30.0, "the two differ by {between}°, expected >30");
1243 }
1244
1245 #[test]
1248 fn b0_direction_isotropic_oblique_agrees_with_naive() {
1249 let (c, s) = (30.0f64.to_radians().cos(), 30.0f64.to_radians().sin());
1250 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
1251 let (bx, by, bz) = b0_direction_from_affine(&a);
1252 assert!(bx.abs() < 1e-9, "{bx}");
1253 assert!((by - s).abs() < 1e-9, "{by} vs {s}");
1254 assert!((bz - c).abs() < 1e-9, "{bz} vs {c}");
1255 assert!((b0_angle_from_affine(&a) - 30.0).abs() < 1e-6);
1256 }
1257
1258 #[test]
1259 fn obliquity_matches_nibabel() {
1260 let axes = obliquity_axes_from_affine(&ukb_swi_affine());
1262 for (got, want) in axes.iter().zip([2.838, 22.869, 22.947]) {
1263 assert!((got - want).abs() < 0.05, "{axes:?}");
1264 }
1265 let norm = obliquity_from_affine(&ukb_swi_affine());
1266 assert!((norm - 32.521).abs() < 0.05, "{norm}");
1267 assert!(obliquity_from_affine(&identity_affine((0.8, 0.8, 3.0))) < 1e-9);
1269 }
1270
1271 #[test]
1272 fn axial_grid_of_an_axial_volume_is_the_same_grid() {
1273 let a = identity_affine((1.0, 1.0, 2.0));
1274 let g = axial_grid_for(4, 5, 6, &a);
1275 assert_eq!(g.dims, (4, 5, 6));
1276 let (vx, vy, vz) = g.voxel_size;
1277 assert!((vx - 1.0).abs() < 1e-9 && (vy - 1.0).abs() < 1e-9 && (vz - 2.0).abs() < 1e-9);
1278 let data: Vec<f64> = (0..4 * 5 * 6).map(|i| i as f64).collect();
1280 let out = resample_to_axial(&data, 4, 5, 6, &a);
1281 for (i, (got, want)) in out.data.iter().zip(data.iter()).enumerate() {
1282 assert!((got - want).abs() < 1e-6, "voxel {i}: {got} vs {want}");
1283 }
1284 }
1285
1286 #[test]
1287 fn resampled_affine_is_cardinal_and_b0_is_z() {
1288 let g = axial_grid_for(32, 32, 12, &ukb_swi_affine());
1289 for (i, v) in g.affine.iter().enumerate() {
1290 let diagonal = matches!(i, 0 | 5 | 10 | 15);
1291 let translation = matches!(i, 3 | 7 | 11);
1292 if !diagonal && !translation {
1293 assert!(v.abs() < 1e-12, "affine[{i}] = {v} should be 0");
1294 }
1295 }
1296 assert!(b0_angle_from_affine(&g.affine) < 1e-9);
1297 assert!(obliquity_from_affine(&g.affine) < 1e-9);
1298 assert!(g.dims.0 >= 32 && g.dims.1 >= 32 && g.dims.2 >= 12, "{:?}", g.dims);
1300 }
1301
1302 #[test]
1305 fn complex_resampling_survives_wraps_where_scalar_does_not() {
1306 let (nx, ny, nz) = (24, 24, 8);
1307 let n = nx * ny * nz;
1308 let (c, s) = (20.0f64.to_radians().cos(), 20.0f64.to_radians().sin());
1310 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
1311
1312 let true_phase = |x: f64, y: f64, _z: f64| 0.8 * x + 0.3 * y;
1314 let mut mag = vec![0.0; n];
1315 let mut pha = vec![0.0; n];
1316 for k in 0..nz {
1317 for j in 0..ny {
1318 for i in 0..nx {
1319 let idx = i + j * nx + k * nx * ny;
1320 mag[idx] = 100.0;
1321 pha[idx] = wrap(true_phase(i as f64, j as f64, k as f64));
1322 }
1323 }
1324 }
1325 let wraps = (0..nz)
1327 .flat_map(|k| (0..ny).flat_map(move |j| (1..nx).map(move |i| (i, j, k))))
1328 .filter(|&(i, j, k)| {
1329 let idx = i + j * nx + k * nx * ny;
1330 (pha[idx] - pha[idx - 1]).abs() > PI
1331 })
1332 .count();
1333 assert!(wraps > 500, "test data should contain plenty of wraps, found {wraps}");
1334
1335 let cpx = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams { noise_fill_fraction: None });
1336 let scalar = resample_to_axial(&pha, nx, ny, nz, &a);
1337 let grid = axial_grid_for(nx, ny, nz, &a);
1338
1339 let (mut cpx_err, mut scalar_err, mut count) = (0.0f64, 0.0f64, 0usize);
1341 let (dx, dy, dz) = grid.dims;
1342 for k in 0..dz {
1343 for j in 0..dy {
1344 for i in 0..dx {
1345 let Some((ox, oy, oz)) = grid.source_coord(i, j, k) else { continue };
1346 if ox < 1.0 || ox > nx as f64 - 2.0 || oy < 1.0 || oy > ny as f64 - 2.0 || oz < 1.0 || oz > nz as f64 - 2.0 {
1348 continue;
1349 }
1350 let idx = i + j * dx + k * dx * dy;
1351 let want = wrap(true_phase(ox, oy, oz));
1352 cpx_err += wrap(cpx.phase[idx] - want).abs();
1353 scalar_err += wrap(scalar.data[idx] - want).abs();
1354 count += 1;
1355 }
1356 }
1357 }
1358 assert!(count > 500, "too few interior samples: {count}");
1359 let (cpx_err, scalar_err) = (cpx_err / count as f64, scalar_err / count as f64);
1360 assert!(cpx_err < 0.05, "complex resampling error {cpx_err} rad is too high");
1361 assert!(
1362 scalar_err > 10.0 * cpx_err,
1363 "scalar {scalar_err} should be far worse than complex {cpx_err}"
1364 );
1365 for (idx, m) in cpx.magnitude.iter().enumerate() {
1371 if grid_interior(&grid, idx) {
1372 assert!(*m > 90.0 && *m < 100.5, "magnitude {m} at {idx} outside the expected dip");
1373 }
1374 }
1375 }
1376
1377 fn grid_interior(grid: &AxialGrid, idx: usize) -> bool {
1378 let (dx, dy, _) = grid.dims;
1379 let i = idx % dx;
1380 let j = (idx / dx) % dy;
1381 let k = idx / (dx * dy);
1382 grid.source_coord(i, j, k)
1383 .map(|(ox, oy, oz)| {
1384 let (nx, ny, nz) = grid.src_dims;
1385 ox > 1.0 && ox < nx as f64 - 2.0 && oy > 1.0 && oy < ny as f64 - 2.0 && oz > 1.0 && oz < nz as f64 - 2.0
1386 })
1387 .unwrap_or(false)
1388 }
1389
1390 fn wrap(p: f64) -> f64 {
1391 let mut v = (p + PI) % (2.0 * PI);
1392 if v < 0.0 {
1393 v += 2.0 * PI;
1394 }
1395 v - PI
1396 }
1397
1398 #[test]
1401 fn round_trip_to_axial_and_back_recovers_the_original() {
1402 let (nx, ny, nz) = (20, 22, 10);
1403 let n = nx * ny * nz;
1404 let a = ukb_swi_affine();
1405 let data: Vec<f64> = (0..n)
1407 .map(|i| {
1408 let (x, y, z) = (i % nx, (i / nx) % ny, i / (nx * ny));
1409 (x as f64 * 0.1).sin() + (y as f64 * 0.07).cos() + z as f64 * 0.02
1410 })
1411 .collect();
1412
1413 let grid = axial_grid_for(nx, ny, nz, &a);
1414 let there = resample_to_axial(&data, nx, ny, nz, &a);
1415 let back = resample_onto(&there.data, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
1416
1417 let (mut err, mut count) = (0.0f64, 0usize);
1420 for z in 2..nz - 2 {
1421 for y in 2..ny - 2 {
1422 for x in 2..nx - 2 {
1423 let i = x + y * nx + z * nx * ny;
1424 err += (back[i] - data[i]).abs();
1425 count += 1;
1426 }
1427 }
1428 }
1429 let mean = err / count as f64;
1430 assert!(mean < 0.05, "round trip lost {mean} per voxel on a smooth field");
1431 }
1432
1433 #[test]
1434 fn round_trip_puts_the_data_back_on_the_original_grid() {
1435 let (nx, ny, nz) = (16, 16, 8);
1436 let a = ukb_swi_affine();
1437 let grid = axial_grid_for(nx, ny, nz, &a);
1438 assert_ne!(grid.dims, (nx, ny, nz), "the axial grid should differ, else this proves nothing");
1439 let there = vec![1.0f64; grid.dims.0 * grid.dims.1 * grid.dims.2];
1440 let back = resample_onto(&there, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
1441 assert_eq!(back.len(), nx * ny * nz, "output must be on the acquired grid");
1442 assert!(back.iter().filter(|v| **v > 0.5).count() > (nx * ny * nz) * 9 / 10);
1444 }
1445
1446 #[test]
1447 fn identity_map_is_a_no_op() {
1448 let (nx, ny, nz) = (6, 5, 4);
1449 let a = identity_affine((1.0, 2.0, 3.0));
1450 let data: Vec<f64> = (0..nx * ny * nz).map(|i| i as f64).collect();
1451 let out = resample_onto(&data, (nx, ny, nz), &a, (nx, ny, nz), &a).unwrap();
1452 for (got, want) in out.iter().zip(data.iter()) {
1453 assert!((got - want).abs() < 1e-9, "{got} vs {want}");
1454 }
1455 }
1456
1457 #[test]
1458 fn singular_affine_is_reported_not_panicked() {
1459 let zero = [0.0f64; 16];
1460 let a = identity_affine((1.0, 1.0, 1.0));
1461 assert!(resample_onto(&[0.0; 8], (2, 2, 2), &zero, (2, 2, 2), &a).is_none());
1462 assert!(GridMap::new((2, 2, 2), &zero, (2, 2, 2), &a).is_none());
1463 }
1464
1465 #[test]
1466 fn mask_round_trip_stays_binary() {
1467 let (nx, ny, nz) = (16, 16, 8);
1468 let a = ukb_swi_affine();
1469 let mut mask = vec![0u8; nx * ny * nz];
1470 for z in 2..nz - 2 {
1471 for y in 4..ny - 4 {
1472 for x in 4..nx - 4 {
1473 mask[x + y * nx + z * nx * ny] = 1;
1474 }
1475 }
1476 }
1477 let grid = axial_grid_for(nx, ny, nz, &a);
1478 let there = resample_mask_to_axial(&mask, nx, ny, nz, &a);
1479 let back = resample_mask_onto(&there, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
1480 assert!(back.iter().all(|v| *v == 0 || *v == 1));
1481 assert!(back.iter().filter(|v| **v == 1).count() > 100, "mask should survive the trip");
1482 }
1483
1484 #[test]
1487 fn complex_round_trip_preserves_wraps() {
1488 let (nx, ny, nz) = (20, 20, 8);
1489 let n = nx * ny * nz;
1490 let a = ukb_swi_affine();
1491 let mag = vec![100.0f64; n];
1492 let pha: Vec<f64> = (0..n)
1493 .map(|i| {
1494 let (x, y) = (i % nx, (i / nx) % ny);
1495 wrap(0.6 * x as f64 + 0.2 * y as f64)
1496 })
1497 .collect();
1498
1499 let grid = axial_grid_for(nx, ny, nz, &a);
1500 let out = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams { noise_fill_fraction: None });
1501 let (_, back) = resample_complex_onto(&out.magnitude, &out.phase, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
1502
1503 let (mut err, mut count) = (0.0f64, 0usize);
1504 for z in 2..nz - 2 {
1505 for y in 3..ny - 3 {
1506 for x in 3..nx - 3 {
1507 let i = x + y * nx + z * nx * ny;
1508 err += wrap(back[i] - pha[i]).abs();
1509 count += 1;
1510 }
1511 }
1512 }
1513 let mean = err / count as f64;
1514 assert!(mean < 0.25, "complex round trip drifted {mean} rad per voxel");
1515 }
1516
1517 #[test]
1518 fn noise_fill_is_deterministic_and_bounded() {
1519 let (nx, ny, nz) = (16, 16, 6);
1520 let n = nx * ny * nz;
1521 let (c, s) = (25.0f64.to_radians().cos(), 25.0f64.to_radians().sin());
1522 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
1523 let mag = vec![50.0; n];
1524 let pha: Vec<f64> = (0..n).map(|i| wrap(0.4 * (i % nx) as f64)).collect();
1525
1526 let p = AxialResampleParams { noise_fill_fraction: Some(QSMXT8_NOISE_FILL_FRACTION) };
1527 let a1 = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &p);
1528 let a2 = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &p);
1529 assert_eq!(a1.phase, a2.phase, "same input must give the same volume");
1530 assert!(a1.phase.iter().all(|v| v.is_finite() && v.abs() <= PI + 1e-9));
1531
1532 let off = resample_complex_to_axial(&mag, &pha, nx, ny, nz, &a, &AxialResampleParams::default());
1534 assert!(off.phase.contains(&0.0));
1535 assert!(off.phase.iter().filter(|v| **v == 0.0).count() > a1.phase.iter().filter(|v| **v == 0.0).count());
1536 }
1537
1538 #[test]
1540 fn mask_resampling_on_an_axial_grid_is_a_no_op() {
1541 let mask = vec![0u8, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0];
1542 for vs in [(1.0, 1.0, 1.0), (0.8, 0.8, 2.0)] {
1543 assert_eq!(resample_mask_to_axial(&mask, 3, 2, 2, &identity_affine(vs)), mask, "{vs:?}");
1544 }
1545 }
1546
1547 #[test]
1548 fn mask_resampling_stays_binary() {
1549 let (nx, ny, nz) = (12, 12, 6);
1550 let (c, s) = (15.0f64.to_radians().cos(), 15.0f64.to_radians().sin());
1551 let a = affine_from([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], [0.0; 3]);
1552 let mut mask = vec![0u8; nx * ny * nz];
1553 for k in 1..nz - 1 {
1554 for j in 3..ny - 3 {
1555 for i in 3..nx - 3 {
1556 mask[i + j * nx + k * nx * ny] = 1;
1557 }
1558 }
1559 }
1560 let out = resample_mask_to_axial(&mask, nx, ny, nz, &a);
1561 assert!(out.iter().all(|v| *v == 0 || *v == 1), "mask must stay binary");
1562 assert!(out.iter().filter(|v| **v == 1).count() > 100, "mask should survive");
1563 }
1564
1565 #[test]
1568 fn label_resampling_only_produces_input_labels() {
1569 let (nx, ny, nz) = (16, 16, 8);
1570 let a = ukb_swi_affine();
1571 let ids = [0.0f64, 2.0, 3.0, 41.0, 42.0, 2035.0];
1572 let labels: Vec<f64> = (0..nx * ny * nz)
1573 .map(|i| {
1574 let (x, y, z) = (i % nx, (i / nx) % ny, i / (nx * ny));
1575 if !(2..nx - 2).contains(&x) || !(2..ny - 2).contains(&y) || !(1..nz - 1).contains(&z) {
1576 0.0
1577 } else {
1578 ids[1 + (x / 3 + y / 5 + z) % (ids.len() - 1)]
1579 }
1580 })
1581 .collect();
1582 let grid = axial_grid_for(nx, ny, nz, &a);
1583 let there = resample_labels_onto(&labels, (nx, ny, nz), &a, grid.dims, &grid.affine).unwrap();
1584 let back = resample_labels_onto(&there, grid.dims, &grid.affine, (nx, ny, nz), &a).unwrap();
1585 for v in there.iter().chain(&back) {
1586 assert!(ids.contains(v), "{v} is not one of the input labels");
1587 }
1588 for id in &ids[1..] {
1590 assert!(back.contains(id), "label {id} was lost");
1591 }
1592 let same = back.iter().zip(&labels).filter(|(a, b)| a == b).count();
1594 assert!(same > nx * ny * nz * 3 / 4, "only {same} of {} voxels kept their label", nx * ny * nz);
1595
1596 let smeared = resample_onto(&labels, (nx, ny, nz), &a, grid.dims, &grid.affine).unwrap();
1598 assert!(smeared.iter().any(|v| !ids.contains(v)), "test volume too easy");
1599
1600 let labels_u16: Vec<u16> = labels.iter().map(|&v| v as u16).collect();
1602 let there_u16 = resample_labels_onto(&labels_u16, (nx, ny, nz), &a, grid.dims, &grid.affine).unwrap();
1603 assert!(there_u16.iter().zip(&there).all(|(&a, &b)| a as f64 == b));
1604 }
1605
1606 #[test]
1607 fn label_resampling_on_the_identity_is_a_no_op() {
1608 let (nx, ny, nz) = (6, 5, 4);
1609 let a = identity_affine((1.0, 2.0, 3.0));
1610 let labels: Vec<u32> = (0..nx * ny * nz).map(|i| (i % 7) as u32 * 100).collect();
1611 assert_eq!(resample_labels_onto(&labels, (nx, ny, nz), &a, (nx, ny, nz), &a).unwrap(), labels);
1612 }
1613
1614 #[test]
1617 fn label_resampling_takes_the_majority_not_the_nearest() {
1618 let src = identity_affine((1.0, 1.0, 1.0));
1619 let mut labels = vec![1u8; 8];
1621 labels[7] = 2; labels[6] = 2; let dst = affine_from([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], [0.5, 0.5, 0.5]);
1624 let nearest = resample_mask_onto(&labels, (2, 2, 2), &src, (1, 1, 1), &dst).unwrap();
1625 assert_eq!(nearest, vec![2]);
1626 assert_eq!(resample_labels_onto(&labels, (2, 2, 2), &src, (1, 1, 1), &dst).unwrap(), vec![1]);
1627 let mut tie = vec![1u8; 8];
1629 for i in [4, 5, 6, 7] {
1630 tie[i] = 2;
1631 }
1632 assert_eq!(resample_labels_onto(&tie, (2, 2, 2), &src, (1, 1, 1), &dst).unwrap(), vec![2]);
1633 }
1634
1635 #[test]
1636 fn label_resampling_reports_bad_input() {
1637 let a = identity_affine((1.0, 1.0, 1.0));
1638 assert!(resample_labels_onto(&[0u8; 7], (2, 2, 2), &a, (2, 2, 2), &a).is_err());
1639 assert!(resample_labels_onto(&[0u8; 8], (2, 2, 2), &[0.0; 16], (2, 2, 2), &a).is_err());
1640 }
1641}