- fix default value
- implement dparameter estimation
This commit is contained in:
1 parent
9257dd1fcf
commit
0049267997
7 files changed
+672
-73
No files matched your search
+271
-1
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user