mirror of https://github.com/SixLabors/ImageSharp
14 changed files with 281 additions and 4 deletions
@ -0,0 +1,221 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers.Binary; |
|||
using SixLabors.ImageSharp.Formats; |
|||
using SixLabors.ImageSharp.Formats.Heif; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Validates complete AV1 reconstruction against independently decoded native component planes.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1DeblockingConformanceTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies deblocking syntax, filter activation, component traversal, and presentation for real eight-, ten-,
|
|||
/// and twelve-bit AV1 and AVIF content.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void DecodeMatchesPinnedLibaomReference() |
|||
{ |
|||
ValidateFixture( |
|||
TestImages.Heif.Av1Deblocking8BitAvif, |
|||
TestImages.Heif.Av1Deblocking8BitPayload, |
|||
TestImages.Heif.Av1Deblocking8BitReference, |
|||
768, |
|||
512, |
|||
Av1BitDepth.EightBit, |
|||
Av1ColorFormat.Yuv420, |
|||
HeifBitDepth.Bit8); |
|||
|
|||
ValidateFixture( |
|||
TestImages.Heif.Av1Deblocking10BitAvif, |
|||
TestImages.Heif.Av1Deblocking10BitPayload, |
|||
TestImages.Heif.Av1Deblocking10BitReference, |
|||
1024, |
|||
428, |
|||
Av1BitDepth.TenBit, |
|||
Av1ColorFormat.Yuv444, |
|||
HeifBitDepth.Bit10); |
|||
|
|||
ValidateNativeFixture( |
|||
TestImages.Heif.Av1Deblocking12BitPayload, |
|||
TestImages.Heif.Av1Deblocking12BitReference, |
|||
1024, |
|||
428, |
|||
Av1BitDepth.TwelveBit, |
|||
Av1ColorFormat.Yuv444); |
|||
|
|||
ValidatePresentedImage(TestImages.Heif.Av1Deblocking12BitAvif, 64, 64, HeifBitDepth.Bit12); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Validates one elementary-stream sample and its containing AVIF image.
|
|||
/// </summary>
|
|||
/// <param name="imagePath">The complete AVIF container.</param>
|
|||
/// <param name="payloadPath">The AV1 elementary-stream sample extracted from the container.</param>
|
|||
/// <param name="referencePath">The native planar output produced by the pinned libaom decoder.</param>
|
|||
/// <param name="width">The expected displayed width.</param>
|
|||
/// <param name="height">The expected displayed height.</param>
|
|||
/// <param name="bitDepth">The expected AV1 sample precision.</param>
|
|||
/// <param name="colorFormat">The expected native chroma-sampling layout.</param>
|
|||
/// <param name="metadataBitDepth">The expected public HEIF sample precision.</param>
|
|||
private static void ValidateFixture( |
|||
string imagePath, |
|||
string payloadPath, |
|||
string referencePath, |
|||
int width, |
|||
int height, |
|||
Av1BitDepth bitDepth, |
|||
Av1ColorFormat colorFormat, |
|||
HeifBitDepth metadataBitDepth) |
|||
{ |
|||
ValidateNativeFixture(payloadPath, referencePath, width, height, bitDepth, colorFormat); |
|||
ValidatePresentedImage(imagePath, width, height, metadataBitDepth); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Validates complete native-plane reconstruction for one AV1 elementary-stream sample.
|
|||
/// </summary>
|
|||
/// <param name="payloadPath">The AV1 elementary-stream sample.</param>
|
|||
/// <param name="referencePath">The native planar output produced by the pinned libaom decoder.</param>
|
|||
/// <param name="width">The expected reconstructed width.</param>
|
|||
/// <param name="height">The expected reconstructed height.</param>
|
|||
/// <param name="bitDepth">The expected AV1 sample precision.</param>
|
|||
/// <param name="colorFormat">The expected native chroma-sampling layout.</param>
|
|||
private static void ValidateNativeFixture( |
|||
string payloadPath, |
|||
string referencePath, |
|||
int width, |
|||
int height, |
|||
Av1BitDepth bitDepth, |
|||
Av1ColorFormat colorFormat) |
|||
{ |
|||
byte[] payload = TestFile.Create(payloadPath).Bytes; |
|||
byte[] reference = TestFile.Create(referencePath).Bytes; |
|||
using Av1Decoder decoder = new(Configuration.Default); |
|||
using Av1FrameBuffer<byte> frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); |
|||
|
|||
Assert.Equal(width, frameBuffer.Width); |
|||
Assert.Equal(height, frameBuffer.Height); |
|||
Assert.Equal(bitDepth, frameBuffer.BitDepth); |
|||
Assert.Equal(colorFormat, frameBuffer.ColorFormat); |
|||
Assert.NotNull(decoder.FrameHeader); |
|||
ObuLoopFilterParameters filterParameters = decoder.FrameHeader.LoopFilterParameters; |
|||
Assert.True( |
|||
filterParameters.FilterLevel[0] != 0 |
|||
|| filterParameters.FilterLevel[1] != 0 |
|||
|| filterParameters.FilterLevelU != 0 |
|||
|| filterParameters.FilterLevelV != 0); |
|||
|
|||
AssertNativePlanesEqual(frameBuffer, reference); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Validates the public presentation and metadata produced from one complete AVIF container.
|
|||
/// </summary>
|
|||
/// <param name="imagePath">The complete AVIF container.</param>
|
|||
/// <param name="width">The expected displayed width.</param>
|
|||
/// <param name="height">The expected displayed height.</param>
|
|||
/// <param name="metadataBitDepth">The expected public HEIF sample precision.</param>
|
|||
private static void ValidatePresentedImage(string imagePath, int width, int height, HeifBitDepth metadataBitDepth) |
|||
{ |
|||
DecoderOptions options = new() { MaxFrames = 1 }; |
|||
byte[] imageBytes = TestFile.Create(imagePath).Bytes; |
|||
using Image<Rgba64> image = Image.Load<Rgba64>(options, imageBytes); |
|||
|
|||
Assert.Equal(width, image.Width); |
|||
Assert.Equal(height, image.Height); |
|||
Assert.Single(image.Frames); |
|||
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); |
|||
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); |
|||
Assert.Equal(metadataBitDepth, metadata.BitDepth); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compares every visible native component sample with the independent planar reference.
|
|||
/// </summary>
|
|||
/// <param name="frameBuffer">The reconstructed AV1 component planes.</param>
|
|||
/// <param name="reference">The planar Y, U, and V samples produced by the pinned libaom decoder.</param>
|
|||
private static void AssertNativePlanesEqual(Av1FrameBuffer<byte> frameBuffer, ReadOnlySpan<byte> reference) |
|||
{ |
|||
(int chromaSubsamplingX, int chromaSubsamplingY) = frameBuffer.ColorFormat switch |
|||
{ |
|||
Av1ColorFormat.Yuv420 => (1, 1), |
|||
Av1ColorFormat.Yuv422 => (1, 0), |
|||
_ => (0, 0) |
|||
}; |
|||
|
|||
int referenceOffset = 0; |
|||
foreach (Av1Plane plane in new[] { Av1Plane.Y, Av1Plane.U, Av1Plane.V }) |
|||
{ |
|||
int subsamplingX = plane == Av1Plane.Y ? 0 : chromaSubsamplingX; |
|||
int subsamplingY = plane == Av1Plane.Y ? 0 : chromaSubsamplingY; |
|||
int planeWidth = GetSubsampledSize(frameBuffer.Width, subsamplingX); |
|||
int planeHeight = GetSubsampledSize(frameBuffer.Height, subsamplingY); |
|||
|
|||
if (frameBuffer.BitDepth == Av1BitDepth.EightBit) |
|||
{ |
|||
Buffer2DRegion<byte> actualPlane = frameBuffer.DeriveBlockPointer(plane, subsamplingX, subsamplingY); |
|||
for (int y = 0; y < planeHeight; y++) |
|||
{ |
|||
Span<byte> actualRow = actualPlane.DangerousGetRowSpan(y)[..planeWidth]; |
|||
ReadOnlySpan<byte> expectedRow = reference.Slice(referenceOffset, planeWidth); |
|||
for (int x = 0; x < planeWidth; x++) |
|||
{ |
|||
AssertSampleEqual(plane, x, y, expectedRow[x], actualRow[x]); |
|||
} |
|||
|
|||
referenceOffset += planeWidth; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// aomdec writes high-bit-depth YUV as little-endian 16-bit values, independently of host endianness.
|
|||
for (int y = 0; y < planeHeight; y++) |
|||
{ |
|||
Span<ushort> actualRow = frameBuffer.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY); |
|||
for (int x = 0; x < planeWidth; x++) |
|||
{ |
|||
ushort expected = BinaryPrimitives.ReadUInt16LittleEndian(reference.Slice(referenceOffset, sizeof(ushort))); |
|||
AssertSampleEqual(plane, x, y, expected, actualRow[x]); |
|||
referenceOffset += sizeof(ushort); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Assert.Equal(reference.Length, referenceOffset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates a component dimension after chroma subsampling with the AV1 rounding rule.
|
|||
/// </summary>
|
|||
/// <param name="size">The luma dimension.</param>
|
|||
/// <param name="subsampling">The component subsampling shift.</param>
|
|||
/// <returns>The subsampled component dimension.</returns>
|
|||
private static int GetSubsampledSize(int size, int subsampling) |
|||
=> (size + (1 << subsampling) - 1) >> subsampling; |
|||
|
|||
/// <summary>
|
|||
/// Reports the exact component coordinate when independently decoded samples differ.
|
|||
/// </summary>
|
|||
/// <param name="plane">The compared component plane.</param>
|
|||
/// <param name="x">The sample X coordinate.</param>
|
|||
/// <param name="y">The sample Y coordinate.</param>
|
|||
/// <param name="expected">The reference sample.</param>
|
|||
/// <param name="actual">The reconstructed sample.</param>
|
|||
private static void AssertSampleEqual(Av1Plane plane, int x, int y, ushort expected, ushort actual) |
|||
{ |
|||
if (expected != actual) |
|||
{ |
|||
Assert.Fail($"Plane {plane} differs at ({x}, {y}): expected {expected}, actual {actual}."); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
# AV1 deblocking conformance fixtures |
|||
|
|||
The AVIF files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. They retain the licenses recorded in libavif's `tests/data/README.md`: the Kodak image is released for unrestricted use, the Cosmos Laundromat frame uses CC BY 3.0, and the libavif color animation is distributed with the libavif test corpus under its BSD-2-Clause license. |
|||
|
|||
The 8- and 10-bit `.bit` files contain the exact AV1 item payloads from the corresponding AVIF files. Each still file has one item occupying the complete `mdat` payload. The genuine 12-bit libavif sequence is retained for container, presentation, alpha, and metadata coverage, but its first color frame disables deblocking and therefore cannot prove the 12-bit filter path. |
|||
|
|||
`libaom-cosmos1650-12b.bit` was encoded from libavif's real 10-bit 4:4:4 Cosmos Laundromat Y4M source with the pinned libaom encoder. libaom promotes the input samples to a 12-bit AV1 profile-2 still-picture stream. The constant-quality level is deliberately lossy so the frame signals nonzero loop-filter levels. The material command options were `--usage=2 --passes=1 --limit=1 --obu --bit-depth=12 --input-bit-depth=10 --profile=2 --end-usage=q --cq-level=30 --cpu-used=6 --threads=1 --lag-in-frames=0 --full-still-picture-hdr`. |
|||
|
|||
The `_libaom.yuv` files were decoded from those exact payloads with `aomdec` built from libaom commit `03087864cf4bea6abb0d28f95cf7843511413d8f`. The reference build used `AOM_TARGET_CPU=generic`, so these files come from libaom's scalar decoder rather than ImageSharp or an architecture-specific implementation. |
|||
|
|||
The native reference layouts are: |
|||
|
|||
- `libavif-kodim23-8b-libaom.yuv`: 768x512, 8-bit YUV 4:2:0, planar Y/U/V. |
|||
- `libavif-cosmos1650-10b-libaom.yuv`: 1024x428, 10-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
- `libaom-cosmos1650-12b-libaom.yuv`: 1024x428, 12-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
|
|||
The conformance test compares every visible reconstructed sample with these files. It also verifies that the parsed frame enables deblocking, so a disabled or bypassed loop-filter stage cannot satisfy the test accidentally. |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:ef712be32af7cf0a95c5c41bdcc51afc05a4ab7c047383f5f65edad2bb986712 |
|||
size 2629632 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a45f9653255c1a660906554bafcc13fdee04f88cc6d0e08e1efa169cf0c6ddd2 |
|||
size 23769 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3bf9f91da471749e7df639ba7945d4d94c1c3e3968c26f3619fbbcfc92790576 |
|||
size 2267 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:9a59dd92a0c579f942acca8281ebd0465dc848be200a4d2ff57eaff589445f6c |
|||
size 2629632 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1c3db1867051ae23ba61ed217f6b7372a4248e6322d76a239feab14bc4c55ab5 |
|||
size 37451 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:f930bf11eb2f61bf4ee0fe61853cd387a4c3dbe53503707db7b2c59d9d273ff3 |
|||
size 37169 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8dde2eec742c39f0579c29ae84cba0fe01522a9008adcb2cffccec0295d18141 |
|||
size 589824 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fa97744b1bad2137cac0f81fb653175362e16c6d8d20613d0769a847f2769bd1 |
|||
size 21032 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b7b1d3f85a870475acf579fbb7a0b59ff94c30f84b3f3a066411a50f9ba1bd20 |
|||
size 20750 |
|||
Loading…
Reference in new issue