pub mod psnr;
pub mod report;
pub mod ssim;
pub mod temporal;
pub mod vmaf;
use crate::error::{CvError, CvResult};
use oximedia_codec::VideoFrame;
pub use psnr::{calculate_psnr, calculate_psnr_planes, PsnrResult, PsnrStatistics};
pub use report::{QualityAnalysis, QualityComparison, QualityLevel, QualityReport};
pub use ssim::{calculate_ms_ssim, calculate_ssim, SsimComponents, SsimResult};
pub use temporal::{calculate_temporal_info, TemporalInfo, TemporalMetrics};
pub use vmaf::{calculate_vmaf, VmafFeatures, VmafResult};
#[derive(Debug, Clone, PartialEq)]
pub struct QualityMetrics {
pub psnr: f64,
pub ssim: f64,
pub vmaf: f64,
pub psnr_planes: Vec<f64>,
pub ssim_planes: Vec<f64>,
pub ms_ssim: f64,
pub psnr_hvs: f64,
pub ciede2000: f64,
pub temporal_info: f64,
}
impl QualityMetrics {
#[must_use]
pub fn new() -> Self {
Self {
psnr: 0.0,
ssim: 0.0,
vmaf: 0.0,
psnr_planes: Vec::new(),
ssim_planes: Vec::new(),
ms_ssim: 0.0,
psnr_hvs: 0.0,
ciede2000: 0.0,
temporal_info: 0.0,
}
}
#[must_use]
pub fn is_acceptable_quality(&self) -> bool {
self.psnr > 30.0 && self.ssim > 0.9 && self.vmaf > 70.0
}
#[must_use]
pub fn is_high_quality(&self) -> bool {
self.psnr > 40.0 && self.ssim > 0.95 && self.vmaf > 85.0
}
#[must_use]
pub fn overall_score(&self) -> f64 {
let psnr_norm = ((self.psnr - 20.0) / 30.0).clamp(0.0, 1.0) * 100.0;
let ssim_norm = self.ssim * 100.0;
(0.5 * self.vmaf + 0.3 * ssim_norm + 0.2 * psnr_norm).clamp(0.0, 100.0)
}
}
impl Default for QualityMetrics {
fn default() -> Self {
Self::new()
}
}
pub fn calculate_metrics(
reference: &VideoFrame,
distorted: &VideoFrame,
) -> CvResult<QualityMetrics> {
if reference.width != distorted.width || reference.height != distorted.height {
return Err(CvError::invalid_parameter(
"frame_dimensions",
format!(
"{}x{} vs {}x{}",
reference.width, reference.height, distorted.width, distorted.height
),
));
}
if reference.format != distorted.format {
return Err(CvError::invalid_parameter(
"pixel_format",
"Reference and distorted frames must have the same pixel format",
));
}
if reference.planes.len() != distorted.planes.len() {
return Err(CvError::invalid_parameter(
"plane_count",
format!("{} vs {}", reference.planes.len(), distorted.planes.len()),
));
}
let psnr_result = calculate_psnr_planes(reference, distorted)?;
let psnr = psnr_result.overall;
let psnr_planes = psnr_result.per_plane;
let ssim_result = calculate_ssim(reference, distorted)?;
let ssim = ssim_result.overall;
let ssim_planes = ssim_result.per_plane;
let ms_ssim = calculate_ms_ssim(reference, distorted)?;
let vmaf_result = calculate_vmaf(reference, distorted)?;
let vmaf = vmaf_result.score;
let psnr_hvs = calculate_psnr_hvs(reference, distorted)?;
let ciede2000 = calculate_ciede2000(reference, distorted)?;
let temporal = calculate_temporal_info(reference)?;
let temporal_info = temporal.ti;
Ok(QualityMetrics {
psnr,
ssim,
vmaf,
psnr_planes,
ssim_planes,
ms_ssim,
psnr_hvs,
ciede2000,
temporal_info,
})
}
pub fn calculate_psnr_hvs(reference: &VideoFrame, distorted: &VideoFrame) -> CvResult<f64> {
if reference.planes.is_empty() || distorted.planes.is_empty() {
return Err(CvError::insufficient_data(1, 0));
}
let ref_plane = &reference.planes[0];
let dist_plane = &distorted.planes[0];
if ref_plane.data.len() != dist_plane.data.len() {
return Err(CvError::invalid_parameter(
"plane_size",
format!("{} vs {}", ref_plane.data.len(), dist_plane.data.len()),
));
}
#[allow(clippy::excessive_precision)]
const HVS_WEIGHTS: [[f64; 8]; 8] = [
[
1.000_000, 0.708_618, 0.652_414, 0.618_845, 0.595_435, 0.578_070, 0.564_593, 0.553_695,
],
[
0.708_618, 0.668_857, 0.627_193, 0.597_475, 0.575_556, 0.558_887, 0.545_817, 0.535_218,
],
[
0.652_414, 0.627_193, 0.594_362, 0.568_807, 0.549_114, 0.533_719, 0.521_449, 0.511_439,
],
[
0.618_845, 0.597_475, 0.568_807, 0.545_895, 0.528_137, 0.514_155, 0.502_883, 0.493_621,
],
[
0.595_435, 0.575_556, 0.549_114, 0.528_137, 0.511_688, 0.498_624, 0.488_068, 0.479_364,
],
[
0.578_070, 0.558_887, 0.533_719, 0.514_155, 0.498_624, 0.486_317, 0.476_283, 0.467_998,
],
[
0.564_593, 0.545_817, 0.521_449, 0.502_883, 0.488_068, 0.476_283, 0.466_660, 0.458_685,
],
[
0.553_695, 0.535_218, 0.511_439, 0.493_621, 0.479_364, 0.467_998, 0.458_685, 0.450_960,
],
];
let width = reference.width as usize;
let height = reference.height as usize;
let stride = ref_plane.stride;
let mut weighted_mse = 0.0;
let mut total_weights = 0.0;
for block_y in (0..height).step_by(8) {
for block_x in (0..width).step_by(8) {
let block_height = (height - block_y).min(8);
let block_width = (width - block_x).min(8);
for dy in 0..block_height {
for dx in 0..block_width {
let y = block_y + dy;
let x = block_x + dx;
if y * stride + x < ref_plane.data.len()
&& y * stride + x < dist_plane.data.len()
{
let ref_val = f64::from(ref_plane.data[y * stride + x]);
let dist_val = f64::from(dist_plane.data[y * stride + x]);
let diff = ref_val - dist_val;
let weight = HVS_WEIGHTS[dy][dx];
weighted_mse += weight * diff * diff;
total_weights += weight;
}
}
}
}
}
if total_weights == 0.0 {
return Ok(f64::INFINITY);
}
let weighted_mse = weighted_mse / total_weights;
if weighted_mse < 1e-10 {
Ok(100.0) } else {
let max_pixel = 255.0;
Ok(10.0 * (max_pixel * max_pixel / weighted_mse).log10())
}
}
pub fn calculate_ciede2000(reference: &VideoFrame, distorted: &VideoFrame) -> CvResult<f64> {
if reference.planes.len() < 3 || distorted.planes.len() < 3 {
return calculate_simple_difference(reference, distorted);
}
let width = reference.width as usize;
let height = reference.height as usize;
let mut total_delta_e = 0.0;
let mut pixel_count = 0;
for y in (0..height).step_by(4) {
for x in (0..width).step_by(4) {
let ref_y = get_pixel_value(&reference.planes[0], x, y);
let ref_u = get_chroma_value(&reference.planes[1], x, y, reference);
let ref_v = get_chroma_value(&reference.planes[2], x, y, reference);
let dist_y = get_pixel_value(&distorted.planes[0], x, y);
let dist_u = get_chroma_value(&distorted.planes[1], x, y, distorted);
let dist_v = get_chroma_value(&distorted.planes[2], x, y, distorted);
let (r1, g1, b1) = yuv_to_rgb(ref_y, ref_u, ref_v);
let (r2, g2, b2) = yuv_to_rgb(dist_y, dist_u, dist_v);
let (l1, a1, b1_lab) = rgb_to_lab(r1, g1, b1);
let (l2, a2, b2_lab) = rgb_to_lab(r2, g2, b2);
let delta_l = l1 - l2;
let delta_a = a1 - a2;
let delta_b = b1_lab - b2_lab;
let delta_e = (delta_l * delta_l + delta_a * delta_a + delta_b * delta_b).sqrt();
total_delta_e += delta_e;
pixel_count += 1;
}
}
if pixel_count == 0 {
return Ok(0.0);
}
Ok(total_delta_e / pixel_count as f64)
}
fn calculate_simple_difference(reference: &VideoFrame, distorted: &VideoFrame) -> CvResult<f64> {
if reference.planes.is_empty() || distorted.planes.is_empty() {
return Err(CvError::insufficient_data(1, 0));
}
let ref_data = &reference.planes[0].data;
let dist_data = &distorted.planes[0].data;
let len = ref_data.len().min(dist_data.len());
let mut sum_diff = 0.0;
for i in 0..len {
let diff = f64::from(ref_data[i]) - f64::from(dist_data[i]);
sum_diff += diff.abs();
}
Ok(sum_diff / len as f64)
}
#[inline]
fn get_pixel_value(plane: &oximedia_codec::Plane, x: usize, y: usize) -> f64 {
let stride = plane.stride;
let idx = y * stride + x;
if idx < plane.data.len() {
f64::from(plane.data[idx])
} else {
0.0
}
}
#[inline]
fn get_chroma_value(plane: &oximedia_codec::Plane, x: usize, y: usize, frame: &VideoFrame) -> f64 {
let (h_ratio, v_ratio) = frame.format.chroma_subsampling();
let chroma_x = x / h_ratio as usize;
let chroma_y = y / v_ratio as usize;
get_pixel_value(plane, chroma_x, chroma_y)
}
#[allow(clippy::many_single_char_names)]
fn yuv_to_rgb(y: f64, u: f64, v: f64) -> (f64, f64, f64) {
let c = y - 16.0;
let d = u - 128.0;
let e = v - 128.0;
let r = (1.164 * c + 1.596 * e).clamp(0.0, 255.0);
let g = (1.164 * c - 0.392 * d - 0.813 * e).clamp(0.0, 255.0);
let b = (1.164 * c + 2.017 * d).clamp(0.0, 255.0);
(r, g, b)
}
#[allow(clippy::many_single_char_names)]
fn rgb_to_lab(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
let r = r / 255.0;
let g = g / 255.0;
let b = b / 255.0;
let r = if r > 0.04045 {
((r + 0.055) / 1.055).powf(2.4)
} else {
r / 12.92
};
let g = if g > 0.04045 {
((g + 0.055) / 1.055).powf(2.4)
} else {
g / 12.92
};
let b = if b > 0.04045 {
((b + 0.055) / 1.055).powf(2.4)
} else {
b / 12.92
};
let x = r * 0.4124 + g * 0.3576 + b * 0.1805;
let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
let z = r * 0.0193 + g * 0.1192 + b * 0.9505;
let x = x / 0.95047;
let y = y / 1.00000;
let z = z / 1.08883;
let fx = if x > 0.008_856 {
x.powf(1.0 / 3.0)
} else {
(7.787 * x) + (16.0 / 116.0)
};
let fy = if y > 0.008_856 {
y.powf(1.0 / 3.0)
} else {
(7.787 * y) + (16.0 / 116.0)
};
let fz = if z > 0.008_856 {
z.powf(1.0 / 3.0)
} else {
(7.787 * z) + (16.0 / 116.0)
};
let l = (116.0 * fy) - 16.0;
let a = 500.0 * (fx - fy);
let b_lab = 200.0 * (fy - fz);
(l, a, b_lab)
}