Skip to main content

oximedia_optimize/
utils.rs

1//! Utility functions and helpers for optimization.
2
3use std::time::{Duration, Instant};
4
5/// Block-level metrics.
6#[derive(Debug, Clone, Copy, Default)]
7pub struct BlockMetrics {
8    /// Block width.
9    pub width: usize,
10    /// Block height.
11    pub height: usize,
12    /// Sum of Absolute Differences.
13    pub sad: u32,
14    /// Sum of Squared Errors.
15    pub sse: u64,
16    /// Variance.
17    pub variance: f64,
18    /// Mean value.
19    pub mean: f64,
20}
21
22impl BlockMetrics {
23    /// Calculates metrics for a block.
24    #[must_use]
25    pub fn calculate(pixels: &[u8], width: usize, height: usize) -> Self {
26        let mut metrics = Self {
27            width,
28            height,
29            ..Default::default()
30        };
31
32        if pixels.is_empty() {
33            return metrics;
34        }
35
36        // Calculate mean
37        metrics.mean = pixels.iter().map(|&p| f64::from(p)).sum::<f64>() / pixels.len() as f64;
38
39        // Calculate variance and SSE
40        metrics.variance = pixels
41            .iter()
42            .map(|&p| {
43                let diff = f64::from(p) - metrics.mean;
44                diff * diff
45            })
46            .sum::<f64>()
47            / pixels.len() as f64;
48
49        metrics.sse = pixels
50            .iter()
51            .map(|&p| {
52                let diff = i32::from(p) - metrics.mean as i32;
53                (diff * diff) as u64
54            })
55            .sum();
56
57        metrics
58    }
59
60    /// Calculates PSNR from SSE.
61    #[must_use]
62    pub fn psnr(&self) -> f64 {
63        if self.sse == 0 {
64            return f64::INFINITY;
65        }
66
67        let num_pixels = self.width * self.height;
68        let mse = self.sse as f64 / num_pixels as f64;
69        10.0 * (255.0 * 255.0 / mse).log10()
70    }
71
72    /// Checks if block is flat.
73    #[must_use]
74    pub fn is_flat(&self, threshold: f64) -> bool {
75        self.variance < threshold
76    }
77
78    /// Checks if block is textured.
79    #[must_use]
80    pub fn is_textured(&self, threshold: f64) -> bool {
81        self.variance > threshold
82    }
83}
84
85/// Frame-level metrics.
86#[derive(Debug, Clone, Default)]
87pub struct FrameMetrics {
88    /// Frame width.
89    pub width: usize,
90    /// Frame height.
91    pub height: usize,
92    /// Total bits used.
93    pub total_bits: u64,
94    /// QP values used.
95    pub qp_values: Vec<u8>,
96    /// Average QP.
97    pub avg_qp: f64,
98    /// PSNR.
99    pub psnr: f64,
100    /// SSIM.
101    pub ssim: f64,
102    /// Encoding time.
103    pub encoding_time: Duration,
104}
105
106impl FrameMetrics {
107    /// Creates a new frame metrics.
108    #[must_use]
109    pub fn new(width: usize, height: usize) -> Self {
110        Self {
111            width,
112            height,
113            ..Default::default()
114        }
115    }
116
117    /// Adds a QP value.
118    pub fn add_qp(&mut self, qp: u8) {
119        self.qp_values.push(qp);
120        self.avg_qp =
121            self.qp_values.iter().map(|&q| f64::from(q)).sum::<f64>() / self.qp_values.len() as f64;
122    }
123
124    /// Sets encoding time.
125    pub fn set_encoding_time(&mut self, duration: Duration) {
126        self.encoding_time = duration;
127    }
128
129    /// Calculates bits per pixel.
130    #[must_use]
131    pub fn bits_per_pixel(&self) -> f64 {
132        let num_pixels = (self.width * self.height) as f64;
133        if num_pixels > 0.0 {
134            self.total_bits as f64 / num_pixels
135        } else {
136            0.0
137        }
138    }
139
140    /// Calculates encoding speed in FPS.
141    #[must_use]
142    pub fn encoding_fps(&self) -> f64 {
143        let secs = self.encoding_time.as_secs_f64();
144        if secs > 0.0 {
145            1.0 / secs
146        } else {
147            0.0
148        }
149    }
150}
151
152/// Optimization statistics.
153#[derive(Debug, Clone, Default)]
154pub struct OptimizationStats {
155    /// Number of frames encoded.
156    pub frames_encoded: usize,
157    /// Total encoding time.
158    pub total_time: Duration,
159    /// Per-frame metrics.
160    pub frame_metrics: Vec<FrameMetrics>,
161    /// Total bits used.
162    pub total_bits: u64,
163    /// Average PSNR.
164    pub avg_psnr: f64,
165    /// Average SSIM.
166    pub avg_ssim: f64,
167}
168
169impl OptimizationStats {
170    /// Creates new optimization statistics.
171    #[must_use]
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Adds frame metrics.
177    pub fn add_frame(&mut self, metrics: FrameMetrics) {
178        self.total_bits += metrics.total_bits;
179        self.total_time += metrics.encoding_time;
180        self.frames_encoded += 1;
181        self.frame_metrics.push(metrics);
182
183        // Update averages
184        self.calculate_averages();
185    }
186
187    fn calculate_averages(&mut self) {
188        if self.frames_encoded == 0 {
189            return;
190        }
191
192        self.avg_psnr =
193            self.frame_metrics.iter().map(|m| m.psnr).sum::<f64>() / self.frames_encoded as f64;
194
195        self.avg_ssim =
196            self.frame_metrics.iter().map(|m| m.ssim).sum::<f64>() / self.frames_encoded as f64;
197    }
198
199    /// Calculates average bitrate in bits per second.
200    #[must_use]
201    pub fn avg_bitrate(&self, fps: f64) -> f64 {
202        if self.frames_encoded == 0 {
203            return 0.0;
204        }
205
206        (self.total_bits as f64 / self.frames_encoded as f64) * fps
207    }
208
209    /// Calculates average encoding speed in FPS.
210    #[must_use]
211    pub fn avg_fps(&self) -> f64 {
212        let secs = self.total_time.as_secs_f64();
213        if secs > 0.0 {
214            self.frames_encoded as f64 / secs
215        } else {
216            0.0
217        }
218    }
219
220    /// Gets compression ratio.
221    #[must_use]
222    pub fn compression_ratio(&self) -> f64 {
223        if self.frame_metrics.is_empty() {
224            return 1.0;
225        }
226
227        let first_frame = &self.frame_metrics[0];
228        let uncompressed_bits =
229            (first_frame.width * first_frame.height * 8 * self.frames_encoded) as f64;
230
231        if self.total_bits > 0 {
232            uncompressed_bits / self.total_bits as f64
233        } else {
234            1.0
235        }
236    }
237
238    /// Prints summary.
239    pub fn print_summary(&self) {
240        println!("Optimization Statistics:");
241        println!("  Frames: {}", self.frames_encoded);
242        println!("  Total bits: {}", self.total_bits);
243        println!("  Avg PSNR: {:.2} dB", self.avg_psnr);
244        println!("  Avg SSIM: {:.4}", self.avg_ssim);
245        println!("  Avg FPS: {:.2}", self.avg_fps());
246        println!("  Compression: {:.2}x", self.compression_ratio());
247    }
248}
249
250/// Timer helper for performance measurement.
251#[derive(Debug)]
252pub struct Timer {
253    start: Instant,
254    label: String,
255}
256
257impl Timer {
258    /// Starts a new timer.
259    #[must_use]
260    pub fn new(label: impl Into<String>) -> Self {
261        Self {
262            start: Instant::now(),
263            label: label.into(),
264        }
265    }
266
267    /// Gets elapsed time.
268    #[must_use]
269    pub fn elapsed(&self) -> Duration {
270        self.start.elapsed()
271    }
272
273    /// Stops timer and returns elapsed time.
274    #[must_use]
275    pub fn stop(self) -> Duration {
276        let elapsed = self.elapsed();
277        println!("{}: {:?}", self.label, elapsed);
278        elapsed
279    }
280}
281
282/// Histogram for distribution analysis.
283#[derive(Debug, Clone)]
284pub struct Histogram {
285    bins: Vec<u32>,
286    min_value: f64,
287    max_value: f64,
288    bin_width: f64,
289}
290
291impl Histogram {
292    /// Creates a new histogram.
293    #[must_use]
294    pub fn new(num_bins: usize, min_value: f64, max_value: f64) -> Self {
295        let bin_width = (max_value - min_value) / num_bins as f64;
296        Self {
297            bins: vec![0; num_bins],
298            min_value,
299            max_value,
300            bin_width,
301        }
302    }
303
304    /// Adds a value to the histogram.
305    pub fn add(&mut self, value: f64) {
306        if value < self.min_value || value >= self.max_value {
307            return;
308        }
309
310        let bin = ((value - self.min_value) / self.bin_width) as usize;
311        if bin < self.bins.len() {
312            self.bins[bin] += 1;
313        }
314    }
315
316    /// Gets the count for a bin.
317    #[must_use]
318    pub fn get_bin(&self, index: usize) -> u32 {
319        self.bins.get(index).copied().unwrap_or(0)
320    }
321
322    /// Gets the total count.
323    #[must_use]
324    pub fn total_count(&self) -> u32 {
325        self.bins.iter().sum()
326    }
327
328    /// Calculates the mean.
329    #[must_use]
330    pub fn mean(&self) -> f64 {
331        let total: u32 = self.total_count();
332        if total == 0 {
333            return 0.0;
334        }
335
336        let weighted_sum: f64 = self
337            .bins
338            .iter()
339            .enumerate()
340            .map(|(i, &count)| {
341                let bin_center = self.min_value + (i as f64 + 0.5) * self.bin_width;
342                bin_center * f64::from(count)
343            })
344            .sum();
345
346        weighted_sum / f64::from(total)
347    }
348
349    /// Calculates the median.
350    #[must_use]
351    pub fn median(&self) -> f64 {
352        let total = self.total_count();
353        if total == 0 {
354            return 0.0;
355        }
356
357        let median_count = total / 2;
358        let mut cumulative = 0;
359
360        for (i, &count) in self.bins.iter().enumerate() {
361            cumulative += count;
362            if cumulative >= median_count {
363                return self.min_value + (i as f64 + 0.5) * self.bin_width;
364            }
365        }
366
367        self.max_value
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_block_metrics_flat() {
377        let pixels = vec![128u8; 64];
378        let metrics = BlockMetrics::calculate(&pixels, 8, 8);
379        assert_eq!(metrics.mean, 128.0);
380        assert_eq!(metrics.variance, 0.0);
381        assert!(metrics.is_flat(10.0));
382    }
383
384    #[test]
385    fn test_block_metrics_varied() {
386        let pixels: Vec<u8> = (0..64).map(|i| i as u8).collect();
387        let metrics = BlockMetrics::calculate(&pixels, 8, 8);
388        assert!(metrics.variance > 0.0);
389        assert!(metrics.is_textured(100.0));
390    }
391
392    #[test]
393    fn test_block_metrics_psnr() {
394        let pixels = vec![128u8; 64];
395        let metrics = BlockMetrics::calculate(&pixels, 8, 8);
396        assert!(metrics.psnr().is_infinite()); // Zero SSE = infinite PSNR
397    }
398
399    #[test]
400    fn test_frame_metrics() {
401        let mut metrics = FrameMetrics::new(1920, 1080);
402        metrics.add_qp(26);
403        metrics.add_qp(28);
404        metrics.add_qp(24);
405        assert_eq!(metrics.avg_qp, 26.0);
406    }
407
408    #[test]
409    fn test_frame_metrics_bpp() {
410        let mut metrics = FrameMetrics::new(1920, 1080);
411        metrics.total_bits = 1920 * 1080; // 1 bit per pixel
412        assert_eq!(metrics.bits_per_pixel(), 1.0);
413    }
414
415    #[test]
416    fn test_optimization_stats() {
417        let mut stats = OptimizationStats::new();
418        let mut frame1 = FrameMetrics::new(1920, 1080);
419        frame1.psnr = 40.0;
420        frame1.ssim = 0.95;
421        stats.add_frame(frame1);
422
423        let mut frame2 = FrameMetrics::new(1920, 1080);
424        frame2.psnr = 42.0;
425        frame2.ssim = 0.96;
426        stats.add_frame(frame2);
427
428        assert_eq!(stats.frames_encoded, 2);
429        assert_eq!(stats.avg_psnr, 41.0);
430        assert_eq!(stats.avg_ssim, 0.955);
431    }
432
433    #[test]
434    fn test_timer() {
435        let timer = Timer::new("test");
436        std::thread::sleep(Duration::from_millis(10));
437        let elapsed = timer.elapsed();
438        assert!(elapsed >= Duration::from_millis(10));
439    }
440
441    #[test]
442    fn test_histogram() {
443        let mut hist = Histogram::new(10, 0.0, 100.0);
444        hist.add(25.0);
445        hist.add(35.0);
446        hist.add(25.0);
447
448        assert_eq!(hist.total_count(), 3);
449        assert_eq!(hist.get_bin(2), 2); // 25.0 falls in bin 2
450        assert_eq!(hist.get_bin(3), 1); // 35.0 falls in bin 3
451    }
452
453    #[test]
454    fn test_histogram_mean() {
455        let mut hist = Histogram::new(10, 0.0, 100.0);
456        hist.add(20.0);
457        hist.add(30.0);
458        hist.add(40.0);
459
460        // bin_width = 10.0; 20 → bin 2 (center 25), 30 → bin 3 (center 35), 40 → bin 4 (center 45)
461        // mean = (25 + 35 + 45) / 3 = 35.0
462        let mean = hist.mean();
463        assert!((mean - 35.0).abs() < 1.0); // Approximately 35
464    }
465}