Skip to main content

chess_corners_core/
descriptor.rs

1//! Corner descriptors and helpers for chessboard detection.
2//!
3//! This module turns raw ChESS corner candidates into richer
4//! [`CornerDescriptor`] values that carry subpixel position,
5//! response, and orientation.
6//!
7//! The detector in [`crate::detect`] produces intermediate
8//! [`Corner`] values; [`corners_to_descriptors`] then samples the
9//! original image on a ChESS ring around each corner to estimate the
10//! additional attributes using the conventions documented on
11//! [`CornerDescriptor`].
12use crate::ring::ring_offsets;
13#[cfg(feature = "tracing")]
14use tracing::instrument;
15
16/// A detected ChESS corner (subpixel).
17#[derive(Clone, Debug)]
18pub struct Corner {
19    /// Subpixel x coordinate in image pixels.
20    pub x: f32,
21    /// Subpixel y coordinate in image pixels.
22    pub y: f32,
23    /// Raw ChESS response at the integer peak (before COM refinement).
24    pub strength: f32,
25}
26
27/// Describes a detected chessboard corner in full-resolution image coordinates.
28#[derive(Clone, Copy, Debug)]
29pub struct CornerDescriptor {
30    /// Subpixel position in full-resolution image pixels.
31    pub x: f32,
32    pub y: f32,
33
34    /// ChESS response / strength at this corner (in the full-res image).
35    pub response: f32,
36
37    /// Orientation of the local grid axis at the corner, in radians.
38    ///
39    /// Convention:
40    /// - in [0, PI)
41    /// - one of the two orthogonal grid axes; the other is theta + PI/2.
42    pub orientation: f32,
43}
44
45/// Convert raw corner candidates into full descriptors by sampling the source image.
46///
47/// Orientation follows the conventions documented on [`CornerDescriptor`].
48#[cfg_attr(
49    feature = "tracing",
50    instrument(
51        level = "info",
52        skip(img, corners),
53        fields(corners = corners.len())
54    )
55)]
56pub fn corners_to_descriptors(
57    img: &[u8],
58    w: usize,
59    h: usize,
60    radius: u32,
61    corners: Vec<Corner>,
62) -> Vec<CornerDescriptor> {
63    let ring = ring_offsets(radius);
64    let mut out = Vec::with_capacity(corners.len());
65    for c in corners {
66        let samples = sample_ring(img, w, h, c.x, c.y, ring);
67        let orientation = estimate_orientation_from_ring(&samples, ring);
68
69        out.push(CornerDescriptor {
70            x: c.x,
71            y: c.y,
72            response: c.strength,
73            orientation,
74        });
75    }
76    out
77}
78
79/// Sample the 16-point ChESS ring around (x, y) using bilinear interpolation.
80fn sample_ring(
81    img: &[u8],
82    w: usize,
83    h: usize,
84    x: f32,
85    y: f32,
86    ring: &[(i32, i32); 16],
87) -> [f32; 16] {
88    let mut samples = [0.0f32; 16];
89    for (i, &(dx, dy)) in ring.iter().enumerate() {
90        let sx = x + dx as f32;
91        let sy = y + dy as f32;
92        samples[i] = sample_bilinear(img, w, h, sx, sy);
93    }
94    samples
95}
96
97#[inline]
98fn estimate_orientation_from_ring(samples: &[f32; 16], ring: &[(i32, i32); 16]) -> f32 {
99    // Same logic you use now: 2nd harmonic over sample index.
100    let mean = samples.iter().copied().sum::<f32>() / 16.0;
101
102    let mut c2 = 0.0f32;
103    let mut s2 = 0.0f32;
104
105    for (&v_raw, &(dx_i, dy_i)) in samples.iter().zip(ring.iter()) {
106        let v = v_raw - mean;
107        let angle = (dy_i as f32).atan2(dx_i as f32);
108        let a2 = 2.0 * angle;
109        c2 += v * a2.cos();
110        s2 += v * a2.sin();
111    }
112
113    let mut theta = 0.5 * s2.atan2(c2);
114    if theta < 0.0 {
115        theta += core::f32::consts::PI;
116    }
117    if !theta.is_finite() {
118        theta = 0.0;
119    }
120
121    theta
122}
123
124fn sample_bilinear(img: &[u8], w: usize, h: usize, x: f32, y: f32) -> f32 {
125    if w == 0 || h == 0 {
126        return 0.0;
127    }
128
129    let max_x = (w - 1) as f32;
130    let max_y = (h - 1) as f32;
131    let xf = x.clamp(0.0, max_x);
132    let yf = y.clamp(0.0, max_y);
133
134    let x0 = xf.floor() as usize;
135    let y0 = yf.floor() as usize;
136    let x1 = (x0 + 1).min(w - 1);
137    let y1 = (y0 + 1).min(h - 1);
138
139    let wx = xf - x0 as f32;
140    let wy = yf - y0 as f32;
141
142    let i00 = img[y0 * w + x0] as f32;
143    let i10 = img[y0 * w + x1] as f32;
144    let i01 = img[y1 * w + x0] as f32;
145    let i11 = img[y1 * w + x1] as f32;
146
147    let i0 = i00 * (1.0 - wx) + i10 * wx;
148    let i1 = i01 * (1.0 - wx) + i11 * wx;
149    i0 * (1.0 - wy) + i1 * wy
150}