Browse Source

Verify AV1 native color payloads

pull/2633/head
James Jackson-South 1 month ago
parent
commit
67b92d735b
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 115
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  3. 6
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

2
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 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. - [~] 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. - [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 ### 7. Write complete AVIF output

115
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -12,30 +12,70 @@ namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
public class Av1EncoderFrameTests 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] [Theory]
[InlineData(8, 8, false, 0)] [InlineData(8, 8, false, EightBit, Yuv400)]
[InlineData(8, 8, true, 0)] [InlineData(8, 8, true, EightBit, Yuv400)]
[InlineData(16, 16, false, 0)] [InlineData(16, 16, false, EightBit, Yuv400)]
[InlineData(16, 16, true, 0)] [InlineData(16, 16, true, EightBit, Yuv400)]
[InlineData(8, 8, false, 1)] [InlineData(8, 8, false, TenBit, Yuv400)]
[InlineData(8, 8, true, 1)] [InlineData(8, 8, true, TenBit, Yuv400)]
[InlineData(8, 8, false, 2)] [InlineData(8, 8, false, TwelveBit, Yuv400)]
[InlineData(8, 8, true, 2)] [InlineData(8, 8, true, TwelveBit, Yuv400)]
public void EncodeWritesReducedStillPictureConsumedByProductionDecoder(int width, int height, bool hasGradient, int bitDepthValue) [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; Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
using Image<Rgba32> source = new(width, height); using Image<Rgba32> source = new(width, height);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
byte value = hasGradient ? (byte)((x * 13) + (y * 17)) : (byte)128; if (colorFormat == Av1ColorFormat.Yuv400)
row[x] = new Rgba32(value, value, value); {
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(); using MemoryStream stream = new();
ObuSequenceHeader encodedHeader = Av1FrameEncoder.Encode( ObuSequenceHeader encodedHeader = Av1FrameEncoder.Encode(
Configuration.Default, Configuration.Default,
@ -54,7 +94,8 @@ public class Av1EncoderFrameTests
Directory.CreateDirectory(outputDirectory); Directory.CreateDirectory(outputDirectory);
string contentName = hasGradient ? "gradient" : "constant"; string contentName = hasGradient ? "gradient" : "constant";
int bitCount = bitDepth.GetBitCount(); 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); File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
@ -62,30 +103,44 @@ public class Av1EncoderFrameTests
Assert.Equal(width, decoded.Width); Assert.Equal(width, decoded.Width);
Assert.Equal(height, decoded.Height); Assert.Equal(height, decoded.Height);
ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit || colorFormat == Av1ColorFormat.Yuv422
? ObuSequenceProfile.Professional ? ObuSequenceProfile.Professional
: ObuSequenceProfile.Main; : colorFormat == Av1ColorFormat.Yuv444
? ObuSequenceProfile.High
: ObuSequenceProfile.Main;
Assert.Equal(expectedProfile, encodedHeader.SequenceProfile); Assert.Equal(expectedProfile, encodedHeader.SequenceProfile);
Assert.True(encodedHeader.IsReducedStillPictureHeader); Assert.True(encodedHeader.IsReducedStillPictureHeader);
Rgba32 first = decoded[0, 0]; Rgba32 first = decoded[0, 0];
Assert.Equal(first.R, first.G);
Assert.Equal(first.R, first.B);
Assert.Equal(byte.MaxValue, first.A); 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 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,
0); 0);
ObuColorConfig colorConfig = CreateMonochromeColorConfig(Av1BitDepth.EightBit); ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit);
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig); Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
@ -143,7 +198,7 @@ public class Av1EncoderFrameTests
0, 0,
0); 0);
ObuColorConfig colorConfig = CreateMonochromeColorConfig(Av1BitDepth.TenBit); ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.TenBit);
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig); Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
@ -233,17 +288,19 @@ public class Av1EncoderFrameTests
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.AllocationId, returned.AllocationId);
} }
private static ObuColorConfig CreateMonochromeColorConfig(Av1BitDepth bitDepth) private static ObuColorConfig CreateColorConfig(
Av1BitDepth bitDepth,
Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400)
=> new() => new()
{ {
IsColorDescriptionPresent = true, IsColorDescriptionPresent = true,
IsMonochrome = true, IsMonochrome = colorFormat == Av1ColorFormat.Yuv400,
ColorPrimaries = ObuColorPrimaries.Bt601, ColorPrimaries = ObuColorPrimaries.Bt601,
TransferCharacteristics = ObuTransferCharacteristics.Bt601, TransferCharacteristics = ObuTransferCharacteristics.Bt601,
MatrixCoefficients = ObuMatrixCoefficients.Bt601, MatrixCoefficients = ObuMatrixCoefficients.Bt601,
ColorRange = true, ColorRange = true,
SubSamplingX = true, SubSamplingX = colorFormat != Av1ColorFormat.Yuv444,
SubSamplingY = true, SubSamplingY = colorFormat == Av1ColorFormat.Yuv400 || colorFormat == Av1ColorFormat.Yuv420,
ChromaSamplePosition = ObuChromoSamplePosition.Unknown, ChromaSamplePosition = ObuChromoSamplePosition.Unknown,
BitDepth = bitDepth BitDepth = bitDepth
}; };

6
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

@ -39,6 +39,7 @@ public class Av1TransformBlockEncoderTests
{ {
const int SourceStride = 13; const int SourceStride = 13;
const int ReconstructionStride = 15; const int ReconstructionStride = 15;
const byte PaddingSentinel = 176;
Av1TransformSize transformSize = Av1TransformSize.Size8x8; Av1TransformSize transformSize = Av1TransformSize.Size8x8;
int width = transformSize.GetWidth(); int width = transformSize.GetWidth();
int height = transformSize.GetHeight(); int height = transformSize.GetHeight();
@ -68,6 +69,7 @@ public class Av1TransformBlockEncoderTests
0, 0,
0); 0);
reconstructionFrame.Luma.DangerousGetSingleSpan().Fill(PaddingSentinel);
Buffer2DRegion<byte> sourcePlane = sourceFrame.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> sourcePlane = sourceFrame.Frame.CodedView.GetPlane(Av1Plane.Y);
Buffer2DRegion<byte> reconstructionPlane = reconstructionFrame.Frame.CodedView.GetPlane(Av1Plane.Y); Buffer2DRegion<byte> reconstructionPlane = reconstructionFrame.Frame.CodedView.GetPlane(Av1Plane.Y);
using Av1EncoderBlockWorkspace expectedWorkspace = new(Configuration.Default); using Av1EncoderBlockWorkspace expectedWorkspace = new(Configuration.Default);
@ -171,8 +173,8 @@ public class Av1TransformBlockEncoderTests
Assert.Equal(expectedQuantized, actualQuantized); Assert.Equal(expectedQuantized, actualQuantized);
Assert.Equal(expectedState.EndOfBlock, actualState.EndOfBlock); Assert.Equal(expectedState.EndOfBlock, actualState.EndOfBlock);
Assert.Equal(expectedState.TransformType, actualState.TransformType); Assert.Equal(expectedState.TransformType, actualState.TransformType);
Assert.Equal(0, completeRow[physicalColumn - 1]); Assert.Equal(PaddingSentinel, completeRow[physicalColumn - 1]);
Assert.Equal(0, completeRow[physicalColumn + width]); Assert.Equal(PaddingSentinel, completeRow[physicalColumn + width]);
} }
/// <summary> /// <summary>

Loading…
Cancel
Save