chess_corners_core/detect/
merge.rs1use super::Corner;
3
4#[cfg(feature = "tracing")]
5use tracing::instrument;
6
7#[cfg_attr(feature = "tracing", instrument(level = "info", skip(corners)))]
22pub fn merge_corners_simple(corners: &mut Vec<Corner>, radius: f32) -> Vec<Corner> {
23 let n = corners.len();
24 if n == 0 {
25 return Vec::new();
26 }
27 if !radius.is_finite() || radius <= 0.0 {
28 return std::mem::take(corners);
29 }
30
31 let r2 = radius * radius;
32
33 let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
35 let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
36 for c in corners.iter() {
37 if c.x < min_x {
38 min_x = c.x;
39 }
40 if c.y < min_y {
41 min_y = c.y;
42 }
43 if c.x > max_x {
44 max_x = c.x;
45 }
46 if c.y > max_y {
47 max_y = c.y;
48 }
49 }
50
51 if !(min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite()) {
54 return merge_corners_naive(corners, r2);
55 }
56
57 let cell = radius;
58 let inv_cell = 1.0 / cell;
59 let grid_w = ((max_x - min_x) * inv_cell).floor() as usize + 1;
60 let grid_h = ((max_y - min_y) * inv_cell).floor() as usize + 1;
61
62 let mut grid: Vec<Vec<u32>> = vec![Vec::new(); grid_w * grid_h];
66 let mut out: Vec<Corner> = Vec::with_capacity(n);
67
68 let cell_of = |c: &Corner| -> (usize, usize) {
69 let gx = (((c.x - min_x) * inv_cell) as usize).min(grid_w - 1);
70 let gy = (((c.y - min_y) * inv_cell) as usize).min(grid_h - 1);
71 (gx, gy)
72 };
73
74 for c in corners.drain(..) {
75 let (gx, gy) = cell_of(&c);
76
77 let y0 = gy.saturating_sub(1);
83 let y1 = (gy + 1).min(grid_h - 1);
84 let x0 = gx.saturating_sub(1);
85 let x1 = (gx + 1).min(grid_w - 1);
86
87 let mut hit: Option<usize> = None;
88 let mut best_idx = u32::MAX;
89 for ny in y0..=y1 {
90 for nx in x0..=x1 {
91 for &idx in &grid[ny * grid_w + nx] {
92 if idx >= best_idx {
93 continue;
94 }
95 let i = idx as usize;
96 let dx = c.x - out[i].x;
97 let dy = c.y - out[i].y;
98 if dx * dx + dy * dy <= r2 {
99 best_idx = idx;
100 hit = Some(i);
101 }
102 }
103 }
104 }
105
106 if let Some(i) = hit {
107 if c.strength > out[i].strength {
108 let (old_gx, old_gy) = cell_of(&out[i]);
109 out[i] = c;
110 let (new_gx, new_gy) = cell_of(&out[i]);
111 if old_gx != new_gx || old_gy != new_gy {
112 let id = i as u32;
113 let old_cell = old_gy * grid_w + old_gx;
114 let new_cell = new_gy * grid_w + new_gx;
115 grid[old_cell].retain(|&j| j != id);
116 grid[new_cell].push(id);
117 }
118 }
119 } else {
120 let new_idx = out.len() as u32;
121 let (gx, gy) = cell_of(&c);
122 out.push(c);
123 grid[gy * grid_w + gx].push(new_idx);
124 }
125 }
126
127 out
128}
129
130fn merge_corners_naive(corners: &mut Vec<Corner>, r2: f32) -> Vec<Corner> {
133 let mut out: Vec<Corner> = Vec::new();
134 'outer: for c in corners.drain(..) {
135 for o in &mut out {
136 let dx = c.x - o.x;
137 let dy = c.y - o.y;
138 if dx * dx + dy * dy <= r2 {
139 if c.strength > o.strength {
140 *o = c;
141 }
142 continue 'outer;
143 }
144 }
145 out.push(c);
146 }
147 out
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn merge_corners_prefers_stronger_entries() {
156 let mut corners = vec![
157 Corner {
158 x: 10.0,
159 y: 10.0,
160 strength: 1.0,
161 },
162 Corner {
163 x: 11.0,
164 y: 11.0,
165 strength: 5.0,
166 },
167 Corner {
168 x: 20.0,
169 y: 20.0,
170 strength: 3.0,
171 },
172 ];
173 let merged = merge_corners_simple(&mut corners, 2.5);
174 assert_eq!(merged.len(), 2);
175 assert!(merged.iter().any(|c| (c.x - 11.0).abs() < 1e-6
176 && (c.y - 11.0).abs() < 1e-6
177 && (c.strength - 5.0).abs() < 1e-6));
178 assert!(merged
179 .iter()
180 .any(|c| (c.x - 20.0).abs() < 1e-6 && (c.y - 20.0).abs() < 1e-6));
181 }
182
183 fn lcg(state: &mut u64) -> u32 {
186 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
187 (*state >> 33) as u32
188 }
189
190 fn rand_corners(seed: u64, n: usize, span: f32) -> Vec<Corner> {
191 let mut state = seed.wrapping_add(0x9E3779B97F4A7C15);
192 let mut out = Vec::with_capacity(n);
193 for _ in 0..n {
194 let rx = lcg(&mut state) as f32 / u32::MAX as f32;
195 let ry = lcg(&mut state) as f32 / u32::MAX as f32;
196 let rs = lcg(&mut state) as f32 / u32::MAX as f32;
197 out.push(Corner {
198 x: rx * span,
199 y: ry * span,
200 strength: rs * 100.0,
201 });
202 }
203 out
204 }
205
206 fn corners_eq_unordered(a: &[Corner], b: &[Corner]) -> bool {
207 if a.len() != b.len() {
208 return false;
209 }
210 let mut used = vec![false; b.len()];
211 'outer: for ca in a.iter() {
212 for (i, cb) in b.iter().enumerate() {
213 if used[i] {
214 continue;
215 }
216 if (ca.x - cb.x).abs() < 1e-5
217 && (ca.y - cb.y).abs() < 1e-5
218 && (ca.strength - cb.strength).abs() < 1e-3
219 {
220 used[i] = true;
221 continue 'outer;
222 }
223 }
224 return false;
225 }
226 true
227 }
228
229 #[test]
230 fn merge_corners_grid_matches_naive_scan() {
231 for seed in [1u64, 7, 42, 123, 1729] {
237 for &n in &[16usize, 64, 256, 1024] {
238 for &radius in &[1.0f32, 2.5, 6.0] {
239 let corners = rand_corners(seed, n, 50.0);
240 let mut a = corners.clone();
241 let mut b = corners.clone();
242 let r2 = radius * radius;
243 let merged_grid = merge_corners_simple(&mut a, radius);
244 let merged_naive = merge_corners_naive(&mut b, r2);
245 assert!(
246 corners_eq_unordered(&merged_grid, &merged_naive),
247 "mismatch at seed={seed}, n={n}, radius={radius}: grid={} naive={}",
248 merged_grid.len(),
249 merged_naive.len(),
250 );
251 }
252 }
253 }
254 }
255
256 #[test]
257 fn merge_corners_handles_empty_and_zero_radius() {
258 let mut empty: Vec<Corner> = Vec::new();
259 let merged = merge_corners_simple(&mut empty, 1.0);
260 assert!(merged.is_empty());
261
262 let mut few = vec![
263 Corner {
264 x: 1.0,
265 y: 1.0,
266 strength: 1.0,
267 },
268 Corner {
269 x: 1.5,
270 y: 1.5,
271 strength: 2.0,
272 },
273 ];
274 let merged = merge_corners_simple(&mut few, 0.0);
276 assert_eq!(merged.len(), 2);
277 }
278}