Coverage Report

Created: 2026-02-26 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/av-scenechange-0.14.1/src/analyze/mod.rs
Line
Count
Source
1
use std::{cmp, collections::BTreeMap, num::NonZeroUsize, sync::Arc};
2
3
use log::debug;
4
use num_rational::Rational32;
5
use v_frame::{
6
    frame::Frame,
7
    pixel::{ChromaSampling, Pixel},
8
    plane::Plane,
9
};
10
11
use self::fast::{detect_scale_factor, FAST_THRESHOLD};
12
use crate::{data::motion::RefMEStats, CpuFeatureLevel, SceneDetectionSpeed};
13
14
mod fast;
15
mod importance;
16
mod inter;
17
mod intra;
18
mod standard;
19
20
/// Experiments have determined this to be an optimal threshold
21
const IMP_BLOCK_DIFF_THRESHOLD: f64 = 7.0;
22
23
/// Fast integer division where divisor is a nonzero power of 2
24
0
pub(crate) fn fast_idiv(n: usize, d: NonZeroUsize) -> usize {
25
0
    debug_assert!(d.is_power_of_two());
26
27
0
    n >> d.trailing_zeros()
28
0
}
29
30
struct ScaleFunction<T: Pixel> {
31
    downscale_in_place: fn(/* &self: */ &Plane<T>, /* in_plane: */ &mut Plane<T>),
32
    downscale: fn(/* &self: */ &Plane<T>) -> Plane<T>,
33
    factor: NonZeroUsize,
34
}
35
36
impl<T: Pixel> ScaleFunction<T> {
37
0
    fn from_scale<const SCALE: usize>() -> Self {
38
0
        assert!(
39
0
            SCALE.is_power_of_two(),
40
0
            "Scaling factor needs to be a nonzero power of two"
41
        );
42
43
0
        Self {
44
0
            downscale: Plane::downscale::<SCALE>,
45
0
            downscale_in_place: Plane::downscale_in_place::<SCALE>,
46
0
            factor: NonZeroUsize::new(SCALE).unwrap(),
47
0
        }
48
0
    }
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u8>>::from_scale::<16>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u8>>::from_scale::<32>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u8>>::from_scale::<2>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u8>>::from_scale::<4>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u8>>::from_scale::<8>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u16>>::from_scale::<16>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u16>>::from_scale::<32>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u16>>::from_scale::<2>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u16>>::from_scale::<4>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<u16>>::from_scale::<8>
Unexecuted instantiation: <av_scenechange::analyze::ScaleFunction<_>>::from_scale::<_>
49
}
50
/// Runs keyframe detection on frames from the lookahead queue.
51
///
52
/// This struct is intended for advanced users who need the ability to analyze
53
/// a small subset of frames at a time, for example in a streaming fashion.
54
/// Most users will prefer to use `new_detector` and `detect_scene_changes`
55
/// at the top level of this crate.
56
pub struct SceneChangeDetector<T: Pixel> {
57
    // User configuration options
58
    /// Scenecut detection mode
59
    scene_detection_mode: SceneDetectionSpeed,
60
    /// Deque offset for current
61
    lookahead_offset: usize,
62
    /// Minimum number of frames between two scenecuts
63
    min_key_frame_interval: usize,
64
    /// Maximum number of frames between two scenecuts
65
    max_key_frame_interval: usize,
66
    /// The CPU feature level to be used.
67
    cpu_feature_level: CpuFeatureLevel,
68
69
    // Internal configuration options
70
    /// Minimum average difference between YUV deltas that will trigger a scene
71
    /// change.
72
    threshold: f64,
73
    /// Width and height of the unscaled frame
74
    resolution: (usize, usize),
75
    /// The bit depth of the video.
76
    bit_depth: usize,
77
    /// The frame rate of the video.
78
    frame_rate: Rational32,
79
    /// The chroma subsampling of the video.
80
    chroma_sampling: ChromaSampling,
81
    /// Number of pixels in scaled frame for fast mode
82
    scaled_pixels: usize,
83
    /// Downscaling function for fast scene detection
84
    scale_func: Option<ScaleFunction<T>>,
85
86
    // Internal data structures
87
    /// Start deque offset based on lookahead
88
    deque_offset: usize,
89
    /// Frame buffer for scaled frames
90
    downscaled_frame_buffer: Option<[Plane<T>; 2]>,
91
    /// Scenechange results for adaptive threshold
92
    score_deque: Vec<ScenecutResult>,
93
    /// Temporary buffer used by `estimate_intra_costs`.
94
    /// We store it on the struct so we only need to allocate it once.
95
    temp_plane: Option<Plane<T>>,
96
    /// Buffer for `FrameMEStats` for cost scenecut
97
    frame_me_stats_buffer: Option<RefMEStats>,
98
99
    /// Calculated intra costs for each input frame.
100
    /// These can be cached for reuse by advanced API users.
101
    /// Caching will occur if this is not `None`.
102
    pub intra_costs: Option<BTreeMap<usize, Box<[u32]>>>,
103
}
104
105
impl<T: Pixel> SceneChangeDetector<T> {
106
    /// Creates a new instance of the `SceneChangeDetector`.
107
    #[allow(clippy::too_many_arguments)]
108
    #[allow(clippy::missing_panics_doc)]
109
    #[inline]
110
0
    pub fn new(
111
0
        resolution: (usize, usize),
112
0
        bit_depth: usize,
113
0
        frame_rate: Rational32,
114
0
        chroma_sampling: ChromaSampling,
115
0
        lookahead_distance: usize,
116
0
        scene_detection_mode: SceneDetectionSpeed,
117
0
        min_key_frame_interval: usize,
118
0
        max_key_frame_interval: usize,
119
0
        cpu_feature_level: CpuFeatureLevel,
120
0
    ) -> Self {
121
        // Downscaling function for fast scene detection
122
0
        let scale_func = detect_scale_factor(resolution, scene_detection_mode);
123
124
        // Set lookahead offset to 5 if normal lookahead available
125
0
        let lookahead_offset = if lookahead_distance >= 5 { 5 } else { 0 };
126
0
        let deque_offset = lookahead_offset;
127
128
0
        let score_deque = Vec::with_capacity(5 + lookahead_distance);
129
130
        // Downscaling factor for fast scenedetect (is currently always a power of 2)
131
0
        let factor = scale_func.as_ref().map_or(
132
0
            NonZeroUsize::new(1).expect("constant should not panic"),
133
            |x| x.factor,
134
        );
135
136
0
        let pixels = if scene_detection_mode == SceneDetectionSpeed::Fast {
137
0
            fast_idiv(resolution.1, factor) * fast_idiv(resolution.0, factor)
138
        } else {
139
0
            1
140
        };
141
142
0
        let threshold = FAST_THRESHOLD * (bit_depth as f64) / 8.0;
143
144
0
        Self {
145
0
            threshold,
146
0
            scene_detection_mode,
147
0
            scale_func,
148
0
            lookahead_offset,
149
0
            deque_offset,
150
0
            score_deque,
151
0
            scaled_pixels: pixels,
152
0
            bit_depth,
153
0
            frame_rate,
154
0
            chroma_sampling,
155
0
            min_key_frame_interval,
156
0
            max_key_frame_interval,
157
0
            cpu_feature_level,
158
0
            downscaled_frame_buffer: None,
159
0
            resolution,
160
0
            temp_plane: None,
161
0
            frame_me_stats_buffer: None,
162
0
            intra_costs: None,
163
0
        }
164
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::new
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::new
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::new
165
166
    /// Enables caching of intra costs. For advanced API users.
167
    #[inline]
168
0
    pub fn enable_cache(&mut self) {
169
0
        if self.intra_costs.is_none() {
170
0
            self.intra_costs = Some(BTreeMap::new());
171
0
        }
172
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::enable_cache
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::enable_cache
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::enable_cache
173
174
    /// Runs keyframe detection on the next frame in the lookahead queue.
175
    ///
176
    /// This function requires that a subset of input frames
177
    /// is passed to it in order, and that `keyframes` is only
178
    /// updated from this method. `input_frameno` should correspond
179
    /// to the second frame in `frame_set`.
180
    ///
181
    /// This will gracefully handle the first frame in the video as well.
182
    #[inline]
183
0
    pub fn analyze_next_frame(
184
0
        &mut self,
185
0
        frame_set: &[&Arc<Frame<T>>],
186
0
        input_frameno: usize,
187
0
        previous_keyframe: usize,
188
0
    ) -> bool {
189
        // Use score deque for adaptive threshold for scene cut
190
        // Declare score_deque offset based on lookahead  for scene change scores
191
192
        // Find the distance to the previous keyframe.
193
0
        let distance = input_frameno - previous_keyframe;
194
195
0
        if frame_set.len() <= self.lookahead_offset {
196
            // Don't insert keyframes in the last few frames of the video
197
            // This is basically a scene flash and a waste of bits
198
0
            return false;
199
0
        }
200
201
0
        if self.scene_detection_mode == SceneDetectionSpeed::None {
202
0
            if let Some(true) = self.handle_min_max_intervals(distance) {
203
0
                return true;
204
0
            };
205
0
            return false;
206
0
        }
207
208
        // Initialization of score deque
209
        // based on frame set length
210
0
        if self.deque_offset > 0
211
0
            && frame_set.len() > self.deque_offset + 1
212
0
            && self.score_deque.is_empty()
213
0
        {
214
0
            self.initialize_score_deque(frame_set, input_frameno, self.deque_offset);
215
0
        } else if self.score_deque.is_empty() {
216
0
            self.initialize_score_deque(frame_set, input_frameno, frame_set.len() - 1);
217
0
218
0
            self.deque_offset = frame_set.len() - 2;
219
0
        }
220
        // Running single frame comparison and adding it to deque
221
        // Decrease deque offset if there is no new frames
222
0
        if frame_set.len() > self.deque_offset + 1 {
223
0
            self.run_comparison(
224
0
                frame_set[self.deque_offset].clone(),
225
0
                frame_set[self.deque_offset + 1].clone(),
226
0
                input_frameno + self.deque_offset,
227
0
            );
228
0
        } else {
229
0
            self.deque_offset -= 1;
230
0
        }
231
232
        // Adaptive scenecut check
233
0
        let (scenecut, score) = self.adaptive_scenecut();
234
0
        let scenecut = self.handle_min_max_intervals(distance).unwrap_or(scenecut);
235
0
        debug!(
236
0
            "[SC-Detect] Frame {}: Raw={:5.1}  ImpBl={:5.1}  Bwd={:5.1}  Fwd={:5.1}  Th={:.1}  {}",
237
            input_frameno,
238
            score.inter_cost,
239
            score.imp_block_cost,
240
            score.backward_adjusted_cost,
241
            score.forward_adjusted_cost,
242
            score.threshold,
243
0
            if scenecut { "Scenecut" } else { "No cut" }
244
        );
245
246
        // Keep score deque of 5 backward frames
247
        // and forward frames of length of lookahead offset
248
0
        if self.score_deque.len() > 5 + self.lookahead_offset {
249
0
            self.score_deque.pop();
250
0
        }
251
252
0
        scenecut
253
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::analyze_next_frame
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::analyze_next_frame
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::analyze_next_frame
254
255
0
    fn handle_min_max_intervals(&mut self, distance: usize) -> Option<bool> {
256
        // Handle minimum and maximum keyframe intervals.
257
0
        if distance < self.min_key_frame_interval {
258
0
            return Some(false);
259
0
        }
260
0
        if distance >= self.max_key_frame_interval {
261
0
            return Some(true);
262
0
        }
263
0
        None
264
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::handle_min_max_intervals
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::handle_min_max_intervals
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::handle_min_max_intervals
265
266
    // Initially fill score deque with frame scores
267
0
    fn initialize_score_deque(
268
0
        &mut self,
269
0
        frame_set: &[&Arc<Frame<T>>],
270
0
        input_frameno: usize,
271
0
        init_len: usize,
272
0
    ) {
273
0
        for x in 0..init_len {
274
0
            self.run_comparison(
275
0
                frame_set[x].clone(),
276
0
                frame_set[x + 1].clone(),
277
0
                input_frameno + x,
278
0
            );
279
0
        }
280
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::initialize_score_deque
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::initialize_score_deque
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::initialize_score_deque
281
282
    /// Runs scene change comparison between 2 given frames
283
    /// Insert result to start of score deque
284
0
    fn run_comparison(
285
0
        &mut self,
286
0
        frame1: Arc<Frame<T>>,
287
0
        frame2: Arc<Frame<T>>,
288
0
        input_frameno: usize,
289
0
    ) {
290
0
        let mut result = match self.scene_detection_mode {
291
0
            SceneDetectionSpeed::Fast => self.fast_scenecut(frame1, frame2),
292
0
            SceneDetectionSpeed::Standard => self.cost_scenecut(frame1, frame2, input_frameno),
293
0
            _ => unreachable!(),
294
        };
295
296
        // Subtract the highest metric value of surrounding frames from the current one.
297
        // It makes the peaks in the metric more distinct.
298
0
        if self.scene_detection_mode == SceneDetectionSpeed::Standard && self.deque_offset > 0 {
299
0
            if input_frameno == 1 {
300
0
                // Accounts for the second frame not having a score to adjust against.
301
0
                // It should always be 0 because the first frame of the video is always a
302
0
                // keyframe.
303
0
                result.backward_adjusted_cost = 0.0;
304
0
            } else {
305
0
                let mut adjusted_cost = f64::MAX;
306
0
                for other_cost in self
307
0
                    .score_deque
308
0
                    .iter()
309
0
                    .take(self.deque_offset)
310
0
                    .map(|i| i.inter_cost)
311
                {
312
0
                    let this_cost = result.inter_cost - other_cost;
313
0
                    if this_cost < adjusted_cost {
314
0
                        adjusted_cost = this_cost;
315
0
                    }
316
0
                    if adjusted_cost < 0.0 {
317
0
                        adjusted_cost = 0.0;
318
0
                        break;
319
0
                    }
320
                }
321
0
                result.backward_adjusted_cost = adjusted_cost;
322
            }
323
0
            if !self.score_deque.is_empty() {
324
0
                for i in 0..cmp::min(self.deque_offset, self.score_deque.len()) {
325
0
                    let adjusted_cost = self.score_deque[i].inter_cost - result.inter_cost;
326
0
                    if i == 0 || adjusted_cost < self.score_deque[i].forward_adjusted_cost {
327
0
                        self.score_deque[i].forward_adjusted_cost = adjusted_cost;
328
0
                    }
329
0
                    if self.score_deque[i].forward_adjusted_cost < 0.0 {
330
0
                        self.score_deque[i].forward_adjusted_cost = 0.0;
331
0
                    }
332
                }
333
0
            }
334
0
        }
335
0
        self.score_deque.insert(0, result);
336
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::run_comparison
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::run_comparison
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::run_comparison
337
338
    /// Compares current scene score to adapted threshold based on previous
339
    /// scores
340
    ///
341
    /// Value of current frame is offset by lookahead, if lookahead >=5
342
    ///
343
    /// Returns true if current scene score is higher than adapted threshold
344
0
    fn adaptive_scenecut(&mut self) -> (bool, ScenecutResult) {
345
0
        let score = self.score_deque[self.deque_offset];
346
347
        // We use the importance block algorithm's cost metrics as a secondary algorithm
348
        // because, although it struggles in certain scenarios such as
349
        // finding the end of a pan, it is very good at detecting hard scenecuts
350
        // or detecting if a pan exists.
351
        //
352
        // Because of this, we only consider a frame for a scenechange if
353
        // the importance block algorithm is over the threshold either on this frame
354
        // (hard scenecut) or within the past few frames (pan). This helps
355
        // filter out a few false positives produced by the cost-based
356
        // algorithm.
357
0
        let imp_block_threshold = IMP_BLOCK_DIFF_THRESHOLD * (self.bit_depth as f64) / 8.0;
358
0
        if !&self.score_deque[self.deque_offset..]
359
0
            .iter()
360
0
            .any(|result| result.imp_block_cost >= imp_block_threshold)
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::adaptive_scenecut::{closure#0}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::adaptive_scenecut::{closure#0}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::adaptive_scenecut::{closure#0}
361
        {
362
0
            return (false, score);
363
0
        }
364
365
0
        let cost = score.forward_adjusted_cost;
366
0
        if cost >= score.threshold {
367
0
            let back_deque = &self.score_deque[self.deque_offset + 1..];
368
0
            let forward_deque = &self.score_deque[..self.deque_offset];
369
0
            let back_over_tr_count = back_deque
370
0
                .iter()
371
0
                .filter(|result| result.backward_adjusted_cost >= result.threshold)
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::adaptive_scenecut::{closure#1}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::adaptive_scenecut::{closure#1}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::adaptive_scenecut::{closure#1}
372
0
                .count();
373
0
            let forward_over_tr_count = forward_deque
374
0
                .iter()
375
0
                .filter(|result| result.forward_adjusted_cost >= result.threshold)
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::adaptive_scenecut::{closure#2}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::adaptive_scenecut::{closure#2}
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::adaptive_scenecut::{closure#2}
376
0
                .count();
377
378
            // Check for scenecut after the flashes
379
            // No frames over threshold forward
380
            // and some frames over threshold backward
381
0
            let back_count_req = if self.scene_detection_mode == SceneDetectionSpeed::Fast {
382
                // Fast scenecut is more sensitive to false flash detection,
383
                // so we want more "evidence" of there being a flash before creating a keyframe.
384
0
                2
385
            } else {
386
0
                1
387
            };
388
0
            if forward_over_tr_count == 0 && back_over_tr_count >= back_count_req {
389
0
                return (true, score);
390
0
            }
391
392
            // Check for scenecut before flash
393
            // If distance longer than max flash length
394
0
            if back_over_tr_count == 0
395
0
                && forward_over_tr_count == 1
396
0
                && forward_deque[0].forward_adjusted_cost >= forward_deque[0].threshold
397
            {
398
0
                return (true, score);
399
0
            }
400
401
0
            if back_over_tr_count != 0 || forward_over_tr_count != 0 {
402
0
                return (false, score);
403
0
            }
404
0
        }
405
406
0
        (cost >= score.threshold, score)
407
0
    }
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u16>>::adaptive_scenecut
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<u8>>::adaptive_scenecut
Unexecuted instantiation: <av_scenechange::analyze::SceneChangeDetector<_>>::adaptive_scenecut
408
}
409
410
#[derive(Debug, Clone, Copy)]
411
struct ScenecutResult {
412
    inter_cost: f64,
413
    imp_block_cost: f64,
414
    backward_adjusted_cost: f64,
415
    forward_adjusted_cost: f64,
416
    threshold: f64,
417
}