// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Numerics; using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// /// Verifies AV1 color conversion, sample-range handling, chroma reconstruction, and encoder downsampling. /// [Trait("Format", "Avif")] public class Av1YuvConverterTests { /// /// The hardware configurations covering 512-bit, 256-bit, 128-bit, and scalar conversion paths. /// private const HwIntrinsics AlphaConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; /// /// Verifies known RGB-to-YUV values across coefficient, identity, and YCgCo matrices and sample ranges. /// /// The source red component. /// The source green component. /// The source blue component. /// The expected luma or first encoded component. /// The expected first chroma or second encoded component. /// The expected second chroma or third encoded component. /// Whether the encoded samples use the full range. /// The matrix coefficients used for conversion. [Theory] [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 = 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.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); } /// /// Verifies known YUV-to-RGB values across coefficient, identity, and YCgCo matrices and sample ranges. /// /// The expected red component. /// The expected green component. /// The expected blue component. /// The source luma or first encoded component. /// The source first chroma or second encoded component. /// The source second chroma or third encoded component. /// Whether the encoded samples use the full range. /// The matrix coefficients used for conversion. [Theory] [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 = 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); // Assert frame.DangerousTryGetSinglePixelMemory(out Memory memory); Rgb24 actual = memory.Span[0]; Assert.Equal(r, actual.R, 1d); Assert.Equal(g, actual.G, 1d); Assert.Equal(b, actual.B, 1d); } /// /// Verifies that limited-range monochrome samples expand to the complete RGB output range. /// [Fact] public void Yuv400ToRgbExpandsLimitedRangeLuma() { // Assign using Image image = new(2, 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 2, 1, false, ObuMatrixCoefficients.Identity, Av1ColorFormat.Yuv400); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); Span yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); yRow[0] = 16; yRow[1] = 235; // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); // Assert Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Assert.Equal(new Rgb24(0, 0, 0), actual[0]); Assert.Equal(new Rgb24(255, 255, 255), actual[1]); } /// /// Verifies RGB-to-monochrome conversion and range quantization for every supported AV1 bit depth. /// /// The encoded AV1 bit depth. /// Whether the luma samples use the full range. /// The expected encoded luma sample. [Theory] [InlineData(Av1BitDepth.EightBit, true, 107)] [InlineData(Av1BitDepth.EightBit, false, 108)] [InlineData(Av1BitDepth.TenBit, true, 429)] [InlineData(Av1BitDepth.TenBit, false, 432)] [InlineData(Av1BitDepth.TwelveBit, true, 1719)] [InlineData(Av1BitDepth.TwelveBit, false, 1727)] public void RgbToYuv400WritesQuantizedLuma(int bitDepth, bool fullRange, int expectedLuma) { // Rgb48 values scaled from eight-bit components exercise the precision-preserving high-bit-depth path. using Image image = new(1, 1); image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(150 * 257, 100 * 257, 50 * 257); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 1, 1, fullRange, colorFormat: Av1ColorFormat.Yuv400, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); Assert.Equal(expectedLuma, GetPlaneSample(frameBuffer, Av1Plane.Y, 0, 0, 0, 0)); Assert.Null(frameBuffer.BufferCb); Assert.Null(frameBuffer.BufferCr); } /// /// Verifies full- and limited-range expansion for 10-bit and 12-bit reconstructed samples. /// /// The reconstructed AV1 bit depth. /// Whether the samples use the full range. /// The encoded black luma sample. /// The encoded white luma sample. /// The neutral encoded chroma sample. [Theory] [InlineData(Av1BitDepth.TenBit, true, 0, 1023, 512)] [InlineData(Av1BitDepth.TenBit, false, 64, 940, 512)] [InlineData(Av1BitDepth.TwelveBit, true, 0, 4095, 2048)] [InlineData(Av1BitDepth.TwelveBit, false, 256, 3760, 2048)] public void HighBitDepthYuvToRgbExpandsSignaledRange( int bitDepth, bool fullRange, ushort black, ushort white, ushort neutralChroma) { // Assign using Image image = new(2, 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(2, 1, fullRange, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); Span yRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); yRow[0] = black; yRow[1] = white; frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0).Fill(neutralChroma); frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0).Fill(neutralChroma); // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); // Assert Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Assert.Equal(new Rgb24(0, 0, 0), actual[0]); Assert.Equal(new Rgb24(255, 255, 255), actual[1]); } /// /// Verifies that high-bit-depth frame strides and row access use 16-bit sample units consistently. /// /// The reconstructed AV1 bit depth. [Theory] [InlineData(Av1BitDepth.TenBit)] [InlineData(Av1BitDepth.TwelveBit)] public void HighBitDepthFrameBufferUsesSampleUnitStrides(int bitDepth) { // Assign ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 3, 3, colorFormat: Av1ColorFormat.Yuv420, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); // Act Span block = frameBuffer.DeriveBlockPointer16(Av1Plane.Y, Point.Empty, 0, 0, out int stride); block[stride] = 321; Span chromaRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 1, 1); // Assert Assert.Equal(2, frameBuffer.BytesPerSample); Assert.Equal(3 + 144, stride); Assert.Equal(stride * 2, frameBuffer.BufferY!.Width); Assert.Equal(321, frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0]); Assert.Equal(2, chromaRow.Length); } /// /// Verifies centered horizontal chroma reconstruction for a YUV 4:2:2 frame. /// [Fact] public void Yuv422ToRgbBilinearlyUpsamplesCenteredChroma() { // Assign using Image image = new(4, 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(4, 1, colorFormat: Av1ColorFormat.Yuv422); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv422, false); frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0).Fill(128); Span uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 0).DangerousGetRowSpan(0); uRow[0] = 128; uRow[1] = 192; frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 0).DangerousGetRowSpan(0).Fill(128); // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); // Assert Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Assert.Equal(new Rgb24(128, 128, 128), actual[0]); Assert.Equal(new Rgb24(128, 125, 158), actual[1]); Assert.Equal(new Rgb24(128, 119, 217), actual[2]); Assert.Equal(new Rgb24(128, 116, 247), actual[3]); } /// /// Verifies vertical and horizontal YUV 4:2:0 reconstruction at every AV1 chroma sample position. /// /// The signaled AV1 chroma sample position. /// The expected blue component in the top-row probe pixel. /// The expected blue component in the left-column probe pixel. [Theory] [InlineData(ObuChromoSamplePosition.Unknown, 158, 98)] [InlineData(ObuChromoSamplePosition.Vertical, 187, 98)] [InlineData(ObuChromoSamplePosition.Colocated, 187, 69)] public void Yuv420ToRgbUsesChromaSamplePosition(int chromaSamplePosition, byte expectedTopBlue, byte expectedLeftBlue) { // Assign using Image image = new(4, 4); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 4, 4, colorFormat: Av1ColorFormat.Yuv420, chromaSamplePosition: (ObuChromoSamplePosition)chromaSamplePosition); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1); Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1); for (int y = 0; y < yPlane.Height; y++) { yPlane.DangerousGetRowSpan(y).Fill(128); } uPlane.DangerousGetRowSpan(0)[0] = 128; uPlane.DangerousGetRowSpan(0)[1] = 192; uPlane.DangerousGetRowSpan(1)[0] = 64; uPlane.DangerousGetRowSpan(1)[1] = 255; vPlane.DangerousGetRowSpan(0).Fill(128); vPlane.DangerousGetRowSpan(1).Fill(128); // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); // Assert Assert.Equal(expectedTopBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[1].B); Assert.Equal(expectedLeftBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(1)[0].B); } /// /// Verifies that encoding selects the horizontal and vertical sample coordinates defined by each AV1 chroma position. /// /// The signaled AV1 chroma sample position. /// The expected encoded blue-difference sample. /// The expected encoded red-difference sample. [Theory] [InlineData(ObuChromoSamplePosition.Unknown, 128, 128)] [InlineData(ObuChromoSamplePosition.Vertical, 96, 192)] [InlineData(ObuChromoSamplePosition.Colocated, 128, 128)] public void RgbToYuv420UsesChromaSamplePosition(int chromaSamplePosition, byte expectedChromaBlue, byte expectedChromaRed) { using Image source = new(2, 2); // The four distinct YCgCo samples make left, centered, and vertically averaged selection observable as // exact integer chroma values without introducing transfer-function or coefficient-rounding tolerances. source[0, 0] = new Rgba32(0, 0, 0); source[1, 0] = new Rgba32(0, byte.MaxValue, 0); source[0, 1] = new Rgba32(byte.MaxValue, 0, 0); source[1, 1] = new Rgba32(0, 0, byte.MaxValue); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 2, 2, matrixCoefficients: ObuMatrixCoefficients.SmpteYCgCo, colorFormat: Av1ColorFormat.Yuv420, chromaSamplePosition: (ObuChromoSamplePosition)chromaSamplePosition); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); Assert.Equal(expectedChromaBlue, frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1).DangerousGetRowSpan(0)[0]); Assert.Equal(expectedChromaRed, frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1).DangerousGetRowSpan(0)[0]); } /// /// Verifies that odd image dimensions retain the final YUV 4:2:0 chroma row and column. /// [Fact] public void Yuv420UsesCeilingChromaPlaneDimensions() { // Assign ObuSequenceHeader sequenceHeader = CreateSequenceHeader(3, 3, colorFormat: Av1ColorFormat.Yuv420); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); // Act Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1); Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1); // Assert Assert.Equal(new Size(2, 2), uPlane.Size); Assert.Equal(new Size(2, 2), vPlane.Size); } /// /// Compares SIMD-first RGB-to-YUV conversion with the independent scalar reference over randomized pixels. /// [Fact] public void RgbToYuvCompareToReferenceRandomPixels() { const int sampleCount = 1000; // Assign using Image image = new(sampleCount, 1); ImageFrame frame = image.Frames.RootFrame; frame.DangerousTryGetSinglePixelMemory(out Memory memory); Random rnd = new(42); Span input = new byte[sampleCount * 3]; CreateTestData(rnd, input); PixelOperations.Instance.FromBgr24Bytes(Configuration.Default, input, memory.Span, image.Width); ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); Span referenceOutput = Av1ReferenceYuvConverter.RgbToYuv(memory.Span, true); // Assert Span actual = new Rgb24[frameBuffer.Width]; 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(); pixel.R = yRow[i]; pixel.G = uRow[i]; pixel.B = vRow[i]; actual[i] = pixel; } Compare(referenceOutput, actual, 3); } /// /// Compares SIMD-first YUV-to-RGB conversion with the independent scalar reference over randomized samples. /// [Fact] public void YuvToRgbCompareToReferenceRandomPixels() { const int sampleCount = 1000; // Assign using Image image = new(sampleCount, 1); ImageFrame frame = image.Frames.RootFrame; 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); CreateTestData(rnd, frameBuffer, Av1Plane.V); // Act Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true); // Assert frame.DangerousTryGetSinglePixelMemory(out Memory memory); Span actual = memory.Span; Compare(referenceOutput, actual, 3); } /// /// Compares packed RGB rows within the permitted per-component tolerance. /// /// The independently converted reference pixels. /// The pixels produced by the implementation under test. /// The permitted absolute component difference. private static void Compare(Span referenceOutput, Span actual, int allowedDifference) { for (int i = 0; i < actual.Length; i++) { if (Math.Abs(referenceOutput[i].R - actual[i].R) > allowedDifference || Math.Abs(referenceOutput[i].G - actual[i].G) > allowedDifference || Math.Abs(referenceOutput[i].B - actual[i].B) > allowedDifference) { Assert.Fail($"Difference at index {i}, expected: {referenceOutput[i]} but was {actual[i]}"); } } } /// /// Fills one reconstructed plane with deterministic pseudo-random test samples. /// /// The deterministic random number generator. /// The frame containing the destination plane. /// The destination plane. private static void CreateTestData(Random rnd, Av1FrameBuffer frameBuffer, Av1Plane plane) { const int bitCount = 8; Buffer2DRegion region = frameBuffer.DeriveBlockPointer(plane, 0, 0); for (int y = 0; y < region.Height; y++) { CreateTestData(rnd, region.DangerousGetRowSpan(y), bitCount); } } /// /// Fills an eight-bit sample span with deterministic pseudo-random values. /// /// The deterministic random number generator. /// The destination sample span. /// The number of significant sample bits. private static void CreateTestData(Random rnd, Span span, int bitCount = 8) { int max = (1 << bitCount) - 1; for (int i = 0; i < span.Length; i++) { byte current = (byte)rnd.Next(max); span[i] = current; } } /// /// Fills a high-bit-depth sample span with deterministic pseudo-random values. /// /// The deterministic random number generator. /// The destination sample span. /// The number of significant sample bits. private static void CreateTestData(Random rnd, Span span, int bitCount) { int max = (1 << bitCount) - 1; for (int i = 0; i < span.Length; i++) { ushort current = (ushort)rnd.Next(max); span[i] = current; } } /// /// Verifies RGB-to-YUV-to-RGB conversion for representative single-pixel colors. /// /// The source red component. /// The source green component. /// The source blue component. [Theory] [InlineData(255, 255, 255)] [InlineData(0, 0, 0)] [InlineData(42, 42, 42)] [InlineData(42, 0, 0)] [InlineData(42, 42, 0)] [InlineData(42, 0, 42)] [InlineData(0, 42, 42)] [InlineData(0, 0, 42)] [InlineData(150, 100, 50)] public void RoundTripSinglePixel(byte r, byte g, byte b) { // 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 = CreateSequenceHeader(1, 1); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); using Image actual = new(image.Width, image.Height); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); // Assert actual.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory actualMemory); Rgb24 actualPixel = actualMemory.Span[0]; Assert.Equal(r, actualPixel.R, 2d); Assert.Equal(g, actualPixel.G, 2d); Assert.Equal(b, actualPixel.B, 2d); } /// /// Verifies 10-bit and 12-bit round trips for coefficient, identity, and YCgCo matrices. /// /// The encoded AV1 bit depth. /// The matrix coefficients used for conversion. [Theory] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.SmpteYCgCo)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.IptC2)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Bt709)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Identity)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.SmpteYCgCo)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.IptC2)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo)] public void HighBitDepthRoundTrip(int bitDepth, int matrixCoefficients) { // Assign using Image image = new(3, 1); Span source = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); source[0] = new Rgb24(0, 0, 0); source[1] = new Rgb24(150, 100, 50); source[2] = new Rgb24(255, 255, 255); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( image.Width, image.Height, matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); using Image actual = new(image.Width, image.Height); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); // Assert Span actualPixels = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); for (int x = 0; x < source.Length; x++) { Assert.Equal(source[x].R, actualPixels[x].R, 1D); Assert.Equal(source[x].G, actualPixels[x].G, 1D); Assert.Equal(source[x].B, actualPixels[x].B, 1D); } } /// /// Verifies the H.273 IPT-C2 matrices in both directions against independently calculated code values. /// [Fact] public void IptC2MatchesKnownLinearTransferValuesInBothDirections() { // Assign // The linear transfer characteristic isolates the two normative IPT-C2 matrices from transfer-curve error. using Image source = new(1, 1); source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb24(150, 100, 50); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 1, 1, matrixCoefficients: ObuMatrixCoefficients.IptC2, transferCharacteristics: ObuTransferCharacteristics.Linear); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); // Assert Assert.Equal(100, frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]); Assert.Equal(178, frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]); Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]); using Image destination = new(1, 1); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); } /// /// Verifies the reversible YCgCo lifting stages against known pure-red code values in both directions. /// /// The encoded AV1 bit depth. /// The reversible YCgCo variant. /// The expected encoded luma value. /// The expected encoded Cg value. /// The expected encoded Co value. [Theory] [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 15, 97, 191)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 63, 385, 767)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 255, 1537, 3071)] [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 31, 65, 255)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 127, 257, 1023)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 511, 1025, 4095)] public void ReversibleYCgCoMatchesKnownPureRedValuesInBothDirections( int bitDepth, int matrixCoefficients, int expectedY, int expectedU, int expectedV) { // Assign // Pure red exercises positive odd Co and negative odd Cg. The expected samples come directly from // the H.273 integer lifting equations at the logical RGB precision selected by each matrix code point. using Image source = new(1, 1); source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(ushort.MaxValue, 0, 0); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 1, 1, matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); // Assert int actualY; int actualU; int actualV; if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit) { actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]; actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]; actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]; } else { actualY = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0]; actualU = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0)[0]; actualV = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0)[0]; } Assert.Equal(expectedY, actualY); Assert.Equal(expectedU, actualU); Assert.Equal(expectedV, actualV); using Image destination = new(1, 1); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); } /// /// Verifies that limited-range reversible YCgCo applies range adjustment to RGB code values before lifting. /// /// The encoded AV1 bit depth. /// The reversible YCgCo variant. /// The expected black luma code value. /// The expected white luma code value. [Theory] [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 4, 59)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 16, 235)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 64, 940)] [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 8, 118)] [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 32, 470)] [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 128, 1880)] public void ReversibleYCgCoAppliesLimitedRangeBeforeLifting( int bitDepth, int matrixCoefficients, int expectedBlack, int expectedWhite) { // Assign // Black and white have zero Cg and Co, exposing the RGB-domain range mapping without opponent-axis noise. using Image source = new(2, 1); Span sourcePixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); sourcePixels[0] = new Rgb48(0, 0, 0); sourcePixels[1] = new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 2, 1, fullRange: false, matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); // Act Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); // Assert int expectedChromaBias = 1 << (((Av1BitDepth)bitDepth).GetBitCount() - 1); if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit) { Span y = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); Span u = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0); Span v = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0); Assert.Equal(expectedBlack, y[0]); Assert.Equal(expectedWhite, y[1]); Assert.Equal(expectedChromaBias, u[0]); Assert.Equal(expectedChromaBias, u[1]); Assert.Equal(expectedChromaBias, v[0]); Assert.Equal(expectedChromaBias, v[1]); } else { Span y = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); Span u = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0); Span v = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0); Assert.Equal(expectedBlack, y[0]); Assert.Equal(expectedWhite, y[1]); Assert.Equal(expectedChromaBias, u[0]); Assert.Equal(expectedChromaBias, u[1]); Assert.Equal(expectedChromaBias, v[0]); Assert.Equal(expectedChromaBias, v[1]); } using Image destination = new(2, 1); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Span destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]); Assert.Equal(new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue), destinationPixels[1]); } /// /// Verifies that reversible YCgCo rejects chroma subsampling in both conversion directions. /// /// The reversible YCgCo variant. [Theory] [InlineData(ObuMatrixCoefficients.YCgCoRe)] [InlineData(ObuMatrixCoefficients.YCgCoRo)] public void ReversibleYCgCoRequiresFullChroma(int matrixCoefficients) { // Assign using Image image = new(2, 2); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 2, 2, matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, colorFormat: Av1ColorFormat.Yuv420); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); // Act and assert Assert.Throws( () => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer)); Assert.Throws( () => Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame)); } /// /// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail. /// /// The matrix coefficients selecting the color operator. /// The transfer characteristics used by nonlinear operators. [Theory] [InlineData(ObuMatrixCoefficients.Bt709, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.Identity, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.SmpteYCgCo, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.Bt2020ConstantLuminance, ObuTransferCharacteristics.Bt202010Bit)] [InlineData(ObuMatrixCoefficients.Smpte2085, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)] [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)] [InlineData(ObuMatrixCoefficients.IptC2, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.YCgCoRe, ObuTransferCharacteristics.Bt709)] [InlineData(ObuMatrixCoefficients.YCgCoRo, ObuTransferCharacteristics.Bt709)] public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics) { const int width = 31; // Thirty-one samples exercise Vector512, Vector256, Vector128, and scalar stages on AVX-512 hardware. // The same row still reaches the widest available stages and scalar tail on narrower SIMD hardware. using Image source = new(width, 1); source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0).Fill(new Rgb48(39999, 27777, 12345)); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( width, 1, matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, bitDepth: Av1BitDepth.TwelveBit, transferCharacteristics: (ObuTransferCharacteristics)transferCharacteristics, colorPrimaries: ObuColorPrimaries.Bt2020); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); using Image destination = new(width, 1); Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0); AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0); AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.V, 0, 0); Span pixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); for (int x = 1; x < pixels.Length; x++) { Assert.Equal(pixels[0], pixels[x]); } } /// /// Verifies horizontal and vertical chroma downsampling against full-resolution encoded components. /// /// The subsampled AV1 color format. /// The encoded AV1 bit depth. [Theory] [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.EightBit)] [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.TwelveBit)] [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.EightBit)] [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.TwelveBit)] public void RgbToYuvSubsamplingAveragesFullResolutionChroma(int colorFormat, int bitDepth) { const int width = 35; const int height = 3; using Image source = new(width, height); for (int y = 0; y < height; y++) { Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { row[x] = new Rgb48( (ushort)((x * 1879) + (y * 791)), (ushort)((x * 977) + (y * 3251)), (ushort)((x * 613) + (y * 4987))); } } ObuSequenceHeader fullResolutionHeader = CreateSequenceHeader( width, height, colorFormat: Av1ColorFormat.Yuv444, bitDepth: (Av1BitDepth)bitDepth); ObuSequenceHeader subsampledHeader = CreateSequenceHeader( width, height, colorFormat: (Av1ColorFormat)colorFormat, bitDepth: (Av1BitDepth)bitDepth); using Av1FrameBuffer fullResolution = new(Configuration.Default, fullResolutionHeader, Av1ColorFormat.Yuv444, false); using Av1FrameBuffer subsampled = new(Configuration.Default, subsampledHeader, (Av1ColorFormat)colorFormat, false); Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, fullResolution); Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, subsampled); AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.U); AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.V); } /// /// Verifies an image-wide RGB-to-YUV-to-RGB conversion against the configured similarity tolerance. /// /// The source test-image provider. // [Theory] // [WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)] public void RoundTrip(TestImageProvider provider) { // Assign using Image image = provider.GetImage(); ImageFrame frame = image.Frames.RootFrame; 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 Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); // Assert ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual); } /// /// Verifies that same-sized AV1 alpha composition preserves color and maps full-range luma exactly with and /// without hardware intrinsics. /// [Fact] public void ComposeAlphaMapsEightBitLumaExactlyAcrossIntrinsicWidths() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateEightBitAlphaComposition, AlphaConfigurations); /// /// Verifies that scaled 10-bit and 12-bit AV1 alpha composition matches ImageSharp's established box resampler /// with and without hardware intrinsics. /// [Fact] public void ComposeAlphaScalesHighBitDepthLumaAcrossIntrinsicWidths() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateHighBitDepthAlphaScaling, AlphaConfigurations); /// /// Verifies that alpha scaling remains exact when the bounded working buffer must advance through multiple /// source-row windows. /// [Fact] public void ComposeAlphaScalesAcrossMultipleWorkingWindows() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateSlidingWindowAlphaScaling, AlphaConfigurations); /// /// Verifies exact limited-range endpoints and out-of-range clamping for every supported AV1 alpha bit depth. /// [Fact] public void ComposeAlphaExpandsLimitedRangeAcrossIntrinsicWidths() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ValidateLimitedRangeAlphaComposition, AlphaConfigurations); /// /// Exercises direct full-range byte alpha composition against exact code-value expansion. /// private static void ValidateEightBitAlphaComposition() { const int width = 19; const int height = 5; ObuSequenceHeader sequenceHeader = CreateSequenceHeader(width, height, colorFormat: Av1ColorFormat.Yuv400); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Image destination = new(width, height); Buffer2DRegion luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); for (int y = 0; y < height; y++) { Span sourceRow = luma.DangerousGetRowSpan(y); Span destinationRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { sourceRow[x] = (byte)((x * 11) + (y * 7)); destinationRow[x] = new Rgba64((ushort)(1000 + x), (ushort)(2000 + y), 3000, ushort.MaxValue); } } Av1YuvConverter.ComposeAlpha( Configuration.Default, frameBuffer, destination.Frames.RootFrame, destination.Size, destination.Bounds, false); for (int y = 0; y < height; y++) { Span sourceRow = luma.DangerousGetRowSpan(y); Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { Assert.Equal((ushort)(1000 + x), actualRow[x].R); Assert.Equal((ushort)(2000 + y), actualRow[x].G); Assert.Equal((ushort)3000, actualRow[x].B); Assert.Equal((ushort)(sourceRow[x] * 257), actualRow[x].A); } } } /// /// Exercises the direct box-resize path for both supported high-bit-depth sample layouts. /// private static void ValidateHighBitDepthAlphaScaling() { const int sourceWidth = 5; const int sourceHeight = 3; const int destinationWidth = 9; const int destinationHeight = 7; foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) { ObuSequenceHeader sequenceHeader = CreateSequenceHeader( sourceWidth, sourceHeight, colorFormat: Av1ColorFormat.Yuv400, bitDepth: bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Image expected = new(sourceWidth, sourceHeight); using Image destination = new(destinationWidth, destinationHeight, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); ushort maximum = (ushort)((1 << bitDepth.GetBitCount()) - 1); for (int y = 0; y < sourceHeight; y++) { Span sourceRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < sourceWidth; x++) { sourceRow[x] = (ushort)(((x * 223) + (y * 151)) & maximum); expectedRow[x] = L16.FromScaledVector4(new Vector4((float)sourceRow[x] / maximum)); } } expected.Mutate(context => context.Resize(destinationWidth, destinationHeight, KnownResamplers.Box)); Av1YuvConverter.ComposeAlpha( Configuration.Default, frameBuffer, destination.Frames.RootFrame, destination.Size, destination.Bounds, false); for (int y = 0; y < destinationHeight; y++) { Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < destinationWidth; x++) { Assert.Equal((ushort)1000, actualRow[x].R); Assert.Equal((ushort)2000, actualRow[x].G); Assert.Equal((ushort)3000, actualRow[x].B); Assert.Equal(expectedRow[x].PackedValue, actualRow[x].A); } } } } /// /// Exercises overlapping box kernels across multiple transposed source-row windows. /// private static void ValidateSlidingWindowAlphaScaling() { const int sourceWidth = 13; const int sourceHeight = 41; const int destinationWidth = 23; const int destinationHeight = 17; Configuration configuration = Configuration.CreateDefaultInstance(); configuration.WorkingBufferSizeHintInBytes = 1; ObuSequenceHeader sequenceHeader = CreateSequenceHeader( sourceWidth, sourceHeight, colorFormat: Av1ColorFormat.Yuv400, bitDepth: Av1BitDepth.TwelveBit); using Av1FrameBuffer frameBuffer = new(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false); using Image expected = new(configuration, sourceWidth, sourceHeight); using Image destination = new(configuration, destinationWidth, destinationHeight, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); for (int y = 0; y < sourceHeight; y++) { Span sourceRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < sourceWidth; x++) { sourceRow[x] = (ushort)(((x * 277) + (y * 193)) & 4095); expectedRow[x] = L16.FromScaledVector4(new Vector4(sourceRow[x] / 4095F)); } } expected.Mutate(context => context.Resize(destinationWidth, destinationHeight, KnownResamplers.Box)); Av1YuvConverter.ComposeAlpha( configuration, frameBuffer, destination.Frames.RootFrame, destination.Size, destination.Bounds, false); for (int y = 0; y < destinationHeight; y++) { Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); for (int x = 0; x < destinationWidth; x++) { Assert.Equal((ushort)1000, actualRow[x].R); Assert.Equal((ushort)2000, actualRow[x].G); Assert.Equal((ushort)3000, actualRow[x].B); Assert.Equal(expectedRow[x].PackedValue, actualRow[x].A); } } } /// /// Exercises luma-range expansion and clamping for 8-bit, 10-bit, and 12-bit alpha samples. /// private static void ValidateLimitedRangeAlphaComposition() { foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) { int bitCount = bitDepth.GetBitCount(); ushort minimum = (ushort)(16 << (bitCount - 8)); ushort maximum = (ushort)(235 << (bitCount - 8)); ushort storageMaximum = (ushort)((1 << bitCount) - 1); ObuSequenceHeader sequenceHeader = CreateSequenceHeader( 4, 1, fullRange: false, colorFormat: Av1ColorFormat.Yuv400, bitDepth: bitDepth); using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); using Image destination = new(4, 1, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); if (bitDepth == Av1BitDepth.EightBit) { Span luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); luma[0] = 0; luma[1] = (byte)minimum; luma[2] = (byte)maximum; luma[3] = byte.MaxValue; } else { Span luma = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); luma[0] = 0; luma[1] = minimum; luma[2] = maximum; luma[3] = storageMaximum; } Av1YuvConverter.ComposeAlpha( Configuration.Default, frameBuffer, destination.Frames.RootFrame, destination.Size, destination.Bounds, false); Span actual = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Assert.Equal((ushort)0, actual[0].A); Assert.Equal((ushort)0, actual[1].A); Assert.Equal(ushort.MaxValue, actual[2].A); Assert.Equal(ushort.MaxValue, actual[3].A); } } /// /// Creates a sequence header containing the color signaling required by a conversion test. /// /// The frame width. /// The frame height. /// Whether encoded samples use the full range. /// The matrix coefficients used for conversion. /// The encoded plane layout. /// The signaled chroma sample position. /// The encoded sample bit depth. /// The transfer characteristics used by nonlinear matrices. /// The color primaries used by derived matrices. /// The configured sequence header. private static ObuSequenceHeader CreateSequenceHeader( int width, int height, bool fullRange = true, ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709, Av1ColorFormat colorFormat = Av1ColorFormat.Yuv444, ObuChromoSamplePosition chromaSamplePosition = ObuChromoSamplePosition.Unknown, Av1BitDepth bitDepth = Av1BitDepth.EightBit, ObuTransferCharacteristics transferCharacteristics = ObuTransferCharacteristics.Bt709, ObuColorPrimaries colorPrimaries = ObuColorPrimaries.Bt709) => new() { MaxFrameWidth = width, MaxFrameHeight = height, ColorConfig = new ObuColorConfig { IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, BitDepth = bitDepth, MatrixCoefficients = matrixCoefficients, TransferCharacteristics = transferCharacteristics, ColorPrimaries = colorPrimaries, ColorRange = fullRange, SubSamplingX = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422, SubSamplingY = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420, ChromaSamplePosition = chromaSamplePosition, }, }; /// /// Verifies that every high-bit-depth sample in a plane matches its first sample. /// /// The encoded frame buffer. /// The plane to inspect. /// The horizontal subsampling shift. /// The vertical subsampling shift. private static void AssertPlaneContainsRepeatedSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int subX, int subY) { Span samples = frameBuffer.GetHighBitDepthRowSpan(plane, 0, subX, subY); for (int x = 1; x < samples.Length; x++) { Assert.Equal(samples[0], samples[x]); } } /// /// Verifies that a subsampled plane contains the rounded mean of the corresponding full-resolution samples. /// /// The full-resolution encoded frame. /// The subsampled encoded frame. /// The chroma plane to compare. private static void AssertSubsampledPlaneMatchesAverage( Av1FrameBuffer fullResolution, Av1FrameBuffer subsampled, Av1Plane plane) { int subY = subsampled.ColorConfig.SubSamplingY ? 1 : 0; int chromaHeight = (subsampled.Height + subY) >> subY; int chromaWidth = (subsampled.Width + 1) >> 1; for (int y = 0; y < chromaHeight; y++) { int sourceY = y << subY; int rowCount = subY == 0 ? 1 : Math.Min(2, fullResolution.Height - sourceY); for (int x = 0; x < chromaWidth; x++) { int sourceX = x << 1; int columnCount = Math.Min(2, fullResolution.Width - sourceX); int sum = 0; for (int row = 0; row < rowCount; row++) { for (int column = 0; column < columnCount; column++) { sum += GetPlaneSample(fullResolution, plane, sourceX + column, sourceY + row, 0, 0); } } int expected = (int)MathF.Round((float)sum / (rowCount * columnCount), MidpointRounding.AwayFromZero); int actual = GetPlaneSample(subsampled, plane, x, y, 1, subY); Assert.True( actual >= expected - 1 && actual <= expected + 1, $"Plane {plane}, sample ({x}, {y}): expected {expected} +/- 1 from sum {sum} over {rowCount * columnCount} samples but found {actual}."); } } } /// /// Gets one encoded sample from an eight-bit or high-bit-depth frame plane. /// /// The encoded frame buffer. /// The plane containing the sample. /// The horizontal sample coordinate. /// The vertical sample coordinate. /// The horizontal subsampling shift. /// The vertical subsampling shift. /// The encoded sample value. private static int GetPlaneSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int x, int y, int subX, int subY) => frameBuffer.BitDepth == Av1BitDepth.EightBit ? frameBuffer.DeriveBlockPointer(plane, subX, subY).DangerousGetRowSpan(y)[x] : frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)[x]; }