Skip to main content

scan_core/aruco/
dictionary.rs

1use std::collections::HashMap;
2
3pub struct Dictionary {
4    pub n_bits: usize,
5    pub tau: u32,
6    pub codes: HashMap<u64, usize>, // code -> id
7    pub code_list: Vec<u64>,
8}
9
10impl Dictionary {
11    pub fn new(name: &str) -> Option<Self> {
12        match name {
13            "ARUCO_4X4_50" => Some(Self::aruco_4x4_50()),
14            "ARUCO_5X5" => Some(Self::aruco_5x5()),
15            "ARUCO_6X6" => Some(Self::aruco_6x6()),
16            _ => None,
17        }
18    }
19
20    fn from_codes(n_bits: usize, tau_override: Option<u32>, codes_slice: &[u64]) -> Self {
21        let mut code_list = Vec::with_capacity(codes_slice.len());
22        let mut codes = HashMap::new();
23
24        for (i, &val) in codes_slice.iter().enumerate() {
25            code_list.push(val);
26            codes.insert(val, i);
27        }
28
29        let tau = if let Some(t) = tau_override {
30            t
31        } else {
32            // Calculate tau (min distance between any two codes)
33            let mut t = u32::MAX;
34            // Scan subset if too large? 5x5 has 1024 codes -> 1M comparisons. 
35            // In JS they do full scan. 1M ops in Rust is fast.
36            for i in 0..code_list.len() {
37                for j in (i + 1)..code_list.len() {
38                    let d = (code_list[i] ^ code_list[j]).count_ones();
39                    if d < t { t = d; }
40                }
41            }
42            t
43        };
44
45        Self { n_bits, tau, codes, code_list }
46    }
47
48    fn aruco_4x4_50() -> Self {
49        let n_bits = 16;
50        // manually converted to u64 for reuse with from_codes if needed, 
51        // but keeping original byte definition for now to avoid huge diff, 
52        // just converting it before call
53        let raw_codes: Vec<&[u8]> = vec![
54            &[181,50],&[15,154],&[51,45],&[153,70],&[84,158],&[121,205],&[158,46],&[196,242],&[254,218],&[207,86],
55            &[249,145],&[17,167],&[14,183],&[42,15],&[36,177],&[38,62],&[70,101],&[102,0],&[108,94],&[118,175],
56            &[134,139],&[176,43],&[204,213],&[221,130],&[254,71],&[148,113],&[172,228],&[165,84],&[33,35],&[52,111],
57            &[68,21],&[87,178],&[158,207],&[240,203],&[8,174],&[9,41],&[24,117],&[4,255],&[13,246],&[28,90],
58            &[23,24],&[42,40],&[50,140],&[56,178],&[36,232],&[46,235],&[45,63],&[75,100],&[80,46],&[80,19]
59        ];
60
61        let mut u64_codes = Vec::new();
62        for bytes in raw_codes {
63             u64_codes.push(Self::bytes_to_u64(bytes));
64        }
65        
66        Self::from_codes(n_bits, None, &u64_codes)
67    }
68
69    fn aruco_5x5() -> Self {
70        // ARUCO dictionary (5x5, 25 bits)
71        // Tau is 3 explicitly in JS definition
72        use crate::aruco::dict_data::ARUCO_5X5_CODES;
73        Self::from_codes(25, Some(3), ARUCO_5X5_CODES)
74    }
75
76    fn aruco_6x6() -> Self {
77        // ARUCO_MIP_36h12 (6x6, 36 bits)
78        // Tau is 12 explicitly in JS definition
79        use crate::aruco::dict_data::ARUCO_6X6_CODES;
80        Self::from_codes(36, Some(12), ARUCO_6X6_CODES)
81    }
82
83    fn bytes_to_u64(bytes: &[u8]) -> u64 {
84        let mut val = 0u64;
85        for &b in bytes {
86            val = (val << 8) | (b as u64);
87        }
88        val
89    }
90
91    pub fn find(&self, bits: u64, max_hamming: u32) -> Option<(usize, u32)> {
92        // Direct lookup
93        if let Some(&id) = self.codes.get(&bits) {
94            return Some((id, 0));
95        }
96
97        // Find nearest within max_hamming
98        let mut min_dist = u32::MAX;
99        let mut min_id = 0;
100
101        for (id, &code) in self.code_list.iter().enumerate() {
102            let dist = (bits ^ code).count_ones();
103            if dist < min_dist {
104                min_dist = dist;
105                min_id = id;
106            }
107        }
108
109        if min_dist <= max_hamming {
110            Some((min_id, min_dist))
111        } else {
112            None
113        }
114    }
115
116    /// Recognize a marker from a warped+thresholded image.
117    /// Port of AR.Detector.prototype.getMarker from JS.
118    ///
119    /// Key differences from previous Rust version:
120    /// 1. Validates that border cells are BLACK (non-zero count < minZero)
121    /// 2. Reads bits using countNonZero over the full cell area (not 3x3 sample)
122    /// 3. Tries all 4 rotations to find best match
123    pub fn recognize_marker(&self, img: &image::GrayImage, _mark_size: usize, max_hamming: u32, corners: &[crate::Point], mut debug_log: Option<&mut Vec<String>>) -> Option<crate::aruco::Marker> {
124        let grid_size = (self.n_bits as f64).sqrt() as usize + 2; // e.g. 6 for 4x4
125        let cell_w_f = (img.width() as f32) / (grid_size as f32);
126        let cell_h_f = (img.height() as f32) / (grid_size as f32);
127        let data = img.as_raw();
128        let img_w = img.width() as usize;
129
130        // --- Step 1: Validate border cells are black ---
131        for i in 0..grid_size {
132            let inc = if i == 0 || i == grid_size - 1 { 1 } else { grid_size - 1 };
133
134            let mut j = 0;
135            while j < grid_size {
136                let x1 = (j as f32 * cell_w_f + cell_w_f * 0.2) as usize;
137                let y1 = (i as f32 * cell_h_f + cell_h_f * 0.2) as usize;
138                let x2 = ((j as f32 + 1.0) * cell_w_f - cell_w_f * 0.2) as usize;
139                let y2 = ((i as f32 + 1.0) * cell_h_f - cell_h_f * 0.2) as usize;
140                
141                let w = (x2 - x1).max(1);
142                let h = (y2 - y1).max(1);
143                let min_zero = (w * h) >> 1; // half the sampled area
144
145                let nz = crate::cv::geometry::count_non_zero(data, img_w, x1, y1, w, h);
146                if nz > min_zero {
147                    if let Some(ref mut logs) = debug_log {
148                         logs.push(format!("DEBUG: Border check failed at {},{}. NZ={} > {}", j, i, nz, min_zero));
149                    }
150                    return None; // Border cell is not black
151                }
152                j += inc;
153            }
154        }
155
156        // --- Step 2: Read inner bits using centered sampling ---
157        let inner_size = grid_size - 2;
158        let mut bits: Vec<Vec<u8>> = Vec::new();
159
160        for i in 0..inner_size {
161            let mut row = Vec::new();
162            for j in 0..inner_size {
163                // Use inner cells (skip first border)
164                let x1 = ((j + 1) as f32 * cell_w_f + cell_w_f * 0.2) as usize;
165                let y1 = ((i + 1) as f32 * cell_h_f + cell_h_f * 0.2) as usize;
166                let x2 = ((j + 2) as f32 * cell_w_f - cell_w_f * 0.2) as usize;
167                let y2 = ((i + 2) as f32 * cell_h_f - cell_h_f * 0.2) as usize;
168                
169                let w = (x2 - x1).max(1);
170                let h = (y2 - y1).max(1);
171                let min_zero = (w * h) >> 1;
172
173                let nz = crate::cv::geometry::count_non_zero(data, img_w, x1, y1, w, h);
174                row.push(if nz > min_zero { 1 } else { 0 });
175            }
176            bits.push(row);
177        }
178
179        // --- Step 3: Try all 4 rotations for best match ---
180        let mut current_bits = bits;
181        let mut best_found: Option<(usize, u32)> = None;
182        let mut best_rot = 0;
183        let mut debug_codes_tried = Vec::new();
184
185        for rot in 0..4 {
186            // Flatten to u64
187            let mut val = 0u64;
188            for row in &current_bits {
189                for &b in row {
190                    val = (val << 1) | (b as u64);
191                }
192            }
193            
194            if debug_log.is_some() { debug_codes_tried.push(format!("0x{:x}", val)); }
195
196            if let Some((id, dist)) = self.find(val, max_hamming) {
197                if best_found.is_none() || dist < best_found.unwrap().1 {
198                    best_found = Some((id, dist));
199                    best_rot = rot;
200                    if dist == 0 { break; } // Perfect match
201                }
202            }
203
204            // Rotate 90° CW for next iteration
205            current_bits = rotate_grid_2d(&current_bits);
206        }
207
208        if let Some((id, dist)) = best_found {
209            let shift = (4 - best_rot) % 4;
210            let mut new_corners = vec![crate::Point { x: 0.0, y: 0.0 }; 4];
211            for i in 0..4 {
212                new_corners[i] = corners[(i + shift) % 4];
213            }
214            Some(crate::aruco::Marker::new(id as u32, new_corners, dist))
215        } else {
216            if let Some(ref mut logs) = debug_log {
217                logs.push(format!("DEBUG: Recognition failed. Tried codes: {:?}. MaxHamming={}", debug_codes_tried, max_hamming));
218            }
219            None
220        }
221    }
222
223    /// Genera una imagen (GrayImage) de un marcador ArUco dado su ID.
224    /// La imagen tendrá un borde negro de 1 celda y el patrón NxN interno.
225    pub fn generate_marker_image(&self, id: usize, cell_size: usize) -> Option<image::GrayImage> {
226        if let Some(&code) = self.code_list.get(id) {
227            let n = (self.n_bits as f64).sqrt() as usize;
228            let grid_size = n + 2;
229            let img_size = grid_size * cell_size;
230            
231            // GrayImage::new inicializa con 0 (negro), lo cual nos sirve para el borde.
232            let mut img = image::GrayImage::new(img_size as u32, img_size as u32);
233            
234            // Dibujar bits internos blancos
235            for row in 0..n {
236                for col in 0..n {
237                    let bit_idx = (n * n) - 1 - (row * n + col);
238                    let bit = (code >> bit_idx) & 1;
239                    if bit == 1 {
240                        // Celda blanca
241                        for py in 0..cell_size {
242                            for px in 0..cell_size {
243                                img.put_pixel(
244                                    ((col + 1) * cell_size + px) as u32,
245                                    ((row + 1) * cell_size + py) as u32,
246                                    image::Luma([255])
247                                );
248                            }
249                        }
250                    }
251                }
252            }
253            Some(img)
254        } else {
255            None
256        }
257    }
258
259    /// Genera una cadena con el contenido de un archivo SVG para el marcador dado su ID.
260    /// El SVG es vectorial y escala perfectamente sin pérdida de calidad.
261    pub fn generate_marker_svg(&self, id: usize) -> Option<String> {
262        if let Some(&code) = self.code_list.get(id) {
263            let n = (self.n_bits as f64).sqrt() as usize;
264            let grid_size = n + 2;
265            
266            let mut svg = format!(
267                r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>
268<svg width="{0}" height="{0}" viewBox="0 0 {0} {0}" xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
269  <rect x="0" y="0" width="{0}" height="{0}" fill="black" />"#,
270                grid_size
271            );
272
273            // Dibujar bits internos blancos
274            for row in 0..n {
275                for col in 0..n {
276                    let bit_idx = (n * n) - 1 - (row * n + col);
277                    let bit = (code >> bit_idx) & 1;
278                    if bit == 1 {
279                        svg.push_str(&format!(
280                            r#"
281  <rect x="{}" y="{}" width="1" height="1" fill="white" />"#,
282                            col + 1,
283                            row + 1
284                        ));
285                    }
286                }
287            }
288            
289            svg.push_str("\n</svg>");
290            Some(svg)
291        } else {
292            None
293        }
294    }
295}
296
297/// Rotate a 2D grid 90° clockwise.
298/// Port of AR.Detector.prototype.rotate from JS.
299fn rotate_grid_2d(src: &[Vec<u8>]) -> Vec<Vec<u8>> {
300    let len = src.len();
301    let mut dst = vec![vec![0u8; len]; len];
302    for i in 0..len {
303        for j in 0..src[i].len() {
304            dst[i][j] = src[src[i].len() - j - 1][i];
305        }
306    }
307    dst
308}