Skip to main content

chess_corners_core/refine/
mod.rs

1//! Pluggable subpixel-refinement backends.
2//!
3//! All refiners implement the [`CornerRefiner`] trait and are
4//! addressed through the user-facing [`RefinerKind`] enum. Default
5//! settings preserve the historical center-of-mass behaviour.
6//!
7//! - [`center_of_mass`] — legacy 5×5 weighted centroid on the response
8//!   map. Cheap; the default.
9//! - [`forstner`] — gradient structure-tensor refinement on the image
10//!   intensity patch.
11//! - [`saddle_point`] — quadratic surface fit, robust to mild blur.
12//! - [`radon_peak`] — Radon-projection refiner along candidate axes;
13//!   robust to blur and low contrast.
14
15use crate::imageview::ImageView;
16use crate::ResponseMap;
17use serde::{Deserialize, Serialize};
18
19pub mod center_of_mass;
20pub mod forstner;
21pub mod radon_peak;
22pub mod saddle_point;
23
24pub use center_of_mass::{CenterOfMassConfig, CenterOfMassRefiner};
25pub use forstner::{ForstnerConfig, ForstnerRefiner};
26pub use radon_peak::{RadonPeakConfig, RadonPeakRefiner};
27pub use saddle_point::{SaddlePointConfig, SaddlePointRefiner};
28
29/// Status of a refinement attempt.
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum RefineStatus {
33    Accepted,
34    Rejected,
35    OutOfBounds,
36    IllConditioned,
37}
38
39/// Result of refining a single corner candidate.
40#[derive(Copy, Clone, Debug)]
41#[non_exhaustive]
42pub struct RefineResult {
43    /// Refined subpixel x coordinate.
44    pub x: f32,
45    /// Refined subpixel y coordinate.
46    pub y: f32,
47    pub score: f32,
48    pub status: RefineStatus,
49}
50
51impl RefineResult {
52    #[inline]
53    pub fn accepted(xy: [f32; 2], score: f32) -> Self {
54        Self {
55            x: xy[0],
56            y: xy[1],
57            score,
58            status: RefineStatus::Accepted,
59        }
60    }
61}
62
63/// Inputs shared by refinement methods.
64#[derive(Copy, Clone, Debug, Default)]
65#[non_exhaustive]
66pub struct RefineContext<'a> {
67    pub image: Option<ImageView<'a>>,
68    pub response: Option<&'a ResponseMap>,
69}
70
71impl<'a> RefineContext<'a> {
72    /// Construct a [`RefineContext`] with the given image and response.
73    #[inline]
74    pub fn new(image: Option<ImageView<'a>>, response: Option<&'a ResponseMap>) -> Self {
75        Self { image, response }
76    }
77}
78
79/// Trait implemented by pluggable refinement backends.
80pub trait CornerRefiner {
81    /// Half-width of the patch the refiner needs around the seed.
82    fn radius(&self) -> i32;
83    fn refine(&mut self, seed_xy: [f32; 2], ctx: RefineContext<'_>) -> RefineResult;
84}
85
86/// User-facing enum selecting a refinement backend.
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88#[non_exhaustive]
89pub enum RefinerKind {
90    CenterOfMass(CenterOfMassConfig),
91    Forstner(ForstnerConfig),
92    SaddlePoint(SaddlePointConfig),
93    RadonPeak(RadonPeakConfig),
94}
95
96impl Default for RefinerKind {
97    fn default() -> Self {
98        Self::CenterOfMass(CenterOfMassConfig::default())
99    }
100}
101
102/// Runtime refiner with reusable scratch buffers.
103#[derive(Debug)]
104#[non_exhaustive]
105pub enum Refiner {
106    CenterOfMass(CenterOfMassRefiner),
107    Forstner(ForstnerRefiner),
108    SaddlePoint(SaddlePointRefiner),
109    RadonPeak(RadonPeakRefiner),
110}
111
112impl Refiner {
113    pub fn from_kind(kind: RefinerKind) -> Self {
114        match kind {
115            RefinerKind::CenterOfMass(cfg) => Refiner::CenterOfMass(CenterOfMassRefiner::new(cfg)),
116            RefinerKind::Forstner(cfg) => Refiner::Forstner(ForstnerRefiner::new(cfg)),
117            RefinerKind::SaddlePoint(cfg) => Refiner::SaddlePoint(SaddlePointRefiner::new(cfg)),
118            RefinerKind::RadonPeak(cfg) => Refiner::RadonPeak(RadonPeakRefiner::new(cfg)),
119        }
120    }
121}
122
123impl CornerRefiner for Refiner {
124    #[inline]
125    fn radius(&self) -> i32 {
126        match self {
127            Refiner::CenterOfMass(r) => r.radius(),
128            Refiner::Forstner(r) => r.radius(),
129            Refiner::SaddlePoint(r) => r.radius(),
130            Refiner::RadonPeak(r) => r.radius(),
131        }
132    }
133
134    #[inline]
135    fn refine(&mut self, seed_xy: [f32; 2], ctx: RefineContext<'_>) -> RefineResult {
136        match self {
137            Refiner::CenterOfMass(r) => r.refine(seed_xy, ctx),
138            Refiner::Forstner(r) => r.refine(seed_xy, ctx),
139            Refiner::SaddlePoint(r) => r.refine(seed_xy, ctx),
140            Refiner::RadonPeak(r) => r.refine(seed_xy, ctx),
141        }
142    }
143}
144
145#[cfg(test)]
146pub(crate) mod test_fixtures {
147    /// Mildly-blurred synthetic chessboard centred at `offset`. Used
148    /// across the per-refiner test modules.
149    pub(crate) fn synthetic_checkerboard(
150        size: usize,
151        offset: (f32, f32),
152        dark: u8,
153        bright: u8,
154    ) -> Vec<u8> {
155        let mut img = vec![0u8; size * size];
156        let ox = offset.0;
157        let oy = offset.1;
158        for y in 0..size {
159            for x in 0..size {
160                let xf = x as f32 - ox;
161                let yf = y as f32 - oy;
162                let dark_quad = (xf >= 0.0 && yf >= 0.0) || (xf < 0.0 && yf < 0.0);
163                img[y * size + x] = if dark_quad { dark } else { bright };
164            }
165        }
166        let mut blurred = img.clone();
167        for y in 1..(size - 1) {
168            for x in 1..(size - 1) {
169                let mut acc = 0u32;
170                for ky in -1..=1 {
171                    for kx in -1..=1 {
172                        acc +=
173                            img[(y as i32 + ky) as usize * size + (x as i32 + kx) as usize] as u32;
174                    }
175                }
176                blurred[y * size + x] = (acc / 9) as u8;
177            }
178        }
179        blurred
180    }
181}