1use crate::float_helpers::lit;
11use crate::homography::{estimate_homography_with_quality, Homography, HomographyQuality};
12use crate::Float;
13use crate::GridCoords;
14use nalgebra::Point2;
15use std::collections::HashMap;
16
17#[non_exhaustive]
18#[derive(thiserror::Error, Debug)]
19pub enum GridMeshError {
20 #[error("not enough grid corners (need at least 2×2)")]
21 NotEnoughCorners,
22 #[error("no valid grid cells found (each cell needs all 4 corners)")]
23 NoValidCells,
24 #[error("homography estimation failed for cell ({ci}, {cj})")]
25 HomographyFailed { ci: usize, cj: usize },
26}
27
28#[derive(Clone, Copy, Debug)]
29struct CellHomography<F: Float> {
30 h_img_from_cellrect: Homography<F>,
31 quality: HomographyQuality<F>,
32 valid: bool,
33}
34
35#[derive(Clone, Debug)]
40pub struct SquareGridHomographyMesh<F: Float = f32> {
41 pub min_i: i32,
43 pub min_j: i32,
45 pub cells_x: usize,
47 pub cells_y: usize,
49 pub px_per_cell: F,
51 pub valid_cells: usize,
53 pub rect_width: usize,
55 pub rect_height: usize,
57
58 cells: Vec<CellHomography<F>>,
59}
60
61impl<F: Float> SquareGridHomographyMesh<F> {
62 pub fn from_corners(
72 corners: &HashMap<GridCoords, Point2<F>>,
73 px_per_cell: F,
74 ) -> Result<Self, GridMeshError> {
75 Self::from_corners_with_min_singular_value(corners, px_per_cell, F::zero())
76 }
77
78 pub fn from_corners_with_min_singular_value(
94 corners: &HashMap<GridCoords, Point2<F>>,
95 px_per_cell: F,
96 min_singular_value: F,
97 ) -> Result<Self, GridMeshError> {
98 if corners.len() < 4 {
99 return Err(GridMeshError::NotEnoughCorners);
100 }
101
102 let (mut min_i, mut min_j) = (i32::MAX, i32::MAX);
103 let (mut max_i, mut max_j) = (i32::MIN, i32::MIN);
104 for g in corners.keys() {
105 min_i = min_i.min(g.i);
106 min_j = min_j.min(g.j);
107 max_i = max_i.max(g.i);
108 max_j = max_j.max(g.j);
109 }
110
111 if max_i - min_i < 1 || max_j - min_j < 1 {
112 return Err(GridMeshError::NoValidCells);
113 }
114
115 let cells_x = (max_i - min_i) as usize;
116 let cells_y = (max_j - min_j) as usize;
117
118 let rect_width = nalgebra::try_convert::<F, f64>(
119 (lit::<F>(cells_x as f64) * px_per_cell)
120 .floor()
121 .max(F::one()),
122 )
123 .unwrap_or(1.0) as usize;
124 let rect_height = nalgebra::try_convert::<F, f64>(
125 (lit::<F>(cells_y as f64) * px_per_cell)
126 .floor()
127 .max(F::one()),
128 )
129 .unwrap_or(1.0) as usize;
130
131 let s = px_per_cell;
132 let cell_rect = [
133 Point2::new(F::zero(), F::zero()),
134 Point2::new(s, F::zero()),
135 Point2::new(F::zero(), s),
136 Point2::new(s, s),
137 ];
138
139 let zero_quality = HomographyQuality {
140 max_singular_value: F::zero(),
141 min_singular_value: F::zero(),
142 condition: F::zero(),
143 determinant: F::zero(),
144 };
145 let mut cells = vec![
146 CellHomography {
147 h_img_from_cellrect: Homography::zero(),
148 quality: zero_quality,
149 valid: false,
150 };
151 cells_x * cells_y
152 ];
153
154 let mut valid_cells = 0usize;
155
156 for cj in 0..cells_y {
157 for ci in 0..cells_x {
158 let i0 = min_i + ci as i32;
159 let j0 = min_j + cj as i32;
160
161 let g00 = GridCoords { i: i0, j: j0 };
162 let g10 = GridCoords { i: i0 + 1, j: j0 };
163 let g01 = GridCoords { i: i0, j: j0 + 1 };
164 let g11 = GridCoords {
165 i: i0 + 1,
166 j: j0 + 1,
167 };
168
169 let Some(p00) = corners.get(&g00).copied() else {
170 continue;
171 };
172 let Some(p10) = corners.get(&g10).copied() else {
173 continue;
174 };
175 let Some(p01) = corners.get(&g01).copied() else {
176 continue;
177 };
178 let Some(p11) = corners.get(&g11).copied() else {
179 continue;
180 };
181
182 let img_quad = [p00, p10, p01, p11];
183
184 let (h, quality) = estimate_homography_with_quality(&cell_rect, &img_quad)
185 .ok_or(GridMeshError::HomographyFailed { ci, cj })?;
186
187 let idx = cj * cells_x + ci;
188 let valid = quality.min_singular_value >= min_singular_value;
189 cells[idx] = CellHomography {
190 h_img_from_cellrect: h,
191 quality,
192 valid,
193 };
194 if valid {
195 valid_cells += 1;
196 }
197 }
198 }
199
200 if valid_cells == 0 {
201 return Err(GridMeshError::NoValidCells);
202 }
203
204 Ok(Self {
205 min_i,
206 min_j,
207 cells_x,
208 cells_y,
209 px_per_cell,
210 valid_cells,
211 rect_width,
212 rect_height,
213 cells,
214 })
215 }
216
217 pub fn rect_to_img(&self, p_rect: Point2<F>) -> Option<Point2<F>> {
221 let s = self.px_per_cell;
222 if s <= F::zero() {
223 return None;
224 }
225
226 let ci_f = (p_rect.x / s).floor();
227 let cj_f = (p_rect.y / s).floor();
228 let ci = nalgebra::try_convert::<F, f64>(ci_f).unwrap_or(0.0) as i32;
229 let cj = nalgebra::try_convert::<F, f64>(cj_f).unwrap_or(0.0) as i32;
230 if ci < 0 || cj < 0 || ci >= self.cells_x as i32 || cj >= self.cells_y as i32 {
231 return None;
232 }
233
234 let x_local = p_rect.x - lit::<F>(ci as f64) * s;
235 let y_local = p_rect.y - lit::<F>(cj as f64) * s;
236 self.cell_rect_to_img(ci as usize, cj as usize, Point2::new(x_local, y_local))
237 }
238
239 pub fn cell_quality(&self, ci: usize, cj: usize) -> Option<HomographyQuality<F>> {
244 let idx = cj.checked_mul(self.cells_x)?.checked_add(ci)?;
245 let cell = self.cells.get(idx)?;
246 if cell.quality.max_singular_value <= F::zero() {
247 None
248 } else {
249 Some(cell.quality)
250 }
251 }
252
253 pub fn cell_rect_to_img(&self, ci: usize, cj: usize, p_cell: Point2<F>) -> Option<Point2<F>> {
258 let idx = cj.checked_mul(self.cells_x)?.checked_add(ci)?;
259 let cell = *self.cells.get(idx)?;
260 if !cell.valid {
261 return None;
262 }
263 Some(cell.h_img_from_cellrect.apply(p_cell))
264 }
265
266 pub fn cell_corners_img(&self, ci: usize, cj: usize) -> Option<[Point2<F>; 4]> {
268 let s = self.px_per_cell;
269 let pts = [
270 Point2::new(F::zero(), F::zero()),
271 Point2::new(s, F::zero()),
272 Point2::new(s, s),
273 Point2::new(F::zero(), s),
274 ];
275 Some([
276 self.cell_rect_to_img(ci, cj, pts[0])?,
277 self.cell_rect_to_img(ci, cj, pts[1])?,
278 self.cell_rect_to_img(ci, cj, pts[2])?,
279 self.cell_rect_to_img(ci, cj, pts[3])?,
280 ])
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn axis_aligned_grid(rows: i32, cols: i32, spacing: f32) -> HashMap<GridCoords, Point2<f32>> {
289 let mut out = HashMap::new();
290 for j in 0..rows {
291 for i in 0..cols {
292 out.insert(
293 GridCoords { i, j },
294 Point2::new(i as f32 * spacing, j as f32 * spacing),
295 );
296 }
297 }
298 out
299 }
300
301 #[test]
302 fn from_corners_default_accepts_clean_grid() {
303 let corners = axis_aligned_grid(3, 3, 50.0);
304 let mesh =
305 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("mesh builds");
306 assert_eq!(mesh.cells_x, 2);
308 assert_eq!(mesh.cells_y, 2);
309 assert_eq!(mesh.valid_cells, 4);
310 for cj in 0..2 {
312 for ci in 0..2 {
313 let q = mesh.cell_quality(ci, cj).expect("quality");
314 assert!(q.min_singular_value > 0.0);
315 assert!(q.condition.is_finite());
316 }
317 }
318 }
319
320 #[test]
321 fn min_singular_value_threshold_skips_degenerate_cells() {
322 let mut corners = HashMap::new();
325 corners.insert(GridCoords { i: 0, j: 0 }, Point2::new(0.0_f32, 0.0));
326 corners.insert(GridCoords { i: 1, j: 0 }, Point2::new(50.0, 0.0));
327 corners.insert(GridCoords { i: 1, j: 1 }, Point2::new(50.0, 1e-4));
329 corners.insert(GridCoords { i: 0, j: 1 }, Point2::new(0.0, 1e-4));
330
331 let lenient =
333 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("lenient");
334 assert_eq!(lenient.valid_cells, 1);
335
336 let strict = SquareGridHomographyMesh::<f32>::from_corners_with_min_singular_value(
338 &corners, 32.0, 0.01,
339 );
340 assert!(strict.is_err());
342 }
343
344 #[test]
345 fn cell_quality_returns_none_for_skipped_cell() {
346 let mut corners = axis_aligned_grid(3, 3, 50.0);
351 corners.remove(&GridCoords { i: 1, j: 1 });
352 let result = SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0);
353 assert!(matches!(result, Err(GridMeshError::NoValidCells)));
354 }
355
356 #[test]
357 fn cell_quality_partial_mesh_keeps_intact_cells() {
358 let mut corners = axis_aligned_grid(4, 4, 50.0);
361 corners.remove(&GridCoords { i: 2, j: 2 });
362 let mesh =
363 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("partial mesh");
364 assert_eq!(mesh.valid_cells, 9 - 4);
367 assert!(mesh.cell_quality(0, 0).is_some());
369 assert!(mesh.cell_quality(1, 1).is_none());
371 }
372}