pub const SMALL_W: usize = 64;
pub const SMALL_H: usize = 36;
const BINS: usize = 32;
const WINDOW: usize = 24;
#[derive(Debug, Clone)]
pub struct SceneParams {
pub threshold: f32,
pub ratio: f32,
pub min_gap: f64,
}
impl Default for SceneParams {
fn default() -> Self {
Self {
threshold: 0.30,
ratio: 3.0,
min_gap: 1.0,
}
}
}
#[derive(Debug)]
pub struct SceneDetector {
params: SceneParams,
prev: Option<(Vec<u8>, [f32; BINS])>,
recent: std::collections::VecDeque<f32>,
last_cut: f64,
pub cuts: Vec<f64>,
seconds: Vec<(f32, f32, u32)>,
}
impl SceneDetector {
pub fn new(params: SceneParams) -> Self {
Self {
params,
prev: None,
recent: std::collections::VecDeque::with_capacity(WINDOW),
last_cut: f64::NEG_INFINITY,
cuts: Vec::new(),
seconds: Vec::new(),
}
}
pub fn push(&mut self, t: f64, luma: &[u8]) -> bool {
debug_assert_eq!(luma.len(), SMALL_W * SMALL_H);
let hist = histogram(luma);
#[allow(clippy::cast_precision_loss)]
let mean = luma.iter().map(|&v| f32::from(v)).sum::<f32>() / luma.len() as f32 / 255.0;
let mut cut = false;
let mut diff = 0.0;
if let Some((pl, ph)) = &self.prev {
#[allow(clippy::cast_precision_loss)]
let mad = pl
.iter()
.zip(luma)
.map(|(&a, &b)| f32::from(a.abs_diff(b)))
.sum::<f32>()
/ luma.len() as f32
/ 255.0;
let hd = ph
.iter()
.zip(&hist)
.map(|(a, b)| (a - b).abs())
.sum::<f32>()
/ 2.0;
let score = 0.5 * (mad * 3.0).min(1.0) + 0.5 * hd;
diff = mad;
let median = median(&self.recent);
if score > self.params.threshold
&& score > median * self.params.ratio
&& t - self.last_cut >= self.params.min_gap
{
cut = true;
self.last_cut = t;
self.cuts.push(t);
}
if self.recent.len() == WINDOW {
self.recent.pop_front();
}
self.recent.push_back(score);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let sec = t.max(0.0) as usize;
if self.seconds.len() <= sec {
self.seconds.resize(sec + 1, (0.0, 0.0, 0));
}
let s = &mut self.seconds[sec];
s.0 += (diff * 10.0).min(1.0);
s.1 += mean;
s.2 += 1;
self.prev = Some((luma.to_vec(), hist));
cut
}
pub fn per_second(&self) -> (Vec<f32>, Vec<f32>) {
let mut act = Vec::with_capacity(self.seconds.len());
let mut luma = Vec::with_capacity(self.seconds.len());
let (mut a, mut l) = (0.0, 0.0);
for &(sa, sl, n) in &self.seconds {
if n > 0 {
#[allow(clippy::cast_precision_loss)]
let n = n as f32;
(a, l) = (sa / n, sl / n);
}
act.push(round3(a));
luma.push(round3(l));
}
(act, luma)
}
}
fn round3(v: f32) -> f32 {
(v * 1000.0).round() / 1000.0
}
fn histogram(luma: &[u8]) -> [f32; BINS] {
let mut h = [0f32; BINS];
for &v in luma {
h[usize::from(v) * BINS / 256] += 1.0;
}
#[allow(clippy::cast_precision_loss)]
let n = luma.len() as f32;
for v in &mut h {
*v /= n;
}
h
}
fn median(v: &std::collections::VecDeque<f32>) -> f32 {
if v.is_empty() {
return 0.0;
}
let mut s: Vec<f32> = v.iter().copied().collect();
s.sort_by(f32::total_cmp);
s[s.len() / 2]
}
pub fn small_luma(rgb: &[u8], w: usize, h: usize) -> Vec<u8> {
let mut out = vec![0u8; SMALL_W * SMALL_H];
for (oy, row) in out.chunks_mut(SMALL_W).enumerate() {
let (y0, y1) = (
oy * h / SMALL_H,
((oy + 1) * h / SMALL_H).max(oy * h / SMALL_H + 1),
);
for (ox, px) in row.iter_mut().enumerate() {
let (x0, x1) = (
ox * w / SMALL_W,
((ox + 1) * w / SMALL_W).max(ox * w / SMALL_W + 1),
);
let (mut sum, mut n) = (0u32, 0u32);
for y in y0..y1.min(h) {
for x in x0..x1.min(w) {
let i = (y * w + x) * 3;
let (r, g, b) = (
u32::from(rgb[i]),
u32::from(rgb[i + 1]),
u32::from(rgb[i + 2]),
);
sum += (r * 54 + g * 183 + b * 19) >> 8;
n += 1;
}
}
#[allow(clippy::cast_possible_truncation)]
{
*px = (sum / n.max(1)) as u8;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(f: impl Fn(usize, usize) -> u8) -> Vec<u8> {
(0..SMALL_W * SMALL_H)
.map(|i| f(i % SMALL_W, i / SMALL_W))
.collect()
}
#[test]
fn hard_cut_fires_once() {
let mut d = SceneDetector::new(SceneParams::default());
for i in 0..40 {
let t = f64::from(i) * 0.25;
#[allow(clippy::cast_possible_truncation)]
let f = if t < 5.0 {
frame(|x, _| ((x + i as usize) * 3 % 200) as u8 + 40)
} else {
frame(|_, y| (y * 2) as u8)
};
d.push(t, &f);
}
assert_eq!(d.cuts, [5.0]);
let (act, luma) = d.per_second();
assert_eq!(act.len(), 10);
assert!(luma[0] > luma[9]);
}
#[test]
fn static_picture_has_no_activity() {
let mut d = SceneDetector::new(SceneParams::default());
let f = frame(|x, y| (x + y) as u8);
for i in 0..20 {
d.push(f64::from(i) * 0.5, &f);
}
assert_eq!(d.cuts, [] as [f64; 0]);
assert!(d.per_second().0.iter().all(|&a| a == 0.0));
}
#[test]
fn downsamples_rgb() {
let rgb = vec![255u8; 128 * 72 * 3];
assert!(small_luma(&rgb, 128, 72).iter().all(|&v| v == 255));
}
}