/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 | | } |