From 78123582a9750a98e6d1b4eef38091febf813057 Mon Sep 17 00:00:00 2001 From: Hannes Vernooij Date: Wed, 11 Dec 2024 14:58:25 +0100 Subject: [PATCH 1/3] Replace hardcoded `u8` by the `Pixel` trait to allow 10 bit signals In the current implementation, 10-bit signals can be stored, but they are derived from 8-bit signals, offering no visual improvement. All incoming data has been assumed to be 8-bit sRGB. This PR takes the first step in removing these assumptions by enabling the storage of signals with higher bit depth. It lays the groundwork for full support of writing HDR images. --- ravif/src/av1encoder.rs | 209 ++++++++++++---------------------------- ravif/src/dirtyalpha.rs | 118 +++++++++++++++-------- 2 files changed, 139 insertions(+), 188 deletions(-) diff --git a/ravif/src/av1encoder.rs b/ravif/src/av1encoder.rs index 1a3fabf..cd4b52f 100644 --- a/ravif/src/av1encoder.rs +++ b/ravif/src/av1encoder.rs @@ -5,16 +5,16 @@ use crate::error::Error; use crate::rayoff as rayon; use imgref::{Img, ImgVec}; use rav1e::prelude::*; -use rgb::{RGB8, RGBA8}; +use rgb::{Rgb, Rgba}; /// For [`Encoder::with_internal_color_model`] #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ColorModel { - /// Standard color model for photographic content. Usually the best choice. + /// Standard color space for photographic content. Usually the best choice. /// This library always uses full-resolution color (4:4:4). /// This library will automatically choose between BT.601 or BT.709. YCbCr, - /// RGB channels are encoded without color space transformation. + /// RGB channels are encoded without colorspace transformation. /// Usually results in larger file sizes, and is less compatible than `YCbCr`. /// Use only if the content really makes use of RGB, e.g. anaglyph images or RGB subpixel anti-aliasing. RGB, @@ -43,8 +43,16 @@ pub enum BitDepth { #[default] Eight, Ten, - /// Pick 8 or 10 depending on image format and decoder compatibility - Auto, +} + +impl BitDepth { + /// Returns the bit depth in usize, this can currently be either `8` or `10`. + fn to_usize(self) -> usize { + match self { + BitDepth::Eight => 8, + BitDepth::Ten => 10, + } + } } /// The newly-created image file + extra info FYI @@ -77,7 +85,7 @@ pub struct Encoder { /// [`AlphaColorMode`] alpha_color_mode: AlphaColorMode, /// 8 or 10 - depth: BitDepth, + depth: Option, } /// Builder methods @@ -89,7 +97,7 @@ impl Encoder { quantizer: quality_to_quantizer(80.), alpha_quantizer: quality_to_quantizer(80.), speed: 5, - depth: BitDepth::default(), + depth: None, premultiplied_alpha: false, color_model: ColorModel::YCbCr, threads: None, @@ -107,16 +115,12 @@ impl Encoder { self } - #[doc(hidden)] - #[deprecated(note = "Renamed to with_bit_depth")] - pub fn with_depth(self, depth: Option) -> Self { - self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Auto)) - } - - /// Depth 8 or 10. + /// Depth 8 or 10. `None` picks automatically. #[inline(always)] + #[track_caller] #[must_use] - pub fn with_bit_depth(mut self, depth: BitDepth) -> Self { + pub fn with_depth(mut self, depth: Option) -> Self { + assert!(depth.map_or(true, |d| d == 8 || d == 10)); self.depth = depth; self } @@ -154,7 +158,6 @@ impl Encoder { } #[doc(hidden)] - #[deprecated = "Renamed to `with_internal_color_model()`"] pub fn with_internal_color_space(self, color_model: ColorModel) -> Self { self.with_internal_color_model(color_model) } @@ -199,13 +202,11 @@ impl Encoder { /// /// If all pixels are opaque, the alpha channel will be left out automatically. /// - /// This function takes 8-bit inputs, but will generate an AVIF file using 10-bit depth. - /// /// returns AVIF file with info about sizes about AV1 payload. - pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA]>) -> Result { + pub fn encode_rgba(&self, in_buffer: Img<&[Rgba

]>) -> Result { let new_alpha = self.convert_alpha(in_buffer); - let buffer = new_alpha.as_ref().map(|b| b.as_ref()).unwrap_or(in_buffer); - let use_alpha = buffer.pixels().any(|px| px.a != 255); + let buffer = new_alpha.as_ref().map(|b: &Img>>| b.as_ref()).unwrap_or(in_buffer); + let use_alpha = buffer.pixels().any(|px| Into::::into(px.a) != 255); if !use_alpha { return self.encode_rgb_internal(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb())); } @@ -216,49 +217,31 @@ impl Encoder { ColorModel::YCbCr => MatrixCoefficients::BT601, ColorModel::RGB => MatrixCoefficients::Identity, }; - match self.depth { - BitDepth::Eight | BitDepth::Auto => { - let planes = buffer.pixels().map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px.rgb(), BT601), - ColorModel::RGB => rgb_to_8_bit_gbr(px.rgb()), - }; - [y, u, v] - }); - let alpha = buffer.pixels().map(|px| px.a); - self.encode_raw_planes_8_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) - }, - BitDepth::Ten => { - let planes = buffer.pixels().map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px.rgb(), BT601), - ColorModel::RGB => rgb_to_10_bit_gbr(px.rgb()), - }; - [y, u, v] - }); - let alpha = buffer.pixels().map(|px| to_ten(px.a)); - self.encode_raw_planes_10_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) - }, - } + let planes = buffer.pixels().map(|px| match self.color_model { + ColorModel::YCbCr => rgb_to_ycbcr(Rgb::new(px.r, px.g, px.b), self.depth, BT601), + ColorModel::RGB => [px.g, px.b, px.r], + }); + let alpha = buffer.pixels().map(|px| px.a); + self.encode_raw_planes(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients, self.depth) } - fn convert_alpha(&self, in_buffer: Img<&[RGBA8]>) -> Option> { + /// Todo: We shouldn't assume this is 8 bits, 10 bit input signals only has 2 alpha bits. + fn convert_alpha(&self, in_buffer: Img<&[Rgba

]>) -> Option>> { match self.alpha_color_mode { AlphaColorMode::UnassociatedDirty => None, AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer), AlphaColorMode::Premultiplied => { - let prem = in_buffer.pixels() - .filter(|px| px.a != 255) + let prem = in_buffer + .pixels() + .filter(|px| px.a != P::cast_from(255)) .map(|px| { - if px.a == 0 { - RGBA8::default() + if Into::::into(px.a) == 0 { + Rgba::new(px.a, px.a, px.a, px.a) } else { - RGBA8::new( - (u16::from(px.r) * 255 / u16::from(px.a)) as u8, - (u16::from(px.g) * 255 / u16::from(px.a)) as u8, - (u16::from(px.b) * 255 / u16::from(px.a)) as u8, - px.a, - ) + let r = px.r * P::cast_from(255) / px.a; + let g = px.g * P::cast_from(255) / px.a; + let b = px.b * P::cast_from(255) / px.a; + Rgba::new(r, g, b, px.a) } }) .collect(); @@ -284,76 +267,38 @@ impl Encoder { /// /// returns AVIF file, size of color metadata #[inline] - pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result { + pub fn encode_rgb(&self, buffer: Img<&[Rgb

]>) -> Result { self.encode_rgb_internal(buffer.width(), buffer.height(), buffer.pixels()) } - fn encode_rgb_internal(&self, width: usize, height: usize, pixels: impl Iterator + Send + Sync) -> Result { + fn encode_rgb_internal( + &self, width: usize, height: usize, pixels: impl Iterator> + Send + Sync, + ) -> Result { let matrix_coefficients = match self.color_model { ColorModel::YCbCr => MatrixCoefficients::BT601, ColorModel::RGB => MatrixCoefficients::Identity, }; - match self.depth { - BitDepth::Eight => { - let planes = pixels.map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px, BT601), - ColorModel::RGB => rgb_to_8_bit_gbr(px), - }; - [y, u, v] - }); - self.encode_raw_planes_8_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) - }, - BitDepth::Ten | BitDepth::Auto => { - let planes = pixels.map(|px| { - let (y, u, v) = match self.color_model { - ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px, BT601), - ColorModel::RGB => rgb_to_10_bit_gbr(px), - }; - [y, u, v] - }); - self.encode_raw_planes_10_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) - }, - } - } - - /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, - /// with sRGB transfer characteristics and color primaries. - /// - /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. - /// If there's no alpha, use `None::<[_; 0]>`. - /// - /// returns AVIF file, size of color metadata, size of alpha metadata overhead - #[inline] - pub fn encode_raw_planes_8_bit( - &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, - ) -> Result { - self.encode_raw_planes(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 8) + let planes = pixels.map(|px| match self.color_model { + ColorModel::YCbCr => rgb_to_ycbcr(px, self.depth, BT601), + ColorModel::RGB => [px.g, px.b, px.r], + }); + self.encode_raw_planes(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients, self.depth) } /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, /// with sRGB transfer characteristics and color primaries. /// - /// The pixels are 10-bit (values `0.=1023`). + /// If pixels are 10-bit values range from `0.=1023`. /// /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. /// If there's no alpha, use `None::<[_; 0]>`. /// /// returns AVIF file, size of color metadata, size of alpha metadata overhead - #[inline] - pub fn encode_raw_planes_10_bit( - &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, - ) -> Result { - self.encode_raw_planes(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 10) - } - #[inline(never)] - fn encode_raw_planes( + fn encode_raw_planes( &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, bit_depth: u8, + color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, bit_depth: BitDepth, ) -> Result { let color_description = Some(ColorDescription { transfer_characteristics: TransferCharacteristics::SRGB, @@ -370,7 +315,7 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.into(), + bit_depth: bit_depth.to_usize(), quantizer: self.quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer), threads, @@ -387,7 +332,7 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.into(), + bit_depth: bit_depth.to_usize(), quantizer: self.alpha_quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer), threads, @@ -417,7 +362,7 @@ impl Encoder { _ => return Err(Error::Unsupported("matrix coefficients")), }) .premultiplied_alpha(self.premultiplied_alpha) - .to_vec(&color, alpha.as_deref(), width as u32, height as u32, bit_depth); + .to_vec(&color, alpha.as_deref(), width as u32, height as u32, bit_depth.to_usize() as u8); let color_byte_size = color.len(); let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len()); @@ -427,45 +372,19 @@ impl Encoder { } } -#[inline(always)] -fn to_ten(x: u8) -> u16 { - (u16::from(x) << 2) | (u16::from(x) >> 6) -} - -#[inline(always)] -fn rgb_to_10_bit_gbr(px: rgb::RGB) -> (u16, u16, u16) { - (to_ten(px.g), to_ten(px.b), to_ten(px.r)) -} - -#[inline(always)] -fn rgb_to_8_bit_gbr(px: rgb::RGB) -> (u8, u8, u8) { - (px.g, px.b, px.r) -} - // const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722]; const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140]; #[inline(always)] -fn rgb_to_ycbcr(px: rgb::RGB, depth: u8, matrix: [f32; 3]) -> (f32, f32, f32) { +fn rgb_to_ycbcr(px: Rgb

