mirror of https://github.com/SixLabors/ImageSharp
4 changed files with 841 additions and 3 deletions
@ -0,0 +1,161 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.InteropServices; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
|
||||
|
/// <content>
|
||||
|
/// Defines the sample-storage operations used by fixed intra superblock traversal.
|
||||
|
/// </content>
|
||||
|
internal static partial class Av1IntraSuperblockEncoder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines type-specific block encoding without coupling traversal to sample storage width.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
||||
|
private interface IBlockEncodingOperator<TSample> |
||||
|
where TSample : unmanaged |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets temporary contiguous storage for left reference samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="residual">The reusable residual workspace.</param>
|
||||
|
/// <param name="length">The number of reference samples.</param>
|
||||
|
/// <returns>The writable reference span.</returns>
|
||||
|
public static abstract Span<TSample> GetLeftReference(Span<short> residual, int length); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes and reconstructs one DC intra transform block.
|
||||
|
/// </summary>
|
||||
|
/// <param name="workspace">The reusable block workspace.</param>
|
||||
|
/// <param name="source">The coded source plane.</param>
|
||||
|
/// <param name="reconstruction">The coded reconstruction plane.</param>
|
||||
|
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
|
||||
|
/// <param name="above">The top reference samples.</param>
|
||||
|
/// <param name="left">The left reference samples.</param>
|
||||
|
/// <param name="hasLeft">Whether the left reference is available.</param>
|
||||
|
/// <param name="hasAbove">Whether the top reference is available.</param>
|
||||
|
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
|
||||
|
/// <param name="transformSize">The transform dimensions.</param>
|
||||
|
/// <param name="qIndex">The effective segment quantizer index.</param>
|
||||
|
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
|
||||
|
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
|
||||
|
/// <param name="plane">The component plane containing the block.</param>
|
||||
|
/// <param name="bitDepth">The coded sample bit depth.</param>
|
||||
|
/// <param name="state">The retained transform state.</param>
|
||||
|
public static abstract void Encode( |
||||
|
Av1EncoderBlockWorkspace workspace, |
||||
|
Buffer2DRegion<TSample> source, |
||||
|
Buffer2DRegion<TSample> reconstruction, |
||||
|
Point blockOrigin, |
||||
|
ReadOnlySpan<TSample> above, |
||||
|
ReadOnlySpan<TSample> left, |
||||
|
bool hasLeft, |
||||
|
bool hasAbove, |
||||
|
Span<int> quantizedCoefficients, |
||||
|
Av1TransformSize transformSize, |
||||
|
int qIndex, |
||||
|
int dcDeltaQ, |
||||
|
int acDeltaQ, |
||||
|
Av1Plane plane, |
||||
|
Av1BitDepth bitDepth, |
||||
|
ref Av1EncoderTransformBlockState state); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes blocks stored as eight-bit samples.
|
||||
|
/// </summary>
|
||||
|
private readonly struct ByteOperator : IBlockEncodingOperator<byte> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static Span<byte> GetLeftReference(Span<short> residual, int length) |
||||
|
=> MemoryMarshal.AsBytes(residual)[..length]; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void Encode( |
||||
|
Av1EncoderBlockWorkspace workspace, |
||||
|
Buffer2DRegion<byte> source, |
||||
|
Buffer2DRegion<byte> reconstruction, |
||||
|
Point blockOrigin, |
||||
|
ReadOnlySpan<byte> above, |
||||
|
ReadOnlySpan<byte> left, |
||||
|
bool hasLeft, |
||||
|
bool hasAbove, |
||||
|
Span<int> quantizedCoefficients, |
||||
|
Av1TransformSize transformSize, |
||||
|
int qIndex, |
||||
|
int dcDeltaQ, |
||||
|
int acDeltaQ, |
||||
|
Av1Plane plane, |
||||
|
Av1BitDepth bitDepth, |
||||
|
ref Av1EncoderTransformBlockState state) |
||||
|
=> Av1TransformBlockEncoder.EncodeIntraDcLossy( |
||||
|
workspace, |
||||
|
source, |
||||
|
reconstruction, |
||||
|
blockOrigin, |
||||
|
above, |
||||
|
left, |
||||
|
hasLeft, |
||||
|
hasAbove, |
||||
|
quantizedCoefficients, |
||||
|
transformSize, |
||||
|
Av1TransformType.DctDct, |
||||
|
qIndex, |
||||
|
dcDeltaQ, |
||||
|
acDeltaQ, |
||||
|
plane, |
||||
|
ref state); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes blocks stored as high-bit-depth samples.
|
||||
|
/// </summary>
|
||||
|
private readonly struct UInt16Operator : IBlockEncodingOperator<ushort> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static Span<ushort> GetLeftReference(Span<short> residual, int length) |
||||
|
=> MemoryMarshal.Cast<short, ushort>(residual)[..length]; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static void Encode( |
||||
|
Av1EncoderBlockWorkspace workspace, |
||||
|
Buffer2DRegion<ushort> source, |
||||
|
Buffer2DRegion<ushort> reconstruction, |
||||
|
Point blockOrigin, |
||||
|
ReadOnlySpan<ushort> above, |
||||
|
ReadOnlySpan<ushort> left, |
||||
|
bool hasLeft, |
||||
|
bool hasAbove, |
||||
|
Span<int> quantizedCoefficients, |
||||
|
Av1TransformSize transformSize, |
||||
|
int qIndex, |
||||
|
int dcDeltaQ, |
||||
|
int acDeltaQ, |
||||
|
Av1Plane plane, |
||||
|
Av1BitDepth bitDepth, |
||||
|
ref Av1EncoderTransformBlockState state) |
||||
|
=> Av1TransformBlockEncoder.EncodeIntraDcLossy( |
||||
|
workspace, |
||||
|
source, |
||||
|
reconstruction, |
||||
|
blockOrigin, |
||||
|
above, |
||||
|
left, |
||||
|
hasLeft, |
||||
|
hasAbove, |
||||
|
quantizedCoefficients, |
||||
|
transformSize, |
||||
|
Av1TransformType.DctDct, |
||||
|
qIndex, |
||||
|
dcDeltaQ, |
||||
|
acDeltaQ, |
||||
|
plane, |
||||
|
bitDepth, |
||||
|
ref state); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,276 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds fixed DC intra decisions and reconstructed samples for one AV1 superblock.
|
||||
|
/// </summary>
|
||||
|
internal static partial class Av1IntraSuperblockEncoder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Encodes one superblock stored as eight-bit samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The coded source frame.</param>
|
||||
|
/// <param name="reconstruction">The reconstructed frame updated by the block transforms.</param>
|
||||
|
/// <param name="picture">The frame coding and mode-information state.</param>
|
||||
|
/// <param name="superblock">The reusable partition and final-block decisions.</param>
|
||||
|
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
|
||||
|
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
|
||||
|
public static void Encode( |
||||
|
Av1EncoderFrame<byte> source, |
||||
|
Av1EncoderFrame<byte> reconstruction, |
||||
|
Av1PictureControlSet picture, |
||||
|
Av1Superblock superblock, |
||||
|
Av1EncoderCoefficientBuffer coefficientBuffer, |
||||
|
Av1EncoderBlockWorkspace blockWorkspace) |
||||
|
=> Encode<byte, ByteOperator>(source, reconstruction, picture, superblock, coefficientBuffer, blockWorkspace); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes one superblock stored as high-bit-depth samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The coded source frame.</param>
|
||||
|
/// <param name="reconstruction">The reconstructed frame updated by the block transforms.</param>
|
||||
|
/// <param name="picture">The frame coding and mode-information state.</param>
|
||||
|
/// <param name="superblock">The reusable partition and final-block decisions.</param>
|
||||
|
/// <param name="coefficientBuffer">The frame-owned quantized coefficient and transform state.</param>
|
||||
|
/// <param name="blockWorkspace">The reusable block arithmetic workspace.</param>
|
||||
|
public static void Encode( |
||||
|
Av1EncoderFrame<ushort> source, |
||||
|
Av1EncoderFrame<ushort> reconstruction, |
||||
|
Av1PictureControlSet picture, |
||||
|
Av1Superblock superblock, |
||||
|
Av1EncoderCoefficientBuffer coefficientBuffer, |
||||
|
Av1EncoderBlockWorkspace blockWorkspace) |
||||
|
=> Encode<ushort, UInt16Operator>(source, reconstruction, picture, superblock, coefficientBuffer, blockWorkspace); |
||||
|
|
||||
|
private static void Encode<TSample, TOperator>( |
||||
|
Av1EncoderFrame<TSample> source, |
||||
|
Av1EncoderFrame<TSample> reconstruction, |
||||
|
Av1PictureControlSet picture, |
||||
|
Av1Superblock superblock, |
||||
|
Av1EncoderCoefficientBuffer coefficientBuffer, |
||||
|
Av1EncoderBlockWorkspace blockWorkspace) |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, IBlockEncodingOperator<TSample> |
||||
|
{ |
||||
|
int superblockSize = picture.Sequence.SequenceHeader.SuperblockSize.GetWidth(); |
||||
|
Point superblockOrigin = new( |
||||
|
(superblock.Index % coefficientBuffer.SuperblockColumnCount) * superblockSize, |
||||
|
(superblock.Index / coefficientBuffer.SuperblockColumnCount) * superblockSize); |
||||
|
|
||||
|
superblock.Workspace.Reset(); |
||||
|
Traversal<TSample, TOperator> traversal = new( |
||||
|
source.CodedView, |
||||
|
reconstruction.CodedView, |
||||
|
picture, |
||||
|
superblock, |
||||
|
coefficientBuffer, |
||||
|
blockWorkspace); |
||||
|
|
||||
|
traversal.EncodePartitionTree(superblockOrigin, picture.Sequence.SequenceHeader.SuperblockSize); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retains the stack-only state shared by recursive partition and final-block traversal.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
|
||||
|
/// <typeparam name="TOperator">The type-specific block encoding operations.</typeparam>
|
||||
|
private ref struct Traversal<TSample, TOperator> |
||||
|
where TSample : unmanaged |
||||
|
where TOperator : struct, IBlockEncodingOperator<TSample> |
||||
|
{ |
||||
|
private readonly Av1EncoderFrame<TSample>.PlanarView source; |
||||
|
private readonly Av1EncoderFrame<TSample>.PlanarView reconstruction; |
||||
|
private readonly Av1PictureControlSet picture; |
||||
|
private readonly Av1Superblock superblock; |
||||
|
private readonly Av1EncoderBlockWorkspace blockWorkspace; |
||||
|
private readonly ObuQuantizationParameters quantization; |
||||
|
private readonly Av1BitDepth bitDepth; |
||||
|
private readonly Span<int> lumaCoefficients; |
||||
|
private readonly Span<int> blueCoefficients; |
||||
|
private readonly Span<int> redCoefficients; |
||||
|
private readonly Span<Av1EncoderTransformBlockState> lumaTransformBlocks; |
||||
|
private readonly Span<Av1EncoderTransformBlockState> blueTransformBlocks; |
||||
|
private readonly Span<Av1EncoderTransformBlockState> redTransformBlocks; |
||||
|
private int partitionIndex; |
||||
|
private int finalBlockIndex; |
||||
|
private int codedAreaLuma; |
||||
|
private int codedAreaChroma; |
||||
|
|
||||
|
public Traversal( |
||||
|
Av1EncoderFrame<TSample>.PlanarView source, |
||||
|
Av1EncoderFrame<TSample>.PlanarView reconstruction, |
||||
|
Av1PictureControlSet picture, |
||||
|
Av1Superblock superblock, |
||||
|
Av1EncoderCoefficientBuffer coefficientBuffer, |
||||
|
Av1EncoderBlockWorkspace blockWorkspace) |
||||
|
{ |
||||
|
this.source = source; |
||||
|
this.reconstruction = reconstruction; |
||||
|
this.picture = picture; |
||||
|
this.superblock = superblock; |
||||
|
this.blockWorkspace = blockWorkspace; |
||||
|
this.quantization = picture.Parent.FrameHeader.QuantizationParameters; |
||||
|
this.bitDepth = picture.Sequence.SequenceHeader.ColorConfig.BitDepth; |
||||
|
this.lumaCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.Y); |
||||
|
this.blueCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.U); |
||||
|
this.redCoefficients = coefficientBuffer.GetPlaneSpan(superblock.Index, Av1Plane.V); |
||||
|
this.lumaTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.Y); |
||||
|
this.blueTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.U); |
||||
|
this.redTransformBlocks = coefficientBuffer.GetTransformBlockSpan(superblock.Index, Av1Plane.V); |
||||
|
this.partitionIndex = 0; |
||||
|
this.finalBlockIndex = 0; |
||||
|
this.codedAreaLuma = 0; |
||||
|
this.codedAreaChroma = 0; |
||||
|
} |
||||
|
|
||||
|
public void EncodePartitionTree(Point blockOrigin, Av1BlockSize blockSize) |
||||
|
{ |
||||
|
Av1EncoderCommon common = this.picture.Parent.Common; |
||||
|
Point modeInfoPosition = blockOrigin >> Av1Constants.ModeInfoSizeLog2; |
||||
|
if (modeInfoPosition.Y >= common.ModeInfoRowCount || modeInfoPosition.X >= common.ModeInfoColumnCount) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (blockSize == Av1BlockSize.Block8x8) |
||||
|
{ |
||||
|
this.superblock.CodingUnitPartitionTypes[this.partitionIndex++] = (byte)Av1PartitionType.None; |
||||
|
this.EncodeFinalBlock(blockOrigin, modeInfoPosition); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.superblock.CodingUnitPartitionTypes[this.partitionIndex++] = (byte)Av1PartitionType.Split; |
||||
|
Av1BlockSize subSize = Av1PartitionType.Split.GetBlockSubSize(blockSize); |
||||
|
int halfBlockSize = blockSize.GetWidth() >> 1; |
||||
|
|
||||
|
// The preorder and out-of-frame pruning match tile writing, so one reusable decision workspace is sufficient.
|
||||
|
this.EncodePartitionTree(blockOrigin, subSize); |
||||
|
this.EncodePartitionTree(blockOrigin + new Size(halfBlockSize, 0), subSize); |
||||
|
this.EncodePartitionTree(blockOrigin + new Size(0, halfBlockSize), subSize); |
||||
|
this.EncodePartitionTree(blockOrigin + new Size(halfBlockSize, halfBlockSize), subSize); |
||||
|
} |
||||
|
|
||||
|
private void EncodeFinalBlock(Point blockOrigin, Point modeInfoPosition) |
||||
|
{ |
||||
|
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; |
||||
|
const Av1TransformSize LumaTransformSize = Av1TransformSize.Size8x8; |
||||
|
int qIndex = this.quantization.QIndex[0]; |
||||
|
ref Av1MacroBlockModeInfo modeInfo = ref this.picture.GetMacroBlockModeInfo(modeInfoPosition); |
||||
|
modeInfo.Block = new Av1EncoderBlockModeInfo |
||||
|
{ |
||||
|
BlockSize = BlockSize, |
||||
|
PartitionType = Av1PartitionType.None, |
||||
|
SegmentId = 0, |
||||
|
TransformSize = LumaTransformSize, |
||||
|
Mode = Av1PredictionMode.DC, |
||||
|
UvMode = Av1ChromaPredictionMode.DC |
||||
|
}; |
||||
|
|
||||
|
modeInfo.CdefStrength = 0; |
||||
|
ref Av1EncoderBlockStruct block = ref this.superblock.FinalBlocks[this.finalBlockIndex++]; |
||||
|
block.HasChroma = !this.source.IsMonochrome; |
||||
|
block.QuantizationIndex = qIndex; |
||||
|
block.SegmentId = 0; |
||||
|
|
||||
|
int lumaTransformIndex = this.codedAreaLuma / |
||||
|
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount; |
||||
|
|
||||
|
ref Av1EncoderTransformBlockState lumaState = ref this.lumaTransformBlocks[lumaTransformIndex]; |
||||
|
this.EncodePlaneBlock( |
||||
|
Av1Plane.Y, |
||||
|
blockOrigin, |
||||
|
LumaTransformSize, |
||||
|
this.lumaCoefficients[this.codedAreaLuma..], |
||||
|
ref lumaState); |
||||
|
|
||||
|
this.codedAreaLuma += LumaTransformSize.GetSize2d(); |
||||
|
if (this.source.IsMonochrome) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; |
||||
|
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; |
||||
|
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; |
||||
|
Point chromaOrigin = new(blockOrigin.X >> subsamplingX, blockOrigin.Y >> subsamplingY); |
||||
|
Av1TransformSize chromaTransformSize = BlockSize.GetMaxUvTransformSize( |
||||
|
colorConfig.SubSamplingX, |
||||
|
colorConfig.SubSamplingY); |
||||
|
|
||||
|
int chromaTransformIndex = this.codedAreaChroma / |
||||
|
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount; |
||||
|
|
||||
|
ref Av1EncoderTransformBlockState blueState = ref this.blueTransformBlocks[chromaTransformIndex]; |
||||
|
ref Av1EncoderTransformBlockState redState = ref this.redTransformBlocks[chromaTransformIndex]; |
||||
|
this.EncodePlaneBlock( |
||||
|
Av1Plane.U, |
||||
|
chromaOrigin, |
||||
|
chromaTransformSize, |
||||
|
this.blueCoefficients[this.codedAreaChroma..], |
||||
|
ref blueState); |
||||
|
|
||||
|
this.EncodePlaneBlock( |
||||
|
Av1Plane.V, |
||||
|
chromaOrigin, |
||||
|
chromaTransformSize, |
||||
|
this.redCoefficients[this.codedAreaChroma..], |
||||
|
ref redState); |
||||
|
|
||||
|
this.codedAreaChroma += chromaTransformSize.GetSize2d(); |
||||
|
} |
||||
|
|
||||
|
private void EncodePlaneBlock( |
||||
|
Av1Plane plane, |
||||
|
Point blockOrigin, |
||||
|
Av1TransformSize transformSize, |
||||
|
Span<int> coefficients, |
||||
|
ref Av1EncoderTransformBlockState state) |
||||
|
{ |
||||
|
Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(plane); |
||||
|
Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(plane); |
||||
|
int width = transformSize.GetWidth(); |
||||
|
int height = transformSize.GetHeight(); |
||||
|
bool hasLeft = blockOrigin.X > 0; |
||||
|
bool hasAbove = blockOrigin.Y > 0; |
||||
|
ReadOnlySpan<TSample> above = hasAbove |
||||
|
? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width) |
||||
|
: []; |
||||
|
|
||||
|
Span<TSample> left = TOperator.GetLeftReference(this.blockWorkspace.Residual, height); |
||||
|
if (hasLeft) |
||||
|
{ |
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Prediction consumes every gathered reference before residual construction reuses the same workspace bytes.
|
||||
|
TOperator.Encode( |
||||
|
this.blockWorkspace, |
||||
|
sourcePlane, |
||||
|
reconstructionPlane, |
||||
|
blockOrigin, |
||||
|
above, |
||||
|
left, |
||||
|
hasLeft, |
||||
|
hasAbove, |
||||
|
coefficients, |
||||
|
transformSize, |
||||
|
this.quantization.QIndex[0], |
||||
|
this.quantization.DeltaQDc[(int)plane], |
||||
|
this.quantization.DeltaQAc[(int)plane], |
||||
|
plane, |
||||
|
this.bitDepth, |
||||
|
ref state); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,401 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
||||
|
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.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies fixed DC intra superblock traversal and reconstruction.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1IntraSuperblockEncoderTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void EncodesClipped128SuperblockInWriterPreorderWithoutAllocation() |
||||
|
{ |
||||
|
const int Width = 16; |
||||
|
const int Height = 16; |
||||
|
ObuColorConfig colorConfig = new() |
||||
|
{ |
||||
|
IsMonochrome = false, |
||||
|
SubSamplingX = true, |
||||
|
SubSamplingY = true, |
||||
|
BitDepth = Av1BitDepth.EightBit |
||||
|
}; |
||||
|
|
||||
|
using Av1EncoderFrameBuffer<byte> source = new( |
||||
|
Configuration.Default, |
||||
|
Width, |
||||
|
Height, |
||||
|
8, |
||||
|
Av1ColorFormat.Yuv420, |
||||
|
1, |
||||
|
1); |
||||
|
|
||||
|
using Av1EncoderFrameBuffer<byte> reconstruction = new( |
||||
|
Configuration.Default, |
||||
|
Width, |
||||
|
Height, |
||||
|
8, |
||||
|
Av1ColorFormat.Yuv420, |
||||
|
1, |
||||
|
1); |
||||
|
|
||||
|
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.Y), 251, 17); |
||||
|
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.U), 239, 31); |
||||
|
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.V), 233, 47); |
||||
|
ClearPlane(reconstruction.Luma); |
||||
|
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaBlue)); |
||||
|
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaRed)); |
||||
|
|
||||
|
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); |
||||
|
Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: true, qIndex: 73); |
||||
|
using Av1EncoderCoefficientBuffer coefficients = new( |
||||
|
Configuration.Default, |
||||
|
picture.Sequence.SequenceHeader, |
||||
|
Width, |
||||
|
Height); |
||||
|
|
||||
|
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); |
||||
|
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); |
||||
|
Av1Superblock superblock = new() |
||||
|
{ |
||||
|
Workspace = superblockWorkspace, |
||||
|
TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), |
||||
|
Index = 0 |
||||
|
}; |
||||
|
|
||||
|
Av1IntraSuperblockEncoder.Encode( |
||||
|
source.Frame, |
||||
|
reconstruction.Frame, |
||||
|
picture, |
||||
|
superblock, |
||||
|
coefficients, |
||||
|
blockWorkspace); |
||||
|
|
||||
|
long before = GC.GetAllocatedBytesForCurrentThread(); |
||||
|
for (int iteration = 0; iteration < 8; iteration++) |
||||
|
{ |
||||
|
Av1IntraSuperblockEncoder.Encode( |
||||
|
source.Frame, |
||||
|
reconstruction.Frame, |
||||
|
picture, |
||||
|
superblock, |
||||
|
coefficients, |
||||
|
blockWorkspace); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); |
||||
|
Av1PartitionType[] expectedPartitions = |
||||
|
[ |
||||
|
Av1PartitionType.Split, |
||||
|
Av1PartitionType.Split, |
||||
|
Av1PartitionType.Split, |
||||
|
Av1PartitionType.Split, |
||||
|
Av1PartitionType.None, |
||||
|
Av1PartitionType.None, |
||||
|
Av1PartitionType.None, |
||||
|
Av1PartitionType.None |
||||
|
]; |
||||
|
|
||||
|
for (int index = 0; index < expectedPartitions.Length; index++) |
||||
|
{ |
||||
|
Assert.Equal(expectedPartitions[index], (Av1PartitionType)superblock.CodingUnitPartitionTypes[index]); |
||||
|
} |
||||
|
|
||||
|
for (int index = 0; index < 4; index++) |
||||
|
{ |
||||
|
Assert.True(superblock.FinalBlocks[index].HasChroma); |
||||
|
Assert.Equal(73, superblock.FinalBlocks[index].QuantizationIndex); |
||||
|
Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, superblock.FinalBlocks[index].FilterIntraMode); |
||||
|
} |
||||
|
|
||||
|
Point[] modeInfoPositions = [new(0, 0), new(2, 0), new(0, 2), new(2, 2)]; |
||||
|
foreach (Point position in modeInfoPositions) |
||||
|
{ |
||||
|
ref Av1MacroBlockModeInfo block = ref picture.GetMacroBlockModeInfo(position); |
||||
|
Assert.Equal(Av1BlockSize.Block8x8, block.Block.BlockSize); |
||||
|
Assert.Equal(Av1TransformSize.Size8x8, block.Block.TransformSize); |
||||
|
Assert.Equal(Av1PredictionMode.DC, block.Block.Mode); |
||||
|
Assert.Equal(Av1ChromaPredictionMode.DC, block.Block.UvMode); |
||||
|
Assert.False(block.Block.Skip); |
||||
|
} |
||||
|
|
||||
|
Span<Av1EncoderTransformBlockState> lumaStates = coefficients.GetTransformBlockSpan(0, Av1Plane.Y); |
||||
|
Span<Av1EncoderTransformBlockState> blueStates = coefficients.GetTransformBlockSpan(0, Av1Plane.U); |
||||
|
Span<Av1EncoderTransformBlockState> redStates = coefficients.GetTransformBlockSpan(0, Av1Plane.V); |
||||
|
int[] lumaStateIndices = [0, 4, 8, 12]; |
||||
|
for (int index = 0; index < 4; index++) |
||||
|
{ |
||||
|
Assert.NotEqual((ushort)0, lumaStates[lumaStateIndices[index]].EndOfBlock); |
||||
|
Assert.Equal(Av1TransformType.DctDct, lumaStates[lumaStateIndices[index]].TransformType); |
||||
|
Assert.NotEqual((ushort)0, blueStates[index].EndOfBlock); |
||||
|
Assert.NotEqual((ushort)0, redStates[index].EndOfBlock); |
||||
|
} |
||||
|
|
||||
|
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y)); |
||||
|
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.U)); |
||||
|
AssertContainsNonzero(reconstruction.Frame.CodedView.GetPlane(Av1Plane.V)); |
||||
|
|
||||
|
// Edge contexts cover the complete 128x128 superblock because partition updates retain the coded geometry
|
||||
|
// even when most of the superblock lies beyond this deliberately clipped frame.
|
||||
|
const int ContextUnitCount = 128 >> Av1Constants.ModeInfoSizeLog2; |
||||
|
using Av1NeighborArrayUnit<Av1PartitionContext> partitions = new( |
||||
|
Configuration.Default, |
||||
|
ContextUnitCount, |
||||
|
ContextUnitCount) |
||||
|
{ |
||||
|
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
||||
|
}; |
||||
|
|
||||
|
using Av1NeighborArrayUnit<byte> lumaContexts = new( |
||||
|
Configuration.Default, |
||||
|
ContextUnitCount, |
||||
|
ContextUnitCount) |
||||
|
{ |
||||
|
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
||||
|
}; |
||||
|
|
||||
|
using Av1NeighborArrayUnit<byte> blueContexts = new( |
||||
|
Configuration.Default, |
||||
|
ContextUnitCount, |
||||
|
ContextUnitCount) |
||||
|
{ |
||||
|
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
||||
|
}; |
||||
|
|
||||
|
using Av1NeighborArrayUnit<byte> redContexts = new( |
||||
|
Configuration.Default, |
||||
|
ContextUnitCount, |
||||
|
ContextUnitCount) |
||||
|
{ |
||||
|
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
||||
|
}; |
||||
|
|
||||
|
using Av1NeighborArrayUnit<byte> transformContexts = new( |
||||
|
Configuration.Default, |
||||
|
ContextUnitCount, |
||||
|
ContextUnitCount) |
||||
|
{ |
||||
|
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 |
||||
|
}; |
||||
|
|
||||
|
picture.PartitionContexts = [partitions]; |
||||
|
picture.LuminanceDcSignLevelCoefficientNeighbors = [lumaContexts]; |
||||
|
picture.CbDcSignLevelCoefficientNeighbors = [blueContexts]; |
||||
|
picture.CrDcSignLevelCoefficientNeighbors = [redContexts]; |
||||
|
picture.TransformFunctionContexts = [transformContexts]; |
||||
|
Av1TileWriter.Av1EntropyCodingContext entropyContext = new() |
||||
|
{ |
||||
|
MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo }, |
||||
|
MacroBlockModeInfo = picture.GetMacroBlockModeInfo(default), |
||||
|
SuperblockOrigin = default |
||||
|
}; |
||||
|
|
||||
|
using Av1SymbolEncoder writer = new(Configuration.Default, 512, 73); |
||||
|
Av1TileWriter.WriteSuperblock( |
||||
|
picture, |
||||
|
entropyContext, |
||||
|
writer, |
||||
|
superblock, |
||||
|
coefficients, |
||||
|
tileIndex: 0); |
||||
|
|
||||
|
using IMemoryOwner<byte> encoded = writer.Exit(); |
||||
|
|
||||
|
// The writer must consume exactly the transform areas populated above, proving both traversals stay synchronized.
|
||||
|
Assert.Equal(256, entropyContext.CodedAreaSuperblock); |
||||
|
Assert.Equal(64, entropyContext.CodedAreaSuperblockUv); |
||||
|
Assert.NotEqual(0, encoded.GetSpan().Length); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void PreservesTwelveBitMonochromeReconstructionPrecision() |
||||
|
{ |
||||
|
const int Width = 8; |
||||
|
const int Height = 8; |
||||
|
ObuColorConfig colorConfig = new() |
||||
|
{ |
||||
|
IsMonochrome = true, |
||||
|
SubSamplingX = true, |
||||
|
SubSamplingY = true, |
||||
|
BitDepth = Av1BitDepth.TwelveBit |
||||
|
}; |
||||
|
|
||||
|
using Av1EncoderFrameBuffer<ushort> source = new( |
||||
|
Configuration.Default, |
||||
|
Width, |
||||
|
Height, |
||||
|
12, |
||||
|
Av1ColorFormat.Yuv400, |
||||
|
0, |
||||
|
0); |
||||
|
|
||||
|
using Av1EncoderFrameBuffer<ushort> reconstruction = new( |
||||
|
Configuration.Default, |
||||
|
Width, |
||||
|
Height, |
||||
|
12, |
||||
|
Av1ColorFormat.Yuv400, |
||||
|
0, |
||||
|
0); |
||||
|
|
||||
|
Buffer2DRegion<ushort> sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y); |
||||
|
for (int y = 0; y < sourcePlane.Height; y++) |
||||
|
{ |
||||
|
Span<ushort> row = sourcePlane.DangerousGetRowSpan(y); |
||||
|
for (int x = 0; x < row.Length; x++) |
||||
|
{ |
||||
|
row[x] = (ushort)(3000 + (((x * 71) + (y * 113)) % 1000)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ClearPlane(reconstruction.Luma); |
||||
|
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true); |
||||
|
Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37); |
||||
|
using Av1EncoderCoefficientBuffer coefficients = new( |
||||
|
Configuration.Default, |
||||
|
picture.Sequence.SequenceHeader, |
||||
|
Width, |
||||
|
Height); |
||||
|
|
||||
|
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default); |
||||
|
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default); |
||||
|
Av1Superblock superblock = new() |
||||
|
{ |
||||
|
Workspace = superblockWorkspace, |
||||
|
TileInfo = new Av1TileInfo(0, 0, picture.Parent.FrameHeader), |
||||
|
Index = 0 |
||||
|
}; |
||||
|
|
||||
|
Av1IntraSuperblockEncoder.Encode( |
||||
|
source.Frame, |
||||
|
reconstruction.Frame, |
||||
|
picture, |
||||
|
superblock, |
||||
|
coefficients, |
||||
|
blockWorkspace); |
||||
|
|
||||
|
Buffer2DRegion<ushort> reconstructionPlane = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y); |
||||
|
ushort maximum = 0; |
||||
|
for (int y = 0; y < reconstructionPlane.Height; y++) |
||||
|
{ |
||||
|
foreach (ushort sample in reconstructionPlane.DangerousGetRowSpan(y)) |
||||
|
{ |
||||
|
maximum = Math.Max(maximum, sample); |
||||
|
Assert.InRange(sample, (ushort)0, (ushort)4095); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Assert.InRange(maximum, (ushort)(byte.MaxValue + 1), (ushort)4095); |
||||
|
Assert.False(superblock.FinalBlocks[0].HasChroma); |
||||
|
Assert.Equal(37, superblock.FinalBlocks[0].QuantizationIndex); |
||||
|
Assert.NotEqual((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.Y)[0].EndOfBlock); |
||||
|
Assert.Equal(0, coefficients.GetPlaneSpan(0, Av1Plane.U).Length); |
||||
|
Assert.Equal(0, coefficients.GetPlaneSpan(0, Av1Plane.V).Length); |
||||
|
} |
||||
|
|
||||
|
private static Av1PictureControlSet CreatePicture( |
||||
|
Av1EncoderModeInfoBuffer modeInfo, |
||||
|
ObuColorConfig colorConfig, |
||||
|
bool use128x128Superblock, |
||||
|
int qIndex) |
||||
|
{ |
||||
|
ObuTileGroupHeader tiles = new() |
||||
|
{ |
||||
|
TileColumnCount = 1, |
||||
|
TileRowCount = 1 |
||||
|
}; |
||||
|
|
||||
|
tiles.TileColumnStartModeInfo[1] = modeInfo.ModeInfoColumnCount; |
||||
|
tiles.TileRowStartModeInfo[1] = modeInfo.ModeInfoRowCount; |
||||
|
ObuSequenceHeader sequenceHeader = new() |
||||
|
{ |
||||
|
Use128x128Superblock = use128x128Superblock, |
||||
|
ColorConfig = colorConfig |
||||
|
}; |
||||
|
|
||||
|
ObuFrameHeader frameHeader = new() |
||||
|
{ |
||||
|
ModeInfoColumnCount = modeInfo.ModeInfoColumnCount, |
||||
|
ModeInfoRowCount = modeInfo.ModeInfoRowCount, |
||||
|
TilesInfo = tiles |
||||
|
}; |
||||
|
|
||||
|
frameHeader.QuantizationParameters.BaseQIndex = qIndex; |
||||
|
frameHeader.QuantizationParameters.QIndex.Fill(qIndex); |
||||
|
return new Av1PictureControlSet |
||||
|
{ |
||||
|
PartitionContexts = [], |
||||
|
LuminanceDcSignLevelCoefficientNeighbors = [], |
||||
|
CrDcSignLevelCoefficientNeighbors = [], |
||||
|
CbDcSignLevelCoefficientNeighbors = [], |
||||
|
TransformFunctionContexts = [], |
||||
|
Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader }, |
||||
|
Parent = new Av1PictureParentControlSet |
||||
|
{ |
||||
|
Common = new Av1EncoderCommon |
||||
|
{ |
||||
|
ModeInfoColumnCount = modeInfo.ModeInfoColumnCount, |
||||
|
ModeInfoRowCount = modeInfo.ModeInfoRowCount, |
||||
|
ModeInfoStride = modeInfo.ModeInfoStride, |
||||
|
TilesInfo = tiles, |
||||
|
FrameSize = new ObuFrameSize() |
||||
|
}, |
||||
|
FrameHeader = frameHeader, |
||||
|
PreviousQIndex = [qIndex] |
||||
|
}, |
||||
|
SegmentationNeighborMap = new byte[modeInfo.ModeInfoColumnCount * modeInfo.ModeInfoRowCount], |
||||
|
ModeInfoGrid = modeInfo.Grid, |
||||
|
ModeInfoAllocation = modeInfo.Allocation, |
||||
|
ModeInfoStride = modeInfo.ModeInfoStride, |
||||
|
Disallow4x4AllFrames = modeInfo.Disallow4x4AllFrames, |
||||
|
CdefPreset = [[-1, -1, -1, -1]] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static void FillPlane(Buffer2DRegion<byte> plane, int modulus, int seed) |
||||
|
{ |
||||
|
for (int y = 0; y < plane.Height; y++) |
||||
|
{ |
||||
|
Span<byte> row = plane.DangerousGetRowSpan(y); |
||||
|
for (int x = 0; x < row.Length; x++) |
||||
|
{ |
||||
|
row[x] = (byte)(1 + ((seed + (x * 43) + (y * 79)) % modulus)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void ClearPlane<TSample>(Buffer2D<TSample> plane) |
||||
|
where TSample : unmanaged |
||||
|
{ |
||||
|
for (int y = 0; y < plane.Height; y++) |
||||
|
{ |
||||
|
plane.DangerousGetRowSpan(y).Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void AssertContainsNonzero<TSample>(Buffer2DRegion<TSample> plane) |
||||
|
where TSample : unmanaged, IEquatable<TSample> |
||||
|
{ |
||||
|
bool containsNonzero = false; |
||||
|
for (int y = 0; y < plane.Height; y++) |
||||
|
{ |
||||
|
foreach (TSample sample in plane.DangerousGetRowSpan(y)) |
||||
|
{ |
||||
|
containsNonzero |= !sample.Equals(default); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Assert.True(containsNonzero); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue