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 #[serde(default = "default_template_ratio")]
26 pub template_ratio: f32,
27}
28
29fn default_template_ratio() -> f32 {
30 3400.0 / 4400.0
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum ScanMode {
36 #[default]
37 Aruco4x4,
38 Aruco5x5,
39 Aruco6x6,
40 Invisible,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AnchorConfig {
45 pub name: String,
46 pub x: f32,
48 pub y: f32,
49 pub width: f32,
50 pub height: f32,
51 pub reference: Option<String>,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct ScanResult {
57 pub success: bool,
58 pub error: Option<String>,
59 pub cropped_image: Option<String>,
61 pub marked_image: Option<String>,
63 pub corners: Option<Vec<Point>>,
65 pub bounding_box: Option<Vec<Point>>,
67 pub detected_markers: Option<Vec<crate::aruco::Marker>>,
69 pub match_score: Option<f32>,
71 pub debug_image: Option<String>,
73 pub logs: Vec<String>,
75}
76
77impl ScanResult {
78 pub fn new() -> Self {
79 Self {
80 success: false,
81 error: None,
82 cropped_image: None,
83 marked_image: None,
84 corners: None,
85 bounding_box: None,
86 detected_markers: Some(Vec::new()),
87 match_score: None,
88 debug_image: None,
89 logs: Vec::new(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
95pub struct Point {
96 pub x: f32,
97 pub y: f32,
98}
99
100impl Default for ScanConfig {
101 fn default() -> Self {
102 Self {
103 mode: ScanMode::Aruco4x4,
104 debug: false,
105 marker_ids: None,
106 min_markers: 2,
107 max_hamming: 1,
108 anchors: None,
109 template_ratio: default_template_ratio(),
110 }
111 }
112}
113
114pub fn scan(image: &image::GrayImage, config: &ScanConfig) -> anyhow::Result<ScanResult> {
115 use detector::Detector;
116
117 match config.mode {
118 ScanMode::Invisible => {
119 let detector = invisible::detector::InvisibleDetector::new(config.clone());
120 detector.detect(image)
121 },
122 _ => {
123 let detector = aruco::ArucoDetector::new(config.clone());
124 detector.detect(image)
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::aruco::Dictionary;
133
134 #[test]
135 fn test_dictionary_load() {
136 let dict = Dictionary::new("ARUCO_4X4_50");
137 assert!(dict.is_some(), "Should load ARUCO_4X4_50");
138
139 let dict = dict.unwrap();
140 assert_eq!(dict.n_bits, 16);
141 }
142
143 #[test]
144 fn test_hamming_distance() {
145 let dict = Dictionary::new("ARUCO_4X4_50").unwrap();
146 let code_0_val = 0xB532;
148
149 let found = dict.find(code_0_val, 0);
151 assert!(found.is_some());
152 assert_eq!(found.unwrap().0, 0); let code_error = code_0_val ^ 1;
156 let found_err = dict.find(code_error, 1);
157 assert!(found_err.is_some());
158 assert_eq!(found_err.unwrap().0, 0);
159
160 let code_error_2 = code_0_val ^ 3;
162 let found_err_2 = dict.find(code_error_2, 1);
163 assert!(found_err_2.is_none());
164 }
165
166 #[test]
167 fn test_perspective_math() {
168 use crate::transform::perspective::PerspectiveTransform;
169 use crate::Point;
170
171 let src = vec![
172 Point{x:0.0, y:0.0}, Point{x:10.0, y:0.0},
173 Point{x:10.0, y:10.0}, Point{x:0.0, y:10.0}
174 ];
175 let dst = src.clone();
176
177 let pt = PerspectiveTransform::new(&src, &dst).expect("Valid transform");
178
179 let p = pt.transform_inverse(5.0, 5.0);
180 assert!((p.x - 5.0).abs() < 0.001);
181 assert!((p.y - 5.0).abs() < 0.001);
182 }
183
184 #[test]
185 fn test_perspective_16_points() {
186 use crate::transform::perspective::PerspectiveTransform;
187 use crate::Point;
188
189 let mut src = Vec::new();
191 let mut dst = Vec::new();
192
193 let marker_pos = [(0.0, 0.0), (90.0, 0.0), (90.0, 90.0), (0.0, 90.0)];
195 for (mx, my) in marker_pos {
196 for (dx, dy) in [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] {
197 dst.push(Point { x: mx + dx, y: my + dy });
198 src.push(Point { x: mx + dx + 0.1, y: my + dy - 0.1 });
200 }
201 }
202
203 let pt = PerspectiveTransform::new(&src, &dst).expect("Should solve 16 points");
204
205 let p = pt.transform(50.0, 50.0);
207 println!("Projected 50,50 -> {:.4}, {:.4}", p.x, p.y);
208 assert!((p.x - 50.1).abs() < 0.5);
209 assert!((p.y - 49.9).abs() < 0.5);
210 }
211}