1use crate::batch::{DetectionBatch, DetectionBatchView};
2use crate::config::DetectorConfig;
3use crate::decoder::{TagDecoder, family_to_decoder};
4use crate::error::DetectorError;
5use crate::image::ImageView;
6use bumpalo::Bump;
7
8pub struct DetectorState {
13 pub arena: Bump,
15 pub batch: DetectionBatch,
17 pub upscale_buf: Vec<u8>,
19}
20
21impl DetectorState {
22 #[must_use]
24 pub fn new() -> Self {
25 Self {
26 arena: Bump::new(),
27 batch: DetectionBatch::new(),
28 upscale_buf: Vec::new(),
29 }
30 }
31
32 pub fn reset(&mut self) {
34 self.arena.reset();
35 }
36}
37
38impl Default for DetectorState {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44pub struct Detector {
48 config: DetectorConfig,
49 decoders: Vec<Box<dyn TagDecoder + Send + Sync>>,
50 state: DetectorState,
51}
52
53impl Default for Detector {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl Detector {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::builder().build()
64 }
65
66 #[must_use]
68 pub fn builder() -> DetectorBuilder {
69 DetectorBuilder::new()
70 }
71
72 #[must_use]
74 pub fn with_config(config: DetectorConfig) -> Self {
75 Self::builder().with_config(config).build()
76 }
77
78 #[must_use]
80 pub fn state(&self) -> &DetectorState {
81 &self.state
82 }
83
84 pub fn set_families(&mut self, families: &[crate::config::TagFamily]) {
86 self.decoders.clear();
87 for &family in families {
88 self.decoders
89 .push(crate::decoder::family_to_decoder(family));
90 }
91 }
92
93 #[allow(clippy::similar_names)]
102 #[allow(clippy::too_many_lines)]
103 pub fn detect(
104 &mut self,
105 img: &ImageView,
106 intrinsics: Option<&crate::pose::CameraIntrinsics>,
107 tag_size: Option<f64>,
108 pose_mode: crate::config::PoseEstimationMode,
109 debug_telemetry: bool,
110 ) -> Result<DetectionBatchView<'_>, DetectorError> {
111 self.state.reset();
112 let state = &mut self.state;
113
114 let (detection_img, _effective_scale, refinement_img) = if self.config.decimation > 1 {
115 let new_w = img.width / self.config.decimation;
116 let new_h = img.height / self.config.decimation;
117 let decimated_data = state.arena.alloc_slice_fill_copy(new_w * new_h, 0u8);
118 let decimated_img = img
119 .decimate_to(self.config.decimation, decimated_data)
120 .map_err(DetectorError::Preprocessing)?;
121 (decimated_img, 1.0 / self.config.decimation as f64, *img)
122 } else if self.config.upscale_factor > 1 {
123 let new_w = img.width * self.config.upscale_factor;
124 let new_h = img.height * self.config.upscale_factor;
125 state.upscale_buf.resize(new_w * new_h, 0);
126
127 let upscaled_img = img
128 .upscale_to(self.config.upscale_factor, &mut state.upscale_buf)
129 .map_err(DetectorError::Preprocessing)?;
130 (
131 upscaled_img,
132 self.config.upscale_factor as f64,
133 upscaled_img,
134 )
135 } else {
136 (*img, 1.0, *img)
137 };
138
139 let img = &detection_img;
140
141 let filtered_img = if self.config.enable_bilateral {
143 let filtered = state
144 .arena
145 .alloc_slice_fill_copy(img.width * img.height, 0u8);
146 crate::filter::bilateral_filter(
147 &state.arena,
148 img,
149 filtered,
150 3, self.config.bilateral_sigma_space,
152 self.config.bilateral_sigma_color,
153 );
154 ImageView::new(filtered, img.width, img.height, img.width)
155 .map_err(DetectorError::InvalidImage)?
156 } else {
157 *img
158 };
159
160 let sharpened_img = if self.config.enable_sharpening {
162 let sharpened = state
163 .arena
164 .alloc_slice_fill_copy(filtered_img.width * filtered_img.height, 0u8);
165 crate::filter::laplacian_sharpen(&filtered_img, sharpened);
166
167 ImageView::new(
168 sharpened,
169 filtered_img.width,
170 filtered_img.height,
171 filtered_img.width,
172 )
173 .map_err(DetectorError::InvalidImage)?
174 } else {
175 filtered_img
176 };
177
178 let binarized = state
179 .arena
180 .alloc_slice_fill_copy(img.width * img.height, 0u8);
181 let threshold_map = state
182 .arena
183 .alloc_slice_fill_copy(img.width * img.height, 0u8);
184
185 let (n, unrefined) = {
187 let engine = crate::threshold::ThresholdEngine::from_config(&self.config);
188 let tile_stats = engine.compute_tile_stats(&state.arena, &sharpened_img);
189 engine.apply_threshold_with_map(
190 &state.arena,
191 &sharpened_img,
192 &tile_stats,
193 binarized,
194 threshold_map,
195 );
196
197 let label_result = crate::simd_ccl_fusion::label_components_lsl(
199 &state.arena,
200 &sharpened_img,
201 threshold_map,
202 self.config.segmentation_connectivity
203 == crate::config::SegmentationConnectivity::Eight,
204 self.config.quad_min_area,
205 );
206
207 let (n, unrefined) = crate::quad::extract_quads_soa(
209 &mut state.batch,
210 &sharpened_img,
211 &label_result,
212 &self.config,
213 self.config.decimation,
214 &refinement_img,
215 debug_telemetry,
216 );
217
218 crate::funnel::apply_funnel_gate(
221 &mut state.batch,
222 n,
223 &sharpened_img,
224 &tile_stats,
225 self.config.threshold_tile_size,
226 self.config.decoder_min_contrast,
227 1.0 / self.config.decimation as f64,
228 );
229
230 (n, unrefined)
231 };
232
233 let mut jitter_ptr = std::ptr::null();
235 let mut num_jitter = 0;
236 if let (true, Some(unrefined_pts)) = (debug_telemetry, unrefined) {
237 num_jitter = unrefined_pts.len();
239 let jitter = state.arena.alloc_slice_fill_copy(num_jitter * 8, 0.0f32);
241 for (i, unrefined_corners) in unrefined_pts.iter().enumerate() {
242 for (j, unrefined_corner) in unrefined_corners.iter().enumerate() {
243 let dx = state.batch.corners[i][j].x - unrefined_corner.x as f32;
244 let dy = state.batch.corners[i][j].y - unrefined_corner.y as f32;
245 jitter[i * 8 + j * 2] = dx;
246 jitter[i * 8 + j * 2 + 1] = dy;
247 }
248 }
249 jitter_ptr = jitter.as_ptr();
250 }
251
252 crate::decoder::compute_homographies_soa(
254 &state.batch.corners[0..n],
255 &state.batch.status_mask[0..n],
256 &mut state.batch.homographies[0..n],
257 );
258
259 let mut gwlf_fallback_count = 0;
261 let mut gwlf_avg_delta = 0.0f32;
262 if self.config.refinement_mode == crate::config::CornerRefinementMode::Gwlf {
263 let mut total_delta = 0.0f32;
264 let mut count = 0;
265 for i in 0..n {
266 let coarse = [
267 [state.batch.corners[i][0].x, state.batch.corners[i][0].y],
268 [state.batch.corners[i][1].x, state.batch.corners[i][1].y],
269 [state.batch.corners[i][2].x, state.batch.corners[i][2].y],
270 [state.batch.corners[i][3].x, state.batch.corners[i][3].y],
271 ];
272 if let Some((refined, covs)) = crate::gwlf::refine_quad_gwlf_with_cov(
273 &refinement_img,
274 &coarse,
275 self.config.gwlf_transversal_alpha,
276 ) {
277 for j in 0..4 {
278 let dx = refined[j][0] - coarse[j][0];
279 let dy = refined[j][1] - coarse[j][1];
280 total_delta += (dx * dx + dy * dy).sqrt();
281 count += 1;
282
283 state.batch.corners[i][j].x = refined[j][0];
284 state.batch.corners[i][j].y = refined[j][1];
285
286 state.batch.corner_covariances[i][j * 4] = covs[j][(0, 0)] as f32;
288 state.batch.corner_covariances[i][j * 4 + 1] = covs[j][(0, 1)] as f32;
289 state.batch.corner_covariances[i][j * 4 + 2] = covs[j][(1, 0)] as f32;
290 state.batch.corner_covariances[i][j * 4 + 3] = covs[j][(1, 1)] as f32;
291 }
292 } else {
293 gwlf_fallback_count += 1;
294 }
295 }
296 if count > 0 {
297 gwlf_avg_delta = total_delta / count as f32;
298 }
299
300 crate::decoder::compute_homographies_soa(
302 &state.batch.corners[0..n],
303 &state.batch.status_mask[0..n],
304 &mut state.batch.homographies[0..n],
305 );
306 }
307
308 crate::decoder::decode_batch_soa(
310 &mut state.batch,
311 n,
312 &refinement_img,
313 &self.decoders,
314 &self.config,
315 );
316
317 let v = state.batch.partition(n);
319
320 let (repro_errors_ptr, num_repro) = run_pose_refinement(
322 &mut state.batch,
323 &state.arena,
324 v,
325 intrinsics,
326 tag_size,
327 &refinement_img,
328 pose_mode,
329 &self.config,
330 debug_telemetry,
331 );
332
333 let telemetry = if debug_telemetry {
337 Some(crate::batch::TelemetryPayload {
338 binarized_ptr: binarized.as_ptr(),
339 threshold_map_ptr: threshold_map.as_ptr(),
340 subpixel_jitter_ptr: jitter_ptr,
341 num_jitter,
342 reprojection_errors_ptr: repro_errors_ptr,
343 num_reprojection: num_repro,
344 gwlf_fallback_count,
345 gwlf_avg_delta,
346 width: img.width,
347 height: img.height,
348 stride: img.width,
349 })
350 } else {
351 None
352 };
353
354 Ok(self.state.batch.view_with_telemetry(v, n, telemetry))
355 }
356
357 #[must_use]
359 pub fn config(&self) -> DetectorConfig {
360 self.config
361 }
362
363 #[cfg(feature = "bench-internals")]
366 #[must_use]
367 pub fn bench_api_get_batch_cloned(&self) -> DetectionBatch {
368 let mut new_batch = DetectionBatch::new();
369 new_batch.corners.copy_from_slice(&self.state.batch.corners);
370 new_batch
371 .homographies
372 .copy_from_slice(&self.state.batch.homographies);
373 new_batch.ids.copy_from_slice(&self.state.batch.ids);
374 new_batch
375 .payloads
376 .copy_from_slice(&self.state.batch.payloads);
377 new_batch
378 .error_rates
379 .copy_from_slice(&self.state.batch.error_rates);
380 new_batch.poses.copy_from_slice(&self.state.batch.poses);
381 new_batch
382 .status_mask
383 .copy_from_slice(&self.state.batch.status_mask);
384 new_batch
385 .funnel_status
386 .copy_from_slice(&self.state.batch.funnel_status);
387 new_batch
388 .corner_covariances
389 .copy_from_slice(&self.state.batch.corner_covariances);
390 new_batch
391 }
392}
393
394#[allow(clippy::too_many_arguments)]
398fn run_pose_refinement(
399 batch: &mut crate::batch::DetectionBatch,
400 arena: &bumpalo::Bump,
401 v: usize,
402 intrinsics: Option<&crate::pose::CameraIntrinsics>,
403 tag_size: Option<f64>,
404 refinement_img: &ImageView,
405 pose_mode: crate::config::PoseEstimationMode,
406 config: &DetectorConfig,
407 debug_telemetry: bool,
408) -> (*const f32, usize) {
409 let mut repro_errors_ptr = std::ptr::null();
410 let mut num_repro = 0;
411
412 if let (Some(intr), Some(size)) = (intrinsics, tag_size) {
413 crate::pose::refine_poses_soa_with_config(
414 batch,
415 v,
416 intr,
417 size,
418 Some(refinement_img),
419 pose_mode,
420 config,
421 );
422
423 if debug_telemetry {
424 num_repro = v;
425 let repro_errors = arena.alloc_slice_fill_copy(num_repro, 0.0f32);
426
427 let model_pts = [
428 nalgebra::Vector3::new(0.0, 0.0, 0.0),
429 nalgebra::Vector3::new(size, 0.0, 0.0),
430 nalgebra::Vector3::new(size, size, 0.0),
431 nalgebra::Vector3::new(0.0, size, 0.0),
432 ];
433
434 for (i, repro_error) in repro_errors.iter_mut().enumerate().take(v) {
435 let det_pose_data = batch.poses[i].data;
436 let det_t = nalgebra::Vector3::new(
437 f64::from(det_pose_data[0]),
438 f64::from(det_pose_data[1]),
439 f64::from(det_pose_data[2]),
440 );
441 let det_q = nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(
442 f64::from(det_pose_data[6]),
443 f64::from(det_pose_data[3]),
444 f64::from(det_pose_data[4]),
445 f64::from(det_pose_data[5]),
446 ));
447
448 let pose = crate::pose::Pose {
449 rotation: *det_q.to_rotation_matrix().matrix(),
450 translation: det_t,
451 };
452
453 let mut sum_sq_err = 0.0;
454 for (j, model_pt) in model_pts.iter().enumerate() {
455 let proj = pose.project(model_pt, intr);
456 let dx = proj[0] - f64::from(batch.corners[i][j].x);
457 let dy = proj[1] - f64::from(batch.corners[i][j].y);
458 sum_sq_err += dx * dx + dy * dy;
459 }
460 *repro_error = (sum_sq_err / 4.0).sqrt() as f32;
461 }
462 repro_errors_ptr = repro_errors.as_ptr();
463 }
464 }
465
466 (repro_errors_ptr, num_repro)
467}
468
469pub struct DetectorBuilder {
471 config: DetectorConfig,
472 families: Vec<crate::config::TagFamily>,
473}
474
475impl DetectorBuilder {
476 #[must_use]
478 pub fn new() -> Self {
479 Self {
480 config: DetectorConfig::default(),
481 families: Vec::new(),
482 }
483 }
484
485 #[must_use]
487 pub fn with_config(mut self, config: DetectorConfig) -> Self {
488 self.config = config;
489 self
490 }
491
492 #[must_use]
494 pub fn with_decimation(mut self, decimation: usize) -> Self {
495 self.config.decimation = decimation;
496 self
497 }
498
499 #[must_use]
501 pub fn with_family(mut self, family: crate::config::TagFamily) -> Self {
502 if !self.families.contains(&family) {
503 self.families.push(family);
504 }
505 self
506 }
507
508 #[must_use]
510 pub fn with_threads(mut self, threads: usize) -> Self {
511 self.config.nthreads = threads;
512 self
513 }
514
515 #[must_use]
517 pub fn with_upscale_factor(mut self, factor: usize) -> Self {
518 self.config.upscale_factor = factor;
519 self
520 }
521
522 #[must_use]
524 pub fn with_corner_refinement(mut self, mode: crate::config::CornerRefinementMode) -> Self {
525 self.config.refinement_mode = mode;
526 self
527 }
528
529 #[must_use]
531 pub fn with_decode_mode(mut self, mode: crate::config::DecodeMode) -> Self {
532 self.config.decode_mode = mode;
533 self
534 }
535
536 #[must_use]
538 pub fn with_connectivity(
539 mut self,
540 connectivity: crate::config::SegmentationConnectivity,
541 ) -> Self {
542 self.config.segmentation_connectivity = connectivity;
543 self
544 }
545
546 #[must_use]
548 pub fn with_threshold_tile_size(mut self, size: usize) -> Self {
549 self.config.threshold_tile_size = size;
550 self
551 }
552
553 #[must_use]
555 pub fn with_threshold_min_range(mut self, range: u8) -> Self {
556 self.config.threshold_min_range = range;
557 self
558 }
559
560 #[must_use]
562 pub fn with_adaptive_threshold_constant(mut self, c: i16) -> Self {
563 self.config.adaptive_threshold_constant = c;
564 self
565 }
566
567 #[must_use]
569 pub fn with_quad_min_area(mut self, area: u32) -> Self {
570 self.config.quad_min_area = area;
571 self
572 }
573
574 #[must_use]
576 pub fn with_quad_min_fill_ratio(mut self, ratio: f32) -> Self {
577 self.config.quad_min_fill_ratio = ratio;
578 self
579 }
580
581 #[must_use]
583 pub fn with_quad_min_edge_score(mut self, score: f64) -> Self {
584 self.config.quad_min_edge_score = score;
585 self
586 }
587
588 #[must_use]
590 pub fn with_max_hamming_error(mut self, errors: u32) -> Self {
591 self.config.max_hamming_error = errors;
592 self
593 }
594
595 #[must_use]
597 pub fn with_decoder_min_contrast(mut self, contrast: f64) -> Self {
598 self.config.decoder_min_contrast = contrast;
599 self
600 }
601
602 #[must_use]
604 pub fn with_gwlf_transversal_alpha(mut self, alpha: f64) -> Self {
605 self.config.gwlf_transversal_alpha = alpha;
606 self
607 }
608
609 #[must_use]
611 pub fn with_quad_max_elongation(mut self, elongation: f64) -> Self {
612 self.config.quad_max_elongation = elongation;
613 self
614 }
615
616 #[must_use]
618 pub fn with_quad_min_density(mut self, density: f64) -> Self {
619 self.config.quad_min_density = density;
620 self
621 }
622
623 #[must_use]
625 pub fn with_quad_extraction_mode(mut self, mode: crate::config::QuadExtractionMode) -> Self {
626 self.config.quad_extraction_mode = mode;
627 self
628 }
629
630 #[must_use]
632 pub fn with_sharpening(mut self, enable: bool) -> Self {
633 self.config.enable_sharpening = enable;
634 self
635 }
636
637 #[must_use]
639 pub fn build(self) -> Detector {
640 let mut decoders = Vec::new();
641 let families = if self.families.is_empty() {
642 vec![crate::config::TagFamily::AprilTag36h11]
643 } else {
644 self.families
645 };
646 for family in families {
647 decoders.push(family_to_decoder(family));
648 }
649
650 Detector {
651 config: self.config,
652 decoders,
653 state: DetectorState::new(),
654 }
655 }
656}
657
658impl Default for DetectorBuilder {
659 fn default() -> Self {
660 Self::new()
661 }
662}