📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

953 lines
36 KiB

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Memory;
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, 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<Rgba32> source = new(width, height);
for (int y = 0; y < height; y++)
{
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < width; x++)
{
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 = CreateColorConfig(bitDepth, colorFormat);
using MemoryStream stream = new();
ObuSequenceHeader encodedHeader = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
string contentName = hasGradient ? "gradient" : "constant";
int bitCount = bitDepth.GetBitCount();
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);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
Assert.Equal(width, decoded.Width);
Assert.Equal(height, decoded.Height);
ObuSequenceProfile expectedProfile = bitDepth == Av1BitDepth.TwelveBit || colorFormat == Av1ColorFormat.Yuv422
? ObuSequenceProfile.Professional
: colorFormat == Av1ColorFormat.Yuv444
? ObuSequenceProfile.High
: ObuSequenceProfile.Main;
Assert.Equal(expectedProfile, encodedHeader.SequenceProfile);
Assert.True(encodedHeader.IsReducedStillPictureHeader);
Rgba32 first = decoded[0, 0];
Assert.Equal(byte.MaxValue, first.A);
if (colorFormat == Av1ColorFormat.Yuv400)
{
Assert.Equal(first.R, first.G);
Assert.Equal(first.R, first.B);
}
else
{
Rgba32 center = decoded[width / 2, height / 2];
Assert.True(center.R != center.G || center.G != center.B);
}
if (hasGradient)
{
Assert.NotEqual(first, decoded[width - 1, height - 1]);
}
else
{
if (colorFormat == Av1ColorFormat.Yuv400)
{
Assert.InRange(first.R, 120, 136);
for (int y = 0; y < height; y++)
{
foreach (Rgba32 pixel in decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))
{
Assert.Equal(first, pixel);
}
}
}
}
}
[Theory]
[InlineData(EightBit)]
[InlineData(TenBit)]
[InlineData(TwelveBit)]
public void EncodeAlphaWritesMonochromeReducedStillPicture(int bitDepthValue)
{
const int Width = 16;
const int Height = 16;
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
using Image<Rgba64> source = new(Width, Height);
for (int y = 0; y < Height; y++)
{
Span<Rgba64> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < Width; x++)
{
ushort alpha = (ushort)(((x + y) * ushort.MaxValue) / (Width + Height - 2));
row[x] = new Rgba64(ushort.MaxValue, 0, 0, alpha);
}
}
using MemoryStream stream = new();
ObuSequenceHeader encodedHeader = Av1FrameEncoder.EncodeAlpha(
Configuration.Default,
source.Frames.RootFrame,
stream,
CreateColorConfig(bitDepth),
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba64> decoded = decoder.Decode<Rgba64>(payload);
Assert.True(encodedHeader.ColorConfig.IsMonochrome);
Assert.Equal(
bitDepth == Av1BitDepth.TwelveBit ? ObuSequenceProfile.Professional : ObuSequenceProfile.Main,
encodedHeader.SequenceProfile);
Assert.Equal(new Size(Width, Height), decoded.Size);
Assert.True(decoded[0, 0].R < decoded[Width - 1, Height - 1].R);
Assert.Equal(decoded[0, 0].R, decoded[0, 0].G);
Assert.Equal(decoded[0, 0].R, decoded[0, 0].B);
Assert.Equal(ushort.MaxValue, decoded[0, 0].A);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-alpha-{Width}x{Height}-{bitDepth.GetBitCount()}b.obu"),
payload);
}
[Fact]
public void AlphaConversionUsesOnePooledRowAndPreservesTwelveBitPrecision()
{
const int Width = 19;
const int Border = Av1EncoderFrame<ushort>.LumaBorder;
using Image<Rgba64> image = new(Width, 1);
ushort[] expected = new ushort[Width];
for (int x = 0; x < Width; x++)
{
ushort alpha = (ushort)((x * (long)ushort.MaxValue) / (Width - 1));
image[x, 0] = new Rgba64(0, 0, 0, alpha);
expected[x] = (ushort)(((alpha * 4095L) + (ushort.MaxValue / 2)) / ushort.MaxValue);
}
using Av1EncoderFrameBuffer<ushort> frameBuffer = new(
Configuration.Default,
Width,
1,
12,
Av1ColorFormat.Yuv400,
0,
0);
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
HeifPlanarAlphaEncoder.Convert<
Rgba64,
Av1EncoderFrame<ushort>.PlanarView,
ushort,
HeifUShortSampleConverter>(
configuration,
image.Frames.RootFrame,
frameBuffer.Frame.View);
frameBuffer.Frame.ExtendBorders();
AssertReplicatedSingleRow(frameBuffer.Luma, Border, expected);
TestMemoryAllocator.AllocationRequest allocation = Assert.Single(allocator.AllocationLog);
Assert.Equal(typeof(float), allocation.ElementType);
Assert.Equal(Width * 3, allocation.Length);
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId);
}
[Theory]
[InlineData(EightBit, Yuv400, 0x1F, 0x1C)]
[InlineData(TenBit, Yuv420, 0x1F, 0x4C)]
[InlineData(TenBit, Yuv444, 0x3F, 0x40)]
[InlineData(TwelveBit, Yuv422, 0x5F, 0x68)]
public void CodecConfigurationWritesFixedHeaderFromEncodedSequenceHeader(
int bitDepthValue,
int colorFormatValue,
byte expectedProfileAndLevel,
byte expectedColorFlags)
{
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
using Image<Rgba32> source = new(8, 8);
using MemoryStream stream = new();
ObuSequenceHeader sequenceHeader = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
CreateColorConfig(bitDepth, colorFormat),
qIndex: 37,
effort: 5);
Av1CodecConfiguration configuration = new(sequenceHeader);
byte[] fixedHeader = new byte[Av1CodecConfiguration.FixedHeaderSize];
configuration.WriteFixedHeader(fixedHeader);
Assert.Equal([0x81, expectedProfileAndLevel, expectedColorFlags, 0x00], fixedHeader);
Av1CodecConfiguration parsed = new(fixedHeader, new DecoderOptions());
Assert.True(configuration.HasMatchingImageConfiguration(parsed));
parsed.Validate(sequenceHeader);
}
[Fact]
public void ScreenContentDetectorMatchesLibaomFeatureThresholds()
{
const int width = 160;
const int height = 16;
using Av1EncoderFrameBuffer<byte> byteFrame = new(
Configuration.Default,
width,
height,
8,
Av1ColorFormat.Yuv400,
0,
0);
for (int row = 0; row < height; row++)
{
Span<byte> samples = byteFrame.Frame.View.GetLumaRowSpan(row)[..width];
samples.Fill(96);
for (int column = 0; column < 16; column++)
{
samples[column] = column < 8 ? (byte)32 : (byte)224;
}
}
// One qualifying block is exactly ten percent of this frame, and the reference threshold is strict.
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(byteFrame.Frame));
Av1ScreenContentDetector.Detect(
byteFrame.Frame,
out bool allowScreenContentTools,
out bool allowIntraBlockCopy);
Assert.False(allowScreenContentTools);
Assert.False(allowIntraBlockCopy);
for (int row = 0; row < height; row++)
{
Span<byte> samples = byteFrame.Frame.View.GetLumaRowSpan(row);
for (int column = 16; column < 32; column++)
{
samples[column] = column < 24 ? (byte)48 : (byte)208;
}
}
Assert.True(Av1ScreenContentDetector.IsPaletteLikely(byteFrame.Frame));
Av1ScreenContentDetector.Detect(
byteFrame.Frame,
out allowScreenContentTools,
out allowIntraBlockCopy);
Assert.True(allowScreenContentTools);
Assert.True(allowIntraBlockCopy);
using Av1EncoderFrameBuffer<ushort> highBitDepthFrame = new(
Configuration.Default,
16,
16,
10,
Av1ColorFormat.Yuv400,
0,
0);
for (int row = 0; row < 16; row++)
{
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
for (int column = 0; column < 16; column++)
{
samples[column] = column < 8 ? (ushort)128 : (ushort)131;
}
}
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
Av1ScreenContentDetector.Detect(
highBitDepthFrame.Frame,
out allowScreenContentTools,
out allowIntraBlockCopy);
Assert.False(allowScreenContentTools);
Assert.False(allowIntraBlockCopy);
for (int row = 0; row < 16; row++)
{
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
samples[8..16].Fill(640);
}
Assert.True(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
Av1ScreenContentDetector.Detect(
highBitDepthFrame.Frame,
out allowScreenContentTools,
out allowIntraBlockCopy);
Assert.True(allowScreenContentTools);
Assert.True(allowIntraBlockCopy);
for (int row = 0; row < 16; row++)
{
Span<ushort> samples = highBitDepthFrame.Frame.View.GetLumaRowSpan(row);
for (int column = 0; column < 16; column++)
{
samples[column] = (ushort)((column % 5) * 200);
}
}
Assert.False(Av1ScreenContentDetector.IsPaletteLikely(highBitDepthFrame.Frame));
Av1ScreenContentDetector.Detect(
highBitDepthFrame.Frame,
out allowScreenContentTools,
out allowIntraBlockCopy);
Assert.False(allowScreenContentTools);
Assert.False(allowIntraBlockCopy);
}
[Fact]
public void ScreenContentDetectorMatchesLibaomIntraBlockCopyVarianceThreshold()
{
const int Width = 16;
const int Height = 16;
using Av1EncoderFrameBuffer<byte> frame = new(
Configuration.Default,
Width,
Height,
8,
Av1ColorFormat.Yuv400,
0,
0);
Buffer2DRegion<byte> luma = frame.Frame.View.GetPlane(Av1Plane.Y);
for (int row = 0; row < Height; row++)
{
luma.DangerousGetRowSpan(row).Fill(96);
}
// A single delta of eleven leaves total variance below half a sample after per-pixel rounding.
luma.DangerousGetRowSpan(0)[0] = 107;
Av1ScreenContentDetector.Detect(
frame.Frame,
out bool allowScreenContentTools,
out bool allowIntraBlockCopy);
Assert.True(allowScreenContentTools);
Assert.False(allowIntraBlockCopy);
// Raising that delta to twelve crosses the exact integer rounding boundary used by libaom.
luma.DangerousGetRowSpan(0)[0] = 108;
Av1ScreenContentDetector.Detect(
frame.Frame,
out allowScreenContentTools,
out allowIntraBlockCopy);
Assert.True(allowScreenContentTools);
Assert.True(allowIntraBlockCopy);
}
[Fact]
public void EncodeActivatesScreenContentTools()
{
const int width = 16;
const int height = 16;
using Image<Rgba32> source = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = (((column >> 2) + (row >> 2)) & 1) == 0
? new Rgba32(224, 32, 32)
: new Rgba32(32, 32, 224);
}
}
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
using MemoryStream stream = new();
_ = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort: 5);
byte[] payload = stream.ToArray();
Av1BitStreamReader reader = new(payload);
Av1TileDecoderStub tileReader = new();
ObuReader obuReader = new();
obuReader.ReadAll(ref reader, payload.Length, () => tileReader);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(obuReader.FrameHeader);
Assert.True(frameHeader.AllowScreenContentTools);
Assert.True(frameHeader.AllowIntraBlockCopy);
using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
Assert.Equal(new Size(width, height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-frame-16x16-8b-444-palette.obu"), payload);
}
[Theory]
[InlineData(0, false, false, false)]
[InlineData(1, false, false, false)]
[InlineData(2, false, false, false)]
[InlineData(3, false, false, false)]
[InlineData(4, true, false, false)]
[InlineData(5, true, true, false)]
[InlineData(6, true, true, true)]
public void EncodeEffortControlsSearchFeatures(
int effort,
bool enableFilterIntra,
bool enableScreenContentTools,
bool selectTransformSize)
{
const int width = 16;
const int height = 16;
using Image<Rgba32> source = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = (((column >> 2) + (row >> 2)) & 1) == 0
? new Rgba32(224, 32, 32)
: new Rgba32(32, 32, 224);
}
}
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
using MemoryStream stream = new();
_ = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
Assert.Equal(enableFilterIntra, sequenceHeader.EnableFilterIntra);
Assert.Equal(enableScreenContentTools, frameHeader.AllowScreenContentTools);
Assert.Equal(enableScreenContentTools, frameHeader.AllowIntraBlockCopy);
Assert.Equal(
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
frameHeader.TransformMode);
Assert.Equal(new Size(width, height), decoded.Size);
int modeCount = 0;
foreach (Av1BlockModeInfo modeInfo in frameInfo.GetSuperblock(Point.Empty).GetModeInfos())
{
modeCount++;
if (effort == 0)
{
Assert.Equal(Av1PredictionMode.DC, modeInfo.YMode);
Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.UvMode);
}
if (effort <= 1)
{
Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.Y));
Assert.Equal(0, modeInfo.GetAngleDelta(Av1Plane.U));
}
if (effort < 4)
{
Assert.False(modeInfo.UseFilterIntra);
}
if (effort < 5)
{
Assert.False(modeInfo.UseIntraBlockCopy);
Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.Y));
Assert.Equal(0, modeInfo.GetPaletteSize(Av1Plane.U));
}
}
Assert.NotEqual(0, modeCount);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, $"encoder-frame-16x16-8b-444-effort-{effort}.obu"), payload);
}
[Fact]
public void EncodeEffortSixSelectsFourByFourLumaTransforms()
{
const int Width = 16;
const int Height = 16;
using Image<Rgba32> source = new(Width, Height);
for (int row = 0; row < Height; row++)
{
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < Width; column++)
{
byte value = (byte)(16 + ((((row >> 2) * 4) + (column >> 2)) * 14));
pixels[column] = new Rgba32(value, value, value);
}
}
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400);
using MemoryStream stream = new();
_ = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort: 6);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
using Image<L8> decoded = decoder.Decode<L8>(payload);
Assert.NotNull(decoder.FrameHeader);
Assert.Equal(Av1TransformMode.Select, decoder.FrameHeader.TransformMode);
Assert.NotNull(decoder.FrameInfo);
bool foundSplitTransform = false;
foreach (Av1BlockModeInfo modeInfo in decoder.FrameInfo.GetSuperblock(Point.Empty).GetModeInfos())
{
foundSplitTransform |= modeInfo.GetTransformUnitCount(Av1Plane.Y) == 4;
}
Assert.True(foundSplitTransform);
Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, "encoder-frame-16x16-8b-400-transform-size-select.obu"),
payload);
}
[Theory]
[InlineData(5, false)]
[InlineData(6, true)]
public void EncodeSelectsIntraBlockCopyForRepeatedScreenContent(
int effort,
bool selectTransformSize)
{
const int Width = 328;
const int Height = 16;
const ulong Pattern = 0xD6A5_3C97_E18B_4F20UL;
using Image<Rgba32> source = new(Width, Height);
for (int row = 0; row < Height; row++)
{
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < Width; column++)
{
int patternIndex = ((row & 7) * 8) + (column & 7);
pixels[column] = ((Pattern >> patternIndex) & 1) == 0
? new Rgba32(224, 32, 32)
: new Rgba32(32, 32, 224);
}
}
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv444);
using MemoryStream stream = new();
_ = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload);
Assert.NotNull(decoder.FrameHeader);
Assert.True(decoder.FrameHeader.AllowScreenContentTools);
Assert.True(decoder.FrameHeader.AllowIntraBlockCopy);
Assert.Equal(
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
decoder.FrameHeader.TransformMode);
Assert.NotNull(decoder.FrameInfo);
Av1SuperblockInfo targetSuperblock = decoder.FrameInfo.GetSuperblock(new Point(5, 0));
bool usesIntraBlockCopy = false;
foreach (Av1BlockModeInfo modeInfo in targetSuperblock.GetModeInfos())
{
usesIntraBlockCopy |= modeInfo.UseIntraBlockCopy;
}
Assert.True(usesIntraBlockCopy);
Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
string fileName = effort == 5
? "encoder-frame-328x16-8b-444-intrabc.obu"
: "encoder-frame-328x16-8b-444-intrabc-effort-6.obu";
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
}
[Fact]
public void PrepareSourceConvertsRgba32DirectlyIntoBorderedEightBitPlane()
{
const int width = 4;
const int height = 1;
const int border = Av1EncoderFrame<byte>.LumaBorder;
using Image<Rgba32> image = new(width, height);
image[0, 0] = new Rgba32(byte.MaxValue, 0, 0, 0);
image[1, 0] = new Rgba32(0, byte.MaxValue, 0);
image[2, 0] = new Rgba32(0, 0, byte.MaxValue);
image[3, 0] = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue);
using Av1EncoderFrameBuffer<byte> frameBuffer = new(
Configuration.Default,
width,
height,
8,
Av1ColorFormat.Yuv400,
0,
0);
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit);
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
byte[] expected = [76, 150, 29, 255];
AssertReplicatedSingleRow(frameBuffer.Luma, border, expected);
}
[Fact]
public void PrepareSourcePreservesHighBitDepthPrecision()
{
const int width = 4;
const int height = 1;
const int border = Av1EncoderFrame<ushort>.LumaBorder;
using Image<Rgba64> image = new(width, height);
image[0, 0] = new Rgba64(ushort.MaxValue, 0, 0, 0);
image[1, 0] = new Rgba64(0, ushort.MaxValue, 0, ushort.MaxValue);
image[2, 0] = new Rgba64(0, 0, ushort.MaxValue, ushort.MaxValue);
image[3, 0] = new Rgba64(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
using Av1EncoderFrameBuffer<ushort> frameBuffer = new(
Configuration.Default,
width,
height,
10,
Av1ColorFormat.Yuv400,
0,
0);
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.TenBit);
Av1FrameEncoder.PrepareSource(Configuration.Default, image.Frames.RootFrame, frameBuffer.Frame, colorConfig);
ushort[] expected = [306, 601, 117, 1023];
AssertReplicatedSingleRow(frameBuffer.Luma, border, expected);
}
[Fact]
public void ExtendBordersReplicatesEveryPhysicalPlaneEdge()
{
const int visibleWidth = 5;
const int visibleHeight = 3;
const int lumaBorder = Av1EncoderFrame<byte>.LumaBorder;
const int chromaBorder = lumaBorder / 2;
using Av1EncoderFrameBuffer<byte> frameBuffer = new(
Configuration.Default,
visibleWidth,
visibleHeight,
8,
Av1ColorFormat.Yuv420,
1,
1);
Buffer2D<byte> luma = frameBuffer.Luma;
Buffer2D<byte> chromaBlue = Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaBlue);
Buffer2D<byte> chromaRed = Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaRed);
FillVisible(luma, lumaBorder, lumaBorder, visibleWidth, visibleHeight, 10);
FillVisible(chromaBlue, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 80);
FillVisible(chromaRed, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 120);
frameBuffer.Frame.ExtendBorders();
AssertReplicatedPlane(luma, lumaBorder, lumaBorder, visibleWidth, visibleHeight, 10);
AssertReplicatedPlane(chromaBlue, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 80);
AssertReplicatedPlane(chromaRed, chromaBorder, chromaBorder, (visibleWidth + 1) / 2, (visibleHeight + 1) / 2, 120);
}
[Theory]
[InlineData(5, 3, 0, 0, 160, 136)]
[InlineData(5, 3, 1, 0, 80, 136)]
[InlineData(5, 3, 1, 1, 80, 68)]
[InlineData(1921, 1081, 0, 0, 2080, 1216)]
[InlineData(1921, 1081, 1, 1, 1040, 608)]
public void GetPlaneBufferSizeMatchesLibaomLayout(
int width,
int height,
int subsamplingX,
int subsamplingY,
int expectedWidth,
int expectedHeight)
{
Size actual = Av1EncoderFrame<byte>.GetPlaneBufferSize(width, height, subsamplingX, subsamplingY);
Assert.Equal(new Size(expectedWidth, expectedHeight), actual);
}
[Fact]
public void FrameBufferUsesOneExactSizeOwnerForAllPlanes()
{
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
TestMemoryAllocator.AllocationRequest allocation;
using (Av1EncoderFrameBuffer<byte> frameBuffer = new(
configuration,
64,
64,
8,
Av1ColorFormat.Yuv420,
1,
1))
{
allocation = Assert.Single(allocator.AllocationLog);
Assert.Empty(allocator.ReturnLog);
Assert.Equal(typeof(byte), allocation.ElementType);
Assert.Equal(55_296, allocation.Length);
Assert.Single(frameBuffer.Luma.MemoryGroup);
Assert.Single(Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaBlue).MemoryGroup);
Assert.Single(Assert.IsType<Buffer2D<byte>>(frameBuffer.ChromaRed).MemoryGroup);
}
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId);
}
[Fact]
public void EncodeReturnsEveryOperationAllocationAndUsesOneLibaomSizedTileReservation()
{
const int Width = 64;
const int Height = 64;
const int ExpectedTileOutputLength = 60 * 1024;
using Image<Rgba32> source = new(Width, Height);
for (int y = 0; y < Height; y++)
{
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < Width; x++)
{
row[x] = new Rgba32(
(byte)((x * 3) + y),
(byte)(x + (y * 5)),
(byte)((x * 7) + (y * 11)));
}
}
TestMemoryAllocator allocator = new();
allocator.EnableNonThreadSafeLogging();
Configuration configuration = Configuration.Default.Clone();
configuration.MemoryAllocator = allocator;
using MemoryStream storage = new();
using NonSeekableStream destination = new(storage);
_ = Av1FrameEncoder.Encode(
configuration,
source.Frames.RootFrame,
destination,
CreateColorConfig(Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv444),
qIndex: 37,
effort: 5);
Assert.False(destination.CanSeek);
Assert.NotEqual(0, storage.Length);
TestMemoryAllocator.AllocationRequest tileOutput = Assert.Single(
allocator.AllocationLog,
allocation => allocation.ElementType == typeof(byte) && allocation.Length == ExpectedTileOutputLength);
Assert.Equal(ExpectedTileOutputLength, tileOutput.Length);
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
Assert.Equal(
allocator.AllocationLog.Select(allocation => allocation.AllocationId).Order(),
allocator.ReturnLog.Select(returned => returned.AllocationId).Order());
}
private static ObuColorConfig CreateColorConfig(
Av1BitDepth bitDepth,
Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400)
=> new()
{
IsColorDescriptionPresent = true,
IsMonochrome = colorFormat == Av1ColorFormat.Yuv400,
ColorPrimaries = ObuColorPrimaries.Bt601,
TransferCharacteristics = ObuTransferCharacteristics.Bt601,
MatrixCoefficients = ObuMatrixCoefficients.Bt601,
ColorRange = true,
SubSamplingX = colorFormat != Av1ColorFormat.Yuv444,
SubSamplingY = colorFormat == Av1ColorFormat.Yuv400 || colorFormat == Av1ColorFormat.Yuv420,
ChromaSamplePosition = ObuChromoSamplePosition.Unknown,
BitDepth = bitDepth
};
private static void FillVisible(Buffer2D<byte> plane, int originX, int originY, int width, int height, int seed)
{
for (int y = 0; y < height; y++)
{
Span<byte> row = plane.DangerousGetRowSpan(originY + y);
for (int x = 0; x < width; x++)
{
row[originX + x] = (byte)(seed + (y * width) + x);
}
}
}
private static void AssertReplicatedPlane(Buffer2D<byte> plane, int originX, int originY, int width, int height, int seed)
{
for (int y = 0; y < plane.Height; y++)
{
ReadOnlySpan<byte> row = plane.DangerousGetRowSpan(y);
int sourceY = Math.Clamp(y - originY, 0, height - 1);
for (int x = 0; x < row.Length; x++)
{
int sourceX = Math.Clamp(x - originX, 0, width - 1);
Assert.Equal((byte)(seed + (sourceY * width) + sourceX), row[x]);
}
}
}
private static void AssertReplicatedSingleRow<TSample>(
Buffer2D<TSample> plane,
int originX,
ReadOnlySpan<TSample> expected)
where TSample : unmanaged, IEquatable<TSample>
{
for (int y = 0; y < plane.Height; y++)
{
ReadOnlySpan<TSample> row = plane.DangerousGetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{
int sourceX = Math.Clamp(x - originX, 0, expected.Length - 1);
Assert.Equal(expected[sourceX], row[x]);
}
}
}
}