Now build SimpleITK into static libs and use (auto)cxx.

This commit is contained in:
Wim Pomp
2025-09-12 18:03:24 +02:00
parent 0c6a38e4fa
commit 5750fd7f99
17 changed files with 8110 additions and 1426 deletions
File diff suppressed because it is too large Load Diff
+90 -474
View File
@@ -1,485 +1,101 @@
mod sys;
//! This crate does two things:
//! - find an affine transform or translation that transforms one image into the other
//! - use bspline or nearest neighbor interpolation to apply a transformation to an image
//!
//! To do this, [SimpleITK](https://github.com/SimpleITK/SimpleITK), which is written in
//! C++, is used. An adapter library is created using [autocxx](https://crates.io/crates/autocxx)
//! to expose the required functionality in SimpleITK. Because of this, compilation of this crate
//! requires quite some time, several GB of memory, up to 50 GB of hard disk space, as well as
//! cmake, a C++ compiler, llvm and git. Use at your own risk!
//!
//! # Examples
//! ## Registration
//! ```
//! use ndarray::Array2;
//! use sitk_registration_sys::registration::{AffineTransform, julia_image};
//!
//! let j = julia_image(0f32, 0f32).unwrap();
//! let shape = j.shape();
//! let origin = [
//! ((shape[1] - 1) as f64) / 2f64,
//! ((shape[0] - 1) as f64) / 2f64,
//! ];
//! let s = AffineTransform::new([1.2, 0., 0., 1., 5., 7.], origin, [shape[0], shape[1]]);
//! let k: Array2<_> = s.transform_image_bspline(j.view()).unwrap().into();
//! let t = AffineTransform::register_affine(j.view(), k.view()).unwrap().inverse().unwrap();
//! let d = (t.matrix() - s.matrix()).powi(2).sum();
//! assert!(d < 0.025, "d: {}, t: {:?}", d, t.parameters);
//! ```
//!
//! ## Interpolation
//! ```
//! use ndarray::Array2;
//! use sitk_registration_sys::registration::{AffineTransform, julia_image};
//!
//! let j = julia_image(-120f32, 10f32).unwrap();
//! let k = julia_image(0f32, 0f32).unwrap();
//! let shape = j.shape();
//! let origin = [
//! ((shape[1] - 1) as f64) / 2f64,
//! ((shape[0] - 1) as f64) / 2f64,
//! ];
//! let transform = AffineTransform::new([1., 0., 0., 1., 120., -10.], origin, [shape[0], shape[1]]);
//! let n: Array2<_> = transform.transform_image_bspline(j.view()).unwrap().into();
//! let d = (k.mapv(|x| x as f64) - n.mapv(|x| x as f64)).powi(2).sum();
//! assert!(d <= (shape[0] * shape[1]) as f64);
//! ```
use crate::sys::{interp, register};
use anyhow::{Result, anyhow};
use ndarray::{Array2, ArrayView2, AsArray, Ix2, array, s};
use serde::{Deserialize, Serialize};
use serde_yaml::{from_reader, to_writer};
use std::fs::File;
use std::ops::Mul;
use std::path::PathBuf;
extern crate link_cplusplus;
pub mod registration;
/// a trait marking number types that can be used in sitk:
/// (u/i)(8/16/32/64), (u/i)size, f(32/64)
pub trait PixelType: Clone {
const PT: u8;
/// The bindings generated by [autocxx](https://google.github.io/autocxx).
/// Everything in here is unsafe.
pub use ffi::itk::simple;
use autocxx::prelude::*;
include_cpp! {
#include "sitkAdditionalProcedures.h"
#include "sitkAffineTransform.h"
#include "sitkElastixImageFilter.h"
#include "sitkInterpolator.h"
#include "sitkImage.h"
#include "sitkTransform.h"
safety!(unsafe)
generate!("itk::simple::AffineTransform")
generate!("itk::simple::ElastixImageFilter")
generate!("itk::simple::InterpolatorEnum")
generate!("itk::simple::Image")
generate!("itk::simple::Resample")
generate!("itk::simple::Transform")
opaque!("itk::simple::ElastixImageFilter") // deleted constructor
}
macro_rules! sitk_impl {
($($T:ty: $sitk:expr $(,)?)*) => {
$(
impl PixelType for $T {
const PT: u8 = $sitk;
}
)*
};
}
/// Manually generated bindings for some things that autocxx does not understand.
#[cxx::bridge]
pub mod ffi_extra {
unsafe extern "C++" {
include!("ffi_extra.h");
sitk_impl! {
u8: 1,
i8: 2,
u16: 3,
i16: 4,
u32: 5,
i32: 6,
u64: 7,
i64: 8,
f32: 9,
f64: 10,
}
#[namespace = "itk::simple"]
type ElastixImageFilter = crate::ffi::itk::simple::ElastixImageFilter;
type ParameterMap;
#[cfg(target_pointer_width = "64")]
sitk_impl!(usize: 7);
#[cfg(target_pointer_width = "32")]
sitk_impl!(usize: 5);
#[cfg(target_pointer_width = "64")]
sitk_impl!(isize: 8);
#[cfg(target_pointer_width = "32")]
sitk_impl!(isize: 6);
fn new_parameter_map() -> UniquePtr<ParameterMap>;
fn insert(self: Pin<&mut ParameterMap>, key: &CxxString, value: &CxxVector<CxxString>);
fn keys(self: &ParameterMap) -> UniquePtr<CxxVector<CxxString>>;
fn get(self: &ParameterMap, key: &CxxString) -> UniquePtr<CxxVector<CxxString>>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Transform {
pub parameters: [f64; 6],
pub dparameters: [f64; 6],
pub origin: [f64; 2],
pub shape: [usize; 2],
}
fn get_transform_parameter_map(
tfilter: &mut ElastixImageFilter,
index: u32,
) -> UniquePtr<ParameterMap>;
impl Mul for Transform {
type Output = Transform;
fn get_default_parameter_map(kind: &CxxString) -> UniquePtr<ParameterMap>;
#[allow(clippy::suspicious_arithmetic_impl)]
fn mul(self, other: Transform) -> Transform {
let m = self.matrix().dot(&other.matrix());
let dm = self.dmatrix().dot(&other.matrix()) + self.matrix().dot(&other.dmatrix());
Transform {
parameters: [
m[[0, 0]],
m[[0, 1]],
m[[1, 0]],
m[[1, 1]],
m[[2, 0]],
m[[2, 1]],
],
dparameters: [
dm[[0, 0]],
dm[[0, 1]],
dm[[1, 0]],
dm[[1, 1]],
dm[[2, 0]],
dm[[2, 1]],
],
origin: self.origin,
shape: self.shape,
}
}
}
impl PartialEq<Self> for Transform {
fn eq(&self, other: &Self) -> bool {
self.parameters == other.parameters
&& self.dparameters == other.dparameters
&& self.origin == other.origin
&& self.shape == other.shape
}
}
impl Eq for Transform {}
impl Transform {
/// parameters: flat 2x2 part of matrix, translation; origin: center of rotation
pub fn new(parameters: [f64; 6], origin: [f64; 2], shape: [usize; 2]) -> Self {
Self {
parameters,
dparameters: [0f64; 6],
origin,
shape,
}
}
/// find the affine transform which transforms moving into fixed
pub fn register_affine<'a, A, T>(fixed: A, moving: A) -> Result<Transform>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
let (parameters, origin, shape) = register(fixed, moving, true)?;
Ok(Transform {
parameters,
dparameters: [0f64; 6],
origin,
shape,
})
}
/// find the translation which transforms moving into fixed
pub fn register_translation<'a, A, T>(fixed: A, moving: A) -> Result<Transform>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
let (parameters, origin, shape) = register(fixed, moving, false)?;
Ok(Transform {
parameters,
dparameters: [0f64; 6],
origin,
shape,
})
}
/// create a transform from a xy translation
pub fn from_translation(translation: [f64; 2]) -> Self {
Transform {
parameters: [1f64, 0f64, 0f64, 1f64, translation[0], translation[1]],
dparameters: [0f64; 6],
origin: [0f64; 2],
shape: [0usize; 2],
}
}
/// read a transform from a file
pub fn from_file(path: PathBuf) -> Result<Self> {
let file = File::open(path)?;
Ok(from_reader(file)?)
}
/// write a transform to a file
pub fn to_file(&self, path: PathBuf) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
to_writer(&mut file, self)?;
Ok(())
}
/// true if transform does nothing
pub fn is_unity(&self) -> bool {
self.parameters == [1f64, 0f64, 0f64, 1f64, 0f64, 0f64]
}
/// transform an image using nearest neighbor interpolation
pub fn transform_image_bspline<'a, A, T>(&self, image: A) -> Result<Array2<T>>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
interp(self.parameters, self.origin, image, false)
}
/// transform an image using bspline interpolation
pub fn transform_image_nearest_neighbor<'a, A, T>(&self, image: A) -> Result<Array2<T>>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
interp(self.parameters, self.origin, image, true)
}
/// get coordinates resulting from transforming input coordinates
pub fn transform_coordinates<'a, A, T>(&self, coordinates: A) -> Result<Array2<f64>>
where
T: 'a + Clone + Into<f64>,
A: AsArray<'a, T, Ix2>,
{
let coordinates = coordinates.into();
let s = coordinates.shape();
if s[1] != 2 {
return Err(anyhow!("coordinates must have two columns"));
}
let m = self.matrix();
let mut res = Array2::zeros([s[0], s[1]]);
for i in 0..s[0] {
let a = array![
coordinates[[i, 0]].clone().into(),
coordinates[[i, 1]].clone().into(),
1f64
]
.to_owned();
let b = m.dot(&a);
res.slice_mut(s![i, ..]).assign(&b.slice(s![..2]));
}
Ok(res)
}
/// get the matrix defining the transform
pub fn matrix(&self) -> Array2<f64> {
Array2::from_shape_vec(
(3, 3),
vec![
self.parameters[0],
self.parameters[1],
self.parameters[4],
self.parameters[2],
self.parameters[3],
self.parameters[5],
0f64,
0f64,
1f64,
],
)
.unwrap()
}
/// get the matrix describing the error of the transform
pub fn dmatrix(&self) -> Array2<f64> {
Array2::from_shape_vec(
(3, 3),
vec![
self.dparameters[0],
self.dparameters[1],
self.dparameters[4],
self.dparameters[2],
self.dparameters[3],
self.dparameters[5],
0f64,
0f64,
1f64,
],
)
.unwrap()
}
/// get the inverse transform
pub fn inverse(&self) -> Result<Transform> {
fn det(a: ArrayView2<f64>) -> f64 {
(a[[0, 0]] * a[[1, 1]]) - (a[[0, 1]] * a[[1, 0]])
}
let m = self.matrix();
let d = det(m.slice(s![..2, ..2]));
if d == 0f64 {
return Err(anyhow!("transform matrix is not invertible"));
}
let parameters = [
det(m.slice(s![1.., 1..])) / d,
-det(m.slice(s![..;2, 1..])) / d,
-det(m.slice(s![1.., ..;2])) / d,
det(m.slice(s![..;2, ..;2])) / d,
det(m.slice(s![..2, 1..])) / d,
-det(m.slice(s![..2, ..;2])) / d,
];
Ok(Transform {
parameters,
dparameters: [0f64; 6],
origin: self.origin,
shape: self.shape,
})
}
/// adapt the transform to a new origin and shape
pub fn adapt(&mut self, origin: [f64; 2], shape: [usize; 2]) {
self.origin = [
origin[0] + (((self.shape[0] - shape[0]) as f64) / 2f64),
origin[1] + (((self.shape[1] - shape[1]) as f64) / 2f64),
];
self.shape = shape;
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use ndarray::Array2;
use num::Complex;
use tempfile::NamedTempFile;
/// An example of generating julia fractals.
fn julia_image(shift_x: f32, shift_y: f32) -> Result<Array2<u8>> {
let imgx = 800;
let imgy = 600;
let scalex = 3.0 / imgx as f32;
let scaley = 3.0 / imgy as f32;
let mut im = Array2::<u8>::zeros((imgy, imgx));
for x in 0..imgx {
for y in 0..imgy {
let cy = (y as f32 + shift_y) * scalex - 1.5;
let cx = (x as f32 + shift_x) * scaley - 1.5;
let c = Complex::new(-0.4, 0.6);
let mut z = Complex::new(cy, cx);
let mut i = 0;
while i < 255 && z.norm() <= 2.0 {
z = z * z + c;
i += 1;
}
im[[y, x]] = i as u8;
}
}
Ok(im)
}
#[test]
fn test_serialization() -> Result<()> {
let file = NamedTempFile::new()?;
let t = Transform::new([1.2, 0.3, -0.4, 0.9, 10.2, -9.5], [59.5, 49.5], [120, 100]);
t.to_file(file.path().to_path_buf())?;
let s = Transform::from_file(file.path().to_path_buf())?;
assert_eq!(s, t);
Ok(())
}
macro_rules! interp_tests_bspline {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(-120f32, 10f32)?.mapv(|x| x as $t);
let k = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let transform = Transform::new([1., 0., 0., 1., 120., -10.], origin, [shape[0], shape[1]]);
let n = transform.transform_image_bspline(j.view())?;
let d = (k.mapv(|x| x as f64) - n.mapv(|x| x as f64)).powi(2).sum();
assert!(d <= (shape[0] * shape[1]) as f64);
Ok(())
}
)*
}
}
interp_tests_bspline! {
interpbs_u8: u8,
interpbs_i8: i8,
interpbs_u16: u16,
interpbs_i16: i16,
interpbs_u32: u32,
interpbs_i32: i32,
interpbs_u64: u64,
interpbs_i64: i64,
interpbs_f32: f32,
interpbs_f64: f64,
}
macro_rules! interp_tests_nearest_neighbor {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(-120f32, 10f32)?.mapv(|x| x as $t);
let k = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let j0 = j.clone();
let k0 = k.clone();
let transform = Transform::new([1., 0., 0., 1., 120., -10.], origin, [shape[0], shape[1]]);
// make sure j & k weren't mutated
assert!(j.iter().zip(j0.iter()).map(|(a, b)| a == b).all(|x| x));
assert!(k.iter().zip(k0.iter()).map(|(a, b)| a == b).all(|x| x));
let n = transform.transform_image_nearest_neighbor(j.view())?;
let d = (k.mapv(|x| x as f64) - n.mapv(|x| x as f64)).powi(2).sum();
assert!(d <= (shape[0] * shape[1]) as f64);
Ok(())
}
)*
}
}
interp_tests_nearest_neighbor! {
interpnn_u8: u8,
interpnn_i8: i8,
interpnn_u16: u16,
interpnn_i16: i16,
interpnn_u32: u32,
interpnn_i32: i32,
interpnn_u64: u64,
interpnn_i64: i64,
interpnn_f32: f32,
interpnn_f64: f64,
}
macro_rules! registration_tests_translation {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let k = julia_image(10f32, 20f32)?.mapv(|x| x as $t);
let j0 = j.clone();
let k0 = k.clone();
let t = Transform::register_translation(j.view(), k.view())?;
// make sure j & k weren't mutated
assert!(j.iter().zip(j0.iter()).map(|(a, b)| a == b).all(|x| x));
assert!(k.iter().zip(k0.iter()).map(|(a, b)| a == b).all(|x| x));
let mut m = Array2::eye(3);
m[[0, 2]] = -10f64;
m[[1, 2]] = -20f64;
let d = (t.matrix() - m).powi(2).sum();
assert!(d < 0.01);
Ok(())
}
)*
}
}
registration_tests_translation! {
registration_translation_u8: u8,
registration_translation_i8: i8,
registration_translation_u16: u16,
registration_translation_i16: i16,
registration_translation_u32: u32,
registration_translation_i32: i32,
registration_translation_u64: u64,
registration_translation_i64: i64,
registration_translation_f32: f32,
registration_translation_f64: f64,
}
macro_rules! registration_tests_affine {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let s = Transform::new([1.2, 0., 0., 1., 5., 7.], origin, [shape[0], shape[1]]);
let k = s.transform_image_bspline(j.view())?;
let t = Transform::register_affine(j.view(), k.view())?.inverse()?;
let d = (t.matrix() - s.matrix()).powi(2).sum();
assert!(d < 0.01);
Ok(())
}
)*
}
}
registration_tests_affine! {
registration_tests_affine_u8: u8,
registration_tests_affine_i8: i8,
registration_tests_affine_u16: u16,
registration_tests_affine_i16: i16,
registration_tests_affine_u32: u32,
registration_tests_affine_i32: i32,
registration_tests_affine_u64: u64,
registration_tests_affine_i64: i64,
registration_tests_affine_f32: f32,
registration_tests_affine_f64: f64,
fn set_parameter_map(
tfilter: &mut ElastixImageFilter,
parameter_map: &UniquePtr<ParameterMap>,
);
}
}
+645
View File
@@ -0,0 +1,645 @@
//! Some structs and methods to make working with registration and interpolation methods in
//! SimpleITK more Rust friendly.
use crate::simple;
use anyhow::{Result, anyhow};
use autocxx::prelude::*;
use cxx::{CxxVector, UniquePtr, let_cxx_string};
use ndarray::{Array2, ArrayView2, AsArray, Ix2, array, s};
use serde::{Deserialize, Serialize};
use serde_yaml::{from_reader, to_writer};
use std::fs::File;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut, Mul};
use std::path::PathBuf;
use num::Complex;
use tempfile::tempdir;
/// a trait marking number types that can be used in sitk:
/// (u/i)(8/16/32/64), (u/i)size, f(32/64)
pub trait PixelType: Clone {
const PT: simple::PixelIDValueEnum;
}
macro_rules! pixel_type_impl {
($($T:ty: $sitk:expr $(,)?)*) => {
$(
impl PixelType for $T {
const PT: simple::PixelIDValueEnum = $sitk;
}
)*
};
}
pixel_type_impl! {
u8: simple::PixelIDValueEnum::sitkUInt8,
i8: simple::PixelIDValueEnum::sitkInt8,
u16: simple::PixelIDValueEnum::sitkUInt16,
i16: simple::PixelIDValueEnum::sitkInt16,
u32: simple::PixelIDValueEnum::sitkUInt32,
i32: simple::PixelIDValueEnum::sitkInt32,
u64: simple::PixelIDValueEnum::sitkUInt64,
i64: simple::PixelIDValueEnum::sitkInt64,
f32: simple::PixelIDValueEnum::sitkFloat32,
f64: simple::PixelIDValueEnum::sitkFloat64,
}
#[cfg(target_pointer_width = "64")]
pixel_type_impl!(usize: simple::PixelIDValueEnum::sitkUInt64);
#[cfg(target_pointer_width = "32")]
pixel_type_impl!(usize: simple::PixelIDValueEnum::sitkUInt32);
#[cfg(target_pointer_width = "64")]
pixel_type_impl!(isize: simple::PixelIDValueEnum::sitkInt64);
#[cfg(target_pointer_width = "32")]
pixel_type_impl!(isize: simple::PixelIDValueEnum::sitkInt32);
/// Struct holding a pointer to an image
pub struct Image<T: PixelType> {
image: UniquePtr<simple::Image>,
pixel_type: PhantomData<T>,
}
impl<T: PixelType> Deref for Image<T> {
type Target = UniquePtr<simple::Image>;
fn deref(&self) -> &Self::Target {
&self.image
}
}
impl<T: PixelType> Image<T> {
/// encapsulate an itk::simple::Image
pub fn new(image: UniquePtr<simple::Image>) -> Self {
Self {
image,
pixel_type: PhantomData,
}
}
/// take an ndarray Array2 and turn it into a SimpleITK image
pub fn from_array<'a, A>(array: A) -> Self
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
let array = array.into();
let shape = array.shape();
let width = (shape[1] as u32).into();
let height = (shape[0] as u32).into();
let mut image = simple::Image::new3(width, height, T::PT).within_unique_ptr();
image.pin_mut().MakeUnique();
let buffer = image.pin_mut().GetBufferAsVoid();
unsafe { std::ptr::copy(array.as_ptr(), buffer as *mut T, shape[0] * shape[1]) };
Self {
image,
pixel_type: PhantomData,
}
}
/// return as an ndarray Array2
pub fn as_array(&self) -> Array2<T> {
let width = u32::from(self.image.GetWidth()) as usize;
let height = u32::from(self.image.GetHeight()) as usize;
let mut array = Array2::<T>::uninit((height, width));
let buffer = self.image.GetBufferAsVoid1();
unsafe {
std::ptr::copy(
buffer as *const T,
array.as_mut_ptr() as *mut T,
width * height,
);
array.assume_init()
}
}
}
impl<'a, A, T> From<A> for Image<T>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
fn from(value: A) -> Self {
Self::from_array(value.into())
}
}
impl<T> From<Image<T>> for Array2<T>
where
T: PixelType,
{
fn from(value: Image<T>) -> Self {
value.as_array()
}
}
/// a struct describing the transform
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct AffineTransform {
/// flattened 2x2 rotation matrix + translation
pub parameters: [f64; 6],
/// error / significance on parameters
pub dparameters: [f64; 6],
/// the point about which rotations are performed
pub origin: [f64; 2],
/// the shape of images for which this transform is meant
pub shape: [usize; 2],
}
impl Mul for AffineTransform {
type Output = AffineTransform;
#[allow(clippy::suspicious_arithmetic_impl)]
fn mul(self, other: AffineTransform) -> AffineTransform {
let m = self.matrix().dot(&other.matrix());
let dm = self.dmatrix().dot(&other.matrix()) + self.matrix().dot(&other.dmatrix());
AffineTransform {
parameters: [
m[[0, 0]],
m[[0, 1]],
m[[1, 0]],
m[[1, 1]],
m[[2, 0]],
m[[2, 1]],
],
dparameters: [
dm[[0, 0]],
dm[[0, 1]],
dm[[1, 0]],
dm[[1, 1]],
dm[[2, 0]],
dm[[2, 1]],
],
origin: self.origin,
shape: self.shape,
}
}
}
impl Eq for AffineTransform {}
impl AffineTransform {
/// parameters: flat 2x2 part of matrix, translation; origin: center of rotation
pub fn new(parameters: [f64; 6], origin: [f64; 2], shape: [usize; 2]) -> Self {
Self {
parameters,
dparameters: [0f64; 6],
origin,
shape,
}
}
/// find the affine transform which transforms moving into fixed
pub fn register_affine<F, M, T>(fixed: F, moving: M) -> Result<AffineTransform>
where
F: Into<Image<T>>,
M: Into<Image<T>>,
T: PixelType,
{
Self::register(fixed, moving, true)
}
/// find the translation which transforms moving into fixed
pub fn register_translation<F, M, T>(fixed: F, moving: M) -> Result<AffineTransform>
where
F: Into<Image<T>>,
M: Into<Image<T>>,
T: PixelType,
{
Self::register(fixed, moving, false)
}
/// find the transform which transforms moving into fixed
pub fn register<F, M, T>(fixed: F, moving: M, affine: bool) -> Result<AffineTransform>
where
F: Into<Image<T>>,
M: Into<Image<T>>,
T: PixelType,
{
let tmp_folder = tempdir()?;
let fixed = fixed.into();
let moving = moving.into();
let width = u32::from(fixed.GetWidth()) as usize;
let height = u32::from(fixed.GetHeight()) as usize;
let_cxx_string!(transform_name = if affine { "affine" } else { "translation" });
let parameter_map = crate::ffi_extra::get_default_parameter_map(&transform_name);
let mut tfilter = simple::ElastixImageFilter::new().within_box();
tfilter.as_mut().LogToConsoleOff();
tfilter.as_mut().LogToFileOff();
tfilter.as_mut().SetLogToFile(false);
tfilter.as_mut().SetFixedImage(&fixed);
tfilter.as_mut().SetMovingImage(&moving);
crate::ffi_extra::set_parameter_map(&mut tfilter, &parameter_map);
tfilter.as_mut().SetParameter("WriteResultImage", "False");
tfilter
.as_mut()
.SetOutputDirectory(tmp_folder.path().display().to_string());
let _ = tfilter.as_mut().Execute().within_unique_ptr();
let_cxx_string!(tp = "TransformParameters");
let p = crate::ffi_extra::get_transform_parameter_map(tfilter.as_mut().deref_mut(), 0)
.get(&tp)
.iter()
.map(|i| i.to_string_lossy().parse::<f64>())
.collect::<Result<Vec<f64>, _>>()?;
let parameters = if affine {
[p[0], p[1], p[2], p[3], p[4], p[5]]
} else {
[1.0, 0.0, 0.0, 1.0, p[0], p[1]]
};
let origin = [((height - 1) as f64) / 2.0, ((width - 1) as f64) / 2.0];
let shape = [height, width];
Ok(AffineTransform::new(parameters, origin, shape))
}
/// create a transform from a xy translation
pub fn from_translation(translation: [f64; 2]) -> Self {
AffineTransform {
parameters: [1f64, 0f64, 0f64, 1f64, translation[0], translation[1]],
dparameters: [0f64; 6],
origin: [0f64; 2],
shape: [0usize; 2],
}
}
/// read a transform from a file
pub fn from_file(path: PathBuf) -> Result<Self> {
let file = File::open(path)?;
Ok(from_reader(file)?)
}
/// write a transform to a file
pub fn to_file(&self, path: PathBuf) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
to_writer(&mut file, self)?;
Ok(())
}
/// true if transform does nothing
pub fn is_unity(&self) -> bool {
self.parameters == [1f64, 0f64, 0f64, 1f64, 0f64, 0f64]
}
/// transform an image using nearest neighbor interpolation
pub fn transform_image_bspline<I, T>(&self, image: I) -> Result<Image<T>>
where
I: Into<Image<T>>,
T: PixelType,
{
Self::interp(self, image, false)
}
/// transform an image using bspline interpolation
pub fn transform_image_nearest_neighbor<I, T>(&self, image: I) -> Result<Image<T>>
where
I: Into<Image<T>>,
T: PixelType,
{
Self::interp(self, image, true)
}
/// transform an image
pub fn interp<I, T>(&self, image: I, nearest_neighbor: bool) -> Result<Image<T>>
where
I: Into<Image<T>>,
T: PixelType,
{
let image = image.into();
let width = u32::from(image.GetWidth()) as usize;
let height = u32::from(image.GetHeight()) as usize;
let origin = [((width - 1) as f64) / 2f64, ((height - 1) as f64) / 2f64];
let p = self.parameters;
let matrix = cxx_vector([p[0], p[1], p[2], p[3]]);
let translation = cxx_vector([p[4], p[5]]);
let fixed_center = cxx_vector(origin);
let affine_transform =
simple::AffineTransform::new3(&matrix, &translation, &fixed_center).within_unique_ptr();
let transform = <_ as AsRef<simple::Transform>>::as_ref(affine_transform.as_ref().unwrap());
let interpolator = if nearest_neighbor {
simple::InterpolatorEnum::sitkBSpline
} else {
simple::InterpolatorEnum::sitkNearestNeighbor
};
Ok(Image::<T>::new(
simple::Resample(
&image,
transform,
interpolator,
0.0,
simple::PixelIDValueEnum::sitkUnknown,
nearest_neighbor,
)
.within_unique_ptr(),
))
}
/// get coordinates resulting from transforming input coordinates, coordinates must have two
/// columns: x & y
pub fn transform_coordinates<'a, A, T>(&self, coordinates: A) -> Result<Array2<f64>>
where
T: 'a + Clone + Into<f64>,
A: AsArray<'a, T, Ix2>,
{
let coordinates = coordinates.into();
let s = coordinates.shape();
if s[1] != 2 {
return Err(anyhow!("coordinates must have two columns"));
}
let m = self.matrix();
let mut res = Array2::zeros([s[0], s[1]]);
for i in 0..s[0] {
let a = array![
coordinates[[i, 0]].clone().into(),
coordinates[[i, 1]].clone().into(),
1f64
]
.to_owned();
let b = m.dot(&a);
res.slice_mut(s![i, ..]).assign(&b.slice(s![..2]));
}
Ok(res)
}
/// get the matrix defining the transform
pub fn matrix(&self) -> Array2<f64> {
Array2::from_shape_vec(
(3, 3),
vec![
self.parameters[0],
self.parameters[1],
self.parameters[4],
self.parameters[2],
self.parameters[3],
self.parameters[5],
0f64,
0f64,
1f64,
],
)
.unwrap()
}
/// get the matrix describing the error of the transform
pub fn dmatrix(&self) -> Array2<f64> {
Array2::from_shape_vec(
(3, 3),
vec![
self.dparameters[0],
self.dparameters[1],
self.dparameters[4],
self.dparameters[2],
self.dparameters[3],
self.dparameters[5],
0f64,
0f64,
1f64,
],
)
.unwrap()
}
/// get the inverse transform
pub fn inverse(&self) -> Result<AffineTransform> {
fn det(a: ArrayView2<f64>) -> f64 {
(a[[0, 0]] * a[[1, 1]]) - (a[[0, 1]] * a[[1, 0]])
}
let m = self.matrix();
let d = det(m.slice(s![..2, ..2]));
if d == 0f64 {
return Err(anyhow!("transform matrix is not invertible"));
}
let parameters = [
det(m.slice(s![1.., 1..])) / d,
-det(m.slice(s![..;2, 1..])) / d,
-det(m.slice(s![1.., ..;2])) / d,
det(m.slice(s![..;2, ..;2])) / d,
det(m.slice(s![..2, 1..])) / d,
-det(m.slice(s![..2, ..;2])) / d,
];
Ok(AffineTransform {
parameters,
dparameters: [0f64; 6],
origin: self.origin,
shape: self.shape,
})
}
/// adapt the transform to a new origin and shape
pub fn adapt(&mut self, origin: [f64; 2], shape: [usize; 2]) {
self.origin = [
origin[0] + (((self.shape[0] - shape[0]) as f64) / 2f64),
origin[1] + (((self.shape[1] - shape[1]) as f64) / 2f64),
];
self.shape = shape;
}
}
/// conveniently collect an iterator into a CxxVector
pub fn cxx_vector<T, I>(vec: I) -> UniquePtr<CxxVector<T>>
where
I: IntoIterator<Item = T>,
T: cxx::vector::VectorElement + cxx::ExternType<Kind = cxx::kind::Trivial>,
{
let mut v = CxxVector::new();
v.pin_mut().extend(vec);
v
}
/// An example of generating julia fractals, for testing purposes.
pub fn julia_image(shift_x: f32, shift_y: f32) -> Result<Array2<u8>> {
let imgx = 800;
let imgy = 600;
let scalex = 3.0 / imgx as f32;
let scaley = 3.0 / imgy as f32;
let mut im = Array2::<u8>::zeros((imgy, imgx));
for x in 0..imgx {
for y in 0..imgy {
let cy = (y as f32 + shift_y) * scalex - 1.5;
let cx = (x as f32 + shift_x) * scaley - 1.5;
let c = Complex::new(-0.4, 0.6);
let mut z = Complex::new(cy, cx);
let mut i = 0;
while i < 255 && z.norm() <= 2.0 {
z = z * z + c;
i += 1;
}
im[[y, x]] = i as u8;
}
}
Ok(im)
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use ndarray::Array2;
use tempfile::NamedTempFile;
#[test]
fn test_serialization() -> Result<()> {
let file = NamedTempFile::new()?;
let t = AffineTransform::new([1.2, 0.3, -0.4, 0.9, 10.2, -9.5], [59.5, 49.5], [120, 100]);
t.to_file(file.path().to_path_buf())?;
let s = AffineTransform::from_file(file.path().to_path_buf())?;
assert_eq!(s, t);
Ok(())
}
macro_rules! interp_tests_bspline {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(-120f32, 10f32)?.mapv(|x| x as $t);
let k = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let transform = AffineTransform::new([1., 0., 0., 1., 120., -10.], origin, [shape[0], shape[1]]);
let n: Array2<_> = transform.transform_image_bspline(j.view())?.into();
let d = (k.mapv(|x| x as f64) - n.mapv(|x| x as f64)).powi(2).sum();
assert!(d <= (shape[0] * shape[1]) as f64);
Ok(())
}
)*
}
}
interp_tests_bspline! {
interpbs_u8: u8,
interpbs_i8: i8,
interpbs_u16: u16,
interpbs_i16: i16,
interpbs_u32: u32,
interpbs_i32: i32,
interpbs_u64: u64,
interpbs_i64: i64,
interpbs_f32: f32,
interpbs_f64: f64,
}
macro_rules! interp_tests_nearest_neighbor {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(-120f32, 10f32)?.mapv(|x| x as $t);
let k = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let j0 = j.clone();
let k0 = k.clone();
let transform = AffineTransform::new([1., 0., 0., 1., 120., -10.], origin, [shape[0], shape[1]]);
// make sure j & k weren't mutated
assert!(j.iter().zip(j0.iter()).map(|(a, b)| a == b).all(|x| x));
assert!(k.iter().zip(k0.iter()).map(|(a, b)| a == b).all(|x| x));
let n: Array2<_> = transform.transform_image_nearest_neighbor(j.view())?.into();
let d = (k.mapv(|x| x as f64) - n.mapv(|x| x as f64)).powi(2).sum();
assert!(d <= (shape[0] * shape[1]) as f64);
Ok(())
}
)*
}
}
interp_tests_nearest_neighbor! {
interpnn_u8: u8,
interpnn_i8: i8,
interpnn_u16: u16,
interpnn_i16: i16,
interpnn_u32: u32,
interpnn_i32: i32,
interpnn_u64: u64,
interpnn_i64: i64,
interpnn_f32: f32,
interpnn_f64: f64,
}
macro_rules! registration_tests_translation {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let k = julia_image(10f32, 20f32)?.mapv(|x| x as $t);
let j0 = j.clone();
let k0 = k.clone();
let t = AffineTransform::register_translation(j.view(), k.view())?;
// make sure j & k weren't mutated
assert!(j.iter().zip(j0.iter()).map(|(a, b)| a == b).all(|x| x));
assert!(k.iter().zip(k0.iter()).map(|(a, b)| a == b).all(|x| x));
let mut m = Array2::eye(3);
m[[0, 2]] = -10f64;
m[[1, 2]] = -20f64;
let d = (t.matrix() - m).powi(2).sum();
assert!(d < 0.01, "d: {}, t: {:?}", d, t.parameters);
Ok(())
}
)*
}
}
registration_tests_translation! {
registration_translation_u8: u8,
registration_translation_i8: i8,
registration_translation_u16: u16,
registration_translation_i16: i16,
registration_translation_u32: u32,
registration_translation_i32: i32,
registration_translation_u64: u64,
registration_translation_i64: i64,
registration_translation_f32: f32,
registration_translation_f64: f64,
}
macro_rules! registration_tests_affine {
($($name:ident: $t:ty $(,)?)*) => {
$(
#[test]
fn $name() -> Result<()> {
let j = julia_image(0f32, 0f32)?.mapv(|x| x as $t);
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let s = AffineTransform::new([1.2, 0., 0., 1., 5., 7.], origin, [shape[0], shape[1]]);
let k: Array2<_> = s.transform_image_bspline(j.view())?.into();
let t = AffineTransform::register_affine(j.view(), k.view())?.inverse()?;
let d = (t.matrix() - s.matrix()).powi(2).sum();
assert!(d < 0.025, "d: {}, t: {:?}", d, t.parameters);
Ok(())
}
)*
}
}
registration_tests_affine! {
registration_tests_affine_u8: u8,
registration_tests_affine_i8: i8,
registration_tests_affine_u16: u16,
registration_tests_affine_i16: i16,
registration_tests_affine_u32: u32,
registration_tests_affine_i32: i32,
registration_tests_affine_u64: u64,
registration_tests_affine_i64: i64,
registration_tests_affine_f32: f32,
registration_tests_affine_f64: f64,
}
}
-362
View File
@@ -1,362 +0,0 @@
use crate::PixelType;
use anyhow::Result;
use libc::{c_double, c_uint};
use ndarray::{Array2, AsArray, Ix2};
use one_at_a_time_please::one_at_a_time;
use std::ptr;
macro_rules! register_fn {
($($name:ident: $T:ty $(,)?)*) => {
$(
fn $name(
width: c_uint,
height: c_uint,
fixed_arr: *const $T,
moving_arr: *const $T,
translation_or_affine: bool,
transform: &mut *mut c_double,
);
)*
};
}
macro_rules! interp_fn {
($($name:ident: $T:ty $(,)?)*) => {
$(
fn $name(
width: c_uint,
height: c_uint,
transform: *const c_double,
origin: *const c_double,
image: &mut *mut $T,
bspline_or_nn: bool,
);
)*
};
}
unsafe extern "C" {
register_fn! {
register_u8: u8,
register_i8: i8,
register_u16: u16,
register_i16: i16,
register_u32: u32,
register_i32: i32,
register_u64: u64,
register_i64: i64,
register_f32: f32,
register_f64: f64,
}
interp_fn! {
interp_u8: u8,
interp_i8: i8,
interp_u16: u16,
interp_i16: i16,
interp_u32: u32,
interp_i32: i32,
interp_u64: u64,
interp_i64: i64,
interp_f32: f32,
interp_f64: f64,
}
}
pub(crate) fn interp<'a, A, T>(
parameters: [f64; 6],
origin: [f64; 2],
image: A,
bspline_or_nn: bool,
) -> Result<Array2<T>>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
let image = image.into();
let shape: Vec<usize> = image.shape().to_vec();
let width = shape[1] as c_uint;
let height = shape[0] as c_uint;
let mut im: Vec<_> = image.into_iter().cloned().collect();
let im_ptr: *mut T = ptr::from_mut(unsafe { &mut *im.as_mut_ptr() });
match T::PT {
1 => unsafe {
interp_u8(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut u8),
bspline_or_nn,
);
},
2 => unsafe {
interp_i8(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut i8),
bspline_or_nn,
);
},
3 => unsafe {
interp_u16(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut u16),
bspline_or_nn,
);
},
4 => unsafe {
interp_i16(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut i16),
bspline_or_nn,
);
},
5 => unsafe {
interp_u32(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut u32),
bspline_or_nn,
);
},
6 => unsafe {
interp_i32(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut i32),
bspline_or_nn,
);
},
7 => unsafe {
interp_u64(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut u64),
bspline_or_nn,
);
},
8 => unsafe {
interp_i64(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut i64),
bspline_or_nn,
);
},
9 => unsafe {
interp_f32(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut f32),
bspline_or_nn,
);
},
10 => unsafe {
interp_f64(
width,
height,
parameters.as_ptr(),
origin.as_ptr(),
&mut (im_ptr as *mut f64),
bspline_or_nn,
);
},
_ => {}
}
Ok(Array2::from_shape_vec(
(shape[0], shape[1]),
im.into_iter().collect(),
)?)
}
#[one_at_a_time]
pub(crate) fn register<'a, A, T>(
fixed: A,
moving: A,
translation_or_affine: bool,
) -> Result<([f64; 6], [f64; 2], [usize; 2])>
where
T: 'a + PixelType,
A: AsArray<'a, T, Ix2>,
{
let fixed = fixed.into();
let moving = moving.into();
let shape: Vec<usize> = fixed.shape().to_vec();
let width = shape[1] as c_uint;
let height = shape[0] as c_uint;
let fixed: Vec<_> = fixed.into_iter().collect();
let moving: Vec<_> = moving.into_iter().collect();
let fixed_ptr = fixed.as_ptr();
let moving_ptr = moving.as_ptr();
let mut transform: Vec<c_double> = vec![0.0; 6];
let mut transform_ptr: *mut c_double = ptr::from_mut(unsafe { &mut *transform.as_mut_ptr() });
// let ma0 = &mut moving as *mut Vec<T> as usize;
// println!("ma0: {:#x}", ma0);
match T::PT {
1 => {
unsafe {
register_u8(
width,
height,
fixed_ptr as *const u8,
moving_ptr as *const u8,
translation_or_affine,
&mut transform_ptr,
)
};
}
2 => {
unsafe {
register_i8(
width,
height,
fixed_ptr as *const i8,
moving_ptr as *const i8,
translation_or_affine,
&mut transform_ptr,
)
};
}
3 => {
unsafe {
register_u16(
width,
height,
fixed_ptr as *const u16,
moving_ptr as *const u16,
translation_or_affine,
&mut transform_ptr,
)
};
}
4 => {
unsafe {
register_i16(
width,
height,
fixed_ptr as *const i16,
moving_ptr as *const i16,
translation_or_affine,
&mut transform_ptr,
)
};
}
5 => {
unsafe {
register_u32(
width,
height,
fixed_ptr as *const u32,
moving_ptr as *const u32,
translation_or_affine,
&mut transform_ptr,
)
};
}
6 => {
unsafe {
register_i32(
width,
height,
fixed_ptr as *const i32,
moving_ptr as *const i32,
translation_or_affine,
&mut transform_ptr,
)
};
}
7 => {
unsafe {
register_u64(
width,
height,
fixed_ptr as *const u64,
moving_ptr as *const u64,
translation_or_affine,
&mut transform_ptr,
)
};
}
8 => {
unsafe {
register_i64(
width,
height,
fixed_ptr as *const i64,
moving_ptr as *const i64,
translation_or_affine,
&mut transform_ptr,
)
};
}
9 => {
unsafe {
register_f32(
width,
height,
fixed_ptr as *const f32,
moving_ptr as *const f32,
translation_or_affine,
&mut transform_ptr,
)
};
}
10 => {
unsafe {
register_f64(
width,
height,
fixed_ptr as *const f64,
moving_ptr as *const f64,
translation_or_affine,
&mut transform_ptr,
)
};
}
_ => {}
}
// let ma1 = &mut moving as *mut Vec<T> as usize;
// println!("ma1: {:#x}", ma1);
// println!("{}", fixed.len());
// println!("{}", moving.len());
Ok((
[
transform[0] as f64,
transform[1] as f64,
transform[2] as f64,
transform[3] as f64,
transform[4] as f64,
transform[5] as f64,
],
[
((shape[0] - 1) as f64) / 2f64,
((shape[1] - 1) as f64) / 2f64,
],
[shape[0], shape[1]],
))
}