use visual_cortex_capture::FrameView;
use crate::detector::{Detector, DetectorError, DetectorOutput};
use crate::luma::luma;
pub struct Brightness;
impl Detector for Brightness {
fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError> {
let mut sum: u64 = 0;
let mut count: u64 = 0;
for row in view.rows() {
for px in row.chunks_exact(4) {
sum += luma(px[2], px[1], px[0]) as u64;
count += 1;
}
}
Ok(DetectorOutput::Number(sum as f64 / count as f64))
}
}
pub struct ColorMatch {
rgb: [u8; 3],
tolerance: u8,
min_fraction: f32,
}
impl ColorMatch {
pub fn new(rgb: [u8; 3], tolerance: u8, min_fraction: f32) -> Self {
assert!(
(0.0..=1.0).contains(&min_fraction),
"ColorMatch min_fraction must be in 0.0..=1.0"
);
Self {
rgb,
tolerance,
min_fraction,
}
}
}
impl Detector for ColorMatch {
fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError> {
let [tr, tg, tb] = self.rgb;
let tol = self.tolerance as i16;
let mut hits: u64 = 0;
let mut total: u64 = 0;
for row in view.rows() {
for px in row.chunks_exact(4) {
let close = (px[2] as i16 - tr as i16).abs() <= tol
&& (px[1] as i16 - tg as i16).abs() <= tol
&& (px[0] as i16 - tb as i16).abs() <= tol;
hits += close as u64;
total += 1;
}
}
Ok(DetectorOutput::Bool(
hits as f32 / total as f32 >= self.min_fraction,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Channel {
Red,
Green,
Blue,
}
const DOMINANCE_MARGIN: u8 = 30;
const MIN_CHANNEL_VALUE: u8 = 60;
pub struct ColorDominance {
channel: Channel,
min_fraction: f32,
}
impl ColorDominance {
pub fn new(channel: Channel, min_fraction: f32) -> Self {
assert!(
(0.0..=1.0).contains(&min_fraction),
"ColorDominance min_fraction must be in 0.0..=1.0"
);
Self {
channel,
min_fraction,
}
}
pub fn red(min_fraction: f32) -> Self {
Self::new(Channel::Red, min_fraction)
}
pub fn green(min_fraction: f32) -> Self {
Self::new(Channel::Green, min_fraction)
}
pub fn blue(min_fraction: f32) -> Self {
Self::new(Channel::Blue, min_fraction)
}
}
impl Detector for ColorDominance {
fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError> {
let mut hits: u64 = 0;
let mut total: u64 = 0;
for row in view.rows() {
for px in row.chunks_exact(4) {
let (b, g, r) = (px[0], px[1], px[2]);
let (value, o1, o2) = match self.channel {
Channel::Red => (r, g, b),
Channel::Green => (g, r, b),
Channel::Blue => (b, r, g),
};
let dominant = value >= MIN_CHANNEL_VALUE
&& value >= o1.saturating_add(DOMINANCE_MARGIN)
&& value >= o2.saturating_add(DOMINANCE_MARGIN);
hits += dominant as u64;
total += 1;
}
}
Ok(DetectorOutput::Bool(
hits as f32 / total as f32 >= self.min_fraction,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use visual_cortex_capture::{Frame, PxRect};
fn full_view(frame: &Frame) -> FrameView<'_> {
frame
.view(PxRect {
x: 0,
y: 0,
w: frame.width(),
h: frame.height(),
})
.unwrap()
}
#[test]
fn brightness_of_black_is_zero() {
let f = Frame::solid(4, 4, [0, 0, 0, 255]);
let mut det = Brightness;
assert_eq!(
det.evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Number(0.0)
);
}
#[test]
fn brightness_of_white_is_255() {
let f = Frame::solid(4, 4, [255, 255, 255, 255]);
let mut det = Brightness;
assert_eq!(
det.evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Number(255.0)
);
}
#[test]
fn brightness_averages_mixed_pixels() {
let f = Frame::from_fn(2, 1, |x, _| {
if x == 0 {
[0, 0, 0, 255]
} else {
[255, 255, 255, 255]
}
});
let mut det = Brightness;
assert_eq!(
det.evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Number(127.5)
);
}
#[test]
fn color_match_counts_fraction_within_tolerance() {
let f = Frame::from_fn(4, 2, |x, _| {
if x < 2 {
[0, 0, 255, 255] } else {
[255, 0, 0, 255] }
});
let mut half_red = ColorMatch::new([255, 0, 0], 10, 0.5);
assert_eq!(
half_red.evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Bool(true)
);
let mut mostly_red = ColorMatch::new([255, 0, 0], 10, 0.6);
assert_eq!(
mostly_red.evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Bool(false)
);
}
#[test]
fn color_dominance_detects_dominant_channel() {
let red = Frame::solid(4, 4, [20, 20, 200, 255]); assert_eq!(
ColorDominance::red(0.9).evaluate(&full_view(&red)).unwrap(),
DetectorOutput::Bool(true)
);
assert_eq!(
ColorDominance::green(0.1)
.evaluate(&full_view(&red))
.unwrap(),
DetectorOutput::Bool(false)
);
}
#[test]
fn color_dominance_rejects_gray() {
let gray = Frame::solid(4, 4, [128, 128, 128, 255]);
assert_eq!(
ColorDominance::red(0.01)
.evaluate(&full_view(&gray))
.unwrap(),
DetectorOutput::Bool(false)
);
}
#[test]
fn color_dominance_fraction_thresholds() {
let f = Frame::from_fn(4, 2, |x, _| {
if x < 2 {
[20, 20, 200, 255]
} else {
[90, 90, 90, 255]
}
});
assert_eq!(
ColorDominance::red(0.4).evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Bool(true)
);
assert_eq!(
ColorDominance::red(0.6).evaluate(&full_view(&f)).unwrap(),
DetectorOutput::Bool(false)
);
}
#[test]
#[should_panic]
fn color_dominance_rejects_bad_fraction() {
let _ = ColorDominance::red(1.5);
}
}