projective_grid/
grid_smoothness.rs1use crate::float_helpers::lit;
8use crate::grid_index::GridIndex;
9use crate::Float;
10use nalgebra::Point2;
11use std::collections::HashMap;
12
13pub fn predict_grid_position<F: Float>(
22 grid: &HashMap<GridIndex, Point2<F>>,
23 idx: GridIndex,
24) -> Option<Point2<F>> {
25 let half: F = lit(0.5);
26 let mut pred_sum = Point2::new(F::zero(), F::zero());
27 let mut pred_count = 0u32;
28
29 let left = GridIndex {
31 i: idx.i - 1,
32 j: idx.j,
33 };
34 let right = GridIndex {
35 i: idx.i + 1,
36 j: idx.j,
37 };
38 if let (Some(&pl), Some(&pr)) = (grid.get(&left), grid.get(&right)) {
39 let mid = Point2::new(half * (pl.x + pr.x), half * (pl.y + pr.y));
40 pred_sum.x += mid.x;
41 pred_sum.y += mid.y;
42 pred_count += 1;
43 }
44
45 let up = GridIndex {
47 i: idx.i,
48 j: idx.j - 1,
49 };
50 let down = GridIndex {
51 i: idx.i,
52 j: idx.j + 1,
53 };
54 if let (Some(&pu), Some(&pd)) = (grid.get(&up), grid.get(&down)) {
55 let mid = Point2::new(half * (pu.x + pd.x), half * (pu.y + pd.y));
56 pred_sum.x += mid.x;
57 pred_sum.y += mid.y;
58 pred_count += 1;
59 }
60
61 if pred_count == 0 {
62 return None;
63 }
64
65 let n: F = lit(pred_count as f64);
66 Some(Point2::new(pred_sum.x / n, pred_sum.y / n))
67}
68
69pub fn find_inconsistent_corners<F: Float>(
74 grid: &HashMap<GridIndex, Point2<F>>,
75 threshold: F,
76) -> Vec<(GridIndex, Point2<F>)> {
77 let threshold_sq = threshold * threshold;
78 let mut flagged = Vec::new();
79
80 for (&idx, &pos) in grid {
81 if let Some(predicted) = predict_grid_position(grid, idx) {
82 let dx = pos.x - predicted.x;
83 let dy = pos.y - predicted.y;
84 if dx * dx + dy * dy > threshold_sq {
85 flagged.push((idx, predicted));
86 }
87 }
88 }
89
90 flagged
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn make_grid(rows: i32, cols: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
98 let mut map = HashMap::new();
99 for j in 0..rows {
100 for i in 0..cols {
101 map.insert(
102 GridIndex { i, j },
103 Point2::new(i as f32 * spacing, j as f32 * spacing),
104 );
105 }
106 }
107 map
108 }
109
110 #[test]
111 fn clean_grid_has_no_inconsistencies() {
112 let grid = make_grid(5, 5, 60.0);
113 let flagged = find_inconsistent_corners(&grid, 3.0);
114 assert!(flagged.is_empty());
115 }
116
117 #[test]
118 fn displaced_corner_is_flagged() {
119 let mut grid = make_grid(3, 3, 60.0);
120 let center = GridIndex { i: 1, j: 1 };
121 grid.insert(center, Point2::new(69.0, 69.0)); let flagged = find_inconsistent_corners(&grid, 3.0);
124 assert_eq!(1, flagged.len());
125 assert_eq!(center, flagged[0].0);
126
127 let pred = flagged[0].1;
129 assert!((pred.x - 60.0).abs() < 0.01);
130 assert!((pred.y - 60.0).abs() < 0.01);
131 }
132
133 #[test]
134 fn perspective_distorted_grid_passes() {
135 let spacing = 60.0;
136 let mut grid = HashMap::new();
137 for j in 0..5 {
138 let scale = 1.0 + 0.02 * j as f32;
139 for i in 0..5 {
140 grid.insert(
141 GridIndex { i, j },
142 Point2::new(i as f32 * spacing * scale, j as f32 * spacing * scale),
143 );
144 }
145 }
146
147 let flagged = find_inconsistent_corners(&grid, 3.0);
149 assert!(flagged.is_empty());
150 }
151
152 #[test]
153 fn isolated_corners_are_skipped() {
154 let mut grid = HashMap::new();
155 grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
156 grid.insert(GridIndex { i: 5, j: 5 }, Point2::new(300.0, 300.0));
157
158 let flagged = find_inconsistent_corners(&grid, 3.0);
159 assert!(flagged.is_empty());
160 }
161
162 #[test]
163 fn predict_from_single_pair() {
164 let mut grid = HashMap::new();
165 grid.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
166 grid.insert(GridIndex { i: 1, j: 0 }, Point2::new(60.0, 0.0));
167 grid.insert(GridIndex { i: 2, j: 0 }, Point2::new(120.0, 0.0));
168
169 let pred = predict_grid_position(&grid, GridIndex { i: 1, j: 0 }).unwrap();
170 assert!((pred.x - 60.0f32).abs() < 0.01);
171 assert!((pred.y - 0.0f32).abs() < 0.01);
172 }
173}