- First commit

This commit is contained in:
Wim Pomp
2025-03-01 19:40:37 +01:00
commit e2184b2d33
11 changed files with 1474 additions and 0 deletions

282
src/lib.rs Normal file
View File

@@ -0,0 +1,282 @@
mod sys;
use crate::sys::{PixelType, interp, register};
use anyhow::{Result, anyhow};
use ndarray::{Array2, ArrayView2, array, s};
use std::ops::Mul;
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct Transform {
pub parameters: [f64; 6],
pub dparameters: [f64; 6],
pub origin: [f64; 2],
pub shape: [usize; 2],
}
impl Mul for Transform {
type Output = Transform;
#[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 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<T: PixelType>(
fixed: ArrayView2<T>,
moving: ArrayView2<T>,
) -> Result<Transform> {
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<T: PixelType>(
fixed: ArrayView2<T>,
moving: ArrayView2<T>,
) -> Result<Transform> {
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(file: PathBuf) -> Result<Self> {
todo!()
}
/// write a transform to a file
pub fn to_file(&self, file: PathBuf) -> Result<()> {
todo!()
}
/// 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<T: PixelType>(&self, image: ArrayView2<T>) -> Result<Array2<T>> {
interp(self.parameters, self.origin, image, false)
}
/// transform an image using bspline interpolation
pub fn transform_image_nearest_neighbor<T: PixelType>(
&self,
image: ArrayView2<T>,
) -> Result<Array2<T>> {
interp(self.parameters, self.origin, image, true)
}
/// get coordinates resulting from transforming input coordinates
pub fn transform_coordinates<T>(&self, coordinates: ArrayView2<T>) -> Result<Array2<f64>>
where
T: Clone + Into<f64>,
{
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 d0 = det(m.slice(s![1.., 1..]));
if d0 == 0f64 {
return Err(anyhow!("transform matrix is not invertible"));
}
let d2 = det(m.slice(s![..2, ..2]));
let parameters = [
d0 / d2,
-det(m.slice(s![..;2, 1..])) / d2,
-det(m.slice(s![1.., ..;2])) / d2,
det(m.slice(s![..;2, ..;2])) / d2,
det(m.slice(s![..2, 1..])) / d2,
-det(m.slice(s![..2, ..;2])) / d2,
];
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 tiffwrite::IJTiffFile;
/// An example of generating julia fractals.
fn julia_image() -> 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 cx = y as f32 * scalex - 1.5;
let cy = x as f32 * scaley - 1.5;
let c = Complex::new(-0.4, 0.6);
let mut z = Complex::new(cx, cy);
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_interp() -> Result<()> {
let j = julia_image()?;
let mut tif = IJTiffFile::new("interp_test.tif")?;
tif.save(&j, 0, 0, 0)?;
let shape = j.shape();
let origin = [
((shape[1] - 1) as f64) / 2f64,
((shape[0] - 1) as f64) / 2f64,
];
let transform = Transform::new([1.2, 0., 0., 1., 10., 0.], origin, [shape[0], shape[1]]);
let k = transform.transform_image_bspline(j.view())?;
tif.save(&k, 1, 0, 0)?;
let t = Transform::register_affine(k.view(), j.view())?;
println!("t: {:#?}", t);
println!("m: {:#?}", t.matrix());
println!("i: {:#?}", t.inverse()?.matrix());
Ok(())
}
}

359
src/sys.rs Normal file
View File

@@ -0,0 +1,359 @@
use anyhow::Result;
use libc::{c_double, c_uint};
use ndarray::{Array2, ArrayView2};
use std::ptr;
macro_rules! register_fn {
($T:ty, $t:ident) => {
fn $t(
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 {
($T:ty, $t:ident) => {
fn $t(
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!(u8, register_u8);
register_fn!(i8, register_i8);
register_fn!(u16, register_u16);
register_fn!(i16, register_i16);
register_fn!(u32, register_u32);
register_fn!(i32, register_i32);
register_fn!(u64, register_u64);
register_fn!(i64, register_i64);
register_fn!(f32, register_f32);
register_fn!(f64, register_f64);
interp_fn!(u8, interp_u8);
interp_fn!(i8, interp_i8);
interp_fn!(u16, interp_u16);
interp_fn!(i16, interp_i16);
interp_fn!(u32, interp_u32);
interp_fn!(i32, interp_i32);
interp_fn!(u64, interp_u64);
interp_fn!(i64, interp_i64);
interp_fn!(f32, interp_f32);
interp_fn!(f64, interp_f64);
}
pub trait PixelType: Clone {
const PT: u8;
}
macro_rules! sitk_impl {
($T:ty, $sitk:expr) => {
impl PixelType for $T {
const PT: u8 = $sitk;
}
};
}
sitk_impl!(u8, 1);
sitk_impl!(i8, 2);
sitk_impl!(u16, 3);
sitk_impl!(i16, 4);
sitk_impl!(u32, 5);
sitk_impl!(i32, 6);
sitk_impl!(u64, 7);
sitk_impl!(i64, 8);
#[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);
sitk_impl!(f32, 9);
sitk_impl!(f64, 10);
pub(crate) fn interp<T: PixelType>(
parameters: [f64; 6],
origin: [f64; 2],
image: ArrayView2<T>,
bspline_or_nn: bool,
) -> Result<Array2<T>> {
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(),
)?)
}
pub(crate) fn register<T: PixelType>(
fixed: ArrayView2<T>,
moving: ArrayView2<T>,
translation_or_affine: bool,
) -> Result<([f64; 6], [f64; 2], [usize; 2])> {
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 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() });
match T::PT {
1 => {
unsafe {
register_u8(
width,
height,
fixed.as_ptr() as *const u8,
moving.as_ptr() as *const u8,
translation_or_affine,
&mut transform_ptr,
)
};
}
2 => {
unsafe {
register_i8(
width,
height,
fixed.as_ptr() as *const i8,
moving.as_ptr() as *const i8,
translation_or_affine,
&mut transform_ptr,
)
};
}
3 => {
unsafe {
register_u16(
width,
height,
fixed.as_ptr() as *const u16,
moving.as_ptr() as *const u16,
translation_or_affine,
&mut transform_ptr,
)
};
}
4 => {
unsafe {
register_i16(
width,
height,
fixed.as_ptr() as *const i16,
moving.as_ptr() as *const i16,
translation_or_affine,
&mut transform_ptr,
)
};
}
5 => {
unsafe {
register_u32(
width,
height,
fixed.as_ptr() as *const u32,
moving.as_ptr() as *const u32,
translation_or_affine,
&mut transform_ptr,
)
};
}
6 => {
unsafe {
register_i32(
width,
height,
fixed.as_ptr() as *const i32,
moving.as_ptr() as *const i32,
translation_or_affine,
&mut transform_ptr,
)
};
}
7 => {
unsafe {
register_u64(
width,
height,
fixed.as_ptr() as *const u64,
moving.as_ptr() as *const u64,
translation_or_affine,
&mut transform_ptr,
)
};
}
8 => {
unsafe {
register_i64(
width,
height,
fixed.as_ptr() as *const i64,
moving.as_ptr() as *const i64,
translation_or_affine,
&mut transform_ptr,
)
};
}
9 => {
unsafe {
register_f32(
width,
height,
fixed.as_ptr() as *const f32,
moving.as_ptr() as *const f32,
translation_or_affine,
&mut transform_ptr,
)
};
}
10 => {
unsafe {
register_f64(
width,
height,
fixed.as_ptr() as *const f64,
moving.as_ptr() as *const f64,
translation_or_affine,
&mut transform_ptr,
)
};
}
_ => {}
}
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]],
))
}