calib_targets_core/corner.rs
1use nalgebra::Point2;
2use projective_grid::Coord;
3use projective_grid::LocalAxis as NextLocalAxis;
4use serde::{Deserialize, Serialize};
5
6/// Local estimate of one undirected grid axis at a detected corner.
7///
8/// `angle` is in radians. `sigma` is the 1σ angular uncertainty in radians.
9/// Default-constructed axes carry `sigma = π`, the workspace's no-information
10/// sentinel for axis-aware grid builders.
11#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
12pub struct AxisEstimate {
13 /// Axis angle in radians.
14 pub angle: f32,
15 /// 1σ angular uncertainty in radians.
16 pub sigma: f32,
17}
18
19impl Default for AxisEstimate {
20 fn default() -> Self {
21 Self {
22 angle: 0.0,
23 sigma: std::f32::consts::PI,
24 }
25 }
26}
27
28impl AxisEstimate {
29 /// Construct an axis estimate from a bare angle with no uncertainty
30 /// penalty (`sigma = 0.0`).
31 pub fn from_angle(angle: f32) -> Self {
32 Self { angle, sigma: 0.0 }
33 }
34}
35
36// ---- Conversions to / from projective-grid ----
37
38/// Promote [`AxisEstimate`] into the [`projective_grid`] crate's generic
39/// local-axis shape.
40#[inline]
41pub fn axis_estimate_to_next(a: AxisEstimate) -> NextLocalAxis {
42 NextLocalAxis::new(a.angle, Some(a.sigma))
43}
44
45#[cfg(test)]
46mod axis_tests {
47 use super::*;
48
49 #[test]
50 fn default_axis_is_no_information_sentinel() {
51 let axis = AxisEstimate::default();
52 assert_eq!(axis.angle, 0.0);
53 assert_eq!(axis.sigma, std::f32::consts::PI);
54 }
55
56 #[test]
57 fn from_angle_sets_zero_sigma() {
58 let axis = AxisEstimate::from_angle(1.25);
59 assert_eq!(axis.angle, 1.25);
60 assert_eq!(axis.sigma, 0.0);
61 }
62
63 #[test]
64 fn to_next_carries_angle_and_sigma() {
65 let axis = AxisEstimate {
66 angle: 0.75,
67 sigma: 0.02,
68 };
69 let next = axis_estimate_to_next(axis);
70 assert_eq!(next.angle_rad, 0.75);
71 assert_eq!(next.sigma_rad, Some(0.02));
72 }
73}
74
75/// The kind of target that a detection corresponds to.
76#[non_exhaustive]
77#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum TargetKind {
80 /// A plain chessboard: integer-labelled X-junction corners only.
81 Chessboard,
82 /// A ChArUco board: a chessboard fused with ArUco markers in the
83 /// white cells, giving each corner an absolute ID.
84 Charuco,
85 /// A checkerboard marker board: a chessboard with a small set of
86 /// circular markers identifying its orientation.
87 CheckerboardMarker,
88 /// A PuzzleBoard: a self-identifying chessboard whose edge dots give
89 /// every corner an absolute `(I, J)` label.
90 PuzzleBoard,
91}
92
93/// A corner that is part of a detected target, with optional ID info.
94///
95/// `#[non_exhaustive]`: this carrier accretes optional fields as detectors
96/// gain capabilities. Construct it with [`LabeledCorner::new`] (position +
97/// score) and attach grid / ID / target-space metadata with the `with_*`
98/// setters.
99#[non_exhaustive]
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct LabeledCorner {
102 /// Pixel position.
103 pub position: Point2<f32>,
104
105 /// Optional integer grid coordinates `(u, v)`.
106 pub grid: Option<Coord>,
107
108 /// Optional logical ID (e.g. ChArUco or marker-board ID).
109 pub id: Option<u32>,
110
111 /// Optional target-space position in millimeters (paired with `id`).
112 #[serde(default)]
113 pub target_position: Option<Point2<f32>>,
114
115 /// Detection score (higher is better).
116 ///
117 /// The meaning depends on the detector (it may be unnormalized).
118 #[serde(alias = "confidence")]
119 pub score: f32,
120}
121
122impl LabeledCorner {
123 /// Create a corner from its required fields (`position`, `score`).
124 ///
125 /// `grid`, `id`, and `target_position` start unset; attach them with
126 /// [`Self::with_grid`], [`Self::with_id`], and
127 /// [`Self::with_target_position`].
128 pub fn new(position: Point2<f32>, score: f32) -> Self {
129 Self {
130 position,
131 grid: None,
132 id: None,
133 target_position: None,
134 score,
135 }
136 }
137
138 /// Attach integer grid coordinates `(u, v)`.
139 #[must_use]
140 pub fn with_grid(mut self, grid: Coord) -> Self {
141 self.grid = Some(grid);
142 self
143 }
144
145 /// Attach a logical ID (e.g. ChArUco or marker-board ID).
146 #[must_use]
147 pub fn with_id(mut self, id: u32) -> Self {
148 self.id = Some(id);
149 self
150 }
151
152 /// Attach a target-space position in millimeters (paired with `id`).
153 #[must_use]
154 pub fn with_target_position(mut self, target_position: Point2<f32>) -> Self {
155 self.target_position = Some(target_position);
156 self
157 }
158}
159
160/// One detected target (board instance) in an image.
161///
162/// `#[non_exhaustive]`: construct with [`TargetDetection::new`].
163#[non_exhaustive]
164#[derive(Clone, Debug, Serialize, Deserialize)]
165pub struct TargetDetection {
166 /// Which kind of calibration target this detection describes.
167 pub kind: TargetKind,
168 /// The detected corners. No ordering or completeness is promised by
169 /// this generic carrier; each detector documents its own guarantees.
170 pub corners: Vec<LabeledCorner>,
171}
172
173impl TargetDetection {
174 /// Create a detection from its target kind and labelled corners.
175 pub fn new(kind: TargetKind, corners: Vec<LabeledCorner>) -> Self {
176 Self { kind, corners }
177 }
178}