use std::collections::VecDeque;
use crate::convert::to_luma8;
use crate::types::{Frame, Rect};
pub fn blur_variance(frame: &Frame) -> f32 {
let luma = to_luma8(frame);
let width = frame.width as usize;
if width == 0 {
return 0.0;
}
let height = luma.len() / width;
laplacian_variance(&luma, width, height)
}
pub fn blur_variance_in(frame: &Frame, region: Rect) -> f32 {
let luma = to_luma8(frame);
let width = frame.width as usize;
if width == 0 {
return 0.0;
}
let height = luma.len() / width;
let left = (region.x as usize).min(width);
let top = (region.y as usize).min(height);
let right = (region.x as usize + region.width as usize).min(width);
let bottom = (region.y as usize + region.height as usize).min(height);
if left >= right || top >= bottom {
return 0.0;
}
let cropped_width = right - left;
let cropped_height = bottom - top;
let mut cropped = Vec::with_capacity(cropped_width * cropped_height);
for row in top..bottom {
let start = row * width + left;
let end = start + cropped_width;
cropped.extend_from_slice(&luma[start..end]);
}
laplacian_variance(&cropped, cropped_width, cropped_height)
}
pub fn blur_variance_subsampled(frame: &Frame, stride: u32) -> f32 {
let step = (stride.max(1)) as usize;
if step == 1 {
return blur_variance(frame);
}
let luma = to_luma8(frame);
let source_width = frame.width as usize;
if source_width == 0 {
return 0.0;
}
let source_height = luma.len() / source_width;
let target_width = source_width.div_ceil(step);
let target_height = source_height.div_ceil(step);
if target_width < 3 || target_height < 3 {
return 0.0;
}
let mut downsampled = Vec::with_capacity(target_width * target_height);
for row in 0..target_height {
let source_row = row * step;
if source_row >= source_height {
break;
}
let source_row_start = source_row * source_width;
for col in 0..target_width {
let source_col = col * step;
if source_col >= source_width {
break;
}
downsampled.push(luma[source_row_start + source_col]);
}
}
laplacian_variance(&downsampled, target_width, target_height)
}
#[derive(Clone, Debug, Default)]
pub struct Ring {
pub capacity: usize,
pub frames: VecDeque<Frame>,
}
pub fn ring_new(capacity: usize) -> Ring {
Ring {
capacity,
frames: VecDeque::with_capacity(capacity),
}
}
pub fn ring_push(ring: &mut Ring, frame: Frame) {
if ring.capacity == 0 {
return;
}
if ring.frames.len() >= ring.capacity {
ring.frames.pop_front();
}
ring.frames.push_back(frame);
}
pub fn take_sharpest(ring: &Ring) -> Option<Frame> {
ring.frames
.iter()
.map(|frame| (blur_variance(frame), frame))
.max_by(|left, right| {
left.0
.partial_cmp(&right.0)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(_, frame)| frame.clone())
}
fn laplacian_variance(luma: &[u8], width: usize, height: usize) -> f32 {
if width < 3 || height < 3 {
return 0.0;
}
let pixel_count = width * height;
if luma.len() < pixel_count {
return 0.0;
}
let mut sum: f64 = 0.0;
let mut sum_squared: f64 = 0.0;
let mut count: u64 = 0;
for row in 0..height {
let row_above = row.saturating_sub(1);
let row_below = (row + 1).min(height - 1);
let row_offset = row * width;
let above_offset = row_above * width;
let below_offset = row_below * width;
for col in 0..width {
let col_left = col.saturating_sub(1);
let col_right = (col + 1).min(width - 1);
let center = luma[row_offset + col] as i32;
let above = luma[above_offset + col] as i32;
let below = luma[below_offset + col] as i32;
let neighbor_left = luma[row_offset + col_left] as i32;
let neighbor_right = luma[row_offset + col_right] as i32;
let response = above + below + neighbor_left + neighbor_right - 4 * center;
let response_f = response as f64;
sum += response_f;
sum_squared += response_f * response_f;
count += 1;
}
}
if count == 0 {
return 0.0;
}
let inverse_count = 1.0 / count as f64;
let mean = sum * inverse_count;
let variance = sum_squared * inverse_count - mean * mean;
variance.max(0.0) as f32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Frame, FrameQuality, PixelFormat};
use bytes::Bytes;
use std::time::Duration;
fn rgb_frame(width: u32, height: u32, pixels: Vec<u8>) -> Frame {
Frame {
width,
height,
stride: 0,
timestamp: Duration::ZERO,
pixel_format: PixelFormat::Rgb8,
quality: FrameQuality::Intact,
plane_primary: Bytes::from(pixels),
plane_secondary: Bytes::new(),
}
}
fn checkerboard(width: u32, height: u32) -> Frame {
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for row in 0..height {
for col in 0..width {
let value = if (row + col) % 2 == 0 { 255 } else { 0 };
pixels.extend_from_slice(&[value, value, value]);
}
}
rgb_frame(width, height, pixels)
}
#[test]
fn sharp_beats_flat() {
let flat = rgb_frame(16, 16, vec![128u8; 16 * 16 * 3]);
let sharp = checkerboard(16, 16);
assert!(blur_variance(&sharp) > blur_variance(&flat));
}
#[test]
fn roi_within_bounds() {
let sharp = checkerboard(16, 16);
let center = Rect {
x: 4,
y: 4,
width: 8,
height: 8,
};
assert!(blur_variance_in(&sharp, center) > 0.0);
}
#[test]
fn roi_out_of_bounds_returns_zero() {
let sharp = checkerboard(16, 16);
let offscreen = Rect {
x: 100,
y: 100,
width: 10,
height: 10,
};
assert_eq!(blur_variance_in(&sharp, offscreen), 0.0);
}
#[test]
fn roi_degenerate_returns_zero() {
let sharp = checkerboard(16, 16);
let empty = Rect {
x: 0,
y: 0,
width: 0,
height: 0,
};
assert_eq!(blur_variance_in(&sharp, empty), 0.0);
}
#[test]
fn subsample_stride_one_matches_full() {
let sharp = checkerboard(16, 16);
let full = blur_variance(&sharp);
let strided = blur_variance_subsampled(&sharp, 1);
assert!((full - strided).abs() < 1e-3);
}
#[test]
fn subsample_stride_zero_matches_full() {
let sharp = checkerboard(16, 16);
let full = blur_variance(&sharp);
let strided = blur_variance_subsampled(&sharp, 0);
assert!((full - strided).abs() < 1e-3);
}
#[test]
fn ring_push_evicts_oldest() {
let mut ring = ring_new(2);
ring_push(&mut ring, rgb_frame(4, 4, vec![0u8; 48]));
ring_push(&mut ring, rgb_frame(4, 4, vec![64u8; 48]));
ring_push(&mut ring, rgb_frame(4, 4, vec![128u8; 48]));
assert_eq!(ring.frames.len(), 2);
assert_eq!(ring.frames[0].plane_primary[0], 64);
assert_eq!(ring.frames[1].plane_primary[0], 128);
}
#[test]
fn take_sharpest_picks_highest_variance() {
let mut ring = ring_new(3);
ring_push(&mut ring, rgb_frame(16, 16, vec![128u8; 16 * 16 * 3]));
ring_push(&mut ring, checkerboard(16, 16));
ring_push(&mut ring, rgb_frame(16, 16, vec![64u8; 16 * 16 * 3]));
let sharpest = take_sharpest(&ring).expect("ring has frames");
let sharp_variance = blur_variance(&sharpest);
let flat_variance = blur_variance(&ring.frames[0]);
assert!(sharp_variance > flat_variance);
}
#[test]
fn take_sharpest_empty_returns_none() {
let ring = ring_new(4);
assert!(take_sharpest(&ring).is_none());
}
#[test]
fn ring_zero_capacity_rejects_pushes() {
let mut ring = ring_new(0);
ring_push(&mut ring, rgb_frame(4, 4, vec![0u8; 48]));
assert!(ring.frames.is_empty());
}
}