blob: 96728c8038c5fd82362fc60a6724f2d5111877e0 [file] [log] [blame]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::coeffs::*;
use super::rgb;
use super::rgb::*;
use crate::image::Plane;
use crate::image::PlaneRow;
use crate::image::YuvRange;
use crate::internal_utils::*;
use crate::*;
use std::cmp::min;
#[derive(Clone, Copy, PartialEq)]
enum Mode {
YuvCoefficients(f32, f32, f32),
Identity,
Ycgco,
YcgcoRe,
YcgcoRo,
}
impl From<&image::Image> for Mode {
fn from(image: &image::Image) -> Self {
match image.matrix_coefficients {
MatrixCoefficients::Identity => Mode::Identity,
MatrixCoefficients::Ycgco => Mode::Ycgco,
MatrixCoefficients::YcgcoRe => Mode::YcgcoRe,
MatrixCoefficients::YcgcoRo => Mode::YcgcoRo,
_ => {
let coeffs =
calculate_yuv_coefficients(image.color_primaries, image.matrix_coefficients);
Mode::YuvCoefficients(coeffs[0], coeffs[1], coeffs[2])
}
}
}
}
fn identity_yuv8_to_rgb8_full_range(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()> {
if image.yuv_format != PixelFormat::Yuv444 || rgb.format == Format::Rgb565 {
return Err(AvifError::NotImplemented);
}
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let channel_count = rgb.channel_count() as usize;
for i in 0..image.height {
let y = image.row(Plane::Y, i)?;
let u = image.row(Plane::U, i)?;
let v = image.row(Plane::V, i)?;
let rgb_pixels = rgb.row_mut(i)?;
for j in 0..image.width as usize {
rgb_pixels[(j * channel_count) + r_offset] = v[j];
rgb_pixels[(j * channel_count) + g_offset] = y[j];
rgb_pixels[(j * channel_count) + b_offset] = u[j];
}
}
Ok(())
}
// This is a macro and not a function because this is invoked per-pixel and there is a non-trivial
// performance impact if this is made into a function call.
macro_rules! store_rgb_pixel8 {
($dst:ident, $rgb_565: ident, $index: ident, $r: ident, $g: ident, $b: ident, $r_offset: ident,
$g_offset: ident, $b_offset: ident, $rgb_channel_count: ident, $rgb_max_channel_f: ident) => {
let r8 = (0.5 + ($r * $rgb_max_channel_f)) as u8;
let g8 = (0.5 + ($g * $rgb_max_channel_f)) as u8;
let b8 = (0.5 + ($b * $rgb_max_channel_f)) as u8;
if $rgb_565 {
// References for RGB565 color conversion:
// * https://docs.microsoft.com/en-us/windows/win32/directshow/working-with-16-bit-rgb
// * https://chromium.googlesource.com/libyuv/libyuv/+/9892d70c965678381d2a70a1c9002d1cf136ee78/source/row_common.cc#2362
let r16 = ((r8 >> 3) as u16) << 11;
let g16 = ((g8 >> 2) as u16) << 5;
let b16 = (b8 >> 3) as u16;
let rgb565 = (r16 | g16 | b16).to_le_bytes();
$dst[($index * $rgb_channel_count) + $r_offset] = rgb565[0];
$dst[($index * $rgb_channel_count) + $r_offset + 1] = rgb565[1];
} else {
$dst[($index * $rgb_channel_count) + $r_offset] = r8;
$dst[($index * $rgb_channel_count) + $g_offset] = g8;
$dst[($index * $rgb_channel_count) + $b_offset] = b8;
}
};
}
fn yuv8_to_rgb8_color(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let table_uv = match &table_uv {
Some(table_uv) => table_uv,
None => &table_y,
};
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let rgb_565 = rgb.format == rgb::Format::Rgb565;
let chroma_shift = image.yuv_format.chroma_shift_x();
for j in 0..image.height {
let uv_j = j >> image.yuv_format.chroma_shift_y();
let y_row = image.row(Plane::Y, j)?;
let u_row = image.row(Plane::U, uv_j)?;
let v_row = image.row(Plane::V, uv_j)?;
let dst = rgb.row_mut(j)?;
for i in 0..image.width as usize {
let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
let y = table_y[y_row[i] as usize];
let cb = table_uv[u_row[uv_i] as usize];
let cr = table_uv[v_row[uv_i] as usize];
let r = y + (2.0 * (1.0 - kr)) * cr;
let b = y + (2.0 * (1.0 - kb)) * cb;
let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
let r = clamp_f32(r, 0.0, 1.0);
let g = clamp_f32(g, 0.0, 1.0);
let b = clamp_f32(b, 0.0, 1.0);
store_rgb_pixel8!(
dst,
rgb_565,
i,
r,
g,
b,
r_offset,
g_offset,
b_offset,
rgb_channel_count,
rgb_max_channel_f
);
}
}
Ok(())
}
fn yuv16_to_rgb16_color(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let table_uv = match &table_uv {
Some(table_uv) => table_uv,
None => &table_y,
};
let yuv_max_channel = image.max_channel();
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let chroma_shift = image.yuv_format.chroma_shift_x();
for j in 0..image.height {
let uv_j = j >> image.yuv_format.chroma_shift_y();
let y_row = image.row16(Plane::Y, j)?;
let u_row = image.row16(Plane::U, uv_j)?;
// If V plane is missing, then the format is P010. In that case, set V
// as U plane but starting at offset 1.
let v_row = image.row16(Plane::V, uv_j).unwrap_or(&u_row[1..]);
let dst = rgb.row16_mut(j)?;
for i in 0..image.width as usize {
let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
let y = table_y[min(y_row[i], yuv_max_channel) as usize];
let cb = table_uv[min(u_row[uv_i], yuv_max_channel) as usize];
let cr = table_uv[min(v_row[uv_i], yuv_max_channel) as usize];
let r = y + (2.0 * (1.0 - kr)) * cr;
let b = y + (2.0 * (1.0 - kb)) * cb;
let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
let r = clamp_f32(r, 0.0, 1.0);
let g = clamp_f32(g, 0.0, 1.0);
let b = clamp_f32(b, 0.0, 1.0);
dst[(i * rgb_channel_count) + r_offset] = (0.5 + (r * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + g_offset] = (0.5 + (g * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + b_offset] = (0.5 + (b * rgb_max_channel_f)) as u16;
}
}
Ok(())
}
fn yuv16_to_rgb8_color(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let table_uv = match &table_uv {
Some(table_uv) => table_uv,
None => &table_y,
};
let yuv_max_channel = image.max_channel();
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let rgb_565 = rgb.format == rgb::Format::Rgb565;
let chroma_shift = image.yuv_format.chroma_shift_x();
for j in 0..image.height {
let uv_j = j >> image.yuv_format.chroma_shift_y();
let y_row = image.row16(Plane::Y, j)?;
let u_row = image.row16(Plane::U, uv_j)?;
// If V plane is missing, then the format is P010. In that case, set V
// as U plane but starting at offset 1.
let v_row = image.row16(Plane::V, uv_j).unwrap_or(&u_row[1..]);
let dst = rgb.row_mut(j)?;
for i in 0..image.width as usize {
let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
let y = table_y[min(y_row[i], yuv_max_channel) as usize];
let cb = table_uv[min(u_row[uv_i], yuv_max_channel) as usize];
let cr = table_uv[min(v_row[uv_i], yuv_max_channel) as usize];
let r = y + (2.0 * (1.0 - kr)) * cr;
let b = y + (2.0 * (1.0 - kb)) * cb;
let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
let r = clamp_f32(r, 0.0, 1.0);
let g = clamp_f32(g, 0.0, 1.0);
let b = clamp_f32(b, 0.0, 1.0);
store_rgb_pixel8!(
dst,
rgb_565,
i,
r,
g,
b,
r_offset,
g_offset,
b_offset,
rgb_channel_count,
rgb_max_channel_f
);
}
}
Ok(())
}
fn yuv8_to_rgb16_color(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let table_uv = match &table_uv {
Some(table_uv) => table_uv,
None => &table_y,
};
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let chroma_shift = image.yuv_format.chroma_shift_x();
for j in 0..image.height {
let uv_j = j >> image.yuv_format.chroma_shift_y();
let y_row = image.row(Plane::Y, j)?;
let u_row = image.row(Plane::U, uv_j)?;
let v_row = image.row(Plane::V, uv_j)?;
let dst = rgb.row16_mut(j)?;
for i in 0..image.width as usize {
let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
let y = table_y[y_row[i] as usize];
let cb = table_uv[u_row[uv_i] as usize];
let cr = table_uv[v_row[uv_i] as usize];
let r = y + (2.0 * (1.0 - kr)) * cr;
let b = y + (2.0 * (1.0 - kb)) * cb;
let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
let r = clamp_f32(r, 0.0, 1.0);
let g = clamp_f32(g, 0.0, 1.0);
let b = clamp_f32(b, 0.0, 1.0);
dst[(i * rgb_channel_count) + r_offset] = (0.5 + (r * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + g_offset] = (0.5 + (g * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + b_offset] = (0.5 + (b * rgb_max_channel_f)) as u16;
}
}
Ok(())
}
fn yuv8_to_rgb8_monochrome(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let rgb_565 = rgb.format == rgb::Format::Rgb565;
for j in 0..image.height {
let y_row = image.row(Plane::Y, j)?;
let dst = rgb.row_mut(j)?;
for i in 0..image.width as usize {
let y = table_y[y_row[i] as usize];
store_rgb_pixel8!(
dst,
rgb_565,
i,
y,
y,
y,
r_offset,
g_offset,
b_offset,
rgb_channel_count,
rgb_max_channel_f
);
}
}
Ok(())
}
fn yuv16_to_rgb16_monochrome(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let yuv_max_channel = image.max_channel();
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
for j in 0..image.height {
let y_row = image.row16(Plane::Y, j)?;
let dst = rgb.row16_mut(j)?;
for i in 0..image.width as usize {
let y = table_y[min(y_row[i], yuv_max_channel) as usize];
let rgb_pixel = (0.5 + (y * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + r_offset] = rgb_pixel;
dst[(i * rgb_channel_count) + g_offset] = rgb_pixel;
dst[(i * rgb_channel_count) + b_offset] = rgb_pixel;
}
}
Ok(())
}
fn yuv16_to_rgb8_monochrome(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let yuv_max_channel = image.max_channel();
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let rgb_565 = rgb.format == rgb::Format::Rgb565;
for j in 0..image.height {
let y_row = image.row16(Plane::Y, j)?;
let dst = rgb.row_mut(j)?;
for i in 0..image.width as usize {
let y = table_y[min(y_row[i], yuv_max_channel) as usize];
store_rgb_pixel8!(
dst,
rgb_565,
i,
y,
y,
y,
r_offset,
g_offset,
b_offset,
rgb_channel_count,
rgb_max_channel_f
);
}
}
Ok(())
}
fn yuv8_to_rgb16_monochrome(
image: &image::Image,
rgb: &mut rgb::Image,
kr: f32,
kg: f32,
kb: f32,
) -> AvifResult<()> {
let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
let rgb_max_channel_f = rgb.max_channel_f();
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
for j in 0..image.height {
let y_row = image.row(Plane::Y, j)?;
let dst = rgb.row16_mut(j)?;
for i in 0..image.width as usize {
let y = table_y[y_row[i] as usize];
let rgb_pixel = (0.5 + (y * rgb_max_channel_f)) as u16;
dst[(i * rgb_channel_count) + r_offset] = rgb_pixel;
dst[(i * rgb_channel_count) + g_offset] = rgb_pixel;
dst[(i * rgb_channel_count) + b_offset] = rgb_pixel;
}
}
Ok(())
}
pub(crate) fn yuv_to_rgb_fast(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()> {
let mode: Mode = image.into();
match mode {
Mode::Identity => {
if image.depth == 8 && rgb.depth == 8 && image.yuv_range == YuvRange::Full {
identity_yuv8_to_rgb8_full_range(image, rgb)
} else {
// TODO: Add more fast paths for identity.
Err(AvifError::NotImplemented)
}
}
Mode::YuvCoefficients(kr, kg, kb) => {
let has_color = image.yuv_format != PixelFormat::Yuv400;
match (image.depth == 8, rgb.depth == 8, has_color) {
(true, true, true) => yuv8_to_rgb8_color(image, rgb, kr, kg, kb),
(false, false, true) => yuv16_to_rgb16_color(image, rgb, kr, kg, kb),
(false, true, true) => yuv16_to_rgb8_color(image, rgb, kr, kg, kb),
(true, false, true) => yuv8_to_rgb16_color(image, rgb, kr, kg, kb),
(true, true, false) => yuv8_to_rgb8_monochrome(image, rgb, kr, kg, kb),
(false, false, false) => yuv16_to_rgb16_monochrome(image, rgb, kr, kg, kb),
(false, true, false) => yuv16_to_rgb8_monochrome(image, rgb, kr, kg, kb),
(true, false, false) => yuv8_to_rgb16_monochrome(image, rgb, kr, kg, kb),
}
}
Mode::Ycgco | Mode::YcgcoRe | Mode::YcgcoRo => Err(AvifError::NotImplemented),
}
}
fn unorm_lookup_tables(
image: &image::Image,
mode: Mode,
) -> AvifResult<(Vec<f32>, Option<Vec<f32>>)> {
let count = 1usize << image.depth;
let mut table_y: Vec<f32> = create_vec_exact(count)?;
let bias_y;
let range_y;
// Formula specified in ISO/IEC 23091-2.
if image.yuv_range == YuvRange::Limited {
bias_y = (16 << (image.depth - 8)) as f32;
range_y = (219 << (image.depth - 8)) as f32;
} else {
bias_y = 0.0;
range_y = image.max_channel_f();
}
for cp in 0..count {
table_y.push(((cp as f32) - bias_y) / range_y);
}
if mode == Mode::Identity {
Ok((table_y, None))
} else {
// Formula specified in ISO/IEC 23091-2.
let bias_uv = (1 << (image.depth - 1)) as f32;
let range_uv = if image.yuv_range == YuvRange::Limited {
(224 << (image.depth - 8)) as f32
} else {
image.max_channel_f()
};
let mut table_uv: Vec<f32> = create_vec_exact(count)?;
for cp in 0..count {
table_uv.push(((cp as f32) - bias_uv) / range_uv);
}
Ok((table_y, Some(table_uv)))
}
}
#[allow(clippy::too_many_arguments)]
fn compute_rgb(
y: f32,
cb: f32,
cr: f32,
has_color: bool,
mode: Mode,
clamped_y: u16,
yuv_max_channel: u16,
rgb_max_channel: u16,
rgb_max_channel_f: f32,
) -> (f32, f32, f32) {
let r: f32;
let g: f32;
let b: f32;
if has_color {
match mode {
Mode::Identity => {
g = y;
b = cb;
r = cr;
}
Mode::Ycgco => {
let t = y - cb;
g = y + cb;
b = t - cr;
r = t + cr;
}
Mode::YcgcoRe | Mode::YcgcoRo => {
// Equations (62) through (65) in https://www.itu.int/rec/T-REC-H.273
let cg = (0.5 + cb * yuv_max_channel as f32).floor() as i32;
let co = (0.5 + cr * yuv_max_channel as f32).floor() as i32;
let t = clamped_y as i32 - (cg >> 1);
let rgb_max_channel = rgb_max_channel as i32;
g = clamp_i32(t + cg, 0, rgb_max_channel) as f32 / rgb_max_channel_f;
let tmp_b = clamp_i32(t - (co >> 1), 0, rgb_max_channel) as f32;
b = tmp_b / rgb_max_channel_f;
r = clamp_i32(tmp_b as i32 + co, 0, rgb_max_channel) as f32 / rgb_max_channel_f;
}
Mode::YuvCoefficients(kr, kg, kb) => {
r = y + (2.0 * (1.0 - kr)) * cr;
b = y + (2.0 * (1.0 - kb)) * cb;
g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
}
}
} else {
r = y;
g = y;
b = y;
}
(
clamp_f32(r, 0.0, 1.0),
clamp_f32(g, 0.0, 1.0),
clamp_f32(b, 0.0, 1.0),
)
}
fn clamped_pixel(row: PlaneRow, index: usize, max_channel: u16) -> u16 {
match row {
PlaneRow::Depth8(row) => row[index] as u16,
PlaneRow::Depth16(row) => min(max_channel, row[index]),
}
}
fn unorm_value(row: PlaneRow, index: usize, max_channel: u16, table: &[f32]) -> f32 {
table[clamped_pixel(row, index, max_channel) as usize]
}
pub(crate) fn yuv_to_rgb_any(
image: &image::Image,
rgb: &mut rgb::Image,
alpha_multiply_mode: AlphaMultiplyMode,
) -> AvifResult<()> {
let mode: Mode = image.into();
let (table_y, table_uv) = unorm_lookup_tables(image, mode)?;
let table_uv = match &table_uv {
Some(table_uv) => table_uv,
None => &table_y,
};
let r_offset = rgb.format.r_offset();
let g_offset = rgb.format.g_offset();
let b_offset = rgb.format.b_offset();
let rgb_channel_count = rgb.channel_count() as usize;
let rgb_depth = rgb.depth;
let chroma_upsampling = rgb.chroma_upsampling;
let has_color = image.has_plane(Plane::U)
&& image.has_plane(Plane::V)
&& image.yuv_format != PixelFormat::Yuv400;
let yuv_max_channel = image.max_channel();
let rgb_max_channel = rgb.max_channel();
let rgb_max_channel_f = rgb.max_channel_f();
let chroma_shift = image.yuv_format.chroma_shift_x();
for j in 0..image.height {
let uv_j = j >> image.yuv_format.chroma_shift_y();
let y_row = image.row_generic(Plane::Y, j)?;
let u_row = image.row_generic(Plane::U, uv_j).ok();
let v_row = image.row_generic(Plane::V, uv_j).ok();
let a_row = image.row_generic(Plane::A, j).ok();
for i in 0..image.width as usize {
let clamped_y = clamped_pixel(y_row, i, yuv_max_channel);
let y = table_y[clamped_y as usize];
let mut cb = 0.5;
let mut cr = 0.5;
if has_color {
let u_row = u_row.unwrap();
let v_row = v_row.unwrap();
let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
if image.yuv_format == PixelFormat::Yuv444
|| matches!(
chroma_upsampling,
ChromaUpsampling::Fastest | ChromaUpsampling::Nearest
)
{
cb = unorm_value(u_row, uv_i, yuv_max_channel, table_uv);
cr = unorm_value(v_row, uv_i, yuv_max_channel, table_uv);
} else {
if image.chroma_sample_position != ChromaSamplePosition::CENTER {
return Err(AvifError::NotImplemented);
}
// Bilinear filtering with weights. See
// https://github.com/AOMediaCodec/libavif/blob/0580334466d57fedb889d5ed7ae9574d6f66e00c/src/reformat.c#L657-L685.
let image_width_minus_1 = (image.width - 1) as usize;
let uv_adj_i = if i == 0 || (i == image_width_minus_1 && (i % 2) != 0) {
uv_i
} else if (i % 2) != 0 {
uv_i + 1
} else {
uv_i - 1
};
let uv_adj_j = if j == 0
|| (j == image.height - 1 && (j % 2) != 0)
|| image.yuv_format == PixelFormat::Yuv422
{
uv_j
} else if (j % 2) != 0 {
uv_j + 1
} else {
uv_j - 1
};
let u_adj_row = image.row_generic(Plane::U, uv_adj_j)?;
let v_adj_row = image.row_generic(Plane::V, uv_adj_j)?;
let mut unorm_u: [[f32; 2]; 2] = [[0.0; 2]; 2];
let mut unorm_v: [[f32; 2]; 2] = [[0.0; 2]; 2];
unorm_u[0][0] = unorm_value(u_row, uv_i, yuv_max_channel, table_uv);
unorm_v[0][0] = unorm_value(v_row, uv_i, yuv_max_channel, table_uv);
unorm_u[1][0] = unorm_value(u_row, uv_adj_i, yuv_max_channel, table_uv);
unorm_v[1][0] = unorm_value(v_row, uv_adj_i, yuv_max_channel, table_uv);
unorm_u[0][1] = unorm_value(u_adj_row, uv_i, yuv_max_channel, table_uv);
unorm_v[0][1] = unorm_value(v_adj_row, uv_i, yuv_max_channel, table_uv);
unorm_u[1][1] = unorm_value(u_adj_row, uv_adj_i, yuv_max_channel, table_uv);
unorm_v[1][1] = unorm_value(v_adj_row, uv_adj_i, yuv_max_channel, table_uv);
cb = (unorm_u[0][0] * (9.0 / 16.0))
+ (unorm_u[1][0] * (3.0 / 16.0))
+ (unorm_u[0][1] * (3.0 / 16.0))
+ (unorm_u[1][1] * (1.0 / 16.0));
cr = (unorm_v[0][0] * (9.0 / 16.0))
+ (unorm_v[1][0] * (3.0 / 16.0))
+ (unorm_v[0][1] * (3.0 / 16.0))
+ (unorm_v[1][1] * (1.0 / 16.0));
}
}
let (mut rc, mut gc, mut bc) = compute_rgb(
y,
cb,
cr,
has_color,
mode,
clamped_y,
yuv_max_channel,
rgb_max_channel,
rgb_max_channel_f,
);
if alpha_multiply_mode != AlphaMultiplyMode::NoOp {
let unorm_a = clamped_pixel(a_row.unwrap(), i, yuv_max_channel);
let ac = clamp_f32((unorm_a as f32) / (yuv_max_channel as f32), 0.0, 1.0);
if ac == 0.0 {
rc = 0.0;
gc = 0.0;
bc = 0.0;
} else if ac < 1.0 {
match alpha_multiply_mode {
AlphaMultiplyMode::Multiply => {
rc *= ac;
gc *= ac;
bc *= ac;
}
AlphaMultiplyMode::UnMultiply => {
rc = f32::min(rc / ac, 1.0);
gc = f32::min(gc / ac, 1.0);
bc = f32::min(bc / ac, 1.0);
}
_ => {} // Not reached.
}
}
}
if rgb_depth == 8 {
let dst = rgb.row_mut(j)?;
dst[(i * rgb_channel_count) + r_offset] = (0.5 + (rc * rgb_max_channel_f)) as u8;
dst[(i * rgb_channel_count) + g_offset] = (0.5 + (gc * rgb_max_channel_f)) as u8;
dst[(i * rgb_channel_count) + b_offset] = (0.5 + (bc * rgb_max_channel_f)) as u8;
} else {
let dst16 = rgb.row16_mut(j)?;
dst16[(i * rgb_channel_count) + r_offset] = (0.5 + (rc * rgb_max_channel_f)) as u16;
dst16[(i * rgb_channel_count) + g_offset] = (0.5 + (gc * rgb_max_channel_f)) as u16;
dst16[(i * rgb_channel_count) + b_offset] = (0.5 + (bc * rgb_max_channel_f)) as u16;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yuv_to_rgb() {
fn create_420(
matrix_coefficients: MatrixCoefficients,
y: &[&[u8]],
u: &[&[u8]],
v: &[&[u8]],
) -> image::Image {
let mut yuv = image::Image {
width: y[0].len() as u32,
height: y.len() as u32,
depth: 8,
yuv_format: PixelFormat::Yuv420,
matrix_coefficients,
yuv_range: YuvRange::Limited,
..Default::default()
};
assert!(yuv.allocate_planes(Category::Color).is_ok());
for plane in image::YUV_PLANES {
let samples = if plane == Plane::Y {
&y
} else if plane == Plane::U {
&u
} else {
&v
};
assert_eq!(yuv.height(plane), samples.len());
for y in 0..yuv.height(plane) {
assert_eq!(yuv.width(plane), samples[y].len());
for x in 0..yuv.width(plane) {
yuv.row_mut(plane, y as u32).unwrap()[x] = samples[y][x];
}
}
}
yuv
}
fn assert_near(yuv: &image::Image, r: &[&[u8]], g: &[&[u8]], b: &[&[u8]]) {
let mut dst = rgb::Image::create_from_yuv(yuv);
dst.format = rgb::Format::Rgb;
dst.chroma_upsampling = ChromaUpsampling::Bilinear;
assert!(dst.allocate().is_ok());
assert!(yuv_to_rgb_any(yuv, &mut dst, AlphaMultiplyMode::NoOp).is_ok());
assert_eq!(dst.height, r.len() as u32);
assert_eq!(dst.height, g.len() as u32);
assert_eq!(dst.height, b.len() as u32);
for y in 0..dst.height {
assert_eq!(dst.width, r[y as usize].len() as u32);
assert_eq!(dst.width, g[y as usize].len() as u32);
assert_eq!(dst.width, b[y as usize].len() as u32);
for x in 0..dst.width {
let i = (x * dst.pixel_size() + 0) as usize;
let pixel = &dst.row(y).unwrap()[i..i + 3];
assert_eq!(pixel[0], r[y as usize][x as usize]);
assert_eq!(pixel[1], g[y as usize][x as usize]);
assert_eq!(pixel[2], b[y as usize][x as usize]);
}
}
}
// Testing identity 4:2:0 -> RGB would be simpler to check upsampling
// but this is not allowed (not a real use case).
assert_near(
&create_420(
MatrixCoefficients::Bt601,
/*y=*/
&[
&[0, 100, 200], //
&[10, 110, 210], //
&[50, 150, 250],
],
/*u=*/
&[
&[0, 100], //
&[10, 110],
],
/*v=*/
&[
&[57, 57], //
&[57, 57],
],
),
/*r=*/
&[
&[0, 0, 101], //
&[0, 0, 113], //
&[0, 43, 159],
],
/*g=*/
&[
&[89, 196, 255], //
&[100, 207, 255], //
&[145, 251, 255],
],
/*b=*/
&[
&[0, 0, 107], //
&[0, 0, 124], //
&[0, 0, 181],
],
);
// Extreme values.
assert_near(
&create_420(
MatrixCoefficients::Bt601,
/*y=*/ &[&[0]],
/*u=*/ &[&[0]],
/*v=*/ &[&[0]],
),
/*r=*/ &[&[0]],
/*g=*/ &[&[136]],
/*b=*/ &[&[0]],
);
assert_near(
&create_420(
MatrixCoefficients::Bt601,
/*y=*/ &[&[255]],
/*u=*/ &[&[255]],
/*v=*/ &[&[255]],
),
/*r=*/ &[&[255]],
/*g=*/ &[&[125]],
/*b=*/ &[&[255]],
);
// Top-right square "bleeds" into other samples during upsampling.
assert_near(
&create_420(
MatrixCoefficients::Bt601,
/*y=*/
&[
&[0, 0, 255, 255],
&[0, 0, 255, 255],
&[0, 0, 0, 0],
&[0, 0, 0, 0],
],
/*u=*/
&[
&[0, 255], //
&[0, 0],
],
/*v=*/
&[
&[0, 255], //
&[0, 0],
],
),
/*r=*/
&[
&[0, 0, 255, 255],
&[0, 0, 255, 255],
&[0, 0, 0, 0],
&[0, 0, 0, 0],
],
/*g=*/
&[
&[136, 59, 202, 125],
&[136, 78, 255, 202],
&[136, 116, 78, 59],
&[136, 136, 136, 136],
],
/*b=*/
&[
&[0, 0, 255, 255],
&[0, 0, 255, 255],
&[0, 0, 0, 0],
&[0, 0, 0, 0],
],
);
// Middle square does not "bleed" into other samples during upsampling.
assert_near(
&create_420(
MatrixCoefficients::Bt601,
/*y=*/
&[
&[0, 0, 0, 0],
&[0, 255, 255, 0],
&[0, 255, 255, 0],
&[0, 0, 0, 0],
],
/*u=*/
&[
&[0, 0], //
&[0, 0],
],
/*v=*/
&[
&[0, 0], //
&[0, 0],
],
),
/*r=*/
&[
&[0, 0, 0, 0],
&[0, 74, 74, 0],
&[0, 74, 74, 0],
&[0, 0, 0, 0],
],
/*g=*/
&[
&[136, 136, 136, 136],
&[136, 255, 255, 136],
&[136, 255, 255, 136],
&[136, 136, 136, 136],
],
/*b=*/
&[
&[0, 0, 0, 0],
&[0, 20, 20, 0],
&[0, 20, 20, 0],
&[0, 0, 0, 0],
],
);
}
}