1use bumpalo::Bump;
11use bumpalo::collections::Vec as BumpVec;
12use rayon::prelude::*;
13
14pub struct UnionFind<'a> {
16 parent: &'a mut [u32],
17 rank: &'a mut [u8],
18}
19
20impl<'a> UnionFind<'a> {
21 pub fn new_in(arena: &'a Bump, size: usize) -> Self {
23 let parent = arena.alloc_slice_fill_with(size, |i| i as u32);
24 let rank = arena.alloc_slice_fill_copy(size, 0u8);
25 Self { parent, rank }
26 }
27
28 #[inline]
30 pub fn find(&mut self, i: u32) -> u32 {
31 let mut root = i;
32 while self.parent[root as usize] != root {
33 self.parent[root as usize] = self.parent[self.parent[root as usize] as usize];
34 root = self.parent[root as usize];
35 }
36 root
37 }
38
39 #[inline]
41 pub fn union(&mut self, i: u32, j: u32) {
42 let root_i = self.find(i);
43 let root_j = self.find(j);
44 if root_i != root_j {
45 match self.rank[root_i as usize].cmp(&self.rank[root_j as usize]) {
46 std::cmp::Ordering::Less => self.parent[root_i as usize] = root_j,
47 std::cmp::Ordering::Greater => self.parent[root_j as usize] = root_i,
48 std::cmp::Ordering::Equal => {
49 self.parent[root_i as usize] = root_j;
50 self.rank[root_j as usize] += 1;
51 },
52 }
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug)]
59pub struct ComponentStats {
60 pub min_x: u16,
62 pub max_x: u16,
64 pub min_y: u16,
66 pub max_y: u16,
68 pub pixel_count: u32,
70 pub first_pixel_x: u16,
72 pub first_pixel_y: u16,
74}
75
76impl Default for ComponentStats {
77 fn default() -> Self {
78 Self {
79 min_x: u16::MAX,
80 max_x: 0,
81 min_y: u16::MAX,
82 max_y: 0,
83 pixel_count: 0,
84 first_pixel_x: 0,
85 first_pixel_y: 0,
86 }
87 }
88}
89
90pub struct LabelResult<'a> {
92 pub labels: &'a [u32],
94 pub component_stats: Vec<ComponentStats>,
96}
97
98#[derive(Clone, Copy, Debug)]
100struct Run {
101 y: u32,
102 x_start: u32,
103 x_end: u32,
104 id: u32,
105}
106
107pub fn label_components<'a>(
109 arena: &'a Bump,
110 binary: &[u8],
111 width: usize,
112 height: usize,
113 use_8_connectivity: bool,
114) -> &'a [u32] {
115 label_components_with_stats(arena, binary, width, height, use_8_connectivity).labels
116}
117
118#[allow(clippy::too_many_lines)]
120pub fn label_components_with_stats<'a>(
121 arena: &'a Bump,
122 binary: &[u8],
123 width: usize,
124 height: usize,
125 use_8_connectivity: bool,
126) -> LabelResult<'a> {
127 let all_runs: Vec<Vec<Run>> = binary
129 .par_chunks(width)
130 .enumerate()
131 .map(|(y, row)| {
132 let mut row_runs = Vec::new();
133 let mut x = 0;
134 while x < width {
135 if let Some(pos) = row[x..].iter().position(|&p| p == 0) {
137 let start = x + pos;
138 let len = row[start..].iter().take_while(|&&p| p == 0).count();
140 row_runs.push(Run {
141 y: y as u32,
142 x_start: start as u32,
143 x_end: (start + len - 1) as u32,
144 id: 0,
145 });
146 x = start + len;
147 } else {
148 break;
149 }
150 }
151 row_runs
152 })
153 .collect();
154
155 let total_runs: usize = all_runs.iter().map(std::vec::Vec::len).sum();
156 let mut runs: BumpVec<Run> = BumpVec::with_capacity_in(total_runs, arena);
157 for (id, mut run) in all_runs.into_iter().flatten().enumerate() {
158 run.id = id as u32;
159 runs.push(run);
160 }
161
162 if runs.is_empty() {
163 return LabelResult {
164 labels: arena.alloc_slice_fill_copy(width * height, 0u32),
165 component_stats: Vec::new(),
166 };
167 }
168
169 let mut uf = UnionFind::new_in(arena, runs.len());
170 let mut curr_row_range = 0..0; let mut i = 0;
172
173 while i < runs.len() {
175 let y = runs[i].y;
176
177 let start = i;
179 while i < runs.len() && runs[i].y == y {
180 i += 1;
181 }
182 let prev_row_range = curr_row_range; curr_row_range = start..i;
184
185 if y > 0 && !prev_row_range.is_empty() && runs[prev_row_range.start].y == y - 1 {
186 let mut p_idx = prev_row_range.start;
187 for c_idx in curr_row_range.clone() {
188 let curr = &runs[c_idx];
189
190 if use_8_connectivity {
191 while p_idx < prev_row_range.end && runs[p_idx].x_end + 1 < curr.x_start {
193 p_idx += 1;
194 }
195 let mut temp_p = p_idx;
196 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end + 1 {
197 uf.union(curr.id, runs[temp_p].id);
198 temp_p += 1;
199 }
200 } else {
201 while p_idx < prev_row_range.end && runs[p_idx].x_end < curr.x_start {
203 p_idx += 1;
204 }
205 let mut temp_p = p_idx;
206 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end {
207 uf.union(curr.id, runs[temp_p].id);
208 temp_p += 1;
209 }
210 }
211 }
212 }
213 }
214
215 let mut root_to_label: Vec<u32> = vec![0; runs.len()];
217 let mut component_stats: Vec<ComponentStats> = Vec::new();
218 let mut next_label = 1u32;
219
220 let mut run_roots = Vec::with_capacity(runs.len());
222
223 for run in &runs {
224 let root = uf.find(run.id) as usize;
225 run_roots.push(root);
226 if root_to_label[root] == 0 {
227 root_to_label[root] = next_label;
228 next_label += 1;
229 let new_stat = ComponentStats {
230 first_pixel_x: run.x_start as u16,
231 first_pixel_y: run.y as u16,
232 ..Default::default()
233 };
234 component_stats.push(new_stat);
235 }
236 let label_idx = (root_to_label[root] - 1) as usize;
237 let stats = &mut component_stats[label_idx];
238 stats.min_x = stats.min_x.min(run.x_start as u16);
239 stats.max_x = stats.max_x.max(run.x_end as u16);
240 stats.min_y = stats.min_y.min(run.y as u16);
241 stats.max_y = stats.max_y.max(run.y as u16);
242 stats.pixel_count += run.x_end - run.x_start + 1;
243 }
244
245 let labels = arena.alloc_slice_fill_copy(width * height, 0u32);
247 for (run, root) in runs.iter().zip(run_roots) {
248 let label = root_to_label[root];
249 let row_off = run.y as usize * width;
250 labels[row_off + run.x_start as usize..=row_off + run.x_end as usize].fill(label);
251 }
252
253 LabelResult {
254 labels,
255 component_stats,
256 }
257}
258
259#[allow(clippy::too_many_lines)]
276#[allow(clippy::cast_sign_loss)]
277#[allow(clippy::cast_possible_wrap, clippy::too_many_arguments)]
278pub fn label_components_threshold_model<'a>(
279 arena: &'a Bump,
280 grayscale: &[u8],
281 grayscale_stride: usize,
282 threshold_map: &[u8],
283 width: usize,
284 height: usize,
285 use_8_connectivity: bool,
286 min_area: u32,
287 margin: i16,
288) -> LabelResult<'a> {
289 let mut runs = BumpVec::new_in(arena);
292
293 let all_runs: Vec<Vec<Run>> = (0..height)
298 .into_par_iter()
299 .map(|y| {
300 let row_gs = &grayscale[y * grayscale_stride..y * grayscale_stride + width];
301 let row_th = &threshold_map[y * width..(y + 1) * width];
302 let mut row_runs = Vec::new();
303 let mut x = 0;
304 while x < width {
305 let gs = row_gs[x];
306 let th = row_th[x];
307 if i16::from(gs) - i16::from(th) < -margin {
308 let start = x;
309 x += 1;
310 while x < width {
311 if i16::from(row_gs[x]) - i16::from(row_th[x]) >= -margin {
312 break;
313 }
314 x += 1;
315 }
316 row_runs.push(Run {
317 y: y as u32,
318 x_start: start as u32,
319 x_end: (x - 1) as u32,
320 id: 0,
321 });
322 } else {
323 x += 1;
324 }
325 }
326 row_runs
327 })
328 .collect();
329
330 for (id, mut run) in all_runs.into_iter().flatten().enumerate() {
331 run.id = id as u32;
332 runs.push(run);
333 }
334
335 if runs.is_empty() {
336 return LabelResult {
337 labels: arena.alloc_slice_fill_copy(width * height, 0u32),
338 component_stats: Vec::new(),
339 };
340 }
341
342 let mut uf = UnionFind::new_in(arena, runs.len());
344 let mut curr_row_range = 0..0;
345 let mut i = 0;
346
347 while i < runs.len() {
348 let y = runs[i].y;
349 let start = i;
350 while i < runs.len() && runs[i].y == y {
351 i += 1;
352 }
353 let prev_row_range = curr_row_range;
354 curr_row_range = start..i;
355
356 if y > 0 && !prev_row_range.is_empty() && runs[prev_row_range.start].y == y - 1 {
357 let mut p_idx = prev_row_range.start;
358 for c_idx in curr_row_range.clone() {
359 let curr = &runs[c_idx];
360 if use_8_connectivity {
361 while p_idx < prev_row_range.end && runs[p_idx].x_end + 1 < curr.x_start {
362 p_idx += 1;
363 }
364 let mut temp_p = p_idx;
365 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end + 1 {
366 uf.union(curr.id, runs[temp_p].id);
367 temp_p += 1;
368 }
369 } else {
370 while p_idx < prev_row_range.end && runs[p_idx].x_end < curr.x_start {
371 p_idx += 1;
372 }
373 let mut temp_p = p_idx;
374 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end {
375 uf.union(curr.id, runs[temp_p].id);
376 temp_p += 1;
377 }
378 }
379 }
380 }
381 }
382
383 let mut root_to_temp_idx = vec![usize::MAX; runs.len()];
385 let mut temp_stats = Vec::new();
386
387 for run in &runs {
388 let root = uf.find(run.id) as usize;
389 if root_to_temp_idx[root] == usize::MAX {
390 root_to_temp_idx[root] = temp_stats.len();
391 temp_stats.push(ComponentStats {
392 first_pixel_x: run.x_start as u16,
393 first_pixel_y: run.y as u16,
394 ..Default::default()
395 });
396 }
397 let s_idx = root_to_temp_idx[root];
398 let stats = &mut temp_stats[s_idx];
399 stats.min_x = stats.min_x.min(run.x_start as u16);
400 stats.max_x = stats.max_x.max(run.x_end as u16);
401 stats.min_y = stats.min_y.min(run.y as u16);
402 stats.max_y = stats.max_y.max(run.y as u16);
403 stats.pixel_count += run.x_end - run.x_start + 1;
404 }
405
406 let mut component_stats = Vec::with_capacity(temp_stats.len());
408 let mut root_to_final_label = vec![0u32; runs.len()];
409 let mut next_label = 1u32;
410
411 for root in 0..runs.len() {
412 let s_idx = root_to_temp_idx[root];
413 if s_idx != usize::MAX {
414 let s = temp_stats[s_idx];
415 if s.pixel_count >= min_area {
416 component_stats.push(s);
417 root_to_final_label[root] = next_label;
418 next_label += 1;
419 }
420 }
421 }
422
423 let labels = arena.alloc_slice_fill_copy(width * height, 0u32);
425 let mut runs_by_y: Vec<Vec<(usize, usize, u32)>> = vec![Vec::new(); height];
426
427 for run in &runs {
428 let root = uf.find(run.id) as usize;
429 let label = root_to_final_label[root];
430 if label > 0 {
431 runs_by_y[run.y as usize].push((run.x_start as usize, run.x_end as usize, label));
432 }
433 }
434
435 labels
436 .par_chunks_exact_mut(width)
437 .enumerate()
438 .for_each(|(y, row)| {
439 for &(x_start, x_end, label) in &runs_by_y[y] {
440 row[x_start..=x_end].fill(label);
441 }
442 });
443
444 LabelResult {
445 labels,
446 component_stats,
447 }
448}
449
450#[cfg(test)]
451#[allow(clippy::unwrap_used, clippy::cast_sign_loss)]
452mod tests {
453 use super::*;
454 use bumpalo::Bump;
455 use proptest::prelude::*;
456
457 #[test]
458 fn test_union_find() {
459 let arena = Bump::new();
460 let mut uf = UnionFind::new_in(&arena, 10);
461
462 uf.union(1, 2);
463 uf.union(2, 3);
464 uf.union(5, 6);
465
466 assert_eq!(uf.find(1), uf.find(3));
467 assert_eq!(uf.find(1), uf.find(2));
468 assert_ne!(uf.find(1), uf.find(5));
469
470 uf.union(3, 5);
471 assert_eq!(uf.find(1), uf.find(6));
472 }
473
474 #[test]
475 fn test_label_components_simple() {
476 let arena = Bump::new();
477 let binary = [
481 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
482 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255,
483 ];
484 let width = 6;
485 let height = 6;
486
487 let result = label_components_with_stats(&arena, &binary, width, height, false);
488
489 assert_eq!(result.component_stats.len(), 2);
490
491 let s1 = result.component_stats[0];
493 assert_eq!(s1.pixel_count, 4);
494 assert_eq!(s1.min_x, 0);
495 assert_eq!(s1.max_x, 1);
496 assert_eq!(s1.min_y, 0);
497 assert_eq!(s1.max_y, 1);
498
499 let s2 = result.component_stats[1];
501 assert_eq!(s2.pixel_count, 4);
502 assert_eq!(s2.min_x, 3);
503 assert_eq!(s2.max_x, 4);
504 assert_eq!(s2.min_y, 3);
505 assert_eq!(s2.max_y, 4);
506 }
507
508 #[test]
509 fn test_segmentation_with_decimation() {
510 let arena = Bump::new();
511 let width = 32;
512 let height = 32;
513 let mut binary = vec![255u8; width * height];
514 for y in 10..20 {
516 for x in 10..20 {
517 binary[y * width + x] = 0;
518 }
519 }
520
521 let img =
523 crate::image::ImageView::new(&binary, width, height, width).expect("valid creation");
524
525 let mut decimated_data = vec![0u8; 16 * 16];
527 let decimated_img = img
528 .decimate_to(2, &mut decimated_data)
529 .expect("decimation failed");
530 let result = label_components_with_stats(&arena, decimated_img.data, 16, 16, true);
532
533 assert_eq!(result.component_stats.len(), 1);
534 let s = result.component_stats[0];
535 assert_eq!(s.pixel_count, 25);
536 assert_eq!(s.min_x, 5);
537 assert_eq!(s.max_x, 9);
538 assert_eq!(s.min_y, 5);
539 assert_eq!(s.max_y, 9);
540 }
541
542 proptest! {
543 #[test]
544 fn prop_union_find_reflexivity(size in 1..1000usize) {
545 let arena = Bump::new();
546 let mut uf = UnionFind::new_in(&arena, size);
547 for i in 0..size as u32 {
548 assert_eq!(uf.find(i), i);
549 }
550 }
551
552 #[test]
553 fn prop_union_find_transitivity(size in 1..1000usize, pairs in prop::collection::vec((0..1000u32, 0..1000u32), 0..100)) {
554 let arena = Bump::new();
555 let real_size = size.max(1001); let mut uf = UnionFind::new_in(&arena, real_size);
557
558 for (a, b) in pairs {
559 let a = a % real_size as u32;
560 let b = b % real_size as u32;
561 uf.union(a, b);
562 assert_eq!(uf.find(a), uf.find(b));
563 }
564 }
565
566 #[test]
567 fn prop_label_components_no_panic(
568 width in 1..64usize,
569 height in 1..64usize,
570 data in prop::collection::vec(0..=1u8, 64 * 64)
571 ) {
572 let arena = Bump::new();
573 let binary: Vec<u8> = data.iter().map(|&b| if b == 0 { 0 } else { 255 }).collect();
574 let real_width = width.min(64);
575 let real_height = height.min(64);
576 let slice = &binary[..real_width * real_height];
577
578 let result = label_components_with_stats(&arena, slice, real_width, real_height, true);
579
580 for stat in result.component_stats {
581 assert!(stat.pixel_count > 0);
582 assert!(stat.max_x < real_width as u16);
583 assert!(stat.max_y < real_height as u16);
584 assert!(stat.min_x <= stat.max_x);
585 assert!(stat.min_y <= stat.max_y);
586 }
587 }
588 }
589
590 use crate::config::TagFamily;
595 use crate::image::ImageView;
596 use crate::test_utils::{TestImageParams, generate_test_image_with_params};
597 use crate::threshold::ThresholdEngine;
598
599 fn generate_binarized_tag(tag_size: usize, canvas_size: usize) -> (Vec<u8>, [[f64; 2]; 4]) {
601 let params = TestImageParams {
602 family: TagFamily::AprilTag36h11,
603 id: 0,
604 tag_size,
605 canvas_size,
606 ..Default::default()
607 };
608
609 let (data, corners) = generate_test_image_with_params(¶ms);
610 let img = ImageView::new(&data, canvas_size, canvas_size, canvas_size).unwrap();
611
612 let arena = Bump::new();
613 let engine = ThresholdEngine::new();
614 let stats = engine.compute_tile_stats(&arena, &img);
615 let mut binary = vec![0u8; canvas_size * canvas_size];
616 engine.apply_threshold(&arena, &img, &stats, &mut binary);
617
618 (binary, corners)
619 }
620
621 #[test]
623 fn test_segmentation_at_varying_tag_sizes() {
624 let canvas_size = 640;
625 let tag_sizes = [32, 64, 100, 200, 300];
626
627 for tag_size in tag_sizes {
628 let arena = Bump::new();
629 let (binary, corners) = generate_binarized_tag(tag_size, canvas_size);
630
631 let result =
632 label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
633
634 assert!(
635 !result.component_stats.is_empty(),
636 "Tag size {tag_size}: No components found"
637 );
638
639 let largest = result
640 .component_stats
641 .iter()
642 .max_by_key(|s| s.pixel_count)
643 .unwrap();
644
645 let expected_min_x = corners[0][0] as u16;
646 let expected_max_x = corners[1][0] as u16;
647 let tolerance = 5;
648
649 assert!(
650 (i32::from(largest.min_x) - i32::from(expected_min_x)).abs() <= tolerance,
651 "Tag size {tag_size}: min_x mismatch"
652 );
653 assert!(
654 (i32::from(largest.max_x) - i32::from(expected_max_x)).abs() <= tolerance,
655 "Tag size {tag_size}: max_x mismatch"
656 );
657
658 println!(
659 "Tag size {:>3}px: {} components, largest has {} px",
660 tag_size,
661 result.component_stats.len(),
662 largest.pixel_count
663 );
664 }
665 }
666
667 #[test]
669 fn test_segmentation_component_accuracy() {
670 let canvas_size = 320;
671 let tag_size = 120;
672
673 let arena = Bump::new();
674 let (binary, corners) = generate_binarized_tag(tag_size, canvas_size);
675
676 let result = label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
677
678 let largest = result
679 .component_stats
680 .iter()
681 .max_by_key(|s| s.pixel_count)
682 .unwrap();
683
684 let expected_min = (tag_size * tag_size / 3) as u32;
685 let expected_max = (tag_size * tag_size) as u32;
686
687 assert!(largest.pixel_count >= expected_min);
688 assert!(largest.pixel_count <= expected_max);
689
690 let gt_width = (corners[1][0] - corners[0][0]).abs() as i32;
691 let gt_height = (corners[2][1] - corners[0][1]).abs() as i32;
692 let bbox_width = i32::from(largest.max_x - largest.min_x);
693 let bbox_height = i32::from(largest.max_y - largest.min_y);
694
695 assert!((bbox_width - gt_width).abs() <= 2);
696 assert!((bbox_height - gt_height).abs() <= 2);
697
698 println!(
699 "Component accuracy: {} pixels, bbox={}x{} (GT: {}x{})",
700 largest.pixel_count, bbox_width, bbox_height, gt_width, gt_height
701 );
702 }
703
704 #[test]
706 fn test_segmentation_noisy_boundaries() {
707 use rand::prelude::*;
708
709 let canvas_size = 320;
710 let tag_size = 120;
711
712 let arena = Bump::new();
713 let (mut binary, _corners) = generate_binarized_tag(tag_size, canvas_size);
714
715 let mut rng = rand::thread_rng();
716 let noise_rate = 0.05;
717
718 for y in 1..(canvas_size - 1) {
719 for x in 1..(canvas_size - 1) {
720 let idx = y * canvas_size + x;
721 let current = binary[idx];
722 let left = binary[idx - 1];
723 let right = binary[idx + 1];
724 let up = binary[idx - canvas_size];
725 let down = binary[idx + canvas_size];
726
727 let is_edge =
728 current != left || current != right || current != up || current != down;
729 if is_edge && rng.gen_range(0.0..1.0_f32) < noise_rate {
730 binary[idx] = if current == 0 { 255 } else { 0 };
731 }
732 }
733 }
734
735 let result = label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
736
737 assert!(!result.component_stats.is_empty());
738
739 let largest = result
740 .component_stats
741 .iter()
742 .max_by_key(|s| s.pixel_count)
743 .unwrap();
744
745 let min_expected = (tag_size * tag_size / 4) as u32;
746 assert!(largest.pixel_count >= min_expected);
747
748 println!(
749 "Noisy segmentation: {} components, largest has {} px",
750 result.component_stats.len(),
751 largest.pixel_count
752 );
753 }
754
755 #[test]
756 fn test_segmentation_correctness_large_image() {
757 let arena = Bump::new();
758 let width = 3840; let height = 2160; let mut binary = vec![255u8; width * height];
761
762 for y in 100..200 {
765 for x in 100..200 {
766 binary[y * width + x] = 0;
767 }
768 }
769
770 for x in 500..3000 {
772 binary[1000 * width + x] = 0;
773 binary[1001 * width + x] = 0;
774 }
775
776 for y in 1800..2000 {
778 if y % 4 == 0 {
779 for x in 1800..2000 {
780 binary[y * width + x] = 0;
781 }
782 }
783 }
784
785 for y in 0..height {
787 if y % 10 == 0 {
788 for x in 0..width {
789 if (!(100..200).contains(&x) || !(100..200).contains(&y)) && (x + y) % 31 == 0 {
790 binary[y * width + x] = 0;
791 }
792 }
793 }
794 }
795
796 let start = std::time::Instant::now();
798 let result = label_components_with_stats(&arena, &binary, width, height, true);
799 let duration = start.elapsed();
800
801 assert!(result.component_stats.len() > 1000);
803
804 println!(
805 "Found {} components on 4K image in {:?}",
806 result.component_stats.len(),
807 duration
808 );
809 }
810}