Skip to main content

oximedia_codec/multipass/
complexity.rs

1//! Frame complexity analysis for multi-pass encoding.
2//!
3//! This module provides advanced frame complexity metrics used in multi-pass
4//! encoding to make better bitrate allocation decisions.
5
6#![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/// Frame complexity metrics for bitrate allocation.
15#[derive(Clone, Debug)]
16pub struct FrameComplexity {
17    /// Frame index in the stream.
18    pub frame_index: u64,
19    /// Frame type (Key, Inter, BiDir).
20    pub frame_type: FrameType,
21    /// Spatial complexity (0.0-1.0, based on variance).
22    pub spatial_complexity: f64,
23    /// Temporal complexity (0.0-1.0, based on motion).
24    pub temporal_complexity: f64,
25    /// Combined complexity metric.
26    pub combined_complexity: f64,
27    /// Sum of Absolute Differences with previous frame.
28    pub sad: u64,
29    /// Average luma variance across blocks.
30    pub variance: f64,
31    /// Estimated encoding difficulty (1.0 = average).
32    pub encoding_difficulty: f64,
33    /// Is this frame a scene change.
34    pub is_scene_change: bool,
35}
36
37impl FrameComplexity {
38    /// Create a new frame complexity with default values.
39    #[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    /// Calculate relative difficulty compared to average frame.
55    #[must_use]
56    pub fn relative_difficulty(&self) -> f64 {
57        self.encoding_difficulty
58    }
59}
60
61/// Complexity analyzer for video frames.
62pub 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    /// Create a new complexity analyzer.
74    #[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    /// Set the scene change detection threshold.
88    pub fn set_scene_change_threshold(&mut self, threshold: f64) {
89        self.scene_change_threshold = threshold.clamp(0.0, 1.0);
90    }
91
92    /// Analyze a video frame and compute complexity metrics.
93    #[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        // Get luma plane
98        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            // Calculate spatial complexity
103            complexity.spatial_complexity = self.compute_spatial_complexity(luma_data, stride);
104            complexity.variance = self.compute_variance(luma_data, stride);
105
106            // Calculate temporal complexity
107            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            // Store current frame for next iteration
117            self.prev_frame = Some(luma_data.to_vec());
118
119            // Update history
120            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            // Calculate combined complexity
128            complexity.combined_complexity = self.compute_combined_complexity(&complexity);
129
130            // Estimate encoding difficulty
131            complexity.encoding_difficulty = self.estimate_difficulty(&complexity);
132        }
133
134        complexity
135    }
136
137    /// Compute spatial complexity using block-based variance.
138    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        // Normalize to 0-1 range (assuming max variance ~1000)
163        (avg_variance / 1000.0).min(1.0)
164    }
165
166    /// Compute block variance.
167    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    /// Compute overall frame variance.
207    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    /// Compute Sum of Absolute Differences between frames.
237    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    /// Compute temporal complexity from SAD value.
256    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        // Average SAD per pixel
263        let avg_sad = sad as f64 / pixels as f64;
264        // Normalize to 0-1 range (assuming max avg SAD ~50)
265        (avg_sad / 50.0).min(1.0)
266    }
267
268    /// Detect scene changes based on SAD threshold.
269    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    /// Compute combined complexity metric.
281    fn compute_combined_complexity(&self, complexity: &FrameComplexity) -> f64 {
282        // Weight spatial and temporal components
283        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    /// Estimate encoding difficulty based on complexity metrics.
291    fn estimate_difficulty(&self, complexity: &FrameComplexity) -> f64 {
292        let mut difficulty = 1.0;
293
294        // Base difficulty on combined complexity
295        difficulty *= 0.5 + complexity.combined_complexity;
296
297        // Adjust for frame type
298        difficulty *= match complexity.frame_type {
299            FrameType::Key => 2.0,    // Keyframes are more expensive
300            FrameType::Inter => 1.0,  // Inter frames are baseline
301            FrameType::BiDir => 0.8,  // B-frames can be cheaper
302            FrameType::Switch => 1.5, // Switch frames need extra bits
303        };
304
305        // Scene changes require more bits
306        if complexity.is_scene_change {
307            difficulty *= 1.5;
308        }
309
310        // Normalize against historical average
311        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    /// Reset the analyzer state.
327    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); // Solid color = low spatial
365        assert!(complexity.variance < 1.0); // Very low variance
366    }
367
368    #[test]
369    fn test_scene_change_detection() {
370        let mut analyzer = ComplexityAnalyzer::new(320, 240);
371
372        // First frame
373        let frame1 = create_test_frame(320, 240, 0);
374        let complexity1 = analyzer.analyze(&frame1, 0);
375        assert!(complexity1.is_scene_change); // First frame is always scene change
376
377        // Very different second frame
378        let frame2 = create_test_frame(320, 240, 255);
379        let complexity2 = analyzer.analyze(&frame2, 1);
380        assert!(complexity2.is_scene_change); // Should detect big difference
381    }
382
383    #[test]
384    fn test_no_scene_change() {
385        let mut analyzer = ComplexityAnalyzer::new(320, 240);
386
387        // First frame
388        let frame1 = create_test_frame(320, 240, 128);
389        let _ = analyzer.analyze(&frame1, 0);
390
391        // Similar second frame
392        let frame2 = create_test_frame(320, 240, 130);
393        let complexity2 = analyzer.analyze(&frame2, 1);
394        assert!(!complexity2.is_scene_change); // Should not detect scene change
395    }
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}