1#![allow(unsafe_code)]
11
12use bumpalo::Bump;
13use bumpalo::collections::Vec as BumpVec;
14use rayon::prelude::*;
15
16pub struct UnionFind<'a> {
18 parent: &'a mut [u32],
19 rank: &'a mut [u8],
20}
21
22impl<'a> UnionFind<'a> {
23 pub fn new_in(arena: &'a Bump, size: usize) -> Self {
25 let parent = arena.alloc_slice_fill_with(size, |i| i as u32);
26 let rank = arena.alloc_slice_fill_copy(size, 0u8);
27 Self { parent, rank }
28 }
29
30 #[inline]
32 pub fn find(&mut self, i: u32) -> u32 {
33 let mut root = i;
34 while self.parent[root as usize] != root {
35 self.parent[root as usize] = self.parent[self.parent[root as usize] as usize];
36 root = self.parent[root as usize];
37 }
38 root
39 }
40
41 #[inline]
43 pub fn union(&mut self, i: u32, j: u32) {
44 let root_i = self.find(i);
45 let root_j = self.find(j);
46 if root_i != root_j {
47 match self.rank[root_i as usize].cmp(&self.rank[root_j as usize]) {
48 std::cmp::Ordering::Less => self.parent[root_i as usize] = root_j,
49 std::cmp::Ordering::Greater => self.parent[root_j as usize] = root_i,
50 std::cmp::Ordering::Equal => {
51 self.parent[root_i as usize] = root_j;
52 self.rank[root_j as usize] += 1;
53 },
54 }
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug)]
61pub struct ComponentStats {
62 pub min_x: u16,
64 pub max_x: u16,
66 pub min_y: u16,
68 pub max_y: u16,
70 pub pixel_count: u32,
72 pub first_pixel_x: u16,
74 pub first_pixel_y: u16,
76 pub m10: u64,
78 pub m01: u64,
80 pub m20: u64,
82 pub m02: u64,
84 pub m11: u64,
86}
87
88impl Default for ComponentStats {
89 fn default() -> Self {
90 Self {
91 min_x: u16::MAX,
92 max_x: 0,
93 min_y: u16::MAX,
94 max_y: 0,
95 pixel_count: 0,
96 first_pixel_x: 0,
97 first_pixel_y: 0,
98 m10: 0,
99 m01: 0,
100 m20: 0,
101 m02: 0,
102 m11: 0,
103 }
104 }
105}
106
107#[must_use]
116pub fn compute_moment_shape(stats: &ComponentStats) -> Option<(f64, f64)> {
117 let m00 = f64::from(stats.pixel_count);
118 if m00 < 1.0 {
119 return None;
120 }
121
122 let x_bar = stats.m10 as f64 / m00;
123 let y_bar = stats.m01 as f64 / m00;
124
125 let mu20 = stats.m20 as f64 - x_bar * stats.m10 as f64;
127 let mu02 = stats.m02 as f64 - y_bar * stats.m01 as f64;
128 let mu11 = stats.m11 as f64 - x_bar * stats.m01 as f64;
129
130 let sum = mu20 + mu02;
133 let diff = mu20 - mu02;
134 let disc = (diff * diff + 4.0 * mu11 * mu11).sqrt();
135 let lambda_max = (sum + disc) / (2.0 * m00);
136 let lambda_min = (sum - disc) / (2.0 * m00);
137
138 if lambda_min < 1e-9 {
139 return None;
140 }
141
142 let elongation = lambda_max / lambda_min;
143
144 let bbox_w = u32::from(stats.max_x - stats.min_x) + 1;
145 let bbox_h = u32::from(stats.max_y - stats.min_y) + 1;
146 let bbox_area = f64::from(bbox_w * bbox_h);
147 let density = m00 / bbox_area;
148
149 Some((elongation, density))
150}
151
152pub struct LabelResult<'a> {
154 pub labels: &'a [u32],
156 pub component_stats: Vec<ComponentStats>,
158}
159
160#[derive(Clone, Copy, Debug)]
162struct Run {
163 y: u32,
164 x_start: u32,
165 x_end: u32,
166 id: u32,
167}
168
169pub fn label_components<'a>(
171 arena: &'a Bump,
172 binary: &[u8],
173 width: usize,
174 height: usize,
175 use_8_connectivity: bool,
176) -> &'a [u32] {
177 label_components_with_stats(arena, binary, width, height, use_8_connectivity).labels
178}
179
180#[allow(clippy::too_many_lines)]
182pub fn label_components_with_stats<'a>(
183 arena: &'a Bump,
184 binary: &[u8],
185 width: usize,
186 height: usize,
187 use_8_connectivity: bool,
188) -> LabelResult<'a> {
189 let all_runs: Vec<Vec<Run>> = binary
191 .par_chunks(width)
192 .enumerate()
193 .map(|(y, row)| {
194 let mut row_runs = Vec::with_capacity(width / 4 + 1);
195 let mut x = 0;
196 while x < width {
197 if let Some(pos) = row[x..].iter().position(|&p| p == 0) {
199 let start = x + pos;
200 let len = row[start..].iter().take_while(|&&p| p == 0).count();
202 row_runs.push(Run {
203 y: y as u32,
204 x_start: start as u32,
205 x_end: (start + len - 1) as u32,
206 id: 0,
207 });
208 x = start + len;
209 } else {
210 break;
211 }
212 }
213 row_runs
214 })
215 .collect();
216
217 let total_runs: usize = all_runs.iter().map(std::vec::Vec::len).sum();
218 let mut runs: BumpVec<Run> = BumpVec::with_capacity_in(total_runs, arena);
219 for (id, mut run) in all_runs.into_iter().flatten().enumerate() {
220 run.id = id as u32;
221 runs.push(run);
222 }
223
224 if runs.is_empty() {
225 return LabelResult {
226 labels: arena.alloc_slice_fill_copy(width * height, 0u32),
227 component_stats: Vec::new(),
228 };
229 }
230
231 let mut uf = UnionFind::new_in(arena, runs.len());
232 let mut curr_row_range = 0..0; let mut i = 0;
234
235 while i < runs.len() {
237 let y = runs[i].y;
238
239 let start = i;
241 while i < runs.len() && runs[i].y == y {
242 i += 1;
243 }
244 let prev_row_range = curr_row_range; curr_row_range = start..i;
246
247 if y > 0 && !prev_row_range.is_empty() && runs[prev_row_range.start].y == y - 1 {
248 let mut p_idx = prev_row_range.start;
249 for c_idx in curr_row_range.clone() {
250 let curr = &runs[c_idx];
251
252 if use_8_connectivity {
253 while p_idx < prev_row_range.end && runs[p_idx].x_end + 1 < curr.x_start {
255 p_idx += 1;
256 }
257 let mut temp_p = p_idx;
258 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end + 1 {
259 uf.union(curr.id, runs[temp_p].id);
260 temp_p += 1;
261 }
262 } else {
263 while p_idx < prev_row_range.end && runs[p_idx].x_end < curr.x_start {
265 p_idx += 1;
266 }
267 let mut temp_p = p_idx;
268 while temp_p < prev_row_range.end && runs[temp_p].x_start <= curr.x_end {
269 uf.union(curr.id, runs[temp_p].id);
270 temp_p += 1;
271 }
272 }
273 }
274 }
275 }
276
277 let mut root_to_label: Vec<u32> = vec![0; runs.len()];
279 let mut component_stats: Vec<ComponentStats> = Vec::new();
280 let mut next_label = 1u32;
281
282 let mut run_roots = Vec::with_capacity(runs.len());
284
285 for run in &runs {
286 let root = uf.find(run.id) as usize;
287 run_roots.push(root);
288 if root_to_label[root] == 0 {
289 root_to_label[root] = next_label;
290 next_label += 1;
291 let new_stat = ComponentStats {
292 first_pixel_x: run.x_start as u16,
293 first_pixel_y: run.y as u16,
294 ..Default::default()
295 };
296 component_stats.push(new_stat);
297 }
298 let label_idx = (root_to_label[root] - 1) as usize;
299 let stats = &mut component_stats[label_idx];
300 stats.min_x = stats.min_x.min(run.x_start as u16);
301 stats.max_x = stats.max_x.max(run.x_end as u16);
302 stats.min_y = stats.min_y.min(run.y as u16);
303 stats.max_y = stats.max_y.max(run.y as u16);
304 stats.pixel_count += run.x_end - run.x_start + 1;
305 let a = u64::from(run.x_start);
308 let b = u64::from(run.x_end) + 1; let yu = u64::from(run.y);
310 stats.m10 += b * (b - 1) / 2 - a * a.saturating_sub(1) / 2;
314 stats.m01 += yu * (b - a);
315 stats.m20 +=
316 (b - 1) * b * (2 * b - 1) / 6 - a.saturating_sub(1) * a * (2 * a).saturating_sub(1) / 6;
317 stats.m02 += yu * yu * (b - a);
318 stats.m11 += yu * (b * (b - 1) / 2 - a * a.saturating_sub(1) / 2);
319 }
320
321 let labels = arena.alloc_slice_fill_copy(width * height, 0u32);
323 for (run, root) in runs.iter().zip(run_roots) {
324 let label = root_to_label[root];
325 let row_off = run.y as usize * width;
326 labels[row_off + run.x_start as usize..=row_off + run.x_end as usize].fill(label);
327 }
328
329 LabelResult {
330 labels,
331 component_stats,
332 }
333}
334#[cfg(test)]
335#[allow(clippy::unwrap_used, clippy::cast_sign_loss)]
336mod tests {
337 use super::*;
338 use bumpalo::Bump;
339 use proptest::prelude::prop;
340 use proptest::proptest;
341
342 #[test]
343 fn test_union_find() {
344 let arena = Bump::new();
345 let mut uf = UnionFind::new_in(&arena, 10);
346
347 uf.union(1, 2);
348 uf.union(2, 3);
349 uf.union(5, 6);
350
351 assert_eq!(uf.find(1), uf.find(3));
352 assert_eq!(uf.find(1), uf.find(2));
353 assert_ne!(uf.find(1), uf.find(5));
354
355 uf.union(3, 5);
356 assert_eq!(uf.find(1), uf.find(6));
357 }
358
359 #[test]
360 fn test_label_components_simple() {
361 let arena = Bump::new();
362 let binary = [
366 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
367 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255,
368 ];
369 let width = 6;
370 let height = 6;
371
372 let result = label_components_with_stats(&arena, &binary, width, height, false);
373
374 assert_eq!(result.component_stats.len(), 2);
375
376 let s1 = result.component_stats[0];
378 assert_eq!(s1.pixel_count, 4);
379 assert_eq!(s1.min_x, 0);
380 assert_eq!(s1.max_x, 1);
381 assert_eq!(s1.min_y, 0);
382 assert_eq!(s1.max_y, 1);
383
384 let s2 = result.component_stats[1];
386 assert_eq!(s2.pixel_count, 4);
387 assert_eq!(s2.min_x, 3);
388 assert_eq!(s2.max_x, 4);
389 assert_eq!(s2.min_y, 3);
390 assert_eq!(s2.max_y, 4);
391 }
392
393 #[test]
394 fn test_segmentation_with_decimation() {
395 let arena = Bump::new();
396 let width = 32;
397 let height = 32;
398 let mut binary = vec![255u8; width * height];
399 for y in 10..20 {
401 for x in 10..20 {
402 binary[y * width + x] = 0;
403 }
404 }
405
406 let img =
408 crate::image::ImageView::new(&binary, width, height, width).expect("valid creation");
409
410 let mut decimated_data = vec![0u8; 16 * 16];
412 let decimated_img = img
413 .decimate_to(2, &mut decimated_data)
414 .expect("decimation failed");
415 let result = label_components_with_stats(&arena, decimated_img.data, 16, 16, true);
417
418 assert_eq!(result.component_stats.len(), 1);
419 let s = result.component_stats[0];
420 assert_eq!(s.pixel_count, 25);
421 assert_eq!(s.min_x, 5);
422 assert_eq!(s.max_x, 9);
423 assert_eq!(s.min_y, 5);
424 assert_eq!(s.max_y, 9);
425 }
426
427 proptest! {
428 #[test]
429 fn prop_union_find_reflexivity(size in 1..1000usize) {
430 let arena = Bump::new();
431 let mut uf = UnionFind::new_in(&arena, size);
432 for i in 0..size as u32 {
433 assert_eq!(uf.find(i), i);
434 }
435 }
436
437 #[test]
438 fn prop_union_find_transitivity(size in 1..1000usize, pairs in prop::collection::vec((0..1000u32, 0..1000u32), 0..100)) {
439 let arena = Bump::new();
440 let real_size = size.max(1001); let mut uf = UnionFind::new_in(&arena, real_size);
442
443 for (a, b) in pairs {
444 let a = a % real_size as u32;
445 let b = b % real_size as u32;
446 uf.union(a, b);
447 assert_eq!(uf.find(a), uf.find(b));
448 }
449 }
450
451 #[test]
452 fn prop_label_components_no_panic(
453 width in 1..64usize,
454 height in 1..64usize,
455 data in prop::collection::vec(0..=1u8, 64 * 64)
456 ) {
457 let arena = Bump::new();
458 let binary: Vec<u8> = data.iter().map(|&b| if b == 0 { 0 } else { 255 }).collect();
459 let real_width = width.min(64);
460 let real_height = height.min(64);
461 let slice = &binary[..real_width * real_height];
462
463 let result = label_components_with_stats(&arena, slice, real_width, real_height, true);
464
465 for stat in result.component_stats {
466 assert!(stat.pixel_count > 0);
467 assert!(stat.max_x < real_width as u16);
468 assert!(stat.max_y < real_height as u16);
469 assert!(stat.min_x <= stat.max_x);
470 assert!(stat.min_y <= stat.max_y);
471 }
472 }
473 }
474
475 use crate::config::TagFamily;
480 use crate::image::ImageView;
481 use crate::test_utils::{TestImageParams, generate_test_image_with_params};
482 use crate::threshold::ThresholdEngine;
483
484 fn generate_binarized_tag(tag_size: usize, canvas_size: usize) -> (Vec<u8>, [[f64; 2]; 4]) {
486 let params = TestImageParams {
487 family: TagFamily::AprilTag36h11,
488 id: 0,
489 tag_size,
490 canvas_size,
491 ..Default::default()
492 };
493
494 let (data, corners) = generate_test_image_with_params(¶ms);
495 let img = ImageView::new(&data, canvas_size, canvas_size, canvas_size).unwrap();
496
497 let arena = Bump::new();
498 let engine = ThresholdEngine::new();
499 let stats = engine.compute_tile_stats(&arena, &img);
500 let mut binary = vec![0u8; canvas_size * canvas_size];
501 engine.apply_threshold(&arena, &img, &stats, &mut binary);
502
503 (binary, corners)
504 }
505
506 #[test]
508 fn test_segmentation_at_varying_tag_sizes() {
509 let canvas_size = 640;
510 let tag_sizes = [32, 64, 100, 200, 300];
511
512 for tag_size in tag_sizes {
513 let arena = Bump::new();
514 let (binary, corners) = generate_binarized_tag(tag_size, canvas_size);
515
516 let result =
517 label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
518
519 assert!(
520 !result.component_stats.is_empty(),
521 "Tag size {tag_size}: No components found"
522 );
523
524 let largest = result
525 .component_stats
526 .iter()
527 .max_by_key(|s| s.pixel_count)
528 .unwrap();
529
530 let expected_min_x = corners[0][0] as u16;
531 let expected_max_x = corners[1][0] as u16;
532 let tolerance = 5;
533
534 assert!(
535 (i32::from(largest.min_x) - i32::from(expected_min_x)).abs() <= tolerance,
536 "Tag size {tag_size}: min_x mismatch"
537 );
538 assert!(
539 (i32::from(largest.max_x) - i32::from(expected_max_x)).abs() <= tolerance,
540 "Tag size {tag_size}: max_x mismatch"
541 );
542
543 println!(
544 "Tag size {:>3}px: {} components, largest has {} px",
545 tag_size,
546 result.component_stats.len(),
547 largest.pixel_count
548 );
549 }
550 }
551
552 #[test]
554 fn test_segmentation_component_accuracy() {
555 let canvas_size = 320;
556 let tag_size = 120;
557
558 let arena = Bump::new();
559 let (binary, corners) = generate_binarized_tag(tag_size, canvas_size);
560
561 let result = label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
562
563 let largest = result
564 .component_stats
565 .iter()
566 .max_by_key(|s| s.pixel_count)
567 .unwrap();
568
569 let expected_min = (tag_size * tag_size / 3) as u32;
570 let expected_max = (tag_size * tag_size) as u32;
571
572 assert!(largest.pixel_count >= expected_min);
573 assert!(largest.pixel_count <= expected_max);
574
575 let gt_width = (corners[1][0] - corners[0][0]).abs() as i32;
576 let gt_height = (corners[2][1] - corners[0][1]).abs() as i32;
577 let bbox_width = i32::from(largest.max_x - largest.min_x);
578 let bbox_height = i32::from(largest.max_y - largest.min_y);
579
580 assert!((bbox_width - gt_width).abs() <= 2);
581 assert!((bbox_height - gt_height).abs() <= 2);
582
583 println!(
584 "Component accuracy: {} pixels, bbox={}x{} (GT: {}x{})",
585 largest.pixel_count, bbox_width, bbox_height, gt_width, gt_height
586 );
587 }
588
589 #[test]
591 fn test_segmentation_noisy_boundaries() {
592 let canvas_size = 320;
593 let tag_size = 120;
594
595 let arena = Bump::new();
596 let (mut binary, _corners) = generate_binarized_tag(tag_size, canvas_size);
597
598 let noise_rate = 0.05;
599
600 for y in 1..(canvas_size - 1) {
601 for x in 1..(canvas_size - 1) {
602 let idx = y * canvas_size + x;
603 let current = binary[idx];
604 let left = binary[idx - 1];
605 let right = binary[idx + 1];
606 let up = binary[idx - canvas_size];
607 let down = binary[idx + canvas_size];
608
609 let is_edge =
610 current != left || current != right || current != up || current != down;
611 if is_edge && rand::random_range(0.0..1.0_f32) < noise_rate {
612 binary[idx] = if current == 0 { 255 } else { 0 };
613 }
614 }
615 }
616
617 let result = label_components_with_stats(&arena, &binary, canvas_size, canvas_size, true);
618
619 assert!(!result.component_stats.is_empty());
620
621 let largest = result
622 .component_stats
623 .iter()
624 .max_by_key(|s| s.pixel_count)
625 .unwrap();
626
627 let min_expected = (tag_size * tag_size / 4) as u32;
628 assert!(largest.pixel_count >= min_expected);
629
630 println!(
631 "Noisy segmentation: {} components, largest has {} px",
632 result.component_stats.len(),
633 largest.pixel_count
634 );
635 }
636
637 #[test]
638 fn test_segmentation_correctness_large_image() {
639 let arena = Bump::new();
640 let width = 3840; let height = 2160; let mut binary = vec![255u8; width * height];
643
644 for y in 100..200 {
647 for x in 100..200 {
648 binary[y * width + x] = 0;
649 }
650 }
651
652 for x in 500..3000 {
654 binary[1000 * width + x] = 0;
655 binary[1001 * width + x] = 0;
656 }
657
658 for y in 1800..2000 {
660 if y % 4 == 0 {
661 for x in 1800..2000 {
662 binary[y * width + x] = 0;
663 }
664 }
665 }
666
667 for y in 0..height {
669 if y % 10 == 0 {
670 for x in 0..width {
671 if (!(100..200).contains(&x) || !(100..200).contains(&y)) && (x + y) % 31 == 0 {
672 binary[y * width + x] = 0;
673 }
674 }
675 }
676 }
677
678 let start = std::time::Instant::now();
680 let result = label_components_with_stats(&arena, &binary, width, height, true);
681 let duration = start.elapsed();
682
683 assert!(result.component_stats.len() > 1000);
685
686 println!(
687 "Found {} components on 4K image in {:?}",
688 result.component_stats.len(),
689 duration
690 );
691 }
692}