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