checkerboard_calibrate/chessboard/
order.rs1use std::collections::HashMap;
23
24use super::link::LinkedQuad;
25
26const CELL: [(i32, i32); 4] = [(0, 0), (1, 0), (1, 1), (0, 1)];
31
32pub fn order_quad_corners(quad: &mut LinkedQuad) {
36 let cx = quad.corners.iter().map(|c| c.0).sum::<f32>() / 4.0;
37 let cy = quad.corners.iter().map(|c| c.1).sum::<f32>() / 4.0;
38
39 let mut idx = [0usize, 1, 2, 3];
40 idx.sort_by(|&a, &b| {
41 let aa = (quad.corners[a].1 - cy).atan2(quad.corners[a].0 - cx);
42 let ab = (quad.corners[b].1 - cy).atan2(quad.corners[b].0 - cx);
43 aa.partial_cmp(&ab).unwrap()
44 });
45
46 let corners = quad.corners;
47 let neighbors = quad.neighbors;
48 for (new_i, &old_i) in idx.iter().enumerate() {
49 quad.corners[new_i] = corners[old_i];
50 quad.neighbors[new_i] = neighbors[old_i];
51 }
52}
53
54pub fn order_all_corners(quads: &mut [LinkedQuad]) {
56 for q in quads.iter_mut() {
57 order_quad_corners(q);
58 }
59}
60
61pub type QuadGrid = [(i32, i32); 4];
63
64pub fn assign_grid(quads: &[LinkedQuad], component: &[usize]) -> HashMap<usize, QuadGrid> {
75 let mut coords: HashMap<usize, QuadGrid> = HashMap::new();
76 if component.is_empty() {
77 return coords;
78 }
79
80 let seed = component[0];
81 coords.insert(seed, CELL);
82 let mut stack = vec![seed];
83
84 while let Some(a) = stack.pop() {
85 let a_grid = coords[&a];
86 for (ci, &shared) in a_grid.iter().enumerate() {
87 let Some(b) = quads[a].neighbors[ci] else {
88 continue;
89 };
90 if coords.contains_key(&b) {
91 continue;
92 }
93 let cj = quads[b]
95 .neighbors
96 .iter()
97 .position(|&n| n == Some(a))
98 .expect("neighbor link is reciprocal");
99
100 let ox = shared.0 - CELL[cj].0;
102 let oy = shared.1 - CELL[cj].1;
103 let b_grid: QuadGrid = std::array::from_fn(|k| (ox + CELL[k].0, oy + CELL[k].1));
104
105 coords.insert(b, b_grid);
106 stack.push(b);
107 }
108 }
109 coords
110}
111
112pub fn corner_lattice(
115 quads: &[LinkedQuad],
116 coords: &HashMap<usize, QuadGrid>,
117) -> HashMap<(i32, i32), ((f32, f32), usize)> {
118 let mut acc: HashMap<(i32, i32), ((f64, f64), usize)> = HashMap::new();
120 for (&qi, grid) in coords {
121 for (k, &cell) in grid.iter().enumerate() {
122 let entry = acc.entry(cell).or_insert(((0.0, 0.0), 0));
123 entry.0.0 += quads[qi].corners[k].0 as f64;
124 entry.0.1 += quads[qi].corners[k].1 as f64;
125 entry.1 += 1;
126 }
127 }
128
129 acc.into_iter()
130 .map(|(lattice, ((sx, sy), count))| {
131 let n = count as f64;
132 (lattice, (((sx / n) as f32, (sy / n) as f32), count))
133 })
134 .collect()
135}
136
137pub fn inner_corner_lattice(
144 quads: &[LinkedQuad],
145 coords: &HashMap<usize, QuadGrid>,
146) -> Vec<((i32, i32), (f32, f32))> {
147 corner_lattice(quads, coords)
148 .into_iter()
149 .filter(|(_, (_, count))| *count >= 2)
150 .map(|(lattice, (pos, _))| (lattice, pos))
151 .collect()
152}
153
154pub fn ordered_inner_corners(
157 quads: &[LinkedQuad],
158 coords: &HashMap<usize, QuadGrid>,
159) -> Vec<(f32, f32)> {
160 let mut inner = inner_corner_lattice(quads, coords);
161 inner.sort_by_key(|a| (a.0.1, a.0.0));
163 inner.into_iter().map(|(_, pt)| pt).collect()
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::chessboard::link::{connected_components, link_quads};
170 use crate::chessboard::quad::Quad;
171
172 fn linked(corners: [(f32, f32); 4]) -> LinkedQuad {
173 LinkedQuad {
174 corners,
175 neighbors: [None; 4],
176 edge_len: 0.0,
177 }
178 }
179
180 fn dist2(a: (f32, f32), b: (f32, f32)) -> f32 {
181 (a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)
182 }
183
184 #[test]
185 fn orders_into_rotational_sequence() {
186 let mut q = linked([(10.0, 10.0), (0.0, 0.0), (10.0, 0.0), (0.0, 10.0)]);
187 order_quad_corners(&mut q);
188 for i in 0..4 {
189 approx::assert_abs_diff_eq!(
190 dist2(q.corners[i], q.corners[(i + 1) % 4]),
191 100.0,
192 epsilon = 1e-3
193 );
194 }
195 approx::assert_abs_diff_eq!(dist2(q.corners[0], q.corners[2]), 200.0, epsilon = 1e-3);
196 }
197
198 #[test]
199 fn permutes_neighbors_with_corners() {
200 let mut q = linked([(10.0, 10.0), (0.0, 0.0), (10.0, 0.0), (0.0, 10.0)]);
201 q.neighbors[0] = Some(42);
202 order_quad_corners(&mut q);
203 let pos = q.corners.iter().position(|&c| c == (10.0, 10.0)).unwrap();
204 assert_eq!(q.neighbors[pos], Some(42));
205 assert_eq!(q.neighbors.iter().filter(|n| n.is_some()).count(), 1);
206 }
207
208 fn black_square_board(cells: i32, side: i32) -> Vec<Quad> {
212 let mut quads = Vec::new();
213 for cy in 0..cells {
214 for cx in 0..cells {
215 if (cx + cy) % 2 != 0 {
216 continue;
217 }
218 let x = cx * side;
219 let y = cy * side;
220 quads.push(Quad {
221 corners: [(x, y), (x + side, y), (x + side, y + side), (x, y + side)],
222 });
223 }
224 }
225 quads
226 }
227
228 #[test]
229 fn recovers_inner_corner_lattice() {
230 let side = 20;
232 let quads = black_square_board(4, side);
233 let mut linked = link_quads(&quads);
234 order_all_corners(&mut linked);
235
236 let comps = connected_components(&linked);
237 assert_eq!(comps.len(), 1, "black squares should form one component");
238
239 let grid = assign_grid(&linked, &comps[0]);
240 let corners = ordered_inner_corners(&linked, &grid);
241
242 let mut expected = Vec::new();
244 for y in 1..=3 {
245 for x in 1..=3 {
246 expected.push(((x * side) as f32, (y * side) as f32));
247 }
248 }
249 assert_eq!(corners.len(), 9, "got {corners:?}");
250 for (got, want) in corners.iter().zip(expected.iter()) {
251 approx::assert_abs_diff_eq!(got.0, want.0, epsilon = 1e-3);
252 approx::assert_abs_diff_eq!(got.1, want.1, epsilon = 1e-3);
253 }
254 }
255}