use crate::detect::roi::TileMask;
use crate::frame::{RawFrame, Rect};
#[derive(Debug, Clone)]
pub struct WorkingFrame {
pub cols: u16,
pub rows: u16,
pub luma: Box<[u8]>,
}
impl WorkingFrame {
pub fn from_raw(frame: &RawFrame, cols: u16, rows: u16) -> Self {
let cols = cols.max(1);
let rows = rows.max(1);
let w = frame.width.max(1);
let h = frame.height.max(1);
let mut luma = vec![0u8; cols as usize * rows as usize];
for ty in 0..rows as u32 {
let y0 = (ty * h) / rows as u32;
let y1 = (((ty + 1) * h) / rows as u32).max(y0 + 1).min(h);
for tx in 0..cols as u32 {
let x0 = (tx * w) / cols as u32;
let x1 = (((tx + 1) * w) / cols as u32).max(x0 + 1).min(w);
let mut sum: u64 = 0;
let mut count: u64 = 0;
let step_x = ((x1 - x0) / 16).max(1);
let step_y = ((y1 - y0) / 16).max(1);
let mut py = y0;
while py < y1 {
let row_off = py as usize * frame.stride as usize;
let mut px = x0;
while px < x1 {
let off = row_off + px as usize * 4;
let b = frame.buffer.get(off).copied().unwrap_or(0) as u64;
let g = frame.buffer.get(off + 1).copied().unwrap_or(0) as u64;
let r = frame.buffer.get(off + 2).copied().unwrap_or(0) as u64;
sum += (54 * r + 183 * g + 19 * b) >> 8;
count += 1;
px += step_x;
}
py += step_y;
}
let mean = sum.checked_div(count).unwrap_or(0) as u8;
luma[(ty * cols as u32 + tx) as usize] = mean;
}
}
Self {
cols,
rows,
luma: luma.into_boxed_slice(),
}
}
pub fn len(&self) -> usize {
self.luma.len()
}
pub fn is_empty(&self) -> bool {
self.luma.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct TileDiff {
pub changed: Box<[bool]>,
pub changed_count: u32,
pub area_ratio: f32,
pub bboxes: Vec<Rect>,
pub cols: u16,
pub rows: u16,
}
impl TileDiff {
pub fn empty(cols: u16, rows: u16) -> Self {
Self {
changed: vec![false; cols as usize * rows as usize].into_boxed_slice(),
changed_count: 0,
area_ratio: 0.0,
bboxes: Vec::new(),
cols,
rows,
}
}
}
pub fn diff(
prev: &WorkingFrame,
cur: &WorkingFrame,
threshold: u8,
ignore: &TileMask,
frame_w: u32,
frame_h: u32,
) -> TileDiff {
let cols = cur.cols;
let rows = cur.rows;
let len = cur.luma.len();
let mut changed = vec![false; len].into_boxed_slice();
let mut count = 0u32;
if prev.luma.len() == len {
for i in 0..len {
if ignore.get(i) {
continue;
}
let d = prev.luma[i].abs_diff(cur.luma[i]);
if d > threshold {
changed[i] = true;
count += 1;
}
}
}
let total = len.max(1) as f32;
let bboxes = connected_bboxes(&changed, cols, rows, frame_w, frame_h);
TileDiff {
changed,
changed_count: count,
area_ratio: count as f32 / total,
bboxes,
cols,
rows,
}
}
fn connected_bboxes(
changed: &[bool],
cols: u16,
rows: u16,
frame_w: u32,
frame_h: u32,
) -> Vec<Rect> {
let cols = cols as usize;
let rows = rows as usize;
let mut visited = vec![false; changed.len()];
let mut boxes = Vec::new();
let mut stack: Vec<usize> = Vec::new();
for start in 0..changed.len() {
if !changed[start] || visited[start] {
continue;
}
let (mut min_c, mut min_r, mut max_c, mut max_r) = (usize::MAX, usize::MAX, 0usize, 0usize);
stack.clear();
stack.push(start);
visited[start] = true;
while let Some(idx) = stack.pop() {
let c = idx % cols;
let r = idx / cols;
min_c = min_c.min(c);
min_r = min_r.min(r);
max_c = max_c.max(c);
max_r = max_r.max(r);
if c > 0 {
let n = idx - 1;
if changed[n] && !visited[n] {
visited[n] = true;
stack.push(n);
}
}
if c + 1 < cols {
let n = idx + 1;
if changed[n] && !visited[n] {
visited[n] = true;
stack.push(n);
}
}
if r > 0 {
let n = idx - cols;
if changed[n] && !visited[n] {
visited[n] = true;
stack.push(n);
}
}
if r + 1 < rows {
let n = idx + cols;
if changed[n] && !visited[n] {
visited[n] = true;
stack.push(n);
}
}
}
let px0 = (min_c as u32 * frame_w) / cols as u32;
let py0 = (min_r as u32 * frame_h) / rows as u32;
let px1 = ((max_c as u32 + 1) * frame_w) / cols as u32;
let py1 = ((max_r as u32 + 1) * frame_h) / rows as u32;
boxes.push(Rect::new(
px0 as i32,
py0 as i32,
px1.saturating_sub(px0),
py1.saturating_sub(py0),
));
}
boxes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detect::roi::TileMask;
use crate::frame::{RawFrame, WindowInfo};
use std::time::Instant;
fn wf(cols: u16, rows: u16, luma: Vec<u8>) -> WorkingFrame {
WorkingFrame {
cols,
rows,
luma: luma.into_boxed_slice(),
}
}
#[test]
fn empty_diff_and_working_frame_len() {
let d = TileDiff::empty(4, 3);
assert_eq!(d.changed.len(), 12);
assert_eq!(d.changed_count, 0);
let w = wf(4, 3, vec![0; 12]);
assert_eq!(w.len(), 12);
assert!(!w.is_empty());
}
#[test]
fn flags_tiles_above_threshold_only() {
let prev = wf(4, 1, vec![0, 0, 0, 0]);
let cur = wf(4, 1, vec![0, 100, 5, 0]);
let td = diff(&prev, &cur, 12, &TileMask::empty(4), 40, 10);
assert!(td.changed[1] && !td.changed[2]); assert_eq!(td.changed_count, 1);
assert!(td.area_ratio > 0.0);
}
#[test]
fn ignore_mask_excludes_tiles() {
let prev = wf(2, 1, vec![0, 0]);
let cur = wf(2, 1, vec![200, 200]);
let mut ignore = TileMask::empty(2);
ignore.set(0);
let td = diff(&prev, &cur, 12, &ignore, 20, 10);
assert!(!td.changed[0] && td.changed[1]);
}
#[test]
fn disjoint_clusters_yield_separate_bboxes() {
let prev = wf(5, 1, vec![0; 5]);
let cur = wf(5, 1, vec![200, 0, 0, 0, 200]); let td = diff(&prev, &cur, 12, &TileMask::empty(5), 50, 10);
assert_eq!(td.changed_count, 2);
assert_eq!(td.bboxes.len(), 2);
}
#[test]
fn mismatched_grid_sizes_report_no_change() {
let prev = wf(2, 1, vec![0, 0]);
let cur = wf(4, 1, vec![0; 4]);
let td = diff(&prev, &cur, 12, &TileMask::empty(4), 40, 10);
assert_eq!(td.changed_count, 0);
}
#[test]
fn from_raw_downsamples_to_grid() {
let f = RawFrame::from_bgra(
vec![128u8; 4 * 2 * 4],
4,
2,
Instant::now(),
chrono::Utc::now(),
WindowInfo::synthetic("t", 4, 2),
);
let w = WorkingFrame::from_raw(&f, 2, 2);
assert_eq!((w.cols, w.rows, w.luma.len()), (2, 2, 4));
assert!(w.luma.iter().all(|&l| l > 100)); }
}