diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 651330e34e..e19bda1968 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -52,6 +52,14 @@ The initial post-merge snapshot was established on 2026-08-24: This snapshot pins or classifies the available references and failures; it does not complete Phase 0. The full WIP provenance map, disabled-test inventory, HEVC reference selection, and feature-state matrix remain required. +### Provenance map in progress + +| Managed implementation | Normative behavior | Reviewed implementation reference | Use | +| --- | --- | --- | --- | +| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, and scalar row conversion | H.273 formulas 20-31 and the identity, YCgCo, and non-constant-luminance matrix formulas | libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Scalar behavioral oracle for 8-bit YUV 4:4:4 full/limited-range conversion; later subsampling, high-bit-depth, and SIMD paths must match it. | + +This table is intentionally incomplete. Add a row before each additional AV1 or HEVC algorithm is ported or materially reshaped. + ## Current implementation assessment This assessment is based on the current source after the upstream ImageSharp merge and the baseline recorded above. Unless a result is stated explicitly, each item is a source-inspection finding rather than a verified interoperability claim. diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs index 79e6fc8515..6bef7e8999 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs @@ -25,6 +25,7 @@ internal class Av1FrameBuffer : IDisposable this.MaxWidth = sequenceHeader.MaxFrameWidth; this.MaxHeight = sequenceHeader.MaxFrameHeight; this.BitDepth = sequenceHeader.ColorConfig.BitDepth; + this.ColorConfig = sequenceHeader.ColorConfig; int bitsPerPixel = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; this.ColorFormat = colorFormat; this.BufferEnableMask = sequenceHeader.ColorConfig.IsMonochrome ? PictureBufferLumaMask : PictureBufferFullMask; @@ -147,6 +148,11 @@ internal class Av1FrameBuffer : IDisposable /// public Av1BitDepth BitDepth { get; set; } + /// + /// Gets the color configuration signaled by the AV1 sequence header. + /// + public ObuColorConfig ColorConfig { get; } + /// /// Gets or sets the chroma subsampling. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs index 89b1089fca..e004243a99 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs @@ -1,169 +1,348 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heif.Av1; -internal class Av1YuvConverter +internal static 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; + private const float ByteMaximum = byte.MaxValue; + private const float ChromaBias = 128F; + private const float FullRangeLumaScale = byte.MaxValue; + private const float FullRangeChromaScale = byte.MaxValue; + private const float LimitedRangeLumaBias = 16F; + private const float LimitedRangeLumaScale = 219F; + private const float LimitedRangeChromaScale = 224F; + private enum ConversionMode + { + Coefficients, + Identity, + YCgCo, + } + + /// + /// Converts the reconstructed 8-bit YUV 4:4:4 planes to packed pixels. + /// + /// The destination pixel type. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed AV1 frame. + /// The destination image frame. public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image) where TPixel : unmanaged, IPixel { - using Image rgbImage = new(image.Width, image.Height); - ImageFrame rgbFrame = rgbImage.Frames.RootFrame; + GetConversionParameters( + frameBuffer, + out ConversionMode mode, + out float kr, + out float kg, + out float kb, + out float lumaBias, + out float lumaScale, + out float chromaScale); - // TODO: Support YUV420 and YUV420 also. - if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv444) - { - throw new NotSupportedException("Only able to convert YUV444 to RGB."); - } + Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0); + Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0); + using IMemoryOwner rowOwner = configuration.MemoryAllocator.Allocate(image.Width); + Span rgbRow = rowOwner.GetSpan()[..image.Width]; - ConvertYuvToRgb(frameBuffer, rgbFrame, false); - image.ProcessPixelRows(rgbFrame, (resultAcc, rgbAcc) => + for (int y = 0; y < image.Height; y++) { - for (int y = 0; y < rgbImage.Height; y++) - { - Span rgbRow = rgbAcc.GetRowSpan(y); - Span resultRow = resultAcc.GetRowSpan(y); - PixelOperations.Instance.FromRgb24(configuration, rgbRow, resultRow); - } - }); + ConvertYuv444ToRgbRow( + yPlane.DangerousGetRowSpan(y), + uPlane.DangerousGetRowSpan(y), + vPlane.DangerousGetRowSpan(y), + rgbRow, + mode, + kr, + kg, + kb, + lumaBias, + lumaScale, + chromaScale); + + PixelOperations.Instance.FromRgb24( + configuration, + rgbRow, + image.PixelBuffer.DangerousGetRowSpan(y)); + } } + /// + /// Converts packed pixels to the 8-bit YUV 4:4:4 planes used by the AV1 encoder. + /// + /// The source pixel type. + /// The configuration used for allocation and pixel conversion. + /// The source image frame. + /// The destination AV1 frame. public static void ConvertFromRgb(Configuration configuration, ImageFrame image, Av1FrameBuffer frameBuffer) where TPixel : unmanaged, IPixel { - using Image rgbImage = new(image.Width, image.Height); - ImageFrame rgbFrame = rgbImage.Frames.RootFrame; + GetConversionParameters( + frameBuffer, + out ConversionMode mode, + out float kr, + out float kg, + out float kb, + out float lumaBias, + out float lumaScale, + out float chromaScale); - image.ProcessPixelRows(rgbFrame, (sourceAcc, rgbAcc) => + Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0); + Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0); + using IMemoryOwner rowOwner = configuration.MemoryAllocator.Allocate(image.Width); + Span rgbRow = rowOwner.GetSpan()[..image.Width]; + + for (int y = 0; y < image.Height; y++) { - for (int y = 0; y < rgbImage.Height; y++) - { - Span rgbRow = rgbAcc.GetRowSpan(y); - Span sourceRow = sourceAcc.GetRowSpan(y); - PixelOperations.Instance.ToRgb24(configuration, sourceRow, rgbRow); - } - }); + PixelOperations.Instance.ToRgb24( + configuration, + image.PixelBuffer.DangerousGetRowSpan(y), + rgbRow); - // TODO: Support YUV422 and YUV420 also. - ConvertRgbToYuv444(rgbFrame, frameBuffer); + ConvertRgbToYuv444Row( + rgbRow, + yPlane.DangerousGetRowSpan(y), + uPlane.DangerousGetRowSpan(y), + vPlane.DangerousGetRowSpan(y), + mode, + kr, + kg, + kb, + lumaBias, + lumaScale, + chromaScale); + } } - private static void ConvertYuvToRgb(Av1FrameBuffer buffer, ImageFrame image, bool isSubsampled) + /// + /// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame. + /// + /// The AV1 frame containing the signaled color configuration. + /// The resolved conversion mode. + /// The red luma coefficient. + /// The green luma coefficient. + /// The blue luma coefficient. + /// The encoded luma bias. + /// The encoded luma range. + /// The encoded chroma range. + private static void GetConversionParameters( + Av1FrameBuffer frameBuffer, + out ConversionMode mode, + out float kr, + out float kg, + out float kb, + out float lumaBias, + out float lumaScale, + out float chromaScale) { - // Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer. - // Using BT.709 specification - Guard.NotNull(buffer.BufferY); - Guard.NotNull(buffer.BufferCb); - Guard.NotNull(buffer.BufferCr); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Height, image.Height, nameof(buffer)); - if (isSubsampled) + if (frameBuffer.BitDepth != Av1BitDepth.EightBit) + { + throw new NotSupportedException("Only 8-bit AV1 color conversion is currently supported."); + } + + if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv444) { - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width >> 1, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height >> 1, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width >> 1, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height >> 1, nameof(buffer)); + throw new NotSupportedException("Only AV1 YUV 4:4:4 color conversion is currently supported."); } - else + + mode = ConversionMode.Coefficients; + kr = 0F; + kb = 0F; + + // These values are the matrix table used by libavif and are defined by H.273. + switch (frameBuffer.ColorConfig.MatrixCoefficients) { - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height, nameof(buffer)); + case ObuMatrixCoefficients.Identity: + mode = ConversionMode.Identity; + break; + case ObuMatrixCoefficients.Bt709: + kr = 0.2126F; + kb = 0.0722F; + break; + case ObuMatrixCoefficients.Fcc: + kr = 0.30F; + kb = 0.11F; + break; + case ObuMatrixCoefficients.Bt470BG: + case ObuMatrixCoefficients.Bt601: + case ObuMatrixCoefficients.Unspecified: + // libavif falls back to BT.601 coefficients when the matrix is unavailable. + kr = 0.299F; + kb = 0.114F; + break; + case ObuMatrixCoefficients.Smpte240: + kr = 0.212F; + kb = 0.087F; + break; + case ObuMatrixCoefficients.SmpteYCgCo: + mode = ConversionMode.YCgCo; + break; + case ObuMatrixCoefficients.Bt2020NonConstantLuminance: + kr = 0.2627F; + kb = 0.0593F; + break; + default: + throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported."); } - image.ProcessPixelRows(accessor => + kg = 1F - kr - kb; + bool isFullRange = frameBuffer.ColorConfig.ColorRange; + if (mode == ConversionMode.YCgCo && !isFullRange) + { + throw new NotSupportedException("Limited-range AV1 YCgCo color conversion is not currently supported."); + } + + lumaBias = isFullRange ? 0F : LimitedRangeLumaBias; + lumaScale = isFullRange ? FullRangeLumaScale : LimitedRangeLumaScale; + chromaScale = isFullRange ? FullRangeChromaScale : LimitedRangeChromaScale; + } + + /// + /// Converts one YUV 4:4:4 row to packed RGB using the resolved H.273 conversion state. + /// + /// The luma samples. + /// The blue-difference chroma samples. + /// The red-difference chroma samples. + /// The destination RGB pixels. + /// The conversion mode. + /// The red luma coefficient. + /// The green luma coefficient. + /// The blue luma coefficient. + /// The encoded luma bias. + /// The encoded luma range. + /// The encoded chroma range. + private static void ConvertYuv444ToRgbRow( + ReadOnlySpan ySource, + ReadOnlySpan uSource, + ReadOnlySpan vSource, + Span destination, + ConversionMode mode, + float kr, + float kg, + float kb, + float lumaBias, + float lumaScale, + float chromaScale) + { + for (int x = 0; x < destination.Length; x++) + { + float y = (ySource[x] - lumaBias) / lumaScale; + float cb = (uSource[x] - ChromaBias) / chromaScale; + float cr = (vSource[x] - ChromaBias) / chromaScale; + float r; + float g; + float b; + + switch (mode) { - Span yBuffer = buffer.DeriveBlockPointer(Av1Plane.Y, default, 0, 0, out int yStride); - Span uBuffer = buffer.DeriveBlockPointer(Av1Plane.U, default, 0, 0, out int uStride); - Span vBuffer = buffer.DeriveBlockPointer(Av1Plane.V, default, 0, 0, out int vStride); - int yOffset = yStride; - int uOffset = uStride; - int vOffset = vStride; - for (int y = 0; y < image.Height; y++) - { - Span rgbRow = accessor.GetRowSpan(y); - ref Rgb24 pixel = ref rgbRow[0]; - ref byte yRef = ref yBuffer[yOffset]; - ref byte uRef = ref uBuffer[uOffset]; - ref byte vRef = ref vBuffer[vOffset]; - for (int x = 0; x < image.Width; x++) - { - 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); - vRef = ref Unsafe.Add(ref vRef, 1); - } - - yOffset += yStride; - uOffset += uStride; - vOffset += vStride; - } - }); + case ConversionMode.Identity: + // H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order. + r = (vSource[x] - lumaBias) / lumaScale; + g = y; + b = (uSource[x] - lumaBias) / lumaScale; + break; + case ConversionMode.YCgCo: + float temporary = y - cb; + r = temporary + cr; + g = y + cb; + b = temporary - cr; + break; + default: + r = y + (2F * (1F - kr) * cr); + g = y - (2F * ((kr * (1F - kr) * cr) + (kb * (1F - kb) * cb)) / kg); + b = y + (2F * (1F - kb) * cb); + break; + } + + destination[x] = new Rgb24( + ToByte(r * ByteMaximum), + ToByte(g * ByteMaximum), + ToByte(b * ByteMaximum)); + } } - private static void ConvertRgbToYuv444(ImageFrame image, Av1FrameBuffer buffer) + /// + /// Converts one packed RGB row to YUV 4:4:4 using the resolved H.273 conversion state. + /// + /// The source RGB pixels. + /// The destination luma samples. + /// The destination blue-difference chroma samples. + /// The destination red-difference chroma samples. + /// The conversion mode. + /// The red luma coefficient. + /// The green luma coefficient. + /// The blue luma coefficient. + /// The encoded luma bias. + /// The encoded luma range. + /// The encoded chroma range. + private static void ConvertRgbToYuv444Row( + ReadOnlySpan source, + Span yDestination, + Span uDestination, + Span vDestination, + ConversionMode mode, + float kr, + float kg, + float kb, + float lumaBias, + float lumaScale, + float chromaScale) { - // Weight multiplied by 256 to exploit full byte resolution, rounded to the nearest integer. - Guard.NotNull(buffer.BufferY); - Guard.NotNull(buffer.BufferCb); - Guard.NotNull(buffer.BufferCr); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferY.Height, image.Height, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCb.Height, image.Height, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Width, image.Width, nameof(buffer)); - Guard.MustBeGreaterThanOrEqualTo(buffer.BufferCr.Height, image.Height, nameof(buffer)); - - image.ProcessPixelRows(accessor => + for (int x = 0; x < source.Length; x++) { - Buffer2D yBuffer = buffer.BufferY; - Buffer2D uBuffer = buffer.BufferCb; - Buffer2D vBuffer = buffer.BufferCr; - for (int y = 0; y < image.Height; y++) + Rgb24 pixel = source[x]; + float r = pixel.R / ByteMaximum; + float g = pixel.G / ByteMaximum; + float b = pixel.B / ByteMaximum; + float y; + float cb; + float cr; + + switch (mode) + { + case ConversionMode.Identity: + y = g; + cb = b; + cr = r; + break; + case ConversionMode.YCgCo: + y = (0.5F * g) + (0.25F * (r + b)); + cb = (0.5F * g) - (0.25F * (r + b)); + cr = 0.5F * (r - b); + break; + default: + y = (kr * r) + (kg * g) + (kb * b); + cb = (b - y) / (2F * (1F - kb)); + cr = (r - y) / (2F * (1F - kr)); + break; + } + + yDestination[x] = ToByte((y * lumaScale) + lumaBias); + if (mode == ConversionMode.Identity) { - Span rgbRow = accessor.GetRowSpan(y); - ref Rgb24 pixel = ref rgbRow[0]; - Span ySpan = yBuffer.DangerousGetRowSpan(y); - ref byte yRef = ref ySpan[0]; - Span uSpan = uBuffer.DangerousGetRowSpan(y); - ref byte uRef = ref uSpan[0]; - Span vSpan = vBuffer.DangerousGetRowSpan(y); - ref byte vRef = ref vSpan[0]; - for (int x = 0; x < image.Width; x++) - { - 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); - vRef = ref Unsafe.Add(ref vRef, 1); - } + uDestination[x] = ToByte((cb * lumaScale) + lumaBias); + vDestination[x] = ToByte((cr * lumaScale) + lumaBias); } - }); + else + { + uDestination[x] = ToByte((cb * chromaScale) + ChromaBias); + vDestination[x] = ToByte((cr * chromaScale) + ChromaBias); + } + } } + + /// + /// Rounds and clamps a conversion result to an 8-bit sample. + /// + /// The conversion result. + /// The bounded sample. + private static byte ToByte(float value) + => (byte)Numerics.Clamp((int)MathF.Round(value, MidpointRounding.AwayFromZero), byte.MinValue, byte.MaxValue); } diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs index e9658997fb..f076a794cc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; internal enum ObuMatrixCoefficients { Identity = 0, - Bt407 = 1, + Bt709 = 1, Unspecified = 2, Fcc = 4, Bt470BG = 5, diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs index 8cc97d050a..960d8102e1 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -4,6 +4,7 @@ using System; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -13,51 +14,71 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; public class Av1YuvConverterTests { [Theory] - [InlineData(255, 255, 255, 255, 127, 127)] - [InlineData(0, 0, 0, 0, 127, 127)] - [InlineData(42, 42, 42, 42, 127, 127)] - [InlineData(150, 100, 50, 107, 97, 154)] - public void RgbToYuvSinglePixel(byte r, byte g, byte b, int y, int u, int v) + [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)] + [InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)] + [InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)] + [InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)] + [InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)] + public void RgbToYuvSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients) { // Assign using Image image = new(1, 1); ImageFrame frame = image.Frames.RootFrame; frame.DangerousTryGetSinglePixelMemory(out Memory memory); memory.Span[0] = new Rgb24(r, g, b); - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = 1; - sequenceHeader.MaxFrameHeight = 1; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); // Assert - byte actualY = frameBuffer.BufferY.DangerousGetRowSpan(0)[0]; - byte actualU = frameBuffer.BufferCb.DangerousGetRowSpan(0)[0]; - byte actualV = frameBuffer.BufferCr.DangerousGetRowSpan(0)[0]; + byte actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]; + byte actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]; + byte actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]; Assert.Equal(y, actualY); Assert.Equal(u, actualU); Assert.Equal(v, actualV); } [Theory] - [InlineData(255, 255, 255, 255, 127, 127)] - [InlineData(0, 0, 0, 0, 127, 127)] - [InlineData(42, 42, 42, 42, 127, 127)] - [InlineData(150, 100, 50, 107, 97, 154)] - public void YuvToRgbSinglePixel(byte r, byte g, byte b, int y, int u, int v) + [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)] + [InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)] + [InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)] + [InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)] + [InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)] + public void YuvToRgbSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients) { // Assign using Image image = new(1, 1); ImageFrame frame = image.Frames.RootFrame; - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = 1; - sequenceHeader.MaxFrameHeight = 1; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); - frameBuffer.BufferY.DangerousGetRowSpan(0)[0] = (byte)y; - frameBuffer.BufferCb.DangerousGetRowSpan(0)[0] = (byte)u; - frameBuffer.BufferCr.DangerousGetRowSpan(0)[0] = (byte)v; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0] = (byte)y; + frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0] = (byte)u; + frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0] = (byte)v; // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); @@ -83,10 +104,8 @@ public class Av1YuvConverterTests Span input = new byte[sampleCount * 3]; CreateTestData(rnd, input); PixelOperations.Instance.FromBgr24Bytes(Configuration.Default, input, memory.Span, image.Width); - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = image.Width; - sequenceHeader.MaxFrameHeight = image.Height; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); @@ -94,9 +113,9 @@ public class Av1YuvConverterTests // Assert Span actual = new Rgb24[frameBuffer.Width]; - Span yRow = frameBuffer.BufferY!.DangerousGetSingleSpan(); - Span uRow = frameBuffer.BufferCb!.DangerousGetSingleSpan(); - Span vRow = frameBuffer.BufferCr!.DangerousGetSingleSpan(); + Span yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); + Span uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0); + Span vRow = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0); for (int i = 0; i < frameBuffer.Width; i++) { Rgb24 pixel = new(); @@ -117,10 +136,8 @@ public class Av1YuvConverterTests // Assign using Image image = new(sampleCount, 1); ImageFrame frame = image.Frames.RootFrame; - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = image.Width; - sequenceHeader.MaxFrameHeight = image.Height; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); Random rnd = new(42); CreateTestData(rnd, frameBuffer, Av1Plane.Y); CreateTestData(rnd, frameBuffer, Av1Plane.U); @@ -128,7 +145,7 @@ public class Av1YuvConverterTests // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); - Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, false); + Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true); // Assert frame.DangerousTryGetSinglePixelMemory(out Memory memory); @@ -152,14 +169,11 @@ public class Av1YuvConverterTests private static void CreateTestData(Random rnd, Av1FrameBuffer frameBuffer, Av1Plane plane) { const int bitCount = 8; - Span span = frameBuffer.DeriveBlockPointer(plane, new Point(0, 0), 0, 0, out int stride); - int max = (1 << bitCount) - 1; - for (int i = 0; i < span.Length; i++) + Buffer2DRegion region = frameBuffer.DeriveBlockPointer(plane, 0, 0); + for (int y = 0; y < region.Height; y++) { - byte current = (byte)rnd.Next(max); - span[i] = current; + CreateTestData(rnd, region.DangerousGetRowSpan(y), bitCount); } - } private static void CreateTestData(Random rnd, Span span, int bitCount = 8) @@ -199,10 +213,8 @@ public class Av1YuvConverterTests ImageFrame frame = image.Frames.RootFrame; frame.DangerousTryGetSinglePixelMemory(out Memory memory); memory.Span[0] = new Rgb24(r, g, b); - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = 1; - sequenceHeader.MaxFrameHeight = 1; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); using Image actual = new(image.Width, image.Height); // Act @@ -224,10 +236,8 @@ public class Av1YuvConverterTests // Assign using Image image = provider.GetImage(); ImageFrame frame = image.Frames.RootFrame; - ObuSequenceHeader sequenceHeader = new(); - sequenceHeader.MaxFrameWidth = image.Width; - sequenceHeader.MaxFrameHeight = image.Height; - Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); using Image actual = new(image.Width, image.Height); // Act @@ -237,4 +247,22 @@ public class Av1YuvConverterTests // Assert ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual); } + + private static ObuSequenceHeader CreateSequenceHeader( + int width, + int height, + bool fullRange = true, + ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709) + => new() + { + MaxFrameWidth = width, + MaxFrameHeight = height, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + BitDepth = Av1BitDepth.EightBit, + MatrixCoefficients = matrixCoefficients, + ColorRange = fullRange, + }, + }; }