, bit_depth: BitDepth, matrix: [f32; 3]) -> [P; 3] { + let depth = bit_depth.to_usize(); let max_value = ((1 << depth) - 1) as f32; let scale = max_value / 255.; let shift = (max_value * 0.5).round(); - let y = scale * matrix[0] * f32::from(px.r) + scale * matrix[1] * f32::from(px.g) + scale * matrix[2] * f32::from(px.b); - let cb = (f32::from(px.b) * scale - y).mul_add(0.5 / (1. - matrix[2]), shift); - let cr = (f32::from(px.r) * scale - y).mul_add(0.5 / (1. - matrix[0]), shift); - (y.round(), cb.round(), cr.round()) -} - -#[inline(always)] -fn rgb_to_10_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u16, u16, u16) { - let (y, u, v) = rgb_to_ycbcr(px, 10, matrix); - (y as u16, u as u16, v as u16) -} - -#[inline(always)] -fn rgb_to_8_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u8, u8, u8) { - let (y, u, v) = rgb_to_ycbcr(px, 8, matrix); - (y as u8, u as u8, v as u8) + let y = scale * matrix[0] * u32::cast_from(px.r) as f32 + scale * matrix[1] * u32::cast_from(px.g) as f32 + scale * matrix[2] * u32::cast_from(px.b) as f32; + let cb = P::cast_from((u32::cast_from(px.b) as f32 * scale - y).mul_add(0.5 / (1. - matrix[2]), shift).round() as u16); + let cr = P::cast_from((u32::cast_from(px.r) as f32 * scale - y).mul_add(0.5 / (1. - matrix[0]), shift).round() as u16); + [P::cast_from(y.round() as u16), cb, cr] } fn quality_to_quantizer(quality: f32) -> u8 { @@ -652,9 +571,7 @@ fn rav1e_config(p: &Av1EncodeConfig) -> Config { } } -fn init_frame_3( - width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

