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]
19#[derive(thiserror::Error, Debug)]
20pub enum GridMeshError {
21 #[error("not enough grid corners (need at least 2×2)")]
23 NotEnoughCorners,
24 #[error("no valid grid cells found (each cell needs all 4 corners)")]
27 NoValidCells,
28 #[error("homography estimation failed for cell ({ci}, {cj})")]
30 HomographyFailed {
31 ci: usize,
33 cj: usize,
35 },
36}
37
38#[derive(Clone, Copy, Debug)]
39struct CellHomography<F: Float> {
40 h_img_from_cellrect: Homography<F>,
41 quality: HomographyQuality<F>,
42 valid: bool,
43}
44
45#[derive(Clone, Debug)]
50pub struct SquareGridHomographyMesh<F: Float = f32> {
51 pub min_i: i32,
53 pub min_j: i32,
55 pub cells_x: usize,
57 pub cells_y: usize,
59 pub px_per_cell: F,
61 pub valid_cells: usize,
63 pub rect_width: usize,
65 pub rect_height: usize,
67
68 cells: Vec<CellHomography<F>>,
69}
70
71impl<F: Float> SquareGridHomographyMesh<F> {
72 pub fn from_corners(
82 corners: &HashMap<GridCoords, Point2<F>>,
83 px_per_cell: F,
84 ) -> Result<Self, GridMeshError> {
85 Self::from_corners_with_min_singular_value(corners, px_per_cell, F::zero())
86 }
87
88 pub fn from_corners_with_min_singular_value(
104 corners: &HashMap<GridCoords, Point2<F>>,
105 px_per_cell: F,
106 min_singular_value: F,
107 ) -> Result<Self, GridMeshError> {
108 if corners.len() < 4 {
109 return Err(GridMeshError::NotEnoughCorners);
110 }
111
112 let (mut min_i, mut min_j) = (i32::MAX, i32::MAX);
113 let (mut max_i, mut max_j) = (i32::MIN, i32::MIN);
114 for g in corners.keys() {
115 min_i = min_i.min(g.i);
116 min_j = min_j.min(g.j);
117 max_i = max_i.max(g.i);
118 max_j = max_j.max(g.j);
119 }
120
121 if max_i - min_i < 1 || max_j - min_j < 1 {
122 return Err(GridMeshError::NoValidCells);
123 }
124
125 let cells_x = (max_i - min_i) as usize;
126 let cells_y = (max_j - min_j) as usize;
127
128 let rect_width = nalgebra::try_convert::<F, f64>(
129 (lit::<F>(cells_x as f64) * px_per_cell)
130 .floor()
131 .max(F::one()),
132 )
133 .unwrap_or(1.0) as usize;
134 let rect_height = nalgebra::try_convert::<F, f64>(
135 (lit::<F>(cells_y as f64) * px_per_cell)
136 .floor()
137 .max(F::one()),
138 )
139 .unwrap_or(1.0) as usize;
140
141 let s = px_per_cell;
142 let cell_rect = [
143 Point2::new(F::zero(), F::zero()),
144 Point2::new(s, F::zero()),
145 Point2::new(F::zero(), s),
146 Point2::new(s, s),
147 ];
148
149 let zero_quality = HomographyQuality {
150 max_singular_value: F::zero(),
151 min_singular_value: F::zero(),
152 condition: F::zero(),
153 determinant: F::zero(),
154 };
155 let mut cells = vec![
156 CellHomography {
157 h_img_from_cellrect: Homography::zero(),
158 quality: zero_quality,
159 valid: false,
160 };
161 cells_x * cells_y
162 ];
163
164 let mut valid_cells = 0usize;
165
166 for cj in 0..cells_y {
167 for ci in 0..cells_x {
168 let i0 = min_i + ci as i32;
169 let j0 = min_j + cj as i32;
170
171 let g00 = GridCoords { i: i0, j: j0 };
172 let g10 = GridCoords { i: i0 + 1, j: j0 };
173 let g01 = GridCoords { i: i0, j: j0 + 1 };
174 let g11 = GridCoords {
175 i: i0 + 1,
176 j: j0 + 1,
177 };
178
179 let Some(p00) = corners.get(&g00).copied() else {
180 continue;
181 };
182 let Some(p10) = corners.get(&g10).copied() else {
183 continue;
184 };
185 let Some(p01) = corners.get(&g01).copied() else {
186 continue;
187 };
188 let Some(p11) = corners.get(&g11).copied() else {
189 continue;
190 };
191
192 let img_quad = [p00, p10, p01, p11];
193
194 let (h, quality) = estimate_homography_with_quality(&cell_rect, &img_quad)
195 .ok_or(GridMeshError::HomographyFailed { ci, cj })?;
196
197 let idx = cj * cells_x + ci;
198 let valid = quality.min_singular_value >= min_singular_value;
199 cells[idx] = CellHomography {
200 h_img_from_cellrect: h,
201 quality,
202 valid,
203 };
204 if valid {
205 valid_cells += 1;
206 }
207 }
208 }
209
210 if valid_cells == 0 {
211 return Err(GridMeshError::NoValidCells);
212 }
213
214 Ok(Self {
215 min_i,
216 min_j,
217 cells_x,
218 cells_y,
219 px_per_cell,
220 valid_cells,
221 rect_width,
222 rect_height,
223 cells,
224 })
225 }
226
227 pub fn rect_to_img(&self, p_rect: Point2<F>) -> Option<Point2<F>> {
231 let s = self.px_per_cell;
232 if s <= F::zero() {
233 return None;
234 }
235
236 let ci_f = (p_rect.x / s).floor();
237 let cj_f = (p_rect.y / s).floor();
238 let ci = nalgebra::try_convert::<F, f64>(ci_f).unwrap_or(0.0) as i32;
239 let cj = nalgebra::try_convert::<F, f64>(cj_f).unwrap_or(0.0) as i32;
240 if ci < 0 || cj < 0 || ci >= self.cells_x as i32 || cj >= self.cells_y as i32 {
241 return None;
242 }
243
244 let x_local = p_rect.x - lit::<F>(ci as f64) * s;
245 let y_local = p_rect.y - lit::<F>(cj as f64) * s;
246 self.cell_rect_to_img(ci as usize, cj as usize, Point2::new(x_local, y_local))
247 }
248
249 pub fn cell_quality(&self, ci: usize, cj: usize) -> Option<HomographyQuality<F>> {
254 let idx = cj.checked_mul(self.cells_x)?.checked_add(ci)?;
255 let cell = self.cells.get(idx)?;
256 if cell.quality.max_singular_value <= F::zero() {
257 None
258 } else {
259 Some(cell.quality)
260 }
261 }
262
263 pub fn cell_rect_to_img(&self, ci: usize, cj: usize, p_cell: Point2<F>) -> Option<Point2<F>> {
268 let idx = cj.checked_mul(self.cells_x)?.checked_add(ci)?;
269 let cell = *self.cells.get(idx)?;
270 if !cell.valid {
271 return None;
272 }
273 Some(cell.h_img_from_cellrect.apply(p_cell))
274 }
275
276 pub fn cell_corners_img(&self, ci: usize, cj: usize) -> Option<[Point2<F>; 4]> {
278 let s = self.px_per_cell;
279 let pts = [
280 Point2::new(F::zero(), F::zero()),
281 Point2::new(s, F::zero()),
282 Point2::new(s, s),
283 Point2::new(F::zero(), s),
284 ];
285 Some([
286 self.cell_rect_to_img(ci, cj, pts[0])?,
287 self.cell_rect_to_img(ci, cj, pts[1])?,
288 self.cell_rect_to_img(ci, cj, pts[2])?,
289 self.cell_rect_to_img(ci, cj, pts[3])?,
290 ])
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn axis_aligned_grid(rows: i32, cols: i32, spacing: f32) -> HashMap<GridCoords, Point2<f32>> {
299 let mut out = HashMap::new();
300 for j in 0..rows {
301 for i in 0..cols {
302 out.insert(
303 GridCoords { i, j },
304 Point2::new(i as f32 * spacing, j as f32 * spacing),
305 );
306 }
307 }
308 out
309 }
310
311 #[test]
312 fn from_corners_default_accepts_clean_grid() {
313 let corners = axis_aligned_grid(3, 3, 50.0);
314 let mesh =
315 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("mesh builds");
316 assert_eq!(mesh.cells_x, 2);
318 assert_eq!(mesh.cells_y, 2);
319 assert_eq!(mesh.valid_cells, 4);
320 for cj in 0..2 {
322 for ci in 0..2 {
323 let q = mesh.cell_quality(ci, cj).expect("quality");
324 assert!(q.min_singular_value > 0.0);
325 assert!(q.condition.is_finite());
326 }
327 }
328 }
329
330 #[test]
331 fn min_singular_value_threshold_skips_degenerate_cells() {
332 let mut corners = HashMap::new();
335 corners.insert(GridCoords { i: 0, j: 0 }, Point2::new(0.0_f32, 0.0));
336 corners.insert(GridCoords { i: 1, j: 0 }, Point2::new(50.0, 0.0));
337 corners.insert(GridCoords { i: 1, j: 1 }, Point2::new(50.0, 1e-4));
339 corners.insert(GridCoords { i: 0, j: 1 }, Point2::new(0.0, 1e-4));
340
341 let lenient =
343 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("lenient");
344 assert_eq!(lenient.valid_cells, 1);
345
346 let strict = SquareGridHomographyMesh::<f32>::from_corners_with_min_singular_value(
348 &corners, 32.0, 0.01,
349 );
350 assert!(strict.is_err());
352 }
353
354 #[test]
355 fn cell_quality_returns_none_for_skipped_cell() {
356 let mut corners = axis_aligned_grid(3, 3, 50.0);
361 corners.remove(&GridCoords { i: 1, j: 1 });
362 let result = SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0);
363 assert!(matches!(result, Err(GridMeshError::NoValidCells)));
364 }
365
366 #[test]
367 fn cell_quality_partial_mesh_keeps_intact_cells() {
368 let mut corners = axis_aligned_grid(4, 4, 50.0);
371 corners.remove(&GridCoords { i: 2, j: 2 });
372 let mesh =
373 SquareGridHomographyMesh::<f32>::from_corners(&corners, 32.0).expect("partial mesh");
374 assert_eq!(mesh.valid_cells, 9 - 4);
377 assert!(mesh.cell_quality(0, 0).is_some());
379 assert!(mesh.cell_quality(1, 1).is_none());
381 }
382}