1use crate::geometry::{
74 b0_direction_from_affine, resample_complex_onto, resample_mask_onto, resample_onto,
75 trilinear_sample, voxel_sizes_from_affine, world_to_voxel,
76};
77
78#[cfg(feature = "parallel")]
79use rayon::prelude::*;
80
81const SAMPLE_CHUNK: usize = 8192;
84
85#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
87#[derive(Clone, Debug)]
88pub struct RigidParams {
89 pub levels: usize,
113 pub max_samples: usize,
119 pub max_iterations: usize,
121 pub max_evaluations: usize,
124 pub tolerance: f64,
126 pub rotation_step_deg: f64,
129 pub translation_step_mm: f64,
132 pub min_overlap: f64,
139 pub initial: Option<[f64; 6]>,
149}
150
151impl Default for RigidParams {
152 fn default() -> Self {
153 Self {
154 levels: 5,
155 max_samples: 200_000,
156 max_iterations: 20,
157 max_evaluations: 5000,
158 tolerance: 1e-5,
159 rotation_step_deg: 3.0,
160 translation_step_mm: 3.0,
161 min_overlap: 0.25,
162 initial: None,
163 }
164 }
165}
166
167#[derive(Clone, Debug)]
177pub struct RigidTransform {
178 pub matrix: [f64; 16],
181 pub params: [f64; 6],
185 pub fixed_dims: (usize, usize, usize),
187 pub fixed_affine: [f64; 16],
188 pub moving_dims: (usize, usize, usize),
190 pub moving_affine: [f64; 16],
191 pub ncc: f64,
194 pub overlap: f64,
198 pub evaluations: usize,
200}
201
202impl RigidTransform {
203 pub fn identity(
207 fixed_dims: (usize, usize, usize),
208 fixed_affine: &[f64; 16],
209 moving_dims: (usize, usize, usize),
210 moving_affine: &[f64; 16],
211 ) -> Self {
212 Self {
213 matrix: IDENTITY4,
214 params: [0.0; 6],
215 fixed_dims,
216 fixed_affine: *fixed_affine,
217 moving_dims,
218 moving_affine: *moving_affine,
219 ncc: f64::NAN,
220 overlap: f64::NAN,
221 evaluations: 0,
222 }
223 }
224
225 pub fn rotation_degrees(&self) -> [f64; 3] {
227 [
228 self.params[0].to_degrees(),
229 self.params[1].to_degrees(),
230 self.params[2].to_degrees(),
231 ]
232 }
233
234 pub fn translation_mm(&self) -> [f64; 3] {
236 [self.params[3], self.params[4], self.params[5]]
237 }
238
239 pub fn rotation_magnitude_deg(&self) -> f64 {
242 let r = rotation_of(&self.matrix);
243 let trace = r[0][0] + r[1][1] + r[2][2];
244 (0.5 * (trace - 1.0)).clamp(-1.0, 1.0).acos().to_degrees()
245 }
246
247 pub fn aligned_affine(&self) -> [f64; 16] {
256 mat4_mul(&rigid_inverse(&self.matrix), &self.moving_affine)
257 }
258
259 pub fn b0_direction_in_fixed(
273 &self,
274 declared_moving_b0: Option<(f64, f64, f64)>,
275 ) -> (f64, f64, f64) {
276 let d = declared_moving_b0.unwrap_or_else(|| b0_direction_from_affine(&self.moving_affine));
277 let d = [d.0, d.1, d.2];
278 let in_moving_world = mat3_vec(&direction_cosines(&self.moving_affine), &d);
280 let in_fixed_world = mat3_transpose_vec(&rotation_of(&self.matrix), &in_moving_world);
282 let v = mat3_transpose_vec(&direction_cosines(&self.fixed_affine), &in_fixed_world);
284 let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
285 if n < 1e-12 {
286 (0.0, 0.0, 1.0)
287 } else {
288 (v[0] / n, v[1] / n, v[2] / n)
289 }
290 }
291
292 pub fn resample(&self, moving: &[f64]) -> Option<Vec<f64>> {
295 resample_onto(
296 moving,
297 self.moving_dims,
298 &self.aligned_affine(),
299 self.fixed_dims,
300 &self.fixed_affine,
301 )
302 }
303
304 pub fn resample_mask(&self, mask: &[u8]) -> Option<Vec<u8>> {
306 resample_mask_onto(
307 mask,
308 self.moving_dims,
309 &self.aligned_affine(),
310 self.fixed_dims,
311 &self.fixed_affine,
312 )
313 }
314
315 pub fn resample_complex(
318 &self,
319 magnitude: &[f64],
320 phase: &[f64],
321 ) -> Option<(Vec<f64>, Vec<f64>)> {
322 resample_complex_onto(
323 magnitude,
324 phase,
325 self.moving_dims,
326 &self.aligned_affine(),
327 self.fixed_dims,
328 &self.fixed_affine,
329 )
330 }
331
332 pub fn inverse(&self) -> Self {
337 let matrix = rigid_inverse(&self.matrix);
338 let center = grid_center_world(self.moving_dims, &self.moving_affine);
339 Self {
340 params: params_from_matrix(&matrix, ¢er),
341 matrix,
342 fixed_dims: self.moving_dims,
343 fixed_affine: self.moving_affine,
344 moving_dims: self.fixed_dims,
345 moving_affine: self.fixed_affine,
346 ncc: self.ncc,
347 overlap: self.overlap,
348 evaluations: self.evaluations,
349 }
350 }
351}
352
353#[allow(clippy::too_many_arguments)] pub fn register_rigid(
381 fixed: &[f64],
382 fixed_dims: (usize, usize, usize),
383 fixed_affine: &[f64; 16],
384 moving: &[f64],
385 moving_dims: (usize, usize, usize),
386 moving_affine: &[f64; 16],
387 mask: Option<&[u8]>,
388 params: &RigidParams,
389) -> Option<RigidTransform> {
390 if fixed.len() != fixed_dims.0 * fixed_dims.1 * fixed_dims.2
391 || moving.len() != moving_dims.0 * moving_dims.1 * moving_dims.2
392 {
393 return None;
394 }
395 if let Some(m) = mask {
396 if m.len() != fixed.len() {
397 return None;
398 }
399 }
400 world_to_voxel(fixed_affine)?;
403 world_to_voxel(moving_affine)?;
404
405 let center = grid_center_world(fixed_dims, fixed_affine);
411
412 const MIN_AXIS: usize = 8;
416 let too_small = |d: (usize, usize, usize)| {
417 d.0 < 2 * MIN_AXIS || d.1 < 2 * MIN_AXIS || d.2 < 2 * MIN_AXIS
418 };
419 let mut fixed_levels = vec![Volume::borrowed(fixed, fixed_dims, *fixed_affine)];
420 let mut moving_levels = vec![Volume::borrowed(moving, moving_dims, *moving_affine)];
421 let mut mask_levels: Vec<Option<Vec<u8>>> = vec![None];
423 for level in 1..params.levels.max(1) {
424 let prev = level - 1;
425 if too_small(fixed_levels[prev].dims) || too_small(moving_levels[prev].dims) {
426 break;
427 }
428 let prev_dims = fixed_levels[prev].dims;
429 let next_mask = match (prev, mask) {
430 (0, Some(m)) => Some(decimate_mask(m, prev_dims)),
431 (0, None) => None,
432 (_, _) => mask_levels[prev].as_ref().map(|m| decimate_mask(m, prev_dims)),
433 };
434 let next_fixed = fixed_levels[prev].halved();
435 let next_moving = moving_levels[prev].halved();
436 fixed_levels.push(next_fixed);
437 moving_levels.push(next_moving);
438 mask_levels.push(next_mask);
439 }
440
441 let samples: Vec<Vec<Sample>> = (0..fixed_levels.len())
444 .map(|level| {
445 let m = if level == 0 { mask } else { mask_levels[level].as_deref() };
446 sample_set(&fixed_levels[level], m, params.max_samples)
447 })
448 .collect();
449
450 let mut p = params.initial.unwrap_or([0.0; 6]);
451 let mut evaluations = 0usize;
452 let mut best: Option<(f64, f64)> = None;
453
454 for level in (0..fixed_levels.len()).rev() {
455 if samples[level].is_empty() {
456 continue;
457 }
458 let factor = (1usize << level) as f64;
459 let mv = &moving_levels[level];
460 let mut cost = CostFn {
461 samples: &samples[level],
462 fixed_affine: fixed_levels[level].affine,
463 moving: mv.data(),
464 moving_dims: mv.dims,
465 inv_moving: to_mat4(&world_to_voxel(&mv.affine)?),
466 center,
467 min_overlap: params.min_overlap,
468 evaluations: 0,
469 budget: params.max_evaluations.saturating_sub(evaluations),
470 };
471 let rot = (params.rotation_step_deg * factor).to_radians();
474 let trans = params.translation_step_mm * factor;
475 let scale = [rot, rot, rot, trans, trans, trans];
476 p = powell(&mut cost, p, scale, params.max_iterations, params.tolerance);
477 evaluations += cost.evaluations;
478 best = Some(cost.score(&p));
479 if evaluations >= params.max_evaluations {
480 break;
481 }
482 }
483
484 let (ncc, overlap) = best?;
485 Some(RigidTransform {
486 matrix: matrix_from_params(&p, ¢er),
487 params: p,
488 fixed_dims,
489 fixed_affine: *fixed_affine,
490 moving_dims,
491 moving_affine: *moving_affine,
492 ncc,
493 overlap,
494 evaluations,
495 })
496}
497
498struct Volume<'a> {
502 owned: Option<Vec<f64>>,
503 view: &'a [f64],
504 dims: (usize, usize, usize),
505 affine: [f64; 16],
506}
507
508impl<'a> Volume<'a> {
509 fn borrowed(data: &'a [f64], dims: (usize, usize, usize), affine: [f64; 16]) -> Self {
510 Self { owned: None, view: data, dims, affine }
511 }
512
513 fn data(&self) -> &[f64] {
514 self.owned.as_deref().unwrap_or(self.view)
515 }
516
517 fn halved(&self) -> Volume<'static> {
526 let (nx, ny, nz) = self.dims;
527 let src = self.data();
528 let blurred = binomial_blur3(src, self.dims);
529 let (cx, cy, cz) = (nx.div_ceil(2), ny.div_ceil(2), nz.div_ceil(2));
530 let mut out = Vec::with_capacity(cx * cy * cz);
531 for k in 0..cz {
532 for j in 0..cy {
533 for i in 0..cx {
534 out.push(blurred[2 * i + 2 * j * nx + 2 * k * nx * ny]);
535 }
536 }
537 }
538 let mut affine = self.affine;
539 for row in 0..3 {
540 for col in 0..3 {
541 affine[4 * row + col] *= 2.0;
542 }
543 }
544 Volume { owned: Some(out), view: &[], dims: (cx, cy, cz), affine }
545 }
546}
547
548fn binomial_blur3(data: &[f64], dims: (usize, usize, usize)) -> Vec<f64> {
550 const W: [f64; 5] = [1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, 1.0 / 16.0];
551 let (nx, ny, nz) = dims;
552 let mut a = data.to_vec();
553 let mut b = vec![0.0f64; a.len()];
554 for axis in 0..3 {
557 let (n, stride) = match axis {
558 0 => (nx, 1usize),
559 1 => (ny, nx),
560 _ => (nz, nx * ny),
561 };
562 if n < 2 {
563 continue;
564 }
565 let outer = a.len() / n;
566 for o in 0..outer {
567 let base = match axis {
568 0 => o * nx,
569 1 => (o % nx) + (o / nx) * nx * ny,
570 _ => o,
571 };
572 for t in 0..n {
573 let mut acc = 0.0;
574 for (w, dt) in W.iter().zip(-2i64..=2) {
575 let u = (t as i64 + dt).clamp(0, n as i64 - 1) as usize;
576 acc += w * a[base + u * stride];
577 }
578 b[base + t * stride] = acc;
579 }
580 }
581 std::mem::swap(&mut a, &mut b);
582 }
583 a
584}
585
586fn decimate_mask(mask: &[u8], dims: (usize, usize, usize)) -> Vec<u8> {
589 let (nx, ny, nz) = dims;
590 let (cx, cy, cz) = (nx.div_ceil(2), ny.div_ceil(2), nz.div_ceil(2));
591 let mut out = Vec::with_capacity(cx * cy * cz);
592 for k in 0..cz {
593 for j in 0..cy {
594 for i in 0..cx {
595 out.push(mask[2 * i + 2 * j * nx + 2 * k * nx * ny]);
596 }
597 }
598 }
599 out
600}
601
602#[derive(Clone, Copy)]
610struct Sample {
611 pos: [f32; 3],
612 value: f32,
613}
614
615fn sample_set(fixed: &Volume, mask: Option<&[u8]>, max_samples: usize) -> Vec<Sample> {
622 let (nx, ny, nz) = fixed.dims;
623 let data = fixed.data();
624 let eligible = |idx: usize| match mask {
625 Some(m) => m[idx] != 0,
626 None => true,
627 };
628
629 let count_at = |s: usize| {
630 let mut n = 0usize;
631 for k in (0..nz).step_by(s) {
632 for j in (0..ny).step_by(s) {
633 for i in (0..nx).step_by(s) {
634 if eligible(i + j * nx + k * nx * ny) {
635 n += 1;
636 }
637 }
638 }
639 }
640 n
641 };
642
643 let cap = max_samples.max(64);
644 let mut stride = 1usize;
645 while stride < nx.max(ny).max(nz) && count_at(stride) > cap {
646 stride += 1;
647 }
648
649 let mut out = Vec::new();
650 for k in (0..nz).step_by(stride) {
651 for j in (0..ny).step_by(stride) {
652 for i in (0..nx).step_by(stride) {
653 let idx = i + j * nx + k * nx * ny;
654 if eligible(idx) {
655 out.push(Sample {
656 pos: [i as f32, j as f32, k as f32],
657 value: data[idx] as f32,
658 });
659 }
660 }
661 }
662 }
663 out
664}
665
666#[derive(Clone, Copy, Default)]
668struct Acc {
669 n: usize,
670 sf: f64,
671 sm: f64,
672 sff: f64,
673 smm: f64,
674 sfm: f64,
675}
676
677struct CostFn<'a> {
679 samples: &'a [Sample],
680 fixed_affine: [f64; 16],
681 moving: &'a [f64],
682 moving_dims: (usize, usize, usize),
683 inv_moving: [f64; 16],
685 center: [f64; 3],
686 min_overlap: f64,
687 evaluations: usize,
688 budget: usize,
689}
690
691const COST_DEGENERATE: f64 = 4.0;
695const COST_NO_OVERLAP: f64 = 3.0;
696
697impl CostFn<'_> {
698 fn score(&self, p: &[f64; 6]) -> (f64, f64) {
702 let b = mat4_mul(
703 &self.inv_moving,
704 &mat4_mul(&matrix_from_params(p, &self.center), &self.fixed_affine),
705 );
706 let (nx, ny, nz) = self.moving_dims;
707 let moving = self.moving;
708
709 let partials: Vec<Acc> = maybe_par_chunks!(self.samples, SAMPLE_CHUNK)
710 .map(|chunk| {
711 let mut a = Acc::default();
712 for s in chunk {
713 let (x, y, z) = (s.pos[0] as f64, s.pos[1] as f64, s.pos[2] as f64);
714 let ox = b[0] * x + b[1] * y + b[2] * z + b[3];
715 let oy = b[4] * x + b[5] * y + b[6] * z + b[7];
716 let oz = b[8] * x + b[9] * y + b[10] * z + b[11];
717 if ox < -0.5
720 || ox > nx as f64 - 0.5
721 || oy < -0.5
722 || oy > ny as f64 - 0.5
723 || oz < -0.5
724 || oz > nz as f64 - 0.5
725 {
726 continue;
727 }
728 let f = s.value as f64;
729 let m = trilinear_sample(moving, nx, ny, nz, ox, oy, oz);
730 a.n += 1;
731 a.sf += f;
732 a.sm += m;
733 a.sff += f * f;
734 a.smm += m * m;
735 a.sfm += f * m;
736 }
737 a
738 })
739 .collect();
740
741 let mut t = Acc::default();
742 for a in &partials {
743 t.n += a.n;
744 t.sf += a.sf;
745 t.sm += a.sm;
746 t.sff += a.sff;
747 t.smm += a.smm;
748 t.sfm += a.sfm;
749 }
750
751 let overlap = t.n as f64 / self.samples.len().max(1) as f64;
752 if t.n < 2 {
753 return (f64::NAN, overlap);
754 }
755 let n = t.n as f64;
756 let vf = t.sff - t.sf * t.sf / n;
760 let vm = t.smm - t.sm * t.sm / n;
761 if vf <= 0.0 || vm <= 0.0 {
762 return (f64::NAN, overlap);
763 }
764 let cov = t.sfm - t.sf * t.sm / n;
765 (cov / (vf * vm).sqrt(), overlap)
766 }
767
768 fn cost(&mut self, p: &[f64; 6]) -> f64 {
769 if self.evaluations >= self.budget {
770 return COST_DEGENERATE;
773 }
774 self.evaluations += 1;
775 let (ncc, overlap) = self.score(p);
776 if overlap < self.min_overlap {
777 return COST_NO_OVERLAP + (self.min_overlap - overlap);
778 }
779 if !ncc.is_finite() {
780 return COST_DEGENERATE;
781 }
782 1.0 - ncc
783 }
784}
785
786fn powell(
796 cost: &mut CostFn,
797 start: [f64; 6],
798 scale: [f64; 6],
799 max_cycles: usize,
800 tol: f64,
801) -> [f64; 6] {
802 const N: usize = 6;
803 let mut dirs = [[0.0f64; N]; N];
804 for (i, d) in dirs.iter_mut().enumerate() {
805 d[i] = scale[i];
806 }
807 let mut p = start;
808 let mut fp = cost.cost(&p);
809
810 for _ in 0..max_cycles {
811 let p_start = p;
812 let f_start = fp;
813 let mut biggest_drop = 0.0f64;
814 let mut biggest = 0usize;
815
816 for (i, dir) in dirs.iter().enumerate() {
817 let before = fp;
818 let (np, nf) = line_minimise(cost, &p, dir);
819 p = np;
820 fp = nf;
821 if before - fp > biggest_drop {
822 biggest_drop = before - fp;
823 biggest = i;
824 }
825 }
826
827 if 2.0 * (f_start - fp) <= tol * (f_start.abs() + fp.abs()) + 1e-12 {
828 break;
829 }
830
831 let mut net = [0.0f64; N];
833 let mut extrap = p;
834 for i in 0..N {
835 net[i] = p[i] - p_start[i];
836 extrap[i] = p[i] + net[i];
837 }
838 let f_extrap = cost.cost(&extrap);
839 if f_extrap < f_start {
840 let t = 2.0 * (f_start - 2.0 * fp + f_extrap) * (f_start - fp - biggest_drop).powi(2)
844 - biggest_drop * (f_start - f_extrap).powi(2);
845 if t < 0.0 {
846 let (np, nf) = line_minimise(cost, &p, &net);
847 p = np;
848 fp = nf;
849 dirs[biggest] = dirs[N - 1];
850 dirs[N - 1] = net;
851 }
852 }
853 }
854 p
855}
856
857fn line_minimise(cost: &mut CostFn, p: &[f64; 6], dir: &[f64; 6]) -> ([f64; 6], f64) {
859 let at = |x: f64| {
860 let mut q = *p;
861 for i in 0..6 {
862 q[i] += x * dir[i];
863 }
864 q
865 };
866 let f = |x: f64, c: &mut CostFn| c.cost(&at(x));
867
868 #[allow(clippy::approx_constant)]
876 const GOLD: f64 = 1.618_033_988_749_895;
877 const MAX_EXPANSIONS: usize = 24;
878 let (mut ax, mut bx) = (0.0f64, 1.0f64);
879 let f_at_zero = f(ax, cost);
880 let f_at_one = f(bx, cost);
881 let mut fb = if f_at_one > f_at_zero {
883 std::mem::swap(&mut ax, &mut bx);
884 f_at_zero
885 } else {
886 f_at_one
887 };
888 let mut cx = bx + GOLD * (bx - ax);
889 let mut fc = f(cx, cost);
890 let mut expansions = 0;
891 while fc < fb && expansions < MAX_EXPANSIONS {
892 ax = bx;
893 bx = cx;
894 fb = fc;
895 cx = bx + GOLD * (bx - ax);
896 fc = f(cx, cost);
897 expansions += 1;
898 }
899
900 const ITERS: usize = 40;
902 const ZEPS: f64 = 1e-3;
906 const TOL: f64 = 1e-3;
907 let (mut a, mut b) = (ax.min(cx), ax.max(cx));
908 let (mut x, mut w, mut v) = (bx, bx, bx);
909 let (mut fx, mut fw, mut fv) = (fb, fb, fb);
910 let mut d = 0.0f64;
911 let mut e = 0.0f64;
912 for _ in 0..ITERS {
913 let xm = 0.5 * (a + b);
914 let tol1 = TOL * x.abs() + ZEPS;
915 if (x - xm).abs() <= 2.0 * tol1 - 0.5 * (b - a) {
916 break;
917 }
918 let mut use_golden = true;
919 if e.abs() > tol1 {
920 let r = (x - w) * (fx - fv);
922 let q = (x - v) * (fx - fw);
923 let mut pnum = (x - v) * q - (x - w) * r;
924 let mut qden = 2.0 * (q - r);
925 if qden > 0.0 {
926 pnum = -pnum;
927 } else {
928 qden = -qden;
929 }
930 let etemp = e;
931 e = d;
932 if qden.abs() > 1e-300
933 && pnum.abs() < (0.5 * qden * etemp).abs()
934 && pnum > qden * (a - x)
935 && pnum < qden * (b - x)
936 {
937 d = pnum / qden;
938 let u = x + d;
939 if (u - a) < 2.0 * tol1 || (b - u) < 2.0 * tol1 {
940 d = if xm - x >= 0.0 { tol1 } else { -tol1 };
941 }
942 use_golden = false;
943 }
944 }
945 if use_golden {
946 e = if x >= xm { a - x } else { b - x };
947 d = 0.381_966_011_250_105 * e;
948 }
949 let u = if d.abs() >= tol1 {
950 x + d
951 } else {
952 x + if d >= 0.0 { tol1 } else { -tol1 }
953 };
954 let fu = f(u, cost);
955 if fu <= fx {
956 if u >= x {
957 a = x;
958 } else {
959 b = x;
960 }
961 v = w;
962 fv = fw;
963 w = x;
964 fw = fx;
965 x = u;
966 fx = fu;
967 } else {
968 if u < x {
969 a = u;
970 } else {
971 b = u;
972 }
973 if fu <= fw || w == x {
974 v = w;
975 fv = fw;
976 w = u;
977 fw = fu;
978 } else if fu <= fv || v == x || v == w {
979 v = u;
980 fv = fu;
981 }
982 }
983 }
984 (at(x), fx)
985}
986
987const IDENTITY4: [f64; 16] = [
994 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
998];
999
1000fn mat4_mul(a: &[f64; 16], b: &[f64; 16]) -> [f64; 16] {
1001 let mut out = [0.0f64; 16];
1002 for i in 0..4 {
1003 for j in 0..4 {
1004 out[4 * i + j] = (0..4).map(|k| a[4 * i + k] * b[4 * k + j]).sum();
1005 }
1006 }
1007 out
1008}
1009
1010fn to_mat4(m: &[[f64; 4]; 3]) -> [f64; 16] {
1011 let mut out = IDENTITY4;
1012 for i in 0..3 {
1013 out[4 * i..4 * i + 4].copy_from_slice(&m[i]);
1014 }
1015 out
1016}
1017
1018fn rotation_of(m: &[f64; 16]) -> [[f64; 3]; 3] {
1020 [
1021 [m[0], m[1], m[2]],
1022 [m[4], m[5], m[6]],
1023 [m[8], m[9], m[10]],
1024 ]
1025}
1026
1027fn rigid_inverse(m: &[f64; 16]) -> [f64; 16] {
1029 let r = rotation_of(m);
1030 let t = [m[3], m[7], m[11]];
1031 let mut out = IDENTITY4;
1032 for i in 0..3 {
1033 for j in 0..3 {
1034 out[4 * i + j] = r[j][i];
1035 }
1036 out[4 * i + 3] = -(r[0][i] * t[0] + r[1][i] * t[1] + r[2][i] * t[2]);
1037 }
1038 out
1039}
1040
1041fn mat3_vec(m: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
1042 [
1043 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
1044 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
1045 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
1046 ]
1047}
1048
1049fn mat3_transpose_vec(m: &[[f64; 3]; 3], v: &[f64; 3]) -> [f64; 3] {
1050 [
1051 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2],
1052 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2],
1053 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2],
1054 ]
1055}
1056
1057fn direction_cosines(affine: &[f64; 16]) -> [[f64; 3]; 3] {
1063 let (sx, sy, sz) = voxel_sizes_from_affine(affine);
1064 let s = [sx.max(1e-12), sy.max(1e-12), sz.max(1e-12)];
1065 let mut out = [[0.0f64; 3]; 3];
1066 for i in 0..3 {
1067 for j in 0..3 {
1068 out[i][j] = affine[4 * i + j] / s[j];
1069 }
1070 }
1071 out
1072}
1073
1074fn grid_center_world(dims: (usize, usize, usize), affine: &[f64; 16]) -> [f64; 3] {
1076 let c = [
1077 (dims.0 as f64 - 1.0) * 0.5,
1078 (dims.1 as f64 - 1.0) * 0.5,
1079 (dims.2 as f64 - 1.0) * 0.5,
1080 ];
1081 [
1082 affine[0] * c[0] + affine[1] * c[1] + affine[2] * c[2] + affine[3],
1083 affine[4] * c[0] + affine[5] * c[1] + affine[6] * c[2] + affine[7],
1084 affine[8] * c[0] + affine[9] * c[1] + affine[10] * c[2] + affine[11],
1085 ]
1086}
1087
1088fn euler_rotation(rx: f64, ry: f64, rz: f64) -> [[f64; 3]; 3] {
1090 let (sa, ca) = rx.sin_cos();
1091 let (sb, cb) = ry.sin_cos();
1092 let (sc, cc) = rz.sin_cos();
1093 [
1094 [cc * cb, cc * sb * sa - sc * ca, cc * sb * ca + sc * sa],
1095 [sc * cb, sc * sb * sa + cc * ca, sc * sb * ca - cc * sa],
1096 [-sb, cb * sa, cb * ca],
1097 ]
1098}
1099
1100fn matrix_from_params(p: &[f64; 6], center: &[f64; 3]) -> [f64; 16] {
1102 let r = euler_rotation(p[0], p[1], p[2]);
1103 let rc = mat3_vec(&r, center);
1104 let mut out = IDENTITY4;
1105 for i in 0..3 {
1106 out[4 * i..4 * i + 3].copy_from_slice(&r[i]);
1107 out[4 * i + 3] = center[i] - rc[i] + p[3 + i];
1108 }
1109 out
1110}
1111
1112fn params_from_matrix(m: &[f64; 16], center: &[f64; 3]) -> [f64; 6] {
1115 let r = rotation_of(m);
1116 let ry = (-r[2][0]).clamp(-1.0, 1.0).asin();
1119 let (rx, rz) = if r[2][0].abs() < 1.0 - 1e-9 {
1120 (r[2][1].atan2(r[2][2]), r[1][0].atan2(r[0][0]))
1121 } else {
1122 (0.0, (-r[0][1]).atan2(r[1][1]))
1124 };
1125 let rr = euler_rotation(rx, ry, rz);
1126 let rc = mat3_vec(&rr, center);
1127 [
1128 rx,
1129 ry,
1130 rz,
1131 m[3] - (center[0] - rc[0]),
1132 m[7] - (center[1] - rc[1]),
1133 m[11] - (center[2] - rc[2]),
1134 ]
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140 use crate::geometry::resample_onto;
1141
1142 fn identity_affine(vs: (f64, f64, f64)) -> [f64; 16] {
1143 [
1144 vs.0, 0.0, 0.0, 0.0, 0.0, vs.1, 0.0, 0.0, 0.0, 0.0, vs.2, 0.0, 0.0, 0.0, 0.0, 1.0,
1148 ]
1149 }
1150
1151 fn truth_transform(center: &[f64; 3], deg: [f64; 3], mm: [f64; 3]) -> [f64; 16] {
1160 let shift = |t: [f64; 3]| {
1161 [
1162 1.0, 0.0, 0.0, t[0], 0.0, 1.0, 0.0, t[1], 0.0, 0.0, 1.0, t[2], 0.0, 0.0, 0.0, 1.0,
1166 ]
1167 };
1168 let (sa, ca) = deg[0].to_radians().sin_cos();
1169 let (sb, cb) = deg[1].to_radians().sin_cos();
1170 let (sc, cc) = deg[2].to_radians().sin_cos();
1171 let rx = [
1172 1.0, 0.0, 0.0, 0.0, 0.0, ca, -sa, 0.0, 0.0, sa, ca, 0.0, 0.0, 0.0, 0.0, 1.0,
1176 ];
1177 let ry = [
1178 cb, 0.0, sb, 0.0, 0.0, 1.0, 0.0, 0.0, -sb, 0.0, cb, 0.0, 0.0, 0.0, 0.0, 1.0,
1182 ];
1183 let rz = [
1184 cc, -sc, 0.0, 0.0, sc, cc, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
1188 ];
1189 let c = *center;
1190 let to_origin = shift([-c[0], -c[1], -c[2]]);
1191 let back = shift([c[0] + mm[0], c[1] + mm[1], c[2] + mm[2]]);
1192 mat4_mul(&back, &mat4_mul(&rz, &mat4_mul(&ry, &mat4_mul(&rx, &to_origin))))
1193 }
1194
1195 fn phantom(dims: (usize, usize, usize)) -> Vec<f64> {
1199 let (nx, ny, nz) = dims;
1200 let mut v = vec![0.0f64; nx * ny * nz];
1201 let blobs = [
1202 (0.50, 0.50, 0.50, 0.34, 0.26, 0.30, 1.0),
1204 (0.36, 0.44, 0.52, 0.10, 0.09, 0.11, 2.2),
1205 (0.63, 0.57, 0.44, 0.08, 0.13, 0.07, 1.7),
1206 (0.50, 0.62, 0.64, 0.07, 0.06, 0.09, 2.8),
1207 ];
1208 for k in 0..nz {
1209 for j in 0..ny {
1210 for i in 0..nx {
1211 let (x, y, z) = (
1212 i as f64 / nx as f64,
1213 j as f64 / ny as f64,
1214 k as f64 / nz as f64,
1215 );
1216 let mut acc = 0.0;
1217 for &(cx, cy, cz, rx, ry, rz, val) in &blobs {
1218 let d = ((x - cx) / rx).powi(2) + ((y - cy) / ry).powi(2) + ((z - cz) / rz).powi(2);
1219 if d <= 1.0 {
1220 acc += val * (1.0 - d).sqrt();
1223 }
1224 }
1225 v[i + j * nx + k * nx * ny] = acc;
1226 }
1227 }
1228 }
1229 v
1230 }
1231
1232 fn warp(
1240 fixed: &[f64],
1241 fixed_dims: (usize, usize, usize),
1242 fixed_affine: &[f64; 16],
1243 moving_dims: (usize, usize, usize),
1244 moving_affine: &[f64; 16],
1245 w: &[f64; 16],
1246 ) -> Vec<f64> {
1247 resample_onto(
1248 fixed,
1249 fixed_dims,
1250 &mat4_mul(w, fixed_affine),
1251 moving_dims,
1252 moving_affine,
1253 )
1254 .unwrap()
1255 }
1256
1257 fn rotation_error_deg(a: &[f64; 16], b: &[f64; 16]) -> f64 {
1260 let (ra, rb) = (rotation_of(a), rotation_of(b));
1261 let trace: f64 = (0..3)
1262 .map(|i| (0..3).map(|j| ra[j][i] * rb[j][i]).sum::<f64>())
1263 .sum();
1264 (0.5 * (trace - 1.0)).clamp(-1.0, 1.0).acos().to_degrees()
1265 }
1266
1267 fn translation_error_mm(a: &[f64; 16], b: &[f64; 16]) -> f64 {
1268 [(a[3] - b[3]), (a[7] - b[7]), (a[11] - b[11])]
1269 .iter()
1270 .fold(0.0f64, |m, v| m.max(v.abs()))
1271 }
1272
1273 #[test]
1277 fn mat4_mul_multiplies_row_major() {
1278 let a = [
1279 1.0, 2.0, 0.0, 3.0, 0.0, 1.0, 4.0, 0.0, 5.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0,
1283 ];
1284 let b = [
1285 0.0, 1.0, 2.0, 1.0, 3.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 2.0, 0.0, 0.0, 0.0, 1.0,
1289 ];
1290 let want = [
1292 6.0, 1.0, 4.0, 4.0, 7.0, 4.0, 1.0, 8.0, 1.0, 6.0, 10.0, 8.0, 0.0, 0.0, 0.0, 1.0,
1296 ];
1297 let got = mat4_mul(&a, &b);
1298 for i in 0..16 {
1299 assert!((got[i] - want[i]).abs() < 1e-12, "element {i}: {} vs {}", got[i], want[i]);
1300 }
1301 }
1302
1303 #[test]
1307 fn the_parameterisation_matches_an_independent_construction() {
1308 let center = [12.5, -3.0, 7.25];
1309 for (deg, mm) in [
1310 ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
1311 ([11.0, -7.0, 23.0], [2.0, -4.0, 6.0]),
1312 ([-30.0, 15.0, -5.0], [-1.0, 0.5, 3.0]),
1313 ] {
1314 let want = truth_transform(¢er, deg, mm);
1315 let p = [
1316 deg[0].to_radians(),
1317 deg[1].to_radians(),
1318 deg[2].to_radians(),
1319 mm[0],
1320 mm[1],
1321 mm[2],
1322 ];
1323 let got = matrix_from_params(&p, ¢er);
1324 for i in 0..16 {
1325 assert!(
1326 (got[i] - want[i]).abs() < 1e-9,
1327 "deg {deg:?} mm {mm:?} element {i}: {} vs {}",
1328 got[i], want[i]
1329 );
1330 }
1331 }
1332 }
1333
1334 #[test]
1335 fn euler_round_trips_through_the_matrix() {
1336 let center = [3.0, -7.0, 11.0];
1337 for p in [
1338 [0.0; 6],
1339 [0.1, -0.2, 0.3, 4.0, -5.0, 6.0],
1340 [-0.4, 0.05, -0.6, -1.0, 2.0, -3.0],
1341 ] {
1342 let m = matrix_from_params(&p, ¢er);
1343 let back = params_from_matrix(&m, ¢er);
1344 for i in 0..6 {
1345 assert!((p[i] - back[i]).abs() < 1e-9, "param {i}: {p:?} vs {back:?}");
1346 }
1347 }
1348 }
1349
1350 #[test]
1351 fn rigid_inverse_is_an_inverse() {
1352 let m = matrix_from_params(&[0.3, -0.2, 0.5, 7.0, -2.0, 4.0], &[1.0, 2.0, 3.0]);
1353 let p = mat4_mul(&m, &rigid_inverse(&m));
1354 for (i, (&got, &want)) in p.iter().zip(IDENTITY4.iter()).enumerate() {
1355 assert!((got - want).abs() < 1e-12, "element {i}: {got} vs {want}");
1356 }
1357 }
1358
1359 #[test]
1362 fn rotation_is_about_the_fixed_volume_centre() {
1363 let affine = identity_affine((1.0, 1.0, 2.0));
1364 let dims = (32, 40, 16);
1365 let c = grid_center_world(dims, &affine);
1366 let m = matrix_from_params(&[0.4, -0.3, 0.2, 0.0, 0.0, 0.0], &c);
1367 let moved = [
1368 m[0] * c[0] + m[1] * c[1] + m[2] * c[2] + m[3],
1369 m[4] * c[0] + m[5] * c[1] + m[6] * c[2] + m[7],
1370 m[8] * c[0] + m[9] * c[1] + m[10] * c[2] + m[11],
1371 ];
1372 for i in 0..3 {
1373 assert!((moved[i] - c[i]).abs() < 1e-9, "axis {i}: {moved:?} vs {c:?}");
1374 }
1375 }
1376
1377 #[test]
1380 fn halved_level_keeps_world_coordinates() {
1381 let dims = (24, 24, 24);
1382 let affine = [
1383 0.8, 0.0, 0.0, -11.0, 0.0, 0.9, 0.0, 5.0, 0.0, 0.0, 3.0, 2.0, 0.0, 0.0, 0.0, 1.0,
1387 ];
1388 let data = phantom(dims);
1389 let coarse = Volume::borrowed(&data, dims, affine).halved();
1390 assert_eq!(coarse.dims, (12, 12, 12));
1391 for (i, j, k) in [(0usize, 0usize, 0usize), (3, 5, 7), (11, 11, 11)] {
1392 let fine = [
1393 affine[0] * (2 * i) as f64 + affine[3],
1394 affine[5] * (2 * j) as f64 + affine[7],
1395 affine[10] * (2 * k) as f64 + affine[11],
1396 ];
1397 let a = coarse.affine;
1398 let got = [
1399 a[0] * i as f64 + a[3],
1400 a[5] * j as f64 + a[7],
1401 a[10] * k as f64 + a[11],
1402 ];
1403 for d in 0..3 {
1404 assert!((fine[d] - got[d]).abs() < 1e-12, "{fine:?} vs {got:?}");
1405 }
1406 }
1407 }
1408
1409 #[test]
1414 fn recovers_a_known_rotation_and_translation() {
1415 let dims = (48, 48, 48);
1416 let affine = identity_affine((1.0, 1.0, 1.0));
1417 let fixed = phantom(dims);
1418 let truth = truth_transform(&grid_center_world(dims, &affine), [9.0, -6.0, 12.0], [2.5, -1.5, 3.0]);
1419 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1420
1421 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &RigidParams::default())
1422 .expect("registration should run");
1423
1424 let rot_err = rotation_error_deg(&t.matrix, &truth);
1425 let trans_err = translation_error_mm(&t.matrix, &truth);
1426 println!(
1427 "recovered rot err {rot_err:.3}deg, trans err {trans_err:.3}mm, ncc {:.4}, {} evals",
1428 t.ncc, t.evaluations
1429 );
1430 assert!(rot_err < 1.0, "rotation error {rot_err} deg");
1431 assert!(trans_err < 1.0, "translation error {trans_err} mm");
1432 assert!(t.ncc > 0.99, "ncc {}", t.ncc);
1433 }
1434
1435 #[test]
1439 fn the_recovery_thresholds_can_fail() {
1440 let dims = (48, 48, 48);
1441 let affine = identity_affine((1.0, 1.0, 1.0));
1442 let fixed = phantom(dims);
1443 let truth = truth_transform(&grid_center_world(dims, &affine), [9.0, -6.0, 12.0], [2.5, -1.5, 3.0]);
1444 let t = register_rigid(&fixed, dims, &affine, &fixed, dims, &affine, None, &RigidParams::default())
1445 .expect("registration should run");
1446 assert!(
1447 rotation_error_deg(&t.matrix, &truth) > 1.0,
1448 "a 16-degree rotation must not pass the 1-degree threshold"
1449 );
1450 assert!(
1451 translation_error_mm(&t.matrix, &truth) > 1.0,
1452 "a 3mm offset must not pass the 1mm threshold"
1453 );
1454 }
1455
1456 #[test]
1460 fn self_registration_is_the_identity() {
1461 let dims = (40, 40, 40);
1462 let affine = identity_affine((1.0, 1.0, 1.0));
1463 let fixed = phantom(dims);
1464 let t = register_rigid(&fixed, dims, &affine, &fixed, dims, &affine, None, &RigidParams::default())
1465 .unwrap();
1466 assert!(t.rotation_magnitude_deg() < 0.3, "rotation {}", t.rotation_magnitude_deg());
1467 assert!(
1468 translation_error_mm(&t.matrix, &IDENTITY4) < 0.3,
1469 "translation {:?}",
1470 t.translation_mm()
1471 );
1472 assert!(t.ncc > 0.9999, "ncc {}", t.ncc);
1473 }
1474
1475 #[test]
1479 fn recovers_a_rotation_on_anisotropic_voxels() {
1480 let dims = (48, 48, 24);
1481 let affine = identity_affine((0.8, 0.8, 2.0));
1482 let fixed = phantom(dims);
1483 let truth = truth_transform(&grid_center_world(dims, &affine), [5.0, -4.0, 8.0], [1.6, -2.4, 1.0]);
1484 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1485 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &RigidParams::default())
1486 .unwrap();
1487 let rot_err = rotation_error_deg(&t.matrix, &truth);
1488 let trans_err = translation_error_mm(&t.matrix, &truth);
1489 println!("anisotropic: rot err {rot_err:.3}deg, trans err {trans_err:.3}mm");
1490 assert!(rot_err < 1.5, "rotation error {rot_err} deg");
1491 assert!(trans_err < 1.5, "translation error {trans_err} mm");
1492 }
1493
1494 #[test]
1502 fn recovery_survives_noise() {
1503 let dims = (48, 48, 48);
1504 let affine = identity_affine((1.0, 1.0, 1.0));
1505 let clean = phantom(dims);
1506 let truth = truth_transform(&grid_center_world(dims, &affine), [7.0, 5.0, -9.0], [-2.0, 1.0, 2.0]);
1507 let mut moving = warp(&clean, dims, &affine, dims, &affine, &truth);
1508 let mut fixed = clean.clone();
1509
1510 let n = clean.len() as f64;
1511 let mean = clean.iter().sum::<f64>() / n;
1512 let std = (clean.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n).sqrt();
1513 let sigma = 0.3 * std;
1514
1515 let mut state = 0xC0FFEEu64;
1518 let mut uniform = || {
1519 state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
1520 let mut z = state;
1521 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1522 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1523 ((z ^ (z >> 31)) >> 11) as f64 / (1u64 << 53) as f64
1524 };
1525 for v in fixed.iter_mut().chain(moving.iter_mut()) {
1526 let (u1, u2) = (uniform().max(1e-12), uniform());
1527 *v += sigma * (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
1528 }
1529
1530 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &RigidParams::default())
1531 .unwrap();
1532 let rot_err = rotation_error_deg(&t.matrix, &truth);
1533 let trans_err = translation_error_mm(&t.matrix, &truth);
1534 println!("noisy (sigma {sigma:.3}): rot err {rot_err:.3}deg, trans err {trans_err:.3}mm, ncc {:.3}", t.ncc);
1535 assert!(rot_err < 2.0, "rotation error {rot_err} deg");
1536 assert!(trans_err < 2.0, "translation error {trans_err} mm");
1537 }
1538
1539 #[test]
1542 fn registers_across_different_grids() {
1543 let fixed_dims = (48, 48, 48);
1544 let fixed_affine = identity_affine((1.0, 1.0, 1.0));
1545 let fixed = phantom(fixed_dims);
1546
1547 let moving_dims = (40, 40, 30);
1548 let moving_affine = [
1549 1.2, 0.0, 0.0, 3.0, 0.0, 1.2, 0.0, -2.0, 0.0, 0.0, 1.5, 1.0, 0.0, 0.0, 0.0, 1.0,
1553 ];
1554 let truth = truth_transform(&grid_center_world(fixed_dims, &fixed_affine), [6.0, -5.0, 7.0], [1.0, -1.0, 2.0]);
1555 let moving = warp(&fixed, fixed_dims, &fixed_affine, moving_dims, &moving_affine, &truth);
1556
1557 let t = register_rigid(
1558 &fixed, fixed_dims, &fixed_affine,
1559 &moving, moving_dims, &moving_affine,
1560 None, &RigidParams::default(),
1561 )
1562 .unwrap();
1563 let rot_err = rotation_error_deg(&t.matrix, &truth);
1564 println!("cross-grid: rot err {rot_err:.3}deg, overlap {:.2}, ncc {:.3}", t.overlap, t.ncc);
1565 assert!(rot_err < 2.0, "rotation error {rot_err} deg");
1566 assert!(translation_error_mm(&t.matrix, &truth) < 2.0);
1567 }
1568
1569 #[test]
1574 fn resampling_through_the_transform_restores_the_fixed_volume() {
1575 let dims = (48, 48, 48);
1576 let affine = identity_affine((1.0, 1.0, 1.0));
1577 let fixed = phantom(dims);
1578 let truth = truth_transform(&grid_center_world(dims, &affine), [10.0, 0.0, -8.0], [2.0, 2.0, -2.0]);
1579 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1580 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &RigidParams::default())
1581 .unwrap();
1582
1583 let back = t.resample(&moving).unwrap();
1584 let (mut sf, mut sb, mut sff, mut sbb, mut sfb, mut n) = (0.0, 0.0, 0.0, 0.0, 0.0, 0usize);
1588 for (&a, &b) in fixed.iter().zip(back.iter()) {
1589 if a <= 0.0 && b <= 0.0 {
1590 continue;
1591 }
1592 sf += a;
1593 sb += b;
1594 sff += a * a;
1595 sbb += b * b;
1596 sfb += a * b;
1597 n += 1;
1598 }
1599 let n = n as f64;
1600 let corr = (sfb - sf * sb / n)
1601 / ((sff - sf * sf / n).sqrt() * (sbb - sb * sb / n).sqrt());
1602 println!("round-trip correlation {corr:.5}");
1603 assert!(corr > 0.98, "round-trip correlation {corr}");
1604
1605 let flipped = RigidTransform { matrix: rigid_inverse(&t.matrix), ..t.clone() };
1608 let bad = flipped.resample(&moving).unwrap();
1609 let (mut sf2, mut sb2, mut sff2, mut sbb2, mut sfb2, mut n2) = (0.0, 0.0, 0.0, 0.0, 0.0, 0usize);
1610 for (&a, &b) in fixed.iter().zip(bad.iter()) {
1611 if a <= 0.0 && b <= 0.0 {
1612 continue;
1613 }
1614 sf2 += a;
1615 sb2 += b;
1616 sff2 += a * a;
1617 sbb2 += b * b;
1618 sfb2 += a * b;
1619 n2 += 1;
1620 }
1621 let n2 = n2 as f64;
1622 let bad_corr = (sfb2 - sf2 * sb2 / n2)
1623 / ((sff2 - sf2 * sf2 / n2).sqrt() * (sbb2 - sb2 * sb2 / n2).sqrt());
1624 println!("wrong-direction correlation {bad_corr:.5}");
1625 assert!(bad_corr < corr - 0.01, "the inverted transform scored {bad_corr}, the correct one {corr}");
1626 }
1627
1628 #[test]
1633 fn b0_without_rotation_matches_the_fixed_affine() {
1634 let dims = (16, 16, 16);
1635 let oblique = [
1636 1.0, 0.0, 0.0, 0.0, 0.0, 0.9397, -0.3420, 0.0, 0.0, 0.3420, 0.9397, 0.0, 0.0, 0.0, 0.0, 1.0,
1640 ];
1641 let t = RigidTransform::identity(dims, &oblique, dims, &oblique);
1642 let got = t.b0_direction_in_fixed(None);
1643 let want = b0_direction_from_affine(&oblique);
1644 assert!((got.0 - want.0).abs() < 1e-12);
1645 assert!((got.1 - want.1).abs() < 1e-12);
1646 assert!((got.2 - want.2).abs() < 1e-12);
1647 }
1648
1649 #[test]
1657 fn registration_recovers_b0_where_the_affine_cannot() {
1658 let dims = (48, 48, 48);
1659 let affine = identity_affine((1.0, 1.0, 1.0));
1660 let fixed = phantom(dims);
1661 let angle_deg = 20.0f64;
1662 let angle = angle_deg.to_radians();
1663 let truth = truth_transform(&grid_center_world(dims, &affine), [angle_deg, 0.0, 0.0], [0.0, 0.0, 0.0]);
1664 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1665
1666 assert_eq!(b0_direction_from_affine(&affine), (0.0, 0.0, 1.0));
1668
1669 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &RigidParams::default())
1670 .unwrap();
1671 let b = t.b0_direction_in_fixed(None);
1672
1673 let want = (0.0, angle.sin(), angle.cos());
1676 println!("b0 recovered {b:?}, expected {want:?}");
1677 for (got, exp) in [(b.0, want.0), (b.1, want.1), (b.2, want.2)] {
1678 assert!((got - exp).abs() < 0.03, "{b:?} vs {want:?}");
1679 }
1680 assert!(b.1 > 0.1, "B0 tilted the wrong way: {b:?}");
1682
1683 let spread = b.2.abs().clamp(0.0, 1.0).acos().to_degrees();
1686 assert!(spread > 15.0, "spread against the reference is only {spread} deg");
1687 }
1688
1689 #[test]
1692 fn declared_b0_goes_through_the_same_transport() {
1693 let dims = (16, 16, 16);
1694 let fixed_affine = identity_affine((1.0, 1.0, 1.0));
1695 let moving_affine = [
1696 1.0, 0.0, 0.0, 0.0, 0.0, 0.866, -0.5, 0.0, 0.0, 0.5, 0.866, 0.0, 0.0, 0.0, 0.0, 1.0,
1700 ];
1701 let mut t = RigidTransform::identity(dims, &fixed_affine, dims, &moving_affine);
1702 t.matrix = matrix_from_params(&[0.2, -0.1, 0.3, 0.0, 0.0, 0.0], &grid_center_world(dims, &fixed_affine));
1703
1704 let implicit = t.b0_direction_in_fixed(None);
1705 let explicit = t.b0_direction_in_fixed(Some(b0_direction_from_affine(&moving_affine)));
1706 assert!((implicit.0 - explicit.0).abs() < 1e-12);
1707 assert!((implicit.1 - explicit.1).abs() < 1e-12);
1708 assert!((implicit.2 - explicit.2).abs() < 1e-12);
1709
1710 let other = t.b0_direction_in_fixed(Some((1.0, 0.0, 0.0)));
1713 assert!(
1714 (other.0 - implicit.0).abs() + (other.1 - implicit.1).abs() + (other.2 - implicit.2).abs() > 0.1,
1715 "declared direction had no effect"
1716 );
1717 }
1718
1719 #[test]
1722 fn aligned_affine_agrees_with_resampling() {
1723 let dims = (32, 32, 32);
1724 let affine = identity_affine((1.0, 1.0, 1.0));
1725 let moving = phantom(dims);
1726 let mut t = RigidTransform::identity(dims, &affine, dims, &affine);
1727 t.matrix = matrix_from_params(&[0.15, -0.1, 0.25, 1.0, -2.0, 0.5], &grid_center_world(dims, &affine));
1728
1729 let via_method = t.resample(&moving).unwrap();
1730 let via_affine =
1731 resample_onto(&moving, dims, &t.aligned_affine(), dims, &affine).unwrap();
1732 for (a, b) in via_method.iter().zip(via_affine.iter()) {
1733 assert!((a - b).abs() < 1e-12);
1734 }
1735 }
1736
1737 #[test]
1738 fn inverse_swaps_the_roles() {
1739 let fixed_dims = (32, 32, 32);
1740 let moving_dims = (24, 24, 20);
1741 let fixed_affine = identity_affine((1.0, 1.0, 1.0));
1742 let moving_affine = identity_affine((1.5, 1.5, 2.0));
1743 let mut t = RigidTransform::identity(fixed_dims, &fixed_affine, moving_dims, &moving_affine);
1744 t.matrix = matrix_from_params(&[0.2, 0.1, -0.3, 3.0, -1.0, 2.0], &grid_center_world(fixed_dims, &fixed_affine));
1745
1746 let inv = t.inverse();
1747 assert_eq!(inv.fixed_dims, moving_dims);
1748 assert_eq!(inv.moving_dims, fixed_dims);
1749 let rebuilt = matrix_from_params(&inv.params, &grid_center_world(moving_dims, &moving_affine));
1751 for (i, (&got, &want)) in rebuilt.iter().zip(inv.matrix.iter()).enumerate() {
1752 assert!((got - want).abs() < 1e-9, "element {i}: {got} vs {want}");
1753 }
1754 let round = inv.inverse();
1756 for (i, (&got, &want)) in round.matrix.iter().zip(t.matrix.iter()).enumerate() {
1757 assert!((got - want).abs() < 1e-9, "element {i}: {got} vs {want}");
1758 }
1759 }
1760
1761 #[test]
1764 fn rejects_mismatched_lengths_and_singular_affines() {
1765 let dims = (8, 8, 8);
1766 let affine = identity_affine((1.0, 1.0, 1.0));
1767 let v = vec![0.0f64; 8 * 8 * 8];
1768 let short = vec![0.0f64; 10];
1769 let p = RigidParams::default();
1770 assert!(register_rigid(&short, dims, &affine, &v, dims, &affine, None, &p).is_none());
1771 assert!(register_rigid(&v, dims, &affine, &short, dims, &affine, None, &p).is_none());
1772 assert!(register_rigid(&v, dims, &affine, &v, dims, &affine, Some(&[1u8; 4]), &p).is_none());
1773 let singular = [0.0f64; 16];
1774 assert!(register_rigid(&v, dims, &singular, &v, dims, &affine, None, &p).is_none());
1775 assert!(register_rigid(&v, dims, &affine, &v, dims, &singular, None, &p).is_none());
1776 }
1777
1778 #[test]
1786 fn the_mask_restricts_the_metric() {
1787 let dims = (48, 48, 48);
1788 let affine = identity_affine((1.0, 1.0, 1.0));
1789 let fixed = phantom(dims);
1790 let truth = truth_transform(&grid_center_world(dims, &affine), [8.0, 0.0, 6.0], [1.0, -1.0, 0.0]);
1791 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1792
1793 let (nx, ny, nz) = dims;
1794 let peak = fixed.iter().cloned().fold(0.0f64, f64::max);
1795 let mut mask = vec![0u8; nx * ny * nz];
1796 let mut spoiled = fixed.clone();
1797 for k in 0..nz {
1798 for j in 0..ny {
1799 for i in 0..nx {
1800 let idx = i + j * nx + k * nx * ny;
1801 let (x, y, z) = (i as f64 / nx as f64, j as f64 / ny as f64, k as f64 / nz as f64);
1803 let d = ((x - 0.5) / 0.38).powi(2) + ((y - 0.5) / 0.30).powi(2) + ((z - 0.5) / 0.34).powi(2);
1804 let inside = d <= 1.0;
1805 mask[idx] = inside as u8;
1806 if !inside {
1807 spoiled[idx] = 0.6 * peak
1808 * (1.0 + (0.7 * i as f64).sin() * (0.5 * j as f64).cos() * (0.3 * k as f64).sin());
1809 }
1810 }
1811 }
1812 }
1813 let p = RigidParams::default();
1814 let masked = register_rigid(&spoiled, dims, &affine, &moving, dims, &affine, Some(&mask), &p).unwrap();
1815 let unmasked = register_rigid(&spoiled, dims, &affine, &moving, dims, &affine, None, &p).unwrap();
1816 let masked_err = rotation_error_deg(&masked.matrix, &truth);
1817 let unmasked_err = rotation_error_deg(&unmasked.matrix, &truth);
1818 println!("masked err {masked_err:.3}deg (ncc {:.3}), unmasked err {unmasked_err:.3}deg (ncc {:.3})",
1819 masked.ncc, unmasked.ncc);
1820 assert!(masked_err < 1.5, "masked rotation error {masked_err} deg");
1821 assert!(
1822 unmasked_err > 2.0 * masked_err,
1823 "the mask made no real difference: masked {masked_err}, unmasked {unmasked_err}"
1824 );
1825 }
1826
1827 #[test]
1830 fn max_samples_bounds_the_work() {
1831 let dims = (48, 48, 48);
1832 let affine = identity_affine((1.0, 1.0, 1.0));
1833 let fixed = phantom(dims);
1834 let truth = truth_transform(&grid_center_world(dims, &affine), [7.0, 0.0, -5.0], [1.0, 0.0, -1.0]);
1835 let moving = warp(&fixed, dims, &affine, dims, &affine, &truth);
1836
1837 let cheap = RigidParams { max_samples: 3000, ..Default::default() };
1838 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &cheap).unwrap();
1839 let err = rotation_error_deg(&t.matrix, &truth);
1840 println!("3000-sample registration: rot err {err:.3}deg");
1841 assert!(err < 2.0, "rotation error {err} deg with a 3000-sample cap");
1842
1843 let v = Volume::borrowed(&fixed, dims, affine);
1845 assert!(sample_set(&v, None, 3000).len() <= 3000);
1846 assert!(sample_set(&v, None, 200_000).len() <= 200_000);
1847 }
1848
1849 #[test]
1850 fn evaluation_budget_is_respected() {
1851 let dims = (32, 32, 32);
1852 let affine = identity_affine((1.0, 1.0, 1.0));
1853 let fixed = phantom(dims);
1854 let moving = warp(
1855 &fixed,
1856 dims,
1857 &affine,
1858 dims,
1859 &affine,
1860 &truth_transform(&grid_center_world(dims, &affine), [6.0, 0.0, 0.0], [0.0, 0.0, 0.0]),
1861 );
1862 let p = RigidParams { max_evaluations: 40, ..Default::default() };
1863 let t = register_rigid(&fixed, dims, &affine, &moving, dims, &affine, None, &p).unwrap();
1864 assert!(t.evaluations <= 60, "budget 40 overrun to {}", t.evaluations);
1865 }
1866}