#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::unused_self)]
#![allow(clippy::if_not_else)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::needless_pass_by_value)]
#![forbid(unsafe_code)]
#[derive(Clone, Debug)]
pub struct AdaptiveQuantization {
width: u32,
height: u32,
block_size: u32,
mode: AqMode,
strength: f32,
dark_boost: bool,
dark_threshold: u8,
bright_threshold: u8,
psy_enabled: bool,
psy_strength: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AqMode {
None,
#[default]
Variance,
AutoVariance,
Psychovisual,
Combined,
}
impl AdaptiveQuantization {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
block_size: 16,
mode: AqMode::Variance,
strength: 1.0,
dark_boost: true,
dark_threshold: 40,
bright_threshold: 220,
psy_enabled: false,
psy_strength: 1.0,
}
}
pub fn set_mode(&mut self, mode: AqMode) {
self.mode = mode;
self.psy_enabled = matches!(mode, AqMode::Psychovisual | AqMode::Combined);
}
pub fn set_strength(&mut self, strength: f32) {
self.strength = strength.clamp(0.0, 2.0);
}
pub fn set_dark_boost(&mut self, enable: bool) {
self.dark_boost = enable;
}
pub fn set_thresholds(&mut self, dark: u8, bright: u8) {
self.dark_threshold = dark;
self.bright_threshold = bright;
}
pub fn set_psy_strength(&mut self, strength: f32) {
self.psy_strength = strength.clamp(0.0, 2.0);
}
#[must_use]
pub fn calculate_offsets(&self, luma: &[u8], stride: usize) -> AqResult {
if self.mode == AqMode::None {
return AqResult::default();
}
let blocks_x = self.width / self.block_size;
let blocks_y = self.height / self.block_size;
let total_blocks = (blocks_x * blocks_y) as usize;
if total_blocks == 0 {
return AqResult::default();
}
let mut offsets = Vec::with_capacity(total_blocks);
let mut variances = Vec::with_capacity(total_blocks);
let mut energies = Vec::with_capacity(total_blocks);
for by in 0..blocks_y {
for bx in 0..blocks_x {
let stats = self.calculate_block_stats(luma, stride, bx, by);
variances.push(stats.variance);
energies.push(stats.energy);
}
}
let avg_variance = self.calculate_average(&variances);
let avg_energy = self.calculate_average(&energies);
for by in 0..blocks_y {
for bx in 0..blocks_x {
let stats = self.calculate_block_stats(luma, stride, bx, by);
let offset = self.calculate_block_offset(&stats, avg_variance, avg_energy);
offsets.push(offset);
}
}
AqResult {
offsets,
blocks_x,
blocks_y,
avg_variance,
avg_energy,
}
}
fn calculate_block_stats(&self, luma: &[u8], stride: usize, bx: u32, by: u32) -> BlockStats {
let start_x = (bx * self.block_size) as usize;
let start_y = (by * self.block_size) as usize;
let block_size = self.block_size as usize;
let mut sum = 0u64;
let mut sum_sq = 0u64;
let mut min_val = 255u8;
let mut max_val = 0u8;
let mut count = 0u32;
for y in 0..block_size {
let row_start = (start_y + y) * stride + start_x;
if row_start + block_size > luma.len() {
continue;
}
for x in 0..block_size {
let pixel = luma[row_start + x];
sum += pixel as u64;
sum_sq += (pixel as u64) * (pixel as u64);
min_val = min_val.min(pixel);
max_val = max_val.max(pixel);
count += 1;
}
}
if count == 0 {
return BlockStats::default();
}
let mean = sum as f32 / count as f32;
let mean_sq = sum_sq as f32 / count as f32;
let variance = (mean_sq - mean * mean).max(0.0);
let energy = variance * count as f32;
let edge_strength = (max_val - min_val) as f32;
BlockStats {
mean,
variance,
energy,
edge_strength,
min: min_val,
max: max_val,
}
}
fn calculate_block_offset(
&self,
stats: &BlockStats,
avg_variance: f32,
avg_energy: f32,
) -> f32 {
let mut offset = match self.mode {
AqMode::None => return 0.0,
AqMode::Variance => self.variance_offset(stats.variance, avg_variance),
AqMode::AutoVariance => {
let auto_strength = self.calculate_auto_strength(stats.variance, avg_variance);
self.variance_offset(stats.variance, avg_variance) * auto_strength
}
AqMode::Psychovisual => self.psychovisual_offset(stats, avg_energy),
AqMode::Combined => {
let var_offset = self.variance_offset(stats.variance, avg_variance);
let psy_offset = self.psychovisual_offset(stats, avg_energy);
var_offset * 0.5 + psy_offset * 0.5
}
};
if self.dark_boost && stats.mean < self.dark_threshold as f32 {
let dark_factor = 1.0 - (stats.mean / self.dark_threshold as f32);
offset -= self.strength * dark_factor * 2.0;
}
if stats.mean > self.bright_threshold as f32 {
let bright_factor = (stats.mean - self.bright_threshold as f32)
/ (255.0 - self.bright_threshold as f32);
offset += self.strength * bright_factor * 1.0;
}
offset.clamp(-6.0, 6.0)
}
fn variance_offset(&self, variance: f32, avg_variance: f32) -> f32 {
if avg_variance <= 0.0 {
return 0.0;
}
let ratio = variance / avg_variance;
let log_ratio = ratio.ln();
-log_ratio * self.strength * 2.0
}
fn calculate_auto_strength(&self, variance: f32, avg_variance: f32) -> f32 {
let ratio = variance / avg_variance.max(1.0);
if !(0.1..=10.0).contains(&ratio) {
0.5
} else {
1.0
}
}
fn psychovisual_offset(&self, stats: &BlockStats, avg_energy: f32) -> f32 {
if avg_energy <= 0.0 {
return 0.0;
}
let energy_ratio = stats.energy / avg_energy;
let energy_offset = -energy_ratio.ln() * self.psy_strength;
let edge_factor = (stats.edge_strength / 128.0).min(1.0);
let edge_offset = -edge_factor * self.psy_strength;
(energy_offset + edge_offset) * 0.5
}
fn calculate_average(&self, values: &[f32]) -> f32 {
if values.is_empty() {
return 1.0;
}
values.iter().sum::<f32>() / values.len() as f32
}
#[must_use]
pub fn mode(&self) -> AqMode {
self.mode
}
#[must_use]
pub fn strength(&self) -> f32 {
self.strength
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.mode != AqMode::None
}
}
impl Default for AdaptiveQuantization {
fn default() -> Self {
Self::new(1920, 1080)
}
}
#[derive(Clone, Copy, Debug, Default)]
struct BlockStats {
mean: f32,
variance: f32,
energy: f32,
edge_strength: f32,
#[allow(dead_code)]
min: u8,
#[allow(dead_code)]
max: u8,
}
#[derive(Clone, Debug, Default)]
pub struct AqResult {
pub offsets: Vec<f32>,
pub blocks_x: u32,
pub blocks_y: u32,
pub avg_variance: f32,
pub avg_energy: f32,
}
impl AqResult {
#[must_use]
pub fn get_offset(&self, bx: u32, by: u32) -> f32 {
if bx >= self.blocks_x || by >= self.blocks_y {
return 0.0;
}
let idx = (by * self.blocks_x + bx) as usize;
self.offsets.get(idx).copied().unwrap_or(0.0)
}
#[must_use]
pub fn get_offset_at_pixel(&self, x: u32, y: u32, block_size: u32) -> f32 {
let bx = x / block_size;
let by = y / block_size;
self.get_offset(bx, by)
}
#[must_use]
pub fn total_blocks(&self) -> usize {
self.offsets.len()
}
#[must_use]
pub fn average_offset(&self) -> f32 {
if self.offsets.is_empty() {
return 0.0;
}
self.offsets.iter().sum::<f32>() / self.offsets.len() as f32
}
#[must_use]
pub fn offset_range(&self) -> (f32, f32) {
if self.offsets.is_empty() {
return (0.0, 0.0);
}
let min = self
.offsets
.iter()
.copied()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let max = self
.offsets
.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
(min, max)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AqStrength {
Off,
Light,
Medium,
Strong,
Maximum,
}
impl AqStrength {
#[must_use]
pub fn to_strength(self) -> f32 {
match self {
Self::Off => 0.0,
Self::Light => 0.5,
Self::Medium => 1.0,
Self::Strong => 1.5,
Self::Maximum => 2.0,
}
}
#[must_use]
pub fn to_mode(self) -> AqMode {
if self == Self::Off {
AqMode::None
} else {
AqMode::Variance
}
}
}
impl Default for AqStrength {
fn default() -> Self {
Self::Medium
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_uniform_frame(width: u32, height: u32, value: u8) -> Vec<u8> {
vec![value; (width * height) as usize]
}
fn create_gradient_frame(width: u32, height: u32) -> Vec<u8> {
let mut frame = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
frame.push(((x + y) % 256) as u8);
}
}
frame
}
fn create_half_frame(width: u32, height: u32) -> Vec<u8> {
let mut frame = Vec::with_capacity((width * height) as usize);
for _y in 0..height {
for x in 0..width {
if x < width / 2 {
frame.push(50); } else {
frame.push(200); }
}
}
frame
}
#[test]
fn test_aq_creation() {
let aq = AdaptiveQuantization::new(1920, 1080);
assert!(aq.is_enabled());
assert_eq!(aq.mode(), AqMode::Variance);
}
#[test]
fn test_aq_disabled() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_mode(AqMode::None);
let frame = create_gradient_frame(64, 64);
let result = aq.calculate_offsets(&frame, 64);
assert!(result.offsets.is_empty() || result.offsets.iter().all(|&o| o == 0.0));
}
#[test]
fn test_uniform_frame_offsets() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_dark_boost(false);
let frame = create_uniform_frame(64, 64, 128);
let result = aq.calculate_offsets(&frame, 64);
for offset in &result.offsets {
assert!(
offset.abs() < 1.0,
"Offset {} too large for uniform frame",
offset
);
}
}
#[test]
fn test_gradient_frame_offsets() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_dark_boost(false);
let frame = create_gradient_frame(64, 64);
let result = aq.calculate_offsets(&frame, 64);
assert!(!result.offsets.is_empty());
}
#[test]
fn test_dark_boost() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_dark_boost(true);
aq.set_thresholds(100, 200);
let frame = create_half_frame(64, 64);
let result = aq.calculate_offsets(&frame, 64);
let dark_offset = result.get_offset(0, 0); let bright_offset = result.get_offset(result.blocks_x - 1, 0);
assert!(dark_offset < bright_offset);
}
#[test]
fn test_strength_setting() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_strength(0.5);
assert!((aq.strength() - 0.5).abs() < f32::EPSILON);
aq.set_strength(3.0); assert!((aq.strength() - 2.0).abs() < f32::EPSILON);
}
#[test]
fn test_aq_result_methods() {
let result = AqResult {
offsets: vec![-1.0, 0.0, 1.0, 2.0],
blocks_x: 2,
blocks_y: 2,
avg_variance: 100.0,
avg_energy: 1000.0,
};
assert_eq!(result.total_blocks(), 4);
assert!((result.average_offset() - 0.5).abs() < f32::EPSILON);
let (min, max) = result.offset_range();
assert!((min - (-1.0)).abs() < f32::EPSILON);
assert!((max - 2.0).abs() < f32::EPSILON);
assert!((result.get_offset(0, 0) - (-1.0)).abs() < f32::EPSILON);
assert!((result.get_offset(1, 1) - 2.0).abs() < f32::EPSILON);
}
#[test]
fn test_aq_modes() {
let mut aq = AdaptiveQuantization::new(64, 64);
let frame = create_gradient_frame(64, 64);
for mode in [
AqMode::Variance,
AqMode::AutoVariance,
AqMode::Psychovisual,
AqMode::Combined,
] {
aq.set_mode(mode);
let result = aq.calculate_offsets(&frame, 64);
assert!(!result.offsets.is_empty());
}
}
#[test]
fn test_aq_strength_presets() {
assert!((AqStrength::Off.to_strength() - 0.0).abs() < f32::EPSILON);
assert!((AqStrength::Medium.to_strength() - 1.0).abs() < f32::EPSILON);
assert!((AqStrength::Maximum.to_strength() - 2.0).abs() < f32::EPSILON);
assert_eq!(AqStrength::Off.to_mode(), AqMode::None);
assert_eq!(AqStrength::Medium.to_mode(), AqMode::Variance);
}
#[test]
fn test_get_offset_at_pixel() {
let result = AqResult {
offsets: vec![1.0, 2.0, 3.0, 4.0],
blocks_x: 2,
blocks_y: 2,
avg_variance: 100.0,
avg_energy: 1000.0,
};
assert!((result.get_offset_at_pixel(0, 0, 32) - 1.0).abs() < f32::EPSILON);
assert!((result.get_offset_at_pixel(33, 0, 32) - 2.0).abs() < f32::EPSILON);
assert!((result.get_offset_at_pixel(0, 33, 32) - 3.0).abs() < f32::EPSILON);
assert!((result.get_offset_at_pixel(33, 33, 32) - 4.0).abs() < f32::EPSILON);
}
#[test]
fn test_offset_bounds() {
let mut aq = AdaptiveQuantization::new(64, 64);
aq.set_strength(2.0);
aq.set_dark_boost(true);
let mut frame = vec![0u8; 64 * 64];
for i in 0..(64 * 32) {
frame[i] = 10; }
for i in (64 * 32)..(64 * 64) {
frame[i] = 250; }
let result = aq.calculate_offsets(&frame, 64);
for offset in &result.offsets {
assert!(
*offset >= -6.0 && *offset <= 6.0,
"Offset {} out of bounds",
offset
);
}
}
}