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.
223 lines
9.4 KiB
223 lines
9.4 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Buffers;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
|
|
|
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
|
|
|
|
/// <summary>
|
|
/// Verifies the adaptive distributions and packed contexts used to select an AV1 single-reference inter mode.
|
|
/// </summary>
|
|
[Trait("Format", "Avif")]
|
|
public class Av1InterModeEntropyTests
|
|
{
|
|
/// <summary>
|
|
/// Verifies the normative single-reference inter-mode distributions against libaom's forward Q15 defaults.
|
|
/// </summary>
|
|
[Fact]
|
|
public void InterModeDefaultsMatchLibaom()
|
|
{
|
|
AssertBinaryDefaults(Av1DefaultDistributions.NewMv, [24035, 16630, 15339, 8386, 12222, 4676]);
|
|
AssertBinaryDefaults(Av1DefaultDistributions.ZeroMv, [2175, 1054]);
|
|
AssertBinaryDefaults(Av1DefaultDistributions.RefMv, [23974, 24188, 17848, 28622, 24312, 19923]);
|
|
AssertBinaryDefaults(Av1DefaultDistributions.Drl, [13104, 24560, 18945]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that each inter-mode context occupies the normative field in the packed mode context.
|
|
/// </summary>
|
|
/// <param name="modeContext">The packed mode context.</param>
|
|
/// <param name="expectedNewMv">The expected newly decoded motion-vector context.</param>
|
|
/// <param name="expectedZeroMv">The expected global-motion context.</param>
|
|
/// <param name="expectedRefMv">The expected spatial reference-motion-vector context.</param>
|
|
[Theory]
|
|
[InlineData(0, 0, 0, 0)]
|
|
[InlineData(77, 5, 1, 4)]
|
|
[InlineData(93, 5, 1, 5)]
|
|
public void PackedInterModeContextMatchesLibaom(int modeContext, int expectedNewMv, int expectedZeroMv, int expectedRefMv)
|
|
{
|
|
Assert.Equal(expectedNewMv, Av1SymbolContextHelper.GetNewMvContext(modeContext));
|
|
Assert.Equal(expectedZeroMv, Av1SymbolContextHelper.GetZeroMvContext(modeContext));
|
|
Assert.Equal(expectedRefMv, Av1SymbolContextHelper.GetRefMvContext(modeContext));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the exact short-circuit order and symbol polarity of the single-reference inter-mode tree.
|
|
/// </summary>
|
|
/// <param name="expectedMode">The expected prediction mode.</param>
|
|
/// <param name="newMvSymbol">The new-motion-vector decision.</param>
|
|
/// <param name="zeroMvSymbol">The global-motion decision, or negative when the leaf precedes it.</param>
|
|
/// <param name="refMvSymbol">The spatial reference-motion-vector decision, or negative when the leaf precedes it.</param>
|
|
[Theory]
|
|
[InlineData((int)Av1PredictionMode.NewMotionVector, 0, -1, -1)]
|
|
[InlineData((int)Av1PredictionMode.GlobalMotionVector, 1, 0, -1)]
|
|
[InlineData((int)Av1PredictionMode.NearestMotionVector, 1, 1, 0)]
|
|
[InlineData((int)Av1PredictionMode.NearMotionVector, 1, 1, 1)]
|
|
public void ReadInterModeMatchesLibaomTree(int expectedMode, int newMvSymbol, int zeroMvSymbol, int refMvSymbol)
|
|
{
|
|
const int modeContext = 77;
|
|
Av1Distribution newMv = Av1DefaultDistributions.NewMv[5];
|
|
Av1Distribution zeroMv = Av1DefaultDistributions.ZeroMv[1];
|
|
Av1Distribution refMv = Av1DefaultDistributions.RefMv[4];
|
|
Av1Distribution drl = Av1DefaultDistributions.Drl[2];
|
|
using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true);
|
|
|
|
writer.WriteSymbol(newMvSymbol, newMv);
|
|
if (zeroMvSymbol >= 0)
|
|
{
|
|
writer.WriteSymbol(zeroMvSymbol, zeroMv);
|
|
}
|
|
|
|
if (refMvSymbol >= 0)
|
|
{
|
|
writer.WriteSymbol(refMvSymbol, refMv);
|
|
}
|
|
|
|
// A symbol after the selected leaf proves that the decoder consumed exactly the decisions on that branch.
|
|
writer.WriteSymbol(true, drl);
|
|
|
|
using IMemoryOwner<byte> encoded = writer.Exit();
|
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true);
|
|
|
|
Assert.Equal((Av1PredictionMode)expectedMode, decoder.ReadInterMode(modeContext));
|
|
Assert.True(decoder.ReadDrl(2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the dynamic reference-list reader selects each requested context distribution.
|
|
/// </summary>
|
|
/// <param name="context">The dynamic reference-list context.</param>
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public void ReadDrlUsesRequestedContext(int context)
|
|
{
|
|
bool[] expected = [false, true, true, false, true, false, false, true];
|
|
Av1Distribution writerDistribution = Av1DefaultDistributions.Drl[context];
|
|
using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true);
|
|
|
|
foreach (bool value in expected)
|
|
{
|
|
writer.WriteSymbol(value, writerDistribution);
|
|
}
|
|
|
|
using IMemoryOwner<byte> encoded = writer.Exit();
|
|
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true);
|
|
|
|
foreach (bool value in expected)
|
|
{
|
|
Assert.Equal(value, decoder.ReadDrl(context));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the four candidate-weight pairings used to select a dynamic reference-list context.
|
|
/// </summary>
|
|
/// <param name="currentWeight">The current candidate's weight.</param>
|
|
/// <param name="nextWeight">The next candidate's weight.</param>
|
|
/// <param name="expected">The expected dynamic reference-list context.</param>
|
|
[Theory]
|
|
[InlineData(640, 640, 0)]
|
|
[InlineData(640, 639, 1)]
|
|
[InlineData(639, 639, 2)]
|
|
[InlineData(639, 640, 0)]
|
|
public void DrlContextMatchesCandidateWeightCategories(ushort currentWeight, ushort nextWeight, int expected)
|
|
{
|
|
ushort[] referenceWeights = [currentWeight, nextWeight];
|
|
|
|
Assert.Equal(expected, Av1SymbolContextHelper.GetDrlContext(referenceWeights, 0));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that frame-context copies retain inter-mode adaptation without sharing mutable distributions.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FrameEntropyCopyRetainsIndependentInterModeState()
|
|
{
|
|
Av1FrameEntropyContext source = new(0);
|
|
Av1FrameEntropyContext destination = new(0);
|
|
source.NewMv[5].Update(1);
|
|
source.ZeroMv[1].Update(1);
|
|
source.RefMv[4].Update(1);
|
|
source.Drl[2].Update(1);
|
|
|
|
destination.CopyFrom(source);
|
|
|
|
Assert.Equal(source.NewMv[5][0], destination.NewMv[5][0]);
|
|
Assert.Equal(source.ZeroMv[1][0], destination.ZeroMv[1][0]);
|
|
Assert.Equal(source.RefMv[4][0], destination.RefMv[4][0]);
|
|
Assert.Equal(source.Drl[2][0], destination.Drl[2][0]);
|
|
|
|
source.NewMv[5].Update(0);
|
|
source.ZeroMv[1].Update(0);
|
|
source.RefMv[4].Update(0);
|
|
source.Drl[2].Update(0);
|
|
|
|
Assert.NotEqual(source.NewMv[5][0], destination.NewMv[5][0]);
|
|
Assert.NotEqual(source.ZeroMv[1][0], destination.ZeroMv[1][0]);
|
|
Assert.NotEqual(source.RefMv[4][0], destination.RefMv[4][0]);
|
|
Assert.NotEqual(source.Drl[2][0], destination.Drl[2][0]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that publishing frame state resets the inter-mode distributions' update-rate history.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FrameEntropySnapshotResetsInterModeUpdateCounts()
|
|
{
|
|
const int updateCount = 20;
|
|
Av1FrameEntropyContext source = new(0);
|
|
Av1FrameEntropyContext snapshot = new(0);
|
|
|
|
for (int i = 0; i < updateCount; i++)
|
|
{
|
|
source.NewMv[5].Update(1);
|
|
source.ZeroMv[1].Update(1);
|
|
source.RefMv[4].Update(1);
|
|
source.Drl[2].Update(1);
|
|
}
|
|
|
|
source.SnapshotTo(snapshot);
|
|
|
|
Assert.Equal(source.NewMv[5][0], snapshot.NewMv[5][0]);
|
|
Assert.Equal(source.ZeroMv[1][0], snapshot.ZeroMv[1][0]);
|
|
Assert.Equal(source.RefMv[4][0], snapshot.RefMv[4][0]);
|
|
Assert.Equal(source.Drl[2][0], snapshot.Drl[2][0]);
|
|
|
|
// The source retains twenty observations while the snapshot restarts at zero. Applying the same next symbol
|
|
// therefore moves identical thresholds by different amounts only when the new distributions participate in reset.
|
|
source.NewMv[5].Update(0);
|
|
snapshot.NewMv[5].Update(0);
|
|
source.ZeroMv[1].Update(0);
|
|
snapshot.ZeroMv[1].Update(0);
|
|
source.RefMv[4].Update(0);
|
|
snapshot.RefMv[4].Update(0);
|
|
source.Drl[2].Update(0);
|
|
snapshot.Drl[2].Update(0);
|
|
|
|
Assert.NotEqual(source.NewMv[5][0], snapshot.NewMv[5][0]);
|
|
Assert.NotEqual(source.ZeroMv[1][0], snapshot.ZeroMv[1][0]);
|
|
Assert.NotEqual(source.RefMv[4][0], snapshot.RefMv[4][0]);
|
|
Assert.NotEqual(source.Drl[2][0], snapshot.Drl[2][0]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies binary distribution defaults after their conversion to the inverse cumulative representation.
|
|
/// </summary>
|
|
/// <param name="distributions">The distributions under test.</param>
|
|
/// <param name="forwardThresholds">The normative forward Q15 thresholds.</param>
|
|
private static void AssertBinaryDefaults(Av1Distribution[] distributions, ReadOnlySpan<uint> forwardThresholds)
|
|
{
|
|
Assert.Equal(forwardThresholds.Length, distributions.Length);
|
|
for (int context = 0; context < distributions.Length; context++)
|
|
{
|
|
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[context];
|
|
|
|
Assert.Equal(expected, distributions[context][0]);
|
|
Assert.Equal(2, distributions[context].NumberOfSymbols);
|
|
}
|
|
}
|
|
}
|
|
|