• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / ndarray-vision / #60

pending completion
#60

push

web-flow
transform update for generic transforms (#60)

Co-authored-by: Todd Keeler <tdk@meta.com>

58 of 58 new or added lines in 2 files covered. (100.0%)

763 of 1111 relevant lines covered (68.68%)

1.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.75
/src/transform/mod.rs
1
use crate::core::{ColourModel, Image, ImageBase};
2
use ndarray::{array, prelude::*, s, Data};
3
use ndarray_linalg::*;
4
use num_traits::{Num, NumAssignOps};
5
use std::cmp::{max, min};
6
use std::fmt::Display;
7

8
pub mod affine;
9

10
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
11
pub enum TransformError {
12
    InvalidTransform,
13
    NonInvertibleTransform,
14
}
15

16
impl std::error::Error for TransformError {}
17

18
impl Display for TransformError {
19
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
20
        match self {
×
21
            TransformError::InvalidTransform => return write!(f, "invalid transform"),
×
22
            TransformError::NonInvertibleTransform => {
23
                return write!(
×
24
                    f,
25
                    "Non Invertible Transform, Forward transform not yet implemented "
26
                )
27
            }
28
        }
29
    }
30
}
31

32
pub trait Transform {
33
    fn apply(&self, p: (f64, f64)) -> (f64, f64);
34
    fn apply_inverse(&self, p: (f64, f64)) -> (f64, f64);
35
    fn inverse_exists(&self) -> bool;
36
}
37

38
/// Composition of two transforms.  Specifically, derives transform2(transform1(image)).
39
/// this is not equivalent to running the transforms separately, since the composition of the
40
/// transforms occurs before sampling.  IE, running transforms separately incur a resample per
41
/// transform, whereas composed Transforms only incur a single image resample.
42
pub struct ComposedTransform<T: Transform> {
43
    transform1: T,
44
    transform2: T,
45
}
46

47
impl<T: Transform> Transform for ComposedTransform<T> {
48
    fn apply(&self, p: (f64, f64)) -> (f64, f64) {
×
49
        return self.transform2.apply(self.transform1.apply(p));
×
50
    }
51

52
    fn apply_inverse(&self, p: (f64, f64)) -> (f64, f64) {
×
53
        return self
×
54
            .transform1
×
55
            .apply_inverse(self.transform2.apply_inverse(p));
×
56
    }
57

58
    fn inverse_exists(&self) -> bool {
×
59
        return self.transform1.inverse_exists() && self.transform2.inverse_exists();
×
60
    }
61
}
62

63
pub trait TransformExt<T: Transform>
64
where
65
    Self: Sized,
66
{
67
    /// Output type for the operation
68
    type Output;
69

70
    /// Transforms an image given the transformation matrix and output size.
71
    /// Uses the source index coordinate space
72
    /// Assume nearest-neighbour interpolation
73
    fn transform(
74
        &self,
75
        transform: &T,
76
        output_size: Option<(usize, usize)>,
77
    ) -> Result<Self::Output, TransformError>;
78
}
79

80
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81
struct Rect {
82
    x: isize,
83
    y: isize,
84
    w: usize,
85
    h: usize,
86
}
87

88
fn bounding_box<T: Transform>(dims: (f64, f64), transform: T) -> Rect {
×
89
    let tl = transform.apply((0.0, 0.0));
×
90
    let tr = transform.apply((0.0, dims.1));
×
91
    let br = transform.apply(dims);
×
92
    let bl = transform.apply((dims.0, 0.0));
×
93

94
    let tl = (tl.0.round() as isize, tl.1.round() as isize);
×
95
    let tr = (tr.0.round() as isize, tr.1.round() as isize);
×
96
    let br = (br.0.round() as isize, br.1.round() as isize);
×
97
    let bl = (bl.0.round() as isize, bl.1.round() as isize);
×
98

99
    let leftmost = min(min(tl.0, tr.0), min(br.0, bl.0));
×
100
    let topmost = min(min(tl.1, tr.1), min(br.1, bl.1));
×
101
    let rightmost = max(max(tl.0, tr.0), max(br.0, bl.0));
×
102
    let bottommost = max(max(tl.1, tr.1), max(br.1, bl.1));
×
103

104
    Rect {
105
        x: leftmost,
106
        y: topmost,
107
        w: (rightmost - leftmost) as usize,
×
108
        h: (bottommost - topmost) as usize,
×
109
    }
110
}
111

112
impl<T, U, V> TransformExt<V> for ArrayBase<U, Ix3>
113
where
114
    T: Copy + Clone + Num + NumAssignOps,
