|
| 1 | +use crate::core::{ColourModel, Image}; |
| 2 | +use crate::transform::affine::translation; |
| 3 | +use ndarray::{array, prelude::*, s, Data}; |
| 4 | +use ndarray_linalg::solve::Inverse; |
| 5 | +use num_traits::{Num, NumAssignOps}; |
| 6 | +use std::cmp::{max, min}; |
| 7 | +use std::marker::PhantomData; |
| 8 | + |
| 9 | +pub mod affine; |
| 10 | + |
| 11 | +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] |
| 12 | +pub enum Error { |
| 13 | + InvalidTransformation, |
| 14 | + NonInvertibleTransformation, |
| 15 | +} |
| 16 | + |
| 17 | +pub trait TransformExt |
| 18 | +where |
| 19 | + Self: Sized, |
| 20 | +{ |
| 21 | + /// Output type for the operation |
| 22 | + type Output; |
| 23 | + |
| 24 | + /// Transforms an image given the transformation matrix and output size. |
| 25 | + /// Assume nearest-neighbour interpolation |
| 26 | + fn transform( |
| 27 | + &self, |
| 28 | + transform: ArrayView2<f64>, |
| 29 | + output_size: Option<(usize, usize)>, |
| 30 | + ) -> Result<Self::Output, Error>; |
| 31 | +} |
| 32 | + |
| 33 | +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] |
| 34 | +struct Rect { |
| 35 | + x: isize, |
| 36 | + y: isize, |
| 37 | + w: usize, |
| 38 | + h: usize, |
| 39 | +} |
| 40 | + |
| 41 | +fn source_coordinate(p: (f64, f64), trans: ArrayView2<f64>) -> (f64, f64) { |
| 42 | + let p = match trans.shape()[0] { |
| 43 | + 2 => array![[p.0], [p.1]], |
| 44 | + 3 => array![[p.0], [p.1], [1.0]], |
| 45 | + _ => unreachable!(), |
| 46 | + }; |
| 47 | + |
| 48 | + let result = trans.dot(&p); |
| 49 | + let x = result[[0, 0]]; |
| 50 | + let y = result[[1, 0]]; |
| 51 | + let w = match trans.shape()[0] { |
| 52 | + 2 => 1.0, |
| 53 | + 3 => result[[2, 0]], |
| 54 | + _ => unreachable!(), |
| 55 | + }; |
| 56 | + if (w - 1.0).abs() > std::f64::EPSILON { |
| 57 | + (x / w, y / w) |
| 58 | + } else { |
| 59 | + (x, y) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +fn bounding_box(dims: (f64, f64), transform: ArrayView2<f64>) -> Rect { |
| 64 | + let tl = source_coordinate((0.0, 0.0), transform); |
| 65 | + let tr = source_coordinate((0.0, dims.1), transform); |
| 66 | + let br = source_coordinate(dims, transform); |
| 67 | + let bl = source_coordinate((dims.0, 0.0), transform); |
| 68 | + |
| 69 | + let tl = (tl.0.round() as isize, tl.1.round() as isize); |
| 70 | + let tr = (tr.0.round() as isize, tr.1.round() as isize); |
| 71 | + let br = (br.0.round() as isize, br.1.round() as isize); |
| 72 | + let bl = (bl.0.round() as isize, bl.1.round() as isize); |
| 73 | + |
| 74 | + let leftmost = min(min(tl.0, tr.0), min(br.0, bl.0)); |
| 75 | + let topmost = min(min(tl.1, tr.1), min(br.1, bl.1)); |
| 76 | + let rightmost = max(max(tl.0, tr.0), max(br.0, bl.0)); |
| 77 | + let bottommost = max(max(tl.1, tr.1), max(br.1, bl.1)); |
| 78 | + Rect { |
| 79 | + x: leftmost, |
| 80 | + y: topmost, |
| 81 | + w: (rightmost - leftmost) as usize, |
| 82 | + h: (bottommost - topmost) as usize, |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +impl<T, U> TransformExt for ArrayBase<U, Ix3> |
| 87 | +where |
| 88 | + T: Copy + Clone + Num + NumAssignOps, |
| 89 | + U: Data<Elem = T>, |
| 90 | +{ |
| 91 | + type Output = Array3<T>; |
| 92 | + |
| 93 | + fn transform( |
| 94 | + &self, |
| 95 | + transform: ArrayView2<f64>, |
| 96 | + output_size: Option<(usize, usize)>, |
| 97 | + ) -> Result<Self::Output, Error> { |
| 98 | + let shape = transform.shape(); |
| 99 | + if !(shape[0] == 3 || shape[0] == 2) { |
| 100 | + Err(Error::InvalidTransformation) |
| 101 | + } else { |
| 102 | + let (mut result, new_transform) = match output_size { |
| 103 | + Some((r, c)) => ( |
| 104 | + Self::Output::zeros((r, c, self.shape()[2])), |
| 105 | + transform.into_owned(), |
| 106 | + ), |
| 107 | + None => { |
| 108 | + let dims = (self.shape()[0] as f64, self.shape()[1] as f64); |
| 109 | + let bounds = bounding_box(dims, transform.view()); |
| 110 | + let new_trans = translation(bounds.x as f64, -bounds.y as f64).dot(&transform); |
| 111 | + ( |
| 112 | + Self::Output::zeros((bounds.h, bounds.w, self.shape()[2])), |
| 113 | + new_trans, |
| 114 | + ) |
| 115 | + } |
| 116 | + }; |
| 117 | + |
| 118 | + let transform = new_transform |
| 119 | + .inv() |
| 120 | + .map_err(|_| Error::NonInvertibleTransformation)?; |
| 121 | + for r in 0..result.shape()[0] { |
| 122 | + for c in 0..result.shape()[1] { |
| 123 | + let (x, y) = source_coordinate((c as f64, r as f64), transform.view()); |
| 124 | + let x = x.round() as isize; |
| 125 | + let y = y.round() as isize; |
| 126 | + if x >= 0 |
| 127 | + && y >= 0 |
| 128 | + && (x as usize) < self.shape()[1] |
| 129 | + && (y as usize) < self.shape()[0] |
| 130 | + { |
| 131 | + result |
| 132 | + .slice_mut(s![r, c, ..]) |
| 133 | + .assign(&self.slice(s![y, x, ..])); |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + Ok(result) |
| 138 | + } |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +impl<T, C> TransformExt for Image<T, C> |
| 143 | +where |
| 144 | + T: Copy + Clone + Num + NumAssignOps, |
| 145 | + C: ColourModel, |
| 146 | +{ |
| 147 | + type Output = Self; |
| 148 | + |
| 149 | + fn transform( |
| 150 | + &self, |
| 151 | + transform: ArrayView2<f64>, |
| 152 | + output_size: Option<(usize, usize)>, |
| 153 | + ) -> Result<Self::Output, Error> { |
| 154 | + let data = self.data.transform(transform, output_size)?; |
| 155 | + Ok(Self { |
| 156 | + data, |
| 157 | + model: PhantomData, |
| 158 | + }) |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +#[cfg(test)] |
| 163 | +mod tests { |
| 164 | + extern crate openblas_src; |
| 165 | + |
| 166 | + use super::affine; |
| 167 | + use super::*; |
| 168 | + use crate::core::colour_models::Gray; |
| 169 | + use std::f64::consts::PI; |
| 170 | + |
| 171 | + #[test] |
| 172 | + fn translation() { |
| 173 | + let src_data = vec![2.0, 0.0, 1.0, 0.0, 5.0, 0.0, 1.0, 2.0, 3.0]; |
| 174 | + let src = Image::<f64, Gray>::from_shape_data(3, 3, src_data); |
| 175 | + |
| 176 | + let trans = affine::translation(2.0, 1.0); |
| 177 | + |
| 178 | + let res = src.transform(trans.view(), Some((3, 3))); |
| 179 | + assert!(res.is_ok()); |
| 180 | + let res = res.unwrap(); |
| 181 | + |
| 182 | + let expected = vec![0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0]; |
| 183 | + let expected = Image::<f64, Gray>::from_shape_data(3, 3, expected); |
| 184 | + |
| 185 | + assert_eq!(expected, res) |
| 186 | + } |
| 187 | + |
| 188 | + #[test] |
| 189 | + fn rotate() { |
| 190 | + let src = Image::<u8, Gray>::from_shape_data(5, 5, (0..25).collect()); |
| 191 | + let trans = affine::rotate_around_centre(PI, (2.0, 2.0)); |
| 192 | + let upside_down = src.transform(trans.view(), Some((5, 5))).unwrap(); |
| 193 | + |
| 194 | + let res = upside_down.transform(trans.view(), Some((5, 5))).unwrap(); |
| 195 | + |
| 196 | + assert_eq!(src, res); |
| 197 | + |
| 198 | + let trans_2 = affine::rotate_around_centre(PI / 2.0, (2.0, 2.0)); |
| 199 | + let trans_3 = affine::rotate_around_centre(-PI / 2.0, (2.0, 2.0)); |
| 200 | + |
| 201 | + let upside_down_sideways = upside_down.transform(trans_2.view(), Some((5, 5))).unwrap(); |
| 202 | + let src_sideways = src.transform(trans_3.view(), Some((5, 5))).unwrap(); |
| 203 | + |
| 204 | + assert_eq!(upside_down_sideways, src_sideways); |
| 205 | + } |
| 206 | + |
| 207 | + #[test] |
| 208 | + fn scale() { |
| 209 | + let src = Image::<u8, Gray>::from_shape_data(4, 4, (0..16).collect()); |
| 210 | + let trans = affine::scale(0.5, 2.0); |
| 211 | + let res = src.transform(trans.view(), None).unwrap(); |
| 212 | + |
| 213 | + assert_eq!(res.rows(), 8); |
| 214 | + assert_eq!(res.cols(), 2); |
| 215 | + } |
| 216 | +} |
0 commit comments