Skip to main content

projective_grid/feature/
mod.rs

1//! Generic feature evidence consumed by grid tasks.
2//!
3//! These types deliberately avoid target-specific identifiers. A caller that
4//! decodes marker IDs, ring IDs, or any other target metadata should convert
5//! that information into coordinate hypotheses or caller-side filtering before
6//! using this crate.
7//!
8//! The detection surface is pinned to `f32`: the topological pipeline,
9//! the homography fit, and every downstream consumer (chessboard,
10//! puzzleboard, charuco) operate in single precision. The remaining
11//! generic-`F` surface in the crate is the pure-geometry
12//! [`crate::geometry`] module.
13
14use nalgebra::Point2;
15
16use crate::lattice::Coord;
17
18/// One detected image-space feature.
19#[derive(Clone, Copy, Debug, PartialEq)]
20#[non_exhaustive]
21pub struct PointFeature {
22    /// Stable caller-owned index for this feature.
23    pub source_index: usize,
24    /// Feature position in image-frame pixel-center coordinates.
25    pub position: Point2<f32>,
26}
27
28impl PointFeature {
29    /// Construct a point feature from its source index and image position.
30    pub fn new(source_index: usize, position: Point2<f32>) -> Self {
31        Self {
32            source_index,
33            position,
34        }
35    }
36}
37
38/// One undirected local lattice-axis observation.
39#[derive(Clone, Copy, Debug, PartialEq)]
40#[non_exhaustive]
41pub struct LocalAxis {
42    /// Axis angle in radians in the image frame.
43    pub angle_rad: f32,
44    /// Optional angular uncertainty in radians.
45    pub sigma_rad: Option<f32>,
46}
47
48impl LocalAxis {
49    /// Construct a local axis with optional angular uncertainty.
50    pub fn new(angle_rad: f32, sigma_rad: Option<f32>) -> Self {
51        Self {
52            angle_rad,
53            sigma_rad,
54        }
55    }
56}
57
58/// A point feature augmented with one or more local lattice directions.
59#[derive(Clone, Copy, Debug, PartialEq)]
60#[non_exhaustive]
61pub struct OrientedFeature<const N: usize> {
62    /// The underlying image-space point feature.
63    pub point: PointFeature,
64    /// Local lattice directions associated with this feature.
65    pub axes: [LocalAxis; N],
66}
67
68impl<const N: usize> OrientedFeature<N> {
69    /// Construct an oriented feature from a point and fixed-size axis set.
70    pub fn new(point: PointFeature, axes: [LocalAxis; N]) -> Self {
71        Self { point, axes }
72    }
73}
74
75/// Caller-supplied hypothesis that a feature lies at a lattice coordinate.
76#[derive(Clone, Copy, Debug, PartialEq)]
77#[non_exhaustive]
78pub struct CoordinateHypothesis {
79    /// Source index of the feature this hypothesis labels.
80    pub source_index: usize,
81    /// Proposed lattice coordinate.
82    pub coord: Coord,
83    /// Optional caller confidence. The v1 consistency checker preserves this
84    /// field only as evidence metadata and does not weight the fit with it.
85    pub confidence: Option<f32>,
86}
87
88impl CoordinateHypothesis {
89    /// Construct a coordinate hypothesis.
90    pub fn new(source_index: usize, coord: Coord, confidence: Option<f32>) -> Self {
91        Self {
92            source_index,
93            coord,
94            confidence,
95        }
96    }
97
98    /// Construct an unweighted coordinate hypothesis with no caller confidence.
99    pub fn unweighted(source_index: usize, coord: Coord) -> Self {
100        Self::new(source_index, coord, None)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use nalgebra::Point2;
107
108    use super::*;
109
110    #[test]
111    fn point_feature_constructs() {
112        let p = PointFeature::new(7, Point2::new(1.0_f32, 2.0));
113        assert_eq!(p.source_index, 7);
114        assert_eq!(p.position, Point2::new(1.0, 2.0));
115    }
116
117    #[test]
118    fn oriented_feature_arities_construct() {
119        let p = PointFeature::new(0, Point2::new(0.0_f32, 0.0));
120        let a = LocalAxis::new(0.0, Some(0.1));
121        let one = OrientedFeature::<1>::new(p, [a]);
122        let two = OrientedFeature::<2>::new(p, [a, LocalAxis::new(1.0, None)]);
123        let three =
124            OrientedFeature::<3>::new(p, [a, LocalAxis::new(1.0, None), LocalAxis::new(2.0, None)]);
125        assert_eq!(one.axes.len(), 1);
126        assert_eq!(two.axes.len(), 2);
127        assert_eq!(three.axes.len(), 3);
128    }
129
130    #[test]
131    fn coordinate_hypothesis_constructs() {
132        let h = CoordinateHypothesis::new(3, Coord::new(4, -2), Some(0.9_f32));
133        assert_eq!(h.source_index, 3);
134        assert_eq!(h.coord, Coord::new(4, -2));
135        assert_eq!(h.confidence, Some(0.9));
136    }
137
138    #[test]
139    fn coordinate_hypothesis_unweighted_has_no_confidence() {
140        let h = CoordinateHypothesis::unweighted(7, Coord::new(1, 2));
141        assert_eq!(h.source_index, 7);
142        assert_eq!(h.coord, Coord::new(1, 2));
143        assert_eq!(h.confidence, None);
144    }
145}