- register2 passing

This commit is contained in:
w.pomp
2026-07-28 11:27:08 +02:00
parent 5b7dc18a4d
commit 9f8764ee00
5 changed files with 305 additions and 77 deletions
+190 -10
View File
@@ -749,22 +749,13 @@ mod tests {
),
crate::register::RegistrationStep::new(
crate::metric::Sigma::Absolute(vec![2.0]),
crate::metric::SamplingArg::FixedAt(all_points.clone()),
crate::metric::SamplingArg::FixedAt(all_points),
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(
@@ -792,6 +783,195 @@ mod tests {
Ok(())
}
#[test]
fn register2_random_affine() -> Result<(), Box<dyn std::error::Error>> {
use rand::prelude::*;
let mut rng = rand::rngs::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],
&center,
&[-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.851.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_with_center(params.clone(), center.to_vec(), shape.to_vec());
let moving = transform.interpolate::<3, _, _>(fixed.view())?;
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],
&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;
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],
&center,
&[-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(), &center);
let im_b = rotation.interpolate::<3, _, _>(im_a.view())?;
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(())
}
#[test]
fn register2_interpolate() -> Result<(), Box<dyn std::error::Error>> {
let shape = [200, 200];