oximedia_codec/multipass/
complexity.rs1#![forbid(unsafe_code)]
7#![allow(clippy::cast_precision_loss)]
8#![allow(clippy::cast_possible_truncation)]
9#![allow(clippy::cast_sign_loss)]
10#![allow(clippy::cast_lossless)]
11
12use crate::frame::{FrameType, VideoFrame};
13
14#[derive(Clone, Debug)]
16pub struct FrameComplexity {
17 pub frame_index: u64,
19 pub frame_type: FrameType,
21 pub spatial_complexity: f64,
23 pub temporal_complexity: f64,
25 pub combined_complexity: f64,
27 pub sad: u64,
29 pub variance: f64,
31 pub encoding_difficulty: f64,
33 pub is_scene_change: bool,
35}
36
37impl FrameComplexity {
38 #[must_use]
40 pub fn new(frame_index: u64, frame_type: FrameType) -> Self {
41 Self {
42 frame_index,
43 frame_type,
44 spatial_complexity: 0.5,
45 temporal_complexity: 0.5,
46 combined_complexity: 0.5,
47 sad: 0,
48 variance: 0.0,
49 encoding_difficulty: 1.0,
50 is_scene_change: false,
51 }
52 }
53
54 #[must_use]
56 pub fn relative_difficulty(&self) -> f64 {
57 self.encoding_difficulty
58 }
59}
60
61pub struct ComplexityAnalyzer {
63 width: u32,
64 height: u32,
65 block_size: usize,
66 prev_frame: Option<Vec<u8>>,
67 spatial_history: Vec<f64>,
68 temporal_history: Vec<f64>,
69 scene_change_threshold: f64,
70}
71
72impl ComplexityAnalyzer {
73 #[must_use]
75 pub fn new(width: u32, height: u32) -> Self {
76 Self {
77 width,
78 height,
79 block_size: 16,
80 prev_frame: None,
81 spatial_history: Vec::new(),
82 temporal_history: Vec::new(),
83 scene_change_threshold: 0.4,
84 }
85 }
86
87 pub fn set_scene_change_threshold(&mut self, threshold: f64) {
89 self.scene_change_threshold = threshold.clamp(0.0, 1.0);
90 }
91
92 #[must_use]
94 pub fn analyze(&mut self, frame: &VideoFrame, frame_index: u64) -> FrameComplexity {
95 let mut complexity = FrameComplexity::new(frame_index, frame.frame_type);
96
97 if let Some(luma_plane) = frame.planes.first() {
99 let luma_data = luma_plane.data.as_ref();
100 let stride = luma_plane.stride;
101
102 complexity.spatial_complexity = self.compute_spatial_complexity(luma_data, stride);
104 complexity.variance = self.compute_variance(luma_data, stride);
105
106 if let Some(prev) = &self.prev_frame {
108 complexity.sad = self.compute_sad(luma_data, prev, stride);
109 complexity.temporal_complexity = self.compute_temporal_complexity(complexity.sad);
110 complexity.is_scene_change = self.detect_scene_change(complexity.sad);
111 } else {
112 complexity.temporal_complexity = 1.0;
113 complexity.is_scene_change = true;
114 }
115
116 self.prev_frame = Some(luma_data.to_vec());
118
119 self.spatial_history.push(complexity.spatial_complexity);
121 self.temporal_history.push(complexity.temporal_complexity);
122 if self.spatial_history.len() > 100 {
123 self.spatial_history.remove(0);
124 self.temporal_history.remove(0);
125 }
126
127 complexity.combined_complexity = self.compute_combined_complexity(&complexity);
129
130 complexity.encoding_difficulty = self.estimate_difficulty(&complexity);
132 }
133
134 complexity
135 }
136
137 fn compute_spatial_complexity(&self, luma: &[u8], stride: usize) -> f64 {
139 let blocks_x = (self.width as usize) / self.block_size;
140 let blocks_y = (self.height as usize) / self.block_size;
141
142 if blocks_x == 0 || blocks_y == 0 {
143 return 0.5;
144 }
145
146 let mut total_variance = 0.0;
147 let mut block_count = 0;
148
149 for by in 0..blocks_y {
150 for bx in 0..blocks_x {
151 let variance = self.compute_block_variance(luma, stride, bx, by);
152 total_variance += variance;
153 block_count += 1;
154 }
155 }
156
157 if block_count == 0 {
158 return 0.5;
159 }
160
161 let avg_variance = total_variance / block_count as f64;
162 (avg_variance / 1000.0).min(1.0)
164 }
165
166 fn compute_block_variance(&self, luma: &[u8], stride: usize, bx: usize, by: usize) -> f64 {
168 let start_x = bx * self.block_size;
169 let start_y = by * self.block_size;
170
171 let mut sum = 0u64;
172 let mut sum_sq = 0u64;
173 let mut count = 0u64;
174
175 for y in 0..self.block_size {
176 let row_y = start_y + y;
177 if row_y >= self.height as usize {
178 break;
179 }
180
181 for x in 0..self.block_size {
182 let col_x = start_x + x;
183 if col_x >= self.width as usize {
184 break;
185 }
186
187 let idx = row_y * stride + col_x;
188 if idx < luma.len() {
189 let pixel = luma[idx] as u64;
190 sum += pixel;
191 sum_sq += pixel * pixel;
192 count += 1;
193 }
194 }
195 }
196
197 if count == 0 {
198 return 0.0;
199 }
200
201 let mean = sum as f64 / count as f64;
202 let mean_sq = sum_sq as f64 / count as f64;
203 (mean_sq - mean * mean).max(0.0)
204 }
205
206 fn compute_variance(&self, luma: &[u8], stride: usize) -> f64 {
208 let height = self.height as usize;
209 let width = self.width as usize;
210
211 let mut sum = 0u64;
212 let mut sum_sq = 0u64;
213 let mut count = 0u64;
214
215 for y in 0..height {
216 for x in 0..width {
217 let idx = y * stride + x;
218 if idx < luma.len() {
219 let pixel = luma[idx] as u64;
220 sum += pixel;
221 sum_sq += pixel * pixel;
222 count += 1;
223 }
224 }
225 }
226
227 if count == 0 {
228 return 0.0;
229 }
230
231 let mean = sum as f64 / count as f64;
232 let mean_sq = sum_sq as f64 / count as f64;
233 (mean_sq - mean * mean).max(0.0)
234 }
235
236 fn compute_sad(&self, current: &[u8], previous: &[u8], stride: usize) -> u64 {
238 let height = self.height as usize;
239 let width = self.width as usize;
240 let mut sad = 0u64;
241
242 for y in 0..height {
243 for x in 0..width {
244 let idx = y * stride + x;
245 if idx < current.len() && idx < previous.len() {
246 let diff = (current[idx] as i32 - previous[idx] as i32).abs();
247 sad += diff as u64;
248 }
249 }
250 }
251
252 sad
253 }
254
255 fn compute_temporal_complexity(&self, sad: u64) -> f64 {
257 let pixels = (self.width as u64) * (self.height as u64);
258 if pixels == 0 {
259 return 0.5;
260 }
261
262 let avg_sad = sad as f64 / pixels as f64;
264 (avg_sad / 50.0).min(1.0)
266 }
267
268 fn detect_scene_change(&self, sad: u64) -> bool {
270 let pixels = (self.width as u64) * (self.height as u64);
271 if pixels == 0 {
272 return false;
273 }
274
275 let avg_sad = sad as f64 / pixels as f64;
276 let normalized_sad = (avg_sad / 50.0).min(1.0);
277 normalized_sad > self.scene_change_threshold
278 }
279
280 fn compute_combined_complexity(&self, complexity: &FrameComplexity) -> f64 {
282 let spatial_weight = 0.6;
284 let temporal_weight = 0.4;
285
286 spatial_weight * complexity.spatial_complexity
287 + temporal_weight * complexity.temporal_complexity
288 }
289
290 fn estimate_difficulty(&self, complexity: &FrameComplexity) -> f64 {
292 let mut difficulty = 1.0;
293
294 difficulty *= 0.5 + complexity.combined_complexity;
296
297 difficulty *= match complexity.frame_type {
299 FrameType::Key => 2.0, FrameType::Inter => 1.0, FrameType::BiDir => 0.8, FrameType::Switch => 1.5, };
304
305 if complexity.is_scene_change {
307 difficulty *= 1.5;
308 }
309
310 if !self.spatial_history.is_empty() {
312 let avg_spatial: f64 =
313 self.spatial_history.iter().sum::<f64>() / self.spatial_history.len() as f64;
314 let avg_temporal: f64 =
315 self.temporal_history.iter().sum::<f64>() / self.temporal_history.len() as f64;
316
317 let historical_avg = 0.6 * avg_spatial + 0.4 * avg_temporal;
318 if historical_avg > 0.01 {
319 difficulty *= complexity.combined_complexity / historical_avg;
320 }
321 }
322
323 difficulty.max(0.1).min(10.0)
324 }
325
326 pub fn reset(&mut self) {
328 self.prev_frame = None;
329 self.spatial_history.clear();
330 self.temporal_history.clear();
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::frame::Plane;
338 use oximedia_core::{PixelFormat, Rational, Timestamp};
339
340 fn create_test_frame(width: u32, height: u32, value: u8) -> VideoFrame {
341 let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
342 let size = (width * height) as usize;
343 let data = vec![value; size];
344 frame.planes.push(Plane::new(data, width as usize));
345 frame.timestamp = Timestamp::new(0, Rational::new(1, 30));
346 frame
347 }
348
349 #[test]
350 fn test_complexity_analyzer_new() {
351 let analyzer = ComplexityAnalyzer::new(1920, 1080);
352 assert_eq!(analyzer.width, 1920);
353 assert_eq!(analyzer.height, 1080);
354 assert_eq!(analyzer.block_size, 16);
355 }
356
357 #[test]
358 fn test_analyze_solid_frame() {
359 let mut analyzer = ComplexityAnalyzer::new(320, 240);
360 let frame = create_test_frame(320, 240, 128);
361
362 let complexity = analyzer.analyze(&frame, 0);
363 assert_eq!(complexity.frame_index, 0);
364 assert!(complexity.spatial_complexity < 0.1); assert!(complexity.variance < 1.0); }
367
368 #[test]
369 fn test_scene_change_detection() {
370 let mut analyzer = ComplexityAnalyzer::new(320, 240);
371
372 let frame1 = create_test_frame(320, 240, 0);
374 let complexity1 = analyzer.analyze(&frame1, 0);
375 assert!(complexity1.is_scene_change); let frame2 = create_test_frame(320, 240, 255);
379 let complexity2 = analyzer.analyze(&frame2, 1);
380 assert!(complexity2.is_scene_change); }
382
383 #[test]
384 fn test_no_scene_change() {
385 let mut analyzer = ComplexityAnalyzer::new(320, 240);
386
387 let frame1 = create_test_frame(320, 240, 128);
389 let _ = analyzer.analyze(&frame1, 0);
390
391 let frame2 = create_test_frame(320, 240, 130);
393 let complexity2 = analyzer.analyze(&frame2, 1);
394 assert!(!complexity2.is_scene_change); }
396
397 #[test]
398 fn test_encoding_difficulty() {
399 let mut analyzer = ComplexityAnalyzer::new(320, 240);
400 let frame = create_test_frame(320, 240, 128);
401
402 let complexity = analyzer.analyze(&frame, 0);
403 assert!(complexity.encoding_difficulty > 0.0);
404 assert!(complexity.encoding_difficulty <= 10.0);
405 }
406}