Skip to main content

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}