Skip to main content

scan_core/
lib.rs

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    /// If using ArUco, list of marker IDs to expect (e.g. TL, TR, BR, BL order)
15    pub marker_ids: Option<Vec<u32>>,
16    /// Minimum number of markers required
17    pub min_markers: usize,
18    /// Max hamming distance for marker detection
19    pub max_hamming: u32,
20    /// If using Invisible mode, validation anchors
21    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    // Typically percentage coordinates for validation
38    pub x: f32,
39    pub y: f32,
40    pub width: f32,
41    pub height: f32,
42    /// Reference image in Base64 for NCC matching
43    pub reference: Option<String>,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct ScanResult {
48    pub success: bool,
49    pub error: Option<String>,
50    /// Base64 encoded PNG of the cropped/rectified exam
51    pub cropped_image: Option<String>, 
52    /// Base64 encoded JPEG of original image with marker outlines and bounding quad
53    pub marked_image: Option<String>,
54    /// Coordinates of the detected corners (TL, TR, BR, BL) in the original image
55    pub corners: Option<Vec<Point>>,
56    /// Alias for corners (used by skeleton-scanner-exams)
57    pub bounding_box: Option<Vec<Point>>,
58    /// Full markers found (ID + corners)
59    pub detected_markers: Option<Vec<crate::aruco::Marker>>,
60    /// Confidence score for invisible mode (0.0 to 1.0)
61    pub match_score: Option<f32>,
62    /// Base64 encoded PNG debugging image (threshold binary)
63    pub debug_image: Option<String>,
64    /// Debugging logs captured during processing
65    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        // Code 0 in definition: [181, 50] -> 10110101 00110010 = 0xB532
137        let code_0_val = 0xB532; 
138        
139        // Exact match
140        let found = dict.find(code_0_val, 0);
141        assert!(found.is_some());
142        assert_eq!(found.unwrap().0, 0); // ID 0
143        
144        // 1 bit error
145        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        // 2 bit error (fail)
151        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}