Skip to main content

oximedia_codec/rate_control/
aq.rs

1//! Adaptive Quantization (AQ) module.
2//!
3//! Adaptive Quantization adjusts QP on a per-block basis to improve
4//! perceptual quality. Different regions of a frame may benefit from
5//! different quantization levels:
6//!
7//! - High detail areas: Lower QP for better quality
8//! - Low detail areas: Higher QP to save bits
9//! - Dark regions: Special handling to avoid banding
10//! - Bright regions: Can tolerate more compression
11
12#![allow(clippy::cast_lossless)]
13#![allow(clippy::cast_precision_loss)]
14#![allow(clippy::cast_possible_truncation)]
15#![allow(clippy::cast_sign_loss)]
16#![allow(clippy::unused_self)]
17#![allow(clippy::if_not_else)]
18#![allow(clippy::missing_panics_doc)]
19#![allow(clippy::needless_pass_by_value)]
20#![forbid(unsafe_code)]
21
22/// Adaptive Quantization controller.
23#[derive(Clone, Debug)]
24pub struct AdaptiveQuantization {
25    /// Frame width.
26    width: u32,
27    /// Frame height.
28    height: u32,
29    /// Block size for AQ analysis.
30    block_size: u32,
31    /// AQ mode.
32    mode: AqMode,
33    /// AQ strength (0.0-2.0).
34    strength: f32,
35    /// Enable dark region boost.
36    dark_boost: bool,
37    /// Dark threshold (0-255).
38    dark_threshold: u8,
39    /// Bright threshold (0-255).
40    bright_threshold: u8,
41    /// Enable psychovisual optimization.
42    psy_enabled: bool,
43    /// Psychovisual strength.
44    psy_strength: f32,
45}
46
47/// AQ operation mode.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
49pub enum AqMode {
50    /// No adaptive quantization.
51    None,
52    /// Variance-based AQ.
53    #[default]
54    Variance,
55    /// Auto-variance AQ (adaptive strength).
56    AutoVariance,
57    /// Psychovisual AQ.
58    Psychovisual,
59    /// Combined variance and psychovisual.
60    Combined,
61}
62
63impl AdaptiveQuantization {
64    /// Create a new AQ controller.
65    #[must_use]
66    pub fn new(width: u32, height: u32) -> Self {
67        Self {
68            width,
69            height,
70            block_size: 16,
71            mode: AqMode::Variance,
72            strength: 1.0,
73            dark_boost: true,
74            dark_threshold: 40,
75            bright_threshold: 220,
76            psy_enabled: false,
77            psy_strength: 1.0,
78        }
79    }
80
81    /// Set AQ mode.
82    pub fn set_mode(&mut self, mode: AqMode) {
83        self.mode = mode;
84        self.psy_enabled = matches!(mode, AqMode::Psychovisual | AqMode::Combined);
85    }
86
87    /// Set AQ strength.
88    pub fn set_strength(&mut self, strength: f32) {
89        self.strength = strength.clamp(0.0, 2.0);
90    }
91
92    /// Enable or disable dark region boost.
93    pub fn set_dark_boost(&mut self, enable: bool) {
94        self.dark_boost = enable;
95    }
96
97    /// Set dark and bright thresholds.
98    pub fn set_thresholds(&mut self, dark: u8, bright: u8) {
99        self.dark_threshold = dark;
100        self.bright_threshold = bright;
101    }
102
103    /// Set psychovisual strength.
104    pub fn set_psy_strength(&mut self, strength: f32) {
105        self.psy_strength = strength.clamp(0.0, 2.0);
106    }
107
108    /// Calculate per-block QP offsets for a frame.
109    #[must_use]
110    pub fn calculate_offsets(&self, luma: &[u8], stride: usize) -> AqResult {
111        if self.mode == AqMode::None {
112            return AqResult::default();
113        }
114
115        let blocks_x = self.width / self.block_size;
116        let blocks_y = self.height / self.block_size;
117        let total_blocks = (blocks_x * blocks_y) as usize;
118
119        if total_blocks == 0 {
120            return AqResult::default();
121        }
122
123        let mut offsets = Vec::with_capacity(total_blocks);
124        let mut variances = Vec::with_capacity(total_blocks);
125        let mut energies = Vec::with_capacity(total_blocks);
126
127        // First pass: calculate block statistics
128        for by in 0..blocks_y {
129            for bx in 0..blocks_x {
130                let stats = self.calculate_block_stats(luma, stride, bx, by);
131                variances.push(stats.variance);
132                energies.push(stats.energy);
133            }
134        }
135
136        // Calculate reference values
137        let avg_variance = self.calculate_average(&variances);
138        let avg_energy = self.calculate_average(&energies);
139
140        // Second pass: calculate QP offsets
141        for by in 0..blocks_y {
142            for bx in 0..blocks_x {
143                let stats = self.calculate_block_stats(luma, stride, bx, by);
144                let offset = self.calculate_block_offset(&stats, avg_variance, avg_energy);
145                offsets.push(offset);
146            }
147        }
148
149        AqResult {
150            offsets,
151            blocks_x,
152            blocks_y,
153            avg_variance,
154            avg_energy,
155        }
156    }
157
158    /// Calculate statistics for a single block.
159    fn calculate_block_stats(&self, luma: &[u8], stride: usize, bx: u32, by: u32) -> BlockStats {
160        let start_x = (bx * self.block_size) as usize;
161        let start_y = (by * self.block_size) as usize;
162        let block_size = self.block_size as usize;
163
164        let mut sum = 0u64;
165        let mut sum_sq = 0u64;
166        let mut min_val = 255u8;
167        let mut max_val = 0u8;
168        let mut count = 0u32;
169
170        for y in 0..block_size {
171            let row_start = (start_y + y) * stride + start_x;
172            if row_start + block_size > luma.len() {
173                continue;
174            }
175
176            for x in 0..block_size {
177                let pixel = luma[row_start + x];
178                sum += pixel as u64;
179                sum_sq += (pixel as u64) * (pixel as u64);
180                min_val = min_val.min(pixel);
181                max_val = max_val.max(pixel);
182                count += 1;
183            }
184        }
185
186        if count == 0 {
187            return BlockStats::default();
188        }
189
190        let mean = sum as f32 / count as f32;
191        let mean_sq = sum_sq as f32 / count as f32;
192        let variance = (mean_sq - mean * mean).max(0.0);
193
194        // Calculate AC energy (sum of squared differences from mean)
195        let energy = variance * count as f32;
196
197        // Calculate edge strength (simplified)
198        let edge_strength = (max_val - min_val) as f32;
199
200        BlockStats {
201            mean,
202            variance,
203            energy,
204            edge_strength,
205            min: min_val,
206            max: max_val,
207        }
208    }
209
210    /// Calculate QP offset for a block.
211    fn calculate_block_offset(
212        &self,
213        stats: &BlockStats,
214        avg_variance: f32,
215        avg_energy: f32,
216    ) -> f32 {
217        let mut offset = match self.mode {
218            AqMode::None => return 0.0,
219            AqMode::Variance => self.variance_offset(stats.variance, avg_variance),
220            AqMode::AutoVariance => {
221                let auto_strength = self.calculate_auto_strength(stats.variance, avg_variance);
222                self.variance_offset(stats.variance, avg_variance) * auto_strength
223            }
224            AqMode::Psychovisual => self.psychovisual_offset(stats, avg_energy),
225            AqMode::Combined => {
226                let var_offset = self.variance_offset(stats.variance, avg_variance);
227                let psy_offset = self.psychovisual_offset(stats, avg_energy);
228                var_offset * 0.5 + psy_offset * 0.5
229            }
230        };
231
232        // Apply dark region boost
233        if self.dark_boost && stats.mean < self.dark_threshold as f32 {
234            let dark_factor = 1.0 - (stats.mean / self.dark_threshold as f32);
235            offset -= self.strength * dark_factor * 2.0;
236        }
237
238        // Apply bright region handling
239        if stats.mean > self.bright_threshold as f32 {
240            let bright_factor = (stats.mean - self.bright_threshold as f32)
241                / (255.0 - self.bright_threshold as f32);
242            offset += self.strength * bright_factor * 1.0;
243        }
244
245        // Clamp to reasonable range
246        offset.clamp(-6.0, 6.0)
247    }
248
249    /// Calculate variance-based QP offset.
250    fn variance_offset(&self, variance: f32, avg_variance: f32) -> f32 {
251        if avg_variance <= 0.0 {
252            return 0.0;
253        }
254
255        // Log-based offset calculation
256        // High variance (detail) -> negative offset (lower QP, better quality)
257        // Low variance (flat) -> positive offset (higher QP, save bits)
258        let ratio = variance / avg_variance;
259        let log_ratio = ratio.ln();
260
261        -log_ratio * self.strength * 2.0
262    }
263
264    /// Calculate auto-adjusted strength.
265    fn calculate_auto_strength(&self, variance: f32, avg_variance: f32) -> f32 {
266        // Reduce strength for very high or very low variance
267        let ratio = variance / avg_variance.max(1.0);
268
269        if !(0.1..=10.0).contains(&ratio) {
270            0.5
271        } else {
272            1.0
273        }
274    }
275
276    /// Calculate psychovisual QP offset.
277    fn psychovisual_offset(&self, stats: &BlockStats, avg_energy: f32) -> f32 {
278        if avg_energy <= 0.0 {
279            return 0.0;
280        }
281
282        // Psychovisual model considers:
283        // - Texture masking: high detail areas can hide artifacts
284        // - Edge preservation: edges are perceptually important
285
286        let energy_ratio = stats.energy / avg_energy;
287        let energy_offset = -energy_ratio.ln() * self.psy_strength;
288
289        // Edge importance factor
290        let edge_factor = (stats.edge_strength / 128.0).min(1.0);
291        let edge_offset = -edge_factor * self.psy_strength;
292
293        (energy_offset + edge_offset) * 0.5
294    }
295
296    /// Calculate average of a slice.
297    fn calculate_average(&self, values: &[f32]) -> f32 {
298        if values.is_empty() {
299            return 1.0;
300        }
301        values.iter().sum::<f32>() / values.len() as f32
302    }
303
304    /// Get AQ mode.
305    #[must_use]
306    pub fn mode(&self) -> AqMode {
307        self.mode
308    }
309
310    /// Get AQ strength.
311    #[must_use]
312    pub fn strength(&self) -> f32 {
313        self.strength
314    }
315
316    /// Check if AQ is enabled.
317    #[must_use]
318    pub fn is_enabled(&self) -> bool {
319        self.mode != AqMode::None
320    }
321}
322
323impl Default for AdaptiveQuantization {
324    fn default() -> Self {
325        Self::new(1920, 1080)
326    }
327}
328
329/// Block statistics for AQ calculation.
330#[derive(Clone, Copy, Debug, Default)]
331struct BlockStats {
332    /// Mean pixel value.
333    mean: f32,
334    /// Variance.
335    variance: f32,
336    /// AC energy.
337    energy: f32,
338    /// Edge strength.
339    edge_strength: f32,
340    /// Minimum pixel value (reserved for future use).
341    #[allow(dead_code)]
342    min: u8,
343    /// Maximum pixel value (reserved for future use).
344    #[allow(dead_code)]
345    max: u8,
346}
347
348/// Result of AQ calculation.
349#[derive(Clone, Debug, Default)]
350pub struct AqResult {
351    /// Per-block QP offsets.
352    pub offsets: Vec<f32>,
353    /// Number of blocks horizontally.
354    pub blocks_x: u32,
355    /// Number of blocks vertically.
356    pub blocks_y: u32,
357    /// Average variance.
358    pub avg_variance: f32,
359    /// Average energy.
360    pub avg_energy: f32,
361}
362
363impl AqResult {
364    /// Get offset for a specific block.
365    #[must_use]
366    pub fn get_offset(&self, bx: u32, by: u32) -> f32 {
367        if bx >= self.blocks_x || by >= self.blocks_y {
368            return 0.0;
369        }
370        let idx = (by * self.blocks_x + bx) as usize;
371        self.offsets.get(idx).copied().unwrap_or(0.0)
372    }
373
374    /// Get offset at pixel coordinates.
375    #[must_use]
376    pub fn get_offset_at_pixel(&self, x: u32, y: u32, block_size: u32) -> f32 {
377        let bx = x / block_size;
378        let by = y / block_size;
379        self.get_offset(bx, by)
380    }
381
382    /// Get total number of blocks.
383    #[must_use]
384    pub fn total_blocks(&self) -> usize {
385        self.offsets.len()
386    }
387
388    /// Get average offset.
389    #[must_use]
390    pub fn average_offset(&self) -> f32 {
391        if self.offsets.is_empty() {
392            return 0.0;
393        }
394        self.offsets.iter().sum::<f32>() / self.offsets.len() as f32
395    }
396
397    /// Get offset range (min, max).
398    #[must_use]
399    pub fn offset_range(&self) -> (f32, f32) {
400        if self.offsets.is_empty() {
401            return (0.0, 0.0);
402        }
403
404        let min = self
405            .offsets
406            .iter()
407            .copied()
408            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
409            .unwrap_or(0.0);
410        let max = self
411            .offsets
412            .iter()
413            .copied()
414            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
415            .unwrap_or(0.0);
416
417        (min, max)
418    }
419}
420
421/// AQ strength presets.
422#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423pub enum AqStrength {
424    /// No AQ.
425    Off,
426    /// Light AQ adjustment.
427    Light,
428    /// Medium AQ (default).
429    Medium,
430    /// Strong AQ adjustment.
431    Strong,
432    /// Maximum AQ adjustment.
433    Maximum,
434}
435
436impl AqStrength {
437    /// Get strength value.
438    #[must_use]
439    pub fn to_strength(self) -> f32 {
440        match self {
441            Self::Off => 0.0,
442            Self::Light => 0.5,
443            Self::Medium => 1.0,
444            Self::Strong => 1.5,
445            Self::Maximum => 2.0,
446        }
447    }
448
449    /// Get AQ mode for this strength.
450    #[must_use]
451    pub fn to_mode(self) -> AqMode {
452        if self == Self::Off {
453            AqMode::None
454        } else {
455            AqMode::Variance
456        }
457    }
458}
459
460impl Default for AqStrength {
461    fn default() -> Self {
462        Self::Medium
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    fn create_uniform_frame(width: u32, height: u32, value: u8) -> Vec<u8> {
471        vec![value; (width * height) as usize]
472    }
473
474    fn create_gradient_frame(width: u32, height: u32) -> Vec<u8> {
475        let mut frame = Vec::with_capacity((width * height) as usize);
476        for y in 0..height {
477            for x in 0..width {
478                frame.push(((x + y) % 256) as u8);
479            }
480        }
481        frame
482    }
483
484    fn create_half_frame(width: u32, height: u32) -> Vec<u8> {
485        let mut frame = Vec::with_capacity((width * height) as usize);
486        for _y in 0..height {
487            for x in 0..width {
488                if x < width / 2 {
489                    frame.push(50); // Dark left
490                } else {
491                    frame.push(200); // Bright right
492                }
493            }
494        }
495        frame
496    }
497
498    #[test]
499    fn test_aq_creation() {
500        let aq = AdaptiveQuantization::new(1920, 1080);
501        assert!(aq.is_enabled());
502        assert_eq!(aq.mode(), AqMode::Variance);
503    }
504
505    #[test]
506    fn test_aq_disabled() {
507        let mut aq = AdaptiveQuantization::new(64, 64);
508        aq.set_mode(AqMode::None);
509
510        let frame = create_gradient_frame(64, 64);
511        let result = aq.calculate_offsets(&frame, 64);
512
513        assert!(result.offsets.is_empty() || result.offsets.iter().all(|&o| o == 0.0));
514    }
515
516    #[test]
517    fn test_uniform_frame_offsets() {
518        let mut aq = AdaptiveQuantization::new(64, 64);
519        aq.set_dark_boost(false);
520
521        let frame = create_uniform_frame(64, 64, 128);
522        let result = aq.calculate_offsets(&frame, 64);
523
524        // Uniform frame should have near-zero offsets
525        for offset in &result.offsets {
526            assert!(
527                offset.abs() < 1.0,
528                "Offset {} too large for uniform frame",
529                offset
530            );
531        }
532    }
533
534    #[test]
535    fn test_gradient_frame_offsets() {
536        let mut aq = AdaptiveQuantization::new(64, 64);
537        aq.set_dark_boost(false);
538
539        let frame = create_gradient_frame(64, 64);
540        let result = aq.calculate_offsets(&frame, 64);
541
542        // Should have some non-zero offsets
543        assert!(!result.offsets.is_empty());
544    }
545
546    #[test]
547    fn test_dark_boost() {
548        let mut aq = AdaptiveQuantization::new(64, 64);
549        aq.set_dark_boost(true);
550        aq.set_thresholds(100, 200);
551
552        let frame = create_half_frame(64, 64);
553        let result = aq.calculate_offsets(&frame, 64);
554
555        // Check that dark blocks get negative (quality boost) offsets
556        let dark_offset = result.get_offset(0, 0); // Left side (dark)
557        let bright_offset = result.get_offset(result.blocks_x - 1, 0); // Right side (bright)
558
559        // Dark regions should have lower (more negative) offsets than bright
560        assert!(dark_offset < bright_offset);
561    }
562
563    #[test]
564    fn test_strength_setting() {
565        let mut aq = AdaptiveQuantization::new(64, 64);
566
567        aq.set_strength(0.5);
568        assert!((aq.strength() - 0.5).abs() < f32::EPSILON);
569
570        aq.set_strength(3.0); // Should clamp to 2.0
571        assert!((aq.strength() - 2.0).abs() < f32::EPSILON);
572    }
573
574    #[test]
575    fn test_aq_result_methods() {
576        let result = AqResult {
577            offsets: vec![-1.0, 0.0, 1.0, 2.0],
578            blocks_x: 2,
579            blocks_y: 2,
580            avg_variance: 100.0,
581            avg_energy: 1000.0,
582        };
583
584        assert_eq!(result.total_blocks(), 4);
585        assert!((result.average_offset() - 0.5).abs() < f32::EPSILON);
586
587        let (min, max) = result.offset_range();
588        assert!((min - (-1.0)).abs() < f32::EPSILON);
589        assert!((max - 2.0).abs() < f32::EPSILON);
590
591        assert!((result.get_offset(0, 0) - (-1.0)).abs() < f32::EPSILON);
592        assert!((result.get_offset(1, 1) - 2.0).abs() < f32::EPSILON);
593    }
594
595    #[test]
596    fn test_aq_modes() {
597        let mut aq = AdaptiveQuantization::new(64, 64);
598        let frame = create_gradient_frame(64, 64);
599
600        for mode in [
601            AqMode::Variance,
602            AqMode::AutoVariance,
603            AqMode::Psychovisual,
604            AqMode::Combined,
605        ] {
606            aq.set_mode(mode);
607            let result = aq.calculate_offsets(&frame, 64);
608            assert!(!result.offsets.is_empty());
609        }
610    }
611
612    #[test]
613    fn test_aq_strength_presets() {
614        assert!((AqStrength::Off.to_strength() - 0.0).abs() < f32::EPSILON);
615        assert!((AqStrength::Medium.to_strength() - 1.0).abs() < f32::EPSILON);
616        assert!((AqStrength::Maximum.to_strength() - 2.0).abs() < f32::EPSILON);
617
618        assert_eq!(AqStrength::Off.to_mode(), AqMode::None);
619        assert_eq!(AqStrength::Medium.to_mode(), AqMode::Variance);
620    }
621
622    #[test]
623    fn test_get_offset_at_pixel() {
624        let result = AqResult {
625            offsets: vec![1.0, 2.0, 3.0, 4.0],
626            blocks_x: 2,
627            blocks_y: 2,
628            avg_variance: 100.0,
629            avg_energy: 1000.0,
630        };
631
632        // Block size of 32: pixel (0,0) is block (0,0), pixel (33,0) is block (1,0)
633        assert!((result.get_offset_at_pixel(0, 0, 32) - 1.0).abs() < f32::EPSILON);
634        assert!((result.get_offset_at_pixel(33, 0, 32) - 2.0).abs() < f32::EPSILON);
635        assert!((result.get_offset_at_pixel(0, 33, 32) - 3.0).abs() < f32::EPSILON);
636        assert!((result.get_offset_at_pixel(33, 33, 32) - 4.0).abs() < f32::EPSILON);
637    }
638
639    #[test]
640    fn test_offset_bounds() {
641        let mut aq = AdaptiveQuantization::new(64, 64);
642        aq.set_strength(2.0);
643        aq.set_dark_boost(true);
644
645        // Create extreme frame
646        let mut frame = vec![0u8; 64 * 64];
647        for i in 0..(64 * 32) {
648            frame[i] = 10; // Very dark top half
649        }
650        for i in (64 * 32)..(64 * 64) {
651            frame[i] = 250; // Very bright bottom half
652        }
653
654        let result = aq.calculate_offsets(&frame, 64);
655
656        // All offsets should be clamped to [-6, 6]
657        for offset in &result.offsets {
658            assert!(
659                *offset >= -6.0 && *offset <= 6.0,
660                "Offset {} out of bounds",
661                offset
662            );
663        }
664    }
665}