1use crate::float_helpers::lit;
8use crate::homography::{estimate_homography, Homography};
9use crate::Float;
10use crate::GridCoords;
11use nalgebra::Point2;
12use std::collections::HashMap;
13
14fn sqrt3_half<F: Float>() -> F {
15 lit::<F>(3.0).sqrt() / lit::<F>(2.0)
16}
17
18#[non_exhaustive]
20#[derive(thiserror::Error, Debug)]
21pub enum HexGridRectifyError {
22 #[error("not enough grid corners with positions (need >= 4, got {got})")]
24 NotEnoughPoints {
25 got: usize,
27 },
28 #[error("homography estimation failed")]
30 HomographyFailed,
31 #[error("homography not invertible")]
33 NonInvertible,
34}
35
36#[derive(Clone, Debug)]
44pub struct HexGridHomography<F: Float = f32> {
45 pub h_img_from_rect: Homography<F>,
47 pub h_rect_from_img: Homography<F>,
49 pub min_q: i32,
51 pub min_r: i32,
53 pub max_q: i32,
55 pub max_r: i32,
57 pub px_per_cell: F,
59 pub rect_width: usize,
61 pub rect_height: usize,
63
64 x_offset: F,
68 y_offset: F,
69}
70
71impl<F: Float> HexGridHomography<F> {
72 pub fn from_corners(
78 corners: &HashMap<GridCoords, Point2<F>>,
79 px_per_cell: F,
80 margin_cells: F,
81 ) -> Result<Self, HexGridRectifyError> {
82 if corners.len() < 4 {
83 return Err(HexGridRectifyError::NotEnoughPoints { got: corners.len() });
84 }
85
86 let (mut min_q, mut min_r) = (i32::MAX, i32::MAX);
88 let (mut max_q, mut max_r) = (i32::MIN, i32::MIN);
89 for g in corners.keys() {
90 min_q = min_q.min(g.i);
91 min_r = min_r.min(g.j);
92 max_q = max_q.max(g.i);
93 max_r = max_r.max(g.j);
94 }
95
96 let s = px_per_cell;
97 let s3h: F = sqrt3_half();
98 let half: F = lit(0.5);
99
100 let mut x_min = F::max_value().unwrap_or_else(|| lit(1e30));
101 let mut x_max = -x_min;
102 let mut y_min = x_min;
103 let mut y_max = -y_min;
104
105 for g in corners.keys() {
106 let q: F = lit(g.i as f64);
107 let r: F = lit(g.j as f64);
108 let x = s * (q + r * half);
109 let y = s * (r * s3h);
110 x_min = if x < x_min { x } else { x_min };
111 x_max = if x > x_max { x } else { x_max };
112 y_min = if y < y_min { y } else { y_min };
113 y_max = if y > y_max { y } else { y_max };
114 }
115
116 let margin_px = margin_cells * s;
117 x_min -= margin_px;
118 y_min -= margin_px;
119 x_max += margin_px;
120 y_max += margin_px;
121
122 let rect_width = nalgebra::try_convert::<F, f64>((x_max - x_min).round().max(F::one()))
123 .unwrap_or(1.0) as usize;
124 let rect_height = nalgebra::try_convert::<F, f64>((y_max - y_min).round().max(F::one()))
125 .unwrap_or(1.0) as usize;
126
127 let mut rect_pts = Vec::with_capacity(corners.len());
129 let mut img_pts = Vec::with_capacity(corners.len());
130 for (g, &pos) in corners {
131 let q: F = lit(g.i as f64);
132 let r: F = lit(g.j as f64);
133 let rx = s * (q + r * half) - x_min;
134 let ry = s * (r * s3h) - y_min;
135 rect_pts.push(Point2::new(rx, ry));
136 img_pts.push(pos);
137 }
138
139 let h_img_from_rect = estimate_homography(&rect_pts, &img_pts)
140 .ok_or(HexGridRectifyError::HomographyFailed)?;
141
142 let h_rect_from_img = h_img_from_rect
143 .inverse()
144 .ok_or(HexGridRectifyError::NonInvertible)?;
145
146 let margin_ceil =
148 nalgebra::try_convert::<F, f64>(margin_cells.ceil()).unwrap_or(0.0) as i32;
149 let mq = min_q - margin_ceil;
150 let mr = min_r - margin_ceil;
151 let aq = max_q + margin_ceil;
152 let ar = max_r + margin_ceil;
153
154 Ok(Self {
155 h_img_from_rect,
156 h_rect_from_img,
157 min_q: mq,
158 min_r: mr,
159 max_q: aq,
160 max_r: ar,
161 px_per_cell,
162 rect_width,
163 rect_height,
164 x_offset: x_min,
165 y_offset: y_min,
166 })
167 }
168
169 pub fn rect_to_img(&self, p_rect: Point2<F>) -> Point2<F> {
171 self.h_img_from_rect.apply(p_rect)
172 }
173
174 pub fn img_to_rect(&self, p_img: Point2<F>) -> Point2<F> {
176 self.h_rect_from_img.apply(p_img)
177 }
178
179 pub fn axial_to_rect(&self, q: F, r: F) -> Point2<F> {
186 let s = self.px_per_cell;
187 let half: F = lit(0.5);
188 let x = s * (q + r * half) - self.x_offset;
189 let y = s * (r * sqrt3_half::<F>()) - self.y_offset;
190 Point2::new(x, y)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn make_hex_corners(radius: i32, spacing: f32) -> HashMap<GridCoords, Point2<f32>> {
199 let sqrt3 = 3.0f32.sqrt();
200 let mut map = HashMap::new();
201 for q in -radius..=radius {
202 for r in -radius..=radius {
203 if (q + r).abs() > radius {
204 continue;
205 }
206 let x = spacing * (q as f32 + r as f32 * 0.5);
207 let y = spacing * (r as f32 * sqrt3 / 2.0);
208 map.insert(GridCoords { i: q, j: r }, Point2::new(x, y));
209 }
210 }
211 map
212 }
213
214 #[test]
215 fn round_trip_rect_to_img() {
216 let corners = make_hex_corners(3, 60.0);
217 let h = HexGridHomography::from_corners(&corners, 60.0, 1.0).unwrap();
218
219 for &pos in corners.values() {
220 let rect = h.img_to_rect(pos);
221 let recovered = h.rect_to_img(rect);
222 assert!(
223 (recovered.x - pos.x).abs() < 0.5,
224 "x: {} vs {}",
225 recovered.x,
226 pos.x
227 );
228 assert!(
229 (recovered.y - pos.y).abs() < 0.5,
230 "y: {} vs {}",
231 recovered.y,
232 pos.y
233 );
234 }
235 }
236
237 #[test]
238 fn identity_case_with_ideal_positions() {
239 let corners = make_hex_corners(2, 50.0);
240 let h = HexGridHomography::from_corners(&corners, 50.0, 0.0).unwrap();
241
242 assert!(h.rect_width > 0);
243 assert!(h.rect_height > 0);
244
245 for &img_pos in corners.values() {
246 let rect_pos = h.img_to_rect(img_pos);
247 let recovered = h.rect_to_img(rect_pos);
248 assert!((recovered.x - img_pos.x).abs() < 0.1);
249 assert!((recovered.y - img_pos.y).abs() < 0.1);
250 }
251 }
252
253 #[test]
254 fn axial_to_rect_then_rect_to_img_matches_corners() {
255 let corners = make_hex_corners(3, 60.0);
256 let h = HexGridHomography::from_corners(&corners, 60.0, 1.0).unwrap();
257
258 for (g, &img_pos) in &corners {
259 let rect_pt = h.axial_to_rect(g.i as f32, g.j as f32);
260 let recovered = h.rect_to_img(rect_pt);
261 assert!(
262 (recovered.x - img_pos.x).abs() < 0.5,
263 "x mismatch at ({},{}): {} vs {}",
264 g.i,
265 g.j,
266 recovered.x,
267 img_pos.x,
268 );
269 assert!(
270 (recovered.y - img_pos.y).abs() < 0.5,
271 "y mismatch at ({},{}): {} vs {}",
272 g.i,
273 g.j,
274 recovered.y,
275 img_pos.y,
276 );
277 }
278 }
279
280 #[test]
281 fn too_few_corners_errors() {
282 let mut corners = HashMap::new();
283 corners.insert(GridCoords { i: 0, j: 0 }, Point2::new(0.0, 0.0));
284 corners.insert(GridCoords { i: 1, j: 0 }, Point2::new(50.0, 0.0));
285
286 let result = HexGridHomography::from_corners(&corners, 50.0, 0.0);
287 assert!(result.is_err());
288 }
289}