Skip to main content

chess_corners_core/orientation/
api.rs

1//! Public API for sampling and fitting axes at a single point.
2//!
3//! Two entry points:
4//!
5//! - [`fit_axes_at_point`] samples the 16-point ChESS ring around an
6//!   image point and dispatches to the chosen [`OrientationMethod`].
7//!   This is the workhorse used by the descriptor pipeline and by the
8//!   orientation benchmark.
9//! - [`fit_axes_from_samples`] takes pre-sampled ring values directly,
10//!   which is convenient for unit tests that don't want to construct a
11//!   real image. Both routes converge on the same dispatcher.
12
13use super::descriptor::{ring_angles, sample_ring};
14use super::{disk_sector, ring_fit, ring_fit_for_image, OrientationMethod};
15use crate::detect::chess::ring::ring_offsets;
16
17/// Result of a two-axis orientation fit at a single corner.
18///
19/// This is the public mirror of the (crate-private) `TwoAxisFit` used
20/// by the descriptor path. All [`OrientationMethod`] variants populate
21/// the same fields.
22#[derive(Clone, Copy, Debug)]
23#[non_exhaustive]
24pub struct AxisFitResult {
25    /// Bright/dark amplitude `|A|` (≥ 0) recovered by the fit. Units
26    /// are gray levels.
27    pub amp: f32,
28    /// First axis direction, radians in `[0, π)` (line-direction
29    /// representative — see [`crate::detect::CornerDescriptor`]).
30    pub theta1: f32,
31    /// Second axis direction, radians in `(theta1, theta1 + π) ⊂ [0, 2π)`.
32    pub theta2: f32,
33    /// 1σ angular uncertainty for `theta1` (radians).
34    pub sigma_theta1: f32,
35    /// 1σ angular uncertainty for `theta2` (radians).
36    pub sigma_theta2: f32,
37    /// RMS fit residual of the two-axis intensity model (gray levels).
38    pub rms: f32,
39}
40
41impl From<ring_fit::TwoAxisFit> for AxisFitResult {
42    #[inline]
43    fn from(v: ring_fit::TwoAxisFit) -> Self {
44        Self {
45            amp: v.amp,
46            theta1: v.theta1,
47            theta2: v.theta2,
48            sigma_theta1: v.sigma_theta1,
49            sigma_theta2: v.sigma_theta2,
50            rms: v.rms,
51        }
52    }
53}
54
55/// Sample the 16-point ChESS ring at `(cx, cy)` with `radius` and run
56/// the chosen orientation method.
57///
58/// Public so the orientation benchmark can drive the fit directly
59/// without going through the detection pipeline.
60pub fn fit_axes_at_point(
61    img: &[u8],
62    w: usize,
63    h: usize,
64    cx: f32,
65    cy: f32,
66    radius: u32,
67    method: OrientationMethod,
68) -> AxisFitResult {
69    let ring = ring_offsets(radius);
70    let ring_phi = ring_angles(ring);
71    let samples = sample_ring(img, w, h, cx, cy, ring);
72    match method {
73        OrientationMethod::RingFit => {
74            ring_fit_for_image(img, w, h, cx, cy, radius, &samples, &ring_phi).into()
75        }
76        OrientationMethod::DiskFit => {
77            disk_sector::fit(img, w, h, cx, cy, radius, &samples, &ring_phi).into()
78        }
79    }
80}
81
82/// Run the chosen orientation method on pre-sampled ring values.
83///
84/// Use this when you already have the 16 ring samples in hand (e.g.
85/// from a custom sampler or a synthetic test) and only need the fit.
86/// Image-dependent variants will fall back to the ring-only
87/// [`OrientationMethod::RingFit`] result when invoked through this
88/// helper.
89pub fn fit_axes_from_samples(
90    samples: &[f32; 16],
91    ring_phi: &[f32; 16],
92    method: OrientationMethod,
93) -> AxisFitResult {
94    match method {
95        OrientationMethod::RingFit => ring_fit::fit_ring(samples, ring_phi).into(),
96        // DiskFit needs image data; fall back to RingFit when not available.
97        OrientationMethod::DiskFit => ring_fit::fit_ring(samples, ring_phi).into(),
98    }
99}