1use crate::grid_index::GridIndex;
8use crate::homography::{estimate_homography, Homography};
9use nalgebra::Point2;
10use std::collections::HashMap;
11
12const SQRT3_HALF: f64 = 0.866_025_403_784_438_6;
14
15#[non_exhaustive]
16#[derive(thiserror::Error, Debug)]
17pub enum HexRectifyError {
18 #[error("not enough grid corners with positions (need >= 4, got {got})")]
19 NotEnoughPoints { got: usize },
20 #[error("homography estimation failed")]
21 HomographyFailed,
22 #[error("homography not invertible")]
23 NonInvertible,
24}
25
26#[derive(Clone, Debug)]
34pub struct HexGridHomography {
35 pub h_img_from_rect: Homography,
37 pub h_rect_from_img: Homography,
39 pub min_q: i32,
41 pub min_r: i32,
42 pub max_q: i32,
43 pub max_r: i32,
44 pub px_per_cell: f32,
46 pub rect_width: usize,
48 pub rect_height: usize,
49
50 x_offset: f64,
54 y_offset: f64,
55}
56
57impl HexGridHomography {
58 pub fn from_corners(
64 corners: &HashMap<GridIndex, Point2<f32>>,
65 px_per_cell: f32,
66 margin_cells: f32,
67 ) -> Result<Self, HexRectifyError> {
68 if corners.len() < 4 {
69 return Err(HexRectifyError::NotEnoughPoints { got: corners.len() });
70 }
71
72 let (mut min_q, mut min_r) = (i32::MAX, i32::MAX);
74 let (mut max_q, mut max_r) = (i32::MIN, i32::MIN);
75 for g in corners.keys() {
76 min_q = min_q.min(g.i);
77 min_r = min_r.min(g.j);
78 max_q = max_q.max(g.i);
79 max_r = max_r.max(g.j);
80 }
81
82 let s = px_per_cell as f64;
86 let sqrt3_half = SQRT3_HALF;
87
88 let mut x_min = f64::MAX;
90 let mut x_max = f64::MIN;
91 let mut y_min = f64::MAX;
92 let mut y_max = f64::MIN;
93
94 for g in corners.keys() {
95 let x = s * (g.i as f64 + g.j as f64 * 0.5);
96 let y = s * (g.j as f64 * sqrt3_half);
97 x_min = x_min.min(x);
98 x_max = x_max.max(x);
99 y_min = y_min.min(y);
100 y_max = y_max.max(y);
101 }
102
103 let margin_px = margin_cells as f64 * s;
104 x_min -= margin_px;
105 y_min -= margin_px;
106 x_max += margin_px;
107 y_max += margin_px;
108
109 let rect_width = ((x_max - x_min).round().max(1.0)) as usize;
110 let rect_height = ((y_max - y_min).round().max(1.0)) as usize;
111
112 let mut rect_pts = Vec::with_capacity(corners.len());
114 let mut img_pts = Vec::with_capacity(corners.len());
115 for (g, &pos) in corners {
116 let rx = s * (g.i as f64 + g.j as f64 * 0.5) - x_min;
117 let ry = s * (g.j as f64 * sqrt3_half) - y_min;
118 rect_pts.push(Point2::new(rx as f32, ry as f32));
119 img_pts.push(pos);
120 }
121
122 let h_img_from_rect =
123 estimate_homography(&rect_pts, &img_pts).ok_or(HexRectifyError::HomographyFailed)?;
124
125 let h_rect_from_img = h_img_from_rect
126 .inverse()
127 .ok_or(HexRectifyError::NonInvertible)?;
128
129 let mq = min_q - margin_cells.ceil() as i32;
131 let mr = min_r - margin_cells.ceil() as i32;
132 let aq = max_q + margin_cells.ceil() as i32;
133 let ar = max_r + margin_cells.ceil() as i32;
134
135 Ok(Self {
136 h_img_from_rect,
137 h_rect_from_img,
138 min_q: mq,
139 min_r: mr,
140 max_q: aq,
141 max_r: ar,
142 px_per_cell,
143 rect_width,
144 rect_height,
145 x_offset: x_min,
146 y_offset: y_min,
147 })
148 }
149
150 pub fn rect_to_img(&self, p_rect: Point2<f32>) -> Point2<f32> {
152 self.h_img_from_rect.apply(p_rect)
153 }
154
155 pub fn img_to_rect(&self, p_img: Point2<f32>) -> Point2<f32> {
157 self.h_rect_from_img.apply(p_img)
158 }
159
160 pub fn axial_to_rect(&self, q: f64, r: f64) -> Point2<f64> {
167 let s = self.px_per_cell as f64;
168 let x = s * (q + r * 0.5) - self.x_offset;
169 let y = s * (r * SQRT3_HALF) - self.y_offset;
170 Point2::new(x, y)
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn make_hex_corners(radius: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
179 let sqrt3 = 3.0f32.sqrt();
180 let mut map = HashMap::new();
181 for q in -radius..=radius {
182 for r in -radius..=radius {
183 if (q + r).abs() > radius {
184 continue;
185 }
186 let x = spacing * (q as f32 + r as f32 * 0.5);
187 let y = spacing * (r as f32 * sqrt3 / 2.0);
188 map.insert(GridIndex { i: q, j: r }, Point2::new(x, y));
189 }
190 }
191 map
192 }
193
194 #[test]
195 fn round_trip_rect_to_img() {
196 let corners = make_hex_corners(3, 60.0);
197 let h = HexGridHomography::from_corners(&corners, 60.0, 1.0).unwrap();
198
199 for &pos in corners.values() {
200 let rect = h.img_to_rect(pos);
201 let recovered = h.rect_to_img(rect);
202 assert!(
203 (recovered.x - pos.x).abs() < 0.5,
204 "x: {} vs {}",
205 recovered.x,
206 pos.x
207 );
208 assert!(
209 (recovered.y - pos.y).abs() < 0.5,
210 "y: {} vs {}",
211 recovered.y,
212 pos.y
213 );
214 }
215 }
216
217 #[test]
218 fn identity_case_with_ideal_positions() {
219 let corners = make_hex_corners(2, 50.0);
222 let h = HexGridHomography::from_corners(&corners, 50.0, 0.0).unwrap();
223
224 assert!(h.rect_width > 0);
225 assert!(h.rect_height > 0);
226
227 for &img_pos in corners.values() {
229 let rect_pos = h.img_to_rect(img_pos);
231 let recovered = h.rect_to_img(rect_pos);
232 assert!((recovered.x - img_pos.x).abs() < 0.1);
233 assert!((recovered.y - img_pos.y).abs() < 0.1);
234 }
235 }
236
237 #[test]
238 fn axial_to_rect_then_rect_to_img_matches_corners() {
239 let corners = make_hex_corners(3, 60.0);
240 let h = HexGridHomography::from_corners(&corners, 60.0, 1.0).unwrap();
241
242 for (g, &img_pos) in &corners {
243 let rect_pt = h.axial_to_rect(g.i as f64, g.j as f64);
244 let recovered = h.rect_to_img(Point2::new(rect_pt.x as f32, rect_pt.y as f32));
245 assert!(
246 (recovered.x - img_pos.x).abs() < 0.5,
247 "x mismatch at ({},{}): {} vs {}",
248 g.i,
249 g.j,
250 recovered.x,
251 img_pos.x,
252 );
253 assert!(
254 (recovered.y - img_pos.y).abs() < 0.5,
255 "y mismatch at ({},{}): {} vs {}",
256 g.i,
257 g.j,
258 recovered.y,
259 img_pos.y,
260 );
261 }
262 }
263
264 #[test]
265 fn too_few_corners_errors() {
266 let mut corners = HashMap::new();
267 corners.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
268 corners.insert(GridIndex { i: 1, j: 0 }, Point2::new(50.0, 0.0));
269
270 let result = HexGridHomography::from_corners(&corners, 50.0, 0.0);
271 assert!(result.is_err());
272 }
273}