use crate::util::FastRound;
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct FrameComplexity {
pub(crate) spatial_activity: f32,
pub(crate) temporal_sad: f32,
}
pub(crate) fn select_cq_qindex(
nominal_q: u8,
max_delta: u8,
current: FrameComplexity,
mean: FrameComplexity,
) -> u8 {
if nominal_q == 0 || max_delta == 0 {
return nominal_q;
}
if mean.spatial_activity <= 0.0 && mean.temporal_sad <= 0.0 {
return nominal_q;
}
let spatial_ratio = (current.spatial_activity + 1.0) / (mean.spatial_activity + 1.0);
let temporal_ratio = (current.temporal_sad + 1.0) / (mean.temporal_sad + 1.0);
let score = 3.0 * temporal_ratio.log2() - 2.0 * spatial_ratio.log2();
let delta = score
.fast_round()
.clamp(-(max_delta as f32), max_delta as f32) as i32;
(i32::from(nominal_q) + delta).clamp(1, 254) as u8
}
pub(crate) fn spatial_activity(luma: &[u8], width: usize, height: usize) -> f32 {
if width < 2 || height < 2 || luma.len() < width * height {
return 0.0;
}
let mut sum: u64 = 0;
let mut count: u64 = 0;
for y in 0..height {
let row = &luma[y * width..y * width + width];
for x in 0..width - 1 {
sum += (row[x] as i32 - row[x + 1] as i32).unsigned_abs() as u64;
count += 1;
}
if y + 1 < height {
let below = &luma[(y + 1) * width..(y + 1) * width + width];
for x in 0..width {
sum += (row[x] as i32 - below[x] as i32).unsigned_abs() as u64;
count += 1;
}
}
}
if count == 0 {
0.0
} else {
sum as f32 / count as f32
}
}
pub(crate) struct Lookahead {
window: std::collections::VecDeque<FrameComplexity>,
cap: usize,
}
impl Lookahead {
pub(crate) fn new(cap: usize) -> Self {
Self {
window: std::collections::VecDeque::with_capacity(cap.max(1)),
cap: cap.max(1),
}
}
pub(crate) fn push(&mut self, c: FrameComplexity) {
if self.window.len() == self.cap {
self.window.pop_front();
}
self.window.push_back(c);
}
pub(crate) fn mean_temporal_sad(&self) -> f32 {
if self.window.is_empty() {
return 0.0;
}
let s: f32 = self.window.iter().map(|c| c.temporal_sad).sum();
s / self.window.len() as f32
}
pub(crate) fn mean_spatial_activity(&self) -> f32 {
if self.window.is_empty() {
return 0.0;
}
let s: f32 = self.window.iter().map(|c| c.spatial_activity).sum();
s / self.window.len() as f32
}
#[cfg(test)]
fn len(&self) -> usize {
self.window.len()
}
#[cfg(test)]
pub(crate) fn capacity(&self) -> usize {
self.cap
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_plane_has_zero_activity() {
let flat = vec![128u8; 64 * 64];
assert_eq!(spatial_activity(&flat, 64, 64), 0.0);
}
#[test]
fn textured_plane_has_activity() {
let mut t = vec![0u8; 8 * 8];
for (i, p) in t.iter_mut().enumerate() {
*p = if i % 2 == 0 { 0 } else { 255 };
}
assert!(spatial_activity(&t, 8, 8) > 0.0);
}
#[test]
fn lookahead_evicts_and_averages() {
let mut la = Lookahead::new(2);
la.push(FrameComplexity {
spatial_activity: 0.0,
temporal_sad: 10.0,
});
la.push(FrameComplexity {
spatial_activity: 0.0,
temporal_sad: 20.0,
});
la.push(FrameComplexity {
spatial_activity: 0.0,
temporal_sad: 30.0,
});
assert_eq!(la.len(), 2); assert_eq!(la.mean_temporal_sad(), 25.0); }
#[test]
fn cq_selection_is_bounded_and_uses_both_complexity_axes() {
let mean = FrameComplexity {
spatial_activity: 8.0,
temporal_sad: 4.0,
};
let motion = select_cq_qindex(
120,
6,
FrameComplexity {
spatial_activity: 8.0,
temporal_sad: 40.0,
},
mean,
);
let detail = select_cq_qindex(
120,
6,
FrameComplexity {
spatial_activity: 64.0,
temporal_sad: 4.0,
},
mean,
);
assert_eq!(motion, 126);
assert_eq!(detail, 114);
assert_eq!(select_cq_qindex(0, 6, mean, mean), 0);
}
}