- 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
@@ -4,7 +4,7 @@ Find an ND affine transformation that registers an ND test image to a fixed imag
|
||||
other images.
|
||||
|
||||
`image-registration` is a pure-Rust, dependency-light implementation of intensity-based image registration in the spirit
|
||||
of [SimpleElastix]([https://elastix.lumc.nl](https://simpleelastix.github.io/)). It finds an affine (rigid + scale + shear + translation) transform
|
||||
of [SimpleElastix](https://simpleelastix.github.io/). It finds an affine (rigid + scale + shear + translation) transform
|
||||
that aligns a *moving* image to a *fixed* image by maximizing
|
||||
the [Mattes mutual-information](https://doi.org/10.1109/TMI.2003.809072) metric with a B-spline Parzen
|
||||
window, using a multi-resolution Gaussian image pyramid and a hybrid ASGD + L-BFGS optimization strategy.
|
||||
@@ -12,6 +12,10 @@ window, using a multi-resolution Gaussian image pyramid and a hybrid ASGD + L-BF
|
||||
Everything operates on [`ndarray`](https://docs.rs/ndarray) arrays, works in arbitrary dimensions (1D, 2D, 3D, …), and
|
||||
is parallelized with [rayon](https://docs.rs/rayon).
|
||||
|
||||
So far, 2D registration seems to work, but 3D and higher dimensions often do not. Also, this code is still a lot slower
|
||||
than (Simple)Elastix. The biggest benefit of this crate is that it's Rust and does not require a lengthy compilation of
|
||||
C++ code.
|
||||
|
||||
## Features
|
||||
|
||||
- **Affine registration** in any number of dimensions — rotation, scale, shear, and translation parameters
|
||||
@@ -36,13 +40,13 @@ Register a moving image to a fixed image and obtain the affine transform:
|
||||
```rust
|
||||
use image_registration::error::Error;
|
||||
use image_registration::transform::Transform;
|
||||
use ndarray::Array2;
|
||||
use ndarray::{Array2, Ix2};
|
||||
|
||||
fn register(fixed: &Array2<f64>, moving: &Array2<f64>) -> Result<Transform, Error> {
|
||||
fn register(fixed: &Array2<f64>, moving: &Array2<f64>) -> Result<Transform<Ix2>, Error> {
|
||||
// All 6 parameters of the 2D affine transform are free (None = optimize).
|
||||
// steps=None uses the default multi-resolution pyramid; initial_guess=None
|
||||
// initializes by aligning the geometric centers of the two images.
|
||||
Transform::<ndarray::Ix2>::register(
|
||||
Transform::<Ix2>::register(
|
||||
fixed.view(),
|
||||
moving.view(),
|
||||
vec![None; 6],
|
||||
@@ -57,7 +61,8 @@ The returned `Transform` maps coordinates from the *fixed* image to the *moving*
|
||||
|
||||
```rust
|
||||
let transform = register(&fixed, &moving)?;
|
||||
let registered = transform.interpolate_par::<3, _, _>(moving.view())?; // cubic B-spline, parallel
|
||||
// out-of-range pixels are filled with the default value (0.0 by default)
|
||||
let registered = transform.interpolate_par::<3, _, _>(moving.view(), &Default::default())?; // cubic B-spline, parallel
|
||||
```
|
||||
|
||||
## Concepts
|
||||
@@ -71,15 +76,17 @@ in total:
|
||||
- 2D: `[m00, m01, m10, m11, tx, ty]` (row-major linear part, then translation)
|
||||
- 3D: `[m00, m01, m02, m10, m11, m12, m20, m21, m22, tx, ty, tz]`
|
||||
|
||||
The transformation is applied about a center saved in ```Transform```:
|
||||
The transformation is applied about a center `c` that is supplied where the transform is used:
|
||||
|
||||
```text
|
||||
v = A·(p − c) + t + c
|
||||
```
|
||||
|
||||
where `A` is the linear part, `t` the translation, `c` the center of the transformation, and `p` the input coordinates.
|
||||
`Transform::new(parameters, shape)` sets the center to `(shape − 1) / 2` automatically; `Transform::new_with_center`
|
||||
lets you override it.
|
||||
`transform_point`/`transform_points` take the center as an argument, and resampling (`interpolate`, `interpolate_par`)
|
||||
uses the geometric center of the image, `(shape − 1) / 2`. `Transform::new_around_center(parameters, center)`
|
||||
re-expresses the parameters so that the transformation acts about a chosen center (this is how
|
||||
`from_rotation`/`with_rotation` rotate about a given point).
|
||||
|
||||
### Registration convention
|
||||
|
||||
@@ -101,19 +108,24 @@ fn main() -> Result<(), Error> {
|
||||
let rot = Transform::<Ix2>::from_rotation(std::f64::consts::FRAC_PI_4, &[99.5, 99.5]);
|
||||
|
||||
// Compose: translate, then scale, then rotate
|
||||
let t = rot.with_scaling(&[0.9, 1.1]).with_translation(&[10.0, -5.0]);
|
||||
let t = rot.add_scaling(&[0.9, 1.1]).with_translation(&[10.0, -5.0]);
|
||||
|
||||
// Transform a single point (the center is subtracted and re-added)
|
||||
let p = t.transform_point(&[200, 300]);
|
||||
let p = t.transform_point(&[200.0, 300.0], &[99.5, 99.5]);
|
||||
|
||||
// Transform many points at once (rows of an N×2 array)
|
||||
let points = ndarray::array![[0.0, 0.0], [199.0, 199.0]];
|
||||
let pts = t.transform_points(points.view())?;
|
||||
let pts = t.transform_points(points.view(), &[99.5, 99.5])?;
|
||||
|
||||
// Matrix form (3×3 homogeneous) and exact inverse
|
||||
let m = t.matrix();
|
||||
let inv = t.inverse()?;
|
||||
assert!(m.dot(&inv.matrix()).iter().all(|x| (x - 1.0).abs() < 1e-9));
|
||||
let identity = ndarray::Array2::<f64>::eye(3);
|
||||
assert!(m
|
||||
.dot(&inv.matrix())
|
||||
.iter()
|
||||
.zip(identity.iter())
|
||||
.all(|(x, y)| (x - y).abs() < 1e-9));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
@@ -132,11 +144,11 @@ fn main() -> Result<(), Error> {
|
||||
let image = ndarray::Array2::<f64>::zeros((200, 150));
|
||||
let transform = Transform::<ndarray::Ix2>::from_rotation(0.3, &[99.5, 74.5]);
|
||||
|
||||
// Order-1 (linear) interpolation, single-threaded
|
||||
let warped_linear = transform.interpolate::<1, _, _>(image.view())?;
|
||||
// Order-1 (linear) interpolation, single-threaded (out-of-range pixels → 0.0)
|
||||
let warped_linear = transform.interpolate::<1, _, _>(image.view(), &Default::default())?;
|
||||
|
||||
// Order-3 (cubic) B-spline interpolation, parallel
|
||||
let warped_cubic = transform.interpolate_par::<3, _, _>(image.view())?;
|
||||
let warped_cubic = transform.interpolate_par::<3, _, _>(image.view(), &Default::default())?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
@@ -163,11 +175,10 @@ fn main() -> Result<(), Error> {
|
||||
let theta = 10.0f64.to_radians();
|
||||
let (s, c) = theta.sin_cos();
|
||||
let params = vec![c * 1.05, -s, s, c * 0.95, 12.0, -8.0];
|
||||
let ground_truth =
|
||||
Transform::<Ix2>::new_with_center(params.clone(), center.to_vec(), shape.to_vec());
|
||||
let ground_truth = Transform::<Ix2>::new_around_center(params.clone(), ¢er);
|
||||
|
||||
// Moving image = ground truth applied to the fixed image
|
||||
let moving = ground_truth.interpolate::<3, _, _>(fixed.view())?;
|
||||
let moving = ground_truth.interpolate::<3, _, _>(fixed.view(), &Default::default())?;
|
||||
|
||||
// Register (all 6 parameters free, default steps)
|
||||
let t = Transform::<Ix2>::register(fixed.view(), moving.view(), vec![None; 6], None, None)?;
|
||||
@@ -218,7 +229,7 @@ use image_registration::metric::{SamplingArg, Sigma};
|
||||
use image_registration::register::{Optimizer, Registration, RegistrationStep};
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let mut coarse = RegistrationStep::new(
|
||||
let coarse = RegistrationStep::new(
|
||||
Sigma::Absolute(vec![4.0, 4.0]), // Gaussian sigma per dimension
|
||||
SamplingArg::Fixed(3000), // 3000 sampled points (cached)
|
||||
32, // histogram bins
|
||||
@@ -226,10 +237,10 @@ fn main() -> Result<(), Error> {
|
||||
0.05, // edge fraction
|
||||
512, // max iterations
|
||||
1.0, // learning rate
|
||||
Optimizer::ASGD, // robust to noisy gradients
|
||||
);
|
||||
coarse.optimizer = Optimizer::ASGD; // robust to noisy gradients
|
||||
|
||||
let mut fine = RegistrationStep::new(
|
||||
let fine = RegistrationStep::new(
|
||||
Sigma::Absolute(vec![0.5, 0.5]),
|
||||
SamplingArg::Fixed(3000),
|
||||
32,
|
||||
@@ -237,9 +248,10 @@ fn main() -> Result<(), Error> {
|
||||
0.05,
|
||||
2048,
|
||||
1.0,
|
||||
); // optimizer stays LBFGS (the default)
|
||||
Optimizer::LBFGS, // the default optimizer
|
||||
);
|
||||
|
||||
let mut reg = Registration::new(vec![None; 6]);
|
||||
let mut reg = Registration::new(vec![None; 6])?;
|
||||
reg.set_steps(vec![coarse, fine]);
|
||||
let t = reg.register(fixed.view(), moving.view())?;
|
||||
Ok(())
|
||||
@@ -284,18 +296,23 @@ Build a `MattesMetric` and evaluate the (negative) mutual information and its gr
|
||||
- `FixedAt(points)` — an explicit list of sample positions (fully deterministic; recommended for reproducible results).
|
||||
|
||||
```rust
|
||||
use algos::ObjectiveFunction;
|
||||
use image_registration::bspline::BSpline;
|
||||
use algos::ObjectiveFunction; // direct dependency of image-registration; add it to your crate to name the trait
|
||||
use image_registration::bspline::{BSpline, BSplineTrait};
|
||||
use image_registration::error::Error;
|
||||
use image_registration::metric::{MattesMetric, SamplingArg, Sigma};
|
||||
use image_registration::transform::Transform;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// `fixed` and `moving` are Array2<f64>
|
||||
let fixed = ndarray::Array2::<f64>::zeros((64, 64));
|
||||
let moving = ndarray::Array2::<f64>::zeros((64, 64));
|
||||
// `fixed` and `moving` are Array2<f64>; moving is a rotated + shifted copy of fixed
|
||||
let center = [31.5f64, 31.5];
|
||||
let fixed = julia_image(&[64, 64], &[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], ¢er, &[-0.8, 0.156])
|
||||
.mapv(|i| i as f64);
|
||||
let moving = Transform::<ndarray::Ix2>::from_rotation(10.0f64.to_radians(), ¢er)
|
||||
.interpolate_par::<3, _, _>(fixed.view(), &Default::default())?;
|
||||
|
||||
let f = Sigma::Absolute(vec![2.0, 2.0]).smooth(fixed.view())?;
|
||||
let m = Sigma::Absolute(vec![2.0, 2.0]).smooth(moving.view())?;
|
||||
let sigma = Sigma::Absolute(vec![2.0, 2.0]);
|
||||
let f = sigma.smooth(fixed.view())?;
|
||||
let m = sigma.smooth(moving.view())?;
|
||||
|
||||
let metric = MattesMetric::new(
|
||||
BSpline::<0, _>::new(f.view()), // fixed image B-spline (nearest-neighbor)
|
||||
@@ -336,11 +353,11 @@ fn main() -> Result<(), Error> {
|
||||
|
||||
| Module | Contents |
|
||||
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `image_registration::transform` | `Transform`, the affine transform type: construction (`new`, `new_around_center`, `from_translation`, `from_scaling`, `from_rotation`), algebra (`inverse`, `add_translation`, `add_scaling`, `with_rotation`, `with_rotation_around`, `Mul`), coordinate transforms (`transform_point`, `transform_points`, `matrix`, `dmatrix`), resampling (`interpolate`, `interpolate_par`), registration (`register`, `register_debug`, `register_affine`, `register_translation`), serialization (`to_file`, `from_file`), plus the free function `transform_point` and a blas-free `matrix_inverse`. |
|
||||
| `image_registration::register` | `Registration`, `RegistrationStep`, `RegistrationResult`, `Optimizer` (LBFGS/ASGD). |
|
||||
| `image_registration::metric` | `MattesMetric` (Mattes mutual information with analytic gradient), `SamplingArg` (Fixed/FixedAt/Random), `Sigma` (None/Absolute/Relative), `FixedMu`. |
|
||||
| `image_registration::bspline` | `BSpline<N, D>` B-spline interpolation of arbitrary order, `BSplineTrait`, `BSplineMem`, Parzen kernel helpers. |
|
||||
| `image_registration::filter` | Gaussian smoothing and FFT helpers: `gaussian_smooth`, `gaussian_kernel`, `fft`, `ifft`, `fft_freq`, `fft_shift`. |
|
||||
| `image_registration::transform` | `Transform`, the affine transform type: construction (`new`, `new_around_center`, `from_translation`, `from_scaling`, `from_rotation`), algebra (`inverse`, `add_translation`, `add_scaling`, `with_rotation`, `Mul`), coordinate transforms (`transform_point`, `transform_points`, `matrix`, `dmatrix`), resampling (`interpolate`, `interpolate_par`, `interpolate_order`, `interpolate_par_order`), registration (`register`, `register_debug`, `register_affine`, `register_translation`, `register_translation_fft`), serialization (`to_file`, `from_file`, and serde), plus the free function `transform_point`. |
|
||||
| `image_registration::register` | `Registration` (`new_affine`, `new_translation`, `with_steps`, `with_optimizer`, …), `RegistrationStep`, `RegistrationResult`, `Optimizer` (LBFGS/ASGD). |
|
||||
| `image_registration::metric` | `MattesMetric` (Mattes mutual information with analytic gradient; `evaluate`/`gradient` via the `algos::ObjectiveFunction` trait), `new_from_arrays`, `residual_fit`, `SamplingArg` (Fixed/FixedAt/Random), `Sigma` (None/Absolute/Relative), `FixedMu`. |
|
||||
| `image_registration::bspline` | `BSpline<N, D>` B-spline interpolation of arbitrary order, `BSplineTrait`, `BSplineMem`, `DefaultValue` (fill value for out-of-range pixels), Parzen kernel helpers. |
|
||||
| `image_registration::filter` | Gaussian smoothing and FFT helpers: `gaussian_smooth`, `gaussian_kernel`, `gaussian`, `fft`, `ifft`, `fft_freq`, `fft_shift`. |
|
||||
| `image_registration::optimize` | Optimizers: `lbfgs_minimize`, `asgd_minimize`, `AsgdConfig`. |
|
||||
| `image_registration::par_indexed_iter` | Parallel iterators over array indices. |
|
||||
| `image_registration::error` | `Error` type. |
|
||||
|
||||
Reference in new issue
Block a user