- 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

+271 -1
View File
@@ -9,7 +9,7 @@ use itertools::Itertools;
use ndarray::{Array, Array1, Array2, Array3, AsArray, Axis, Dimension, s};
use num::cast::AsPrimitive;
use rand::RngExt;
use rayon::iter::{ParallelIterator, IntoParallelIterator};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::ops::Deref;
@@ -64,6 +64,22 @@ impl FixedMu {
.collect()
}
/// combine the dparameters of the variable parameters into a complete
/// parameter set, with no uncertainty (0) on the fixed parameters
pub fn combine_dparameters(&self, dparameters: &[f64]) -> Vec<f64> {
debug_assert_eq!(dparameters.len(), self.len_variable);
let mut dparameters_iter = dparameters.iter();
self.iter()
.map(|i| {
if i.is_some() {
0.0
} else {
*dparameters_iter.next().unwrap()
}
})
.collect()
}
/// extract mu from complete parameter set
pub fn extract_variable(&self, mu: &[f64]) -> Vec<f64> {
self.iter()
@@ -203,6 +219,24 @@ fn image_jacobian(point: &[f64], center: &[f64], image_derivative: &[f64]) -> Ve
.collect()
}
/// Local Gauss-Newton statistics of the intensity fit
/// `fixed(x) = moving(T(x)) + noise` at a parameter point.
///
/// `hessian` is the weighted normal matrix `JᵀWJ` and `variance` is the
/// weighted residual variance `Σ w r² / (Σ w − q − 1)`, with `q` the number of
/// variable parameters. The extra degree of freedom accounts for the weighted
/// mean intensity offset that is removed from the residuals. The parameter
/// covariance of the fit is `variance * hessian⁻¹`.
#[derive(Clone, Debug)]
pub struct ResidualFit {
/// weighted normal matrix `JᵀWJ`
pub hessian: Vec<Vec<f64>>,
/// residual variance (mean squared residual, corrected for the dof)
pub variance: f64,
/// number of sample points that contributed
pub n_samples: usize,
}
pub struct MattesMetric<D>
where
D: Dimension,
@@ -439,6 +473,93 @@ where
}
Ok(())
}
/// Compute the Gauss-Newton normal matrix and the residual variance of the
/// intensity fit at `point`, using the same sample points and edge
/// weighting as [`Self::eval`].
///
/// Residuals are `fixed(x) - moving(T(x))`, so a well-registered image pair
/// gives a small variance. Returns `None` when interpolation fails or when
/// too few samples contribute to estimate the residual variance.
pub fn residual_fit(&self, point: &[f64]) -> Option<ResidualFit> {
let complete_mu = self.fixed_mu.combine(point);
let idx = self.sampling.index();
let shape = self.shape.as_slice();
let shape_f = self.shape.iter().map(|s| s - 0.5).collect::<Vec<_>>();
let center = self.center.as_slice();
let e = 1.0 / self.edge;
let n_params = point.len();
let mut hessian = vec![vec![0.0; n_params]; n_params];
let mut sum_w = 0.0;
let mut sum_wr = 0.0;
let mut sum_wr2 = 0.0;
let mut n_samples = 0;
let mut mem_f = self.fixed.get_mem();
let mut mem_m = self.moving.get_mem();
for u in idx {
let v = transform_point(&u, center, &complete_mu);
if !shape_f
.iter()
.zip_eq(v.iter())
.all(|(s, x)| (-0.5 <= *x) && (x <= s))
{
continue;
}
let f = self
.fixed
.evaluate_at_continuous_index(&u, &mut mem_f)
.ok()?;
let (m, d) = self
.moving
.evaluate_value_and_derivative_at_continuous_index(&v, &mut mem_m)
.ok()?;
// same smooth edge weighting as in `eval`
let mut w = 1.0;
for (s, x) in shape.iter().zip_eq(&v) {
if e * (x + 0.5) < *s {
let k = 3.0 * e * (x + 0.5) / s - 1.5;
w *= square_bspline_integral(k);
} else if e * (x + 0.5) > (e - 1.0) * s {
let k = 3.0 * e * (s - x - 0.5) / s - 1.5;
w *= square_bspline_integral(k);
}
}
if w <= 0.0 || !w.is_finite() {
continue;
}
let r = f - m;
sum_w += w;
sum_wr += w * r;
sum_wr2 += w * r * r;
n_samples += 1;
let j = self
.fixed_mu
.extract_variable(&image_jacobian(&u, center, &d));
for (a, ja) in j.iter().enumerate() {
for (b, jb) in j.iter().enumerate() {
hessian[a][b] += w * ja * jb;
}
}
}
// the residual variance needs at least one degree of freedom
if n_samples == 0 || sum_w <= n_params as f64 + 1.0 {
return None;
}
// subtract the weighted mean intensity mismatch (e.g. a constant
// offset between the images) before estimating the variance
let mean_r = sum_wr / sum_w;
let sse = (sum_wr2 - sum_w * mean_r * mean_r).max(0.0);
// q parameters plus the mean intensity offset that was removed
let variance = sse / (sum_w - n_params as f64 - 1.0);
if !variance.is_finite() {
return None;
}
Some(ResidualFit {
hessian,
variance,
n_samples,
})
}
}
impl<D> ObjectiveFunction<f64> for MattesMetric<D>
@@ -454,6 +575,39 @@ where
self.eval(point).unwrap();
Some(self.metric.borrow().derivative.clone())
}
/// The hessian is computed numerically: central finite differences of
/// `gradient` with respect to each parameter (2 * n full evaluations).
/// `MattesMetric` only accumulates first-order terms, so an analytic hessian
/// is not available. The result is symmetrized, which also damps the small
/// asymmetry introduced by finite differencing.
fn hessian(&self, point: &[f64]) -> Option<Vec<Vec<f64>>> {
let n = point.len();
let mut hessian = vec![vec![0.0; n]; n];
let mut p = point.to_vec();
for j in 0..n {
let h = 1e-5 * f64::max(1.0, point[j].abs());
p[j] += h;
self.eval(&p).unwrap();
let g_plus = self.metric.borrow().derivative.clone();
p[j] -= 2.0 * h;
self.eval(&p).unwrap();
let g_minus = self.metric.borrow().derivative.clone();
p[j] += h;
for i in 0..n {
hessian[i][j] = (g_plus[i] - g_minus[i]) / (2.0 * h);
}
}
#[expect(clippy::needless_range_loop)]
for i in 0..n {
for j in (i + 1)..n {
let w = (hessian[i][j] + hessian[j][i]) / 2.0;
hessian[i][j] = w;
hessian[j][i] = w;
}
}
Some(hessian)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -587,6 +741,122 @@ mod tests {
Ok(())
}
#[test]
fn residual_fit_1d() -> Result<(), Box<dyn std::error::Error>> {
let a = array![1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 4.0, 2.0, 0.0, 1.0];
// b is a shifted right by 3 (padded on the left with the first value)
let b = array![1.0, 1.0, 1.0, 1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0, 4.0];
let points = (0..11).map(|i| vec![i as f64]).collect::<Vec<_>>();
let m = MattesMetric::new_from_arrays(
a.view(),
b.view(),
SamplingArg::FixedAt(points),
16,
0.05,
)?
.with_fixed_mu(vec![Some(1.0), None]);
// fixed(x) = moving(x + 3): no residual
let fit = m.residual_fit(&[3.0]).expect("residual fit");
assert!(fit.n_samples > 0);
assert!(fit.hessian[0][0] > 0.0);
assert!(fit.variance < 1e-12);
// away from the optimum the residual variance must grow
let worse = m.residual_fit(&[0.0]).expect("residual fit");
assert!(worse.variance > fit.variance);
Ok(())
}
#[test]
fn hessian_1d() -> Result<(), Box<dyn std::error::Error>> {
let im_a = array![0.0, 0.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.0, 0.0];
let im_b = array![1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
let a = gaussian_smooth(im_a.view(), &[2.0])?;
let b = gaussian_smooth(im_b.view(), &[2.0])?;
let points = vec![
vec![3.0],
vec![4.0],
vec![5.0],
vec![6.0],
vec![7.0],
vec![8.0],
];
let m = MattesMetric::new_from_arrays(
a.view(),
b.view(),
SamplingArg::FixedAt(points),
5,
0.05,
)?
.with_fixed_mu(vec![Some(1.0), None]);
let p = -3.49;
let h = m.hessian(&[p]).unwrap();
assert_eq!(h.len(), 1);
assert_eq!(h[0].len(), 1);
// cross-check against the second central difference of `evaluate`
let step = 1e-4;
let fd = (m.evaluate(&[p + step]) - 2.0 * m.evaluate(&[p]) + m.evaluate(&[p - step]))
/ (step * step);
println!("hessian = {:?}\nfinite diff = {:?}", h[0][0], fd);
assert!((h[0][0] - fd).abs() < 1e-3 + 1e-2 * fd.abs());
Ok(())
}
#[test]
fn hessian_2d() -> Result<(), Box<dyn std::error::Error>> {
let a = Array2::from_shape_fn((12, 12), |(i, j)| i as f64 + 0.7 * j as f64);
let b = Array2::from_shape_fn((12, 12), |(i, j)| 0.5 * i as f64 + 24.0 - 0.7 * j as f64);
let a = gaussian_smooth(a.view(), &[2.0, 2.0])?;
let b = gaussian_smooth(b.view(), &[2.0, 2.0])?;
let points = (0..12)
.flat_map(|i| (0..12).map(move |j| vec![i as f64, j as f64]))
.step_by(2)
.collect::<Vec<_>>();
let m = MattesMetric::new_from_arrays(
a.view(),
b.view(),
SamplingArg::FixedAt(points),
7,
0.05,
)?;
let n = m.fixed_mu().len_variable();
assert_eq!(n, 2 * 2 + 2);
let p = vec![0.5, -0.3, 0.2, 0.7, -0.4, 0.3];
let h = m.hessian(&p).unwrap();
assert_eq!(h.len(), n);
assert!(h.iter().all(|row| row.len() == n));
// symmetrized by construction
for (i, row) in h.iter().enumerate() {
for (j, v) in row.iter().enumerate() {
assert_eq!(*v, h[j][i]);
}
}
// cross-check each diagonal entry against the 2nd central difference of `evaluate`
let step = 1e-4;
for j in 0..n {
let mut pp = p.clone();
let mut pm = p.clone();
pp[j] += step;
pm[j] -= step;
let fd = (m.evaluate(&pp) - 2.0 * m.evaluate(&p) + m.evaluate(&pm)) / (step * step);
println!("diagonal[{j}] = {:?}\nfinite diff = {:?}", h[j][j], fd);
assert!(
(h[j][j] - fd).abs() < 1e-3 + 1e-2 * fd.abs(),
"diagonal entry {j}"
);
}
Ok(())
}
#[test]
fn test_square_bspline_integral() -> Result<(), Box<dyn std::error::Error>> {
let x = Array1::linspace(-2.0, 2.0, 500);