mod label_extract;
mod morphology;
use super::types::BoundingBox;
use label_extract::extract_labeled_regions;
use morphology::morph_close;
struct UnionFind {
parent: Vec<u32>,
rank: Vec<u8>,
}
impl UnionFind {
fn new(size: usize) -> Self {
Self {
parent: (0..size as u32).collect(),
rank: vec![0; size],
}
}
fn find(&mut self, mut x: u32) -> u32 {
while self.parent[x as usize] != x {
self.parent[x as usize] = self.parent[self.parent[x as usize] as usize];
x = self.parent[x as usize];
}
x
}
fn union(&mut self, a: u32, b: u32) {
let ra = self.find(a);
let rb = self.find(b);
if ra == rb {
return;
}
match self.rank[ra as usize].cmp(&self.rank[rb as usize]) {
std::cmp::Ordering::Less => self.parent[ra as usize] = rb,
std::cmp::Ordering::Greater => self.parent[rb as usize] = ra,
std::cmp::Ordering::Equal => {
self.parent[rb as usize] = ra;
self.rank[ra as usize] += 1;
}
}
}
}
pub struct ComponentInfo {
pub bbox: BoundingBox,
pub pixel_count: u32,
}
pub fn extract_change_mask(output_data: &[u8], width: u32, height: u32) -> Vec<bool> {
let total = (width * height) as usize;
let mut mask = vec![false; total];
#[cfg(target_arch = "aarch64")]
{
unsafe { extract_mask_neon(output_data.as_ptr(), &mut mask, total) };
return mask;
}
#[cfg(not(target_arch = "aarch64"))]
{
for i in 0..total {
let pos = i * 4;
let r = output_data[pos];
let g = output_data[pos + 1];
let b = output_data[pos + 2];
mask[i] = r != g || r != b;
}
mask
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn extract_mask_neon(data: *const u8, mask: &mut [bool], count: usize) {
use std::arch::aarch64::*;
let chunks = count / 16;
let mask_ptr = mask.as_mut_ptr() as *mut u8;
for i in 0..chunks {
let base = i * 16 * 4;
let rgba = vld4q_u8(data.add(base));
let r = rgba.0;
let g = rgba.1;
let b = rgba.2;
let ne_rg = vmvnq_u8(vceqq_u8(r, g));
let ne_rb = vmvnq_u8(vceqq_u8(r, b));
let changed = vorrq_u8(ne_rg, ne_rb);
let ones = vdupq_n_u8(1);
let result = vandq_u8(changed, ones);
vst1q_u8(mask_ptr.add(i * 16), result);
}
let processed = chunks * 16;
for i in processed..count {
let pos = i * 4;
let r = *data.add(pos);
let g = *data.add(pos + 1);
let b = *data.add(pos + 2);
mask[i] = r != g || r != b;
}
}
#[cfg(test)]
fn find_connected_components(mask: &[bool], width: u32, height: u32) -> Vec<ComponentInfo> {
let labels = label_connected_components(mask, width, height);
extract_labeled_regions(&labels, mask, width)
}
fn label_connected_components(mask: &[bool], width: u32, height: u32) -> Vec<i32> {
let w = width as usize;
let h = height as usize;
let total = w * h;
let mut labels = vec![0i32; total];
if total == 0 {
return labels;
}
let mut uf = UnionFind::new(total);
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
if !mask[idx] {
continue;
}
if x > 0 && mask[idx - 1] {
uf.union(idx as u32, (idx - 1) as u32);
}
if y > 0 && mask[idx - w] {
uf.union(idx as u32, (idx - w) as u32);
}
}
}
let mut root_to_label: std::collections::HashMap<u32, i32> = std::collections::HashMap::new();
let mut next_label = 1i32;
for i in 0..total {
if !mask[i] {
continue;
}
let root = uf.find(i as u32);
let label = *root_to_label.entry(root).or_insert_with(|| {
let l = next_label;
next_label += 1;
l
});
labels[i] = label;
}
labels
}
pub fn detect_regions(mask: &[bool], width: u32, height: u32) -> Vec<ComponentInfo> {
let total = (width * height) as usize;
if total == 0 || !mask.iter().any(|&m| m) {
return Vec::new();
}
let closed = morph_close(mask, width, height);
let labels = label_connected_components(&closed, width, height);
extract_labeled_regions(&labels, mask, width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_change_mask_gray_pixels() {
let data = vec![128, 128, 128, 255, 128, 128, 128, 255];
let mask = extract_change_mask(&data, 2, 1);
assert_eq!(mask, vec![false, false]);
}
#[test]
fn test_extract_change_mask_colored_pixels() {
let data = vec![255, 0, 0, 255, 128, 128, 128, 255];
let mask = extract_change_mask(&data, 2, 1);
assert_eq!(mask, vec![true, false]);
}
#[test]
fn test_find_connected_components_empty() {
let mask = vec![false; 9];
let components = find_connected_components(&mask, 3, 3);
assert!(components.is_empty());
}
#[test]
fn test_find_connected_components_single_pixel() {
let mut mask = vec![false; 9];
mask[4] = true;
let components = find_connected_components(&mask, 3, 3);
assert_eq!(components.len(), 1);
assert_eq!(components[0].pixel_count, 1);
assert_eq!(
components[0].bbox,
BoundingBox {
x: 1,
y: 1,
width: 1,
height: 1,
}
);
}
#[test]
fn test_find_connected_components_two_separate() {
let mask = vec![true, false, false, false, true];
let components = find_connected_components(&mask, 5, 1);
assert_eq!(components.len(), 2);
}
#[test]
fn test_find_connected_components_l_shape() {
let mask = vec![true, false, false, true, false, false, true, true, true];
let components = find_connected_components(&mask, 3, 3);
assert_eq!(components.len(), 1);
assert_eq!(components[0].pixel_count, 5);
assert_eq!(
components[0].bbox,
BoundingBox {
x: 0,
y: 0,
width: 3,
height: 3,
}
);
}
#[test]
fn test_detect_regions_empty() {
let mask = vec![false; 100];
let regions = detect_regions(&mask, 10, 10);
assert!(regions.is_empty());
}
#[test]
fn test_detect_regions_single_blob() {
let mut mask = vec![false; 10000]; for y in 40..60 {
for x in 40..60 {
mask[y * 100 + x] = true;
}
}
let regions = detect_regions(&mask, 100, 100);
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].pixel_count, 400);
}
#[test]
fn test_detect_regions_two_separate_blobs() {
let mut mask = vec![false; 10000]; for y in 5..15 {
for x in 5..15 {
mask[y * 100 + x] = true;
}
}
for y in 80..90 {
for x in 80..90 {
mask[y * 100 + x] = true;
}
}
let regions = detect_regions(&mask, 100, 100);
assert_eq!(regions.len(), 2);
}
#[test]
fn test_detect_regions_nearby_blobs_bridged() {
let mut mask = vec![false; 10000]; for y in 40..50 {
for x in 40..48 {
mask[y * 100 + x] = true;
}
}
for y in 40..50 {
for x in 51..60 {
mask[y * 100 + x] = true;
}
}
let regions = detect_regions(&mask, 100, 100);
assert_eq!(
regions.len(),
1,
"nearby blobs should be merged by morph close"
);
}
#[test]
fn test_detect_regions_connected_bridge() {
let mut mask = vec![false; 50 * 20]; for y in 6..14 {
for x in 2..10 {
mask[y * 50 + x] = true;
}
}
for y in 6..14 {
for x in 30..38 {
mask[y * 50 + x] = true;
}
}
for x in 10..30 {
mask[10 * 50 + x] = true;
}
let regions = detect_regions(&mask, 50, 20);
assert_eq!(regions.len(), 1, "connected blobs should be one region");
}
#[test]
fn test_extract_mask_simd_matches_scalar() {
for size in [1, 15, 16, 17, 31, 32, 33, 64, 100] {
let mut data = vec![128u8; size * 4]; for i in (0..size).step_by(3) {
data[i * 4] = 255; }
let mask = extract_change_mask(&data, size as u32, 1);
for i in 0..size {
let r = data[i * 4];
let g = data[i * 4 + 1];
let b = data[i * 4 + 2];
let expected = r != g || r != b;
assert_eq!(
mask[i], expected,
"mismatch at pixel {i} (size={size}): r={r} g={g} b={b}"
);
}
}
}
}