1801 lines
61 KiB
Rust
1801 lines
61 KiB
Rust
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<f64>
|
||
where
|
||
T: AsPrimitive<f64>,
|
||
{
|
||
// [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::<Vec<_>>();
|
||
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::<f64>() + 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<f64>) -> Option<Array2<f64>> {
|
||
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<usize> = (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<D: Dimension = IxDyn> {
|
||
/// flattened NxN rotation matrix + N translation parameters
|
||
pub parameters: Vec<f64>,
|
||
/// error / significance on parameters
|
||
pub dparameters: Vec<f64>,
|
||
#[serde(skip)]
|
||
dimension: PhantomData<D>,
|
||
}
|
||
|
||
impl<D: Dimension> Mul for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: Self) -> Self::Output {
|
||
&self * &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<&Transform<D>> for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: &Transform<D>) -> Self::Output {
|
||
&self * rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: Transform<D>) -> Self::Output {
|
||
self * &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<&Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
#[expect(clippy::suspicious_arithmetic_impl)]
|
||
fn mul(self, rhs: &Transform<D>) -> 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<D: Dimension> Add for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn add(self, rhs: Self) -> Self::Output {
|
||
&self + &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Add<&Transform<D>> for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn add(self, rhs: &Transform<D>) -> Self::Output {
|
||
&self + rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Add<Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn add(self, rhs: Transform<D>) -> Self::Output {
|
||
self + &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Add<&Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn add(self, rhs: &Transform<D>) -> 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<D: Dimension> Sub for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn sub(self, rhs: Self) -> Self::Output {
|
||
&self - &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Sub<&Transform<D>> for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn sub(self, rhs: &Transform<D>) -> Self::Output {
|
||
&self - rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Sub<Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn sub(self, rhs: Transform<D>) -> Self::Output {
|
||
self - &rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Sub<&Transform<D>> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn sub(self, rhs: &Transform<D>) -> 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<D: Dimension> Mul<Transform<D>> for f64 {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: Transform<D>) -> Self::Output {
|
||
rhs * self
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<&Transform<D>> for f64 {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: &Transform<D>) -> Self::Output {
|
||
rhs * self
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<f64> for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn mul(self, rhs: f64) -> Self::Output {
|
||
&self * rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Mul<f64> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
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<D: Dimension> Div<f64> for Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
fn div(self, rhs: f64) -> Self::Output {
|
||
&self / rhs
|
||
}
|
||
}
|
||
|
||
impl<D: Dimension> Div<f64> for &Transform<D> {
|
||
type Output = Transform<D>;
|
||
|
||
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<D: Dimension> Eq for Transform<D> {}
|
||
|
||
impl<D: Dimension> Hash for Transform<D> {
|
||
fn hash<H: Hasher>(&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<D: Dimension> Default for Transform<D> {
|
||
/// 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<D: Dimension> Transform<D> {
|
||
/// parameters: flat NxN part of matrix
|
||
pub fn new(parameters: Vec<f64>) -> Self {
|
||
let dparameters = vec![0.0; parameters.len()];
|
||
Self {
|
||
parameters,
|
||
dparameters,
|
||
dimension: PhantomData,
|
||
}
|
||
}
|
||
|
||
pub fn new_dparameters(parameters: Vec<f64>, dparameters: Vec<f64>) -> Self {
|
||
Self {
|
||
parameters,
|
||
dparameters,
|
||
dimension: PhantomData,
|
||
}
|
||
}
|
||
|
||
pub fn new_around_center(parameters: Vec<f64>, center: &[f64]) -> Self {
|
||
Self::new(parameters).with_translation(center)
|
||
* Self::from_translation(¢er.iter().map(|i| -i).collect::<Vec<_>>())
|
||
}
|
||
|
||
/// find the affine transform which transforms moving into fixed
|
||
pub fn register_affine<'a, F, M, T>(fixed: F, moving: M) -> Result<Self, Error>
|
||
where
|
||
F: AsArray<'a, T, D>,
|
||
M: AsArray<'a, T, D>,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
{
|
||
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<Self, Error>
|
||
where
|
||
F: AsArray<'a, T, D>,
|
||
M: AsArray<'a, T, D>,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
{
|
||
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<Self, Error>
|
||
where
|
||
F: AsArray<'a, T, D>,
|
||
M: AsArray<'a, T, D>,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
{
|
||
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::<Vec<_>>();
|
||
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<Vec<RegistrationStep>>,
|
||
initial_guess: Option<Vec<f64>>,
|
||
) -> Result<Self, Error>
|
||
where
|
||
F: AsArray<'a, T, D>,
|
||
M: AsArray<'a, T, D>,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
G: Into<FixedMu>,
|
||
{
|
||
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<Vec<RegistrationStep>>,
|
||
initial_guess: Option<Vec<f64>>,
|
||
) -> Result<(Self, Vec<RegistrationResult>), Error>
|
||
where
|
||
F: AsArray<'a, T, D>,
|
||
M: AsArray<'a, T, D>,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
G: Into<FixedMu>,
|
||
{
|
||
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<Self, Error> {
|
||
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<f64>
|
||
where
|
||
T: AsPrimitive<f64>,
|
||
{
|
||
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<Array2<f64>, Error>
|
||
where
|
||
T: AsPrimitive<f64>,
|
||
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::<Vec<_>>();
|
||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||
}
|
||
|
||
/// get the matrix defining the transform
|
||
pub fn matrix(&self) -> Array2<f64> {
|
||
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<f64> {
|
||
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<f64>) -> Self {
|
||
self.parameters = parameters;
|
||
self
|
||
}
|
||
|
||
pub fn with_dparameters(mut self, dparameters: Vec<f64>) -> Self {
|
||
self.dparameters = dparameters;
|
||
self
|
||
}
|
||
|
||
pub fn with_matrix(mut self, matrix: ArrayView2<f64>) -> 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<f64>) {
|
||
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<f64>) -> 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<f64>) {
|
||
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<Self, Error> {
|
||
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<Array<f64, D>, Error>
|
||
where
|
||
A: AsArray<'a, T, D>,
|
||
D: Dimension,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
BSpline<B, D>: BSplineTrait<D>,
|
||
{
|
||
let image = image.into();
|
||
let bspline = BSpline::<B, _>::new(&image);
|
||
bspline.interpolate(self, default_value)
|
||
}
|
||
|
||
pub fn interpolate_par<'a, const B: usize, T, A>(
|
||
&self,
|
||
image: A,
|
||
default_value: &DefaultValue,
|
||
) -> Result<Array<f64, D>, Error>
|
||
where
|
||
A: AsArray<'a, T, D>,
|
||
D: Dimension,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
BSpline<B, D>: BSplineTrait<D>,
|
||
{
|
||
let image = image.into();
|
||
let bspline = BSpline::<B, _>::new(&image);
|
||
bspline.interpolate_par(self, default_value)
|
||
}
|
||
|
||
pub fn interpolate_order<'a, T, A>(
|
||
&self,
|
||
image: A,
|
||
order: usize,
|
||
default_value: &DefaultValue,
|
||
) -> Result<Array<f64, D>, Error>
|
||
where
|
||
A: AsArray<'a, T, D>,
|
||
D: Dimension,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
{
|
||
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<Array<f64, D>, Error>
|
||
where
|
||
A: AsArray<'a, T, D>,
|
||
D: Dimension,
|
||
T: 'a + Clone + AsPrimitive<f64>,
|
||
{
|
||
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<D2: Dimension>(self) -> Result<Transform<D2>, 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<IxDyn> {
|
||
Transform {
|
||
parameters: self.parameters,
|
||
dparameters: self.dparameters,
|
||
dimension: PhantomData,
|
||
}
|
||
}
|
||
|
||
fn parameters_unit(ndim: usize) -> Vec<f64> {
|
||
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<D::Larger> {
|
||
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::<Vec<_>>();
|
||
(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<Ix2> {
|
||
/// 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<dyn std::error::Error>> {
|
||
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<dyn std::error::Error>> {
|
||
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<dyn std::error::Error>> {
|
||
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<dyn std::error::Error>> {
|
||
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<dyn std::error::Error>> {
|
||
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::<ndarray::Ix1>::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::<ndarray::Ix1>::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::<ndarray::Ix1>::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<dyn std::error::Error>> {
|
||
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<Vec<f64>> = (0..100).map(|i| vec![i as f64]).collect();
|
||
|
||
let metric = MattesMetric::<ndarray::Ix1>::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<f64>)> = 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::<ndarray::Ix1>::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<dyn std::error::Error>> {
|
||
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::<ndarray::Ix1>::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::<Vec<_>>();
|
||
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::<f64>()
|
||
< 1.0
|
||
);
|
||
assert!(mean_diff < 0.1);
|
||
Ok(())
|
||
}
|
||
#[test]
|
||
fn diag_register1_metric() -> Result<(), Box<dyn std::error::Error>> {
|
||
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::<ndarray::Ix1>::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::<ndarray::Ix1>::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::<ndarray::Ix1>::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<dyn std::error::Error>> {
|
||
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::<Ix2>::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::<Ix2>::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<dyn std::error::Error>> {
|
||
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::<Ix2>::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<dyn std::error::Error>> {
|
||
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::<Ix2>::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<P: AsRef<Path>>(path: P) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
|
||
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::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::U16(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::U32(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::U64(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::I8(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::I16(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::I32(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::I64(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
DecodingSampleBuffer::F16(data) => data.into_iter().map(f64::from).collect::<Vec<_>>(),
|
||
DecodingSampleBuffer::F32(data) => {
|
||
data.into_iter().map(|i| i as f64).collect::<Vec<_>>()
|
||
}
|
||
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<dyn std::error::Error>> {
|
||
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::<Ix2>::register(
|
||
fixed.view(),
|
||
moving.view(),
|
||
vec![None, None, None, None, None, None],
|
||
None,
|
||
None,
|
||
)?;
|
||
|
||
let expected_transform = Transform::<Ix2>::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::<Vec<_>>();
|
||
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<dyn std::error::Error>> {
|
||
let fixed = read_tiff("test_files/fixed.tif")?;
|
||
let e = Transform::<Ix2>::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::<Ix2>::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::<Vec<_>>();
|
||
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(())
|
||
}
|
||
}
|