use super::tables::{VP8_WEIGHT_TRELLIS, VP8_WEIGHT_Y};
#[rustfmt::skip]
const PSY_WEIGHT_Y: [u16; 16] = [
48, 36, 18, 6,
36, 30, 14, 5,
18, 14, 8, 3,
6, 5, 3, 1,
];
#[rustfmt::skip]
const PSY_WEIGHT_UV: [u16; 16] = [
32, 20, 10, 3,
20, 14, 7, 2,
10, 7, 4, 1,
3, 2, 1, 1,
];
#[rustfmt::skip]
const JND_THRESHOLD_Y: [u16; 16] = [
4, 6, 8, 12,
6, 8, 12, 18,
8, 12, 18, 26,
12, 18, 26, 36,
];
#[rustfmt::skip]
const JND_THRESHOLD_UV: [u16; 16] = [
8, 12, 16, 24,
12, 16, 24, 36,
16, 24, 36, 52,
24, 36, 52, 72,
];
#[derive(Clone, Debug)]
#[doc(hidden)]
pub struct PsyConfig {
pub(crate) psy_rd_strength: u32,
pub(crate) psy_trellis_strength: u32,
pub(crate) luma_csf: [u16; 16],
pub(crate) chroma_csf: [u16; 16],
pub(crate) trellis_weights: [u16; 16],
pub(crate) jnd_threshold_y: [u16; 16],
pub(crate) jnd_threshold_uv: [u16; 16],
pub(crate) luminance_factor: u8,
pub(crate) contrast_masking: u8,
}
impl Default for PsyConfig {
fn default() -> Self {
Self {
psy_rd_strength: 0,
psy_trellis_strength: 0,
luma_csf: VP8_WEIGHT_Y,
chroma_csf: VP8_WEIGHT_Y, trellis_weights: VP8_WEIGHT_TRELLIS,
jnd_threshold_y: [0; 16], jnd_threshold_uv: [0; 16], luminance_factor: 128, contrast_masking: 0, }
}
}
impl PsyConfig {
pub(crate) fn new(method: u8, quant_index: u8, _sns_strength: u8) -> Self {
let mut config = Self::default();
if method >= 3 {
config.luma_csf = PSY_WEIGHT_Y;
config.chroma_csf = PSY_WEIGHT_UV;
}
let _ = method;
if method >= 5 {
config.psy_trellis_strength = (quant_index as u32 * 6) >> 7;
let scale = quant_index.max(20) as u32; for i in 0..16 {
config.jnd_threshold_y[i] =
((JND_THRESHOLD_Y[i] as u32 * scale) / 50).min(255) as u16;
config.jnd_threshold_uv[i] =
((JND_THRESHOLD_UV[i] as u32 * scale) / 50).min(255) as u16;
}
}
config
}
#[inline]
pub(crate) fn is_below_jnd_y(&self, pos: usize, coeff: i32) -> bool {
let threshold = self.jnd_threshold_y[pos] as i32;
if threshold == 0 {
return false; }
let abs_coeff = coeff.abs();
let adapted_threshold = if self.luminance_factor < 100 {
(threshold * self.luminance_factor as i32) / 128
} else if self.luminance_factor > 160 {
(threshold * self.luminance_factor as i32) / 128
} else {
threshold
};
let final_threshold = if self.contrast_masking > 0 {
adapted_threshold + (self.contrast_masking as i32 / 4)
} else {
adapted_threshold
};
abs_coeff < final_threshold
}
#[inline]
pub(crate) fn is_below_jnd_uv(&self, pos: usize, coeff: i32) -> bool {
let threshold = self.jnd_threshold_uv[pos] as i32;
if threshold == 0 {
return false;
}
coeff.abs() < threshold
}
#[allow(dead_code)]
pub(crate) fn set_luminance(&mut self, avg_luma: u8) {
self.luminance_factor = avg_luma;
}
#[allow(dead_code)]
pub(crate) fn set_contrast_masking(&mut self, masking: u8) {
self.contrast_masking = masking;
}
}
#[inline]
pub(crate) fn satd_4x4(block: &[u8], stride: usize) -> u32 {
satd_4x4_impl(block, stride)
}
#[inline(always)]
fn satd_4x4_impl(block: &[u8], stride: usize) -> u32 {
let mut tmp = [0i32; 16];
for i in 0..4 {
let row = i * stride;
let a0 = i32::from(block[row]) + i32::from(block[row + 2]);
let a1 = i32::from(block[row + 1]) + i32::from(block[row + 3]);
let a2 = i32::from(block[row + 1]) - i32::from(block[row + 3]);
let a3 = i32::from(block[row]) - i32::from(block[row + 2]);
tmp[i * 4] = a0 + a1;
tmp[i * 4 + 1] = a3 + a2;
tmp[i * 4 + 2] = a3 - a2;
tmp[i * 4 + 3] = a0 - a1;
}
let mut sum = 0u32;
for i in 0..4 {
let a0 = tmp[i] + tmp[8 + i];
let a1 = tmp[4 + i] + tmp[12 + i];
let a2 = tmp[4 + i] - tmp[12 + i];
let a3 = tmp[i] - tmp[8 + i];
sum += (a0 + a1).unsigned_abs();
sum += (a3 + a2).unsigned_abs();
sum += (a3 - a2).unsigned_abs();
sum += (a0 - a1).unsigned_abs();
}
sum
}
#[inline]
pub(crate) fn satd_8x8(block: &[u8], stride: usize) -> u32 {
let mut sum = 0u32;
for y in 0..2 {
for x in 0..2 {
let offset = y * 4 * stride + x * 4;
sum += satd_4x4(&block[offset..], stride);
}
}
sum
}
#[inline]
pub(crate) fn satd_16x16(block: &[u8], stride: usize) -> u32 {
let mut sum = 0u32;
for y in 0..4 {
for x in 0..4 {
let offset = y * 4 * stride + x * 4;
sum += satd_4x4(&block[offset..], stride);
}
}
sum
}
#[inline]
fn ac_energy_4x4(block: &[u8], stride: usize) -> u32 {
let satd = satd_4x4(block, stride);
let mut dc_sum = 0u32;
for y in 0..4 {
for x in 0..4 {
dc_sum += block[y * stride + x] as u32;
}
}
satd.saturating_sub(dc_sum)
}
#[inline]
fn block_luminance_4x4(block: &[u8], stride: usize) -> u8 {
let mut sum = 0u32;
for y in 0..4 {
for x in 0..4 {
sum += block[y * stride + x] as u32;
}
}
(sum / 16) as u8
}
#[inline]
fn edge_strength_4x4(block: &[u8], stride: usize) -> u32 {
let mut h_edge = 0u32;
let mut v_edge = 0u32;
for y in 0..3 {
for x in 0..4 {
let diff =
(block[(y + 1) * stride + x] as i32 - block[y * stride + x] as i32).unsigned_abs();
h_edge += diff;
}
}
for y in 0..4 {
for x in 0..3 {
let diff =
(block[y * stride + x + 1] as i32 - block[y * stride + x] as i32).unsigned_abs();
v_edge += diff;
}
}
h_edge.max(v_edge)
}
pub(crate) fn compute_masking_alpha(src: &[u8], stride: usize) -> u8 {
let mut total_ac_energy = 0u64;
let mut total_luminance = 0u32;
let mut max_edge_strength = 0u32;
let mut min_block_activity = u32::MAX;
for by in 0..4 {
for bx in 0..4 {
let base = by * 4 * stride + bx * 4;
let block = &src[base..];
let ac = ac_energy_4x4(block, stride);
total_ac_energy += ac as u64;
min_block_activity = min_block_activity.min(ac);
total_luminance += block_luminance_4x4(block, stride) as u32;
let edge = edge_strength_4x4(block, stride);
max_edge_strength = max_edge_strength.max(edge);
}
}
let avg_ac_energy = (total_ac_energy / 16) as u32;
let avg_luminance = total_luminance / 16;
let activity_mask = if avg_ac_energy > 0 {
let energy_sqrt = libm::sqrtf(avg_ac_energy as f32);
(energy_sqrt * 8.0).min(255.0) as u32
} else {
0
};
let luminance_factor = if avg_luminance < 50 {
200u32 } else if avg_luminance > 200 {
280u32 } else {
256u32
};
let edge_penalty = if max_edge_strength > 300 {
64u32 } else if max_edge_strength > 150 {
32u32
} else {
0u32
};
let uniformity_penalty = if min_block_activity < 50 && avg_ac_energy < 100 {
80u32
} else {
0u32
};
let adjusted_activity = (activity_mask * luminance_factor) >> 8;
let final_alpha = adjusted_activity
.saturating_sub(edge_penalty)
.saturating_sub(uniformity_penalty);
final_alpha.min(255) as u8
}
pub(crate) fn blend_masking_alpha(dct_alpha: i32, masking_alpha: u8, method: u8) -> i32 {
if method < 4 {
return dct_alpha;
}
let masking_delta = masking_alpha as i32 - 128;
let scaled_delta = if method >= 5 {
(masking_delta * 96) >> 8
} else {
(masking_delta * 64) >> 8
};
dct_alpha + scaled_delta
}
#[inline]
pub(crate) fn psy_rd_cost(src_satd: u32, rec_satd: u32, psy_rd_strength: u32) -> i32 {
if psy_rd_strength == 0 {
return 0;
}
let energy_loss = src_satd.saturating_sub(rec_satd);
((psy_rd_strength as i64 * energy_loss as i64) >> 8) as i32
}
#[cfg(test)]
mod tests {
use super::*;
fn compute_masking_alpha_variance(src: &[u8], stride: usize) -> u8 {
let mut total_variance = 0u64;
for by in 0..4 {
for bx in 0..4 {
let base = by * 4 * stride + bx * 4;
let mut sum = 0u32;
for y in 0..4 {
for x in 0..4 {
sum += src[base + y * stride + x] as u32;
}
}
let mean = sum / 16;
let mut var = 0u64;
for y in 0..4 {
for x in 0..4 {
let diff = src[base + y * stride + x] as i32 - mean as i32;
var += (diff * diff) as u64;
}
}
total_variance += var / 16;
}
}
let avg_variance = total_variance / 16;
((avg_variance * 255) / (avg_variance + 100)).min(255) as u8
}
#[test]
fn test_masking_methods_correlate() {
let flat = [128u8; 256];
let mut textured = [0u8; 256];
for (i, p) in textured.iter_mut().enumerate() {
*p = ((i * 17 + i / 16 * 31) % 256) as u8;
}
let flat_satd = compute_masking_alpha(&flat, 16);
let flat_var = compute_masking_alpha_variance(&flat, 16);
let text_satd = compute_masking_alpha(&textured, 16);
let text_var = compute_masking_alpha_variance(&textured, 16);
assert!(
flat_satd < text_satd,
"SATD: flat={} should < textured={}",
flat_satd,
text_satd
);
assert!(
flat_var < text_var,
"Variance: flat={} should < textured={}",
flat_var,
text_var
);
}
#[test]
fn test_satd_4x4_flat_block() {
let flat = [128u8; 64]; let s = satd_4x4(&flat, 16);
assert_eq!(s, 2048);
}
#[test]
fn test_satd_4x4_noise_has_energy() {
let noise: [u8; 16] = [
10, 200, 50, 180, 90, 30, 220, 70, 150, 110, 40, 190, 60, 250, 20, 100,
];
let s = satd_4x4(&noise, 4);
assert!(s > 2048, "Noisy block SATD={} should exceed flat DC", s);
}
#[test]
fn test_satd_16x16_is_sum_of_4x4s() {
let mut block = [0u8; 16 * 16];
for (i, pixel) in block.iter_mut().enumerate() {
*pixel = (i * 7 + 13) as u8; }
let total = satd_16x16(&block, 16);
let mut manual_sum = 0u32;
for y in 0..4 {
for x in 0..4 {
manual_sum += satd_4x4(&block[y * 4 * 16 + x * 4..], 16);
}
}
assert_eq!(total, manual_sum);
}
#[test]
fn test_psy_rd_cost_disabled() {
assert_eq!(psy_rd_cost(1000, 500, 0), 0);
}
#[test]
fn test_psy_rd_cost_energy_loss() {
let cost = psy_rd_cost(1000, 500, 256);
assert!(cost > 0, "Energy loss should produce positive penalty");
}
#[test]
fn test_psy_rd_cost_energy_gain_no_penalty() {
let cost = psy_rd_cost(500, 1000, 256);
assert_eq!(cost, 0, "Energy gain should not be penalized");
}
#[test]
fn test_psy_rd_cost_scales_with_strength() {
let cost_low = psy_rd_cost(1000, 500, 100);
let cost_high = psy_rd_cost(1000, 500, 200);
assert!(
cost_high > cost_low,
"Higher strength should give higher penalty"
);
}
}