- register2 passing
This commit is contained in:
+190
-10
@@ -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],
|
||||
¢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_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],
|
||||
¢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())?;
|
||||
|
||||
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())?;
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user