diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index c14810f488..28c5cc174b 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -842,6 +842,8 @@ Encoder verification contract: - [~] The combined-frame writer now completes the byte-counted uncompressed frame header before starting the optional multi-tile tile-group flag, matching current libaom's separate frame-header and tile-group writers. A non-uniform two-tile round trip verifies the explicit boundaries, both tile payloads, and complete stream consumption through direct net11 VSTest in Release. - [~] The first internal frame-to-OBU operation encodes 8-, 10-, and 12-bit monochrome reduced still pictures through the production tile writer and production decoder. Coefficient context initialization now stores `min(abs(level), 127)`, matching current libaom; the previous signed clamp converted every negative transform coefficient to zero and selected invalid nonzero-map distributions. Signed dense and sparse entropy round trips, direct level-buffer saturation coverage, and eight constant/gradient frame cases pass 52 of 52 direct net11 VSTest cases in Release. Current-main `aomdec` accepts all eight emitted payloads. Their decoded-frame MD5 values are `d09ea148582b9c93fa78e59426193bbc` (16x16 8-bit constant), `14e7d5ee5f70ad21972b88700338f32b` (16x16 8-bit gradient), `f949f7422913e83dff07ee5e0a5087d3` (8x8 8-bit constant), `ae7233a94558978934469dcc4da764dd` (8x8 8-bit gradient), `09223b227f3abc3134d0a3ea15f70c0a` (8x8 10-bit constant), `539aab0e6e14bcaec271febfa8e25444` (8x8 10-bit gradient), `73117a8fc102e5d028f82444fc4d15ab` (8x8 12-bit constant), and `0a7c7e058d8f56e6f3685c8eb8c3ece0` (8x8 12-bit gradient). This is an independently decodable baseline, not completion evidence for chroma, alpha, options, containers, or the public encoder. - [x] The exact net11 Release rebuild completed at the established 1,005-warning repository baseline with zero errors. The complete HEIF/AV1 namespace passes 8,838 of 8,838 direct VSTest cases with zero failures or skips. Roslynk reports zero compiler errors and no diagnostics in the five changed C# files; `git diff --check` passes and `.gitattributes` is unchanged. +- [~] The same internal frame operation now produces 4:2:0, 4:2:2, and 4:4:4 payloads at 8, 10, and 12 bits. Twenty-one color cases cover constant and spatially varying input at aligned dimensions plus odd 13x11 visible dimensions for every chroma geometry. The production decoder consumes every payload, the decoded output retains non-neutral chroma, and current-main `aomdec` accepts all 29 monochrome and color outputs. The odd-dimension decoded-frame MD5 values are `d025030a44750e703bcca9a6d3e131e8` (4:2:0), `75936a76104ec678c7a1f10b4c560172` (4:2:2), and `044026b1518aa94f3c2db82c7247c0d6` (4:4:4). This proves legal current-libaom payload syntax across native plane geometries; it does not yet prove target quality or native-plane equality with an independently encoded reference. +- [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. ### 7. Write complete AVIF output diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs index a66fbba1dd..7261b8cc19 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs @@ -12,30 +12,70 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; public class Av1EncoderFrameTests { + private const int EightBit = (int)Av1BitDepth.EightBit; + private const int TenBit = (int)Av1BitDepth.TenBit; + private const int TwelveBit = (int)Av1BitDepth.TwelveBit; + private const int Yuv400 = (int)Av1ColorFormat.Yuv400; + private const int Yuv420 = (int)Av1ColorFormat.Yuv420; + private const int Yuv422 = (int)Av1ColorFormat.Yuv422; + private const int Yuv444 = (int)Av1ColorFormat.Yuv444; + [Theory] - [InlineData(8, 8, false, 0)] - [InlineData(8, 8, true, 0)] - [InlineData(16, 16, false, 0)] - [InlineData(16, 16, true, 0)] - [InlineData(8, 8, false, 1)] - [InlineData(8, 8, true, 1)] - [InlineData(8, 8, false, 2)] - [InlineData(8, 8, true, 2)] - public void EncodeWritesReducedStillPictureConsumedByProductionDecoder(int width, int height, bool hasGradient, int bitDepthValue) + [InlineData(8, 8, false, EightBit, Yuv400)] + [InlineData(8, 8, true, EightBit, Yuv400)] + [InlineData(16, 16, false, EightBit, Yuv400)] + [InlineData(16, 16, true, EightBit, Yuv400)] + [InlineData(8, 8, false, TenBit, Yuv400)] + [InlineData(8, 8, true, TenBit, Yuv400)] + [InlineData(8, 8, false, TwelveBit, Yuv400)] + [InlineData(8, 8, true, TwelveBit, Yuv400)] + [InlineData(16, 16, false, EightBit, Yuv420)] + [InlineData(16, 16, true, EightBit, Yuv420)] + [InlineData(13, 11, true, EightBit, Yuv420)] + [InlineData(16, 16, false, TenBit, Yuv420)] + [InlineData(16, 16, true, TenBit, Yuv420)] + [InlineData(16, 16, false, TwelveBit, Yuv420)] + [InlineData(16, 16, true, TwelveBit, Yuv420)] + [InlineData(16, 16, false, EightBit, Yuv422)] + [InlineData(16, 16, true, EightBit, Yuv422)] + [InlineData(13, 11, true, EightBit, Yuv422)] + [InlineData(16, 16, false, TenBit, Yuv422)] + [InlineData(16, 16, true, TenBit, Yuv422)] + [InlineData(16, 16, false, TwelveBit, Yuv422)] + [InlineData(16, 16, true, TwelveBit, Yuv422)] + [InlineData(16, 16, false, EightBit, Yuv444)] + [InlineData(16, 16, true, EightBit, Yuv444)] + [InlineData(13, 11, true, EightBit, Yuv444)] + [InlineData(16, 16, false, TenBit, Yuv444)] + [InlineData(16, 16, true, TenBit, Yuv444)] + [InlineData(16, 16, false, TwelveBit, Yuv444)] + [InlineData(16, 16, true, TwelveBit, Yuv444)] + public void EncodeWritesReducedStillPictureConsumedByProductionDecoder(int width, int height, bool hasGradient, int bitDepthValue, int colorFormatValue) { Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue; 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++) { - byte value = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)128; - row[x] = new Rgba32(value, value, value); + if (colorFormat == Av1ColorFormat.Yuv400) + { + byte value = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)128; + row[x] = new Rgba32(value, value, value); + } + else + { + byte red = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)192; + byte green = hasGradient ? (byte)((x * 7) + (y * 5)) : (byte)64; + byte blue = hasGradient ? (byte)((x * 3) + (y * 11)) : (byte)32; + row[x] = new Rgba32(red, green, blue); + } } } - ObuColorConfig colorConfig = CreateMonochromeColorConfig(bitDepth); + ObuColorConfig colorConfig = CreateColorConfig(bitDepth, colorFormat); using MemoryStream stream = new(); ObuSequenceHeader encodedHeader = Av1FrameEncoder.Encode( Configuration.Default, @@ -54,7 +94,8 @@ public class Av1EncoderFrameTests Directory.CreateDirectory(outputDirectory); string contentName = hasGradient ? "gradient" : "constant"; int bitCount = bitDepth.GetBitCount(); - string fileName = $"encoder-frame-{width}x{height}-{bitCount}b-400-{contentName}.obu"; + string colorName = colorFormat.ToString()[3..]; + string fileName = $"encoder-frame-{width}x{height}-{bitCount}b-{colorName}-{contentName}.obu"; File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload); using Av1Decoder decoder = new(Configuration.Default); @@ -62,30 +103,44 @@ public class Av1EncoderFrameTests Assert.Equal(width, decoded.Width); Assert.Equal(height, decoded.Height); - ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit + ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit || colorFormat == Av1ColorFormat.Yuv422 ? ObuSequenceProfile.Professional - : ObuSequenceProfile.Main; + : colorFormat == Av1ColorFormat.Yuv444 + ? ObuSequenceProfile.High + : ObuSequenceProfile.Main; Assert.Equal(expectedProfile, encodedHeader.SequenceProfile); Assert.True(encodedHeader.IsReducedStillPictureHeader); Rgba32 first = decoded[0, 0]; - Assert.Equal(first.R, first.G); - Assert.Equal(first.R, first.B); Assert.Equal(byte.MaxValue, first.A); - if (hasGradient) + if (colorFormat == Av1ColorFormat.Yuv400) { - Assert.True(first.R < decoded[width - 1, height - 1].R); + Assert.Equal(first.R, first.G); + Assert.Equal(first.R, first.B); } else { - Assert.InRange(first.R, 120, 136); + Rgba32 center = decoded[width / 2, height / 2]; + Assert.True(center.R != center.G || center.G != center.B); + } - for (int y = 0; y < height; y++) + if (hasGradient) + { + Assert.NotEqual(first, decoded[width - 1, height - 1]); + } + else + { + if (colorFormat == Av1ColorFormat.Yuv400) { - foreach (Rgba32 pixel in decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y)) + Assert.InRange(first.R, 120, 136); + + for (int y = 0; y < height; y++) { - Assert.Equal(first, pixel); + foreach (Rgba32 pixel in decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y)) + { + Assert.Equal(first, pixel); + } } } } @@ -113,7 +168,7 @@ public class Av1EncoderFrameTests 0, 0); - ObuColorConfig colorConfig = CreateMonochromeColorConfig(Av1BitDepth.EightBit); + ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit); Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig); @@ -143,7 +198,7 @@ public class Av1EncoderFrameTests 0, 0); - ObuColorConfig colorConfig = CreateMonochromeColorConfig(Av1BitDepth.TenBit); + ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.TenBit); Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig); @@ -233,17 +288,19 @@ public class Av1EncoderFrameTests Assert.Equal(allocation.AllocationId, returned.AllocationId); } - private static ObuColorConfig CreateMonochromeColorConfig(Av1BitDepth bitDepth) + private static ObuColorConfig CreateColorConfig( + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400) => new() { IsColorDescriptionPresent = true, - IsMonochrome = true, + IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, ColorPrimaries = ObuColorPrimaries.Bt601, TransferCharacteristics = ObuTransferCharacteristics.Bt601, MatrixCoefficients = ObuMatrixCoefficients.Bt601, ColorRange = true, - SubSamplingX = true, - SubSamplingY = true, + SubSamplingX = colorFormat != Av1ColorFormat.Yuv444, + SubSamplingY = colorFormat == Av1ColorFormat.Yuv400 || colorFormat == Av1ColorFormat.Yuv420, ChromaSamplePosition = ObuChromoSamplePosition.Unknown, BitDepth = bitDepth }; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs index 12297cabd2..e57c916f5e 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs @@ -39,6 +39,7 @@ public class Av1TransformBlockEncoderTests { const int SourceStride = 13; const int ReconstructionStride = 15; + const byte PaddingSentinel = 176; Av1TransformSize transformSize = Av1TransformSize.Size8x8; int width = transformSize.GetWidth(); int height = transformSize.GetHeight(); @@ -68,6 +69,7 @@ public class Av1TransformBlockEncoderTests 0, 0); + reconstructionFrame.Luma.DangerousGetSingleSpan().Fill(PaddingSentinel); Buffer2DRegion sourcePlane = sourceFrame.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion reconstructionPlane = reconstructionFrame.Frame.CodedView.GetPlane(Av1Plane.Y); using Av1EncoderBlockWorkspace expectedWorkspace = new(Configuration.Default); @@ -171,8 +173,8 @@ public class Av1TransformBlockEncoderTests Assert.Equal(expectedQuantized, actualQuantized); Assert.Equal(expectedState.EndOfBlock, actualState.EndOfBlock); Assert.Equal(expectedState.TransformType, actualState.TransformType); - Assert.Equal(0, completeRow[physicalColumn - 1]); - Assert.Equal(0, completeRow[physicalColumn + width]); + Assert.Equal(PaddingSentinel, completeRow[physicalColumn - 1]); + Assert.Equal(PaddingSentinel, completeRow[physicalColumn + width]); } ///