// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
///
/// Verifies compound prediction through the production block-reconstruction branch.
///
[Trait("Format", "Avif")]
public class Av1CompoundBlockDecoderTests
{
///
/// Verifies that two retained reference planes are predicted and averaged before residual reconstruction.
///
/// The native sample depth.
[Theory]
[InlineData((int)Av1BitDepth.EightBit)]
[InlineData((int)Av1BitDepth.TenBit)]
[InlineData((int)Av1BitDepth.TwelveBit)]
public void DecodeBlockReconstructsEqualAverageCompoundPrediction(int bitDepthValue)
{
Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue;
ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100;
ushort secondValue = bitDepth switch
{
Av1BitDepth.EightBit => 41,
Av1BitDepth.TenBit => 701,
_ => 3001,
};
ushort expected = (ushort)((firstValue + secondValue + 1) >> 1);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth);
ObuFrameHeader frameHeader = CreateFrameHeader();
frameHeader.GetReferenceFrameIndices()[0] = 0;
frameHeader.GetReferenceFrameIndices()[1] = 1;
using Av1ReferenceFrameStore referenceFrames = new();
Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false));
Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false));
using Av1FrameBuffer frameBuffer = new(
Configuration.Default,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
using Av1FrameInfo frameInfo = new(sequenceHeader);
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty);
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty)
{
Skip = true,
YMode = Av1PredictionMode.NearestNearestMotionVector,
CompoundIndex = true,
CompoundType = Av1CompoundType.Average,
};
modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last;
modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2;
modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Regular);
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
Av1LoopFilterContext loopFilterContext = new(sequenceHeader);
Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader);
using Av1BlockDecoder decoder = new(
sequenceHeader,
frameHeader,
frameBuffer,
loopFilterContext,
inverseQuantizer,
referenceFrames);
decoder.UpdateSuperblock(superblockInfo);
decoder.DecodeBlock(
modeInfo,
Point.Empty,
Av1BlockSize.Block8x8,
superblockInfo,
new Av1TileInfo(0, 0, frameHeader));
for (int row = 0; row < 8; row++)
{
if (bitDepth == Av1BitDepth.EightBit)
{
Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row);
for (int column = 0; column < 8; column++)
{
Assert.Equal((byte)expected, samples[column]);
}
}
else
{
Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0);
for (int column = 0; column < 8; column++)
{
Assert.Equal(expected, samples[column]);
}
}
}
}
///
/// Creates one independently owned retained frame filled with a constant visible luma value.
///
private static Av1ReferenceFrame CreateReferenceFrame(ObuSequenceHeader sequenceHeader, ushort value)
{
Av1FrameBuffer frameBuffer = new(
Configuration.Default,
sequenceHeader,
Av1ColorFormat.Yuv400,
false);
if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit)
{
for (int row = 0; row < 8; row++)
{
frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row).Fill((byte)value);
}
}
else
{
for (int row = 0; row < 8; row++)
{
frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0).Fill(value);
}
}
using Av1FrameInfo frameInfo = new(sequenceHeader);
return new Av1ReferenceFrame(frameBuffer, CreateFrameHeader(), frameInfo);
}
///
/// Creates the monochrome 8x8 sequence used by direct reconstruction tests.
///
private static ObuSequenceHeader CreateSequenceHeader(Av1BitDepth bitDepth)
=> new()
{
MaxFrameWidth = 8,
MaxFrameHeight = 8,
Use128x128Superblock = false,
ColorConfig = new ObuColorConfig
{
IsMonochrome = true,
BitDepth = bitDepth,
},
};
///
/// Creates an unscaled 8x8 inter-frame header with one complete tile.
///
private static ObuFrameHeader CreateFrameHeader()
{
ObuFrameHeader frameHeader = new()
{
FrameType = ObuFrameType.InterFrame,
ModeInfoColumnCount = 2,
ModeInfoRowCount = 2,
FrameSize = new ObuFrameSize
{
FrameWidth = 8,
FrameHeight = 8,
},
};
frameHeader.TilesInfo.TileColumnStartModeInfo[1] = frameHeader.ModeInfoColumnCount;
frameHeader.TilesInfo.TileRowStartModeInfo[1] = frameHeader.ModeInfoRowCount;
return frameHeader;
}
}