use crate::Size;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Color {
Bt601Limited,
Bt601Full,
Bt709Limited,
Bt709Full,
}
impl Color {
pub fn infer(size: Size) -> Self {
match size.height <= 576 {
true => Color::Bt601Limited,
false => Color::Bt709Limited,
}
}
#[cfg(target_os = "macos")]
pub(crate) fn with_range(self, limited: bool) -> Self {
match (self, limited) {
(Color::Bt601Limited | Color::Bt601Full, true) => Color::Bt601Limited,
(Color::Bt601Limited | Color::Bt601Full, false) => Color::Bt601Full,
(_, true) => Color::Bt709Limited,
(_, false) => Color::Bt709Full,
}
}
pub(crate) fn limited(self) -> bool {
matches!(self, Color::Bt601Limited | Color::Bt709Limited)
}
#[cfg(any(test, all(target_os = "linux", feature = "nvidia")))]
pub(crate) fn coefficients(self) -> Coefficients {
let (kr, kb) = match self {
Color::Bt601Limited | Color::Bt601Full => (0.299, 0.114),
Color::Bt709Limited | Color::Bt709Full => (0.2126, 0.0722),
};
let kg = 1.0 - kr - kb;
let (luma_scale, chroma_scale, luma_offset) = match self.limited() {
true => (219.0 / 255.0, 224.0 / 255.0, 16.0),
false => (1.0, 1.0, 0.0),
};
let cb = chroma_scale / (2.0 * (1.0 - kb));
let cr = chroma_scale / (2.0 * (1.0 - kr));
Coefficients {
y: [luma_scale * kr, luma_scale * kg, luma_scale * kb, luma_offset],
u: [-cb * kr, -cb * kg, cb * (1.0 - kb), 128.0],
v: [cr * (1.0 - kr), -cr * kg, -cr * kb, 128.0],
}
}
pub(crate) fn yuv(self) -> (yuv::YuvRange, yuv::YuvStandardMatrix) {
let range = match self.limited() {
true => yuv::YuvRange::Limited,
false => yuv::YuvRange::Full,
};
let matrix = match self {
Color::Bt601Limited | Color::Bt601Full => yuv::YuvStandardMatrix::Bt601,
Color::Bt709Limited | Color::Bt709Full => yuv::YuvStandardMatrix::Bt709,
};
(range, matrix)
}
}
#[cfg(any(test, all(target_os = "linux", feature = "nvidia")))]
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Coefficients {
pub y: [f32; 4],
pub u: [f32; 4],
pub v: [f32; 4],
}
#[cfg(any(test, all(target_os = "linux", feature = "nvidia")))]
impl Coefficients {
#[cfg(test)]
pub(crate) fn apply(&self, rgb: [u8; 3]) -> [u8; 3] {
let dot = |w: [f32; 4]| {
let v = w[0] * rgb[0] as f32 + w[1] * rgb[1] as f32 + w[2] * rgb[2] as f32 + w[3];
v.round().clamp(0.0, 255.0) as u8
};
[dot(self.y), dot(self.u), dot(self.v)]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inference_splits_at_standard_definition() {
assert_eq!(Color::infer(Size::new(720, 480)), Color::Bt601Limited);
assert_eq!(Color::infer(Size::new(720, 576)), Color::Bt601Limited);
assert_eq!(Color::infer(Size::new(1280, 720)), Color::Bt709Limited);
}
#[cfg(target_os = "macos")]
#[test]
fn with_range_keeps_the_matrix() {
assert_eq!(Color::Bt709Limited.with_range(false), Color::Bt709Full);
assert_eq!(Color::Bt709Full.with_range(true), Color::Bt709Limited);
assert_eq!(Color::Bt601Limited.with_range(false), Color::Bt601Full);
}
#[test]
fn coefficients_match_the_textbook_values() {
let red = [255, 0, 0];
assert_eq!(Color::Bt709Limited.coefficients().apply(red), [63, 102, 240]);
assert_eq!(Color::Bt709Full.coefficients().apply(red), [54, 99, 255]);
assert_eq!(Color::Bt601Limited.coefficients().apply(red), [81, 90, 240]);
assert_eq!(Color::Bt601Full.coefficients().apply([255; 3]), [255, 128, 128]);
assert_eq!(Color::Bt709Limited.coefficients().apply([0; 3]), [16, 128, 128]);
}
#[test]
fn coefficients_agree_with_the_yuv_crate() {
use yuv::{YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, rgba_to_yuv420};
let colors = [
Color::Bt601Limited,
Color::Bt601Full,
Color::Bt709Limited,
Color::Bt709Full,
];
let pixels: [[u8; 3]; 6] = [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 255],
[17, 200, 90],
[128, 128, 128],
];
for color in colors {
let (range, matrix) = color.yuv();
let coefficients = color.coefficients();
for rgb in pixels {
let rgba: Vec<u8> = std::iter::repeat_n([rgb[0], rgb[1], rgb[2], 255], 4)
.flatten()
.collect();
let mut planar = YuvPlanarImageMut::alloc(2, 2, YuvChromaSubsampling::Yuv420);
rgba_to_yuv420(&mut planar, &rgba, 8, range, matrix, YuvConversionMode::Balanced).unwrap();
let expected = [
planar.y_plane.borrow()[0],
planar.u_plane.borrow()[0],
planar.v_plane.borrow()[0],
];
let actual = coefficients.apply(rgb);
for (channel, (a, e)) in actual.iter().zip(expected).enumerate() {
assert!(
a.abs_diff(e) <= 1,
"{color:?} {rgb:?} channel {channel}: coefficients {actual:?}, yuv crate {expected:?}"
);
}
}
}
}
}