Skip to main content

webarkitlib_rs/
marker.rs

1/*
2 *  marker.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 *  As a special exception, the copyright holders of this library give you
21 *  permission to link this library with independent modules to produce an
22 *  executable, regardless of the license terms of these independent modules, and to
23 *  copy and distribute the resulting executable under terms of your choice,
24 *  provided that you also meet, for each linked independent module, the terms and
25 *  conditions of the license of that module. An independent module is a module
26 *  which is neither derived from nor based on this library. If you modify this
27 *  library, you may extend this exception to your version of the library, but you
28 *  are not obligated to do so. If you do not wish to do so, delete this exception
29 *  statement from your version.
30 *
31 *  Copyright 2026 WebARKit.
32 *
33 *  Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
34 *
35 */
36
37//! Marker Detection Pipeline
38//! Ported from arDetectMarker.c, arDetectMarker2.c, and arGetMarkerInfo.c
39
40use crate::types::{ARLabelInfo, ARMarkerInfo2, ARdouble};
41use log::debug;
42
43pub const AR_AREA_MAX: i32 = 100000;
44pub const AR_AREA_MIN: i32 = 70;
45pub const AR_SQUARE_FIT_THRESH: f64 = 0.05;
46pub const AR_CHAIN_MAX: usize = 10000;
47pub const AR_SQUARE_MAX: usize = 30;
48
49#[derive(Debug, PartialEq, Clone, Copy)]
50pub enum ImageProcMode {
51    FrameImage = 0,
52    FieldImage = 1,
53}
54
55/// High-level AR marker detection pipeline.
56///
57/// This is the main entry point for detecting markers in a video frame.
58/// It orchestrates the full pipeline in order:
59/// 1. **Thresholding** — luma image binarised against `ar_handle.ar_labeling_thresh`.
60/// 2. **Labeling** — [`ar_labeling`](crate::labeling::ar_labeling) extracts connected
61///    dark (or bright) regions and assigns each a unique integer label.
62/// 3. **Candidate selection** - `ar_detect_marker2` filters regions by area and
63///    fits a quadrilateral contour using `ar_get_contour` and `check_square`.
64/// 4. **Identity resolution** — [`ar_get_marker_info`] decodes each candidate:
65///    template matching (via `ar_patt_get_image` + `pattern_match`) **or**
66///    matrix-code reading (via [`crate::matrix::ar_matrix_code_get_id`]),
67///    depending on `ar_handle.ar_pattern_detection_mode`.
68///
69/// Results are written into `ar_handle.marker_info` (up to `AR_SQUARE_MAX` markers).
70///
71/// # Example
72/// ```rust,no_run
73/// use webarkitlib_rs::marker::ar_detect_marker;
74/// use webarkitlib_rs::types::{ARHandle, AR2VideoBufferT};
75///
76/// let mut handle = ARHandle::default();
77/// // ... configure handle, load pattern, set param_lt ...
78/// let frame = AR2VideoBufferT::default();
79/// if ar_detect_marker(&mut handle, &frame).is_ok() {
80///     for i in 0..handle.marker_num as usize {
81///         let m = &handle.marker_info[i];
82///         println!("Marker id={}, cf={:.2}", m.id, m.cf);
83///     }
84/// }
85/// ```
86pub fn ar_detect_marker(
87    ar_handle: &mut crate::types::ARHandle,
88    frame: &crate::types::AR2VideoBufferT,
89) -> Result<(), &'static str> {
90    ar_handle.marker_num = 0;
91    
92    let luma_buff = match &frame.buff_luma {
93        Some(b) => b.as_slice(),
94        None => return Err("AR2VideoBufferT requires buff_luma to be available"),
95    };
96    
97    let color_buff = match &frame.buff {
98        Some(b) => b.as_slice(),
99        None => return Err("AR2VideoBufferT requires buff to be available"),
100    };
101    
102    let thresh = ar_handle.ar_labeling_thresh as u8;
103    let label_mode = if ar_handle.ar_labeling_mode == 0 {
104        crate::labeling::LabelingMode::BlackRegion
105    } else {
106        crate::labeling::LabelingMode::WhiteRegion
107    };
108    
109    let labeling_proc_mode = if ar_handle.ar_image_proc_mode == 0 {
110        crate::labeling::ImageProcMode::FrameImage
111    } else {
112        crate::labeling::ImageProcMode::FieldImage
113    };
114
115    crate::labeling::ar_labeling(
116        luma_buff,
117        ar_handle.xsize,
118        ar_handle.ysize,
119        label_mode,
120        thresh,
121        labeling_proc_mode,
122        &mut ar_handle.label_info,
123        ar_handle.ar_debug != 0
124    )?;
125    
126    if ar_handle.ar_debug != 0 {
127        debug!("ar_labeling found {} labels.", ar_handle.label_info.label_num);
128    }
129
130    let image_proc_mode = if ar_handle.ar_image_proc_mode == 0 {
131        ImageProcMode::FrameImage
132    } else {
133        ImageProcMode::FieldImage
134    };
135
136    ar_detect_marker2(
137        ar_handle.xsize,
138        ar_handle.ysize,
139        &mut ar_handle.label_info,
140        image_proc_mode,
141        AR_AREA_MAX,
142        AR_AREA_MIN,
143        AR_SQUARE_FIT_THRESH,
144        &mut *ar_handle.marker_info2,
145        &mut ar_handle.marker2_num,
146    )?;
147
148    if ar_handle.ar_debug != 0 {
149        debug!("ar_detect_marker2 found {} square candidates.", ar_handle.marker2_num);
150    }
151
152    if ar_handle.ar_param_lt.is_null() {
153        return Err("ARParamLT is null in ARHandle");
154    }
155    
156    let image_proc_mode2 = if ar_handle.ar_image_proc_mode == 0 {
157        ImageProcMode::FrameImage
158    } else {
159        ImageProcMode::FieldImage
160    };
161    
162    let param_ltf = unsafe { &(*ar_handle.ar_param_lt).param_ltf };
163
164    let patt_handle_opt = if !ar_handle.patt_handle.is_null() {
165        Some(unsafe { &*ar_handle.patt_handle })
166    } else {
167        None
168    };
169
170    ar_get_marker_info(
171        color_buff,
172        ar_handle.xsize,
173        ar_handle.ysize,
174        ar_handle.ar_pixel_format,
175        &ar_handle.marker_info2[..],
176        ar_handle.marker2_num,
177        image_proc_mode2,
178        ar_handle.ar_pattern_detection_mode,
179        param_ltf,
180        ar_handle.patt_ratio,
181        patt_handle_opt,
182        &mut *ar_handle.marker_info,
183        &mut ar_handle.marker_num,
184        ar_handle.matrix_code_type,
185    )?;
186
187    if ar_handle.ar_debug != 0 {
188        debug!("ar_get_marker_info produced {} final markers.", ar_handle.marker_num);
189    }
190
191    Ok(())
192}
193
194/// Refine labeled regions into square (marker) candidates.
195///
196/// Ported from `arDetectMarker2.c`. Operates on the output of [`crate::labeling::ar_labeling`].
197/// For each surviving region it:
198/// - Skips labels outside the `[area_min, area_max]` pixel-count range.
199/// - Skips labels touching the image boundary (prevents partial squares).
200/// - Calls `ar_get_contour` to trace the region boundary.
201/// - Calls `check_square` to verify the contour is a plausible square
202///   (four well-separated vertices, low fit error against `square_fit_thresh`).
203/// - Deduplicates overlapping candidates by area proximity.
204///
205/// In `FieldImage` mode areas and coordinates are scaled by ½ for processing
206/// and scaled back to full resolution before returning.
207///
208/// # Parameters
209/// - `label_info` — labeling output produced by [`crate::labeling::ar_labeling`].
210/// - `marker_info2` — output slice (length ≥ `AR_SQUARE_MAX`) to write candidates into.
211/// - `marker2_num` — number of candidates written on return.
212pub fn ar_detect_marker2(
213    xsize: i32,
214    ysize: i32,
215    label_info: &mut ARLabelInfo,
216    image_proc_mode: ImageProcMode,
217    area_max: i32,
218    area_min: i32,
219    square_fit_thresh: ARdouble,
220    marker_info2: &mut [ARMarkerInfo2],
221    marker2_num: &mut i32,
222) -> Result<(), &'static str> {
223    let mut xsize_local = xsize;
224    let mut ysize_local = ysize;
225    let mut area_min_local = area_min;
226    let mut area_max_local = area_max;
227
228    if matches!(image_proc_mode, ImageProcMode::FieldImage) {
229        area_min_local /= 4;
230        area_max_local /= 4;
231        xsize_local /= 2;
232        ysize_local /= 2;
233    }
234
235    *marker2_num = 0;
236    
237    let label_num = label_info.label_num as usize;
238    for i in 0..label_num {
239        if label_info.area[i] < area_min_local || label_info.area[i] > area_max_local {
240            debug!("Label {} skipped due to Area ({}) not in [{}, {}]", i, label_info.area[i], area_min_local, area_max_local); 
241            continue;
242        }
243        if label_info.clip[i][0] <= 1 || label_info.clip[i][1] >= xsize_local - 2 {
244            debug!("Label {} skipped due to X-Clip bounds", i);
245            continue;
246        }
247        if label_info.clip[i][2] <= 1 || label_info.clip[i][3] >= ysize_local - 2 {
248            debug!("Label {} skipped due to Y-Clip bounds", i);
249            continue;
250        }
251
252        let mut current_marker = ARMarkerInfo2::default();
253        
254        let ret = ar_get_contour(
255            &label_info.label_image,
256            xsize_local,
257            ysize_local,
258            (i + 1) as i32,
259            &label_info.clip[i],
260            &mut current_marker,
261        );
262        
263        if ret.is_err() {
264            debug!("ar_get_contour failed for label {}: {:?}", i, ret.unwrap_err());
265            continue;
266        }
267
268        let ret = check_square(label_info.area[i], &mut current_marker, square_fit_thresh);
269        if ret.is_err() {
270            debug!("check_square failed for label {}: {:?}", i, ret.unwrap_err());
271            continue;
272        }
273
274        current_marker.area = label_info.area[i];
275        current_marker.pos[0] = label_info.pos[i][0];
276        current_marker.pos[1] = label_info.pos[i][1];
277        
278        marker_info2[*marker2_num as usize] = current_marker;
279        *marker2_num += 1;
280        if *marker2_num as usize == marker_info2.len() {
281            break;
282        }
283    }
284
285    // Sort/Filter identical overlapping markers
286    let num_markers = *marker2_num as usize;
287    for i in 0..num_markers {
288        for j in i + 1..num_markers {
289            if marker_info2[i].area == 0 || marker_info2[j].area == 0 {
290                continue;
291            }
292            let d = (marker_info2[i].pos[0] - marker_info2[j].pos[0]).powi(2)
293                  + (marker_info2[i].pos[1] - marker_info2[j].pos[1]).powi(2);
294            
295            if marker_info2[i].area > marker_info2[j].area {
296                if d < (marker_info2[i].area as ARdouble) / 4.0 {
297                    marker_info2[j].area = 0;
298                }
299            } else {
300                if d < (marker_info2[j].area as ARdouble) / 4.0 {
301                    marker_info2[i].area = 0;
302                }
303            }
304        }
305    }
306
307    // Compact the array
308    let mut valid_count = 0;
309    for i in 0..num_markers {
310        if marker_info2[i].area > 0 {
311            if i != valid_count {
312                marker_info2[valid_count] = marker_info2[i].clone();
313            }
314            valid_count += 1;
315        }
316    }
317    *marker2_num = valid_count as i32;
318
319    if matches!(image_proc_mode, ImageProcMode::FieldImage) {
320        for i in 0..(*marker2_num as usize) {
321            let pm = &mut marker_info2[i];
322            pm.area *= 4;
323            pm.pos[0] *= 2.0;
324            pm.pos[1] *= 2.0;
325            for j in 0..pm.coord_num as usize {
326                pm.x_coord[j] *= 2;
327                pm.y_coord[j] *= 2;
328            }
329        }
330    }
331
332    Ok(())
333}
334
335/// Trace the boundary contour of a labeled region using 8-connected chain-code walking.
336///
337/// Starting from the top-left pixel of the region's bounding clip, walks the
338/// outer boundary and records (x, y) coordinates into `marker_info2.x_coord` /
339/// `y_coord`. After tracing, rotates the chain so the farthest point from the
340/// start is first - this provides a canonical start for `check_square`.
341///
342/// Returns `Err` if:
343/// - No starting pixel is found in the clip region.
344/// - The contour is broken (no neighbour found in 8 directions).
345/// - The contour exceeds `AR_CHAIN_MAX - 1` pixels.
346fn ar_get_contour(
347    limage: &[crate::types::ARLabelingLabelType],
348    xsize: i32,
349    _ysize: i32,
350    label: i32,
351    clip: &[i32; 4],
352    marker_info2: &mut ARMarkerInfo2,
353) -> Result<(), &'static str> {
354    let xdir = [0, 1, 1, 1, 0, -1, -1, -1];
355    let ydir = [-1, -1, 0, 1, 1, 1, 0, -1];
356    
357    let mut sx = -1;
358    let sy = clip[2];
359    
360    // After labeling Pass 3, limage contains dense sequential IDs.
361    // Compare the pixel value directly against `label`.
362    let mut p_idx = (sy * xsize + clip[0]) as usize;
363    for i in clip[0]..=clip[1] {
364        if p_idx < limage.len() {
365            if limage[p_idx] == label as crate::types::ARLabelingLabelType {
366                sx = i;
367                break;
368            }
369        }
370        p_idx += 1;
371    }
372    
373    if sx == -1 {
374        debug!("ar_get_contour failed. label={}. clip={:?}.", label, clip);
375        return Err("Contour start point not found");
376    }
377
378    marker_info2.coord_num = 1;
379    marker_info2.x_coord[0] = sx;
380    marker_info2.y_coord[0] = sy;
381    let mut dir = 5;
382    
383    loop {
384        let last_idx = (marker_info2.coord_num - 1) as usize;
385        let p_idx = (marker_info2.y_coord[last_idx] * xsize + marker_info2.x_coord[last_idx]) as usize;
386        
387        dir = (dir + 5) % 8;
388        let mut found = false;
389        for _ in 0..8 {
390            let next_idx = (p_idx as isize + ydir[dir] as isize * xsize as isize + xdir[dir] as isize) as usize;
391            if next_idx < limage.len() && limage[next_idx] > 0 {
392                found = true;
393                break;
394            }
395            dir = (dir + 1) % 8;
396        }
397        
398        if !found {
399            return Err("Contour broken");
400        }
401        
402        let curr_idx = marker_info2.coord_num as usize;
403        marker_info2.x_coord[curr_idx] = marker_info2.x_coord[last_idx] + xdir[dir];
404        marker_info2.y_coord[curr_idx] = marker_info2.y_coord[last_idx] + ydir[dir];
405        
406        if marker_info2.x_coord[curr_idx] == sx && marker_info2.y_coord[curr_idx] == sy {
407            break;
408        }
409        
410        marker_info2.coord_num += 1;
411        if marker_info2.coord_num as usize >= AR_CHAIN_MAX - 1 {
412            return Err("Contour too long");
413        }
414    }
415
416    let mut dmax = 0;
417    let mut v1 = 0;
418    
419    for i in 1..marker_info2.coord_num as usize {
420        let d = (marker_info2.x_coord[i] - sx).pow(2) + (marker_info2.y_coord[i] - sy).pow(2);
421        if d > dmax {
422            dmax = d;
423            v1 = i;
424        }
425    }
426
427    let mut wx = vec![0; v1];
428    let mut wy = vec![0; v1];
429    
430    for i in 0..v1 {
431        wx[i] = marker_info2.x_coord[i];
432        wy[i] = marker_info2.y_coord[i];
433    }
434    
435    let coord_num = marker_info2.coord_num as usize;
436    for i in v1..coord_num {
437        marker_info2.x_coord[i - v1] = marker_info2.x_coord[i];
438        marker_info2.y_coord[i - v1] = marker_info2.y_coord[i];
439    }
440    
441    let offset = coord_num - v1;
442    for i in 0..v1 {
443        marker_info2.x_coord[offset + i] = wx[i];
444        marker_info2.y_coord[offset + i] = wy[i];
445    }
446    
447    let end_idx = marker_info2.coord_num as usize;
448    marker_info2.x_coord[end_idx] = marker_info2.x_coord[0];
449    marker_info2.y_coord[end_idx] = marker_info2.y_coord[0];
450    marker_info2.coord_num += 1;
451
452    Ok(())
453}
454
455/// Validates that a traced contour is a plausible planar square.
456///
457/// Uses a recursive vertex-splitting algorithm (similar to the Ramer–Douglas–Peucker
458/// approach) to find exactly **four** corner points from the contour stored in
459/// `marker_info2.{x,y}_coord`.
460///
461/// The fit threshold `factor` is combined with the region `area` to produce an
462/// adaptive pixel-distance tolerance:
463/// ```text
464/// thresh = (area / 0.75) * 0.01 * factor
465/// ```
466/// Values used in production: `area ∈ [AR_AREA_MIN, AR_AREA_MAX]`,
467/// `factor = AR_SQUARE_FIT_THRESH` (0.05).
468///
469/// On success the four vertex indices are written into `marker_info2.vertex`
470/// in counter-clockwise order (indices 0–4, where index 4 wraps back to 0).
471///
472/// Returns `Err` if the split process cannot isolate exactly four corners,
473/// indicating the region is not a valid square marker candidate.
474fn check_square(area: i32, marker_info2: &mut ARMarkerInfo2, factor: ARdouble) -> Result<(), &'static str> {
475    let mut dmax = 0;
476    let mut v1 = 0;
477    let sx = marker_info2.x_coord[0];
478    let sy = marker_info2.y_coord[0];
479    let coord_num = marker_info2.coord_num as usize;
480    
481    for i in 1..(coord_num - 1) {
482        let d = (marker_info2.x_coord[i] - sx).pow(2) + (marker_info2.y_coord[i] - sy).pow(2);
483        if d > dmax {
484            dmax = d;
485            v1 = i;
486        }
487    }
488
489    let thresh = ((area as f64) / 0.75) * 0.01 * factor;
490    let mut vertex = [0; 10];
491    vertex[0] = 0;
492    let mut wv1 = [0; 10];
493    let mut wvnum1 = 0;
494    let mut wv2 = [0; 10];
495    let mut wvnum2 = 0;
496    
497    if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, 0, v1, thresh, &mut wv1, &mut wvnum1).is_err() {
498        return Err("Square check failed");
499    }
500    if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, v1, coord_num - 1, thresh, &mut wv2, &mut wvnum2).is_err() {
501        return Err("Square check failed");
502    }
503
504    if wvnum1 == 1 && wvnum2 == 1 {
505        vertex[1] = wv1[0];
506        vertex[2] = v1;
507        vertex[3] = wv2[0];
508    } else if wvnum1 > 1 && wvnum2 == 0 {
509        let v2 = v1 / 2;
510        wvnum1 = 0;
511        wvnum2 = 0;
512        if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, 0, v2, thresh, &mut wv1, &mut wvnum1).is_err() {
513            return Err("Square check failed");
514        }
515        if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, v2, v1, thresh, &mut wv2, &mut wvnum2).is_err() {
516            return Err("Square check failed");
517        }
518        if wvnum1 == 1 && wvnum2 == 1 {
519            vertex[1] = wv1[0];
520            vertex[2] = wv2[0];
521            vertex[3] = v1;
522        } else {
523            return Err("Not a square");
524        }
525    } else if wvnum1 == 0 && wvnum2 > 1 {
526        let v2 = (v1 + coord_num - 1) / 2;
527        wvnum1 = 0;
528        wvnum2 = 0;
529        if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, v1, v2, thresh, &mut wv1, &mut wvnum1).is_err() {
530            return Err("Square check failed");
531        }
532        if get_vertex(&marker_info2.x_coord, &marker_info2.y_coord, v2, coord_num - 1, thresh, &mut wv2, &mut wvnum2).is_err() {
533            return Err("Square check failed");
534        }
535        if wvnum1 == 1 && wvnum2 == 1 {
536            vertex[1] = v1;
537            vertex[2] = wv1[0];
538            vertex[3] = wv2[0];
539        } else {
540            return Err("Not a square");
541        }
542    } else {
543        return Err("Not a square");
544    }
545
546    marker_info2.vertex[0] = vertex[0] as i32;
547    marker_info2.vertex[1] = vertex[1] as i32;
548    marker_info2.vertex[2] = vertex[2] as i32;
549    marker_info2.vertex[3] = vertex[3] as i32;
550    marker_info2.vertex[4] = (coord_num - 1) as i32;
551
552    Ok(())
553}
554
555/// Recursively find sub-vertices between two contour indices using perpendicular distance.
556///
557/// For the chord from contour point `st` to contour point `ed`, this function
558/// finds the contour point with the maximum perpendicular distance from the chord.
559/// If that distance exceeds `thresh`, the segment is split at that point and the
560/// function recurses on both halves.
561///
562/// The result is appended into `vertex` (up to 5 entries), and `vnum` is
563/// incremented for each found vertex. Returns `Err` if more than 5 vertices are
564/// found (which indicates a non-convex / non-square shape).
565fn get_vertex(
566    x_coord: &[i32],
567    y_coord: &[i32],
568    st: usize,
569    ed: usize,
570    thresh: ARdouble,
571    vertex: &mut [usize],
572    vnum: &mut usize,
573) -> Result<(), &'static str> {
574    let a = (y_coord[ed] - y_coord[st]) as f64;
575    let b = (x_coord[st] - x_coord[ed]) as f64;
576    let c = (x_coord[ed] * y_coord[st] - y_coord[ed] * x_coord[st]) as f64;
577    
578    let mut dmax = 0.0;
579    let mut v1 = st + 1;
580    
581    for i in (st + 1)..ed {
582        let d = a * (x_coord[i] as f64) + b * (y_coord[i] as f64) + c;
583        if d * d > dmax {
584            dmax = d * d;
585            v1 = i;
586        }
587    }
588    
589    if dmax / (a * a + b * b) > thresh {
590        if get_vertex(x_coord, y_coord, st, v1, thresh, vertex, vnum).is_err() {
591            return Err("Vertex expansion failed");
592        }
593        
594        if *vnum > 5 {
595            return Err("Too many vertices");
596        }
597        vertex[*vnum] = v1;
598        *vnum += 1;
599        
600        if get_vertex(x_coord, y_coord, v1, ed, thresh, vertex, vnum).is_err() {
601            return Err("Vertex expansion failed");
602        }
603    }
604    
605    Ok(())
606}
607
608use crate::math::{ARMat, ARVec};
609use crate::types::{ARParamLTf, ARMarkerInfo};
610
611/// Fits undistorted straight lines to each of the four sides of a detected square.
612///
613/// Ported from `arGetLine.c`. For each side of the candidate square (defined by
614/// consecutive vertex index pairs) it:
615/// 1. Trims 5 % of the edge from each end to avoid corner artefacts.
616/// 2. Calls `param_ltf.observ2ideal` to correct lens-distortion on every pixel
617///    along the edge.
618/// 3. Runs PCA on the corrected pixel positions to find the dominant axis, from
619///    which the line equation `(a, b, c)` is derived such that `ax + by + c = 0`.
620/// 4. Computes the four corner coordinates as intersections of adjacent lines.
621///
622/// # Parameters
623/// - `x_coord`, `y_coord` — full pixel contour from `ar_get_contour`.
624/// - `vertex` — five vertex indices into the contour (4 corners + wrap-around).
625/// - `param_ltf` — lens-distortion look-up table for `observ2ideal` mapping.
626/// - `line` — output: four `[a, b, c]` line coefficients.
627/// - `v` — output: four `[x, y]` corner coordinates in ideal (undistorted) space.
628///
629/// Returns `Err` if any edge has fewer than two pixels, PCA fails, or adjacent
630/// lines are nearly parallel (determinant < 0.0001).
631pub fn ar_get_line(
632    x_coord: &[i32],
633    y_coord: &[i32],
634    _coord_num: usize,
635    vertex: &[i32],
636    param_ltf: &ARParamLTf,
637    line: &mut [[ARdouble; 3]; 4],
638    v: &mut [[ARdouble; 2]; 4],
639) -> Result<(), &'static str> {
640    for i in 0..4 {
641        let w1 = ((vertex[i + 1] - vertex[i] + 1) as f64) * 0.05 + 0.5;
642        let st = (vertex[i] as f64 + w1) as usize;
643        let ed = (vertex[i + 1] as f64 - w1) as usize;
644        let n = ed - st + 1;
645        
646        let mut input = ARMat::new(n as i32, 2);
647        for j in 0..n {
648            let (ix, iy) = param_ltf.observ2ideal(x_coord[st + j] as f32, y_coord[st + j] as f32)?;
649            input.m[j * 2 + 0] = ix as f64;
650            input.m[j * 2 + 1] = iy as f64;
651        }
652
653        let mut evec = ARMat::new(2, 2);
654        let mut ev = ARVec::new(2);
655        let mut mean = ARVec::new(2);
656
657        input.pca(&mut evec, &mut ev, &mut mean)?;
658        
659        line[i][0] = evec.m[1];
660        line[i][1] = -evec.m[0];
661        line[i][2] = -(line[i][0] * mean.v[0] + line[i][1] * mean.v[1]);
662    }
663
664    for i in 0..4 {
665        let w1 = line[(i + 3) % 4][0] * line[i][1] - line[i][0] * line[(i + 3) % 4][1];
666        if w1.abs() < 0.0001 {
667            return Err("Lines are near parallel");
668        }
669        v[i][0] = (line[(i + 3) % 4][1] * line[i][2] - line[i][1] * line[(i + 3) % 4][2]) / w1;
670        v[i][1] = (line[i][0] * line[(i + 3) % 4][2] - line[(i + 3) % 4][0] * line[i][2]) / w1;
671    }
672
673    Ok(())
674}
675
676/// Resolves each square candidate into a fully identified marker.
677///
678/// Ported from `arGetMarkerInfo.c`. For every entry in `marker_info2[0..marker2_num]`:
679/// 1. Calls `param_ltf.observ2ideal` on the region centroid to get the
680///    undistorted position.
681/// 2. Calls [`ar_get_line`] to fit four edges and compute corner vertices in
682///    ideal coordinates.
683/// 3. Branches on `patt_detect_mode`:
684///    - **`AR_MATRIX_CODE_DETECTION`** (mode 2): calls
685///      [`crate::matrix::ar_matrix_code_get_id`] to decode the barcode id,
686///      writing results into `marker_info[j].{id_matrix, dir_matrix, cf_matrix}`.
687///    - **template matching modes** (mode 0 or 1): calls `ar_patt_get_image` +
688///      `pattern_match` to match against a loaded pattern set, writing into
689///      `marker_info[j].{id, dir, cf}`.
690///    - **combined mode** (mode 3): performs both branches.
691///
692/// Failures in `ar_get_line` or `observ2ideal` for a particular candidate skip
693/// that candidate silently. `*marker_num` is set to the number of valid markers.
694///
695/// # Parameters
696/// - `image` — raw pixel buffer (any supported `pixel_format`).
697/// - `patt_handle_opt` — loaded pattern database (required for template modes;
698///   pass `None` for pure matrix-code mode).
699/// - `matrix_code_type` — dimension/ECC variant used by [`crate::matrix::ar_matrix_code_get_id`].
700pub fn ar_get_marker_info(
701    image: &[u8],
702    xsize: i32,
703    ysize: i32,
704    pixel_format: crate::types::ARPixelFormat,
705    marker_info2: &[ARMarkerInfo2],
706    marker2_num: i32,
707    image_proc_mode: ImageProcMode,
708    patt_detect_mode: i32,
709    param_ltf: &ARParamLTf,
710    patt_ratio: ARdouble,
711    patt_handle_opt: Option<&crate::types::ARPattHandle>,
712    marker_info: &mut [ARMarkerInfo],
713    marker_num: &mut i32,
714    matrix_code_type: crate::types::ARMatrixCodeType,
715) -> Result<(), &'static str> {
716    let mut j = 0;
717    
718    for i in 0..marker2_num as usize {
719        marker_info[j].area = marker_info2[i].area;
720        
721        if let Ok((ix, iy)) = param_ltf.observ2ideal(marker_info2[i].pos[0] as f32, marker_info2[i].pos[1] as f32) {
722            marker_info[j].pos[0] = ix as f64;
723            marker_info[j].pos[1] = iy as f64;
724        } else {
725            continue;
726        }
727
728        if ar_get_line(
729            &marker_info2[i].x_coord,
730            &marker_info2[i].y_coord,
731            marker_info2[i].coord_num as usize,
732            &marker_info2[i].vertex,
733            param_ltf,
734            &mut marker_info[j].line,
735            &mut marker_info[j].vertex,
736        ).is_err() {
737            continue;
738        }
739
740        // Branch on detection mode
741        let is_matrix_mode = patt_detect_mode == crate::types::AR_MATRIX_CODE_DETECTION
742            || patt_detect_mode == crate::types::AR_TEMPLATE_MATCHING_COLOR_AND_MATRIX_CODE_DETECTION;
743
744        if is_matrix_mode {
745            // Decode the matrix (barcode) code
746            let mut mc_id = -1i32;
747            let mut mc_dir = -1i32;
748            let mut mc_cf = 0.0f64;
749            let mut mc_err = 0i32;
750            match crate::matrix::ar_matrix_code_get_id(
751                image,
752                xsize,
753                ysize,
754                &marker_info[j].vertex,
755                matrix_code_type,
756                pixel_format,
757                patt_ratio,
758                &mut mc_id,
759                &mut mc_dir,
760                &mut mc_cf,
761                &mut mc_err,
762            ) {
763                Ok(()) => {
764                    marker_info[j].id_matrix = mc_id;
765                    marker_info[j].dir_matrix = mc_dir;
766                    marker_info[j].cf_matrix = mc_cf;
767                    marker_info[j].error_corrected = mc_err;
768                    debug!("ar_get_marker_info: barcode id={}, dir={}, cf={:.4}", mc_id, mc_dir, mc_cf);
769                }
770                Err(e) => {
771                    debug!("ar_get_marker_info: barcode decode failed: {}", e);
772                    marker_info[j].id_matrix = -1;
773                    marker_info[j].dir_matrix = -1;
774                    marker_info[j].cf_matrix = 0.0;
775                }
776            }
777        }
778
779        if !is_matrix_mode || patt_detect_mode == crate::types::AR_TEMPLATE_MATCHING_COLOR_AND_MATRIX_CODE_DETECTION {
780            // Template matching branch
781            if let Some(patt_handle) = patt_handle_opt {
782                if patt_handle.patt_num > 0 {
783                    let patt_size = patt_handle.patt_size;
784                    let ext_patt_len = if patt_detect_mode == crate::pattern::AR_TEMPLATE_MATCHING_COLOR {
785                        (patt_size * patt_size * 3) as usize
786                    } else {
787                        (patt_size * patt_size) as usize
788                    };
789                    let mut ext_patt = vec![0u8; ext_patt_len];
790                    
791                    let res = crate::pattern::ar_patt_get_image(
792                        image_proc_mode as i32,
793                        patt_detect_mode,
794                        patt_size,
795                        patt_size * 2,
796                        image,
797                        xsize,
798                        ysize,
799                        pixel_format,
800                        &marker_info[j].vertex,
801                        patt_ratio,
802                        &mut ext_patt,
803                    );
804                    
805                    if res.is_ok() {
806                        let mut p_code = -1;
807                        let mut p_dir = 0;
808                        let mut p_cf = -1.0;
809                        let match_res = crate::pattern::pattern_match(
810                            patt_handle,
811                            patt_detect_mode,
812                            &ext_patt,
813                            patt_size,
814                            &mut p_code,
815                            &mut p_dir,
816                            &mut p_cf,
817                        );
818                        
819                        if match_res.is_ok() && p_code >= 0 {
820                            marker_info[j].id = p_code;
821                            marker_info[j].dir = p_dir;
822                            marker_info[j].cf = p_cf;
823                        } else {
824                            marker_info[j].id = -1;
825                            marker_info[j].dir = 0;
826                            marker_info[j].cf = p_cf;
827                        }
828                    } else {
829                        marker_info[j].id = -1;
830                        marker_info[j].dir = 0;
831                        marker_info[j].cf = -1.0;
832                    }
833                } else {
834                    marker_info[j].id = -1;
835                    marker_info[j].dir = 0;
836                    marker_info[j].cf = 0.0;
837                }
838            } else {
839                marker_info[j].id = -1;
840                marker_info[j].dir = 0;
841                marker_info[j].cf = 0.0;
842            }
843        }
844
845        j += 1;
846    }
847    *marker_num = j as i32;
848
849    Ok(())
850}
851
852#[cfg(test)]
853mod tests {
854    use super::*;
855
856    #[test]
857    fn test_ar_detect_marker2_empty() {
858        let mut label_info = ARLabelInfo::default();
859        let mut marker_info2 = vec![ARMarkerInfo2::default(); AR_SQUARE_MAX];
860        let mut marker2_num = 0;
861
862        let res = ar_detect_marker2(
863            640,
864            480,
865            &mut label_info,
866            ImageProcMode::FrameImage,
867            AR_AREA_MAX,
868            AR_AREA_MIN,
869            AR_SQUARE_FIT_THRESH,
870            &mut marker_info2,
871            &mut marker2_num,
872        );
873
874        assert!(res.is_ok());
875        assert_eq!(marker2_num, 0);
876    }
877}