//! 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 { image: UniquePtr, pixel_type: PhantomData, } impl Deref for Image { type Target = UniquePtr; fn deref(&self) -> &Self::Target { &self.image } } impl Image { /// encapsulate an itk::simple::Image pub fn new(image: UniquePtr) -> 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 { let width = u32::from(self.image.GetWidth()) as usize; let height = u32::from(self.image.GetHeight()) as usize; let mut array = Array2::::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 for Image where T: 'a + PixelType, A: AsArray<'a, T, Ix2>, { fn from(value: A) -> Self { Self::from_array(value.into()) } } impl From> for Array2 where T: PixelType, { fn from(value: Image) -> 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(fixed: F, moving: M) -> Result where F: Into>, M: Into>, T: PixelType, { Self::register(fixed, moving, true) } /// find the translation which transforms moving into fixed pub fn register_translation(fixed: F, moving: M) -> Result where F: Into>, M: Into>, T: PixelType, { Self::register(fixed, moving, false) } /// find the transform which transforms moving into fixed pub fn register(fixed: F, moving: M, affine: bool) -> Result where F: Into>, M: Into>, 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, ¶meter_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::()) .collect::, _>>()?; 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 { 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(&self, image: I) -> Result> where I: Into>, T: PixelType, { Self::interp(self, image, false) } /// transform an image using bspline interpolation pub fn transform_image_nearest_neighbor(&self, image: I) -> Result> where I: Into>, T: PixelType, { Self::interp(self, image, true) } /// transform an image pub fn interp(&self, image: I, nearest_neighbor: bool) -> Result> where I: Into>, 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>::as_ref(affine_transform.as_ref().unwrap()); let interpolator = if nearest_neighbor { simple::InterpolatorEnum::sitkBSpline } else { simple::InterpolatorEnum::sitkNearestNeighbor }; Ok(Image::::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> where T: 'a + Clone + Into, 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 { 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 { 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 { fn det(a: ArrayView2) -> 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(vec: I) -> UniquePtr> where I: IntoIterator, T: cxx::vector::VectorElement + cxx::ExternType, { 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> { let imgx = 800; let imgy = 600; let scalex = 3.0 / imgx as f32; let scaley = 3.0 / imgy as f32; let mut im = Array2::::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, } }