use crate::frame::{Frame, Rect};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColorSpec {
pub rgb: [u8; 3],
pub tolerance: u8,
}
impl ColorSpec {
pub fn new(r: u8, g: u8, b: u8, tolerance: u8) -> Self {
ColorSpec {
rgb: [r, g, b],
tolerance,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ColorBlob {
pub bounds: Rect,
pub area: usize,
}
impl ColorBlob {
pub fn center(&self) -> (i32, i32) {
(
self.bounds.x as i32 + self.bounds.width as i32 / 2,
self.bounds.y as i32 + self.bounds.height as i32 / 2,
)
}
}
struct Run {
x0: usize,
x1: usize, label: usize,
}
struct Node {
parent: usize,
x0: usize,
y0: usize,
x1: usize,
y1: usize,
area: usize,
}
fn find(nodes: &mut [Node], mut i: usize) -> usize {
while nodes[i].parent != i {
let p = nodes[i].parent;
nodes[i].parent = nodes[p].parent; i = nodes[i].parent;
}
i
}
fn union(nodes: &mut [Node], a: usize, b: usize) {
let (ra, rb) = (find(nodes, a), find(nodes, b));
if ra == rb {
return;
}
let (keep, drop) = if ra < rb { (ra, rb) } else { (rb, ra) };
nodes[drop].parent = keep;
}
pub fn find_blobs(
frame: &Frame,
spec: &ColorSpec,
region: Rect,
min_area: usize,
) -> Vec<ColorBlob> {
let r = frame.clamp(region);
let (ro, go, bo) = frame.rgb_offsets();
let tol = spec.tolerance as i32;
let (tr, tg, tb) = (spec.rgb[0] as i32, spec.rgb[1] as i32, spec.rgb[2] as i32);
let sw4 = frame.width * 4;
let x_end = r.x + r.width;
let mut nodes: Vec<Node> = Vec::new();
let mut prev: Vec<Run> = Vec::new();
let mut cur: Vec<Run> = Vec::new();
for y in r.y..r.y + r.height {
cur.clear();
let base = y * sw4;
let mut x = r.x;
while x < x_end {
let i = base + x * 4;
let px = &frame.pixels[i..i + 4];
if (px[ro] as i32 - tr).abs() <= tol
&& (px[go] as i32 - tg).abs() <= tol
&& (px[bo] as i32 - tb).abs() <= tol
{
let s = x;
x += 1;
while x < x_end {
let i = base + x * 4;
let px = &frame.pixels[i..i + 4];
if (px[ro] as i32 - tr).abs() > tol
|| (px[go] as i32 - tg).abs() > tol
|| (px[bo] as i32 - tb).abs() > tol
{
break;
}
x += 1;
}
let label = nodes.len();
nodes.push(Node {
parent: label,
x0: s,
y0: y,
x1: x - 1,
y1: y,
area: x - s,
});
cur.push(Run {
x0: s,
x1: x - 1,
label,
});
} else {
x += 1;
}
}
for c in &cur {
for p in &prev {
if p.x0 <= c.x1 + 1 && p.x1 + 1 >= c.x0 {
union(&mut nodes, c.label, p.label);
}
}
}
std::mem::swap(&mut prev, &mut cur);
}
let mut roots: Vec<Node> = Vec::new(); let mut index: Vec<i32> = vec![-1; nodes.len()];
for i in 0..nodes.len() {
let root = find(&mut nodes, i);
let n = &nodes[i];
if index[root] < 0 {
index[root] = roots.len() as i32;
roots.push(Node {
parent: root,
x0: n.x0,
y0: n.y0,
x1: n.x1,
y1: n.y1,
area: 0,
});
}
let slot = index[root] as usize;
let a = &mut roots[slot];
a.x0 = a.x0.min(n.x0);
a.y0 = a.y0.min(n.y0);
a.x1 = a.x1.max(n.x1);
a.y1 = a.y1.max(n.y1);
a.area += n.area;
}
let mut blobs: Vec<ColorBlob> = roots
.into_iter()
.filter(|n| n.area >= min_area.max(1))
.map(|n| ColorBlob {
bounds: Rect::new(n.x0, n.y0, n.x1 - n.x0 + 1, n.y1 - n.y0 + 1),
area: n.area,
})
.collect();
blobs.sort_unstable_by_key(|b| (b.bounds.y, b.bounds.x));
blobs
}
pub trait FindColor {
fn find_blobs(&self, spec: &ColorSpec, region: Rect, min_area: usize) -> Vec<ColorBlob>;
}
impl FindColor for Frame {
fn find_blobs(&self, spec: &ColorSpec, region: Rect, min_area: usize) -> Vec<ColorBlob> {
find_blobs(self, spec, region, min_area)
}
}
#[cfg(test)]
mod tests {
use super::*;
const W: usize = 64;
const H: usize = 48;
fn scene(blocks: &[(usize, usize, usize, usize, [u8; 3])]) -> Frame {
let mut px = vec![0u8; W * H * 4];
for p in px.chunks_exact_mut(4) {
p[3] = 255;
}
for &(x, y, w, h, col) in blocks {
for yy in y..y + h {
for xx in x..x + w {
let i = (yy * W + xx) * 4;
px[i] = col[0];
px[i + 1] = col[1];
px[i + 2] = col[2];
}
}
}
Frame::rgba8(W, H, px)
}
const RED: [u8; 3] = [255, 0, 0];
#[test]
fn finds_two_separate_blobs() {
let f = scene(&[(10, 10, 20, 8, RED), (40, 30, 8, 8, RED)]);
let blobs = find_blobs(&f, &ColorSpec::new(255, 0, 0, 10), (0, 0, W, H).into(), 1);
assert_eq!(blobs.len(), 2);
assert_eq!(blobs[0].bounds, Rect::new(10, 10, 20, 8));
assert_eq!(blobs[0].area, 160);
assert_eq!(blobs[1].bounds, Rect::new(40, 30, 8, 8));
assert_eq!(blobs[1].center(), (44, 34));
}
#[test]
fn filters_by_min_area() {
let f = scene(&[(10, 10, 20, 8, RED), (40, 30, 4, 4, RED)]);
let blobs = find_blobs(&f, &ColorSpec::new(255, 0, 0, 10), (0, 0, W, H).into(), 100);
assert_eq!(blobs.len(), 1, "16 像素的碎点应被 min_area=100 过滤");
assert_eq!(blobs[0].area, 160);
}
#[test]
fn diagonal_pixels_are_one_blob() {
let f = scene(&[(20, 20, 2, 2, RED), (22, 22, 2, 2, RED)]);
let blobs = find_blobs(&f, &ColorSpec::new(255, 0, 0, 0), (0, 0, W, H).into(), 1);
assert_eq!(blobs.len(), 1);
assert_eq!(blobs[0].bounds, Rect::new(20, 20, 4, 4));
assert_eq!(blobs[0].area, 8);
}
#[test]
fn tolerance_decides_near_miss() {
let f = scene(&[(5, 5, 10, 10, [100, 100, 100])]);
let spec_loose = ColorSpec::new(106, 106, 106, 10);
let spec_strict = ColorSpec::new(106, 106, 106, 3);
assert_eq!(find_blobs(&f, &spec_loose, (0, 0, W, H).into(), 1).len(), 1);
assert!(find_blobs(&f, &spec_strict, (0, 0, W, H).into(), 1).is_empty());
}
#[test]
fn works_on_bgra_frames() {
let rgba = scene(&[(10, 10, 20, 8, RED)]);
let mut bgra_px = rgba.pixels.clone();
for p in bgra_px.chunks_exact_mut(4) {
p.swap(0, 2);
}
let bgra = Frame::bgra8(W, H, bgra_px);
let spec = ColorSpec::new(255, 0, 0, 5);
let a = find_blobs(&rgba, &spec, (0, 0, W, H).into(), 1);
let b = find_blobs(&bgra, &spec, (0, 0, W, H).into(), 1);
assert_eq!(a, b);
assert_eq!(a.len(), 1);
}
#[test]
fn region_limits_detection() {
let f = scene(&[(10, 10, 20, 8, RED)]);
let spec = ColorSpec::new(255, 0, 0, 5);
assert!(find_blobs(&f, &spec, (35, 5, 20, 20).into(), 1).is_empty());
let b = find_blobs(&f, &spec, (0, 0, 32, 24).into(), 1);
assert_eq!(b[0].bounds, Rect::new(10, 10, 20, 8));
}
}