chess_corners_core/detect/radon/mod.rs
1//! Whole-image Duda-Frese Radon detector.
2//!
3//! An alternative to the ChESS detector in [`super::chess`] for
4//! frames where the 16-sample ring fails — heavy motion blur, strong
5//! defocus, low-contrast scenes, or cells smaller than
6//! `~2·ring_radius`. The detector computes a dense 4-angle localized
7//! Radon response `R(x, y) = (max_α S_α − min_α S_α)²` using
8//! summed-area tables for `O(1)`-per-pixel ray sums, then applies the
9//! same peak-fit pipeline as
10//! [`crate::refine::radon_peak::RadonPeakRefiner`]: threshold / NMS /
11//! box-blur / 3-point Gaussian fit in x and y.
12//!
13//! Submodules:
14//!
15//! - [`primitives`] — angular basis, `fit_peak_frac`, `box_blur_inplace`
16//! shared with [`crate::refine::radon_peak`].
17//! - [`response`] — `radon_response_u8` and the SAT / response-map
18//! buffers and types.
19//! - [`detect`] — `detect_peaks_from_radon` (peak detection on the
20//! response map, including the response-map 3-point Gaussian
21//! subpixel fit).
22
23pub mod detect;
24pub mod primitives;
25pub mod response;
26
27pub use detect::detect_peaks_from_radon;
28pub use response::{
29 radon_response_u8, RadonBuffers, RadonDetectorParams, RadonResponseView, SatElem,
30 MAX_IMAGE_UPSAMPLE,
31};
32
33#[cfg(test)]
34pub(super) mod test_fixtures {
35 /// Anti-aliased synthetic chessboard renderer shared between
36 /// `response.rs` and `detect.rs` test modules.
37 pub(crate) fn synthetic_chessboard_aa(
38 size: usize,
39 cell: usize,
40 offset: (f32, f32),
41 dark: u8,
42 bright: u8,
43 ) -> Vec<u8> {
44 const SUPER: usize = 8;
45 let (ox, oy) = offset;
46 let c = cell as f32;
47 let dark_f = dark as f32;
48 let bright_f = bright as f32;
49 let inv_super2 = 1.0 / (SUPER * SUPER) as f32;
50 let mut img = vec![0u8; size * size];
51 for y in 0..size {
52 for x in 0..size {
53 let mut acc = 0.0f32;
54 for sy in 0..SUPER {
55 let yf = y as f32 + (sy as f32 + 0.5) / SUPER as f32 - 0.5;
56 let cy = ((yf - oy) / c).floor() as i32;
57 for sx in 0..SUPER {
58 let xf = x as f32 + (sx as f32 + 0.5) / SUPER as f32 - 0.5;
59 let cx = ((xf - ox) / c).floor() as i32;
60 let dark_cell = (cx + cy).rem_euclid(2) == 0;
61 acc += if dark_cell { dark_f } else { bright_f };
62 }
63 }
64 img[y * size + x] = (acc * inv_super2).round().clamp(0.0, 255.0) as u8;
65 }
66 }
67 img
68 }
69}