mirror of https://github.com/SixLabors/ImageSharp
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
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]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|