, -) -> Result<(), Error> { +fn init_frame_3(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { let mut f = frame.planes.iter_mut(); let mut planes = planes.into_iter(); @@ -677,7 +594,7 @@ fn init_frame_3( Ok(()) } -fn init_frame_1(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { +fn init_frame_1(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { let mut y = frame.planes[0].mut_slice(Default::default()); let mut planes = planes.into_iter(); @@ -691,7 +608,7 @@ fn init_frame_1(width: usize, height: usize, planes: } #[inline(never)] -fn encode_to_av1(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame

) -> Result<(), Error>) -> Result, Error> { +fn encode_to_av1(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame

) -> Result<(), Error>) -> Result, Error> { let mut ctx: Context

= rav1e_config(p).new_context()?; let mut frame = ctx.new_frame(); diff --git a/ravif/src/dirtyalpha.rs b/ravif/src/dirtyalpha.rs index b07b4a1..89c1b8a 100644 --- a/ravif/src/dirtyalpha.rs +++ b/ravif/src/dirtyalpha.rs @@ -1,66 +1,85 @@ -use imgref::{Img, ImgRef}; -use rgb::{ComponentMap, RGB, RGBA8}; +use imgref::Img; +use imgref::ImgRef; +use rav1e::Pixel; +use rgb::ComponentMap; +use rgb::Rgb; +use rgb::Rgba; #[inline] -fn weighed_pixel(px: RGBA8) -> (u16, RGB) { - if px.a == 0 { - return (0, RGB::new(0, 0, 0)); +fn weighed_pixel(px: Rgba

) -> (P, Rgb

) { + if px.a == P::cast_from(0) { + return (px.a, Rgb::new(px.a, px.a, px.a)); } - let weight = 256 - u16::from(px.a); - (weight, RGB::new( - u32::from(px.r) * u32::from(weight), - u32::from(px.g) * u32::from(weight), - u32::from(px.b) * u32::from(weight))) + let weight = P::cast_from(256) - px.a; + ( + weight, + Rgb::new(px.r * weight, px.g * weight, px.b * weight), + ) } /// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1 -pub(crate) fn blurred_dirty_alpha(img: ImgRef) -> Option>> { +pub(crate) fn blurred_dirty_alpha( + img: ImgRef>, +) -> Option>>> { // get dominant visible transparent color (excluding opaque pixels) - let mut sum = RGB::new(0, 0, 0); + let mut sum = Rgb::new(0, 0, 0); let mut weights = 0; // Only consider colors around transparent images // (e.g. solid semitransparent area doesn't need to contribute) loop9::loop9_img(img, |_, _, top, mid, bot| { - if mid.curr.a == 255 || mid.curr.a == 0 { + if mid.curr.a == P::cast_from(255) || mid.curr.a == P::cast_from(0) { return; } - if chain(&top, &mid, &bot).any(|px| px.a == 0) { + if chain(&top, &mid, &bot).any(|px| px.a == P::cast_from(0)) { let (w, px) = weighed_pixel(mid.curr); - weights += u64::from(w); - sum += px.map(u64::from); + weights += Into::::into(w) as u64; + sum += Rgb::new( + Into::::into(px.r) as u64, + Into::::into(px.g) as u64, + Into::::into(px.b) as u64, + ); } }); if weights == 0 { return None; // opaque image } - let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0); + let neutral_alpha = Rgba::new( + P::cast_from((sum.r / weights) as u8), + P::cast_from((sum.g / weights) as u8), + P::cast_from((sum.b / weights) as u8), + P::cast_from(0), + ); let img2 = bleed_opaque_color(img, neutral_alpha); Some(blur_transparent_pixels(img2.as_ref())) } /// copy color from opaque pixels to transparent pixels /// (so that when edges get crushed by compression, the distortion will be away from visible edge) -fn bleed_opaque_color(img: ImgRef, bg: RGBA8) -> Img> { +fn bleed_opaque_color(img: ImgRef>, bg: Rgba

) -> Img>> { let mut out = Vec::with_capacity(img.width() * img.height()); loop9::loop9_img(img, |_, _, top, mid, bot| { - out.push(if mid.curr.a == 255 { + out.push(if mid.curr.a == P::cast_from(255) { mid.curr } else { - let (weights, sum) = chain(&top, &mid, &bot) - .map(|c| weighed_pixel(*c)) - .fold((0u32, RGB::new(0,0,0)), |mut sum, item| { - sum.0 += u32::from(item.0); + let (weights, sum) = chain(&top, &mid, &bot).map(|c| weighed_pixel(*c)).fold( + ( + 0u32, + Rgb::new(P::cast_from(0), P::cast_from(0), P::cast_from(0)), + ), + |mut sum, item| { + sum.0 += Into::::into(item.0); sum.1 += item.1; sum - }); + }, + ); if weights == 0 { bg } else { - let mut avg = sum.map(|c| (c / weights) as u8); - if mid.curr.a == 0 { - avg.with_alpha(0) + let mut avg = sum.map(|c| P::cast_from(Into::::into(c) / weights)); + if mid.curr.a == P::cast_from(0) { + avg.with_alpha(mid.curr.a) } else { // also change non-transparent colors, but only within range where // rounding caused by premultiplied alpha would land on the same color @@ -76,16 +95,16 @@ fn bleed_opaque_color(img: ImgRef, bg: RGBA8) -> Img> { } /// ensure there are no sharp edges created by the cleared alpha -fn blur_transparent_pixels(img: ImgRef) -> Img> { +fn blur_transparent_pixels(img: ImgRef>) -> Img>> { let mut out = Vec::with_capacity(img.width() * img.height()); loop9::loop9_img(img, |_, _, top, mid, bot| { - out.push(if mid.curr.a == 255 { + out.push(if mid.curr.a == P::cast_from(255) { mid.curr } else { - let sum: RGB = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum(); - let mut avg = sum.map(|c| (c / 9) as u8); - if mid.curr.a == 0 { - avg.with_alpha(0) + let sum: Rgb

= chain(&top, &mid, &bot).map(|px| px.rgb()).sum(); + let mut avg = sum.map(|c| (c / P::cast_from(9))); + if mid.curr.a == P::cast_from(0) { + avg.with_alpha(mid.curr.a) } else { // also change non-transparent colors, but only within range where // rounding caused by premultiplied alpha would land on the same color @@ -100,27 +119,42 @@ fn blur_transparent_pixels(img: ImgRef) -> Img> { } #[inline(always)] -fn chain<'a, T>(top: &'a loop9::Triple, mid: &'a loop9::Triple, bot: &'a loop9::Triple) -> impl Iterator + 'a { +fn chain<'a, T>( + top: &'a loop9::Triple, + mid: &'a loop9::Triple, + bot: &'a loop9::Triple, +) -> impl Iterator + 'a { top.iter().chain(mid.iter()).chain(bot.iter()) } #[inline] -fn clamp(px: u8, (min, max): (u8, u8)) -> u8 { - px.max(min).min(max) +fn clamp(px: P, (min, max): (P, P)) -> P { + P::cast_from( + Into::::into(px) + .max(Into::::into(min)) + .min(Into::::into(max)), + ) } /// safe range to change px color given its alpha /// (mostly-transparent colors tolerate more variation) #[inline] -fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) { - let alpha = u16::from(alpha); - let rounded = u16::from(px) * alpha / 255 * 255; +fn premultiplied_minmax(px: P, alpha: T) -> (P, T) +where + P: Pixel + Default, + T: Pixel + Default, +{ + let alpha = Into::::into(alpha); + let rounded = Into::::into(px) * alpha / 255 * 255; // leave some spare room for rounding - let low = ((rounded + 16) / alpha) as u8; - let hi = ((rounded + 239) / alpha) as u8; + let low = (rounded + 16) / alpha; + let hi = (rounded + 239) / alpha; - (low.min(px), hi.max(px)) + ( + P::cast_from(low).min(px), + T::cast_from(hi).max(T::cast_from(Into::::into(px))), + ) } #[test] From 56ad94712277a7b75f359017f5f3589e78d5f338 Mon Sep 17 00:00:00 2001 From: Hannes Vernooij Date: Fri, 13 Dec 2024 09:52:43 +0100 Subject: [PATCH 2/3] Default to 10 bit encoding --- ravif/src/av1encoder.rs | 60 ++++++++++++++++++++++++++++------------- ravif/src/lib.rs | 2 +- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/ravif/src/av1encoder.rs b/ravif/src/av1encoder.rs index cd4b52f..3c50004 100644 --- a/ravif/src/av1encoder.rs +++ b/ravif/src/av1encoder.rs @@ -10,11 +10,11 @@ use rgb::{Rgb, Rgba}; /// For [`Encoder::with_internal_color_model`] #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ColorModel { - /// Standard color space for photographic content. Usually the best choice. + /// Standard color model for photographic content. Usually the best choice. /// This library always uses full-resolution color (4:4:4). /// This library will automatically choose between BT.601 or BT.709. YCbCr, - /// RGB channels are encoded without colorspace transformation. + /// RGB channels are encoded without color space transformation. /// Usually results in larger file sizes, and is less compatible than `YCbCr`. /// Use only if the content really makes use of RGB, e.g. anaglyph images or RGB subpixel anti-aliasing. RGB, @@ -38,10 +38,14 @@ pub enum AlphaColorMode { Premultiplied, } +/// The 8-bit mode only exists as a historical curiosity caused by lack of interoperability with old Safari versions. +/// There's no other reason to use it. 8 bits internally isn't precise enough for a complex codec like AV1, and 10 bits always compresses much better (even if the input and output are 8-bit sRGB). +/// The workaround for Safari is no longer needed, and the 8-bit encoding is planned to be deleted in a few months when usage of the oldest Safari versions becomes negligible. +/// https://github.com/kornelski/cavif-rs/pull/94#discussion_r1883073823 #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] pub enum BitDepth { - #[default] Eight, + #[default] Ten, } @@ -85,7 +89,7 @@ pub struct Encoder { /// [`AlphaColorMode`] alpha_color_mode: AlphaColorMode, /// 8 or 10 - depth: Option, + depth: BitDepth, } /// Builder methods @@ -97,7 +101,7 @@ impl Encoder { quantizer: quality_to_quantizer(80.), alpha_quantizer: quality_to_quantizer(80.), speed: 5, - depth: None, + depth: BitDepth::default(), premultiplied_alpha: false, color_model: ColorModel::YCbCr, threads: None, @@ -115,12 +119,17 @@ impl Encoder { self } - /// Depth 8 or 10. `None` picks automatically. + #[doc(hidden)] + #[deprecated(note = "Renamed to with_bit_depth")] + pub fn with_depth(self, depth: Option) -> Self { + self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Ten)) + } + + /// Depth 8 or 10-bit, default is 10-bit, even when 8 bit input data is provided. #[inline(always)] #[track_caller] #[must_use] - pub fn with_depth(mut self, depth: Option) -> Self { - assert!(depth.map_or(true, |d| d == 8 || d == 10)); + pub fn with_bit_depth(mut self, depth: BitDepth) -> Self { self.depth = depth; self } @@ -206,7 +215,7 @@ impl Encoder { pub fn encode_rgba(&self, in_buffer: Img<&[Rgba

]>) -> Result { let new_alpha = self.convert_alpha(in_buffer); let buffer = new_alpha.as_ref().map(|b: &Img>>| b.as_ref()).unwrap_or(in_buffer); - let use_alpha = buffer.pixels().any(|px| Into::::into(px.a) != 255); + let use_alpha = buffer.pixels().any(|px| px.a != P::cast_from(255)); if !use_alpha { return self.encode_rgb_internal(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb())); } @@ -218,14 +227,13 @@ impl Encoder { ColorModel::RGB => MatrixCoefficients::Identity, }; let planes = buffer.pixels().map(|px| match self.color_model { - ColorModel::YCbCr => rgb_to_ycbcr(Rgb::new(px.r, px.g, px.b), self.depth, BT601), + ColorModel::YCbCr => rgb_to_ycbcr(px.rgb(), self.depth, BT601), ColorModel::RGB => [px.g, px.b, px.r], }); let alpha = buffer.pixels().map(|px| px.a); - self.encode_raw_planes(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients, self.depth) + self.encode_raw_planes(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) } - /// Todo: We shouldn't assume this is 8 bits, 10 bit input signals only has 2 alpha bits. fn convert_alpha(&self, in_buffer: Img<&[Rgba

]>) -> Option>> { match self.alpha_color_mode { AlphaColorMode::UnassociatedDirty => None, @@ -279,11 +287,22 @@ impl Encoder { ColorModel::RGB => MatrixCoefficients::Identity, }; + let is_eight_bit = std::mem::size_of::

() == 1; + let input_bit_depth = if is_eight_bit { BitDepth::Eight } else { BitDepth::Ten }; + + // First convert from RGB to GBR or YCbCr let planes = pixels.map(|px| match self.color_model { - ColorModel::YCbCr => rgb_to_ycbcr(px, self.depth, BT601), + ColorModel::YCbCr => rgb_to_ycbcr(px, input_bit_depth, BT601), ColorModel::RGB => [px.g, px.b, px.r], }); - self.encode_raw_planes(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients, self.depth) + + // Then convert the bit depth when needed. + if self.depth != BitDepth::Eight && is_eight_bit { + let planes_u16 = planes.map(|px| [to_ten(px[0]), to_ten(px[1]), to_ten(px[2])]); + self.encode_raw_planes(width, height, planes_u16, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) + } else { + self.encode_raw_planes(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) + } } /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, @@ -298,7 +317,7 @@ impl Encoder { #[inline(never)] fn encode_raw_planes( &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, - color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, bit_depth: BitDepth, + color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients ) -> Result { let color_description = Some(ColorDescription { transfer_characteristics: TransferCharacteristics::SRGB, @@ -315,7 +334,7 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.to_usize(), + bit_depth: self.depth.to_usize(), quantizer: self.quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer), threads, @@ -332,7 +351,7 @@ impl Encoder { &Av1EncodeConfig { width, height, - bit_depth: bit_depth.to_usize(), + bit_depth: self.depth.to_usize(), quantizer: self.alpha_quantizer.into(), speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer), threads, @@ -362,7 +381,7 @@ impl Encoder { _ => return Err(Error::Unsupported("matrix coefficients")), }) .premultiplied_alpha(self.premultiplied_alpha) - .to_vec(&color, alpha.as_deref(), width as u32, height as u32, bit_depth.to_usize() as u8); + .to_vec(&color, alpha.as_deref(), width as u32, height as u32, self.depth.to_usize() as u8); let color_byte_size = color.len(); let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len()); @@ -375,6 +394,11 @@ impl Encoder { // const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722]; const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140]; +#[inline(always)] +fn to_ten(x: P) -> u16 { + (u16::cast_from(x) << 2) | (u16::cast_from(x) >> 6) +} + #[inline(always)] fn rgb_to_ycbcr(px: Rgb

, bit_depth: BitDepth, matrix: [f32; 3]) -> [P; 3] { let depth = bit_depth.to_usize(); diff --git a/ravif/src/lib.rs b/ravif/src/lib.rs index 3dee2b5..eb62478 100644 --- a/ravif/src/lib.rs +++ b/ravif/src/lib.rs @@ -27,7 +27,7 @@ mod dirtyalpha; #[doc(no_inline)] pub use imgref::Img; #[doc(no_inline)] -pub use rgb::{RGB8, RGBA8}; +pub use rgb::{RGB16, RGB8, RGBA16, RGBA8}; #[cfg(not(feature = "threading"))] mod rayoff { From a8e10d4072a8ec6d9af4bb557457bb1018c25ae1 Mon Sep 17 00:00:00 2001 From: Hannes Vernooij Date: Wed, 18 Dec 2024 12:06:24 +0100 Subject: [PATCH 3/3] Support different bit depths in convert_alpha --- ravif/src/av1encoder.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ravif/src/av1encoder.rs b/ravif/src/av1encoder.rs index 3c50004..fd2f5f7 100644 --- a/ravif/src/av1encoder.rs +++ b/ravif/src/av1encoder.rs @@ -235,20 +235,21 @@ impl Encoder { } fn convert_alpha(&self, in_buffer: Img<&[Rgba

]>) -> Option>> { + let max_value = (1 << self.depth.to_usize()) -1; match self.alpha_color_mode { AlphaColorMode::UnassociatedDirty => None, AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer), AlphaColorMode::Premultiplied => { let prem = in_buffer .pixels() - .filter(|px| px.a != P::cast_from(255)) + .filter(|px| px.a != P::cast_from(max_value)) .map(|px| { if Into::::into(px.a) == 0 { Rgba::new(px.a, px.a, px.a, px.a) } else { - let r = px.r * P::cast_from(255) / px.a; - let g = px.g * P::cast_from(255) / px.a; - let b = px.b * P::cast_from(255) / px.a; + let r = px.r * P::cast_from(max_value) / px.a; + let g = px.g * P::cast_from(max_value) / px.a; + let b = px.b * P::cast_from(max_value) / px.a; Rgba::new(r, g, b, px.a) } })