- fix default value

- implement dparameter estimation
This commit is contained in:
w.pomp committed 2026-10-27 16:14:15 +01:00
1 parent 9257dd1fcf
commit 0049267997
7 files changed
+672 -73

No files matched your search

+34 -16
View File
@@ -48,11 +48,18 @@ where
.collect()
}
/// Calculate the inverse of a square matrix using LU factorization with partial pivoting.
fn matrix_inverse(matrix: Array2<f64>) -> Array2<f64> {
/// 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();
@@ -64,7 +71,9 @@ fn matrix_inverse(matrix: Array2<f64>) -> Array2<f64> {
pivot_row = i;
}
}
assert_ne!(lu[[pivot_row, k]], 0.0, "matrix is singular");
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]);
@@ -102,7 +111,7 @@ fn matrix_inverse(matrix: Array2<f64>) -> Array2<f64> {
}
}
inv
Some(inv)
}
/// a struct describing the transform
@@ -403,8 +412,8 @@ impl<D: Dimension> Transform<D> {
}
pub fn new_around_center(parameters: Vec<f64>, center: &[f64]) -> Self {
Self::new(parameters).with_translation(&center.iter().map(|i| -i).collect::<Vec<_>>())
* Self::from_translation(center)
Self::new(parameters).with_translation(center)
* Self::from_translation(&center.iter().map(|i| -i).collect::<Vec<_>>())
}
/// find the affine transform which transforms moving into fixed
@@ -796,7 +805,7 @@ impl<D: Dimension> Transform<D> {
/// get the inverse transform
pub fn inverse(&self) -> Result<Self, Error> {
let matrix = self.matrix();
let inverse = matrix_inverse(matrix);
let inverse = try_matrix_inverse(matrix).ok_or(Error::SingularMatrix)?;
let parameters = inverse
.slice(s![..self.ndim(), ..self.ndim()])
.iter()
@@ -811,7 +820,11 @@ impl<D: Dimension> Transform<D> {
})
}
pub fn interpolate<'a, const B: usize, T, A>(&self, image: A, default_value: &DefaultValue) -> Result<Array<f64, D>, Error>
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,
@@ -994,7 +1007,7 @@ impl Transform<Ix2> {
#[cfg(test)]
mod tests {
use super::matrix_inverse;
use super::try_matrix_inverse;
use crate::julia_image;
use crate::transform::File;
use crate::transform::Transform;
@@ -1011,7 +1024,7 @@ mod tests {
let a = array![[4.0, 7.0], [2.0, 6.0]];
let expected = array![[0.6, -0.7], [-0.2, 0.4]];
let got = matrix_inverse(a.clone());
let got = try_matrix_inverse(a.clone()).expect("matrix should not be singular");
assert!(
got.iter()
.zip_eq(expected.iter())
@@ -1020,7 +1033,7 @@ mod tests {
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 = matrix_inverse(b.clone());
let got = try_matrix_inverse(b.clone()).expect("matrix should not be singular");
assert!(
got.iter()
.zip_eq(expected.iter())
@@ -1029,13 +1042,16 @@ mod tests {
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 = matrix_inverse(c);
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(())
}
@@ -1186,7 +1202,8 @@ mod tests {
)
.slice(s![.., 0])
.mapv(|i| i as f64);
let im_b = Transform::new(vec![0.85, 4.0]).interpolate::<1, _, _>(&im_a, &Default::default())?;
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]);
@@ -1295,7 +1312,8 @@ mod tests {
];
let q = vec![0.85, 2.0];
let moving = Transform::new(q.clone()).interpolate::<3, _, _>(&fixed, &Default::default())?;
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()?;
@@ -1520,11 +1538,11 @@ mod tests {
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)
// 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) ==="
"=== 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])?;