projective_grid/hex/
rectify.rs1use 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
50impl HexGridHomography {
51 pub fn from_corners(
57 corners: &HashMap<GridIndex, Point2<f32>>,
58 px_per_cell: f32,
59 margin_cells: f32,
60 ) -> Result<Self, HexRectifyError> {
61 if corners.len() < 4 {
62 return Err(HexRectifyError::NotEnoughPoints { got: corners.len() });
63 }
64
65 let (mut min_q, mut min_r) = (i32::MAX, i32::MAX);
67 let (mut max_q, mut max_r) = (i32::MIN, i32::MIN);
68 for g in corners.keys() {
69 min_q = min_q.min(g.i);
70 min_r = min_r.min(g.j);
71 max_q = max_q.max(g.i);
72 max_r = max_r.max(g.j);
73 }
74
75 let s = px_per_cell as f64;
79 let sqrt3_half = SQRT3_HALF;
80
81 let mut x_min = f64::MAX;
83 let mut x_max = f64::MIN;
84 let mut y_min = f64::MAX;
85 let mut y_max = f64::MIN;
86
87 for g in corners.keys() {
88 let x = s * (g.i as f64 + g.j as f64 * 0.5);
89 let y = s * (g.j as f64 * sqrt3_half);
90 x_min = x_min.min(x);
91 x_max = x_max.max(x);
92 y_min = y_min.min(y);
93 y_max = y_max.max(y);
94 }
95
96 let margin_px = margin_cells as f64 * s;
97 x_min -= margin_px;
98 y_min -= margin_px;
99 x_max += margin_px;
100 y_max += margin_px;
101
102 let rect_width = ((x_max - x_min).round().max(1.0)) as usize;
103 let rect_height = ((y_max - y_min).round().max(1.0)) as usize;
104
105 let mut rect_pts = Vec::with_capacity(corners.len());
107 let mut img_pts = Vec::with_capacity(corners.len());
108 for (g, &pos) in corners {
109 let rx = s * (g.i as f64 + g.j as f64 * 0.5) - x_min;
110 let ry = s * (g.j as f64 * sqrt3_half) - y_min;
111 rect_pts.push(Point2::new(rx as f32, ry as f32));
112 img_pts.push(pos);
113 }
114
115 let h_img_from_rect =
116 estimate_homography(&rect_pts, &img_pts).ok_or(HexRectifyError::HomographyFailed)?;
117
118 let h_rect_from_img = h_img_from_rect
119 .inverse()
120 .ok_or(HexRectifyError::NonInvertible)?;
121
122 let mq = min_q - margin_cells.ceil() as i32;
124 let mr = min_r - margin_cells.ceil() as i32;
125 let aq = max_q + margin_cells.ceil() as i32;
126 let ar = max_r + margin_cells.ceil() as i32;
127
128 Ok(Self {
129 h_img_from_rect,
130 h_rect_from_img,
131 min_q: mq,
132 min_r: mr,
133 max_q: aq,
134 max_r: ar,
135 px_per_cell,
136 rect_width,
137 rect_height,
138 })
139 }
140
141 pub fn rect_to_img(&self, p_rect: Point2<f32>) -> Point2<f32> {
143 self.h_img_from_rect.apply(p_rect)
144 }
145
146 pub fn img_to_rect(&self, p_img: Point2<f32>) -> Point2<f32> {
148 self.h_rect_from_img.apply(p_img)
149 }
150
151 pub fn axial_to_rect(&self, q: f64, r: f64) -> Point2<f64> {
156 let s = self.px_per_cell as f64;
157 let x = s * (q + r * 0.5);
161 let y = s * (r * SQRT3_HALF);
162 Point2::new(x, y)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 fn make_hex_corners(radius: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
171 let sqrt3 = 3.0f32.sqrt();
172 let mut map = HashMap::new();
173 for q in -radius..=radius {
174 for r in -radius..=radius {
175 if (q + r).abs() > radius {
176 continue;
177 }
178 let x = spacing * (q as f32 + r as f32 * 0.5);
179 let y = spacing * (r as f32 * sqrt3 / 2.0);
180 map.insert(GridIndex { i: q, j: r }, Point2::new(x, y));
181 }
182 }
183 map
184 }
185
186 #[test]
187 fn round_trip_rect_to_img() {
188 let corners = make_hex_corners(3, 60.0);
189 let h = HexGridHomography::from_corners(&corners, 60.0, 1.0).unwrap();
190
191 for &pos in corners.values() {
192 let rect = h.img_to_rect(pos);
193 let recovered = h.rect_to_img(rect);
194 assert!(
195 (recovered.x - pos.x).abs() < 0.5,
196 "x: {} vs {}",
197 recovered.x,
198 pos.x
199 );
200 assert!(
201 (recovered.y - pos.y).abs() < 0.5,
202 "y: {} vs {}",
203 recovered.y,
204 pos.y
205 );
206 }
207 }
208
209 #[test]
210 fn identity_case_with_ideal_positions() {
211 let corners = make_hex_corners(2, 50.0);
214 let h = HexGridHomography::from_corners(&corners, 50.0, 0.0).unwrap();
215
216 assert!(h.rect_width > 0);
217 assert!(h.rect_height > 0);
218
219 for &img_pos in corners.values() {
221 let rect_pos = h.img_to_rect(img_pos);
223 let recovered = h.rect_to_img(rect_pos);
224 assert!((recovered.x - img_pos.x).abs() < 0.1);
225 assert!((recovered.y - img_pos.y).abs() < 0.1);
226 }
227 }
228
229 #[test]
230 fn too_few_corners_errors() {
231 let mut corners = HashMap::new();
232 corners.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
233 corners.insert(GridIndex { i: 1, j: 0 }, Point2::new(50.0, 0.0));
234
235 let result = HexGridHomography::from_corners(&corners, 50.0, 0.0);
236 assert!(result.is_err());
237 }
238}