1 // Copyright 2024 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use super::coeffs::*;
16 use super::rgb;
17 use super::rgb::*;
18
19 use crate::image::Plane;
20 use crate::image::PlaneRow;
21 use crate::image::YuvRange;
22 use crate::internal_utils::*;
23 use crate::*;
24
25 use std::cmp::min;
26
27 #[derive(Clone, Copy, PartialEq)]
28 enum Mode {
29 YuvCoefficients(f32, f32, f32),
30 Identity,
31 Ycgco,
32 YcgcoRe,
33 YcgcoRo,
34 }
35
36 impl From<&image::Image> for Mode {
from(image: &image::Image) -> Self37 fn from(image: &image::Image) -> Self {
38 match image.matrix_coefficients {
39 MatrixCoefficients::Identity => Mode::Identity,
40 MatrixCoefficients::Ycgco => Mode::Ycgco,
41 MatrixCoefficients::YcgcoRe => Mode::YcgcoRe,
42 MatrixCoefficients::YcgcoRo => Mode::YcgcoRo,
43 _ => {
44 let coeffs =
45 calculate_yuv_coefficients(image.color_primaries, image.matrix_coefficients);
46 Mode::YuvCoefficients(coeffs[0], coeffs[1], coeffs[2])
47 }
48 }
49 }
50 }
51
identity_yuv8_to_rgb8_full_range(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()>52 fn identity_yuv8_to_rgb8_full_range(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()> {
53 if image.yuv_format != PixelFormat::Yuv444 || rgb.format == Format::Rgb565 {
54 return Err(AvifError::NotImplemented);
55 }
56
57 let r_offset = rgb.format.r_offset();
58 let g_offset = rgb.format.g_offset();
59 let b_offset = rgb.format.b_offset();
60 let channel_count = rgb.channel_count() as usize;
61 for i in 0..image.height {
62 let y = image.row(Plane::Y, i)?;
63 let u = image.row(Plane::U, i)?;
64 let v = image.row(Plane::V, i)?;
65 let rgb_pixels = rgb.row_mut(i)?;
66 for j in 0..image.width as usize {
67 rgb_pixels[(j * channel_count) + r_offset] = v[j];
68 rgb_pixels[(j * channel_count) + g_offset] = y[j];
69 rgb_pixels[(j * channel_count) + b_offset] = u[j];
70 }
71 }
72 Ok(())
73 }
74
75 // This is a macro and not a function because this is invoked per-pixel and there is a non-trivial
76 // performance impact if this is made into a function call.
77 macro_rules! store_rgb_pixel8 {
78 ($dst:ident, $rgb_565: ident, $index: ident, $r: ident, $g: ident, $b: ident, $r_offset: ident,
79 $g_offset: ident, $b_offset: ident, $rgb_channel_count: ident, $rgb_max_channel_f: ident) => {
80 let r8 = (0.5 + ($r * $rgb_max_channel_f)) as u8;
81 let g8 = (0.5 + ($g * $rgb_max_channel_f)) as u8;
82 let b8 = (0.5 + ($b * $rgb_max_channel_f)) as u8;
83 if $rgb_565 {
84 // References for RGB565 color conversion:
85 // * https://docs.microsoft.com/en-us/windows/win32/directshow/working-with-16-bit-rgb
86 // * https://chromium.googlesource.com/libyuv/libyuv/+/9892d70c965678381d2a70a1c9002d1cf136ee78/source/row_common.cc#2362
87 let r16 = ((r8 >> 3) as u16) << 11;
88 let g16 = ((g8 >> 2) as u16) << 5;
89 let b16 = (b8 >> 3) as u16;
90 let rgb565 = (r16 | g16 | b16).to_le_bytes();
91 $dst[($index * $rgb_channel_count) + $r_offset] = rgb565[0];
92 $dst[($index * $rgb_channel_count) + $r_offset + 1] = rgb565[1];
93 } else {
94 $dst[($index * $rgb_channel_count) + $r_offset] = r8;
95 $dst[($index * $rgb_channel_count) + $g_offset] = g8;
96 $dst[($index * $rgb_channel_count) + $b_offset] = b8;
97 }
98 };
99 }
100
yuv8_to_rgb8_color( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>101 fn yuv8_to_rgb8_color(
102 image: &image::Image,
103 rgb: &mut rgb::Image,
104 kr: f32,
105 kg: f32,
106 kb: f32,
107 ) -> AvifResult<()> {
108 let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
109 let table_uv = match &table_uv {
110 Some(table_uv) => table_uv,
111 None => &table_y,
112 };
113 let rgb_max_channel_f = rgb.max_channel_f();
114 let r_offset = rgb.format.r_offset();
115 let g_offset = rgb.format.g_offset();
116 let b_offset = rgb.format.b_offset();
117 let rgb_channel_count = rgb.channel_count() as usize;
118 let rgb_565 = rgb.format == rgb::Format::Rgb565;
119 let chroma_shift = image.yuv_format.chroma_shift_x();
120 for j in 0..image.height {
121 let uv_j = j >> image.yuv_format.chroma_shift_y();
122 let y_row = image.row(Plane::Y, j)?;
123 let u_row = image.row(Plane::U, uv_j)?;
124 // If V plane is missing, then the format is NV12. In that case, set V
125 // as U plane but starting at offset 1.
126 let v_row = image.row(Plane::V, uv_j).unwrap_or(&u_row[1..]);
127 let dst = rgb.row_mut(j)?;
128 for i in 0..image.width as usize {
129 let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
130 let y = table_y[y_row[i] as usize];
131 let cb = table_uv[u_row[uv_i] as usize];
132 let cr = table_uv[v_row[uv_i] as usize];
133 let r = y + (2.0 * (1.0 - kr)) * cr;
134 let b = y + (2.0 * (1.0 - kb)) * cb;
135 let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
136 let r = clamp_f32(r, 0.0, 1.0);
137 let g = clamp_f32(g, 0.0, 1.0);
138 let b = clamp_f32(b, 0.0, 1.0);
139 store_rgb_pixel8!(
140 dst,
141 rgb_565,
142 i,
143 r,
144 g,
145 b,
146 r_offset,
147 g_offset,
148 b_offset,
149 rgb_channel_count,
150 rgb_max_channel_f
151 );
152 }
153 }
154 Ok(())
155 }
156
yuv16_to_rgb16_color( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>157 fn yuv16_to_rgb16_color(
158 image: &image::Image,
159 rgb: &mut rgb::Image,
160 kr: f32,
161 kg: f32,
162 kb: f32,
163 ) -> AvifResult<()> {
164 let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
165 let table_uv = match &table_uv {
166 Some(table_uv) => table_uv,
167 None => &table_y,
168 };
169 let yuv_max_channel = image.max_channel();
170 let rgb_max_channel_f = rgb.max_channel_f();
171 let r_offset = rgb.format.r_offset();
172 let g_offset = rgb.format.g_offset();
173 let b_offset = rgb.format.b_offset();
174 let rgb_channel_count = rgb.channel_count() as usize;
175 let chroma_shift = image.yuv_format.chroma_shift_x();
176 for j in 0..image.height {
177 let uv_j = j >> image.yuv_format.chroma_shift_y();
178 let y_row = image.row16(Plane::Y, j)?;
179 let u_row = image.row16(Plane::U, uv_j)?;
180 // If V plane is missing, then the format is P010. In that case, set V
181 // as U plane but starting at offset 1.
182 let v_row = image.row16(Plane::V, uv_j).unwrap_or(&u_row[1..]);
183 let dst = rgb.row16_mut(j)?;
184 for i in 0..image.width as usize {
185 let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
186 let y = table_y[min(y_row[i], yuv_max_channel) as usize];
187 let cb = table_uv[min(u_row[uv_i], yuv_max_channel) as usize];
188 let cr = table_uv[min(v_row[uv_i], yuv_max_channel) as usize];
189 let r = y + (2.0 * (1.0 - kr)) * cr;
190 let b = y + (2.0 * (1.0 - kb)) * cb;
191 let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
192 let r = clamp_f32(r, 0.0, 1.0);
193 let g = clamp_f32(g, 0.0, 1.0);
194 let b = clamp_f32(b, 0.0, 1.0);
195 dst[(i * rgb_channel_count) + r_offset] = (0.5 + (r * rgb_max_channel_f)) as u16;
196 dst[(i * rgb_channel_count) + g_offset] = (0.5 + (g * rgb_max_channel_f)) as u16;
197 dst[(i * rgb_channel_count) + b_offset] = (0.5 + (b * rgb_max_channel_f)) as u16;
198 }
199 }
200 Ok(())
201 }
202
yuv16_to_rgb8_color( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>203 fn yuv16_to_rgb8_color(
204 image: &image::Image,
205 rgb: &mut rgb::Image,
206 kr: f32,
207 kg: f32,
208 kb: f32,
209 ) -> AvifResult<()> {
210 let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
211 let table_uv = match &table_uv {
212 Some(table_uv) => table_uv,
213 None => &table_y,
214 };
215 let yuv_max_channel = image.max_channel();
216 let rgb_max_channel_f = rgb.max_channel_f();
217 let r_offset = rgb.format.r_offset();
218 let g_offset = rgb.format.g_offset();
219 let b_offset = rgb.format.b_offset();
220 let rgb_channel_count = rgb.channel_count() as usize;
221 let rgb_565 = rgb.format == rgb::Format::Rgb565;
222 let chroma_shift = image.yuv_format.chroma_shift_x();
223 for j in 0..image.height {
224 let uv_j = j >> image.yuv_format.chroma_shift_y();
225 let y_row = image.row16(Plane::Y, j)?;
226 let u_row = image.row16(Plane::U, uv_j)?;
227 // If V plane is missing, then the format is P010. In that case, set V
228 // as U plane but starting at offset 1.
229 let v_row = image.row16(Plane::V, uv_j).unwrap_or(&u_row[1..]);
230 let dst = rgb.row_mut(j)?;
231 for i in 0..image.width as usize {
232 let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
233 let y = table_y[min(y_row[i], yuv_max_channel) as usize];
234 let cb = table_uv[min(u_row[uv_i], yuv_max_channel) as usize];
235 let cr = table_uv[min(v_row[uv_i], yuv_max_channel) as usize];
236 let r = y + (2.0 * (1.0 - kr)) * cr;
237 let b = y + (2.0 * (1.0 - kb)) * cb;
238 let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
239 let r = clamp_f32(r, 0.0, 1.0);
240 let g = clamp_f32(g, 0.0, 1.0);
241 let b = clamp_f32(b, 0.0, 1.0);
242 store_rgb_pixel8!(
243 dst,
244 rgb_565,
245 i,
246 r,
247 g,
248 b,
249 r_offset,
250 g_offset,
251 b_offset,
252 rgb_channel_count,
253 rgb_max_channel_f
254 );
255 }
256 }
257 Ok(())
258 }
259
yuv8_to_rgb16_color( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>260 fn yuv8_to_rgb16_color(
261 image: &image::Image,
262 rgb: &mut rgb::Image,
263 kr: f32,
264 kg: f32,
265 kb: f32,
266 ) -> AvifResult<()> {
267 let (table_y, table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
268 let table_uv = match &table_uv {
269 Some(table_uv) => table_uv,
270 None => &table_y,
271 };
272 let rgb_max_channel_f = rgb.max_channel_f();
273 let r_offset = rgb.format.r_offset();
274 let g_offset = rgb.format.g_offset();
275 let b_offset = rgb.format.b_offset();
276 let rgb_channel_count = rgb.channel_count() as usize;
277 let chroma_shift = image.yuv_format.chroma_shift_x();
278 for j in 0..image.height {
279 let uv_j = j >> image.yuv_format.chroma_shift_y();
280 let y_row = image.row(Plane::Y, j)?;
281 let u_row = image.row(Plane::U, uv_j)?;
282 // If V plane is missing, then the format is NV12. In that case, set V
283 // as U plane but starting at offset 1.
284 let v_row = image.row(Plane::V, uv_j).unwrap_or(&u_row[1..]);
285 let dst = rgb.row16_mut(j)?;
286 for i in 0..image.width as usize {
287 let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
288 let y = table_y[y_row[i] as usize];
289 let cb = table_uv[u_row[uv_i] as usize];
290 let cr = table_uv[v_row[uv_i] as usize];
291 let r = y + (2.0 * (1.0 - kr)) * cr;
292 let b = y + (2.0 * (1.0 - kb)) * cb;
293 let g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
294 let r = clamp_f32(r, 0.0, 1.0);
295 let g = clamp_f32(g, 0.0, 1.0);
296 let b = clamp_f32(b, 0.0, 1.0);
297 dst[(i * rgb_channel_count) + r_offset] = (0.5 + (r * rgb_max_channel_f)) as u16;
298 dst[(i * rgb_channel_count) + g_offset] = (0.5 + (g * rgb_max_channel_f)) as u16;
299 dst[(i * rgb_channel_count) + b_offset] = (0.5 + (b * rgb_max_channel_f)) as u16;
300 }
301 }
302 Ok(())
303 }
304
yuv8_to_rgb8_monochrome( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>305 fn yuv8_to_rgb8_monochrome(
306 image: &image::Image,
307 rgb: &mut rgb::Image,
308 kr: f32,
309 kg: f32,
310 kb: f32,
311 ) -> AvifResult<()> {
312 let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
313 let rgb_max_channel_f = rgb.max_channel_f();
314 let r_offset = rgb.format.r_offset();
315 let g_offset = rgb.format.g_offset();
316 let b_offset = rgb.format.b_offset();
317 let rgb_channel_count = rgb.channel_count() as usize;
318 let rgb_565 = rgb.format == rgb::Format::Rgb565;
319 for j in 0..image.height {
320 let y_row = image.row(Plane::Y, j)?;
321 let dst = rgb.row_mut(j)?;
322 for i in 0..image.width as usize {
323 let y = table_y[y_row[i] as usize];
324 store_rgb_pixel8!(
325 dst,
326 rgb_565,
327 i,
328 y,
329 y,
330 y,
331 r_offset,
332 g_offset,
333 b_offset,
334 rgb_channel_count,
335 rgb_max_channel_f
336 );
337 }
338 }
339 Ok(())
340 }
341
yuv16_to_rgb16_monochrome( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>342 fn yuv16_to_rgb16_monochrome(
343 image: &image::Image,
344 rgb: &mut rgb::Image,
345 kr: f32,
346 kg: f32,
347 kb: f32,
348 ) -> AvifResult<()> {
349 let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
350 let yuv_max_channel = image.max_channel();
351 let rgb_max_channel_f = rgb.max_channel_f();
352 let r_offset = rgb.format.r_offset();
353 let g_offset = rgb.format.g_offset();
354 let b_offset = rgb.format.b_offset();
355 let rgb_channel_count = rgb.channel_count() as usize;
356 for j in 0..image.height {
357 let y_row = image.row16(Plane::Y, j)?;
358 let dst = rgb.row16_mut(j)?;
359 for i in 0..image.width as usize {
360 let y = table_y[min(y_row[i], yuv_max_channel) as usize];
361 let rgb_pixel = (0.5 + (y * rgb_max_channel_f)) as u16;
362 dst[(i * rgb_channel_count) + r_offset] = rgb_pixel;
363 dst[(i * rgb_channel_count) + g_offset] = rgb_pixel;
364 dst[(i * rgb_channel_count) + b_offset] = rgb_pixel;
365 }
366 }
367 Ok(())
368 }
369
yuv16_to_rgb8_monochrome( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>370 fn yuv16_to_rgb8_monochrome(
371 image: &image::Image,
372 rgb: &mut rgb::Image,
373 kr: f32,
374 kg: f32,
375 kb: f32,
376 ) -> AvifResult<()> {
377 let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
378 let yuv_max_channel = image.max_channel();
379 let rgb_max_channel_f = rgb.max_channel_f();
380 let r_offset = rgb.format.r_offset();
381 let g_offset = rgb.format.g_offset();
382 let b_offset = rgb.format.b_offset();
383 let rgb_channel_count = rgb.channel_count() as usize;
384 let rgb_565 = rgb.format == rgb::Format::Rgb565;
385 for j in 0..image.height {
386 let y_row = image.row16(Plane::Y, j)?;
387 let dst = rgb.row_mut(j)?;
388 for i in 0..image.width as usize {
389 let y = table_y[min(y_row[i], yuv_max_channel) as usize];
390 store_rgb_pixel8!(
391 dst,
392 rgb_565,
393 i,
394 y,
395 y,
396 y,
397 r_offset,
398 g_offset,
399 b_offset,
400 rgb_channel_count,
401 rgb_max_channel_f
402 );
403 }
404 }
405 Ok(())
406 }
407
yuv8_to_rgb16_monochrome( image: &image::Image, rgb: &mut rgb::Image, kr: f32, kg: f32, kb: f32, ) -> AvifResult<()>408 fn yuv8_to_rgb16_monochrome(
409 image: &image::Image,
410 rgb: &mut rgb::Image,
411 kr: f32,
412 kg: f32,
413 kb: f32,
414 ) -> AvifResult<()> {
415 let (table_y, _table_uv) = unorm_lookup_tables(image, Mode::YuvCoefficients(kr, kg, kb))?;
416 let rgb_max_channel_f = rgb.max_channel_f();
417 let r_offset = rgb.format.r_offset();
418 let g_offset = rgb.format.g_offset();
419 let b_offset = rgb.format.b_offset();
420 let rgb_channel_count = rgb.channel_count() as usize;
421 for j in 0..image.height {
422 let y_row = image.row(Plane::Y, j)?;
423 let dst = rgb.row16_mut(j)?;
424 for i in 0..image.width as usize {
425 let y = table_y[y_row[i] as usize];
426 let rgb_pixel = (0.5 + (y * rgb_max_channel_f)) as u16;
427 dst[(i * rgb_channel_count) + r_offset] = rgb_pixel;
428 dst[(i * rgb_channel_count) + g_offset] = rgb_pixel;
429 dst[(i * rgb_channel_count) + b_offset] = rgb_pixel;
430 }
431 }
432 Ok(())
433 }
434
yuv_to_rgb_fast(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()>435 pub(crate) fn yuv_to_rgb_fast(image: &image::Image, rgb: &mut rgb::Image) -> AvifResult<()> {
436 let mode: Mode = image.into();
437 match mode {
438 Mode::Identity => {
439 if image.depth == 8 && rgb.depth == 8 && image.yuv_range == YuvRange::Full {
440 identity_yuv8_to_rgb8_full_range(image, rgb)
441 } else {
442 // TODO: Add more fast paths for identity.
443 Err(AvifError::NotImplemented)
444 }
445 }
446 Mode::YuvCoefficients(kr, kg, kb) => {
447 let has_color = image.yuv_format != PixelFormat::Yuv400;
448 match (image.depth == 8, rgb.depth == 8, has_color) {
449 (true, true, true) => yuv8_to_rgb8_color(image, rgb, kr, kg, kb),
450 (false, false, true) => yuv16_to_rgb16_color(image, rgb, kr, kg, kb),
451 (false, true, true) => yuv16_to_rgb8_color(image, rgb, kr, kg, kb),
452 (true, false, true) => yuv8_to_rgb16_color(image, rgb, kr, kg, kb),
453 (true, true, false) => yuv8_to_rgb8_monochrome(image, rgb, kr, kg, kb),
454 (false, false, false) => yuv16_to_rgb16_monochrome(image, rgb, kr, kg, kb),
455 (false, true, false) => yuv16_to_rgb8_monochrome(image, rgb, kr, kg, kb),
456 (true, false, false) => yuv8_to_rgb16_monochrome(image, rgb, kr, kg, kb),
457 }
458 }
459 Mode::Ycgco | Mode::YcgcoRe | Mode::YcgcoRo => Err(AvifError::NotImplemented),
460 }
461 }
462
unorm_lookup_tables( image: &image::Image, mode: Mode, ) -> AvifResult<(Vec<f32>, Option<Vec<f32>>)>463 fn unorm_lookup_tables(
464 image: &image::Image,
465 mode: Mode,
466 ) -> AvifResult<(Vec<f32>, Option<Vec<f32>>)> {
467 let count = 1usize << image.depth;
468 let mut table_y: Vec<f32> = create_vec_exact(count)?;
469 let bias_y;
470 let range_y;
471 // Formula specified in ISO/IEC 23091-2.
472 if image.yuv_range == YuvRange::Limited {
473 bias_y = (16 << (image.depth - 8)) as f32;
474 range_y = (219 << (image.depth - 8)) as f32;
475 } else {
476 bias_y = 0.0;
477 range_y = image.max_channel_f();
478 }
479 for cp in 0..count {
480 table_y.push(((cp as f32) - bias_y) / range_y);
481 }
482 if mode == Mode::Identity {
483 Ok((table_y, None))
484 } else {
485 // Formula specified in ISO/IEC 23091-2.
486 let bias_uv = (1 << (image.depth - 1)) as f32;
487 let range_uv = if image.yuv_range == YuvRange::Limited {
488 (224 << (image.depth - 8)) as f32
489 } else {
490 image.max_channel_f()
491 };
492 let mut table_uv: Vec<f32> = create_vec_exact(count)?;
493 for cp in 0..count {
494 table_uv.push(((cp as f32) - bias_uv) / range_uv);
495 }
496 Ok((table_y, Some(table_uv)))
497 }
498 }
499
500 #[allow(clippy::too_many_arguments)]
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)501 fn compute_rgb(
502 y: f32,
503 cb: f32,
504 cr: f32,
505 has_color: bool,
506 mode: Mode,
507 clamped_y: u16,
508 yuv_max_channel: u16,
509 rgb_max_channel: u16,
510 rgb_max_channel_f: f32,
511 ) -> (f32, f32, f32) {
512 let r: f32;
513 let g: f32;
514 let b: f32;
515 if has_color {
516 match mode {
517 Mode::Identity => {
518 g = y;
519 b = cb;
520 r = cr;
521 }
522 Mode::Ycgco => {
523 let t = y - cb;
524 g = y + cb;
525 b = t - cr;
526 r = t + cr;
527 }
528 Mode::YcgcoRe | Mode::YcgcoRo => {
529 // Equations (62) through (65) in https://www.itu.int/rec/T-REC-H.273
530 let cg = (0.5 + cb * yuv_max_channel as f32).floor() as i32;
531 let co = (0.5 + cr * yuv_max_channel as f32).floor() as i32;
532 let t = clamped_y as i32 - (cg >> 1);
533 let rgb_max_channel = rgb_max_channel as i32;
534 g = clamp_i32(t + cg, 0, rgb_max_channel) as f32 / rgb_max_channel_f;
535 let tmp_b = clamp_i32(t - (co >> 1), 0, rgb_max_channel) as f32;
536 b = tmp_b / rgb_max_channel_f;
537 r = clamp_i32(tmp_b as i32 + co, 0, rgb_max_channel) as f32 / rgb_max_channel_f;
538 }
539 Mode::YuvCoefficients(kr, kg, kb) => {
540 r = y + (2.0 * (1.0 - kr)) * cr;
541 b = y + (2.0 * (1.0 - kb)) * cb;
542 g = y - ((2.0 * ((kr * (1.0 - kr) * cr) + (kb * (1.0 - kb) * cb))) / kg);
543 }
544 }
545 } else {
546 r = y;
547 g = y;
548 b = y;
549 }
550 (
551 clamp_f32(r, 0.0, 1.0),
552 clamp_f32(g, 0.0, 1.0),
553 clamp_f32(b, 0.0, 1.0),
554 )
555 }
556
clamped_pixel(row: PlaneRow, index: usize, max_channel: u16) -> u16557 fn clamped_pixel(row: PlaneRow, index: usize, max_channel: u16) -> u16 {
558 match row {
559 PlaneRow::Depth8(row) => row[index] as u16,
560 PlaneRow::Depth16(row) => min(max_channel, row[index]),
561 }
562 }
563
unorm_value(row: PlaneRow, index: usize, max_channel: u16, table: &[f32]) -> f32564 fn unorm_value(row: PlaneRow, index: usize, max_channel: u16, table: &[f32]) -> f32 {
565 table[clamped_pixel(row, index, max_channel) as usize]
566 }
567
yuv_to_rgb_any( image: &image::Image, rgb: &mut rgb::Image, alpha_multiply_mode: AlphaMultiplyMode, ) -> AvifResult<()>568 pub(crate) fn yuv_to_rgb_any(
569 image: &image::Image,
570 rgb: &mut rgb::Image,
571 alpha_multiply_mode: AlphaMultiplyMode,
572 ) -> AvifResult<()> {
573 let mode: Mode = image.into();
574 let (table_y, table_uv) = unorm_lookup_tables(image, mode)?;
575 let table_uv = match &table_uv {
576 Some(table_uv) => table_uv,
577 None => &table_y,
578 };
579 let r_offset = rgb.format.r_offset();
580 let g_offset = rgb.format.g_offset();
581 let b_offset = rgb.format.b_offset();
582 let rgb_channel_count = rgb.channel_count() as usize;
583 let rgb_depth = rgb.depth;
584 let chroma_upsampling = rgb.chroma_upsampling;
585 let has_color = image.has_plane(Plane::U)
586 && image.has_plane(Plane::V)
587 && image.yuv_format != PixelFormat::Yuv400;
588 let yuv_max_channel = image.max_channel();
589 let rgb_max_channel = rgb.max_channel();
590 let rgb_max_channel_f = rgb.max_channel_f();
591 let chroma_shift = image.yuv_format.chroma_shift_x();
592 for j in 0..image.height {
593 let uv_j = j >> image.yuv_format.chroma_shift_y();
594 let y_row = image.row_generic(Plane::Y, j)?;
595 let u_row = image.row_generic(Plane::U, uv_j).ok();
596 let v_row = image.row_generic(Plane::V, uv_j).ok();
597 let a_row = image.row_generic(Plane::A, j).ok();
598 for i in 0..image.width as usize {
599 let clamped_y = clamped_pixel(y_row, i, yuv_max_channel);
600 let y = table_y[clamped_y as usize];
601 let mut cb = 0.5;
602 let mut cr = 0.5;
603 if has_color {
604 let u_row = u_row.unwrap();
605 let v_row = v_row.unwrap();
606 let uv_i = (i >> chroma_shift.0) << chroma_shift.1;
607 if image.yuv_format == PixelFormat::Yuv444
608 || matches!(
609 chroma_upsampling,
610 ChromaUpsampling::Fastest | ChromaUpsampling::Nearest
611 )
612 {
613 cb = unorm_value(u_row, uv_i, yuv_max_channel, table_uv);
614 cr = unorm_value(v_row, uv_i, yuv_max_channel, table_uv);
615 } else {
616 if image.chroma_sample_position != ChromaSamplePosition::CENTER {
617 return Err(AvifError::NotImplemented);
618 }
619
620 // Bilinear filtering with weights. See
621 // https://github.com/AOMediaCodec/libavif/blob/0580334466d57fedb889d5ed7ae9574d6f66e00c/src/reformat.c#L657-L685.
622 let image_width_minus_1 = (image.width - 1) as usize;
623 let uv_adj_i = if i == 0 || (i == image_width_minus_1 && (i % 2) != 0) {
624 uv_i
625 } else if (i % 2) != 0 {
626 uv_i + 1
627 } else {
628 uv_i - 1
629 };
630 let uv_adj_j = if j == 0
631 || (j == image.height - 1 && (j % 2) != 0)
632 || image.yuv_format == PixelFormat::Yuv422
633 {
634 uv_j
635 } else if (j % 2) != 0 {
636 uv_j + 1
637 } else {
638 uv_j - 1
639 };
640 let u_adj_row = image.row_generic(Plane::U, uv_adj_j)?;
641 let v_adj_row = image.row_generic(Plane::V, uv_adj_j)?;
642 let mut unorm_u: [[f32; 2]; 2] = [[0.0; 2]; 2];
643 let mut unorm_v: [[f32; 2]; 2] = [[0.0; 2]; 2];
644 unorm_u[0][0] = unorm_value(u_row, uv_i, yuv_max_channel, table_uv);
645 unorm_v[0][0] = unorm_value(v_row, uv_i, yuv_max_channel, table_uv);
646 unorm_u[1][0] = unorm_value(u_row, uv_adj_i, yuv_max_channel, table_uv);
647 unorm_v[1][0] = unorm_value(v_row, uv_adj_i, yuv_max_channel, table_uv);
648 unorm_u[0][1] = unorm_value(u_adj_row, uv_i, yuv_max_channel, table_uv);
649 unorm_v[0][1] = unorm_value(v_adj_row, uv_i, yuv_max_channel, table_uv);
650 unorm_u[1][1] = unorm_value(u_adj_row, uv_adj_i, yuv_max_channel, table_uv);
651 unorm_v[1][1] = unorm_value(v_adj_row, uv_adj_i, yuv_max_channel, table_uv);
652 cb = (unorm_u[0][0] * (9.0 / 16.0))
653 + (unorm_u[1][0] * (3.0 / 16.0))
654 + (unorm_u[0][1] * (3.0 / 16.0))
655 + (unorm_u[1][1] * (1.0 / 16.0));
656 cr = (unorm_v[0][0] * (9.0 / 16.0))
657 + (unorm_v[1][0] * (3.0 / 16.0))
658 + (unorm_v[0][1] * (3.0 / 16.0))
659 + (unorm_v[1][1] * (1.0 / 16.0));
660 }
661 }
662 let (mut rc, mut gc, mut bc) = compute_rgb(
663 y,
664 cb,
665 cr,
666 has_color,
667 mode,
668 clamped_y,
669 yuv_max_channel,
670 rgb_max_channel,
671 rgb_max_channel_f,
672 );
673 if alpha_multiply_mode != AlphaMultiplyMode::NoOp {
674 let unorm_a = clamped_pixel(a_row.unwrap(), i, yuv_max_channel);
675 let ac = clamp_f32((unorm_a as f32) / (yuv_max_channel as f32), 0.0, 1.0);
676 if ac == 0.0 {
677 rc = 0.0;
678 gc = 0.0;
679 bc = 0.0;
680 } else if ac < 1.0 {
681 match alpha_multiply_mode {
682 AlphaMultiplyMode::Multiply => {
683 rc *= ac;
684 gc *= ac;
685 bc *= ac;
686 }
687 AlphaMultiplyMode::UnMultiply => {
688 rc = f32::min(rc / ac, 1.0);
689 gc = f32::min(gc / ac, 1.0);
690 bc = f32::min(bc / ac, 1.0);
691 }
692 _ => {} // Not reached.
693 }
694 }
695 }
696 if rgb_depth == 8 {
697 let dst = rgb.row_mut(j)?;
698 dst[(i * rgb_channel_count) + r_offset] = (0.5 + (rc * rgb_max_channel_f)) as u8;
699 dst[(i * rgb_channel_count) + g_offset] = (0.5 + (gc * rgb_max_channel_f)) as u8;
700 dst[(i * rgb_channel_count) + b_offset] = (0.5 + (bc * rgb_max_channel_f)) as u8;
701 } else {
702 let dst16 = rgb.row16_mut(j)?;
703 dst16[(i * rgb_channel_count) + r_offset] = (0.5 + (rc * rgb_max_channel_f)) as u16;
704 dst16[(i * rgb_channel_count) + g_offset] = (0.5 + (gc * rgb_max_channel_f)) as u16;
705 dst16[(i * rgb_channel_count) + b_offset] = (0.5 + (bc * rgb_max_channel_f)) as u16;
706 }
707 }
708 }
709 Ok(())
710 }
711
712 #[cfg(test)]
713 mod tests {
714 use super::*;
715
716 #[test]
yuv_to_rgb()717 fn yuv_to_rgb() {
718 fn create_420(
719 matrix_coefficients: MatrixCoefficients,
720 y: &[&[u8]],
721 u: &[&[u8]],
722 v: &[&[u8]],
723 ) -> image::Image {
724 let mut yuv = image::Image {
725 width: y[0].len() as u32,
726 height: y.len() as u32,
727 depth: 8,
728 yuv_format: PixelFormat::Yuv420,
729 matrix_coefficients,
730 yuv_range: YuvRange::Limited,
731 ..Default::default()
732 };
733 assert!(yuv.allocate_planes(Category::Color).is_ok());
734 for plane in image::YUV_PLANES {
735 let samples = if plane == Plane::Y {
736 &y
737 } else if plane == Plane::U {
738 &u
739 } else {
740 &v
741 };
742 assert_eq!(yuv.height(plane), samples.len());
743 for y in 0..yuv.height(plane) {
744 assert_eq!(yuv.width(plane), samples[y].len());
745 for x in 0..yuv.width(plane) {
746 yuv.row_mut(plane, y as u32).unwrap()[x] = samples[y][x];
747 }
748 }
749 }
750 yuv
751 }
752 fn assert_near(yuv: &image::Image, r: &[&[u8]], g: &[&[u8]], b: &[&[u8]]) {
753 let mut dst = rgb::Image::create_from_yuv(yuv);
754 dst.format = rgb::Format::Rgb;
755 dst.chroma_upsampling = ChromaUpsampling::Bilinear;
756 assert!(dst.allocate().is_ok());
757 assert!(yuv_to_rgb_any(yuv, &mut dst, AlphaMultiplyMode::NoOp).is_ok());
758 assert_eq!(dst.height, r.len() as u32);
759 assert_eq!(dst.height, g.len() as u32);
760 assert_eq!(dst.height, b.len() as u32);
761 for y in 0..dst.height {
762 assert_eq!(dst.width, r[y as usize].len() as u32);
763 assert_eq!(dst.width, g[y as usize].len() as u32);
764 assert_eq!(dst.width, b[y as usize].len() as u32);
765 for x in 0..dst.width {
766 let i = (x * dst.pixel_size() + 0) as usize;
767 let pixel = &dst.row(y).unwrap()[i..i + 3];
768 assert_eq!(pixel[0], r[y as usize][x as usize]);
769 assert_eq!(pixel[1], g[y as usize][x as usize]);
770 assert_eq!(pixel[2], b[y as usize][x as usize]);
771 }
772 }
773 }
774
775 // Testing identity 4:2:0 -> RGB would be simpler to check upsampling
776 // but this is not allowed (not a real use case).
777 assert_near(
778 &create_420(
779 MatrixCoefficients::Bt601,
780 /*y=*/
781 &[
782 &[0, 100, 200], //
783 &[10, 110, 210], //
784 &[50, 150, 250],
785 ],
786 /*u=*/
787 &[
788 &[0, 100], //
789 &[10, 110],
790 ],
791 /*v=*/
792 &[
793 &[57, 57], //
794 &[57, 57],
795 ],
796 ),
797 /*r=*/
798 &[
799 &[0, 0, 101], //
800 &[0, 0, 113], //
801 &[0, 43, 159],
802 ],
803 /*g=*/
804 &[
805 &[89, 196, 255], //
806 &[100, 207, 255], //
807 &[145, 251, 255],
808 ],
809 /*b=*/
810 &[
811 &[0, 0, 107], //
812 &[0, 0, 124], //
813 &[0, 0, 181],
814 ],
815 );
816
817 // Extreme values.
818 assert_near(
819 &create_420(
820 MatrixCoefficients::Bt601,
821 /*y=*/ &[&[0]],
822 /*u=*/ &[&[0]],
823 /*v=*/ &[&[0]],
824 ),
825 /*r=*/ &[&[0]],
826 /*g=*/ &[&[136]],
827 /*b=*/ &[&[0]],
828 );
829 assert_near(
830 &create_420(
831 MatrixCoefficients::Bt601,
832 /*y=*/ &[&[255]],
833 /*u=*/ &[&[255]],
834 /*v=*/ &[&[255]],
835 ),
836 /*r=*/ &[&[255]],
837 /*g=*/ &[&[125]],
838 /*b=*/ &[&[255]],
839 );
840
841 // Top-right square "bleeds" into other samples during upsampling.
842 assert_near(
843 &create_420(
844 MatrixCoefficients::Bt601,
845 /*y=*/
846 &[
847 &[0, 0, 255, 255],
848 &[0, 0, 255, 255],
849 &[0, 0, 0, 0],
850 &[0, 0, 0, 0],
851 ],
852 /*u=*/
853 &[
854 &[0, 255], //
855 &[0, 0],
856 ],
857 /*v=*/
858 &[
859 &[0, 255], //
860 &[0, 0],
861 ],
862 ),
863 /*r=*/
864 &[
865 &[0, 0, 255, 255],
866 &[0, 0, 255, 255],
867 &[0, 0, 0, 0],
868 &[0, 0, 0, 0],
869 ],
870 /*g=*/
871 &[
872 &[136, 59, 202, 125],
873 &[136, 78, 255, 202],
874 &[136, 116, 78, 59],
875 &[136, 136, 136, 136],
876 ],
877 /*b=*/
878 &[
879 &[0, 0, 255, 255],
880 &[0, 0, 255, 255],
881 &[0, 0, 0, 0],
882 &[0, 0, 0, 0],
883 ],
884 );
885
886 // Middle square does not "bleed" into other samples during upsampling.
887 assert_near(
888 &create_420(
889 MatrixCoefficients::Bt601,
890 /*y=*/
891 &[
892 &[0, 0, 0, 0],
893 &[0, 255, 255, 0],
894 &[0, 255, 255, 0],
895 &[0, 0, 0, 0],
896 ],
897 /*u=*/
898 &[
899 &[0, 0], //
900 &[0, 0],
901 ],
902 /*v=*/
903 &[
904 &[0, 0], //
905 &[0, 0],
906 ],
907 ),
908 /*r=*/
909 &[
910 &[0, 0, 0, 0],
911 &[0, 74, 74, 0],
912 &[0, 74, 74, 0],
913 &[0, 0, 0, 0],
914 ],
915 /*g=*/
916 &[
917 &[136, 136, 136, 136],
918 &[136, 255, 255, 136],
919 &[136, 255, 255, 136],
920 &[136, 136, 136, 136],
921 ],
922 /*b=*/
923 &[
924 &[0, 0, 0, 0],
925 &[0, 20, 20, 0],
926 &[0, 20, 20, 0],
927 &[0, 0, 0, 0],
928 ],
929 );
930 }
931 }
932