use super::{EncoderFamily, JpegProbe};
use crate::types::Subsampling;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentType {
Photo,
Screenshot,
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeblockAction {
Skip,
Boundary4Tap,
Knusperli,
}
#[derive(Debug, Clone, Copy)]
pub struct DeblockRecommendation {
pub action: DeblockAction,
pub content_type: ContentType,
pub zero_ac_frac: f32,
}
const SCREENSHOT_ZERO_AC_THRESHOLD: f32 = 0.10;
const KNUSPERLI_DC_QUANT_THRESHOLD_IJG: u16 = 27;
const KNUSPERLI_DC_QUANT_THRESHOLD_CJPEGLI: u16 = 40;
pub fn classify_from_probe(probe: &JpegProbe) -> ContentType {
if matches!(
probe.encoder,
EncoderFamily::CjpegliYcbcr | EncoderFamily::CjpegliXyb
) {
return ContentType::Mixed;
}
if probe.subsampling == Subsampling::S444 {
return ContentType::Screenshot;
}
ContentType::Mixed
}
pub fn classify_from_luma_coefficients(
luma_coeffs: &[i16],
num_blocks: usize,
) -> (ContentType, f32) {
if num_blocks == 0 {
return (ContentType::Mixed, 0.0);
}
let mut zero_ac_count = 0u32;
for bi in 0..num_blocks {
let block = &luma_coeffs[bi * 64..(bi + 1) * 64];
if block[1..64].iter().all(|&c| c == 0) {
zero_ac_count += 1;
}
}
let frac = zero_ac_count as f32 / num_blocks as f32;
let content_type = if frac >= SCREENSHOT_ZERO_AC_THRESHOLD {
ContentType::Screenshot
} else {
ContentType::Photo
};
(content_type, frac)
}
pub fn recommend_deblock(
probe: &JpegProbe,
content: ContentType,
zero_ac_frac: f32,
) -> DeblockRecommendation {
let is_cjpegli = matches!(
probe.encoder,
EncoderFamily::CjpegliYcbcr | EncoderFamily::CjpegliXyb
);
let dc_quant = luma_dc_quant(probe);
if content == ContentType::Screenshot {
if is_cjpegli {
return DeblockRecommendation {
action: DeblockAction::Boundary4Tap,
content_type: content,
zero_ac_frac,
};
}
if dc_quant >= 25 {
return DeblockRecommendation {
action: DeblockAction::Boundary4Tap,
content_type: content,
zero_ac_frac,
};
}
return DeblockRecommendation {
action: DeblockAction::Skip,
content_type: content,
zero_ac_frac,
};
}
let knusperli_threshold = if is_cjpegli {
KNUSPERLI_DC_QUANT_THRESHOLD_CJPEGLI
} else {
KNUSPERLI_DC_QUANT_THRESHOLD_IJG
};
let action = if dc_quant >= knusperli_threshold {
DeblockAction::Knusperli
} else {
DeblockAction::Boundary4Tap
};
DeblockRecommendation {
action,
content_type: content,
zero_ac_frac,
}
}
pub fn estimate_blocking_severity(probe: &JpegProbe) -> f32 {
let dc_quant = luma_dc_quant(probe) as f32;
(dc_quant / 40.0).min(1.0)
}
fn luma_dc_quant(probe: &JpegProbe) -> u16 {
probe.dqt_tables.first().map(|t| t.values[0]).unwrap_or(1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero_ac_classification_photo() {
let num_blocks = 100;
let mut coeffs = vec![0i16; num_blocks * 64];
for bi in 0..95 {
coeffs[bi * 64 + 1] = 5;
}
let (ct, frac) = classify_from_luma_coefficients(&coeffs, num_blocks);
assert_eq!(ct, ContentType::Photo);
assert!((frac - 0.05).abs() < 0.001);
}
#[test]
fn test_zero_ac_classification_screenshot() {
let num_blocks = 100;
let mut coeffs = vec![0i16; num_blocks * 64];
for bi in 0..10 {
coeffs[bi * 64 + 1] = 5;
}
let (ct, frac) = classify_from_luma_coefficients(&coeffs, num_blocks);
assert_eq!(ct, ContentType::Screenshot);
assert!((frac - 0.90).abs() < 0.001);
}
#[test]
fn test_zero_ac_threshold_boundary() {
let num_blocks = 100;
let mut coeffs = vec![0i16; num_blocks * 64];
for bi in 0..91 {
coeffs[bi * 64 + 1] = 1;
}
let (ct, frac) = classify_from_luma_coefficients(&coeffs, num_blocks);
assert_eq!(ct, ContentType::Photo);
assert!((frac - 0.09).abs() < 0.001);
coeffs = vec![0i16; num_blocks * 64];
for bi in 0..90 {
coeffs[bi * 64 + 1] = 1;
}
let (ct, frac) = classify_from_luma_coefficients(&coeffs, num_blocks);
assert_eq!(ct, ContentType::Screenshot);
assert!((frac - 0.10).abs() < 0.001);
}
#[test]
fn test_empty_blocks() {
let (ct, frac) = classify_from_luma_coefficients(&[], 0);
assert_eq!(ct, ContentType::Mixed);
assert_eq!(frac, 0.0);
}
}