- 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
+287
-10
@@ -1,10 +1,10 @@
|
||||
use crate::bspline::{BSpline, BSplineTrait};
|
||||
use crate::error::Error;
|
||||
use crate::metric::{FixedMu, MattesMetric, SamplingArg, Sigma};
|
||||
use crate::metric::{FixedMu, MattesMetric, ResidualFit, SamplingArg, Sigma};
|
||||
use crate::optimize::{AsgdConfig, asgd_minimize, lbfgs_minimize};
|
||||
use crate::transform::Transform;
|
||||
use crate::transform::{Transform, try_matrix_inverse};
|
||||
use algos::{ObjectiveFunction, OptimizationConfig};
|
||||
use ndarray::{AsArray, Dimension};
|
||||
use ndarray::{Array2, AsArray, Dimension};
|
||||
use num::cast::AsPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
@@ -286,8 +286,11 @@ impl<D: Dimension> Registration<D> {
|
||||
.unwrap_or_else(|| RegistrationStep::default_steps(ndim, n));
|
||||
|
||||
let mut p_var = self.initial_guess.as_ref().cloned().unwrap_or_else(|| {
|
||||
Self::compute_initial_transform(fixed.shape(), moving.shape(), ndim)
|
||||
let full = Self::compute_initial_transform(fixed.shape(), moving.shape(), ndim);
|
||||
self.fixed_mu.extract_variable(&full)
|
||||
});
|
||||
let mut last_metric: Option<MattesMetric<D>> = None;
|
||||
let mut last_result: Option<algos::OptimizationResult<f64>> = None;
|
||||
|
||||
for RegistrationStep {
|
||||
sigma,
|
||||
@@ -324,13 +327,23 @@ impl<D: Dimension> Registration<D> {
|
||||
&scales,
|
||||
);
|
||||
|
||||
if let Some(result) = optimization_result
|
||||
last_metric = Some(metric);
|
||||
last_result = optimization_result;
|
||||
if let Some(result) = last_result.as_ref()
|
||||
&& result.optimal_point.iter().all(|i| i.is_finite())
|
||||
{
|
||||
p_var = result.optimal_point;
|
||||
p_var = result.optimal_point.clone();
|
||||
}
|
||||
}
|
||||
Ok(Transform::<D>::new(self.fixed_mu.combine(p_var.as_slice())))
|
||||
let mut transform = Transform::<D>::new(self.fixed_mu.combine(p_var.as_slice()));
|
||||
if let (Some(metric), Some(result)) = (&last_metric, last_result.as_ref())
|
||||
&& result.converged
|
||||
&& let Some(fit) = metric.residual_fit(&p_var)
|
||||
{
|
||||
let dparameters = dparameters_from_residual_fit(&fit);
|
||||
transform = transform.with_dparameters(self.fixed_mu.combine_dparameters(&dparameters));
|
||||
}
|
||||
Ok(transform)
|
||||
}
|
||||
|
||||
fn optimize_metric<M: ObjectiveFunction<f64>>(
|
||||
@@ -389,9 +402,12 @@ impl<D: Dimension> Registration<D> {
|
||||
.unwrap_or_else(|| RegistrationStep::default_steps(ndim, n));
|
||||
|
||||
let mut p_var = self.initial_guess.as_ref().cloned().unwrap_or_else(|| {
|
||||
Self::compute_initial_transform(fixed.shape(), moving.shape(), ndim)
|
||||
let full = Self::compute_initial_transform(fixed.shape(), moving.shape(), ndim);
|
||||
self.fixed_mu.extract_variable(&full)
|
||||
});
|
||||
let mut p = vec![0.0; n * n + n];
|
||||
let mut p = vec![0.0; ndim * ndim + ndim];
|
||||
let mut last_metric: Option<MattesMetric<D>> = None;
|
||||
let mut last_result: Option<algos::OptimizationResult<f64>> = None;
|
||||
|
||||
let mut registration_results = Vec::new();
|
||||
for RegistrationStep {
|
||||
@@ -461,6 +477,8 @@ impl<D: Dimension> Registration<D> {
|
||||
.fixed_mu()
|
||||
.combine(&optimization_result.optimal_point);
|
||||
}
|
||||
last_metric = Some(metric);
|
||||
last_result = Some(optimization_result.clone());
|
||||
|
||||
registration_results.push(RegistrationResult {
|
||||
sigma_fixed: sigma.sigma(f_orig_shape.as_slice()),
|
||||
@@ -477,6 +495,265 @@ impl<D: Dimension> Registration<D> {
|
||||
converged: optimization_result.converged,
|
||||
});
|
||||
}
|
||||
Ok((Transform::<D>::new(p), registration_results))
|
||||
let mut transform = Transform::<D>::new(p);
|
||||
if let (Some(metric), Some(result)) = (&last_metric, last_result.as_ref())
|
||||
&& result.converged
|
||||
&& let Some(fit) = metric.residual_fit(&p_var)
|
||||
{
|
||||
let dparameters = dparameters_from_residual_fit(&fit);
|
||||
transform = transform.with_dparameters(self.fixed_mu.combine_dparameters(&dparameters));
|
||||
}
|
||||
Ok((transform, registration_results))
|
||||
}
|
||||
}
|
||||
|
||||
/// Diagonal of the inverse of a symmetric positive-definite matrix.
|
||||
///
|
||||
/// Non-finite entries are treated as zero. Returns `None` if the matrix is
|
||||
/// singular, because a singular normal matrix means the corresponding
|
||||
/// directions are not determined by the data. Diagonal entries of the inverse
|
||||
/// that are not finite and positive are reported as 0.
|
||||
fn variance_from_hessian(hessian: &[Vec<f64>]) -> Option<Vec<f64>> {
|
||||
let n = hessian.len();
|
||||
if hessian.iter().any(|row| row.len() != n) {
|
||||
return None;
|
||||
}
|
||||
let mut data: Vec<f64> = Vec::with_capacity(n * n);
|
||||
data.extend(
|
||||
hessian
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|v| if v.is_finite() { *v } else { 0.0 }),
|
||||
);
|
||||
let matrix = Array2::from_shape_vec((n, n), data).ok()?;
|
||||
let inv = try_matrix_inverse(matrix)?;
|
||||
Some(
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let v = inv[[i, i]];
|
||||
if v.is_finite() && v > 0.0 { v } else { 0.0 }
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute parameter uncertainty (standard error, the `dparameters` field) from
|
||||
/// the intensity fit at the optimum.
|
||||
///
|
||||
/// The moving image is modelled as `moving(T(x)) = fixed(x) + noise` with
|
||||
/// independent residuals. Around the optimum this is a linear least-squares
|
||||
/// problem with Jacobian `J`, so the parameter covariance is
|
||||
///
|
||||
/// ```text
|
||||
/// Cov = σ² (JᵀWJ)⁻¹
|
||||
/// ```
|
||||
///
|
||||
/// with `σ²` the residual variance and `JᵀWJ` the weighted Gauss-Newton normal
|
||||
/// matrix, both computed by [`MattesMetric::residual_fit`]. This is the usual
|
||||
/// standard error of a nonlinear least-squares fit: it is calibrated in
|
||||
/// parameter units and grows when the images fit poorly or when the image
|
||||
/// gradients are small.
|
||||
///
|
||||
/// This deliberately does *not* use the inverse Hessian of `-MI`: mutual
|
||||
/// information is not a likelihood, so its curvature has an arbitrary scale
|
||||
/// (it changes with the number of histogram bins and the Parzen width) and
|
||||
/// `H⁻¹` is not a standard error.
|
||||
///
|
||||
/// Each uncertainty is the square root of the corresponding diagonal. If the
|
||||
/// normal matrix is singular or the residuals cannot be estimated, the result
|
||||
/// is 0 for the affected parameters, meaning "not estimated".
|
||||
pub(crate) fn dparameters_from_residual_fit(fit: &ResidualFit) -> Vec<f64> {
|
||||
let n = fit.hessian.len();
|
||||
if !fit.variance.is_finite() || fit.variance <= 0.0 {
|
||||
return vec![0.0; n];
|
||||
}
|
||||
variance_from_hessian(&fit.hessian)
|
||||
.map(|variance| {
|
||||
variance
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let v = fit.variance * v;
|
||||
if v.is_finite() && v > 0.0 {
|
||||
v.sqrt()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![0.0; n])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ndarray::{Array1, Ix1};
|
||||
|
||||
#[test]
|
||||
fn dparameters_from_residual_fit_1d() {
|
||||
// Cov = variance / (JᵀWJ), so with JᵀWJ = 4 and variance = 1/4 the
|
||||
// standard error is sqrt(1/16) = 1/4.
|
||||
let fit = ResidualFit {
|
||||
hessian: vec![vec![4.0]],
|
||||
variance: 0.25,
|
||||
n_samples: 10,
|
||||
};
|
||||
let d = dparameters_from_residual_fit(&fit);
|
||||
assert!((d[0] - 0.25).abs() < 1e-12);
|
||||
|
||||
// the covariance scales linearly with the residual variance
|
||||
let fit = ResidualFit {
|
||||
variance: 1.0,
|
||||
..fit
|
||||
};
|
||||
let d = dparameters_from_residual_fit(&fit);
|
||||
assert!((d[0] - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dparameters_from_residual_fit_2d() {
|
||||
// inverse of [[2, 1], [1, 2]] has diagonal 2/3
|
||||
let fit = ResidualFit {
|
||||
hessian: vec![vec![2.0, 1.0], vec![1.0, 2.0]],
|
||||
variance: 0.5,
|
||||
n_samples: 20,
|
||||
};
|
||||
let d = dparameters_from_residual_fit(&fit);
|
||||
let expected: f64 = (1.0f64 / 3.0).sqrt();
|
||||
assert!((d[0] - expected).abs() < 1e-9);
|
||||
assert!((d[1] - expected).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dparameters_from_residual_fit_not_estimated() {
|
||||
// a singular normal matrix leaves the parameters undetermined; there
|
||||
// is no diagonal fallback
|
||||
let singular = ResidualFit {
|
||||
hessian: vec![vec![2.0, -2.0], vec![-2.0, 2.0]],
|
||||
variance: 1.0,
|
||||
n_samples: 20,
|
||||
};
|
||||
assert_eq!(dparameters_from_residual_fit(&singular), vec![0.0, 0.0]);
|
||||
|
||||
// a non-positive-definite or non-finite normal matrix is not
|
||||
// informative either
|
||||
for hessian in [vec![vec![0.0]], vec![vec![-4.0]], vec![vec![f64::NAN]]] {
|
||||
let fit = ResidualFit {
|
||||
hessian,
|
||||
variance: 1.0,
|
||||
n_samples: 10,
|
||||
};
|
||||
assert_eq!(dparameters_from_residual_fit(&fit), vec![0.0]);
|
||||
}
|
||||
|
||||
// no residual variance means no standard error is available
|
||||
for variance in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
let fit = ResidualFit {
|
||||
hessian: vec![vec![4.0]],
|
||||
variance,
|
||||
n_samples: 10,
|
||||
};
|
||||
assert_eq!(dparameters_from_residual_fit(&fit), vec![0.0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_dparameters() {
|
||||
let fixed_mu: FixedMu = vec![Some(1.0), None, None].into();
|
||||
assert_eq!(
|
||||
fixed_mu.combine_dparameters(&[0.1, 0.2]),
|
||||
vec![0.0, 0.1, 0.2]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_deterministic_dparameters() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// dense, non-periodic 1D signal; b is a, shifted right by 2: the
|
||||
// optimal translation is +2
|
||||
// incommensurate frequencies keep the signal effectively aperiodic,
|
||||
// so the mutual information peaks at the true shift
|
||||
let n = 256;
|
||||
let a: Array1<f64> = (0..n)
|
||||
.map(|i| {
|
||||
let x = i as f64;
|
||||
1.5 + (0.131 * x).sin()
|
||||
+ 0.7 * (0.397 * x + 1.3).sin()
|
||||
+ 0.3 * (0.883 * x + 0.7).sin()
|
||||
})
|
||||
.collect();
|
||||
let b: Array1<f64> = (0..n)
|
||||
.map(|i| if i < 2 { 0.0 } else { a[[i - 2]] })
|
||||
.collect();
|
||||
let points: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64]).collect();
|
||||
let steps = vec![
|
||||
RegistrationStep::new(
|
||||
Sigma::Absolute(vec![2.0]),
|
||||
SamplingArg::FixedAt(points.clone()),
|
||||
24,
|
||||
1e-6,
|
||||
0.05,
|
||||
1024,
|
||||
1.0,
|
||||
Optimizer::LBFGS,
|
||||
),
|
||||
RegistrationStep::new(
|
||||
Sigma::None,
|
||||
SamplingArg::FixedAt(points),
|
||||
48,
|
||||
1e-6,
|
||||
0.05,
|
||||
2048,
|
||||
1.0,
|
||||
Optimizer::LBFGS,
|
||||
),
|
||||
];
|
||||
let registration = Registration::<Ix1>::new_translation()?.with_steps(steps);
|
||||
let transform = registration.register(a.view(), b.view())?;
|
||||
assert_eq!(transform.parameters.len(), 2);
|
||||
assert_eq!(transform.dparameters.len(), 2);
|
||||
// the fixed scale parameter has no uncertainty
|
||||
assert_eq!(transform.dparameters[0], 0.0);
|
||||
// the data are a perfect shifted copy, so the residual variance is
|
||||
// (numerically) zero and so is the translation uncertainty
|
||||
assert!(transform.dparameters[1].is_finite());
|
||||
assert!(transform.dparameters[1] < 0.05);
|
||||
// the found translation should be close to the applied one
|
||||
assert!((transform.parameters[1] - 2.0).abs() < 1.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noisy_fit_has_positive_dparameters() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let n = 128;
|
||||
let a: Array1<f64> = (0..n)
|
||||
.map(|i| {
|
||||
let x = i as f64;
|
||||
1.5 + (0.131 * x).sin()
|
||||
+ 0.7 * (0.397 * x + 1.3).sin()
|
||||
+ 0.3 * (0.883 * x + 0.7).sin()
|
||||
})
|
||||
.collect();
|
||||
let b: Array1<f64> = (0..n)
|
||||
.map(|i| {
|
||||
let base = if i < 2 { 0.0 } else { a[[i - 2]] };
|
||||
// deterministic broadband perturbation
|
||||
base + 0.05 * ((i * 37 % 19) as f64 / 18.0 - 0.5)
|
||||
})
|
||||
.collect();
|
||||
let points: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64]).collect();
|
||||
let metric = MattesMetric::new_from_arrays(
|
||||
a.view(),
|
||||
b.view(),
|
||||
SamplingArg::FixedAt(points),
|
||||
16,
|
||||
0.05,
|
||||
)?
|
||||
.with_fixed_mu(vec![Some(1.0), None]);
|
||||
|
||||
let fit = metric.residual_fit(&[2.0]).expect("residual fit");
|
||||
assert!(fit.variance > 0.0);
|
||||
let d = dparameters_from_residual_fit(&fit);
|
||||
assert!(d[0] > 0.0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user