115
    U: Data<Elem = T>,
116
    V: Transform,
117
{
118
    type Output = Array<T, Ix3>;
119

120
    fn transform(
2✔
121
        &self,
122
        transform: &V,
123
        output_size: Option<(usize, usize)>,
124
    ) -> Result<Self::Output, TransformError> {
125
        let mut output = match output_size {
2✔
126
            Some((r, c)) => Self::Output::zeros((r, c, self.shape()[2])),
4✔
127
            None => Self::Output::zeros(self.raw_dim()),
1✔
128
        };
129

130
        for r in 0..output.shape()[0] {
6✔
131
            for c in 0..output.shape()[1] {
2✔
132
                let (x, y) = transform.apply_inverse((c as f64, r as f64));
2✔
133
                let x = x.round() as isize;
2✔
134
                let y = y.round() as isize;
2✔
135
                if x >= 0
14✔
136
                    && y >= 0
2✔
137
                    && (x as usize) < self.shape()[1]
4✔
138
                    && (y as usize) < self.shape()[0]
4✔
139
                {
140
                    output
4✔
141
                        .slice_mut(s![r, c, ..])
2✔
142
                        .assign(&self.slice(s![y, x, ..]));
4✔
143
                }
144
            }
145
        }
146

147
        Ok(output)
2✔
148
    }
149
}
150

151
impl<T, U, C, V> TransformExt<V> for ImageBase<U, C>
152
where
153
    U: Data<Elem = T>,
154
    T: Copy + Clone + Num + NumAssignOps,
155
    C: ColourModel,
156
    V: Transform,
157
{
158
    type Output = Image<T, C>;
159

160
    fn transform(
2✔
161
        &self,
162
        transform: &V,
163
        output_size: Option<(usize, usize)>,
164
    ) -> Result<Self::Output, TransformError> {
165
        let data = self.data.transform(transform, output_size)?;
2✔
166
        let result = Self::Output::from_data(data).to_owned();
4✔
167
        Ok(result)
2✔
168
    }
169
}
170

171
#[cfg(test)]
172
mod tests {
173
    use super::affine;
174
    use super::*;
175
    use crate::core::colour_models::Gray;
176
    use std::f64::consts::PI;
177

178
    #[test]
179
    fn translation() {
180
        let src_data = vec![2.0, 0.0, 1.0, 0.0, 5.0, 0.0, 1.0, 2.0, 3.0];
181
        let src = Image::<f64, Gray>::from_shape_data(3, 3, src_data);
182

183
        let trans = affine::transform_from_2dmatrix(affine::translation(2.0, 1.0));
184

185
        let res = src.transform(&trans, Some((3, 3)));
186
        assert!(res.is_ok());
187
        let res = res.unwrap();
188

189
        let expected = vec![0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0];
190
        let expected = Image::<f64, Gray>::from_shape_data(3, 3, expected);
191

192
        assert_eq!(expected, res)
193
    }
194

195
    #[test]
196
    fn rotate() {
197
        let src = Image::<u8, Gray>::from_shape_data(5, 5, (0..25).collect());
198
        let trans = affine::transform_from_2dmatrix(affine::rotate_around_centre(PI, (2.0, 2.0)));
199
        let upside_down = src.transform(&trans, Some((5, 5))).unwrap();
200

201
        let res = upside_down.transform(&trans, Some((5, 5))).unwrap();
202

203
        assert_eq!(src, res);
204

205
        let trans_2 =
206
            affine::transform_from_2dmatrix(affine::rotate_around_centre(PI / 2.0, (2.0, 2.0)));
207
        let trans_3 =
208
            affine::transform_from_2dmatrix(affine::rotate_around_centre(-PI / 2.0, (2.0, 2.0)));
209

210
        let upside_down_sideways = upside_down.transform(&trans_2, Some((5, 5))).unwrap();
211
        let src_sideways = src.transform(&trans_3, Some((5, 5))).unwrap();
212

213
        assert_eq!(upside_down_sideways, src_sideways);
214
    }
215

216
    #[test]
217
    fn scale() {
218
        let src = Image::<u8, Gray>::from_shape_data(4, 4, (0..16).collect());
219
        let trans = affine::transform_from_2dmatrix(affine::scale(0.5, 2.0));
220
        let res = src.transform(&trans, None).unwrap();
221

222
        assert_eq!(res.rows(), 4);
223
        assert_eq!(res.cols(), 4);
224
    }
225
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc