1use box_image_pyramid::PyramidBuffers;
29use chess_corners_core::{ChessBuffers, RadonBuffers};
30
31#[cfg(feature = "ml-refiner")]
32use crate::ml_refiner;
33use crate::multiscale;
34use crate::upscale::{self, UpscaleBuffers};
35use crate::{ChessError, CornerDescriptor, DetectorConfig};
36use chess_corners_core::ImageView;
37
38pub struct Detector {
43 cfg: DetectorConfig,
44 pyramid: PyramidBuffers,
45 chess_buffers: ChessBuffers,
46 radon_buffers: RadonBuffers,
47 upscale: UpscaleBuffers,
48 #[cfg(feature = "ml-refiner")]
49 ml_state: Option<ml_refiner::MlRefinerState>,
50 #[cfg(feature = "ml-refiner")]
51 ml_params: ml_refiner::MlRefinerParams,
52}
53
54impl Detector {
55 pub fn new(cfg: DetectorConfig) -> Result<Self, ChessError> {
62 cfg.upscale.validate()?;
63 Ok(Self {
64 cfg,
65 pyramid: PyramidBuffers::default(),
66 chess_buffers: ChessBuffers::default(),
67 radon_buffers: RadonBuffers::default(),
68 upscale: UpscaleBuffers::new(),
69 #[cfg(feature = "ml-refiner")]
70 ml_state: None,
71 #[cfg(feature = "ml-refiner")]
72 ml_params: ml_refiner::MlRefinerParams::default(),
73 })
74 }
75
76 pub fn with_default() -> Self {
78 Self::new(DetectorConfig::default()).expect("default DetectorConfig is always valid")
81 }
82
83 pub fn config(&self) -> &DetectorConfig {
85 &self.cfg
86 }
87
88 pub fn set_config(&mut self, cfg: DetectorConfig) -> Result<(), ChessError> {
95 cfg.upscale.validate()?;
96 self.cfg = cfg;
97 #[cfg(feature = "ml-refiner")]
100 {
101 self.ml_state = None;
102 }
103 Ok(())
104 }
105
106 pub fn detect_u8(
116 &mut self,
117 img: &[u8],
118 width: u32,
119 height: u32,
120 ) -> Result<Vec<CornerDescriptor>, ChessError> {
121 let src_w = width as usize;
122 let src_h = height as usize;
123 let expected = src_w * src_h;
124 if img.len() != expected {
125 return Err(ChessError::DimensionMismatch {
126 expected,
127 actual: img.len(),
128 });
129 }
130
131 let factor = self.cfg.upscale.effective_factor();
132 if factor <= 1 {
133 let view =
134 ImageView::from_u8_slice(src_w, src_h, img).expect("dimensions were checked above");
135 return Ok(Self::detect_view_inner(
136 &self.cfg,
137 &mut self.pyramid,
138 &mut self.chess_buffers,
139 &mut self.radon_buffers,
140 #[cfg(feature = "ml-refiner")]
141 &mut self.ml_state,
142 #[cfg(feature = "ml-refiner")]
143 &self.ml_params,
144 view,
145 ));
146 }
147
148 let upscaled = upscale::upscale_bilinear_u8(img, src_w, src_h, factor, &mut self.upscale)?;
153 let mut corners = Self::detect_view_inner(
154 &self.cfg,
155 &mut self.pyramid,
156 &mut self.chess_buffers,
157 &mut self.radon_buffers,
158 #[cfg(feature = "ml-refiner")]
159 &mut self.ml_state,
160 #[cfg(feature = "ml-refiner")]
161 &self.ml_params,
162 upscaled,
163 );
164 upscale::rescale_descriptors_to_input(&mut corners, factor);
165 Ok(corners)
166 }
167
168 #[cfg(feature = "image")]
175 pub fn detect(&mut self, img: &image::GrayImage) -> Result<Vec<CornerDescriptor>, ChessError> {
176 self.detect_u8(img.as_raw(), img.width(), img.height())
177 }
178
179 pub fn diagnostics(&self) -> crate::diagnostics::DetectorDiagnostics<'_> {
195 crate::diagnostics::DetectorDiagnostics::new(self)
196 }
197
198 fn detect_view_inner(
199 cfg: &DetectorConfig,
200 pyramid: &mut PyramidBuffers,
201 chess_buffers: &mut ChessBuffers,
202 radon_buffers: &mut RadonBuffers,
203 #[cfg(feature = "ml-refiner")] ml_state: &mut Option<ml_refiner::MlRefinerState>,
204 #[cfg(feature = "ml-refiner")] ml_params: &ml_refiner::MlRefinerParams,
205 view: ImageView<'_>,
206 ) -> Vec<CornerDescriptor> {
207 #[cfg(feature = "ml-refiner")]
208 if Self::is_ml_refiner(cfg) {
209 if ml_state.is_none() {
210 let fallback = chess_corners_core::RefinerKind::CenterOfMass(
211 chess_corners_core::CenterOfMassConfig::default(),
212 );
213 *ml_state = Some(ml_refiner::MlRefinerState::new(ml_params, &fallback));
214 }
215 let state = ml_state.as_mut().expect("ml_state initialised above");
216 return multiscale::detect_with_ml(
217 view,
218 cfg,
219 pyramid,
220 chess_buffers,
221 radon_buffers,
222 ml_params,
223 state,
224 );
225 }
226
227 multiscale::detect_with_buffers(view, cfg, pyramid, chess_buffers, radon_buffers)
228 }
229
230 #[cfg(feature = "ml-refiner")]
234 #[inline]
235 fn is_ml_refiner(cfg: &DetectorConfig) -> bool {
236 matches!(
237 &cfg.strategy,
238 crate::DetectionStrategy::Chess(c) if matches!(c.refiner, crate::ChessRefiner::Ml)
239 )
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::UpscaleConfig;
247 use chess_corners_testutil::aa_chessboard;
248
249 fn synthetic_board(size: usize) -> Vec<u8> {
250 aa_chessboard(size, 12, (0.0, 0.0), 20, 220)
251 }
252
253 #[test]
254 fn detect_u8_reports_dimension_mismatch() {
255 let mut det = Detector::with_default();
256 let img = vec![0u8; 10];
257 let err = det.detect_u8(&img, 8, 8).unwrap_err();
258 match err {
259 ChessError::DimensionMismatch { expected, actual } => {
260 assert_eq!(expected, 64);
261 assert_eq!(actual, 10);
262 }
263 other => panic!("expected ChessError::DimensionMismatch, got {other:?}"),
264 }
265 }
266
267 #[test]
268 fn set_config_valid_swap_changes_detection_outcome() {
269 let size = 96usize;
270 let img = synthetic_board(size);
271
272 let mut det = Detector::new(DetectorConfig::chess().with_threshold(30.0)).unwrap();
273 let low_threshold = det.detect_u8(&img, size as u32, size as u32).unwrap();
274 assert!(
275 !low_threshold.is_empty(),
276 "expected corners at the default threshold"
277 );
278
279 det.set_config(DetectorConfig::chess().with_threshold(5000.0))
280 .expect("valid upscale config");
281 let high_threshold = det.detect_u8(&img, size as u32, size as u32).unwrap();
282 assert!(
283 high_threshold.len() < low_threshold.len(),
284 "raising the response threshold far above real corner strengths \
285 must suppress detections: low={} high={}",
286 low_threshold.len(),
287 high_threshold.len()
288 );
289 }
290
291 #[test]
292 fn set_config_rejects_invalid_upscale_and_leaves_detector_usable() {
293 let mut det = Detector::new(DetectorConfig::chess()).unwrap();
294 let original_upscale = det.config().upscale;
295
296 let bad = DetectorConfig::chess().with_upscale(UpscaleConfig::fixed(5));
297 let err = det.set_config(bad).unwrap_err();
298 assert!(matches!(err, ChessError::Upscale(_)));
299
300 assert_eq!(det.config().upscale, original_upscale);
302
303 let img = synthetic_board(64);
305 let corners = det.detect_u8(&img, 64, 64).unwrap();
306 assert!(!corners.is_empty());
307 }
308}