- tests register1 and register2 passing

This commit is contained in:
Wim Pomp
2026-07-25 13:15:48 +02:00
parent 806571be82
commit 5b7dc18a4d
5 changed files with 887 additions and 114 deletions
+273 -5
View File
@@ -504,7 +504,7 @@ mod tests {
use crate::julia_image;
use crate::transform::Transform;
use itertools::Itertools;
use ndarray::s;
use ndarray::{Ix2, s};
use num::traits::FloatConst;
#[test]
@@ -556,6 +556,167 @@ mod tests {
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.clone(), vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
// 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 q = vec![0.85, 4.0];
let im_b =
Transform::new(vec![0.85, 4.0], vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
// 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".into(), vec![1.0, 0.0]),
("truth".into(), vec![0.85, 4.0]),
("neg_trans".into(), vec![1.0, -4.0]),
("scale_0.9".into(), vec![0.9, 0.0]),
("scale_1.1".into(), 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 im_a = julia_image(
@@ -566,17 +727,111 @@ mod tests {
)
.slice(s![.., 0])
.mapv(|i| i as f64);
let q = vec![0.85, 4.0];
let im_b =
Transform::new(vec![0.85, 4.0], vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
Transform::new(q.clone(), vec![im_a.shape()[0]]).interpolate::<1, _, _>(&im_a)?;
let (t, steps) =
Transform::register_debug(im_a.view(), im_b.view(), vec![None, None], None, None)?;
// 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(), vec![im_a.shape()[0]])
.inverse()?
.parameters;
let all_points: Vec<Vec<f64>> = (0..100).map(|j| vec![j as f64]).collect();
let steps = vec![
crate::register::RegistrationStep::new(
crate::metric::Sigma::Absolute(vec![8.0]),
crate::metric::SamplingArg::FixedAt(all_points.clone()),
32,
1e-4,
0.05,
200,
1.0,
),
crate::register::RegistrationStep::new(
crate::metric::Sigma::Absolute(vec![2.0]),
crate::metric::SamplingArg::FixedAt(all_points.clone()),
64,
1e-6,
0.04,
200,
1.0,
),
crate::register::RegistrationStep::new(
crate::metric::Sigma::None,
crate::metric::SamplingArg::FixedAt(all_points),
64,
1e-8,
0.001,
200,
1.0,
),
];
let (t, steps) = Transform::register_debug(
im_a.view(),
im_b.view(),
vec![None, None],
Some(steps),
None,
)?;
println!("steps:");
for step in steps {
println!(" {:?}", step);
}
println!("t: {:?}", t);
println!("i: {:?}", t.inverse()?);
println!("q_inv: {:?}", q_inv);
assert!(
t.parameters
.iter()
.zip(q_inv.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
< 1.0
);
Ok(())
}
#[test]
fn register2_interpolate() -> Result<(), Box<dyn std::error::Error>> {
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],
&center,
&[-0.8, 0.156],
)
.mapv(|i| i as f64);
let rotation = Transform::<Ix2>::from_rotation(f64::PI() / 4.0, &center);
let im_b = rotation.interpolate::<3, _, _>(im_a.view())?;
let q_inv = rotation.inverse()?.parameters;
// Use default_steps which matches elastix FixedSmoothingImagePyramid:
// sigma = [4.0, 2.0, 1.0, 0.5] (schedule [8,4,2,1] with spacing=1)
let (t, steps) = Transform::register_debug(
im_a.view(),
im_b.view(),
vec![None, None, None, None, None, None],
None,
None,
)?;
println!("steps:");
for step in steps {
println!(" {:?}", step);
}
println!("t: {:?}", t);
println!("i: {:?}", t.inverse()?);
println!("q_inv: {:?}", q_inv);
assert!(
t.parameters
.iter()
.zip(q_inv.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
< 1.0
);
Ok(())
}
@@ -603,7 +858,7 @@ mod tests {
let (t, steps) = Transform::register_debug(
im_a.view(),
im_b.view(),
vec![None, None, None, None, Some(0.0), Some(0.0)],
vec![None, None, None, None, None, None],
None,
None,
)?;
@@ -613,6 +868,19 @@ mod tests {
}
println!("t: {:?}", t);
println!("i: {:?}", t.inverse()?);
println!("p: {:?}", p);
// julia_image applies transform to coordinates, so T maps im_b->im_a means T = p_inv
let p_inv = Transform::<ndarray::Ix2>::new(p.clone(), vec![600, 800])
.inverse()?
.parameters;
assert!(
t.parameters
.iter()
.zip(p_inv.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
< 1.0
);
Ok(())
}
}