register_real_images test working

This commit is contained in:
Wim Pomp
2026-07-29 16:27:21 +02:00
parent 0f315e66ca
commit e677e7400b
6 changed files with 124 additions and 70 deletions
+1
View File
@@ -31,6 +31,7 @@ tiffwrite = "2026.6.0"
[dev-dependencies]
tempfile = "3"
tiff = "0.11"
[profile.release]
debug = true
+13 -7
View File
@@ -167,13 +167,19 @@ impl Sampling {
fn index(&self) -> Vec<Vec<f64>> {
match self {
Self::Fixed(index) => index.clone(),
Self::Random((n_samples, shape, _cached)) => rand::rng()
.random_iter::<f64>()
.take(n_samples * shape.len())
.chunks(shape.len())
.into_iter()
.map(|c| c.zip_eq(shape.iter()).map(|(i, s)| i * s - 0.5).collect())
.collect(),
Self::Random((n_samples, shape, cached)) => {
let mut cache = cached.borrow_mut();
if cache.is_empty() {
*cache = rand::rng()
.random_iter::<f64>()
.take(n_samples * shape.len())
.chunks(shape.len())
.into_iter()
.map(|c| c.zip_eq(shape.iter()).map(|(i, s)| i * s - 0.5).collect())
.collect();
}
cache.clone()
}
}
}
}
+17 -27
View File
@@ -59,35 +59,25 @@ impl RegistrationStep {
/// Default registration steps matching elastix `FixedSmoothingImagePyramid`.
///
/// Elastix uses `MultiResolutionGaussianSmoothingPyramidImageFilter` which applies
/// Gaussian smoothing with σ = 0.5 × factor at **full resolution** — images are
/// **NOT downsampled**. See: `itkMultiResolutionGaussianSmoothingPyramidImageFilter.hxx`
///
/// Schedule [8, 4, 2, 1] means σ = [4.0, 2.0, 1.0, 0.5] (spacing=1).
/// MaximumNumberOfIterations: 256 (matching SimpleElastix default affine).
/// Smooth-only multi-resolution pyramid (no downsampling).
/// Schedule [8, 4, 2, 1] → σ = [4.0, 2.0, 1.0, 0.5] at spacing=1.
/// Uses cached random sampling (NewSamplesEveryIteration=false per level).
pub fn default_steps(ndim: usize, n: usize) -> Vec<Self> {
let sigma_schedule: Vec<f64> = vec![4.0, 2.0, 1.0, 0.5];
let nlevels = sigma_schedule.len();
let mut steps = Vec::new();
for i in 0..nlevels {
let fraction = 1.0 / 2.0_f64.powi((nlevels - 1 - i) as i32);
let samples = (n as f64 * fraction)
.sqrt()
.max(n as f64 * fraction / 10.0)
.max(2048.0) as usize;
steps.push(RegistrationStep::new(
Sigma::Absolute(vec![sigma_schedule[i]; ndim]),
SamplingArg::Random(samples),
32,
1e-6,
0.05,
256,
1.0,
));
}
steps
let n_samples = (n / 16).clamp(2048, 8192);
sigma_schedule
.iter()
.map(|&s| Self {
sigma: Sigma::Absolute(vec![s; ndim]),
samples: SamplingArg::Random(n_samples),
n_bins: 32,
tolerance: 1e-6,
edge: 0.05,
max_iterations: 1500,
learning_rate: 4.0,
downsample: 1,
})
.collect()
}
}
+93 -36
View File
@@ -198,14 +198,27 @@ impl<D: Dimension> Transform<D> {
/// 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) -> Result<Self, Error>
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>,
{
Registration::new(fixed_mu).register(fixed, moving)
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
@@ -504,8 +517,13 @@ mod tests {
use crate::julia_image;
use crate::transform::Transform;
use itertools::Itertools;
use ndarray::{Ix2, s};
use ndarray::{Array2, Ix2, s};
use num::traits::FloatConst;
use std::fs::File;
use std::path::Path;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
use tiffwrite::IJTiffFile;
#[test]
fn interpolate() -> Result<(), Box<dyn std::error::Error>> {
@@ -736,39 +754,9 @@ mod tests {
.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),
64,
1e-6,
0.04,
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);
}
// Use default steps with ASGD — the multi-resolution pyramid converges
// reliably for this 1D affine problem.
let t = Transform::register(im_a.view(), im_b.view(), vec![None; 2], None, None)?;
println!("t: {:?}", t);
println!("i: {:?}", t.inverse()?);
println!("q_inv: {:?}", q_inv);
@@ -1063,4 +1051,73 @@ mod tests {
);
Ok(())
}
fn read_tiff<P: AsRef<Path>>(path: P) -> Result<Array2<f64>, Box<dyn std::error::Error>> {
let mut reader = Decoder::new(File::open(path)?)?;
reader.seek_to_image(0)?;
let bytes = match reader.read_image()? {
DecodingResult::U8(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::U16(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::U32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::U64(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::I8(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::I16(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::I32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::I64(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::F16(data) => data.into_iter().map(|i| f64::from(i)).collect::<Vec<_>>(),
DecodingResult::F32(data) => data.into_iter().map(|i| i as f64).collect::<Vec<_>>(),
DecodingResult::F64(data) => data,
};
let width = reader.get_tag(Tag::ImageWidth)?.into_u32()? as usize;
let height = reader.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 sse = t
.parameters
.iter()
.zip(expected.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>();
let max_err = t
.parameters
.iter()
.zip(expected.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f64, f64::max);
println!("Our: {:?} max_err: {:.4} sse: {:.4}", t, max_err, sse);
assert!(max_err < 1.0);
assert!(sse < 1.0);
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::<1, _, _>(moving.view())?.mapv(|i| i as u16), 1, 0, 0)?;
tif.save(moving.mapv(|i| i as u16), 2, 0, 0)?;
Ok(())
}
}
Binary file not shown.
Binary file not shown.