1use calib_targets_aruco::{BoardCell, Dictionary};
4use nalgebra::Point2;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
9#[serde(rename_all = "snake_case")]
10pub enum MarkerLayout {
11 #[serde(rename = "opencv_charuco", alias = "open_cv_charuco")]
15 #[default]
16 OpenCvCharuco,
17}
18
19#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
23pub struct CharucoBoardSpec {
24 pub rows: u32,
25 pub cols: u32,
26 pub cell_size: f32,
27 pub marker_size_rel: f32,
28 pub dictionary: Dictionary,
29 #[serde(default)]
30 pub marker_layout: MarkerLayout,
31}
32
33#[derive(thiserror::Error, Debug)]
35pub enum CharucoBoardError {
36 #[error("rows and cols must be >= 2")]
37 InvalidSize,
38 #[error("cell_size must be > 0")]
39 InvalidCellSize,
40 #[error("marker_size_rel must be in (0, 1]")]
41 InvalidMarkerSizeRel,
42 #[error("dictionary has no codes")]
43 EmptyDictionary,
44 #[error("board needs {needed} markers, dictionary has {available}")]
45 NotEnoughDictionaryCodes { needed: usize, available: usize },
46}
47
48#[derive(Clone, Debug)]
50pub struct CharucoBoard {
51 spec: CharucoBoardSpec,
52 marker_positions: Vec<BoardCell>,
53}
54
55impl CharucoBoard {
56 pub fn new(spec: CharucoBoardSpec) -> Result<Self, CharucoBoardError> {
58 if spec.rows < 2 || spec.cols < 2 {
59 return Err(CharucoBoardError::InvalidSize);
60 }
61 if !spec.cell_size.is_finite() || spec.cell_size <= 0.0 {
62 return Err(CharucoBoardError::InvalidCellSize);
63 }
64 if !spec.marker_size_rel.is_finite()
65 || spec.marker_size_rel <= 0.0
66 || spec.marker_size_rel > 1.0
67 {
68 return Err(CharucoBoardError::InvalidMarkerSizeRel);
69 }
70 if spec.dictionary.codes.is_empty() {
71 return Err(CharucoBoardError::EmptyDictionary);
72 }
73
74 let marker_positions = match spec.marker_layout {
75 MarkerLayout::OpenCvCharuco => open_cv_charuco_marker_positions(spec.rows, spec.cols),
76 };
77
78 let needed = marker_positions.len();
79 let available = spec.dictionary.codes.len();
80 if available < needed {
81 return Err(CharucoBoardError::NotEnoughDictionaryCodes { needed, available });
82 }
83
84 Ok(Self {
85 spec,
86 marker_positions,
87 })
88 }
89
90 #[inline]
92 pub fn spec(&self) -> CharucoBoardSpec {
93 self.spec
94 }
95
96 #[inline]
98 pub fn expected_inner_rows(&self) -> u32 {
99 self.spec.rows - 1
100 }
101
102 #[inline]
104 pub fn expected_inner_cols(&self) -> u32 {
105 self.spec.cols - 1
106 }
107
108 #[inline]
110 pub fn marker_position(&self, id: u32) -> Option<BoardCell> {
111 self.marker_positions.get(id as usize).cloned()
112 }
113
114 pub fn marker_cell(&self, marker_id: i32) -> Option<(usize, usize)> {
118 let id = u32::try_from(marker_id).ok()?;
119 let bc = self.marker_position(id)?;
120 let sx = usize::try_from(bc.sx).ok()?;
121 let sy = usize::try_from(bc.sy).ok()?;
122 Some((sx, sy))
123 }
124
125 pub fn marker_surrounding_charuco_corners(&self, marker_id: i32) -> Option<[usize; 4]> {
130 let (sx, sy) = self.marker_cell(marker_id)?;
131 marker_surrounding_charuco_corners_for_cell(
132 self.spec.cols as usize,
133 self.spec.rows as usize,
134 sx,
135 sy,
136 )
137 }
138
139 #[inline]
141 pub fn marker_count(&self) -> usize {
142 self.marker_positions.len()
143 }
144
145 pub fn charuco_corner_id_from_board_corner(&self, i: i32, j: i32) -> Option<u32> {
149 let cols = i32::try_from(self.spec.cols).ok()?;
150 let rows = i32::try_from(self.spec.rows).ok()?;
151
152 if i <= 0 || j <= 0 || i >= cols || j >= rows {
153 return None;
154 }
155
156 let inner_cols = cols - 1;
157 let ii = i - 1;
158 let jj = j - 1;
159 Some((jj as u32) * (inner_cols as u32) + (ii as u32))
160 }
161
162 pub fn charuco_object_xy(&self, id: u32) -> Option<Point2<f32>> {
166 let cols = self.spec.cols.checked_sub(1)?; let rows = self.spec.rows.checked_sub(1)?; let count = cols.checked_mul(rows)?;
169 if id >= count {
170 return None;
171 }
172 let i = (id % cols) as f32 + 1.0;
173 let j = (id / cols) as f32 + 1.0;
174 Some(Point2::new(
175 i * self.spec.cell_size,
176 j * self.spec.cell_size,
177 ))
178 }
179}
180
181pub fn is_internal_intersection(squares_x: usize, squares_y: usize, ix: usize, iy: usize) -> bool {
183 squares_x >= 2
184 && squares_y >= 2
185 && (1..=squares_x - 1).contains(&ix)
186 && (1..=squares_y - 1).contains(&iy)
187}
188
189pub fn charuco_corner_id(
191 squares_x: usize,
192 squares_y: usize,
193 ix: usize,
194 iy: usize,
195) -> Option<usize> {
196 if !is_internal_intersection(squares_x, squares_y, ix, iy) {
197 return None;
198 }
199 let stride = squares_x.checked_sub(1)?;
200 let ix0 = ix.checked_sub(1)?;
201 let iy0 = iy.checked_sub(1)?;
202 Some(iy0 * stride + ix0)
203}
204
205fn marker_surrounding_charuco_corners_for_cell(
206 squares_x: usize,
207 squares_y: usize,
208 sx: usize,
209 sy: usize,
210) -> Option<[usize; 4]> {
211 if squares_x < 2 || squares_y < 2 {
212 return None;
213 }
214 if sx == 0 || sy == 0 {
215 return None;
216 }
217 if sx + 1 >= squares_x || sy + 1 >= squares_y {
218 return None;
219 }
220 let tl = charuco_corner_id(squares_x, squares_y, sx, sy)?;
221 let tr = charuco_corner_id(squares_x, squares_y, sx + 1, sy)?;
222 let br = charuco_corner_id(squares_x, squares_y, sx + 1, sy + 1)?;
223 let bl = charuco_corner_id(squares_x, squares_y, sx, sy + 1)?;
224 Some([tl, tr, br, bl])
225}
226
227fn open_cv_charuco_marker_positions(rows: u32, cols: u32) -> Vec<BoardCell> {
228 let mut out = Vec::new();
229 for j in 0..(rows as i32) {
230 for i in 0..(cols as i32) {
231 if ((i + j) & 1) == 1 {
233 out.push(BoardCell { sx: i, sy: j });
234 }
235 }
236 }
237 out
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use calib_targets_aruco::builtins;
244
245 fn build_board() -> CharucoBoard {
246 let dict = builtins::builtin_dictionary("DICT_4X4_50").expect("dict");
247 CharucoBoard::new(CharucoBoardSpec {
248 rows: 5,
249 cols: 6,
250 cell_size: 1.0,
251 marker_size_rel: 0.75,
252 dictionary: dict,
253 marker_layout: MarkerLayout::OpenCvCharuco,
254 })
255 .expect("board")
256 }
257
258 fn build_board_1000() -> CharucoBoard {
259 let dict = builtins::builtin_dictionary("DICT_4X4_1000").expect("dict");
260 CharucoBoard::new(CharucoBoardSpec {
261 rows: 22,
262 cols: 22,
263 cell_size: 1.0,
264 marker_size_rel: 0.75,
265 dictionary: dict,
266 marker_layout: MarkerLayout::OpenCvCharuco,
267 })
268 .expect("board")
269 }
270
271 #[test]
272 fn marker_surrounding_charuco_corners_matches_expected() {
273 let board = build_board();
274 let marker_id = 4;
275 let cell = board.marker_cell(marker_id).expect("marker cell");
276 assert_eq!(cell, (2, 1));
277
278 let corners = board
279 .marker_surrounding_charuco_corners(marker_id)
280 .expect("corners");
281 assert_eq!(corners, [1, 2, 7, 6]);
282 }
283
284 #[test]
285 fn border_marker_has_no_four_corner_neighborhood() {
286 let board = build_board();
287 let marker_id = 0;
288 let cell = board.marker_cell(marker_id).expect("marker cell");
289 assert_eq!(cell, (1, 0));
290 assert!(board
291 .marker_surrounding_charuco_corners(marker_id)
292 .is_none());
293 }
294
295 #[test]
296 fn marker_22() {
297 let board = build_board_1000();
298 let marker_id = 22;
299 let cell = board.marker_cell(marker_id).expect("marker cell");
300 assert_eq!(cell, (1, 2));
301 let corners = board
302 .marker_surrounding_charuco_corners(marker_id)
303 .expect("corners");
304 assert_eq!(corners, [21, 22, 43, 42]);
305 }
306}