diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs index 0d7df3cd84..6ce1a63da1 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs @@ -9,6 +9,13 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; internal class Av1YuvConverter { + // BT.709 SPecificatiuon constants. + private const int UMax = (int)(0.436 * 255); + private const int VMax = (int)(0.615 * 255); + private const int Wr = (int)(0.2126 * 255); + private const int Wb = (int)(0.0722 * 255); + private const int Wg = 255 - Wr - Wb; + public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image) where TPixel : unmanaged, IPixel { @@ -57,10 +64,10 @@ internal class Av1YuvConverter { // Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer. // Using BT.709 specification - const int rvWeight = (int)(1.28033 * 256); - const int guWeight = (int)(-0.21482 * 256); - const int gvWeight = (int)(-0.38059 * 256); - const int buWeight = (int)(2.12798 * 256); + const int rvWeight = (int)(1.28033 * 255); + const int guWeight = (int)(-0.21482 * 255); + const int gvWeight = (int)(-0.38059 * 255); + const int buWeight = (int)(2.12798 * 255); Guard.NotNull(buffer.BufferY); Guard.NotNull(buffer.BufferCb); Guard.NotNull(buffer.BufferCr); @@ -98,9 +105,11 @@ internal class Av1YuvConverter ref byte vRef = ref vSpan[0]; for (int x = 0; x < image.Width; x++) { - pixel.R = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (rvWeight * vRef)) >> 8); - pixel.G = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (guWeight * uRef) + (gvWeight * vRef)) >> 8); - pixel.B = (byte)Av1Math.Clip3(0, 255, ((yRef << 8) + (buWeight * uRef)) >> 8); + int u = uRef; // ((uRef - 127) * 2 * UMax) / 255; + int v = vRef; // ((vRef - 127) * 2 * VMax) / 255; + pixel.R = (byte)Av1Math.Clip3(0, 255, yRef + (v * (255 - Wr) / VMax)); + pixel.G = (byte)Av1Math.Clip3(0, 255, yRef - ((u * Wb * (255 - Wb)) / (UMax * Wg)) - ((v * Wr * (255 - Wr)) / (VMax * Wg))); + pixel.B = (byte)Av1Math.Clip3(0, 255, yRef + ((u * (255 - Wb)) / UMax)); pixel = ref Unsafe.Add(ref pixel, 1); yRef = ref Unsafe.Add(ref yRef, 1); uRef = ref Unsafe.Add(ref uRef, 1); @@ -113,15 +122,15 @@ internal class Av1YuvConverter private static void ConvertRgbToYuv444(ImageFrame image, Av1FrameBuffer buffer) { // Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer. - const int yrWeight = (int)(0.2126 * 256); - const int ygWeight = (int)(0.7152 * 256); - const int ybWeight = (int)(0.0722 * 256); - const int urWeight = (int)(-0.09991 * 256); - const int ugWeight = (int)(-0.33609 * 256); - const int ubWeight = (int)(0.436 * 256); - const int vrWeight = (int)(0.615 * 256); - const int vgWeight = (int)(-0.55861 * 256); - const int vbWeight = (int)(-0.05639 * 256); + const int yrWeight = (int)(0.2126 * 255); + const int ygWeight = (int)(0.7152 * 255); + const int ybWeight = (int)(0.0722 * 255); + const int urWeight = (int)(-0.09991 * 255); + const int ugWeight = (int)(-0.33609 * 255); + const int ubWeight = (int)(0.436 * 255); + const int vrWeight = (int)(0.615 * 255); + const int vgWeight = (int)(-0.55861 * 255); + const int vbWeight = (int)(-0.05639 * 255); Guard.NotNull(buffer.BufferY); Guard.NotNull(buffer.BufferCb); Guard.NotNull(buffer.BufferCr); @@ -149,9 +158,15 @@ internal class Av1YuvConverter ref byte vRef = ref vSpan[0]; for (int x = 0; x < image.Width; x++) { - yRef = (byte)Av1Math.Clip3(0, 255, ((yrWeight * pixel.R) + (ygWeight * pixel.G) + (ybWeight * pixel.B)) >> 8); - uRef = (byte)Av1Math.Clip3(0, 255, ((urWeight * pixel.R) + (ugWeight * pixel.G) + (ubWeight * pixel.B)) >> 8); - vRef = (byte)Av1Math.Clip3(0, 255, ((vrWeight * pixel.R) + (vgWeight * pixel.G) + (vbWeight * pixel.B)) >> 8); + yRef = (byte)Av1Math.Clip3(0, 255, ((Wr * pixel.R) + (Wg * pixel.G) + (Wb * pixel.B)) / 255); + + // Not normalized, where range is [-UMax, UMax] or [-VMax, VMax] + // uRef = (byte)((UMax * (pixel.B - y)) / (255 - Wb)); + // vRef = (byte)((VMax * (pixel.R - y)) / (255 - Wr)); + + // Normalized calculations + uRef = (byte)Av1Math.Clip3(0, 255, ((UMax * (pixel.B - yRef) / (255 - Wb)) + UMax) * 255 / (2 * UMax)); + vRef = (byte)Av1Math.Clip3(0, 255, ((VMax * (pixel.R - yRef) / (255 - Wr)) + VMax) * 255 / (2 * VMax)); pixel = ref Unsafe.Add(ref pixel, 1); yRef = ref Unsafe.Add(ref yRef, 1); uRef = ref Unsafe.Add(ref uRef, 1); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs index ee728b3453..a9e1325bb5 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs @@ -42,16 +42,17 @@ internal class Av1ReferenceYuvConverter private const double Wg = 1 - Wr - Wb; - public static Span RgbToYuv(Span row) + public static Span RgbToYuv(Span row, bool normalized) { Rgb24[] result = new Rgb24[row.Length]; for (int i = 0; i < row.Length; i++) { - double[] current = RgbToYuv(row[i], false, true, false); - byte y = (byte)current[0]; - byte u = (byte)current[1]; - byte v = (byte)current[2]; - result[i] = new Rgb24(y, u, v); + double[] current = RgbToYuv(row[i], normalized, true, false); + double y = Math.Max(0, Math.Min(255, Math.Round(current[0]))); + double u = Math.Max(0, Math.Min(255, Math.Round(current[1]))); + double v = Math.Max(0, Math.Min(255, Math.Round(current[2]))); + + result[i] = new Rgb24((byte)y, (byte)u, (byte)v); } return result; @@ -96,7 +97,7 @@ internal class Av1ReferenceYuvConverter return [y, u, v]; } - public static Span YuvToRgb(Av1FrameBuffer frameBuffer) + public static Span YuvToRgb(Av1FrameBuffer frameBuffer, bool normalized) { Span yRow = frameBuffer.BufferY!.DangerousGetSingleSpan(); Span uRow = frameBuffer.BufferCb!.DangerousGetSingleSpan(); @@ -108,13 +109,25 @@ internal class Av1ReferenceYuvConverter yuv[0] = yRow[i]; yuv[1] = uRow[i]; yuv[2] = vRow[i]; - result[i] = YuvToRgb(yuv, false, true, false); + double[] rgb = YuvToRgb(yuv, normalized, true, false); + double r = rgb[0] * 255; + double g = rgb[1] * 255; + double b = rgb[2] * 255; + byte redByte = (byte)Math.Max(0, Math.Min(255, Math.Round(r))); + byte greenByte = (byte)Math.Max(0, Math.Min(255, Math.Round(g))); + byte blueByte = (byte)Math.Max(0, Math.Min(255, Math.Round(b))); + + // Assert.True(Math.Abs(redByte - r) < 3, $"Red pixel out of byte range: {redByte} iso {r} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); + // Assert.True(Math.Abs(greenByte - g) < 3, $"Green pixel out of byte range: {greenByte} iso {g} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); + // Assert.True(Math.Abs(blueByte - b) < 3, $"Blue pixel out of byte range: {blueByte} iso {b} from input Y={yuv[0]}, U={yuv[1]} and V={yuv[2]}."); + + result[i] = new Rgb24(redByte, greenByte, blueByte); } return result; } - public static Rgb24 YuvToRgb(double[] yuv, bool normalized = false, bool is_8bit = false, bool is_10bit = false) + public static double[] YuvToRgb(double[] yuv, bool normalized = false, bool is_8bit = false, bool is_10bit = false) { double y = yuv[0]; double u = yuv[1]; @@ -151,6 +164,6 @@ internal class Av1ReferenceYuvConverter double g = y - (u * Wb * (1 - Wb) / (Umax * Wg)) - (v * Wr * (1 - Wr) / (Vmax * Wg)); double b = y + (u * (1 - Wb) / Umax); - return new Rgb24((byte)Math.Round(r * 255), (byte)Math.Round(g * 255), (byte)Math.Round(b * 255)); + return [r, g, b]; } } diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs index 3b04f84eff..6355575454 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -89,7 +89,7 @@ public class Av1YuvConverterTests // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); - Span referenceOutput = Av1ReferenceYuvConverter.RgbToYuv(memory.Span); + Span referenceOutput = Av1ReferenceYuvConverter.RgbToYuv(memory.Span, true); // Assert Span actual = new Rgb24[frameBuffer.Width]; @@ -105,7 +105,7 @@ public class Av1YuvConverterTests actual[i] = pixel; } - Compare(referenceOutput, actual, 1); + Compare(referenceOutput, actual, 3); } [Fact] @@ -127,12 +127,12 @@ public class Av1YuvConverterTests // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); - Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer); + Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, false); // Assert frame.DangerousTryGetSinglePixelMemory(out Memory memory); Span actual = memory.Span; - Compare(referenceOutput, actual, 1); + Compare(referenceOutput, actual, 3); } private static void Compare(Span referenceOutput, Span actual, int allowedDifference)