Skip to main content

webarkitlib_rs/
matrix.rs

1/*
2 *  matrix.rs
3 *  WebARKitLib-rs
4 *
5 *  This file is part of WebARKitLib-rs - WebARKit.
6 *
7 *  WebARKitLib-rs is free software: you can redistribute it and/or modify
8 *  it under the terms of the GNU Lesser General Public License as published by
9 *  the Free Software Foundation, either version 3 of the License, or
10 *  (at your option) any later version.
11 *
12 *  WebARKitLib-rs is distributed in the hope that it will be useful,
13 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 *  GNU Lesser General Public License for more details.
16 *
17 *  You should have received a copy of the GNU Lesser General Public License
18 *  along with WebARKitLib-rs.  If not, see <http://www.gnu.org/licenses/>.
19 *
20 *  Copyright 2026 WebARKit.
21 *
22 *  Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
23 *
24 */
25
26//! Matrix Code (Barcode) Marker Decoding
27//! Ported from arGetMatrixCode.c and associated ECC logic.
28
29use crate::types::{ARMatrixCodeType, ARdouble, ARHandle, ARMarkerInfo, ARMarkerInfo2};
30use crate::marker::ar_get_line;
31use log::{debug, trace};
32
33/// Results of a matrix code decoding attempt.
34#[derive(Debug, Clone, PartialEq)]
35pub struct MatrixCodeResult {
36    pub id: i32,
37    pub dir: i32,
38    pub cf: f64,
39    pub error_corrected: i32,
40}
41
42/// Decodes a matrix (barcode) marker code from the raw frame.
43///
44/// This is the main entry point for barcode marker decoding. Given the four
45/// corner vertices of a detected square candidate, it:
46/// 1. Calls `sample_grid` to project `grid_size × grid_size` cells from the
47///    image onto a regular grid using a homography (`get_cpara`).
48/// 2. Computes the adaptive binarisation threshold from the sampled min/max
49///    intensity. Returns `Err("Low contrast…")` if the range < 30.
50/// 3. Scans the four corners of the inner `dim × dim` core to detect the
51///    L-shaped locator pattern (two adjacent `1`-bits and one `0`-bit), which
52///    determines the orientation (`matched_dir ∈ 0..3`).
53/// 4. Reads the data bits from the core in the correct rotation order and
54///    passes them to `decode_matrix_raw`.
55///
56/// # Parameters
57/// - `vertex` — four `[x, y]` corner coordinates in ideal (undistorted) space,
58///   as produced by [`crate::marker::ar_get_line`].
59/// - `code_type` — selects the square dimension (`3..=6`) and ECC scheme.
60/// - `id` / `dir` / `cf` / `error_corrected` — output values.
61///
62/// # Returns
63/// `Ok(())` on successful decode. `Err` on low contrast, missing locator pattern,
64/// or unsupported `code_type`.
65///
66/// # Example
67/// ```rust,no_run
68/// use webarkitlib_rs::matrix::ar_matrix_code_get_id;
69/// use webarkitlib_rs::types::ARMatrixCodeType;
70///
71/// let image = vec![0u8; 640 * 480 * 3];
72/// let vertex = [[100.0, 100.0], [200.0, 100.0], [200.0, 200.0], [100.0, 200.0]];
73/// let mut id = -1i32;
74/// let mut dir = -1i32;
75/// let mut cf = 0.0f64;
76/// let mut err = 0i32;
77/// if ar_matrix_code_get_id(&image, 640, 480, &vertex, ARMatrixCodeType::default(),
78///         webarkitlib_rs::types::ARPixelFormat::RGB, 0.5,
79///         &mut id, &mut dir, &mut cf, &mut err).is_ok() {
80///     println!("Decoded barcode id={}, dir={}, cf={:.2}", id, dir, cf);
81/// }
82/// ```
83pub fn ar_matrix_code_get_id(
84    image: &[u8],
85    xsize: i32,
86    ysize: i32,
87    vertex: &[[ARdouble; 2]; 4],
88    code_type: ARMatrixCodeType,
89    pixel_format: crate::types::ARPixelFormat,
90    patt_ratio: f64,
91    id: &mut i32,
92    dir: &mut i32,
93    cf: &mut f64,
94    error_corrected: &mut i32,
95) -> Result<(), &'static str> {
96    let dim = (code_type as i32) & 0xFF;
97    if dim < 3 || dim > 6 {
98        return Err("Unsupported matrix dimension");
99    }
100
101    let grid_size = dim;
102    let mut bits = vec![0u8; (grid_size * grid_size) as usize];
103
104    sample_grid(image, xsize, ysize, vertex, grid_size, pixel_format, patt_ratio, &mut bits)?;
105    
106    debug!("ar_matrix_code_get_id: vertices v0=({:.1},{:.1}) v1=({:.1},{:.1}) v2=({:.1},{:.1}) v3=({:.1},{:.1})",
107        vertex[0][0], vertex[0][1], vertex[1][0], vertex[1][1],
108        vertex[2][0], vertex[2][1], vertex[3][0], vertex[3][1]);
109
110
111    let mut min_val = 255u8;
112    let mut max_val = 0u8;
113    for &b in &bits {
114        if b < min_val { min_val = b; }
115        if b > max_val { max_val = b; }
116    }
117    
118    let mid_thresh = ((min_val as u32 + max_val as u32) / 2) as u8;
119    debug!("ar_matrix_code_get_id: min={}, max={}, thresh={}", min_val, max_val, mid_thresh);
120
121    if (max_val as i32 - min_val as i32) < 30 {
122        return Err("Low contrast in matrix grid");
123    }
124
125    let mut core_bits = vec![0u8; (grid_size * grid_size) as usize];
126    for i in 0..bits.len() {
127        core_bits[i] = if bits[i] < mid_thresh { 1 } else { 0 };
128    }
129    
130    trace!("ar_matrix_code_get_id: grid_size={}, core_bits={:?}", grid_size, core_bits);
131    debug!("ar_matrix_code_get_id: core_bits={:?}", core_bits);
132
133    // The C reference `get_matrix_code` receives only the dim×dim inner data.
134    // Check orientation based on corners of the core data:
135    // An unrotated pattern has:
136    //   top-left (0,0) = 1, bottom-left (0, dim-1) = 1
137    //   bottom-right (dim-1, dim-1) = 0.
138    let size = dim;
139    let corners = [
140        0,                          // top-left
141        (size - 1) * size,          // bottom-left
142        size * size - 1,            // bottom-right
143        size - 1,                   // top-right
144    ];
145    
146    let mut dir_code = [0u8; 4];
147    for i in 0..4 {
148        dir_code[i] = core_bits[corners[i] as usize];
149    }
150    
151    let mut matched_dir = -1;
152    for i in 0..4 {
153        if dir_code[i] == 1 && dir_code[(i+1)%4] == 1 && dir_code[(i+2)%4] == 0 {
154            matched_dir = i as i32;
155            break;
156        }
157    }
158    
159    if matched_dir == -1 {
160        return Err("Barcode locator pattern not found in grid corners");
161    }
162    
163    // Extract code from the dim×dim core bits.
164    // The 3 pixels forming the locator corners are ignored.
165    let mut code_raw = 0u64;
166    
167    if matched_dir == 0 {
168        for j in 0..size {
169            for i in 0..size {
170                if i == 0 && j == 0 { continue; }
171                if i == 0 && j == size - 1 { continue; }
172                if i == size - 1 && j == size - 1 { continue; }
173                code_raw <<= 1;
174                if core_bits[(j * size + i) as usize] == 1 { code_raw += 1; }
175            }
176        }
177    } else if matched_dir == 1 {
178        for i in 0..size {
179            for j in (0..size).rev() {
180                if i == 0 && j == size - 1 { continue; }
181                if i == size - 1 && j == size - 1 { continue; }
182                if i == size - 1 && j == 0 { continue; }
183                code_raw <<= 1;
184                if core_bits[(j * size + i) as usize] == 1 { code_raw += 1; }
185            }
186        }
187    } else if matched_dir == 2 {
188        for j in (0..size).rev() {
189            for i in (0..size).rev() {
190                if i == size - 1 && j == size - 1 { continue; }
191                if i == size - 1 && j == 0 { continue; }
192                if i == 0 && j == 0 { continue; }
193                code_raw <<= 1;
194                if core_bits[(j * size + i) as usize] == 1 { code_raw += 1; }
195            }
196        }
197    } else if matched_dir == 3 {
198        for i in (0..size).rev() {
199            for j in 0..size {
200                if i == size - 1 && j == 0 { continue; }
201                if i == 0 && j == 0 { continue; }
202                if i == 0 && j == size - 1 { continue; }
203                code_raw <<= 1;
204                if core_bits[(j * size + i) as usize] == 1 { code_raw += 1; }
205            }
206        }
207    }
208    
209    debug!("ar_matrix_code_get_id: code_raw={}, dir={}", code_raw, matched_dir);
210    
211    *dir = matched_dir;
212    *cf = 1.0; 
213
214    // Handle decoding logic mapped from pattern types!
215    if let Ok((decoded_id, err)) = decode_matrix_raw(code_raw, code_type) {
216        *id = decoded_id;
217        *error_corrected = err;
218        Ok(())
219    } else {
220        Err("Failed to decode extracted matrix code string")
221    }
222}
223
224
225/// Higher-level helper: trace contour lines then decode a barcode marker.
226///
227/// Alternative path for barcode detection not yet wired into [`crate::marker::ar_get_marker_info`].
228/// Given a raw `ARMarkerInfo2` candidate, this function:
229/// 1. Calls [`crate::marker::ar_get_line`] via the `ARHandle`'s lens-distortion
230///    table to undistort the square edge lines and compute corner vertices.
231/// 2. Calls [`ar_matrix_code_get_id`] to decode the barcode.
232/// 3. Builds and returns a populated [`crate::types::ARMarkerInfo`] struct.
233///
234/// Returns `Err` if `ar_handle.ar_param_lt` is null, if line fitting fails,
235/// or if the barcode decode fails.
236pub fn ar_get_barcode_marker(
237    image: &[u8],
238    ar_handle: &mut ARHandle, 
239    marker_info2: &mut ARMarkerInfo2
240) -> Result<ARMarkerInfo, &'static str> {
241    trace!("ar_get_barcode_marker: started for area={}, coord_num={}", marker_info2.area, marker_info2.coord_num);
242    
243    let mut marker_info = ARMarkerInfo::default();
244    let mut id = -1;
245    let mut dir = -1;
246    let mut cf = 0.0;
247    let mut error_corrected = 0;
248
249    // Resolve corner coordinates from indices
250    let mut resolved_vertex = [[0.0; 2]; 4];
251    let mut line = [[0.0; 3]; 4];
252
253    if ar_handle.ar_param_lt.is_null() {
254        return Err("arParamLT is null");
255    }
256    let param_lt = unsafe { &*ar_handle.ar_param_lt };
257
258    ar_get_line(
259        &marker_info2.x_coord,
260        &marker_info2.y_coord,
261        marker_info2.coord_num as usize,
262        &marker_info2.vertex,
263        &param_lt.param_ltf,
264        &mut line,
265        &mut resolved_vertex
266    )?;
267
268    ar_matrix_code_get_id(
269        image,
270        ar_handle.xsize,
271        ar_handle.ysize,
272        &resolved_vertex,
273        ar_handle.get_matrix_code_type(),
274        ar_handle.ar_pixel_format,
275        ar_handle.patt_ratio,
276        &mut id,
277        &mut dir,
278        &mut cf,
279        &mut error_corrected
280    )?;
281
282    marker_info.id_matrix = id;
283    marker_info.dir_matrix = dir;
284    marker_info.cf_matrix = cf;
285    marker_info.area = marker_info2.area;
286    marker_info.pos = marker_info2.pos;
287    marker_info.vertex = resolved_vertex;
288    marker_info.line = line;
289    marker_info.error_corrected = error_corrected;
290
291    Ok(marker_info)
292}
293
294
295/// Project image pixels onto a regular grid using a homography.
296///
297/// Samples `grid_size × grid_size` evenly-spaced points inside the square
298/// defined by `vertex`. Uses the same world-coordinate system as `arPattGetImage2`
299/// (i.e. nominal corners at (100,100)…(110,110)) so that `patt_ratio` correctly
300/// controls what fraction of the square area is sampled.
301///
302/// For each grid cell `(x, y)` the homography (`para`) maps world coordinates to
303/// image pixel `(xc, yc)` and reads into `bits` the intensity (G channel for
304/// multi-channel formats, first byte for luma).
305///
306/// # Parameters
307/// - `grid_size` — total grid side length including the one-cell border ring;
308///   equal to `dim + 2` where `dim = code_type & 0xFF`.
309/// - `patt_ratio` — fraction of the square covered by data cells (0.5–0.9).
310/// - `bits` — output: `grid_size * grid_size` raw intensity values.
311fn sample_grid(
312    image: &[u8],
313    xsize: i32,
314    ysize: i32,
315    vertex: &[[ARdouble; 2]; 4],
316    grid_size: i32,
317    pixel_format: crate::types::ARPixelFormat,
318    patt_ratio: f64,
319    bits: &mut [u8],
320) -> Result<(), &'static str> {
321    let nc = match pixel_format {
322        crate::types::ARPixelFormat::MONO => 1,
323        crate::types::ARPixelFormat::RGB | crate::types::ARPixelFormat::BGR => 3,
324        crate::types::ARPixelFormat::RGBA | crate::types::ARPixelFormat::BGRA | crate::types::ARPixelFormat::ARGB => 4,
325        _ => return Err("Unsupported pixel format in sample_grid"),
326    };
327
328    let mut world = [[0.0; 2]; 4];
329    let mut para = [[0.0; 3]; 3];
330
331    // Match C reference arPattGetImage2: world coords (100,100)→(110,110)
332    world[0][0] = 100.0; world[0][1] = 100.0;
333    world[1][0] = 110.0; world[1][1] = 100.0;
334    world[2][0] = 110.0; world[2][1] = 110.0;
335    world[3][0] = 100.0; world[3][1] = 110.0;
336
337    crate::pattern::get_cpara(&world, vertex, &mut para)?;
338
339    let patt_ratio1 = (1.0 - patt_ratio) / 2.0 * 10.0;
340    let patt_ratio2 = patt_ratio * 10.0;
341
342    for y in 0..grid_size {
343        let yw = (100.0 + patt_ratio1) + patt_ratio2 * (y as f64 + 0.5) / grid_size as f64;
344        for x in 0..grid_size {
345            let xw = (100.0 + patt_ratio1) + patt_ratio2 * (x as f64 + 0.5) / grid_size as f64;
346            
347            let d = para[2][0] * xw + para[2][1] * yw + para[2][2];
348            if d == 0.0 { continue; }
349            
350            let xc = ((para[0][0] * xw + para[0][1] * yw + para[0][2]) / d) as i32;
351            let yc = ((para[1][0] * xw + para[1][1] * yw + para[1][2]) / d) as i32;
352
353            if xc >= 0 && xc < xsize && yc >= 0 && yc < ysize {
354                if y == 0 && (x == 0 || x == grid_size - 1) || y == grid_size - 1 && (x == 0 || x == grid_size - 1) {
355                    trace!("sample_grid: grid({},{}) -> image({},{})", x, y, xc, yc);
356                }
357                let idx = ((yc * xsize + xc) * nc) as usize;
358                if idx < image.len() {
359                    // For RGB, averaging R, G, B gives luma. Or we can just take Green channel for simplicity.
360                    // We'll take the G channel (idx + 1) for RGB, or just first byte (idx) if luma
361                    bits[(y * grid_size + x) as usize] = if nc >= 3 {
362                        // Approximation: Read the second byte (G) which usually represents contrast well, 
363                        // or do a simple average if safe
364                        image[idx + 1]
365                    } else {
366                        image[idx]
367                    };
368                }
369            }
370        }
371    }
372
373    Ok(())
374}
375
376
377
378/// Rotates a `dim × dim` bit-grid by `dir * 90°` counter-clockwise.
379///
380/// Used to bring a barcode into a canonical orientation before extracting the
381/// data-bit sequence. Directions follow the ARToolKit convention:
382/// - `0` — no rotation (identity)
383/// - `1` — 90° CCW
384/// - `2` — 180°
385/// - `3` — 270° CCW
386#[allow(dead_code)]
387fn rotate_bits(bits: &[u8], dim: i32, dir: i32) -> Vec<u8> {
388    let mut rotated = vec![0u8; bits.len()];
389    for y in 0..dim {
390        for x in 0..dim {
391            let (nx, ny) = match dir {
392                0 => (x, y),
393                1 => (y, dim - 1 - x),
394                2 => (dim - 1 - x, dim - 1 - y),
395                3 => (dim - 1 - y, x),
396                _ => (x, y),
397            };
398            rotated[(ny * dim + nx) as usize] = bits[(y * dim + x) as usize];
399        }
400    }
401    rotated
402}
403
404/// Decodes a raw bit-word into a marker ID, applying ECC if the `code_type` supports it.
405///
406/// Maps `code_raw` (a `u64` bit-field extracted from the core grid) to a marker
407/// ID using the appropriate lookup table for the given `code_type`:
408///
409/// | `code_type`                  | ECC scheme                              |
410/// |------------------------------|-----------------------------------------|
411/// | `Code3x3`                    | None — raw 6-bit value                  |
412/// | `Code3x3Parity65`            | Single-bit parity (table lookup)        |
413/// | `Code3x3Hamming63`           | Hamming (6,3) (table lookup)            |
414/// | `Code4x4` / `Code5x5`       | BCH (39,12) / BCH (51,12)               |
415fn decode_matrix_raw(code_raw: u64, code_type: ARMatrixCodeType) -> Result<(i32, i32), &'static str> {
416    match code_type {
417        ARMatrixCodeType::Code3x3 => {
418            // Simple 3x3 has no ECC in ARToolKit5! The bits strictly form the value ranging up to 63 since it's 9 bits minus corners?
419            // Actually it's 9 bits, so it ranges up to 511.
420            Ok((code_raw as i32, 0))
421        },
422        ARMatrixCodeType::Code3x3Parity65 => {
423            let code = crate::bch::decode_parity65(code_raw)?;
424            Ok((code as i32, 0))
425        },
426        ARMatrixCodeType::Code3x3Hamming63 => {
427            let (code, err) = crate::bch::decode_hamming63(code_raw)?;
428            Ok((code as i32, err))
429        },
430        ARMatrixCodeType::Code4x4BCH1393 | ARMatrixCodeType::Code4x4BCH1355 |
431        ARMatrixCodeType::Code5x5BCH22125 | ARMatrixCodeType::Code5x5BCH2277 => {
432            let (code, err) = crate::bch::decode_bch(code_type, code_raw)?;
433            Ok((code as i32, err))
434        },
435        _ => {
436            // The unhandled types include GLOBAL_ID, etc.
437            Ok((code_raw as i32, 0))
438        }
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_rotate_bits() {
448        let dim = 3;
449        let bits = vec![
450            1, 0, 0,
451            1, 1, 0,
452            1, 0, 0,
453        ];
454        
455        let rot1 = rotate_bits(&bits, dim, 1);
456        let expected1 = vec![
457            0, 0, 0,
458            0, 1, 0,
459            1, 1, 1,
460        ];
461        assert_eq!(rot1, expected1);
462    }
463}