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