oximedia_transcode/
scene_cut.rs1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CutDetectionMethod {
12 Threshold,
14 Histogram,
16 EdgeDiff,
18 PhaseCor,
20}
21
22impl CutDetectionMethod {
23 #[must_use]
25 pub fn typical_false_positive_rate(&self) -> f32 {
26 match self {
27 Self::Threshold => 0.15,
28 Self::Histogram => 0.08,
29 Self::EdgeDiff => 0.05,
30 Self::PhaseCor => 0.03,
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct SceneCut {
38 pub frame: u64,
40 pub confidence: f32,
42 pub method: CutDetectionMethod,
44}
45
46impl SceneCut {
47 #[must_use]
49 pub fn new(frame: u64, confidence: f32, method: CutDetectionMethod) -> Self {
50 Self {
51 frame,
52 confidence,
53 method,
54 }
55 }
56
57 #[must_use]
59 pub fn is_hard_cut(&self) -> bool {
60 self.confidence > 0.85
61 }
62}
63
64#[must_use]
70pub fn compute_histogram_diff(hist_a: &[u32], hist_b: &[u32]) -> f32 {
71 if hist_a.is_empty() || hist_a.len() != hist_b.len() {
72 return 0.0;
73 }
74 let total: u64 = hist_a.iter().map(|&v| u64::from(v)).sum();
75 if total == 0 {
76 return 0.0;
77 }
78 let sad: u64 = hist_a
79 .iter()
80 .zip(hist_b.iter())
81 .map(|(&a, &b)| (i64::from(a) - i64::from(b)).unsigned_abs())
82 .sum();
83 sad as f32 / total as f32
84}
85
86#[derive(Debug, Clone)]
88pub struct SceneCutDetector {
89 pub method: CutDetectionMethod,
91 pub threshold: f32,
93}
94
95impl Default for SceneCutDetector {
96 fn default() -> Self {
97 Self {
98 method: CutDetectionMethod::Histogram,
99 threshold: 0.4,
100 }
101 }
102}
103
104impl SceneCutDetector {
105 #[must_use]
107 pub fn new(method: CutDetectionMethod, threshold: f32) -> Self {
108 Self { method, threshold }
109 }
110
111 #[must_use]
117 pub fn detect_cuts(&self, frame_histograms: &[Vec<u32>]) -> Vec<SceneCut> {
118 let mut cuts = Vec::new();
119 for (i, pair) in frame_histograms.windows(2).enumerate() {
120 let diff = compute_histogram_diff(&pair[0], &pair[1]);
121 if diff >= self.threshold {
122 let confidence = diff.min(1.0);
124 cuts.push(SceneCut::new((i + 1) as u64, confidence, self.method));
125 }
126 }
127 cuts
128 }
129
130 #[must_use]
132 pub fn cut_count(cuts: &[SceneCut]) -> usize {
133 cuts.len()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
144 fn test_threshold_fpr() {
145 assert!((CutDetectionMethod::Threshold.typical_false_positive_rate() - 0.15).abs() < 1e-6);
146 }
147
148 #[test]
149 fn test_histogram_fpr() {
150 assert!((CutDetectionMethod::Histogram.typical_false_positive_rate() - 0.08).abs() < 1e-6);
151 }
152
153 #[test]
154 fn test_edge_diff_fpr() {
155 assert!((CutDetectionMethod::EdgeDiff.typical_false_positive_rate() - 0.05).abs() < 1e-6);
156 }
157
158 #[test]
159 fn test_phase_cor_fpr() {
160 assert!((CutDetectionMethod::PhaseCor.typical_false_positive_rate() - 0.03).abs() < 1e-6);
161 }
162
163 #[test]
166 fn test_scene_cut_is_hard_cut_true() {
167 let cut = SceneCut::new(5, 0.90, CutDetectionMethod::Histogram);
168 assert!(cut.is_hard_cut());
169 }
170
171 #[test]
172 fn test_scene_cut_is_hard_cut_false() {
173 let cut = SceneCut::new(5, 0.80, CutDetectionMethod::Histogram);
174 assert!(!cut.is_hard_cut());
175 }
176
177 #[test]
178 fn test_scene_cut_boundary_085() {
179 let cut = SceneCut::new(1, 0.85, CutDetectionMethod::Threshold);
181 assert!(!cut.is_hard_cut());
182 }
183
184 #[test]
187 fn test_histogram_diff_identical() {
188 let h = vec![10u32, 20, 30, 40];
189 assert!((compute_histogram_diff(&h, &h) - 0.0).abs() < 1e-6);
190 }
191
192 #[test]
193 fn test_histogram_diff_completely_different() {
194 let a = vec![100u32, 0, 0, 0];
195 let b = vec![0u32, 100, 0, 0];
196 let diff = compute_histogram_diff(&a, &b);
198 assert!((diff - 2.0).abs() < 1e-6);
199 }
200
201 #[test]
202 fn test_histogram_diff_empty() {
203 assert_eq!(compute_histogram_diff(&[], &[]), 0.0);
204 }
205
206 #[test]
207 fn test_histogram_diff_length_mismatch() {
208 let a = vec![1u32, 2];
209 let b = vec![1u32, 2, 3];
210 assert_eq!(compute_histogram_diff(&a, &b), 0.0);
211 }
212
213 #[test]
216 fn test_default_detector() {
217 let det = SceneCutDetector::default();
218 assert_eq!(det.method, CutDetectionMethod::Histogram);
219 assert!((det.threshold - 0.4).abs() < 1e-6);
220 }
221
222 #[test]
223 fn test_detect_no_cuts_identical_frames() {
224 let det = SceneCutDetector::default();
225 let frame = vec![50u32; 256];
226 let histograms = vec![frame.clone(), frame.clone(), frame.clone()];
227 let cuts = det.detect_cuts(&histograms);
228 assert!(cuts.is_empty());
229 }
230
231 #[test]
232 fn test_detect_single_cut() {
233 let det = SceneCutDetector::new(CutDetectionMethod::Histogram, 0.4);
234 let frame_a = {
236 let mut h = vec![0u32; 256];
237 h[0] = 1000;
238 h
239 };
240 let frame_b = {
241 let mut h = vec![0u32; 256];
242 h[255] = 1000;
243 h
244 };
245 let histograms = vec![frame_a, frame_b];
246 let cuts = det.detect_cuts(&histograms);
247 assert_eq!(cuts.len(), 1);
248 assert_eq!(cuts[0].frame, 1);
249 }
250
251 #[test]
252 fn test_cut_count_helper() {
253 let cuts = vec![
254 SceneCut::new(1, 0.9, CutDetectionMethod::Histogram),
255 SceneCut::new(5, 0.95, CutDetectionMethod::Histogram),
256 ];
257 assert_eq!(SceneCutDetector::cut_count(&cuts), 2);
258 }
259}