calib_targets_core/
grid_alignment.rs

1use serde::{Deserialize, Serialize};
2
3/// Integer 2D grid transform (a 2×2 matrix) used for aligning detected grids to a board model.
4///
5/// This represents a linear transform on integer coordinates:
6/// `(i', j') = (a*i + b*j, c*i + d*j)`.
7///
8/// For most calibration targets, valid transforms are the 8 elements of the dihedral group `D4`
9/// (rotations/reflections on the square grid). Those are provided in [`GRID_TRANSFORMS_D4`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
11pub struct GridTransform {
12    pub a: i32,
13    pub b: i32,
14    pub c: i32,
15    pub d: i32,
16}
17
18impl GridTransform {
19    pub const IDENTITY: GridTransform = GridTransform {
20        a: 1,
21        b: 0,
22        c: 0,
23        d: 1,
24    };
25
26    /// Apply the transform to `(i, j)`.
27    #[inline]
28    pub fn apply(&self, i: i32, j: i32) -> [i32; 2] {
29        [self.a * i + self.b * j, self.c * i + self.d * j]
30    }
31
32    /// Invert the transform if it is unimodular (det = ±1).
33    pub fn inverse(&self) -> Option<GridTransform> {
34        let det = self.a * self.d - self.b * self.c;
35        if det != 1 && det != -1 {
36            return None;
37        }
38        Some(GridTransform {
39            a: self.d / det,
40            b: -self.b / det,
41            c: -self.c / det,
42            d: self.a / det,
43        })
44    }
45}
46
47/// A grid alignment `dst = transform(src) + translation`.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct GridAlignment {
50    pub transform: GridTransform,
51    pub translation: [i32; 2],
52}
53
54impl GridAlignment {
55    pub const IDENTITY: GridAlignment = GridAlignment {
56        transform: GridTransform::IDENTITY,
57        translation: [0, 0],
58    };
59
60    /// Map grid coordinates `(i, j)` using this alignment.
61    #[inline]
62    pub fn map(&self, i: i32, j: i32) -> [i32; 2] {
63        let [x, y] = self.transform.apply(i, j);
64        [x + self.translation[0], y + self.translation[1]]
65    }
66
67    pub fn inverse(&self) -> Option<GridAlignment> {
68        let inv = self.transform.inverse()?;
69        let [tx, ty] = self.translation;
70        let [itx, ity] = inv.apply(-tx, -ty);
71        Some(GridAlignment {
72            transform: inv,
73            translation: [itx, ity],
74        })
75    }
76}
77
78/// The 8 dihedral transforms `D4` on the integer grid.
79pub const GRID_TRANSFORMS_D4: [GridTransform; 8] = [
80    // rotations: 0°, 90°, 180°, 270°
81    GridTransform {
82        a: 1,
83        b: 0,
84        c: 0,
85        d: 1,
86    },
87    GridTransform {
88        a: 0,
89        b: 1,
90        c: -1,
91        d: 0,
92    },
93    GridTransform {
94        a: -1,
95        b: 0,
96        c: 0,
97        d: -1,
98    },
99    GridTransform {
100        a: 0,
101        b: -1,
102        c: 1,
103        d: 0,
104    },
105    // reflections (and combinations)
106    GridTransform {
107        a: -1,
108        b: 0,
109        c: 0,
110        d: 1,
111    },
112    GridTransform {
113        a: 1,
114        b: 0,
115        c: 0,
116        d: -1,
117    },
118    GridTransform {
119        a: 0,
120        b: 1,
121        c: 1,
122        d: 0,
123    },
124    GridTransform {
125        a: 0,
126        b: -1,
127        c: -1,
128        d: 0,
129    },
130];