use crate::bspline::{BSpline, BSplineTrait, DefaultValue}; use crate::error::Error; use crate::filter::{fft, ifft}; use crate::metric::FixedMu; use crate::register::{Registration, RegistrationResult, RegistrationStep}; use itertools::Itertools; use ndarray::{ Array, Array2, ArrayD, ArrayView2, AsArray, Dimension, IntoDimension, Ix2, IxDyn, s, }; use num::cast::AsPrimitive; use num::complex::ComplexFloat; use serde::{Deserialize, Serialize}; use serde_yaml::{from_reader, to_writer}; use std::fs::File; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; use std::ops::{Add, Div, Mul, Sub}; use std::path::PathBuf; /// get coordinates resulting from transforming input coordinates, coordinate must have N /// columns: x, y, z, ... #[inline] pub fn transform_point<'a, T>(point: &'a [T], center: &'a [f64], parameters: &'a [f64]) -> Vec where T: AsPrimitive, { // [a, b, c] x = a*x + b*y + c // [d, e, f] y = d*x + e*y + f // [0, 0, 1] 1 = 1 debug_assert_eq!(center.len(), point.len()); debug_assert_eq!((center.len() + 1) * center.len(), parameters.len()); let m = center.len(); let n = m * m; let point = point .iter() .zip_eq(center.iter()) .map(|(p, c)| p.as_() - c) .collect::>(); parameters .iter() .take(n) .chunks(m) .into_iter() .zip_eq(parameters.iter().skip(n)) .zip_eq(center.iter()) .map(|((r, t), c)| r.zip_eq(point.iter()).map(|(i, p)| i * p).sum::() + t + c) .collect() } /// Calculate the inverse of a square matrix using LU factorization with partial /// pivoting. /// /// Returns `None` if the matrix is singular, or numerically rank-deficient. pub(crate) fn try_matrix_inverse(matrix: Array2) -> Option> { let n = matrix.nrows(); debug_assert_eq!(n, matrix.ncols(), "matrix must be square"); // pivot magnitudes must exceed this relative threshold to count as significant let max_abs = matrix.iter().fold(0.0f64, |m, &x| m.max(x.abs())); let threshold = max_abs * (n as f64) * f64::EPSILON; // PA = LU factorization (Doolittle with partial pivoting) let mut lu = matrix; let mut perm: Vec = (0..n).collect(); for k in 0..n { let mut pivot_row = k; for i in (k + 1)..n { if lu[[i, k]].abs() > lu[[pivot_row, k]].abs() { pivot_row = i; } } if lu[[pivot_row, k]].abs() <= threshold || !lu[[pivot_row, k]].is_finite() { return None; } if pivot_row != k { for j in 0..n { lu.swap([k, j], [pivot_row, j]); } perm.swap(k, pivot_row); } for i in (k + 1)..n { let factor = lu[[i, k]] / lu[[k, k]]; lu[[i, k]] = factor; for j in (k + 1)..n { lu[[i, j]] -= factor * lu[[k, j]]; } } } // Solve A * inv = I column by column: first L*y = P*e_col, then U*x = y. let mut inv = Array2::zeros((n, n)); for col in 0..n { let mut y = vec![0.0; n]; for i in 0..n { let rhs = if perm[i] == col { 1.0 } else { 0.0 }; let mut sum = rhs; for j in 0..i { sum -= lu[[i, j]] * y[j]; } y[i] = sum; } for i in (0..n).rev() { let mut sum = y[i]; for j in (i + 1)..n { sum -= lu[[i, j]] * inv[[j, col]]; } inv[[i, col]] = sum / lu[[i, i]]; } } Some(inv) } /// a struct describing the transform /// generic parameter N = # image dimensions #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct Transform { /// flattened NxN rotation matrix + N translation parameters pub parameters: Vec, /// error / significance on parameters pub dparameters: Vec, #[serde(skip)] dimension: PhantomData, } impl Mul for Transform { type Output = Transform; fn mul(self, rhs: Self) -> Self::Output { &self * &rhs } } impl Mul<&Transform> for Transform { type Output = Transform; fn mul(self, rhs: &Transform) -> Self::Output { &self * rhs } } impl Mul> for &Transform { type Output = Transform; fn mul(self, rhs: Transform) -> Self::Output { self * &rhs } } impl Mul<&Transform> for &Transform { type Output = Transform; #[expect(clippy::suspicious_arithmetic_impl)] fn mul(self, rhs: &Transform) -> Self::Output { let m = self.matrix().dot(&rhs.matrix()); let dm = self.dmatrix().dot(&rhs.matrix()) + self.matrix().dot(&rhs.dmatrix()); Self::Output { parameters: m .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(m.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dparameters: dm .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(dm.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dimension: self.dimension, } } } impl Add for Transform { type Output = Transform; fn add(self, rhs: Self) -> Self::Output { &self + &rhs } } impl Add<&Transform> for Transform { type Output = Transform; fn add(self, rhs: &Transform) -> Self::Output { &self + rhs } } impl Add> for &Transform { type Output = Transform; fn add(self, rhs: Transform) -> Self::Output { self + &rhs } } impl Add<&Transform> for &Transform { type Output = Transform; fn add(self, rhs: &Transform) -> Self::Output { let m = self.matrix() + &rhs.matrix(); let dm = (self.dmatrix().powi(2) + rhs.matrix().powi(2)).sqrt(); Self::Output { parameters: m .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(m.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dparameters: dm .slice(s![..self.ndim(), ..]) .flatten() .iter() .chain(dm.slice(s![..self.ndim(), ..]).iter()) .cloned() .collect(), dimension: self.dimension, } } } impl Sub for Transform { type Output = Transform; fn sub(self, rhs: Self) -> Self::Output { &self - &rhs } } impl Sub<&Transform> for Transform { type Output = Transform; fn sub(self, rhs: &Transform) -> Self::Output { &self - rhs } } impl Sub> for &Transform { type Output = Transform; fn sub(self, rhs: Transform) -> Self::Output { self - &rhs } } impl Sub<&Transform> for &Transform { type Output = Transform; fn sub(self, rhs: &Transform) -> Self::Output { let m = self.matrix() - &rhs.matrix(); let dm = (self.dmatrix().powi(2) + rhs.matrix().powi(2)).sqrt(); Self::Output { parameters: m .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(m.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dparameters: dm .slice(s![..self.ndim(), ..]) .flatten() .iter() .chain(dm.slice(s![..self.ndim(), ..]).iter()) .cloned() .collect(), dimension: self.dimension, } } } impl Mul> for f64 { type Output = Transform; fn mul(self, rhs: Transform) -> Self::Output { rhs * self } } impl Mul<&Transform> for f64 { type Output = Transform; fn mul(self, rhs: &Transform) -> Self::Output { rhs * self } } impl Mul for Transform { type Output = Transform; fn mul(self, rhs: f64) -> Self::Output { &self * rhs } } impl Mul for &Transform { type Output = Transform; fn mul(self, rhs: f64) -> Self::Output { let m = self.matrix() * rhs; let dm = self.dmatrix() * rhs; Self::Output { parameters: m .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(m.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dparameters: dm .slice(s![..self.ndim(), ..]) .flatten() .iter() .chain(dm.slice(s![..self.ndim(), ..]).iter()) .cloned() .collect(), dimension: self.dimension, } } } impl Div for Transform { type Output = Transform; fn div(self, rhs: f64) -> Self::Output { &self / rhs } } impl Div for &Transform { type Output = Transform; fn div(self, rhs: f64) -> Self::Output { let m = self.matrix() / rhs; let dm = self.dmatrix() / rhs; Self::Output { parameters: m .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .iter() .chain(m.slice(s![..self.ndim(), self.ndim()]).iter()) .cloned() .collect(), dparameters: dm .slice(s![..self.ndim(), ..]) .flatten() .iter() .chain(dm.slice(s![..self.ndim(), ..]).iter()) .cloned() .collect(), dimension: self.dimension, } } } impl Eq for Transform {} impl Hash for Transform { fn hash(&self, state: &mut H) { for values in [&self.parameters, &self.dparameters] { values.len().hash(state); for v in values { let v = if *v == 0.0 { 0.0 } else { *v }; v.to_bits().hash(state); } } } } impl Default for Transform { /// the unit transform fn default() -> Self { let ndim = D::NDIM.expect("number of dimensions must be known to initialise transform"); let mut parameters = vec![0.0; ndim * ndim + ndim]; for i in 0..ndim { parameters[(ndim + 1) * i] = 1.0; } Self { parameters, dparameters: vec![0f64; ndim * ndim + ndim], dimension: PhantomData, } } } impl Transform { /// parameters: flat NxN part of matrix pub fn new(parameters: Vec) -> Self { let dparameters = vec![0.0; parameters.len()]; Self { parameters, dparameters, dimension: PhantomData, } } pub fn new_dparameters(parameters: Vec, dparameters: Vec) -> Self { Self { parameters, dparameters, dimension: PhantomData, } } pub fn new_around_center(parameters: Vec, center: &[f64]) -> Self { Self::new(parameters).with_translation(center) * Self::from_translation(¢er.iter().map(|i| -i).collect::>()) } /// find the affine transform which transforms moving into fixed pub fn register_affine<'a, F, M, T>(fixed: F, moving: M) -> Result where F: AsArray<'a, T, D>, M: AsArray<'a, T, D>, T: 'a + Clone + AsPrimitive, { Registration::new_affine()?.register(fixed, moving) } /// find the translation which transforms moving into fixed pub fn register_translation<'a, F, M, T>(fixed: F, moving: M) -> Result where F: AsArray<'a, T, D>, M: AsArray<'a, T, D>, T: 'a + Clone + AsPrimitive, { let fixed = fixed.into(); let moving = moving.into(); let initial_guess = Self::register_affine(&fixed, &moving)?; let initial_guess = initial_guess.parameters[initial_guess.ndim() * initial_guess.ndim()..].to_vec(); Registration::new_translation()? .with_initial_guess(initial_guess)? .register(fixed, moving) } /// find the translation which transforms moving into fixed /// using the cross-correlation theorem and fft pub fn register_translation_fft<'a, F, M, T>(fixed: F, moving: M) -> Result where F: AsArray<'a, T, D>, M: AsArray<'a, T, D>, T: 'a + Clone + AsPrimitive, { let f = fft(fixed.into().mapv(|i| i.as_()).view())?.mapv(|i| i.conj()); let m = fft(moving.into().mapv(|i| i.as_()).view())?; let c = ifft(&(f * m))?.mapv(|i| i.re); let (i, _) = c .indexed_iter() .max_by(|(_, a), (_, b)| a.total_cmp(b)) .expect("no max because array is empty"); let d = i.into_dimension(); let j = d .as_array_view() .iter() .zip(c.shape()) .map(|(&i, &s)| { let mut fi = i as f64; let fs = s as f64; if 2.0 * fi > fs { fi -= fs; } fi }) .collect::>(); Ok(Self::from_translation(&j)) } /// find the transform which transforms moving into fixed, using fixed_mu to specify which /// parameters to keep fixed pub fn register<'a, F, M, T, G>( fixed: F, moving: M, fixed_mu: G, steps: Option>, initial_guess: Option>, ) -> Result where F: AsArray<'a, T, D>, M: AsArray<'a, T, D>, T: 'a + Clone + AsPrimitive, G: Into, { let mut registration = Registration::new(fixed_mu)?; if let Some(steps) = steps { registration.set_steps(steps); } if let Some(initial_guess) = initial_guess { registration.set_initial_guess(initial_guess)?; } registration.register(fixed, moving) } /// find the transform which transforms moving into fixed and return the results of each /// optimization step pub fn register_debug<'a, F, M, T, G>( fixed: F, moving: M, fixed_mu: G, steps: Option>, initial_guess: Option>, ) -> Result<(Self, Vec), Error> where F: AsArray<'a, T, D>, M: AsArray<'a, T, D>, T: 'a + Clone + AsPrimitive, G: Into, { let mut registration = Registration::new(fixed_mu)?; if let Some(steps) = steps { registration.set_steps(steps); } if let Some(initial_guess) = initial_guess { registration.set_initial_guess(initial_guess)?; } registration.register_debug(fixed, moving) } /// create a transform from a translation pub fn from_translation(translation: &[f64]) -> Self { let ndim = if let Some(ndim) = D::NDIM { ndim } else { translation.len() }; debug_assert_eq!(translation.len(), ndim); let mut parameters = vec![0.0; ndim * ndim + ndim]; for i in 0..ndim { parameters[(ndim + 1) * i] = 1.0; } for (p, t) in parameters .iter_mut() .skip(ndim * ndim) .zip_eq(translation.iter()) { *p = *t; } Self { parameters, dparameters: vec![0f64; ndim * ndim + ndim], dimension: PhantomData, } } /// create a transform from a scaling pub fn from_scaling(scaling: &[f64]) -> Self { let ndim = if let Some(ndim) = D::NDIM { ndim } else { scaling.len() }; debug_assert_eq!(scaling.len(), ndim); let mut parameters = vec![0.0; ndim * ndim + ndim]; parameters[ndim * ndim + ndim - 1] = 1.0; for (p, s) in parameters .iter_mut() .step_by(ndim + 1) .zip_eq(scaling.iter()) { *p = *s; } Self { parameters, dparameters: vec![0f64; ndim * ndim + ndim], dimension: PhantomData, } } /// replace the transform by a scaling pub fn as_scaling(&self, scaling: &[f64]) -> Self { let ndim = if let Some(ndim) = D::NDIM { ndim } else { scaling.len() }; debug_assert_eq!(scaling.len(), ndim); let mut parameters = vec![0.0; ndim * ndim + ndim]; parameters[ndim * ndim + ndim - 1] = 1.0; for (p, s) in parameters .iter_mut() .step_by(ndim + 1) .zip_eq(scaling.iter()) { *p = *s; } Self { parameters, dparameters: vec![0f64; ndim * ndim + ndim], dimension: PhantomData, } } /// scale self pub fn add_scaling(&self, scaling: &[f64]) -> Self { Self::from_scaling(scaling) * self } /// add a translation to self pub fn with_translation(&self, translation: &[f64]) -> Self { Self::from_translation(translation) * self } /// replace the transform by a translation pub fn as_translation(&self, translation: &[f64]) -> Self { let ndim = if let Some(ndim) = D::NDIM { ndim } else { translation.len() }; debug_assert_eq!(translation.len(), ndim); let mut parameters = vec![0.0; ndim * ndim + ndim]; for i in 0..ndim { parameters[(ndim + 1) * i] = 1.0; } for (p, t) in parameters .iter_mut() .skip(ndim * ndim) .zip_eq(translation.iter()) { *p = *t; } Self { parameters, dparameters: vec![0f64; ndim * ndim + ndim], dimension: PhantomData, } } /// add a translation to self pub fn add_translation(&self, translation: &[f64]) -> Self { Self::from_translation(translation) * self } /// read a transform from a file pub fn from_file(path: PathBuf) -> Result { let file = File::open(path)?; Ok(from_reader(file)?) } /// write a transform to a file pub fn to_file(&self, path: PathBuf) -> Result<(), Error> { let mut file = std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(path)?; to_writer(&mut file, self)?; Ok(()) } /// true if transform does nothing pub fn is_unity(&self) -> bool { let n = self.ndim() * self.ndim(); let m = self.ndim() + 1; self.parameters .iter() .take(n) .enumerate() .all(|(i, &x)| if i % m == 0 { x == 1.0 } else { x == 0.0 }) && self.parameters.iter().skip(n).all(|&x| x == 0.0) } /// get coordinates resulting from transforming input coordinates, coordinate must have N /// columns: x, y, z, ... pub fn transform_point<'a, T>(&'a self, point: &'a [T], center: &'a [f64]) -> Vec where T: AsPrimitive, { transform_point(point, center, &self.parameters) } /// get coordinates resulting from transforming input coordinates, coordinates must have N /// columns: x, y, z, ... pub fn transform_points<'a, A, T>( &'a self, points: A, center: &'a [f64], ) -> Result, Error> where T: AsPrimitive, A: AsArray<'a, T, Ix2>, { // [a, b, c] x = a*x + b*y + c // [d, e, f] y = d*x + e*y + f // [0, 0, 1] 1 = 1 let points = points.into(); let shape = points.shape(); let a = points .rows() .into_iter() .flat_map(|p| { if let Some(q) = p.as_slice() { transform_point(q, center, &self.parameters) } else { transform_point(&p.to_vec(), center, &self.parameters) } }) .collect::>(); Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?) } /// get the matrix defining the transform pub fn matrix(&self) -> Array2 { let n = self.ndim() * self.ndim(); let mut matrix = Array2::eye(self.ndim() + 1); for (m, p) in matrix .slice_mut(s![..self.ndim(), ..self.ndim()]) .iter_mut() .zip_eq(self.parameters.iter().take(n)) { *m = *p; } for (m, p) in matrix .slice_mut(s![..self.ndim(), self.ndim()]) .iter_mut() .zip_eq(self.parameters.iter().skip(n)) { *m = *p; } matrix } /// get the matrix describing the error of the transform pub fn dmatrix(&self) -> Array2 { let n = self.ndim() * self.ndim(); let mut matrix = Array2::zeros([self.ndim() + 1, self.ndim() + 1]); for (m, p) in matrix .slice_mut(s![..self.ndim(), ..self.ndim()]) .iter_mut() .zip_eq(self.dparameters.iter().take(n)) { *m = *p; } for (m, p) in matrix .slice_mut(s![..self.ndim(), self.ndim()]) .iter_mut() .zip_eq(self.dparameters.iter().skip(n)) { *m = *p; } matrix } pub fn with_parameters(mut self, parameters: Vec) -> Self { self.parameters = parameters; self } pub fn with_dparameters(mut self, dparameters: Vec) -> Self { self.dparameters = dparameters; self } pub fn with_matrix(mut self, matrix: ArrayView2) -> Self { let mut parameters = matrix .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .to_vec(); parameters.extend(matrix.slice(s![..self.ndim(), self.ndim()]).to_vec()); self.parameters = parameters; self } pub fn set_matrix(&mut self, matrix: ArrayView2) { let mut parameters = matrix .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .to_vec(); parameters.extend(matrix.slice(s![..self.ndim(), self.ndim()]).to_vec()); self.parameters = parameters; } pub fn with_dmatrix(mut self, dmatrix: ArrayView2) -> Self { let mut dparameters = dmatrix .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .to_vec(); dparameters.extend(dmatrix.slice(s![..self.ndim(), self.ndim()]).to_vec()); self.dparameters = dparameters; self } pub fn set_dmatrix(&mut self, dmatrix: ArrayView2) { let mut dparameters = dmatrix .slice(s![..self.ndim(), ..self.ndim()]) .flatten() .to_vec(); dparameters.extend(dmatrix.slice(s![..self.ndim(), self.ndim()]).to_vec()); self.dparameters = dparameters; } /// get the inverse transform pub fn inverse(&self) -> Result { let matrix = self.matrix(); let inverse = try_matrix_inverse(matrix).ok_or(Error::SingularMatrix)?; let parameters = inverse .slice(s![..self.ndim(), ..self.ndim()]) .iter() .chain(inverse.slice(s![..self.ndim(), self.ndim()])) .cloned() .collect(); Ok(Self { parameters, dparameters: vec![0f64; self.ndim() * self.ndim() + self.ndim()], dimension: PhantomData, }) } pub fn interpolate<'a, const B: usize, T, A>( &self, image: A, default_value: &DefaultValue, ) -> Result, Error> where A: AsArray<'a, T, D>, D: Dimension, T: 'a + Clone + AsPrimitive, BSpline: BSplineTrait, { let image = image.into(); let bspline = BSpline::::new(&image); bspline.interpolate(self, default_value) } pub fn interpolate_par<'a, const B: usize, T, A>( &self, image: A, default_value: &DefaultValue, ) -> Result, Error> where A: AsArray<'a, T, D>, D: Dimension, T: 'a + Clone + AsPrimitive, BSpline: BSplineTrait, { let image = image.into(); let bspline = BSpline::::new(&image); bspline.interpolate_par(self, default_value) } pub fn interpolate_order<'a, T, A>( &self, image: A, order: usize, default_value: &DefaultValue, ) -> Result, Error> where A: AsArray<'a, T, D>, D: Dimension, T: 'a + Clone + AsPrimitive, { match order { 0 => Ok(self.interpolate::<0, _, _>(image, default_value)?), 1 => Ok(self.interpolate::<1, _, _>(image, default_value)?), 2 => Ok(self.interpolate::<2, _, _>(image, default_value)?), 3 => Ok(self.interpolate::<3, _, _>(image, default_value)?), 4 => Ok(self.interpolate::<4, _, _>(image, default_value)?), 5 => Ok(self.interpolate::<5, _, _>(image, default_value)?), _ => Err(Error::InterpolationNotImplemented(order)), } } pub fn interpolate_par_order<'a, T, A>( &self, image: A, order: usize, default_value: &DefaultValue, ) -> Result, Error> where A: AsArray<'a, T, D>, D: Dimension, T: 'a + Clone + AsPrimitive, { match order { 0 => Ok(self.interpolate_par::<0, _, _>(image, default_value)?), 1 => Ok(self.interpolate_par::<1, _, _>(image, default_value)?), 2 => Ok(self.interpolate_par::<2, _, _>(image, default_value)?), 3 => Ok(self.interpolate_par::<3, _, _>(image, default_value)?), 4 => Ok(self.interpolate_par::<4, _, _>(image, default_value)?), 5 => Ok(self.interpolate_par::<5, _, _>(image, default_value)?), _ => Err(Error::InterpolationNotImplemented(order)), } } /// the number of dimensions in the view pub fn ndim(&self) -> usize { if let Some(d) = D::NDIM { d } else { self.parameters.len().isqrt() } } pub fn into_dimensionality(self) -> Result, Error> { if let Some(d) = D2::NDIM { if d == self.ndim() { Ok(Transform { parameters: self.parameters, dparameters: self.dparameters, dimension: PhantomData, }) } else { Err(Error::DimensionalityMismatch(d, self.ndim())) } } else { Ok(Transform { parameters: self.parameters, dparameters: self.dparameters, dimension: PhantomData, }) } } pub fn into_dyn(self) -> Transform { Transform { parameters: self.parameters, dparameters: self.dparameters, dimension: PhantomData, } } fn parameters_unit(ndim: usize) -> Vec { let mut p = vec![0.0; ndim * (ndim + 1)]; for i in 0..ndim { p[i * (ndim + 1)] = 1.0; } p } pub fn add_dimension(&self, axis: usize) -> Transform { let ndim = self.ndim() + 1; let mut parameters = Self::parameters_unit(ndim); let mut dparameters = Self::parameters_unit(ndim); let mut j = 0; for p in &self.parameters { if j == axis * ndim { j += ndim; } if j % ndim == axis { j += 1; } parameters[j] = *p; dparameters[j] = *p; } Transform { parameters, dparameters, dimension: Default::default(), } } pub fn swap_axes(&self, a: usize, b: usize) -> Self { let mut new = self.clone(); let n = self.ndim(); let mut m = (0..n).collect::>(); (m[a], m[b]) = (m[b], m[a]); for (i, j) in (0..n).zip(m) { if i != j { new.parameters[i + n * j] = self.parameters[j + n * i]; new.dparameters[i + n * j] = self.dparameters[j + n * i]; new.parameters[j + n * i] = self.parameters[i + n * j]; new.dparameters[j + n * i] = self.dparameters[i + n * j]; } } new.parameters[n + a] = self.parameters[n + b]; new.dparameters[n + a] = self.dparameters[n + b]; new } } impl Transform { /// create a transform from a rotation in radians pub fn from_rotation(theta: f64, center: &[f64]) -> Self { Self::new_around_center( vec![ theta.cos(), -theta.sin(), theta.sin(), theta.cos(), 0.0, 0.0, ], center, ) } /// multiply self with a rotation matrix pub fn with_rotation(&self, theta: f64, center: &[f64]) -> Self { Self::from_rotation(theta, center) * self } } #[cfg(test)] mod tests { use super::try_matrix_inverse; use crate::julia_image; use crate::transform::File; use crate::transform::Transform; use itertools::Itertools; use ndarray::{Array2, Ix2, array, s}; use num::traits::FloatConst; use std::path::Path; use tiff::decoder::{Decoder, DecodingSampleBuffer}; use tiff::tags::Tag; #[test] fn matrix_inverse_test() -> Result<(), Box> { use ndarray::array; let a = array![[4.0, 7.0], [2.0, 6.0]]; let expected = array![[0.6, -0.7], [-0.2, 0.4]]; let got = try_matrix_inverse(a.clone()).expect("matrix should not be singular"); assert!( got.iter() .zip_eq(expected.iter()) .all(|(x, y)| (x - y).abs() < 1e-12) ); let b = array![[0.0, 2.0], [3.0, 4.0]]; let expected = array![[-2.0 / 3.0, 1.0 / 3.0], [0.5, 0.0]]; let got = try_matrix_inverse(b.clone()).expect("matrix should not be singular"); assert!( got.iter() .zip_eq(expected.iter()) .all(|(x, y)| (x - y).abs() < 1e-12) ); let c = array![[1.0, 2.0, 3.0], [0.0, 1.0, 4.0], [5.0, 6.0, 0.0]]; let reference = array![[-24.0, 18.0, 5.0], [20.0, -15.0, -4.0], [-5.0, 4.0, 1.0]]; let got = try_matrix_inverse(c).expect("matrix should not be singular"); assert!( got.iter() .zip_eq(reference.iter()) .all(|(x, y)| (x - y).abs() < 1e-10) ); let singular = array![[1.0, 2.0], [2.0, 4.0]]; assert!(try_matrix_inverse(singular).is_none()); Ok(()) } #[test] fn interpolate() -> Result<(), Box> { let image_a = julia_image( &[8000, 6000], &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &[3999.5, 2999.5], &[-0.8, 0.156], ); let transform = Transform::from_rotation(1.0, &[3999.5, 2999.5]); let image_b = transform.interpolate_par::<3, _, _>(image_a.view(), &Default::default())?; let image_c = transform.interpolate::<3, _, _>(image_a.view(), &Default::default())?; assert!( image_b .into_iter() .zip_eq(image_c.into_iter()) .all(|(a, b)| a == b) ); Ok(()) } #[test] fn interpolate_serial() -> Result<(), Box> { let image_a = julia_image( &[8000, 6000], &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &[3999.5, 2999.5], &[-0.8, 0.156], ); let transform = Transform::from_rotation(1.0, &[3999.5, 2999.5]); let image_b = transform.interpolate::<3, _, _>(image_a.view(), &Default::default())?; assert_ne!(image_b[[4000, 3000]], 0.0); Ok(()) } #[test] fn interpolate_par() -> Result<(), Box> { let image_a = julia_image( &[8000, 6000], &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &[3999.5, 2999.5], &[-0.8, 0.156], ); let transform = Transform::from_rotation(1.0, &[3999.5, 2999.5]); let image_b = transform.interpolate_par::<3, _, _>(image_a.view(), &Default::default())?; assert_ne!(image_b[[4000, 3000]], 0.0); Ok(()) } #[test] fn grad_check() -> Result<(), Box> { use crate::bspline::{BSpline, BSplineTrait}; use crate::metric::{MattesMetric, SamplingArg}; use algos::ObjectiveFunction; let im_a = julia_image( &[100, 1], &[1.0, 0.0, 0.0, 0.01, 0.0, 0.0], &[99.5, 0.5], &[-0.8, 0.156], ) .slice(s![.., 0]) .mapv(|i| i as f64); let q = vec![0.85, 4.0]; let im_b = Transform::new(q).interpolate::<1, _, _>(&im_a, &Default::default())?; // Test with ALL points (like grad_check uses) let metric_all = MattesMetric::::new( BSpline::new(im_b.view()), BSpline::new(im_a.view()), SamplingArg::FixedAt((0..100).map(|i| vec![i as f64]).collect()), 3, 0.05, )? .with_fixed_mu(crate::metric::FixedMu::new_none(1)); let identity = vec![1.0, 0.0]; let val_i = metric_all.evaluate(&identity); let grad_i = metric_all.gradient(&identity).unwrap(); println!("ALL POINTS - identity: val={}, grad={:?}", val_i, grad_i); let truth = vec![0.85, 4.0]; let val_t = metric_all.evaluate(&truth); let grad_t = metric_all.gradient(&truth).unwrap(); println!("ALL POINTS - truth: val={}, grad={:?}", val_t, grad_t); let inv = vec![1.0 / 0.85, -4.0 / 0.85]; let val_inv = metric_all.evaluate(&inv); println!("ALL POINTS - inverse: val={}", val_inv); // Test with 100 random points (like register1 uses at finest level) let metric_rand = MattesMetric::::new( BSpline::new(im_b.view()), BSpline::new(im_a.view()), SamplingArg::Fixed(100), 3, 0.05, )? .with_fixed_mu(crate::metric::FixedMu::new_none(1)); let val_i2 = metric_rand.evaluate(&identity); let grad_i2 = metric_rand.gradient(&identity).unwrap(); println!("RAND 100 - identity: val={}, grad={:?}", val_i2, grad_i2); // Test with 100 random points and 32 bins (like level 0) let metric_32 = MattesMetric::::new( BSpline::new(im_b.view()), BSpline::new(im_a.view()), SamplingArg::Fixed(100), 32, 0.05, )? .with_fixed_mu(crate::metric::FixedMu::new_none(1)); let val_i3 = metric_32.evaluate(&identity); let grad_i3 = metric_32.gradient(&identity).unwrap(); println!( "RAND 100 bins=32 - identity: val={}, grad={:?}", val_i3, grad_i3 ); let eps = 1e-5; for i in 0..2 { let mut p_plus = identity.clone(); let mut p_minus = identity.clone(); p_plus[i] += eps; p_minus[i] -= eps; let num_grad = (metric_all.evaluate(&p_plus) - metric_all.evaluate(&p_minus)) / (2.0 * eps); println!( "numerical d/dmu[{}] = {} (analytical: {})", i, num_grad, grad_i[i] ); } Ok(()) } #[test] fn metric_landscape() -> Result<(), Box> { use crate::bspline::{BSpline, BSplineTrait}; use crate::metric::{MattesMetric, SamplingArg, Sigma}; use algos::ObjectiveFunction; let im_a = julia_image( &[100, 1], &[1.0, 0.0, 0.0, 0.01, 0.0, 0.0], &[99.5, 0.5], &[-0.8, 0.156], ) .slice(s![.., 0]) .mapv(|i| i as f64); let im_b = Transform::new(vec![0.85, 4.0]).interpolate::<1, _, _>(&im_a, &Default::default())?; // Smooth both images with sigma=8 like level 0 let sigma = Sigma::Absolute(vec![8.0]); let sf = sigma.smooth(im_a.view())?; let sm = sigma.smooth(im_b.view())?; let points: Vec> = (0..100).map(|i| vec![i as f64]).collect(); let metric = MattesMetric::::new( BSpline::new(sf.view()), BSpline::new(sm.view()), SamplingArg::FixedAt(points.clone()), 32, 0.05, )? .with_fixed_mu(crate::metric::FixedMu::new_none(1)); // Test several points let test_points: Vec<(&str, Vec)> = vec![ ("identity", vec![1.0, 0.0]), ("truth", vec![0.85, 4.0]), ("neg_trans", vec![1.0, -4.0]), ("scale_0.9", vec![0.9, 0.0]), ("scale_1.1", vec![1.1, 0.0]), ]; println!("=== SMOOTHED ALL 100 integer points bins=32 ==="); for (name, p) in &test_points { let val = metric.evaluate(p); let grad = metric.gradient(p); match grad { Some(g) => println!(" {}: val={:.6}, grad={:?}", name, val, g), None => println!(" {}: val={:.6}, grad=None", name, val), } } // Now test with Fixed(100) random points let metric_rand = MattesMetric::::new( BSpline::new(sf.view()), BSpline::new(sm.view()), SamplingArg::Fixed(100), 32, 0.05, )? .with_fixed_mu(crate::metric::FixedMu::new_none(1)); println!("=== SMOOTHED Random 100 points bins=32 ==="); for (name, p) in &test_points { let val = metric_rand.evaluate(p); let grad = metric_rand.gradient(p); match grad { Some(g) => println!(" {}: val={:.6}, grad={:?}", name, val, g), None => println!(" {}: val={:.6}, grad=None", name, val), } } Ok(()) } #[test] fn register1() -> Result<(), Box> { let fixed = array![ 0.00133107, 0.00138621, 0.00141719, 0.00137673, 0.00133192, 0.00131586, 0.00129737, 0.00131649, 0.00132131, 0.0013368, 0.0013557, 0.00135557, 0.00135903, 0.00136225, 0.00136999, 0.0013847, 0.00137785, 0.00138041, 0.00140829, 0.00142085, 0.0014344, 0.00149646, 0.00156956, 0.00172352, 0.0019473, 0.00226109, 0.00269055, 0.00312467, 0.00343227, 0.00352425, 0.00335073, 0.00305988, 0.00276482, 0.0024797, 0.00223358, 0.00204837, 0.00191672, 0.00183362, 0.00179435, 0.00177568, 0.00177995, 0.00177444, 0.00178801, 0.00180613, 0.00181906, 0.00183161, 0.00185975, 0.00185648, 0.00187759, 0.00188436, 0.00185824, 0.00185461, 0.00188928, 0.00190146, 0.00185707, 0.00185682, 0.00185372, 0.00184588, 0.00183992, 0.0018354, 0.0018307, 0.00181903, 0.00182027, 0.00181646, 0.00180596, 0.00180369, 0.00180493, 0.0018042, 0.00180887, 0.00180575, 0.00180144, 0.00180303, 0.00180743, 0.00180482, 0.0017817, 0.00175849, 0.00174724, 0.00174177, 0.00174569, 0.00176198, 0.00178241, 0.00180581, 0.00184414, 0.00188772, 0.00192021, 0.00195737, 0.00201045, 0.00207885, 0.00216532, 0.00227495, 0.00240301, 0.00254983, 0.00273925, 0.00298603, 0.00329465, 0.00367626, 0.00415677, 0.0047952, 0.00565066, 0.006755, 0.00808893, 0.00958421, 0.0111741, 0.01279892, 0.01434347, 0.0156247, 0.01649142, 0.01690067, 0.01689863, 0.0165402, 0.0158715, 0.01497264, 0.0139421, 0.01286888, 0.01182453, 0.01084795, 0.00994079, 0.00908256, 0.00826134, 0.00748309, 0.00676281, 0.00611948, 0.00556657, 0.00510174, 0.00470918, 0.0043702, 0.00407155, 0.00381196, 0.00359745, 0.00342831, 0.00329565, 0.00318778, 0.00309373, 0.00300738, 0.00292954, 0.00286483, 0.00281634, 0.00278162, 0.00275487, 0.00273168, 0.0027102, 0.0026901, 0.00267163, 0.00265643, 0.00264636, 0.00264188, 0.00264216, 0.00264419, 0.00264316, 0.00263564, 0.00262231, 0.00260767, 0.00259683, 0.00259233, 0.00259301, 0.00259521, 0.0025956, 0.00259316, 0.00258932, 0.00258678, 0.00258754, 0.00259157, 0.00259742, 0.00260376, 0.00261015, 0.00261699, 0.00262534, 0.00263624, 0.00265, 0.00266603, 0.00268315, 0.00269997, 0.00271538, 0.0027293, 0.00274301, 0.00275855, 0.00277762, 0.00280069, 0.00282693, 0.00285474, 0.00288248, 0.00290876, 0.00293257, 0.00295365, 0.0029727, 0.00299094, 0.00300944, 0.0030288, 0.00304906, 0.00306975, 0.00309011, 0.00310917, 0.00312581, 0.00313895, 0.00314813, 0.00315378, 0.00315703, 0.0031592, 0.00316134, 0.00316407, 0.00316761, 0.00317182, 0.00317628, 0.00318021, 0.00318272, 0.00318301, 0.00318065, 0.00317563, 0.00316838, 0.00315962, 0.00315024, 0.00314115, 0.00313327, 0.00312741, 0.0031241, 0.00312351, 0.00312549, 0.00312965, 0.00313552, 0.00314259, 0.00315038, 0.00315846, 0.00316645, 0.00317408, 0.00318122, 0.00318795, 0.0031945, 0.00320115, 0.00320818, 0.0032158, 0.00322408, 0.00323293, 0.00324213, 0.00325147, 0.00326083, 0.00327028, 0.00328004, 0.00329046, 0.00330194, 0.00331489, 0.0033297, 0.00334668, 0.00336601, 0.00338771, 0.00341162, 0.0034374, 0.0034646, 0.00349265, 0.003521, 0.00354909, 0.00357647, 0.00360277, 0.00362773, 0.00365124, 0.00367333, 0.0036942, 0.00371413, 0.00373349, 0.00375265, 0.0037719, 0.00379146, 0.00381142, 0.00383177, 0.00385234, 0.0038729, 0.0038931, 0.00391258, 0.00393098, 0.00394801, 0.00396348, 0.00397735, 0.0039897, 0.00400073, 0.00401069, 0.0040199, 0.00402867, 0.00403726, 0.0040459, 0.00405473, 0.00406382, 0.00407317, 0.00408273, 0.00409241, 0.00410213, 0.00411181, 0.00412144, 0.00413098, 0.00414044, 0.00414982, 0.00415912, 0.00416834, 0.00417744, 0.00418636, 0.00419505, 0.00420339, 0.0042113, 0.00421866, 0.00422539, 0.00423144, 0.00423681, 0.00424153 ]; let q = vec![0.85, 2.0]; let moving = Transform::new(q.clone()).interpolate::<3, _, _>(&fixed, &Default::default())?; // The registration finds T such that im_b(T(x)) = im_a(x), which is the inverse of q let q_inv = Transform::::new(q.clone()).inverse()?; // Use default steps with ASGD — the multi-resolution pyramid converges // reliably for this 1D affine problem. let t = Transform::register(fixed.view(), moving.view(), vec![None; 2], None, None)?; // Compare transformed coordinates of all pixels let mut sum_diff = 0.0; let mut count = 0; let center = fixed .shape() .iter() .map(|&i| (i as f64) / 2.0) .collect::>(); for point in 0..fixed.len() { let t_point = t.transform_point(&[point], ¢er); let e_point = q_inv.transform_point(&[point], ¢er); let diff_sq = (t_point[0] - e_point[0]).powi(2); sum_diff += diff_sq.sqrt(); count += 1; } let mean_diff = sum_diff / count as f64; println!("Our: {:?}\nmean_coord_diff: {:.6}", t, mean_diff); println!("t: {:?}", t); println!("i: {:?}", t.inverse()?); println!("q_inv: {:?}", q_inv.parameters); assert!( t.parameters .iter() .zip(q_inv.parameters.iter()) .map(|(a, b)| (a - b).powi(2)) .sum::() < 1.0 ); assert!(mean_diff < 0.1); Ok(()) } #[test] fn diag_register1_metric() -> Result<(), Box> { use crate::bspline::{BSpline, BSplineTrait}; use crate::metric::{FixedMu, MattesMetric, SamplingArg, Sigma}; use algos::ObjectiveFunction; let im_a = julia_image( &[100, 1], &[1.0, 0.0, 0.0, 0.01, 0.0, 0.0], &[99.5, 0.5], &[-0.8, 0.156], ) .slice(s![.., 0]) .mapv(|i| i as f64); let q = vec![0.85, 2.0]; let im_b = Transform::new(q.clone()).interpolate::<1, _, _>(&im_a, &Default::default())?; let f = Sigma::Absolute(vec![1.0]).smooth(im_a.view())?; let m = Sigma::Absolute(vec![1.0]).smooth(im_b.view())?; let metric = MattesMetric::::new( BSpline::new(f.view()), BSpline::new(m.view()), SamplingArg::Random(100), 32, 0.05, )? .with_fixed_mu(FixedMu::new_none(1)); let q_inv = Transform::::new(q.clone()) .inverse()? .parameters; // sweep scale at the optimal translation, and translation at scale 1.0 let mut best = (f64::MAX, vec![0.0, 0.0]); let mut rows = Vec::new(); for s in (60..=360).map(|i| i as f64 / 100.0) { for t in (-120..=80).map(|i| i as f64 / 40.0) { let p = vec![s, t]; let v = metric.evaluate(&p); if v < best.0 { best = (v, p); } } let v0 = metric.evaluate(&[s, -2.353]); rows.push(format!("s={:.2} t=-2.353 val={:.4}", s, v0)); } println!("BEST on grid: val={:.4} params={:?}", best.0, best.1); println!("rows:"); // line from identity to q_inv and beyond let mut prev = f64::NAN; for (i, alpha) in (0..=40).map(|i| i as f64 / 10.0).enumerate() { let p = vec![1.0 + (q_inv[0] - 1.0) * alpha, 0.0 + q_inv[1] * alpha]; let v = metric.evaluate(&p); println!("line[{}] alpha={:.1} p={:?} val={:.4}", i, alpha, p, v); if !prev.is_nan() && v > prev { println!(" ^ INCREASING here — minimum is before this point"); } prev = v; } // metric value along diagonal from q_inv to the registered optimum for p in [q_inv.clone(), vec![2.0, -1.0], vec![2.5, -0.3]] { println!("eval {:?}: val={:.4}", p, metric.evaluate(&p)); } // numerical vs analytical gradient at identity and q_inv let eps = 1e-5; for p in [vec![1.0, 0.0], q_inv.clone()] { let grad = metric.gradient(&p).unwrap(); let mut num = Vec::new(); for i in 0..2 { let mut pp = p.clone(); let mut pm = p.clone(); pp[i] += eps; pm[i] -= eps; num.push((metric.evaluate(&pp) - metric.evaluate(&pm)) / (2.0 * eps)); } println!("grad at {:?}: analytical={:?} numerical={:?}", p, grad, num); } let (t, results) = Transform::::register_debug( im_a.view(), im_b.view(), vec![None, None], None, None, )?; println!("register_debug t: {:?}", t); for r in results { println!( " step: opt={:?} iters={} converged={} val={}", r.optimal_point, r.iterations, r.converged, r.optimal_value ); } Ok(()) } #[test] fn register2_random_affine() -> Result<(), Box> { use rand::prelude::*; let mut rng = StdRng::seed_from_u64(1337); let shape = [200, 200]; let center = [99.5, 99.5]; let fixed = julia_image( &shape, &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], ¢er, &[-0.8, 0.156], ) .mapv(|i| i as f64); // random rotation ±25° let angle: f64 = rng.random_range(-25.0f64..25.0).to_radians(); let (s, c) = angle.sin_cos(); // random scale 0.85–1.15 let sx = rng.random_range(0.85..1.15); let sy = rng.random_range(0.85..1.15); // small shear let shx: f64 = rng.random_range(-0.1..0.1); let shy: f64 = rng.random_range(-0.1..0.1); // translation ±30 px let tx: f64 = rng.random_range(-30.0..30.0); let ty: f64 = rng.random_range(-30.0..30.0); // [m00, m01, m10, m11, tx, ty] let params = vec![c * sx, -s * sy + shx, s * sx + shy, c * sy, tx, ty]; let transform = Transform::::new_around_center(params.clone(), ¢er); let moving = transform.interpolate::<3, _, _>(fixed.view(), &Default::default())?; let q_inv = transform.inverse()?.parameters; let (t, _steps) = Transform::::register_debug( fixed.view(), moving.view(), vec![None, None, None, None, None, None], None, None, )?; println!("params: {:?}", params); println!("result: {:?}", t.parameters); println!("q_inv: {:?}", q_inv); let sse: f64 = t .parameters .iter() .zip(q_inv.iter()) .map(|(a, b)| (a - b).powi(2)) .sum(); println!("sse: {sse}"); assert!(sse < 1.0); Ok(()) } #[test] fn metric_landscape_2d() -> Result<(), Box> { use crate::bspline::{BSpline, BSplineTrait}; use crate::filter::gaussian_smooth; use crate::metric::{MattesMetric, SamplingArg}; use algos::ObjectiveFunction; let shape = [200, 200]; let center = [99.5, 99.5]; let im_a = julia_image( &shape, &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], ¢er, &[-0.8, 0.156], ) .mapv(|i| i as f64); let rotation = Transform::::from_rotation(f64::PI() / 4.0, ¢er); let im_b = rotation.interpolate::<3, _, _>(im_a.view(), &Default::default())?; let q_inv = rotation.inverse()?.parameters; let p = rotation.parameters.clone(); let identity = vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; let edge = 0.01; // Test at full resolution with different sigma levels (matching Elastix approach) // Elastix does NOT downsample — it smooths at full resolution with // sigma = 0.5 * factor where factor is from the pyramid schedule [8, 4, 2, 1] println!( "=== Full resolution (no downsampling, matching Elastix FixedSmoothingImagePyramid) ===" ); for (level, sigma_val) in [4.0, 2.0, 1.0, 0.5].iter().enumerate() { let f = gaussian_smooth(im_a.view(), &[*sigma_val; 2])?; let m = gaussian_smooth(im_b.view(), &[*sigma_val; 2])?; let bf = BSpline::<0, _>::new(f.view()); let bm = BSpline::<3, _>::new(m.view()); let metric = MattesMetric::new(bf, bm, SamplingArg::Random(3000), 128, edge)?; let mi_id = metric.evaluate(&identity); let mi_p = metric.evaluate(&p); let mi_qinv = metric.evaluate(&q_inv); println!( " Level {} (sigma={:.1}, {}x{}): id={:.4} p={:.4} q_inv={:.4} → {}", level, sigma_val, f.shape()[0], f.shape()[1], -mi_id, -mi_p, -mi_qinv, if -mi_qinv > -mi_p { "Q_INV correct" } else { "P incorrect (landscape inverted!)" } ); } // Verify: at every sigma level, q_inv should have higher MI than p let f4 = gaussian_smooth(im_a.view(), &[4.0, 4.0])?; let m4 = gaussian_smooth(im_b.view(), &[4.0, 4.0])?; let bf4 = BSpline::<0, _>::new(f4.view()); let bm4 = BSpline::<3, _>::new(m4.view()); let metric4 = MattesMetric::new(bf4, bm4, SamplingArg::Random(5000), 128, edge)?; let mi_p_coarse = metric4.evaluate(&p); let mi_qinv_coarse = metric4.evaluate(&q_inv); println!( "\nCoarsest (sigma=4.0): p={:.6} q_inv={:.6}", -mi_p_coarse, -mi_qinv_coarse ); assert!( -mi_qinv_coarse > -mi_p_coarse, "MI landscape is inverted at coarsest level! q_inv={} should be > p={}", -mi_qinv_coarse, -mi_p_coarse ); Ok(()) } #[test] fn angle_sweep() -> Result<(), Box> { use crate::bspline::{BSpline, BSplineTrait}; use crate::filter::gaussian_smooth; use crate::metric::{MattesMetric, SamplingArg}; use algos::ObjectiveFunction; let shape = [200, 200]; let center = [99.5, 99.5]; let im_a = julia_image( &shape, &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], ¢er, &[-0.8, 0.156], ) .mapv(|i| i as f64); let edge = 0.01; // Full resolution, sigma=4.0 — matching elastix FixedSmoothingImagePyramid (no downsampling) for angle_deg in [5.0f64, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0] { let rotation = Transform::::from_rotation(angle_deg.to_radians(), ¢er); let im_b = rotation.interpolate::<3, _, _>(im_a.view(), &Default::default())?; let q_inv = rotation.inverse()?.parameters; let p = rotation.parameters.clone(); let f = gaussian_smooth(im_a.view(), &[4.0, 4.0])?; let m = gaussian_smooth(im_b.view(), &[4.0, 4.0])?; let metric = MattesMetric::new( BSpline::<0, _>::new(f.view()), BSpline::<3, _>::new(m.view()), SamplingArg::Random(3000), 128, edge, )?; let id = vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; let mi_id = metric.evaluate(&id); let mi_p = metric.evaluate(&p); let mi_qi = metric.evaluate(&q_inv); println!( "{:5.1}°: id={:.3} p={:.3} q_inv={:.3} → {}", angle_deg, -mi_id, -mi_p, -mi_qi, if mi_p > mi_qi { "P wins" } else { "Q_INV wins" } ); } Ok(()) } fn read_tiff>(path: P) -> Result, Box> { let mut reader = Decoder::open(File::open(path)?)?; reader.seek_to_image(0)?; let bytes = match reader.read_image()? { DecodingSampleBuffer::U8(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::U16(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::U32(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::U64(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::I8(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::I16(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::I32(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::I64(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::F16(data) => data.into_iter().map(f64::from).collect::>(), DecodingSampleBuffer::F32(data) => { data.into_iter().map(|i| i as f64).collect::>() } DecodingSampleBuffer::F64(data) => data, }; let mut ifd = reader.current_ifd(); let width = ifd.get_tag(Tag::ImageWidth)?.into_u32()? as usize; let height = ifd.get_tag(Tag::ImageLength)?.into_u32()? as usize; Ok(Array2::from_shape_vec((width, height), bytes)?) } #[test] fn register_real_images() -> Result<(), Box> { let fixed = read_tiff("test_files/fixed.tif")?; let moving = read_tiff("test_files/moving.tif")?; // SimpleElastix expected — the registration result is in TIFF-native coordinates. let expected = [ 0.9899559376493817, 0.011269992506480442, 0.017048860489651384, 0.8533772512806085, -12.877028775909979, 4.143118928117275, ]; let t = Transform::::register( fixed.view(), moving.view(), vec![None, None, None, None, None, None], None, None, )?; let expected_transform = Transform::::new(expected.to_vec()); // Compare transformed coordinates of all pixels let shape = fixed.shape(); let mut sum_diff = 0.0; let mut count = 0; let center = shape.iter().map(|&i| (i as f64) / 2.0).collect::>(); for row in 0..shape[0] { for col in 0..shape[1] { let point = [row as f64, col as f64]; let t_point = t.transform_point(&point, ¢er); let e_point = expected_transform.transform_point(&point, ¢er); let diff_sq = (t_point[0] - e_point[0]).powi(2) + (t_point[1] - e_point[1]).powi(2); sum_diff += diff_sq.sqrt(); count += 1; } } let mean_diff = sum_diff / count as f64; println!("Our: {:?}\nmean_coord_diff: {:.6}", t, mean_diff); // let mut tif = IJTiffFile::new( // std::env::home_dir() // .unwrap() // .join("tmp/register_real_images.tif"), // )?; // tif.save(fixed.mapv(|i| i as u16), 0, 0, 0)?; // tif.save( // t.interpolate_par::<3, _, _>(moving.view())? // .mapv(|i| i as u16), // 1, // 0, // 0, // )?; // tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?; assert!(mean_diff < 0.1); Ok(()) } #[test] fn register_real_images2() -> Result<(), Box> { let fixed = read_tiff("test_files/fixed.tif")?; let e = Transform::::new(vec![0.8, 0.0, 0.0, 1.0, 0.0, 0.0]).inverse()?; let moving = e.interpolate::<3, _, _>(fixed.view(), &Default::default())?; let t = Transform::::register( fixed.view(), moving.view(), vec![None, None, None, None, None, None], None, None, )?; let e_inv = e.inverse()?; // Compare transformed coordinates of all pixels let shape = fixed.shape(); let center = shape.iter().map(|&i| (i as f64) / 2.0).collect::>(); let mut sum_diff = 0.0; let mut count = 0; for row in 0..shape[0] { for col in 0..shape[1] { let point = [row as f64, col as f64]; let t_point = t.transform_point(&point, ¢er); let e_point = e_inv.transform_point(&point, ¢er); let diff_sq = (t_point[0] - e_point[0]).powi(2) + (t_point[1] - e_point[1]).powi(2); sum_diff += diff_sq.sqrt(); count += 1; } } let mean_diff = sum_diff / count as f64; println!("Our: {:?}\nmean_coord_diff: {:.6}", t, mean_diff); // let mut tif = IJTiffFile::new( // std::env::home_dir() // .unwrap() // .join("tmp/register_real_images2.tif"), // )?; // tif.save(fixed.mapv(|i| i as u16), 0, 0, 0)?; // tif.save( // t.interpolate_par::<3, _, _>(moving.view())? // .mapv(|i| i as u16), // 1, // 0, // 0, // )?; // tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?; assert!(mean_diff < 0.1); Ok(()) } }