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}