quad_to_quad_transformer/
lib.rs

1use anyhow::anyhow;
2use log::{debug, warn};
3use na::{Matrix3, Point2};
4
5type Point2D = (f32, f32);
6
7extern crate nalgebra as na;
8
9/**
10A standardised "1x1 units" box to transform all coordinates into
11*/
12const DST_SIZE: f32 = 1.;
13pub const DEFAULT_DST_QUAD: RectCorners = [
14    (0., 0.),
15    (DST_SIZE, 0.),
16    (DST_SIZE, DST_SIZE),
17    (0., DST_SIZE),
18];
19
20/**
21clockwise: 'left top', 'right top', 'right bottom', 'left bottom',
22 */
23pub type RectCorners = [Point2D; 4];
24type Matrix8x8 = na::SMatrix<f32, 8, 8>;
25
26pub struct QuadTransformer {
27    transform_matrix: Option<Matrix3<f32>>,
28    ignore_outside_margin: Option<f32>,
29    dst_quad: Option<RectCorners>,
30}
31
32impl Default for QuadTransformer {
33    fn default() -> Self {
34        Self::new(None, None, None)
35    }
36}
37
38impl QuadTransformer {
39    pub fn new(
40        src_quad: Option<RectCorners>,
41        dst_quad: Option<RectCorners>,
42        ignore_outside_margin: Option<f32>,
43    ) -> QuadTransformer {
44        if ignore_outside_margin.is_none() {
45            warn!("No outside margin value set; points will not be restricted to src_quad");
46        }
47        if let Some(margin) = ignore_outside_margin {
48            warn!("An outside margin value was set; points further than {margin} distance outside of destination quad will be ignored");
49        }
50        let useable_dst_quad: RectCorners = match dst_quad {
51            Some(q) => q,
52            None => DEFAULT_DST_QUAD,
53        };
54        QuadTransformer {
55            transform_matrix: src_quad
56                .map(|quad| build_transform(&quad.clone(), &useable_dst_quad)),
57            dst_quad,
58            ignore_outside_margin,
59        }
60    }
61
62    pub fn set_new_quad(&mut self, src_quad: &RectCorners, dst_quad: Option<RectCorners>) {
63        let useable_dst_quad: RectCorners = match dst_quad {
64            Some(q) => q,
65            None => DEFAULT_DST_QUAD,
66        };
67
68        self.dst_quad = dst_quad;
69
70        self.transform_matrix = Some(build_transform(src_quad, &useable_dst_quad));
71    }
72
73    /** Take a single input point (within the source quad) and return the
74    transformed result (within the destination quad). */
75    pub fn transform(&self, point: &Point2D) -> anyhow::Result<Point2D> {
76        match self.transform_matrix {
77            Some(matrix) => {
78                let (x, y) = point;
79                let nalgebra_point = Point2::new(*x, *y);
80
81                let transformed = matrix.transform_point(&nalgebra_point);
82                Ok((transformed.x, transformed.y))
83            }
84            None => Err(anyhow!("No transform matrix")),
85        }
86    }
87
88    /** Using the `ignore_outside_margin` value (if set), return only the points that are
89    deemed to be "inside the destination quad". */
90    pub fn filter_points_inside(&self, points: &[Point2D]) -> Vec<Point2D> {
91        let points: Vec<Point2D> = points
92            .iter()
93            .filter(|point| match self.ignore_outside_margin {
94                Some(_) => self.point_is_inside_quad(point),
95                None => true,
96            })
97            .map(|p| (p.0, p.1))
98            .collect();
99        points
100    }
101
102    pub fn is_ready(&self) -> bool {
103        self.transform_matrix.is_some()
104    }
105
106    pub fn point_is_inside_quad(&self, point: &Point2D) -> bool {
107        let margin = self.ignore_outside_margin.unwrap_or(0.);
108        let (x, y) = point;
109        debug!("...Is {x}, {y} outside of {margin}?");
110        if let Some(dst_quad) = self.dst_quad {
111            let [a, b, _c, d] = dst_quad;
112            *x >= (a.0 - margin)
113                && *x <= (b.0 + margin)
114                && *y >= (a.1 - margin)
115                && *y <= (d.1 + margin)
116        } else {
117            // No destination quad set; use "default" [0;1]
118            *x >= (0. - margin)
119                && *x <= (DST_SIZE + margin)
120                && *y >= (0. - margin)
121                && *y <= (DST_SIZE + margin)
122        }
123    }
124}
125
126fn build_transform(src_quad: &RectCorners, dst_quad: &RectCorners) -> Matrix3<f32> {
127    // Mappings by row - each should have 8 terms
128
129    let r1: [f32; 8] = [
130        src_quad[0].0,
131        src_quad[0].1,
132        1.,
133        0.,
134        0.,
135        0.,
136        -src_quad[0].0 * dst_quad[0].0,
137        -src_quad[0].1 * dst_quad[0].0,
138    ];
139    let r2: [f32; 8] = [
140        0.,
141        0.,
142        0.,
143        src_quad[0].0,
144        src_quad[0].1,
145        1.,
146        -src_quad[0].0 * dst_quad[0].1,
147        -src_quad[0].1 * dst_quad[0].1,
148    ];
149    let r3: [f32; 8] = [
150        src_quad[1].0,
151        src_quad[1].1,
152        1.,
153        0.,
154        0.,
155        0.,
156        -src_quad[1].0 * dst_quad[1].0,
157        -src_quad[1].1 * dst_quad[1].0,
158    ];
159    let r4: [f32; 8] = [
160        0.,
161        0.,
162        0.,
163        src_quad[1].0,
164        src_quad[1].1,
165        1.,
166        -src_quad[1].0 * dst_quad[1].1,
167        -src_quad[1].1 * dst_quad[1].1,
168    ];
169    let r5: [f32; 8] = [
170        src_quad[2].0,
171        src_quad[2].1,
172        1.,
173        0.,
174        0.,
175        0.,
176        -src_quad[2].0 * dst_quad[2].0,
177        -src_quad[2].1 * dst_quad[2].0,
178    ];
179    let r6: [f32; 8] = [
180        0.,
181        0.,
182        0.,
183        src_quad[2].0,
184        src_quad[2].1,
185        1.,
186        -src_quad[2].0 * dst_quad[2].1,
187        -src_quad[2].1 * dst_quad[2].1,
188    ];
189    let r7: [f32; 8] = [
190        src_quad[3].0,
191        src_quad[3].1,
192        1.,
193        0.,
194        0.,
195        0.,
196        -src_quad[3].0 * dst_quad[3].0,
197        -src_quad[3].1 * dst_quad[3].0,
198    ];
199    let r8: [f32; 8] = [
200        0.,
201        0.,
202        0.,
203        src_quad[3].0,
204        src_quad[3].1,
205        1.,
206        -src_quad[3].0 * dst_quad[3].1,
207        -src_quad[3].1 * dst_quad[3].1,
208    ];
209    let combined = vec![r1, r2, r3, r4, r5, r6, r7, r8].into_iter().flatten();
210
211    let matrix_a = Matrix8x8::from_iterator(combined);
212
213    let dst_quad_elements = vec![
214        dst_quad[0].0,
215        dst_quad[0].1,
216        dst_quad[1].0,
217        dst_quad[1].1,
218        dst_quad[2].0,
219        dst_quad[2].1,
220        dst_quad[3].0,
221        dst_quad[3].1,
222    ]
223    .into_iter();
224
225    // let matrix_b: na::SMatrix<f32, 1, 8> = na::SMatrix::from_iterator(dst_quad_elements);
226    let matrix_b: na::SMatrix<f32, 1, 8> = na::SMatrix::from_iterator(dst_quad_elements);
227
228    // Solve for Ah = B
229    let coefficients = matrix_b * matrix_a.try_inverse().unwrap();
230    //
231    // Create a new 3x3 transform matrix using the elements from above
232    Matrix3::new(
233        coefficients[0],
234        coefficients[1],
235        coefficients[2],
236        coefficients[3],
237        coefficients[4],
238        coefficients[5],
239        coefficients[6],
240        coefficients[7],
241        1.,
242    )
243}
244
245#[cfg(test)]
246mod tests {
247
248    use crate::*;
249
250    use super::RectCorners;
251
252    #[test]
253    #[allow(clippy::excessive_precision)]
254    fn test_get_transform_matrix() {
255        // numbers as per https://github.com/jlouthan/perspective-transform#basic-usage
256
257        let src_quad = [158., 64., 494., 69., 495., 404., 158., 404.];
258        let src_quad: RectCorners = [
259            (src_quad[0], src_quad[1]),
260            (src_quad[2], src_quad[3]),
261            (src_quad[4], src_quad[5]),
262            (src_quad[6], src_quad[7]),
263        ];
264
265        let dst_quad = [100., 500., 152., 564., 148., 604., 100., 560.];
266        let dst_quad: RectCorners = [
267            (dst_quad[0], dst_quad[1]),
268            (dst_quad[2], dst_quad[3]),
269            (dst_quad[4], dst_quad[5]),
270            (dst_quad[6], dst_quad[7]),
271        ];
272
273        let transform_matrix = build_transform(&src_quad, &dst_quad);
274
275        let src_point = (250., 120.);
276
277        let result = {
278            let (x, y) = (src_point.0, src_point.1);
279            let nalgebra_point = nalgebra::Point2::new(x, y);
280
281            let transformed = transform_matrix.transform_point(&nalgebra_point);
282            (transformed.x, transformed.y)
283        };
284        assert_eq!(
285            (result.0.round(), result.1.round()),
286            (
287                117.27521125839255_f32.round(),
288                530.9202410878403_f32.round(),
289            ),
290        );
291    }
292
293    #[test]
294    fn test_get_transform_matrix_simple() {
295        // numbers as per https://github.com/jlouthan/perspective-transform#basic-usage
296
297        let src_quad: RectCorners = [(0., 0.), (1., 0.), (1., 1.), (0., 1.)];
298        let dst_quad: RectCorners = [(1., 2.), (1., 4.), (3., 4.), (3., 2.)];
299
300        let transform_matrix = build_transform(&src_quad, &dst_quad);
301
302        // The transform matrix is a little different to the example in https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/
303        // because their point order is somehow different. The result is the same.
304        // assert_eq!(
305        //     transform_matrix,
306        //     Matrix3::new(2., 0., 1., 0., 2., 2., 0., 0., 1.)
307        // );
308
309        let src_point = (0.5, 0.5);
310
311        let result = {
312            let (x, y) = (src_point.0, src_point.1);
313            let nalgebra_point = nalgebra::Point2::new(x, y);
314
315            let transformed = transform_matrix.transform_point(&nalgebra_point);
316            (transformed.x, transformed.y)
317        };
318
319        assert_eq!(result, (2., 3.));
320    }
321
322    #[test]
323    fn test_inside_standard_quad() {
324        let point: Point2D = (0.5, 0.5);
325
326        let t = QuadTransformer::default();
327        //... Equivalent to:
328        // let t = QuadTransformer::new(None, None, None);
329        assert!(t.point_is_inside_quad(&point));
330
331        // Outside
332        let point: Point2D = (-0.5, 0.5);
333        assert!(!t.point_is_inside_quad(&point));
334
335        // Right on the edge
336        let point: Point2D = (1.0, 1.0);
337        assert!(t.point_is_inside_quad(&point));
338    }
339
340    #[test]
341    fn test_inside_dst_quad() {
342        let centered_dst_quad: RectCorners =
343            [(-100., -100.), (100., -100.), (100., 100.), (-100., 100.)];
344
345        let t = QuadTransformer::new(None, Some(centered_dst_quad), None);
346
347        // Inside
348        let point: Point2D = (0., 0.);
349        assert!(t.point_is_inside_quad(&point));
350
351        // Outside
352        let point: Point2D = (101., 0.);
353        assert!(!t.point_is_inside_quad(&point));
354
355        // Right on the edge
356        let point: Point2D = (100., 0.);
357        assert!(t.point_is_inside_quad(&point));
358    }
359
360    #[test]
361    fn test_inside_with_margin_standard_quad() {
362        let t = QuadTransformer::new(None, None, Some(0.25));
363
364        // 0.1 outside, but margin is 0.25, so ACCEPTED
365        let point: Point2D = (1.1, 0.);
366        assert!(t.point_is_inside_quad(&point));
367
368        // 0.5 ouside, and margin is 0.25, so REJECTED
369        let point: Point2D = (1.5, 0.);
370        assert!(!t.point_is_inside_quad(&point));
371    }
372
373    #[test]
374    fn test_inside_with_margin_dst_quad() {
375        let centered_dst_quad: RectCorners =
376            [(-100., -100.), (100., -100.), (100., 100.), (-100., 100.)];
377
378        let t = QuadTransformer::new(None, Some(centered_dst_quad), Some(10.0));
379
380        // 1 outside, but margin is 10, so ACCEPTED
381        let point: Point2D = (101., 0.);
382        assert!(t.point_is_inside_quad(&point));
383
384        // 15 ouside, and margin is 10, so REJECTED
385        let point: Point2D = (115., 0.);
386        assert!(!t.point_is_inside_quad(&point));
387    }
388}