Skip to main content

webarkitlib_rs/
labeling.rs

1/*
2 *  labeling.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//! Connected Component Labeling Utilities
38//! Translated from ARToolKit C headers (arLabeling.c, arLabelingSub.h)
39
40use crate::types::{ARLabelInfo, ARdouble};
41use log::trace;
42
43pub const AR_LABELING_WORK_SIZE: usize = 1024 * 32;
44
45#[derive(Debug, PartialEq, Clone, Copy)]
46pub enum LabelingMode {
47    BlackRegion,
48    WhiteRegion,
49}
50
51#[derive(Debug, PartialEq, Clone, Copy)]
52pub enum ImageProcMode {
53    FrameImage,
54    FieldImage, // Handles interlaced half-resolution fields
55}
56
57/// Perform connected-component labeling on a grayscale (luma) image.
58///
59/// Ported from the family of `arLabelingSub*` C macros in ARToolKit.
60/// All ~14 macro instantiations (black/white × frame/field × pixel-depth) are
61/// collapsed into a single generic Rust function parameterised by `mode` and
62/// `proc_mode`.
63///
64/// ## Algorithm
65/// Single-pass raster scan with union-find equivalence table:
66/// 1. Each pixel that satisfies the threshold test (`≤ thresh` for
67///    [`LabelingMode::BlackRegion`], `> thresh` for [`LabelingMode::WhiteRegion`])
68///    is provisionally assigned the label of its already-scanned neighbour, or a new
69///    label if none exist.
70/// 2. Conflicting neighbour labels are merged via a union-find structure stored in
71///    `label_info.work`.
72/// 3. A second pass resolves equivalences and accumulates per-label statistics
73///    (area, centroid, bounding clip) into `label_info`.
74///
75/// ## Parameters
76/// - `image` — row-major 8-bit luma buffer of length `xsize * ysize`.
77/// - `mode` — whether dark or bright regions are treated as foreground.
78/// - `thresh` — binarisation threshold (0–255).
79/// - `proc_mode` — `FrameImage` (full resolution) or `FieldImage`
80///   (half-resolution interlaced; dimensions are halved internally).
81/// - `label_info` — output struct; resized automatically if needed.
82///
83/// # Example
84/// ```rust,no_run
85/// use webarkitlib_rs::labeling::{ar_labeling, LabelingMode, ImageProcMode};
86/// use webarkitlib_rs::types::ARLabelInfo;
87///
88/// let width = 320i32;
89/// let height = 240i32;
90/// let luma = vec![0u8; (width * height) as usize];
91/// let mut label_info = ARLabelInfo::default();
92/// ar_labeling(&luma, width, height, LabelingMode::BlackRegion, 100,
93///             ImageProcMode::FrameImage, &mut label_info, false).unwrap();
94/// println!("{} regions found", label_info.label_num);
95/// ```
96pub fn ar_labeling(
97    image: &[u8],
98    xsize: i32,
99    ysize: i32,
100    mode: LabelingMode,
101    thresh: u8,
102    proc_mode: ImageProcMode,
103    label_info: &mut ARLabelInfo,
104    _debug_mode: bool,
105) -> Result<(), &'static str> {
106    
107    let is_region = |pixel: u8| -> bool {
108        match mode {
109            LabelingMode::BlackRegion => pixel <= thresh,
110            LabelingMode::WhiteRegion => pixel > thresh,
111        }
112    };
113
114    let lxsize: usize;
115    let lysize: usize;
116    let row_stride: usize;
117    let _col_stride: usize;
118
119    match proc_mode {
120        ImageProcMode::FrameImage => {
121            lxsize = xsize as usize;
122            lysize = ysize as usize;
123            row_stride = xsize as usize;
124            _col_stride = 1;
125        }
126        ImageProcMode::FieldImage => {
127            lxsize = (xsize / 2) as usize;
128            lysize = (ysize / 2) as usize;
129            row_stride = (xsize * 2) as usize; // skip every other row
130            if ysize < 480 { // Assuming 'height' in the snippet refers to ysize
131                _col_stride = 1;
132            } else {
133                _col_stride = 2; // skip every other column
134            }
135        }
136    }
137
138    if image.len() < (ysize as usize * xsize as usize) {
139        return Err("Image buffer is too small for given dimensions.");
140    }
141
142    // Ensure the label_image buffer is perfectly sized
143    if label_info.label_image.len() < lxsize * lysize {
144        label_info.label_image.resize(lxsize * lysize, 0);
145    }
146    label_info.label_image.fill(0);
147    trace!("ar_labeling: lxsize={}, lysize={}, len={}", lxsize, lysize, label_info.label_image.len());
148
149    // Initialize work equivalence arrays
150    if label_info.work.len() < AR_LABELING_WORK_SIZE {
151        label_info.work.resize(AR_LABELING_WORK_SIZE, 0);
152    }
153    let mut wk_max: usize = 0;
154    let work = &mut label_info.work;
155    // We'll use a local work2 of i64 to avoid overflow during summation
156    let mut work2 = vec![0i64; AR_LABELING_WORK_SIZE * 7];
157    
158    // Initialize xmin, ymin to large values
159    for k in 0..AR_LABELING_WORK_SIZE {
160        work2[k * 7 + 3] = xsize as i64; // xmin
161        work2[k * 7 + 5] = ysize as i64; // ymin
162    }
163    
164    let label_img = &mut label_info.label_image;
165
166    // Helper for Union-Find find with path compression
167    fn find(work: &mut [i32], i: i32) -> i32 {
168        let mut root = i;
169        while work[root as usize - 1] != root {
170            root = work[root as usize - 1];
171        }
172        // Path compression
173        let mut curr = i;
174        while work[curr as usize - 1] != root {
175            let next = work[curr as usize - 1];
176            work[curr as usize - 1] = root;
177            curr = next;
178        }
179        root
180    }
181
182    // Helper for Union-Find union with immediate data transfer
183    fn do_union(work: &mut [i32], work2: &mut [i64], m: i32, n: i32) -> i32 {
184        let root_m = find(work, m);
185        let root_n = find(work, n);
186        if root_m != root_n {
187            if root_m < root_n {
188                work[root_n as usize - 1] = root_m;
189                // Transfer data
190                let m_idx = (root_m as usize - 1) * 7;
191                let n_idx = (root_n as usize - 1) * 7;
192                for i in 0..3 { work2[m_idx + i] += work2[n_idx + i]; }
193                if work2[m_idx + 3] > work2[n_idx + 3] { work2[m_idx + 3] = work2[n_idx + 3]; }
194                if work2[m_idx + 4] < work2[n_idx + 4] { work2[m_idx + 4] = work2[n_idx + 4]; }
195                if work2[m_idx + 5] > work2[n_idx + 5] { work2[m_idx + 5] = work2[n_idx + 5]; }
196                if work2[m_idx + 6] < work2[n_idx + 6] { work2[m_idx + 6] = work2[n_idx + 6]; }
197                // Clear old root (optional)
198                for i in 0..7 { work2[n_idx + i] = 0; }
199                root_m
200            } else {
201                work[root_m as usize - 1] = root_n;
202                // Transfer data
203                let m_idx = (root_m as usize - 1) * 7;
204                let n_idx = (root_n as usize - 1) * 7;
205                for i in 0..3 { work2[n_idx + i] += work2[m_idx + i]; }
206                if work2[n_idx + 3] > work2[m_idx + 3] { work2[n_idx + 3] = work2[m_idx + 3]; }
207                if work2[n_idx + 4] < work2[m_idx + 4] { work2[n_idx + 4] = work2[m_idx + 4]; }
208                if work2[n_idx + 5] > work2[m_idx + 5] { work2[n_idx + 5] = work2[m_idx + 5]; }
209                if work2[n_idx + 6] < work2[m_idx + 6] { work2[n_idx + 6] = work2[m_idx + 6]; }
210                // Clear old root
211                for i in 0..7 { work2[m_idx + i] = 0; }
212                root_n
213            }
214        } else {
215            root_m
216        }
217    }
218
219    // Scan the inner pixel region (skip boundaries 0, and max-1)
220    for j in 1..lysize - 1 {
221        for i in 1..lxsize - 1 {
222            let source_idx = match proc_mode {
223                ImageProcMode::FrameImage => j * row_stride + i,
224                ImageProcMode::FieldImage => (j * 2) * xsize as usize + (i * 2),
225            };
226            let pixel = image[source_idx];
227
228            let p_idx = j * lxsize + i;
229
230            if is_region(pixel) {
231                let left_val = label_img[p_idx - 1];
232                let up_left_val = label_img[p_idx - lxsize - 1];
233                let up_val = label_img[p_idx - lxsize];
234                let up_right_val = label_img[p_idx - lxsize + 1];
235
236                let mut neighbors = [0; 4];
237                let mut n_count = 0;
238                if left_val > 0 { neighbors[n_count] = left_val as i32; n_count += 1; }
239                if up_left_val > 0 { neighbors[n_count] = up_left_val as i32; n_count += 1; }
240                if up_val > 0 { neighbors[n_count] = up_val as i32; n_count += 1; }
241                if up_right_val > 0 { neighbors[n_count] = up_right_val as i32; n_count += 1; }
242
243                if n_count > 0 {
244                    // Sort and unique neighbors
245                    neighbors[0..n_count].sort_unstable();
246                    let mut unique_count = 1;
247                    for k in 1..n_count {
248                        if neighbors[k] != neighbors[k-1] {
249                            neighbors[unique_count] = neighbors[k];
250                            unique_count += 1;
251                        }
252                    }
253                    n_count = unique_count;
254
255                    let mut final_label = neighbors[0];
256                    for k in 1..n_count {
257                        if neighbors[k] != final_label {
258                            final_label = do_union(work, &mut work2, final_label, neighbors[k]);
259                        }
260                    }
261                    label_img[p_idx] = final_label as crate::types::ARLabelingLabelType;
262                    
263                    let l = (final_label as usize - 1) * 7;
264                    work2[l + 0] += 1;
265                    work2[l + 1] += i as i64;
266                    work2[l + 2] += j as i64;
267                    if work2[l + 3] > i as i64 { work2[l + 3] = i as i64; }
268                    if work2[l + 4] < i as i64 { work2[l + 4] = i as i64; }
269                    if work2[l + 5] > j as i64 { work2[l + 5] = j as i64; }
270                    if work2[l + 6] < j as i64 { work2[l + 6] = j as i64; }
271                } else {
272                    wk_max += 1;
273                    if wk_max > AR_LABELING_WORK_SIZE {
274                        return Err("Labeling work array overflow");
275                    }
276                    work[wk_max - 1] = wk_max as i32;
277                    label_img[p_idx] = wk_max as crate::types::ARLabelingLabelType;
278
279                    let l = (wk_max - 1) * 7;
280                    work2[l + 0] = 1;         // area
281                    work2[l + 1] = i as i64;  // pos[0]
282                    work2[l + 2] = j as i64;  // pos[1]
283                    work2[l + 3] = i as i64;  // clip[0] (xmin)
284                    work2[l + 4] = i as i64;  // clip[1] (xmax)
285                    work2[l + 5] = j as i64;  // clip[2] (ymin)
286                    work2[l + 6] = j as i64;  // clip[3] (ymax)
287                    
288                    if wk_max == 32 || wk_max == 41 {
289                        trace!("Label {} created at ({}, {})", wk_max, i, j);
290                    }
291                }
292            } else {
293                label_img[p_idx] = 0;
294            }
295        }
296    }
297
298    // Pass 2: Map equivalence table down to dense sequential indexes
299    let mut num_labels = 0;
300    // label_map[original_label - 1] = dense_id
301    let mut label_map = vec![0; wk_max];
302    
303    // Pass 2a: Assign dense IDs to roots
304    for i in 1..=wk_max {
305        if work[i - 1] == i as i32 { // It's a root
306            num_labels += 1;
307            label_map[i - 1] = num_labels;
308        }
309    }
310    
311    // Pass 2b: Map all original labels back to dense IDs via their roots
312    for i in 1..=wk_max {
313        let root = find(work, i as i32) as usize;
314        label_map[i - 1] = label_map[root - 1];
315    }
316    
317    label_info.label_num = num_labels;
318    if label_info.label_num == 0 {
319        return Ok(());
320    }
321
322    // Pass 3: Update label_image with final labels
323    for i in 0..label_img.len() {
324        if label_img[i] > 0 {
325            label_img[i] = label_map[label_img[i] as usize - 1] as crate::types::ARLabelingLabelType;
326        }
327    }
328
329    // Finalize label info
330    // Transfer from work2 back to label_info
331    
332    // Actually, we can just iterate the work2 table and match by finalized label
333    if label_info.area.len() < num_labels as usize { label_info.area.resize(num_labels as usize, 0); }
334    if label_info.pos.len() < num_labels as usize { label_info.pos.resize(num_labels as usize, [0.0; 2]); }
335    if label_info.clip.len() < num_labels as usize { label_info.clip.resize(num_labels as usize, [0; 4]); }
336
337    label_info.area.fill(0);
338    for v in label_info.pos.iter_mut() { v.fill(0.0); }
339    for v in label_info.clip.iter_mut() { 
340        v[0] = lxsize as i32; v[1] = 0; v[2] = lysize as i32; v[3] = 0; 
341    }
342
343    // Let's redo the finalization loop more cleanly.
344    // We'll iterate all ROOTs and move their data to the dense slots.
345    // Redo finalization: only process ROOTS
346    for k in 1..=wk_max {
347        if work[k - 1] == k as i32 { // It's a root
348            let src = (k - 1) * 7;
349            if work2[src + 0] > 0 {
350                let dense_idx = label_map[k - 1];
351                if dense_idx > 0 {
352                    let i = (dense_idx - 1) as usize;
353                    label_info.area[i] = work2[src + 0] as i32;
354                    label_info.pos[i][0] = work2[src + 1] as ARdouble;
355                    label_info.pos[i][1] = work2[src + 2] as ARdouble;
356                    label_info.clip[i][0] = work2[src + 3] as i32;
357                    label_info.clip[i][1] = work2[src + 4] as i32;
358                    label_info.clip[i][2] = work2[src + 5] as i32;
359                    label_info.clip[i][3] = work2[src + 6] as i32;
360                }
361            }
362        }
363    }
364
365    for i in 0..num_labels as usize {
366        if label_info.area[i] > 0 {
367            label_info.pos[i][0] /= label_info.area[i] as ARdouble;
368            label_info.pos[i][1] /= label_info.area[i] as ARdouble;
369        }
370    }
371
372    Ok(())
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_ar_labeling_two_squares() {
381        // Create an 20x20 image where two 5x5 squares are separate.
382        // S1: (2,2) to (6,6)
383        // S2: (12,12) to (16,16)
384        let mut image = vec![255u8; 400];
385        for j in 2..7 {
386            for i in 2..7 {
387                image[j * 20 + i] = 0;
388            }
389        }
390        for j in 12..17 {
391            for i in 12..17 {
392                image[j * 20 + i] = 0;
393            }
394        }
395
396        let mut info = crate::types::ARLabelInfo::default();
397
398        ar_labeling(
399            &image,
400            20,
401            20,
402            LabelingMode::BlackRegion,
403            100,
404            ImageProcMode::FrameImage,
405            &mut info,
406            false
407        ).unwrap();
408
409        assert_eq!(info.label_num, 2);
410        
411        let mut found_s1 = false;
412        let mut found_s2 = false;
413        
414        for i in 0..info.label_num as usize {
415            if info.area[i] == 25 {
416                if info.clip[i][0] == 2 && info.clip[i][1] == 6 && info.clip[i][2] == 2 && info.clip[i][3] == 6 {
417                    found_s1 = true;
418                } else if info.clip[i][0] == 12 && info.clip[i][1] == 16 && info.clip[i][2] == 12 && info.clip[i][3] == 16 {
419                    found_s2 = true;
420                }
421            }
422        }
423        
424        assert!(found_s1, "Square 1 (2,2)-(6,6) not correctly found. Area={:?}, Clip={:?}", info.area, info.clip);
425        assert!(found_s2, "Square 2 (12,12)-(16,16) not correctly found. Area={:?}, Clip={:?}", info.area, info.clip);
426    }
427}