1use crate::homography::homography_from_4pt;
40use nalgebra::Point2;
41use std::collections::{HashMap, HashSet};
42
43#[non_exhaustive]
49#[derive(Clone, Copy, Debug)]
50pub struct ValidationParams {
51 pub line_tol_rel: f32,
54 pub projective_line_tol_rel: f32,
60 pub line_min_members: usize,
62 pub local_h_tol_rel: f32,
64}
65
66impl Default for ValidationParams {
67 fn default() -> Self {
68 Self {
72 line_tol_rel: 0.15,
73 projective_line_tol_rel: 0.25,
74 line_min_members: 3,
75 local_h_tol_rel: 0.20,
76 }
77 }
78}
79
80impl ValidationParams {
81 pub fn new(
86 line_tol_rel: f32,
87 projective_line_tol_rel: f32,
88 line_min_members: usize,
89 local_h_tol_rel: f32,
90 ) -> Self {
91 Self {
92 line_tol_rel,
93 projective_line_tol_rel,
94 line_min_members,
95 local_h_tol_rel,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug)]
108pub struct LabelledEntry {
109 pub idx: usize,
110 pub pixel: Point2<f32>,
111 pub grid: (i32, i32),
112}
113
114#[derive(Debug, Default)]
116pub struct ValidationResult {
117 pub blacklist: HashSet<usize>,
119 pub local_h_residuals: HashMap<usize, f32>,
123}
124
125pub fn validate(
127 entries: &[LabelledEntry],
128 cell_size: f32,
129 params: &ValidationParams,
130) -> ValidationResult {
131 let line_tol_px = params.line_tol_rel * cell_size;
132 let local_h_tol_px = params.local_h_tol_rel * cell_size;
133 let high_tol_px = 2.0 * local_h_tol_px;
134
135 let by_idx: HashMap<usize, &LabelledEntry> = entries.iter().map(|e| (e.idx, e)).collect();
137 let by_grid: HashMap<(i32, i32), usize> = entries.iter().map(|e| (e.grid, e.idx)).collect();
138
139 let line_flags = line_collinearity_flags(&by_idx, &by_grid, line_tol_px, params);
141
142 let mut residuals: HashMap<usize, f32> = HashMap::new();
144 let mut local_h_flagged: HashMap<usize, f32> = HashMap::new();
145 for entry in entries {
146 let base = pick_local_h_base(&by_grid, entry.idx, entry.grid);
147 if base.len() < 4 {
148 continue;
149 }
150 let Some(resid) = local_h_residual(&by_idx, entry.idx, entry.grid, &base, &by_grid) else {
151 continue;
152 };
153 residuals.insert(entry.idx, resid);
154 if resid > local_h_tol_px {
155 local_h_flagged.insert(entry.idx, resid);
156 }
157 }
158
159 let mut blacklist: HashSet<usize> = HashSet::new();
161 for (&idx, &count) in &line_flags {
163 if count >= 2 {
164 blacklist.insert(idx);
165 }
166 }
167 for (&idx, &resid) in &local_h_flagged {
169 if resid > high_tol_px && line_flags.get(&idx).copied().unwrap_or(0) >= 1 {
170 blacklist.insert(idx);
171 }
172 }
173 for &idx in local_h_flagged.keys() {
176 if line_flags.get(&idx).copied().unwrap_or(0) >= 1 {
177 continue;
178 }
179 if blacklist.contains(&idx) {
180 continue;
181 }
182 let Some(entry) = by_idx.get(&idx) else {
183 continue;
184 };
185 let base = pick_local_h_base(&by_grid, idx, entry.grid);
186 let mut worst: Option<(usize, u32)> = None;
187 for base_idx in &base {
188 if let Some(&flags) = line_flags.get(base_idx) {
189 if flags >= 1 && worst.map(|w| flags > w.1).unwrap_or(true) {
190 worst = Some((*base_idx, flags));
191 }
192 }
193 }
194 if let Some((base_idx, _)) = worst {
195 blacklist.insert(base_idx);
196 }
197 }
198
199 ValidationResult {
200 blacklist,
201 local_h_residuals: residuals,
202 }
203}
204
205fn line_collinearity_flags(
208 by_idx: &HashMap<usize, &LabelledEntry>,
209 by_grid: &HashMap<(i32, i32), usize>,
210 tol_px: f32,
211 params: &ValidationParams,
212) -> HashMap<usize, u32> {
213 let mut flags: HashMap<usize, u32> = HashMap::new();
214
215 let mut rows: HashMap<i32, Vec<(i32, usize)>> = HashMap::new();
217 let mut cols: HashMap<i32, Vec<(i32, usize)>> = HashMap::new();
218 for (&(i, j), &idx) in by_grid {
219 rows.entry(j).or_default().push((i, idx));
220 cols.entry(i).or_default().push((j, idx));
221 }
222
223 let line_min = params.line_min_members;
224
225 for (_, mut members) in rows {
226 if members.len() < line_min {
227 continue;
228 }
229 members.sort_by_key(|(i, _)| *i);
230 flag_line(by_idx, &members, tol_px, &mut flags);
231 }
232 for (_, mut members) in cols {
233 if members.len() < line_min {
234 continue;
235 }
236 members.sort_by_key(|(j, _)| *j);
237 flag_line(by_idx, &members, tol_px, &mut flags);
238 }
239 flags
240}
241
242fn flag_line(
245 by_idx: &HashMap<usize, &LabelledEntry>,
246 members: &[(i32, usize)],
247 tol_px: f32,
248 flags: &mut HashMap<usize, u32>,
249) {
250 let n = members.len() as f32;
251 let mut cx = 0.0_f32;
252 let mut cy = 0.0_f32;
253 for (_, idx) in members {
254 let Some(e) = by_idx.get(idx) else { continue };
255 cx += e.pixel.x;
256 cy += e.pixel.y;
257 }
258 cx /= n;
259 cy /= n;
260 let mut sxx = 0.0_f32;
261 let mut sxy = 0.0_f32;
262 let mut syy = 0.0_f32;
263 for (_, idx) in members {
264 let Some(e) = by_idx.get(idx) else { continue };
265 let dx = e.pixel.x - cx;
266 let dy = e.pixel.y - cy;
267 sxx += dx * dx;
268 sxy += dx * dy;
269 syy += dy * dy;
270 }
271 let trace = sxx + syy;
272 let det = sxx * syy - sxy * sxy;
273 let disc = (trace * trace * 0.25 - det).max(0.0).sqrt();
274 let lambda = trace * 0.5 + disc;
275 let (vx, vy) = if sxy.abs() > f32::EPSILON {
276 (sxy, lambda - sxx)
277 } else if sxx >= syy {
278 (1.0, 0.0)
279 } else {
280 (0.0, 1.0)
281 };
282 let vn = (vx * vx + vy * vy).sqrt().max(f32::EPSILON);
283 let ux = vx / vn;
284 let uy = vy / vn;
285 for (_, idx) in members {
286 let Some(e) = by_idx.get(idx) else { continue };
287 let dx = e.pixel.x - cx;
288 let dy = e.pixel.y - cy;
289 let resid = (dx * -uy + dy * ux).abs();
290 if resid > tol_px {
291 *flags.entry(*idx).or_insert(0) += 1;
292 }
293 }
294}
295
296fn pick_local_h_base(
302 by_grid: &HashMap<(i32, i32), usize>,
303 c_idx: usize,
304 pos: (i32, i32),
305) -> Vec<usize> {
306 let mut cands: Vec<((i32, i32), usize, f32)> = Vec::new();
307 for dj in -2..=2_i32 {
308 for di in -2..=2_i32 {
309 if di == 0 && dj == 0 {
310 continue;
311 }
312 let neigh = (pos.0 + di, pos.1 + dj);
313 if let Some(&idx) = by_grid.get(&neigh) {
314 if idx == c_idx {
315 continue;
316 }
317 let d = ((di * di + dj * dj) as f32).sqrt();
318 cands.push((neigh, idx, d));
319 }
320 }
321 }
322 cands.sort_by(|a, b| a.2.total_cmp(&b.2));
323
324 let mut chosen: Vec<((i32, i32), usize)> = Vec::new();
325 for (ij, idx, _) in &cands {
326 chosen.push((*ij, *idx));
327 if chosen.len() == 4 && !are_collinear_grid(&chosen) {
328 return chosen.iter().map(|(_, i)| *i).collect();
329 }
330 if chosen.len() >= 4 {
331 chosen.pop();
332 }
333 }
334 chosen.iter().map(|(_, i)| *i).collect()
335}
336
337fn are_collinear_grid(pts: &[((i32, i32), usize)]) -> bool {
338 if pts.len() < 3 {
339 return false;
340 }
341 let (i0, j0) = pts[0].0;
342 let (i1, j1) = pts[1].0;
343 let dx1 = i1 - i0;
344 let dy1 = j1 - j0;
345 for &((i, j), _) in &pts[2..] {
346 let dx = i - i0;
347 let dy = j - j0;
348 if dx1 * dy - dy1 * dx != 0 {
349 return false;
350 }
351 }
352 true
353}
354
355fn local_h_residual(
356 by_idx: &HashMap<usize, &LabelledEntry>,
357 c_idx: usize,
358 c_grid: (i32, i32),
359 base: &[usize],
360 by_grid: &HashMap<(i32, i32), usize>,
361) -> Option<f32> {
362 if base.len() < 4 {
363 return None;
364 }
365 let mut base_grid: [(i32, i32); 4] = [(0, 0); 4];
369 let mut base_pixel: [Point2<f32>; 4] = [Point2::new(0.0, 0.0); 4];
370 for (k, &b_idx) in base.iter().take(4).enumerate() {
371 let grid = by_grid
372 .iter()
373 .find_map(|(&g, &v)| if v == b_idx { Some(g) } else { None })?;
374 base_grid[k] = grid;
375 base_pixel[k] = by_idx.get(&b_idx)?.pixel;
376 }
377
378 let grid_pts = [
379 Point2::new(base_grid[0].0 as f32, base_grid[0].1 as f32),
380 Point2::new(base_grid[1].0 as f32, base_grid[1].1 as f32),
381 Point2::new(base_grid[2].0 as f32, base_grid[2].1 as f32),
382 Point2::new(base_grid[3].0 as f32, base_grid[3].1 as f32),
383 ];
384 let h = homography_from_4pt(&grid_pts, &base_pixel)?;
385
386 let c_pixel = by_idx.get(&c_idx)?.pixel;
387 let pred = h.apply(Point2::new(c_grid.0 as f32, c_grid.1 as f32));
388 let dx = pred.x - c_pixel.x;
389 let dy = pred.y - c_pixel.y;
390 Some((dx * dx + dy * dy).sqrt())
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 fn entry(idx: usize, x: f32, y: f32, i: i32, j: i32) -> LabelledEntry {
398 LabelledEntry {
399 idx,
400 pixel: Point2::new(x, y),
401 grid: (i, j),
402 }
403 }
404
405 fn clean_grid(rows: i32, cols: i32, s: f32) -> Vec<LabelledEntry> {
406 let mut out = Vec::new();
407 let mut idx = 0;
408 for j in 0..rows {
409 for i in 0..cols {
410 out.push(entry(idx, i as f32 * s + 50.0, j as f32 * s + 50.0, i, j));
411 idx += 1;
412 }
413 }
414 out
415 }
416
417 #[test]
418 fn clean_grid_empty_blacklist() {
419 let entries = clean_grid(7, 7, 20.0);
420 let res = validate(&entries, 20.0, &ValidationParams::default());
421 assert!(res.blacklist.is_empty(), "{:?}", res.blacklist);
422 }
423
424 #[test]
425 fn displaced_interior_is_blacklisted() {
426 let mut entries = clean_grid(7, 7, 20.0);
427 let target = entries
430 .iter_mut()
431 .find(|e| e.grid == (3, 3))
432 .expect("(3,3) present");
433 target.pixel.x += 6.0;
434 target.pixel.y += 6.0;
435 let target_idx = target.idx;
436 let res = validate(&entries, 20.0, &ValidationParams::default());
437 assert!(
438 res.blacklist.contains(&target_idx),
439 "expected {target_idx} blacklisted, got {:?}",
440 res.blacklist
441 );
442 }
443
444 #[test]
445 fn too_few_members_per_line_is_ignored() {
446 let entries = vec![entry(0, 0.0, 0.0, 0, 0), entry(1, 20.0, 0.0, 1, 0)];
447 let res = validate(&entries, 20.0, &ValidationParams::default());
448 assert!(res.blacklist.is_empty());
449 }
450}