#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransitionType {
Cut,
Gradual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneBoundary {
pub frame_number: u64,
pub score: f32,
pub transition: TransitionType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubclipSpec {
pub start_frame: u64,
pub end_frame: u64,
pub label: String,
pub duration_frames: u64,
}
impl SubclipSpec {
fn new(start: u64, end: u64, index: usize) -> Self {
Self {
start_frame: start,
end_frame: end,
label: format!("Scene {}", index + 1),
duration_frames: end.saturating_sub(start) + 1,
}
}
}
#[must_use]
pub fn boundaries_to_subclips(boundaries: &[SceneBoundary], total_frames: u64) -> Vec<SubclipSpec> {
if total_frames == 0 {
return Vec::new();
}
let mut starts: Vec<u64> = std::iter::once(0)
.chain(boundaries.iter().map(|b| b.frame_number))
.collect();
starts.sort_unstable();
starts.dedup();
starts
.windows(2)
.enumerate()
.map(|(i, w)| SubclipSpec::new(w[0], w[1].saturating_sub(1), i))
.chain(std::iter::once_with(|| {
let last_start = *starts.last().unwrap_or(&0);
SubclipSpec::new(last_start, total_frames - 1, starts.len() - 1)
}))
.collect()
}
pub struct ThresholdSceneDetector {
pub threshold: f32,
pub gradual_threshold: Option<f32>,
pub min_scene_length: u64,
}
impl ThresholdSceneDetector {
#[must_use]
pub fn new(threshold: f32) -> Self {
Self {
threshold,
gradual_threshold: None,
min_scene_length: 1,
}
}
#[must_use]
pub fn with_gradual_threshold(mut self, t: f32) -> Self {
self.gradual_threshold = Some(t);
self
}
#[must_use]
pub fn with_min_scene_length(mut self, frames: u64) -> Self {
self.min_scene_length = frames.max(1);
self
}
#[must_use]
pub fn detect(&self, frames: &[Vec<f32>]) -> Vec<SceneBoundary> {
if frames.len() < 2 {
return Vec::new();
}
let mut boundaries = Vec::new();
let mut last_boundary: Option<u64> = None;
for i in 1..frames.len() {
let a = &frames[i - 1];
let b = &frames[i];
if a.len() != b.len() || a.is_empty() {
continue;
}
let n = a.len() as f32;
let mad: f32 = a
.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).abs())
.sum::<f32>()
/ n;
let frame = i as u64;
if let Some(prev) = last_boundary {
if (frame - prev) < self.min_scene_length {
continue;
}
}
let transition = if mad >= self.threshold {
TransitionType::Cut
} else if let Some(gt) = self.gradual_threshold {
if mad >= gt {
TransitionType::Gradual
} else {
continue;
}
} else {
continue;
};
boundaries.push(SceneBoundary {
frame_number: frame,
score: mad,
transition,
});
last_boundary = Some(frame);
}
boundaries
}
}
pub struct HistogramSceneDetector {
pub threshold: f32,
pub min_scene_length: u64,
}
impl HistogramSceneDetector {
#[must_use]
pub fn new(threshold: f32) -> Self {
Self {
threshold: threshold.clamp(0.0, 1.0),
min_scene_length: 1,
}
}
#[must_use]
pub fn with_min_scene_length(mut self, frames: u64) -> Self {
self.min_scene_length = frames.max(1);
self
}
#[must_use]
pub fn detect(&self, histograms: &[Vec<f32>]) -> Vec<SceneBoundary> {
if histograms.len() < 2 {
return Vec::new();
}
let mut boundaries = Vec::new();
let mut last_boundary: u64 = 0;
let min_intersection = 1.0 - self.threshold;
for i in 1..histograms.len() {
let h1 = &histograms[i - 1];
let h2 = &histograms[i];
if h1.len() != h2.len() || h1.is_empty() {
continue;
}
let intersection: f32 = h1.iter().zip(h2.iter()).map(|(a, b)| a.min(*b)).sum();
let frame = i as u64;
if (frame - last_boundary) < self.min_scene_length {
continue;
}
if intersection < min_intersection {
boundaries.push(SceneBoundary {
frame_number: frame,
score: 1.0 - intersection,
transition: TransitionType::Cut,
});
last_boundary = frame;
}
}
boundaries
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threshold_no_boundaries_same_frames() {
let frames = vec![vec![0.5_f32; 8]; 10];
let det = ThresholdSceneDetector::new(0.3);
assert!(det.detect(&frames).is_empty());
}
#[test]
fn test_threshold_hard_cut_detected() {
let mut frames: Vec<Vec<f32>> = vec![vec![0.0_f32; 16]; 3];
frames.push(vec![1.0_f32; 16]); frames.push(vec![1.0_f32; 16]);
let det = ThresholdSceneDetector::new(0.3);
let bd = det.detect(&frames);
assert_eq!(bd.len(), 1);
assert_eq!(bd[0].frame_number, 3);
assert_eq!(bd[0].transition, TransitionType::Cut);
}
#[test]
fn test_threshold_gradual_detected() {
let frames = vec![
vec![0.0_f32; 8],
vec![0.1_f32; 8], vec![1.0_f32; 8], ];
let det = ThresholdSceneDetector::new(0.5).with_gradual_threshold(0.08);
let bd = det.detect(&frames);
assert_eq!(bd.len(), 2);
assert_eq!(bd[0].transition, TransitionType::Gradual);
assert_eq!(bd[1].transition, TransitionType::Cut);
}
#[test]
fn test_threshold_min_scene_length_suppresses_spurious() {
let mut frames: Vec<Vec<f32>> = vec![vec![0.0_f32; 8]; 2];
frames.push(vec![1.0_f32; 8]); frames.push(vec![0.0_f32; 8]); frames.push(vec![1.0_f32; 8]);
let det = ThresholdSceneDetector::new(0.3).with_min_scene_length(3);
let bd = det.detect(&frames);
assert_eq!(bd[0].frame_number, 2);
}
#[test]
fn test_threshold_empty_input() {
let det = ThresholdSceneDetector::new(0.3);
assert!(det.detect(&[]).is_empty());
assert!(det.detect(&[vec![0.0_f32]]).is_empty());
}
#[test]
fn test_histogram_no_boundaries() {
let hist = vec![vec![0.25_f32; 4]; 5];
let det = HistogramSceneDetector::new(0.3);
assert!(det.detect(&hist).is_empty());
}
#[test]
fn test_histogram_cut_detected() {
let hist = vec![vec![1.0_f32, 0.0, 0.0, 0.0], vec![0.0_f32, 0.0, 0.0, 1.0]];
let det = HistogramSceneDetector::new(0.3);
let bd = det.detect(&hist);
assert_eq!(bd.len(), 1);
assert_eq!(bd[0].frame_number, 1);
}
#[test]
fn test_no_boundaries_single_subclip() {
let subs = boundaries_to_subclips(&[], 100);
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].start_frame, 0);
assert_eq!(subs[0].end_frame, 99);
}
#[test]
fn test_one_boundary_two_subclips() {
let bd = vec![SceneBoundary {
frame_number: 50,
score: 0.9,
transition: TransitionType::Cut,
}];
let subs = boundaries_to_subclips(&bd, 100);
assert_eq!(subs.len(), 2);
assert_eq!(subs[0].start_frame, 0);
assert_eq!(subs[0].end_frame, 49);
assert_eq!(subs[1].start_frame, 50);
assert_eq!(subs[1].end_frame, 99);
}
#[test]
fn test_zero_frames_empty_output() {
let subs = boundaries_to_subclips(&[], 0);
assert!(subs.is_empty());
}
#[test]
fn test_subclip_labels() {
let bd = vec![
SceneBoundary {
frame_number: 30,
score: 0.5,
transition: TransitionType::Cut,
},
SceneBoundary {
frame_number: 60,
score: 0.5,
transition: TransitionType::Cut,
},
];
let subs = boundaries_to_subclips(&bd, 90);
assert_eq!(subs[0].label, "Scene 1");
assert_eq!(subs[1].label, "Scene 2");
assert_eq!(subs[2].label, "Scene 3");
}
}