1pub mod aruco;
2pub mod cv;
3pub mod detector;
4pub mod invisible;
5pub mod transform;
6pub mod image_utils;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ScanConfig {
12 pub mode: ScanMode,
13 pub debug: bool,
14 pub marker_ids: Option<Vec<u32>>,
16 pub min_markers: usize,
18 pub max_hamming: u32,
20 pub anchors: Option<Vec<AnchorConfig>>,
22}
23
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
25#[serde(rename_all = "snake_case")]
26pub enum ScanMode {
27 #[default]
28 Aruco4x4,
29 Aruco5x5,
30 Aruco6x6,
31 Invisible,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AnchorConfig {
36 pub name: String,
37 pub x: f32,
39 pub y: f32,
40 pub width: f32,
41 pub height: f32,
42 pub reference: Option<String>,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct ScanResult {
48 pub success: bool,
49 pub error: Option<String>,
50 pub cropped_image: Option<String>,
52 pub marked_image: Option<String>,
54 pub corners: Option<Vec<Point>>,
56 pub bounding_box: Option<Vec<Point>>,
58 pub detected_markers: Option<Vec<crate::aruco::Marker>>,
60 pub match_score: Option<f32>,
62 pub debug_image: Option<String>,
64 pub logs: Vec<String>,
66}
67
68impl ScanResult {
69 pub fn new() -> Self {
70 Self {
71 success: false,
72 error: None,
73 cropped_image: None,
74 marked_image: None,
75 corners: None,
76 bounding_box: None,
77 detected_markers: Some(Vec::new()),
78 match_score: None,
79 debug_image: None,
80 logs: Vec::new(),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
86pub struct Point {
87 pub x: f32,
88 pub y: f32,
89}
90
91impl Default for ScanConfig {
92 fn default() -> Self {
93 Self {
94 mode: ScanMode::Aruco4x4,
95 debug: false,
96 marker_ids: None,
97 min_markers: 2,
98 max_hamming: 1,
99 anchors: None,
100 }
101 }
102}
103
104pub fn scan(image: &image::GrayImage, config: &ScanConfig) -> anyhow::Result<ScanResult> {
105 use detector::Detector;
106
107 match config.mode {
108 ScanMode::Invisible => {
109 let detector = invisible::detector::InvisibleDetector::new(config.clone());
110 detector.detect(image)
111 },
112 _ => {
113 let detector = aruco::ArucoDetector::new(config.clone());
114 detector.detect(image)
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::aruco::Dictionary;
123
124 #[test]
125 fn test_dictionary_load() {
126 let dict = Dictionary::new("ARUCO_4X4_50");
127 assert!(dict.is_some(), "Should load ARUCO_4X4_50");
128
129 let dict = dict.unwrap();
130 assert_eq!(dict.n_bits, 16);
131 }
132
133 #[test]
134 fn test_hamming_distance() {
135 let dict = Dictionary::new("ARUCO_4X4_50").unwrap();
136 let code_0_val = 0xB532;
138
139 let found = dict.find(code_0_val, 0);
141 assert!(found.is_some());
142 assert_eq!(found.unwrap().0, 0); let code_error = code_0_val ^ 1;
146 let found_err = dict.find(code_error, 1);
147 assert!(found_err.is_some());
148 assert_eq!(found_err.unwrap().0, 0);
149
150 let code_error_2 = code_0_val ^ 3;
152 let found_err_2 = dict.find(code_error_2, 1);
153 assert!(found_err_2.is_none());
154 }
155
156 #[test]
157 fn test_perspective_math() {
158 use crate::transform::perspective::PerspectiveTransform;
159 use crate::Point;
160
161 let src = vec![
162 Point{x:0.0, y:0.0}, Point{x:10.0, y:0.0},
163 Point{x:10.0, y:10.0}, Point{x:0.0, y:10.0}
164 ];
165 let dst = src.clone();
166
167 let pt = PerspectiveTransform::new(&src, &dst).expect("Valid transform");
168
169 let p = pt.transform_inverse(5.0, 5.0);
170 assert!((p.x - 5.0).abs() < 0.001);
171 assert!((p.y - 5.0).abs() < 0.001);
172 }
173}