Skip to main content

chess_corners/
diagnostics.rs

1//! Opt-in diagnostic channel for inspecting how a detection was produced.
2//!
3//! The items in this module expose **intermediate evidence** from the
4//! detector pipeline — the dense ChESS response map and the Radon
5//! heatmap — rather than the detection result itself. They exist for
6//! debugging, parameter tuning, and visualization: rendering a response
7//! map as an overlay, checking why a corner was or was not seeded, or
8//! sweeping a threshold against a heatmap.
9//!
10//! These are **not** part of the normal detection result contract. The
11//! supported way to obtain corners is [`Detector`] plus
12//! [`DetectorConfig`](crate::DetectorConfig); a typical consumer never
13//! needs this module.
14//!
15//! # Stability
16//!
17//! This channel carries a **looser stability promise** than
18//! [`Detector`] and [`DetectorConfig`](crate::DetectorConfig).
19//! The shape of an intermediate response map or heatmap may change as
20//! the detector internals evolve, even when the detection result
21//! contract does not. Treat anything reachable here as advisory
22//! diagnostic data, not a versioned API guarantee.
23
24/// Storage for a dense detector response map (one score per pixel).
25pub use chess_corners_core::ResponseMap;
26
27/// Compute the dense ChESS response map for an 8-bit grayscale image.
28pub use chess_corners_core::chess_response_u8;
29
30/// Compute the Radon corner heatmap for an 8-bit grayscale buffer.
31pub use crate::radon::radon_heatmap_u8;
32
33/// Compute the Radon corner heatmap from an `image::GrayImage`.
34#[cfg(feature = "image")]
35pub use crate::radon::radon_heatmap_image;
36
37use crate::error::ChessError;
38use crate::Detector;
39
40/// Detector-bound accessor for the diagnostic outputs in this module.
41///
42/// This is the detector-bound half of the diagnostics channel: it
43/// exposes the same intermediate response maps and heatmaps as the
44/// free functions above, but sources its parameters from an existing,
45/// already-configured [`Detector`] instead of asking the caller to
46/// re-supply a [`DetectorConfig`](crate::DetectorConfig). It is the
47/// convenient path when you already hold a configured [`Detector`].
48///
49/// Obtain one with [`Detector::diagnostics`]. The handle borrows the
50/// detector; it neither clones the configuration nor mutates the
51/// detector.
52///
53/// # Stability
54///
55/// Like the free functions in this module, the methods here carry a
56/// **looser stability promise** than [`Detector::detect`] and the
57/// detection result contract. Treat their output as advisory
58/// diagnostic data, not a versioned API guarantee.
59pub struct DetectorDiagnostics<'a> {
60    detector: &'a Detector,
61}
62
63impl<'a> DetectorDiagnostics<'a> {
64    /// Wrap a borrowed detector. Constructed via [`Detector::diagnostics`].
65    pub(crate) fn new(detector: &'a Detector) -> Self {
66        Self { detector }
67    }
68
69    /// Compute the dense ChESS response map for an 8-bit grayscale
70    /// buffer, using the ring and threshold parameters of the bound
71    /// detector's [`DetectorConfig`](crate::DetectorConfig).
72    ///
73    /// `img` must be `width * height` bytes in row-major order. The
74    /// response is computed at input resolution (the detector's
75    /// upscale stage is not applied here); the returned
76    /// [`ResponseMap`] therefore has the same dimensions as the input.
77    ///
78    /// This mirrors the free function [`chess_response_u8`] but sources
79    /// [`ChessParams`](chess_corners_core::ChessParams) from the
80    /// bound detector.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`ChessError::DimensionMismatch`] if
85    /// `img.len() != width * height`.
86    pub fn chess_response_u8(
87        &self,
88        img: &[u8],
89        width: u32,
90        height: u32,
91    ) -> Result<ResponseMap, ChessError> {
92        let w = width as usize;
93        let h = height as usize;
94        let expected = w * h;
95        if img.len() != expected {
96            return Err(ChessError::DimensionMismatch {
97                expected,
98                actual: img.len(),
99            });
100        }
101        let params = self.detector.config().chess_params();
102        Ok(chess_response_u8(img, w, h, &params))
103    }
104
105    /// Compute the dense Radon corner heatmap for an 8-bit grayscale
106    /// buffer, using the bound detector's
107    /// [`DetectorConfig`](crate::DetectorConfig).
108    ///
109    /// `img` must be `width * height` bytes in row-major order. As with
110    /// the free function [`radon_heatmap_u8`], if the detector's
111    /// upscale configuration is enabled the input is upscaled first and
112    /// the heatmap is returned at working resolution.
113    ///
114    /// This mirrors [`radon_heatmap_u8`] but sources the
115    /// [`DetectorConfig`](crate::DetectorConfig) from the bound
116    /// detector.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`ChessError::DimensionMismatch`] if
121    /// `img.len() != width * height`. Returns [`ChessError::Upscale`]
122    /// if the detector's upscale configuration is invalid.
123    pub fn radon_heatmap_u8(
124        &self,
125        img: &[u8],
126        width: u32,
127        height: u32,
128    ) -> Result<ResponseMap, ChessError> {
129        crate::radon::radon_heatmap_u8(img, width, height, self.detector.config())
130    }
131
132    /// Compute the dense Radon corner heatmap from an
133    /// [`image::GrayImage`], using the bound detector's
134    /// [`DetectorConfig`](crate::DetectorConfig). See
135    /// [`Self::radon_heatmap_u8`].
136    ///
137    /// # Errors
138    ///
139    /// Inherits the error contract of [`Self::radon_heatmap_u8`].
140    #[cfg(feature = "image")]
141    pub fn radon_heatmap(&self, img: &image::GrayImage) -> Result<ResponseMap, ChessError> {
142        self.radon_heatmap_u8(img.as_raw(), img.width(), img.height())
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::{Detector, DetectorConfig};
150    use chess_corners_testutil::aa_chessboard;
151
152    fn synthetic_board(size: usize) -> Vec<u8> {
153        aa_chessboard(size, 12, (0.0, 0.0), 20, 220)
154    }
155
156    #[test]
157    fn chess_response_map_is_plausibly_shaped_for_chess_strategy() {
158        let size = 96usize;
159        let img = synthetic_board(size);
160        let det = Detector::new(DetectorConfig::chess()).unwrap();
161
162        let map = det
163            .diagnostics()
164            .chess_response_u8(&img, size as u32, size as u32)
165            .unwrap();
166
167        assert_eq!(map.width(), size);
168        assert_eq!(map.height(), size);
169        assert_eq!(map.data().len(), size * size);
170        let max = map.data().iter().copied().fold(f32::NEG_INFINITY, f32::max);
171        assert!(
172            max > 0.0,
173            "expected a positive ChESS response somewhere on a synthetic board"
174        );
175    }
176
177    #[test]
178    fn radon_heatmap_is_plausibly_shaped_for_radon_strategy() {
179        let size = 96usize;
180        let img = synthetic_board(size);
181        let det = Detector::new(DetectorConfig::radon()).unwrap();
182
183        let map = det
184            .diagnostics()
185            .radon_heatmap_u8(&img, size as u32, size as u32)
186            .unwrap();
187
188        assert!(map.width() > 0 && map.height() > 0);
189        assert_eq!(map.data().len(), map.width() * map.height());
190        let max = map.data().iter().copied().fold(f32::NEG_INFINITY, f32::max);
191        assert!(
192            max > 0.0,
193            "expected a positive Radon response somewhere on a synthetic board"
194        );
195    }
196
197    #[test]
198    fn chess_response_u8_reports_dimension_mismatch() {
199        let det = Detector::with_default();
200        let img = vec![0u8; 10];
201        let err = det.diagnostics().chess_response_u8(&img, 8, 8).unwrap_err();
202        match err {
203            ChessError::DimensionMismatch { expected, actual } => {
204                assert_eq!(expected, 64);
205                assert_eq!(actual, 10);
206            }
207            other => panic!("expected ChessError::DimensionMismatch, got {other:?}"),
208        }
209    }
210}