1#![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#[derive(Clone, Debug)]
24pub struct AdaptiveQuantization {
25 width: u32,
27 height: u32,
29 block_size: u32,
31 mode: AqMode,
33 strength: f32,
35 dark_boost: bool,
37 dark_threshold: u8,
39 bright_threshold: u8,
41 psy_enabled: bool,
43 psy_strength: f32,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
49pub enum AqMode {
50 None,
52 #[default]
54 Variance,
55 AutoVariance,
57 Psychovisual,
59 Combined,
61}
62
63impl AdaptiveQuantization {
64 #[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 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 pub fn set_strength(&mut self, strength: f32) {
89 self.strength = strength.clamp(0.0, 2.0);
90 }
91
92 pub fn set_dark_boost(&mut self, enable: bool) {
94 self.dark_boost = enable;
95 }
96
97 pub fn set_thresholds(&mut self, dark: u8, bright: u8) {
99 self.dark_threshold = dark;
100 self.bright_threshold = bright;
101 }
102
103 pub fn set_psy_strength(&mut self, strength: f32) {
105 self.psy_strength = strength.clamp(0.0, 2.0);
106 }
107
108 #[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 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 let avg_variance = self.calculate_average(&variances);
138 let avg_energy = self.calculate_average(&energies);
139
140 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 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 let energy = variance * count as f32;
196
197 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 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 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 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 offset.clamp(-6.0, 6.0)
247 }
248
249 fn variance_offset(&self, variance: f32, avg_variance: f32) -> f32 {
251 if avg_variance <= 0.0 {
252 return 0.0;
253 }
254
255 let ratio = variance / avg_variance;
259 let log_ratio = ratio.ln();
260
261 -log_ratio * self.strength * 2.0
262 }
263
264 fn calculate_auto_strength(&self, variance: f32, avg_variance: f32) -> f32 {
266 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 fn psychovisual_offset(&self, stats: &BlockStats, avg_energy: f32) -> f32 {
278 if avg_energy <= 0.0 {
279 return 0.0;
280 }
281
282 let energy_ratio = stats.energy / avg_energy;
287 let energy_offset = -energy_ratio.ln() * self.psy_strength;
288
289 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 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 #[must_use]
306 pub fn mode(&self) -> AqMode {
307 self.mode
308 }
309
310 #[must_use]
312 pub fn strength(&self) -> f32 {
313 self.strength
314 }
315
316 #[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#[derive(Clone, Copy, Debug, Default)]
331struct BlockStats {
332 mean: f32,
334 variance: f32,
336 energy: f32,
338 edge_strength: f32,
340 #[allow(dead_code)]
342 min: u8,
343 #[allow(dead_code)]
345 max: u8,
346}
347
348#[derive(Clone, Debug, Default)]
350pub struct AqResult {
351 pub offsets: Vec<f32>,
353 pub blocks_x: u32,
355 pub blocks_y: u32,
357 pub avg_variance: f32,
359 pub avg_energy: f32,
361}
362
363impl AqResult {
364 #[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 #[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 #[must_use]
384 pub fn total_blocks(&self) -> usize {
385 self.offsets.len()
386 }
387
388 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423pub enum AqStrength {
424 Off,
426 Light,
428 Medium,
430 Strong,
432 Maximum,
434}
435
436impl AqStrength {
437 #[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 #[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); } else {
491 frame.push(200); }
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 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 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 let dark_offset = result.get_offset(0, 0); let bright_offset = result.get_offset(result.blocks_x - 1, 0); 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); 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 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 let mut frame = vec![0u8; 64 * 64];
647 for i in 0..(64 * 32) {
648 frame[i] = 10; }
650 for i in (64 * 32)..(64 * 64) {
651 frame[i] = 250; }
653
654 let result = aq.calculate_offsets(&frame, 64);
655
656 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}