#![allow(dead_code)]
use std::collections::VecDeque;
#[derive(Debug, Clone, Default)]
pub struct ComplexityHistogramDetector {
bins: usize,
}
impl ComplexityHistogramDetector {
#[must_use]
pub fn new() -> Self {
Self { bins: 256 }
}
#[must_use]
pub fn detect(&self, frame: &[u8], width: u32, height: u32) -> f32 {
let n_pixels = (width as usize) * (height as usize);
if n_pixels == 0 || frame.is_empty() {
return 0.0;
}
let histogram = self.build_histogram(frame, n_pixels);
self.complexity_from_histogram(&histogram, n_pixels)
}
fn build_histogram(&self, frame: &[u8], n_pixels: usize) -> [u64; 256] {
let mut hist = [0u64; 256];
if frame.len() == n_pixels {
for &byte in frame.iter().take(n_pixels) {
hist[byte as usize] += 1;
}
} else {
let samples = frame.len() / 3;
let pixels = samples.min(n_pixels);
for i in 0..pixels {
let r = frame[i * 3] as f32;
let g = frame[i * 3 + 1] as f32;
let b = frame[i * 3 + 2] as f32;
let y = (0.299 * r + 0.587 * g + 0.114 * b).round() as usize;
let y = y.min(255);
hist[y] += 1;
}
}
hist
}
fn complexity_from_histogram(&self, histogram: &[u64; 256], n_pixels: usize) -> f32 {
if n_pixels == 0 {
return 0.0;
}
let n = n_pixels as f64;
let mean: f64 = histogram
.iter()
.enumerate()
.map(|(i, &count)| i as f64 * count as f64)
.sum::<f64>()
/ n;
let variance: f64 = histogram
.iter()
.enumerate()
.map(|(i, &count)| {
let diff = i as f64 - mean;
diff * diff * count as f64
})
.sum::<f64>()
/ n;
let std_dev = variance.sqrt();
let max_std_dev = 127.5_f64;
((std_dev / max_std_dev) as f32).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveThreshold {
pub min_threshold: f32,
pub max_threshold: f32,
pub base_threshold: f32,
pub window_size: usize,
window: VecDeque<f32>,
}
impl AdaptiveThreshold {
#[must_use]
pub fn new(min_threshold: f32, max_threshold: f32, window_size: usize) -> Self {
assert!(
min_threshold <= max_threshold,
"min_threshold must be ≤ max_threshold"
);
assert!(window_size > 0, "window_size must be > 0");
let base = (min_threshold + max_threshold) / 2.0;
Self {
min_threshold,
max_threshold,
base_threshold: base,
window_size,
window: VecDeque::with_capacity(window_size),
}
}
#[must_use]
pub fn default_params() -> Self {
Self::new(0.2, 0.5, 30)
}
pub fn push_complexity(&mut self, score: f32) {
if self.window.len() == self.window_size {
self.window.pop_front();
}
self.window.push_back(score.clamp(0.0, 1.0));
}
#[must_use]
pub fn threshold(&self) -> f32 {
if self.window.is_empty() {
return self.base_threshold;
}
let mean: f32 = self.window.iter().sum::<f32>() / self.window.len() as f32;
let t = self.min_threshold + mean * (self.max_threshold - self.min_threshold);
t.clamp(self.min_threshold, self.max_threshold)
}
#[must_use]
pub fn is_scene_cut(&self, diff: f32) -> bool {
diff >= self.threshold()
}
#[must_use]
pub fn window_len(&self) -> usize {
self.window.len()
}
pub fn reset(&mut self) {
self.window.clear();
}
}
impl Default for AdaptiveThreshold {
fn default() -> Self {
Self::default_params()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn constant_rgb(value: u8, width: u32, height: u32) -> Vec<u8> {
vec![value; (width * height * 3) as usize]
}
fn checkerboard_rgb(width: u32, height: u32) -> Vec<u8> {
let n = (width * height) as usize;
(0..n)
.flat_map(|i| {
let v: u8 = if i % 2 == 0 { 0 } else { 255 };
[v, v, v]
})
.collect()
}
fn gradient_gray(n_pixels: usize) -> Vec<u8> {
(0..n_pixels).map(|i| (i % 256) as u8).collect()
}
#[test]
fn test_black_frame_is_zero_complexity() {
let det = ComplexityHistogramDetector::new();
let frame = constant_rgb(0, 16, 16);
let score = det.detect(&frame, 16, 16);
assert!(
score < 1e-6,
"black frame complexity should be 0, got {score}"
);
}
#[test]
fn test_white_frame_is_zero_complexity() {
let det = ComplexityHistogramDetector::new();
let frame = constant_rgb(255, 16, 16);
let score = det.detect(&frame, 16, 16);
assert!(
score < 1e-6,
"white frame complexity should be 0, got {score}"
);
}
#[test]
fn test_checkerboard_has_high_complexity() {
let det = ComplexityHistogramDetector::new();
let frame = checkerboard_rgb(8, 8);
let score = det.detect(&frame, 8, 8);
assert!(
score > 0.9,
"checkerboard complexity should be near 1, got {score}"
);
}
#[test]
fn test_complex_frame_more_complex_than_constant() {
let det = ComplexityHistogramDetector::new();
let complex = gradient_gray(1024);
let constant = vec![128u8; 1024];
let complex_score = det.detect(&complex, 32, 32);
let constant_score = det.detect(&constant, 32, 32);
assert!(
complex_score > constant_score,
"gradient should be more complex ({}) than constant ({})",
complex_score,
constant_score
);
}
#[test]
fn test_empty_frame_returns_zero() {
let det = ComplexityHistogramDetector::new();
let score = det.detect(&[], 0, 0);
assert_eq!(score, 0.0);
}
#[test]
fn test_grayscale_single_channel() {
let det = ComplexityHistogramDetector::new();
let gray = gradient_gray(256);
let score = det.detect(&gray, 16, 16);
assert!(score > 0.0, "gradient gray should have non-zero complexity");
}
#[test]
fn test_score_in_unit_range() {
let det = ComplexityHistogramDetector::new();
let checker = checkerboard_rgb(32, 32);
let score = det.detect(&checker, 32, 32);
assert!(
(0.0..=1.0).contains(&score),
"score must be in [0,1], got {score}"
);
}
#[test]
fn test_black_to_white_transition_is_detectable() {
let det = ComplexityHistogramDetector::new();
let mut adaptive = AdaptiveThreshold::new(0.1, 0.5, 5);
let black = constant_rgb(0, 16, 16);
let _white = constant_rgb(255, 16, 16);
for _ in 0..5 {
let s = det.detect(&black, 16, 16);
adaptive.push_complexity(s);
}
let threshold = adaptive.threshold();
assert!(
threshold < 0.3,
"threshold for black frames should be low, got {threshold}"
);
let luma_diff = 1.0_f32;
assert!(
adaptive.is_scene_cut(luma_diff),
"black→white transition must be detected as a scene cut (threshold={threshold})"
);
}
#[test]
fn test_empty_window_returns_base_threshold() {
let at = AdaptiveThreshold::new(0.2, 0.6, 10);
assert!(
(at.threshold() - at.base_threshold).abs() < 1e-6,
"empty window must return base_threshold"
);
}
#[test]
fn test_high_complexity_raises_threshold() {
let mut at = AdaptiveThreshold::new(0.1, 0.6, 5);
for _ in 0..5 {
at.push_complexity(1.0); }
assert!(
at.threshold() > 0.5,
"max complexity should push threshold to max ({:.3})",
at.threshold()
);
}
#[test]
fn test_low_complexity_lowers_threshold() {
let mut at = AdaptiveThreshold::new(0.1, 0.6, 5);
for _ in 0..5 {
at.push_complexity(0.0); }
assert!(
(at.threshold() - 0.1).abs() < 1e-6,
"zero complexity should keep threshold at min, got {:.4}",
at.threshold()
);
}
#[test]
fn test_window_eviction_respects_window_size() {
let mut at = AdaptiveThreshold::new(0.0, 1.0, 3);
for _ in 0..10 {
at.push_complexity(0.5);
}
assert_eq!(at.window_len(), 3, "window must be capped at window_size");
}
#[test]
fn test_is_scene_cut_above_threshold() {
let at = AdaptiveThreshold::new(0.3, 0.7, 5); assert!(at.is_scene_cut(0.6));
}
#[test]
fn test_is_not_scene_cut_below_threshold() {
let at = AdaptiveThreshold::new(0.3, 0.7, 5); assert!(!at.is_scene_cut(0.4));
}
#[test]
fn test_reset_clears_window() {
let mut at = AdaptiveThreshold::new(0.2, 0.8, 10);
for i in 0..5 {
at.push_complexity(i as f32 * 0.1);
}
at.reset();
assert_eq!(at.window_len(), 0);
assert!(
(at.threshold() - at.base_threshold).abs() < 1e-6,
"after reset threshold must revert to base"
);
}
#[test]
fn test_threshold_interpolation() {
let mut at = AdaptiveThreshold::new(0.0, 1.0, 5);
for _ in 0..5 {
at.push_complexity(0.5);
}
let t = at.threshold();
assert!(
(t - 0.5).abs() < 1e-5,
"complexity=0.5 with range [0,1] should give threshold=0.5, got {t}"
);
}
}