mirror of https://github.com/SixLabors/ImageSharp
48 changed files with 6920 additions and 523 deletions
@ -1,228 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
|||
|
|||
/// <summary>
|
|||
/// Decodes integer intra-block-copy displacement vectors with tile-adaptive AV1 distributions.
|
|||
/// </summary>
|
|||
internal sealed class Av1DisplacementVectorContext |
|||
{ |
|||
/// <summary>
|
|||
/// The number of magnitude classes defined by AV1.
|
|||
/// </summary>
|
|||
private const int MagnitudeClassCount = 11; |
|||
|
|||
/// <summary>
|
|||
/// The number of class-zero integer magnitude bits.
|
|||
/// </summary>
|
|||
private const int ClassZeroBitCount = 1; |
|||
|
|||
/// <summary>
|
|||
/// The tile-adaptive distribution selecting which vector components are nonzero.
|
|||
/// </summary>
|
|||
private readonly Av1Distribution joint = new(4096, 11264, 19328); |
|||
|
|||
/// <summary>
|
|||
/// The tile-adaptive vertical component distributions.
|
|||
/// </summary>
|
|||
private readonly Component vertical = new(); |
|||
|
|||
/// <summary>
|
|||
/// The tile-adaptive horizontal component distributions.
|
|||
/// </summary>
|
|||
private readonly Component horizontal = new(); |
|||
|
|||
/// <summary>
|
|||
/// Replaces every displacement-vector distribution with state copied from another context.
|
|||
/// </summary>
|
|||
/// <param name="source">The displacement-vector context state to copy.</param>
|
|||
public void CopyFrom(Av1DisplacementVectorContext source) |
|||
{ |
|||
this.joint.CopyFrom(source.joint); |
|||
this.vertical.CopyFrom(source.vertical); |
|||
this.horizontal.CopyFrom(source.horizontal); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resets every observation count used to adapt displacement-vector distributions.
|
|||
/// </summary>
|
|||
public void ResetUpdateCounts() |
|||
{ |
|||
this.joint.ResetUpdateCount(); |
|||
this.vertical.ResetUpdateCounts(); |
|||
this.horizontal.ResetUpdateCounts(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads an integer displacement vector relative to a spatially derived reference.
|
|||
/// </summary>
|
|||
/// <param name="reader">The tile range decoder.</param>
|
|||
/// <param name="reference">The reference displacement vector.</param>
|
|||
/// <returns>The decoded displacement vector in one-eighth-sample units.</returns>
|
|||
public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference) |
|||
{ |
|||
int jointType = reader.ReadSymbol(this.joint); |
|||
|
|||
// Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Intra-block copy
|
|||
// fixes precision to whole luma samples, so the component reader consumes no fractional or high-precision CDFs.
|
|||
int row = jointType >= 2 ? this.vertical.Read(ref reader) : 0; |
|||
int column = (jointType & 1) != 0 ? this.horizontal.Read(ref reader) : 0; |
|||
return reference + new Av1MotionVector(row, column); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes an integer displacement vector relative to a spatially derived reference.
|
|||
/// </summary>
|
|||
/// <param name="writer">The tile range encoder.</param>
|
|||
/// <param name="value">The displacement vector to encode.</param>
|
|||
/// <param name="reference">The spatially derived reference vector.</param>
|
|||
public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference) |
|||
{ |
|||
int row = value.Row - reference.Row; |
|||
int column = value.Column - reference.Column; |
|||
int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0); |
|||
writer.WriteSymbol(jointType, this.joint); |
|||
|
|||
if (row != 0) |
|||
{ |
|||
this.vertical.Write(writer, row); |
|||
} |
|||
|
|||
if (column != 0) |
|||
{ |
|||
this.horizontal.Write(writer, column); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores the adaptive magnitude distributions for one displacement-vector component.
|
|||
/// </summary>
|
|||
private sealed class Component |
|||
{ |
|||
/// <summary>
|
|||
/// The distribution selecting the signed magnitude class.
|
|||
/// </summary>
|
|||
private readonly Av1Distribution magnitudeClass = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767); |
|||
|
|||
/// <summary>
|
|||
/// The distribution selecting the sign of a nonzero component.
|
|||
/// </summary>
|
|||
private readonly Av1Distribution sign = new(16384); |
|||
|
|||
/// <summary>
|
|||
/// The distribution selecting either of the two class-zero integer magnitudes.
|
|||
/// </summary>
|
|||
private readonly Av1Distribution classZero = new(27648); |
|||
|
|||
/// <summary>
|
|||
/// The binary distributions that reconstruct larger magnitude offsets from least to most significant bit.
|
|||
/// </summary>
|
|||
private readonly Av1Distribution[] offsetBits = |
|||
[ |
|||
new(17408), new(17920), new(18944), new(20480), new(22528), |
|||
new(24576), new(28672), new(29952), new(29952), new(30720) |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Replaces every component distribution with state copied from another component.
|
|||
/// </summary>
|
|||
/// <param name="source">The component state to copy.</param>
|
|||
public void CopyFrom(Component source) |
|||
{ |
|||
this.magnitudeClass.CopyFrom(source.magnitudeClass); |
|||
this.sign.CopyFrom(source.sign); |
|||
this.classZero.CopyFrom(source.classZero); |
|||
|
|||
for (int bit = 0; bit < this.offsetBits.Length; bit++) |
|||
{ |
|||
this.offsetBits[bit].CopyFrom(source.offsetBits[bit]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resets every observation count used to adapt one component's distributions.
|
|||
/// </summary>
|
|||
public void ResetUpdateCounts() |
|||
{ |
|||
this.magnitudeClass.ResetUpdateCount(); |
|||
this.sign.ResetUpdateCount(); |
|||
this.classZero.ResetUpdateCount(); |
|||
|
|||
for (int bit = 0; bit < this.offsetBits.Length; bit++) |
|||
{ |
|||
this.offsetBits[bit].ResetUpdateCount(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads one signed integer-precision component.
|
|||
/// </summary>
|
|||
/// <param name="reader">The tile range decoder.</param>
|
|||
/// <returns>The signed component in one-eighth-sample units.</returns>
|
|||
public int Read(ref Av1SymbolReader reader) |
|||
{ |
|||
bool isNegative = reader.ReadSymbol(this.sign) != 0; |
|||
int magnitudeClass = reader.ReadSymbol(this.magnitudeClass); |
|||
int offset; |
|||
int magnitudeBase; |
|||
|
|||
if (magnitudeClass == 0) |
|||
{ |
|||
offset = reader.ReadSymbol(this.classZero); |
|||
magnitudeBase = 0; |
|||
} |
|||
else |
|||
{ |
|||
int bitCount = magnitudeClass + ClassZeroBitCount - 1; |
|||
offset = 0; |
|||
for (int bit = 0; bit < bitCount; bit++) |
|||
{ |
|||
// AV1 transmits the integer offset least-significant bit first, with an independently adapting
|
|||
// distribution for every bit position.
|
|||
offset |= reader.ReadSymbol(this.offsetBits[bit]) << bit; |
|||
} |
|||
|
|||
magnitudeBase = (1 << ClassZeroBitCount) << (magnitudeClass + 2); |
|||
} |
|||
|
|||
// Integer precision substitutes the normative fractional values fr=3 and hp=1. The low three bits are
|
|||
// consequently all one, and the final increment converts the zero-based magnitude representation.
|
|||
int magnitude = magnitudeBase + (offset << 3) + 8; |
|||
return isNegative ? -magnitude : magnitude; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes one signed integer-precision component.
|
|||
/// </summary>
|
|||
/// <param name="writer">The tile range encoder.</param>
|
|||
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
|
|||
public void Write(Av1SymbolWriter writer, int value) |
|||
{ |
|||
int magnitude = Math.Abs(value); |
|||
DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision."); |
|||
|
|||
int magnitudeClass = magnitude <= 16 ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3; |
|||
DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass)); |
|||
writer.WriteSymbol(value < 0, this.sign); |
|||
writer.WriteSymbol(magnitudeClass, this.magnitudeClass); |
|||
|
|||
if (magnitudeClass == 0) |
|||
{ |
|||
writer.WriteSymbol((magnitude >> 3) - 1, this.classZero); |
|||
return; |
|||
} |
|||
|
|||
int magnitudeBase = 8 << magnitudeClass; |
|||
int offset = (magnitude - magnitudeBase - 8) >> 3; |
|||
for (int bit = 0; bit < magnitudeClass; bit++) |
|||
{ |
|||
// The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be
|
|||
// updated in the same order during encoding.
|
|||
writer.WriteSymbol(((offset >> bit) & 1) != 0, this.offsetBits[bit]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,319 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; |
|||
|
|||
/// <summary>
|
|||
/// Owns one independently adaptive AV1 motion-vector entropy context.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Normal inter-prediction vectors and intra-block-copy displacement vectors use identical initial distributions, but
|
|||
/// each syntax domain owns a separate instance so observations from one domain cannot adapt the other.
|
|||
/// </remarks>
|
|||
internal sealed class Av1MotionVectorContext |
|||
{ |
|||
/// <summary>
|
|||
/// The number of magnitude classes defined by AV1.
|
|||
/// </summary>
|
|||
private const int MagnitudeClassCount = 11; |
|||
|
|||
/// <summary>
|
|||
/// The number of integer magnitude bits coded directly for class zero.
|
|||
/// </summary>
|
|||
private const int ClassZeroBitCount = 1; |
|||
|
|||
/// <summary>
|
|||
/// The number of integer magnitude offsets represented by class zero.
|
|||
/// </summary>
|
|||
private const int ClassZeroSize = 1 << ClassZeroBitCount; |
|||
|
|||
/// <summary>
|
|||
/// Gets the distribution selecting which vector components are nonzero.
|
|||
/// </summary>
|
|||
public Av1Distribution Joint { get; } = new(4096, 11264, 19328); |
|||
|
|||
/// <summary>
|
|||
/// Gets the adaptive distributions for the vertical vector component.
|
|||
/// </summary>
|
|||
public Component Vertical { get; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the adaptive distributions for the horizontal vector component.
|
|||
/// </summary>
|
|||
public Component Horizontal { get; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// Replaces every motion-vector distribution with state copied from another context.
|
|||
/// </summary>
|
|||
/// <param name="source">The motion-vector context state to copy.</param>
|
|||
public void CopyFrom(Av1MotionVectorContext source) |
|||
{ |
|||
this.Joint.CopyFrom(source.Joint); |
|||
this.Vertical.CopyFrom(source.Vertical); |
|||
this.Horizontal.CopyFrom(source.Horizontal); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resets every observation count used to adapt the motion-vector distributions.
|
|||
/// </summary>
|
|||
public void ResetUpdateCounts() |
|||
{ |
|||
this.Joint.ResetUpdateCount(); |
|||
this.Vertical.ResetUpdateCounts(); |
|||
this.Horizontal.ResetUpdateCounts(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a motion-vector delta relative to a spatially derived reference.
|
|||
/// </summary>
|
|||
/// <param name="reader">The tile range decoder.</param>
|
|||
/// <param name="reference">The reference motion vector.</param>
|
|||
/// <param name="precision">The fractional precision allowed by the current frame.</param>
|
|||
/// <returns>The decoded motion vector in one-eighth-sample units.</returns>
|
|||
public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference, Av1MotionVectorPrecision precision) |
|||
{ |
|||
int jointType = reader.ReadSymbol(this.Joint); |
|||
|
|||
// Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Reading only the
|
|||
// signaled components preserves the normative entropy-symbol order and leaves zero components unadapted.
|
|||
int row = jointType >= 2 ? this.Vertical.Read(ref reader, precision) : 0; |
|||
int column = (jointType & 1) != 0 ? this.Horizontal.Read(ref reader, precision) : 0; |
|||
|
|||
return reference + new Av1MotionVector(row, column); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes an integer displacement vector relative to a spatially derived reference.
|
|||
/// </summary>
|
|||
/// <param name="writer">The tile range encoder.</param>
|
|||
/// <param name="value">The displacement vector to encode.</param>
|
|||
/// <param name="reference">The spatially derived reference vector.</param>
|
|||
public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference) |
|||
{ |
|||
int row = value.Row - reference.Row; |
|||
int column = value.Column - reference.Column; |
|||
|
|||
// Bit zero signals a horizontal delta and bit one signals a vertical delta, producing the four normative
|
|||
// zero/horizontal/vertical/both joint symbols without a lookup.
|
|||
int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0); |
|||
|
|||
writer.WriteSymbol(jointType, this.Joint); |
|||
if (row != 0) |
|||
{ |
|||
this.Vertical.Write(writer, row); |
|||
} |
|||
|
|||
if (column != 0) |
|||
{ |
|||
this.Horizontal.Write(writer, column); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Owns the adaptive magnitude distributions for one motion-vector component.
|
|||
/// </summary>
|
|||
public sealed class Component |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the distribution selecting the magnitude class of a nonzero component.
|
|||
/// </summary>
|
|||
public Av1Distribution MagnitudeClass { get; } = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767); |
|||
|
|||
/// <summary>
|
|||
/// Gets the fractional distributions selected by the two class-zero integer offsets.
|
|||
/// </summary>
|
|||
public Av1Distribution[] ClassZeroFractional { get; } = |
|||
[ |
|||
new(16384, 24576, 26624), |
|||
new(12288, 21248, 24128) |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the fractional distribution used by nonzero magnitude classes.
|
|||
/// </summary>
|
|||
public Av1Distribution Fractional { get; } = new(8192, 17408, 21248); |
|||
|
|||
/// <summary>
|
|||
/// Gets the distribution selecting the sign of a nonzero component.
|
|||
/// </summary>
|
|||
public Av1Distribution Sign { get; } = new(16384); |
|||
|
|||
/// <summary>
|
|||
/// Gets the eighth-sample distribution used by class-zero magnitudes.
|
|||
/// </summary>
|
|||
public Av1Distribution ClassZeroHighPrecision { get; } = new(20480); |
|||
|
|||
/// <summary>
|
|||
/// Gets the eighth-sample distribution used by nonzero magnitude classes.
|
|||
/// </summary>
|
|||
public Av1Distribution HighPrecision { get; } = new(16384); |
|||
|
|||
/// <summary>
|
|||
/// Gets the distribution selecting either of the two class-zero integer magnitude offsets.
|
|||
/// </summary>
|
|||
public Av1Distribution ClassZero { get; } = new(27648); |
|||
|
|||
/// <summary>
|
|||
/// Gets the binary distributions that reconstruct larger integer magnitude offsets from least to most significant bit.
|
|||
/// </summary>
|
|||
public Av1Distribution[] OffsetBits { get; } = |
|||
[ |
|||
new(17408), new(17920), new(18944), new(20480), new(22528), |
|||
new(24576), new(28672), new(29952), new(29952), new(30720) |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Replaces every component distribution with state copied from another component.
|
|||
/// </summary>
|
|||
/// <param name="source">The component state to copy.</param>
|
|||
public void CopyFrom(Component source) |
|||
{ |
|||
this.MagnitudeClass.CopyFrom(source.MagnitudeClass); |
|||
|
|||
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++) |
|||
{ |
|||
this.ClassZeroFractional[offset].CopyFrom(source.ClassZeroFractional[offset]); |
|||
} |
|||
|
|||
this.Fractional.CopyFrom(source.Fractional); |
|||
this.Sign.CopyFrom(source.Sign); |
|||
this.ClassZeroHighPrecision.CopyFrom(source.ClassZeroHighPrecision); |
|||
this.HighPrecision.CopyFrom(source.HighPrecision); |
|||
this.ClassZero.CopyFrom(source.ClassZero); |
|||
|
|||
for (int bit = 0; bit < this.OffsetBits.Length; bit++) |
|||
{ |
|||
this.OffsetBits[bit].CopyFrom(source.OffsetBits[bit]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resets every observation count used to adapt one component's distributions.
|
|||
/// </summary>
|
|||
public void ResetUpdateCounts() |
|||
{ |
|||
this.MagnitudeClass.ResetUpdateCount(); |
|||
|
|||
for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++) |
|||
{ |
|||
this.ClassZeroFractional[offset].ResetUpdateCount(); |
|||
} |
|||
|
|||
this.Fractional.ResetUpdateCount(); |
|||
this.Sign.ResetUpdateCount(); |
|||
this.ClassZeroHighPrecision.ResetUpdateCount(); |
|||
this.HighPrecision.ResetUpdateCount(); |
|||
this.ClassZero.ResetUpdateCount(); |
|||
|
|||
for (int bit = 0; bit < this.OffsetBits.Length; bit++) |
|||
{ |
|||
this.OffsetBits[bit].ResetUpdateCount(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads one signed motion-vector component at the requested precision.
|
|||
/// </summary>
|
|||
/// <param name="reader">The tile range decoder.</param>
|
|||
/// <param name="precision">The fractional precision allowed by the current frame.</param>
|
|||
/// <returns>The signed component in one-eighth-sample units.</returns>
|
|||
public int Read(ref Av1SymbolReader reader, Av1MotionVectorPrecision precision) |
|||
{ |
|||
bool isNegative = reader.ReadSymbol(this.Sign) != 0; |
|||
int magnitudeClass = reader.ReadSymbol(this.MagnitudeClass); |
|||
bool isClassZero = magnitudeClass == 0; |
|||
int integerOffset; |
|||
int magnitudeBase; |
|||
|
|||
if (isClassZero) |
|||
{ |
|||
integerOffset = reader.ReadSymbol(this.ClassZero); |
|||
magnitudeBase = 0; |
|||
} |
|||
else |
|||
{ |
|||
int bitCount = magnitudeClass + ClassZeroBitCount - 1; |
|||
integerOffset = 0; |
|||
|
|||
for (int bit = 0; bit < bitCount; bit++) |
|||
{ |
|||
// AV1 transmits the integer offset least-significant bit first, with an independently adapting
|
|||
// distribution for every bit position.
|
|||
integerOffset |= reader.ReadSymbol(this.OffsetBits[bit]) << bit; |
|||
} |
|||
|
|||
// Class one uses a base of two whole samples, or sixteen eighth-sample units, and every later class doubles
|
|||
// that base. CLASS0_SIZE shifted by class + 2 expresses the same scale directly in eighth-sample units.
|
|||
magnitudeBase = ClassZeroSize << (magnitudeClass + 2); |
|||
} |
|||
|
|||
int fractional; |
|||
int highPrecision; |
|||
|
|||
if (precision != Av1MotionVectorPrecision.Integer) |
|||
{ |
|||
// Class-zero magnitudes select one of two fractional CDFs using the already decoded integer offset;
|
|||
// larger classes share one fractional CDF because their expanded integer range supplies the context.
|
|||
Av1Distribution fractionalDistribution = isClassZero ? this.ClassZeroFractional[integerOffset] : this.Fractional; |
|||
fractional = reader.ReadSymbol(fractionalDistribution); |
|||
|
|||
// Quarter-sample motion omits the eighth-sample symbol. The normative implicit one, combined with the
|
|||
// final increment below, constrains the result to even one-eighth-sample units.
|
|||
highPrecision = precision == Av1MotionVectorPrecision.EighthSample |
|||
? reader.ReadSymbol(isClassZero ? this.ClassZeroHighPrecision : this.HighPrecision) |
|||
: 1; |
|||
} |
|||
else |
|||
{ |
|||
// Integer motion omits both fractional symbols. The implicit maximum values make the low three bits
|
|||
// all one before the final increment, constraining the result to whole-sample multiples of eight.
|
|||
fractional = 3; |
|||
highPrecision = 1; |
|||
} |
|||
|
|||
// The entropy syntax represents magnitude minus one. Integer offset occupies bits three and above,
|
|||
// fractional occupies bits one and two, and high precision occupies bit zero, all in one-eighth-sample units.
|
|||
int magnitude = magnitudeBase + ((integerOffset << 3) | (fractional << 1) | highPrecision) + 1; |
|||
|
|||
return isNegative ? -magnitude : magnitude; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes one signed integer-precision component.
|
|||
/// </summary>
|
|||
/// <param name="writer">The tile range encoder.</param>
|
|||
/// <param name="value">The nonzero component in one-eighth-sample units.</param>
|
|||
public void Write(Av1SymbolWriter writer, int value) |
|||
{ |
|||
int magnitude = Math.Abs(value); |
|||
DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision."); |
|||
|
|||
// Class zero contains the two whole-sample magnitudes 8 and 16. Above it, the highest set bit of magnitude
|
|||
// minus one selects the doubling range; subtracting three converts the eighth-sample bit index to the class.
|
|||
int magnitudeClass = magnitude <= (ClassZeroSize << 3) ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3; |
|||
DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass)); |
|||
writer.WriteSymbol(value < 0, this.Sign); |
|||
writer.WriteSymbol(magnitudeClass, this.MagnitudeClass); |
|||
|
|||
if (magnitudeClass == 0) |
|||
{ |
|||
writer.WriteSymbol((magnitude >> 3) - 1, this.ClassZero); |
|||
return; |
|||
} |
|||
|
|||
// Remove the class base and the implicit low-bit value 7 plus the final one before coding the remaining
|
|||
// whole-sample offset least-significant bit first.
|
|||
int magnitudeBase = ClassZeroSize << (magnitudeClass + 2); |
|||
int integerOffset = (magnitude - magnitudeBase - 8) >> 3; |
|||
|
|||
for (int bit = 0; bit < magnitudeClass; bit++) |
|||
{ |
|||
// The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be
|
|||
// updated in the same order during encoding.
|
|||
writer.WriteSymbol(((integerOffset >> bit) & 1) != 0, this.OffsetBits[bit]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,297 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
/// <summary>
|
|||
/// Derives the neighboring-block state and fixed-capacity projection samples used to select an AV1 motion mode.
|
|||
/// </summary>
|
|||
internal sealed class Av1MotionVariationCandidates |
|||
{ |
|||
/// <summary>
|
|||
/// The maximum number of neighboring motion samples retained for a local warped-motion projection.
|
|||
/// </summary>
|
|||
private const int ProjectionSampleCapacity = 8; |
|||
|
|||
/// <summary>
|
|||
/// The largest neighbor step used by overlapping motion compensation, measured in 4x4 mode-information units.
|
|||
/// </summary>
|
|||
private const int MaximumNeighborStep = 16; |
|||
|
|||
/// <summary>
|
|||
/// The number of fractional bits in an AV1 motion vector and warped-motion sample position.
|
|||
/// </summary>
|
|||
private const int MotionVectorSubpixelBits = 3; |
|||
|
|||
/// <summary>
|
|||
/// Stores sample positions relative to the current block origin in one-eighth-sample units.
|
|||
/// </summary>
|
|||
private InlineArray8<Point> sourcePoints; |
|||
|
|||
/// <summary>
|
|||
/// Stores the corresponding reference-frame positions in one-eighth-sample units.
|
|||
/// </summary>
|
|||
private InlineArray8<Point> referencePoints; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of valid entries in <see cref="SourcePoints"/> and <see cref="ReferencePoints"/>.
|
|||
/// </summary>
|
|||
public int Count { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether an inter-coded block overlaps the current block's above or left edge.
|
|||
/// </summary>
|
|||
public bool HasOverlappableNeighbor { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the retained current-frame sample positions in one-eighth-sample units relative to the current block.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<Point> SourcePoints => this.sourcePoints[..this.Count]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the retained reference-frame sample positions in one-eighth-sample units relative to the current block.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<Point> ReferencePoints => this.referencePoints[..this.Count]; |
|||
|
|||
/// <summary>
|
|||
/// Derives the spatial state used to select Simple Translation, OBMC, or Warped motion for one inter block.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
|
|||
/// <param name="frameHeader">The current frame dimensions.</param>
|
|||
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
|
|||
public void Build( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame) |
|||
{ |
|||
this.Count = 0; |
|||
this.CollectProjectionSamples(ref partitionInfo, tileInfo, sequenceHeader, frameHeader, referenceFrame); |
|||
this.HasOverlappableNeighbor = FindOverlappableNeighbor(ref partitionInfo, frameHeader); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Collects the at most eight spatial samples permitted by AV1's local warped-motion model.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="sequenceHeader">The sequence-level superblock geometry.</param>
|
|||
/// <param name="frameHeader">The current frame dimensions.</param>
|
|||
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
|
|||
private void CollectProjectionSamples( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame) |
|||
{ |
|||
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; |
|||
int width = blockSize.Get4x4WideCount(); |
|||
int height = blockSize.Get4x4HighCount(); |
|||
int row = partitionInfo.RowIndex; |
|||
int column = partitionInfo.ColumnIndex; |
|||
bool includeTopLeft = true; |
|||
bool includeTopRight = true; |
|||
|
|||
if (partitionInfo.AvailableAbove) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row - 1)); |
|||
int candidateWidth = candidate.BlockSize.Get4x4WideCount(); |
|||
if (width <= candidateWidth) |
|||
{ |
|||
// A wider above block can also cover the diagonal search positions. The signed alignment offset
|
|||
// prevents those positions from contributing the same block a second time.
|
|||
int columnOffset = -column % candidateWidth; |
|||
includeTopLeft = columnOffset >= 0; |
|||
includeTopRight = columnOffset + candidateWidth <= width; |
|||
this.AddProjectionSample(candidate, referenceFrame, 0, -1, columnOffset, 1); |
|||
} |
|||
else |
|||
{ |
|||
int end = Math.Min(width, frameHeader.ModeInfoColumnCount - column); |
|||
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateWidth) |
|||
{ |
|||
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1)); |
|||
candidateWidth = candidate.BlockSize.Get4x4WideCount(); |
|||
this.AddProjectionSample(candidate, referenceFrame, 0, -1, index, 1); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row)); |
|||
int candidateHeight = candidate.BlockSize.Get4x4HighCount(); |
|||
if (height <= candidateHeight) |
|||
{ |
|||
// The same alignment rule suppresses a duplicate top-left sample when one tall left block covers it.
|
|||
int rowOffset = -row % candidateHeight; |
|||
includeTopLeft &= rowOffset >= 0; |
|||
this.AddProjectionSample(candidate, referenceFrame, rowOffset, 1, 0, -1); |
|||
} |
|||
else |
|||
{ |
|||
int end = Math.Min(height, frameHeader.ModeInfoRowCount - row); |
|||
for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateHeight) |
|||
{ |
|||
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index)); |
|||
candidateHeight = candidate.BlockSize.Get4x4HighCount(); |
|||
this.AddProjectionSample(candidate, referenceFrame, index, 1, 0, -1); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (includeTopLeft && partitionInfo.AvailableAbove && partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row - 1)); |
|||
this.AddProjectionSample(candidate, referenceFrame, 0, -1, 0, -1); |
|||
} |
|||
|
|||
int topRightRow = row - 1; |
|||
int topRightColumn = column + width; |
|||
bool topRightInsideTile = |
|||
topRightRow >= tileInfo.ModeInfoRowStart && |
|||
topRightRow < tileInfo.ModeInfoRowEnd && |
|||
topRightColumn >= tileInfo.ModeInfoColumnStart && |
|||
topRightColumn < tileInfo.ModeInfoColumnEnd; |
|||
|
|||
if (includeTopRight && |
|||
this.Count < ProjectionSampleCapacity && |
|||
partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize) && |
|||
topRightInsideTile) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(topRightColumn, topRightRow)); |
|||
this.AddProjectionSample(candidate, referenceFrame, 0, -1, width, 1); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether an inter-coded neighbor covers either complete prediction edge of the current block.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide decoded mode map.</param>
|
|||
/// <param name="frameHeader">The current frame dimensions.</param>
|
|||
/// <returns><see langword="true"/> when an above or left inter block can contribute overlapping prediction.</returns>
|
|||
private static bool FindOverlappableNeighbor(ref Av1PartitionInfo partitionInfo, ObuFrameHeader frameHeader) |
|||
{ |
|||
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; |
|||
int width = blockSize.Get4x4WideCount(); |
|||
int height = blockSize.Get4x4HighCount(); |
|||
int row = partitionInfo.RowIndex; |
|||
int column = partitionInfo.ColumnIndex; |
|||
|
|||
if (partitionInfo.AvailableAbove) |
|||
{ |
|||
int endColumn = Math.Min(column + width, frameHeader.ModeInfoColumnCount); |
|||
for (int aboveColumn = column; aboveColumn < endColumn;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn, row - 1)); |
|||
int step = Math.Min(candidate.BlockSize.Get4x4WideCount(), MaximumNeighborStep); |
|||
if (step == 1) |
|||
{ |
|||
// AV1 treats a 4-sample-wide neighbor as one half of an 8-sample pair and reads the mode record
|
|||
// attached to the pair's second cell before advancing across both cells.
|
|||
aboveColumn &= ~1; |
|||
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn + 1, row - 1)); |
|||
step = 2; |
|||
} |
|||
|
|||
if (IsOverlappable(candidate)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
aboveColumn += step; |
|||
} |
|||
} |
|||
|
|||
if (partitionInfo.AvailableLeft) |
|||
{ |
|||
int endRow = Math.Min(row + height, frameHeader.ModeInfoRowCount); |
|||
for (int leftRow = row; leftRow < endRow;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow)); |
|||
int step = Math.Min(candidate.BlockSize.Get4x4HighCount(), MaximumNeighborStep); |
|||
if (step == 1) |
|||
{ |
|||
// The vertical scan applies the corresponding 4-sample-high pairing rule.
|
|||
leftRow &= ~1; |
|||
candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow + 1)); |
|||
step = 2; |
|||
} |
|||
|
|||
if (IsOverlappable(candidate)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
leftRow += step; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Appends one neighboring single-reference sample when it uses the current block's primary reference.
|
|||
/// </summary>
|
|||
/// <param name="candidate">The neighboring decoded block.</param>
|
|||
/// <param name="referenceFrame">The current block's primary canonical reference.</param>
|
|||
/// <param name="rowOffset">The neighbor center row offset in 4x4 mode-information units.</param>
|
|||
/// <param name="rowSign">The direction from the current block toward the neighbor on the vertical axis.</param>
|
|||
/// <param name="columnOffset">The neighbor center column offset in 4x4 mode-information units.</param>
|
|||
/// <param name="columnSign">The direction from the current block toward the neighbor on the horizontal axis.</param>
|
|||
private void AddProjectionSample( |
|||
Av1BlockModeInfo candidate, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
int rowOffset, |
|||
int rowSign, |
|||
int columnOffset, |
|||
int columnSign) |
|||
{ |
|||
Span<Av1ReferenceFrameType> candidateReferences = candidate.ReferenceFrames; |
|||
if (candidateReferences[0] != referenceFrame || candidateReferences[1] != Av1ReferenceFrameType.None) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; |
|||
int sourceX = (columnOffset * modeInfoSampleSize) + (columnSign * (candidate.BlockSize.GetWidth() >> 1)) - 1; |
|||
int sourceY = (rowOffset * modeInfoSampleSize) + (rowSign * (candidate.BlockSize.GetHeight() >> 1)) - 1; |
|||
Point sourcePoint = new(sourceX << MotionVectorSubpixelBits, sourceY << MotionVectorSubpixelBits); |
|||
Av1MotionVector motionVector = candidate.MotionVectors[0]; |
|||
|
|||
// Neighbor centers and motion vectors share Q3 precision. Adding them directly produces the corresponding
|
|||
// reference position without rounding away the fractional displacement needed by the projection solver.
|
|||
this.sourcePoints[this.Count] = sourcePoint; |
|||
this.referencePoints[this.Count] = new Point(sourcePoint.X + motionVector.Column, sourcePoint.Y + motionVector.Row); |
|||
this.Count++; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether a decoded neighbor can participate in overlapping motion compensation.
|
|||
/// </summary>
|
|||
/// <param name="candidate">The neighboring decoded block.</param>
|
|||
/// <returns><see langword="true"/> for inter prediction or intra-block copy; otherwise, <see langword="false"/>.</returns>
|
|||
private static bool IsOverlappable(Av1BlockModeInfo candidate) |
|||
=> candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; |
|||
|
|||
/// <summary>
|
|||
/// Provides fixed storage for AV1's eight local warped-motion projection samples.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The source or reference point type stored in the inline buffer.</typeparam>
|
|||
[InlineArray(ProjectionSampleCapacity)] |
|||
private struct InlineArray8<T> |
|||
{ |
|||
/// <summary>
|
|||
/// The first element in the compiler-expanded inline buffer.
|
|||
/// </summary>
|
|||
private T element; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
/// <summary>
|
|||
/// Identifies the fractional precision used to decode an AV1 motion-vector delta.
|
|||
/// </summary>
|
|||
internal enum Av1MotionVectorPrecision : sbyte |
|||
{ |
|||
/// <summary>
|
|||
/// Restricts components to whole-sample increments.
|
|||
/// </summary>
|
|||
Integer = -1, |
|||
|
|||
/// <summary>
|
|||
/// Allows components in quarter-sample increments.
|
|||
/// </summary>
|
|||
QuarterSample, |
|||
|
|||
/// <summary>
|
|||
/// Allows components in eighth-sample increments.
|
|||
/// </summary>
|
|||
EighthSample |
|||
} |
|||
@ -0,0 +1,906 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
/// <summary>
|
|||
/// Derives the weighted AV1 reference-motion-vector candidates for one single-reference inter block.
|
|||
/// </summary>
|
|||
internal sealed class Av1ReferenceMotionVectors |
|||
{ |
|||
/// <summary>
|
|||
/// The number of surrounding mode-information rows and columns examined by the spatial search.
|
|||
/// </summary>
|
|||
private const int ReferenceSearchDistance = 3; |
|||
|
|||
/// <summary>
|
|||
/// The weight separating immediately adjacent candidates from temporal and outer spatial candidates.
|
|||
/// </summary>
|
|||
private const int NearestCandidateWeight = 640; |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of distinct candidates retained by AV1.
|
|||
/// </summary>
|
|||
private const int CandidateCapacity = 8; |
|||
|
|||
/// <summary>
|
|||
/// The width and height of the outer spatial and temporal search boundary in 4x4 mode-information units.
|
|||
/// </summary>
|
|||
private const int MaximumSearchBlockSize = 16; |
|||
|
|||
/// <summary>
|
|||
/// The packed mode-context bit containing temporal availability relative to global motion.
|
|||
/// </summary>
|
|||
private const int GlobalMotionContextBit = 1 << 3; |
|||
|
|||
/// <summary>
|
|||
/// The bit offset of the reference-motion-vector context in the packed mode context.
|
|||
/// </summary>
|
|||
private const int ReferenceMotionVectorContextOffset = 4; |
|||
|
|||
/// <summary>
|
|||
/// Stores the unique candidates in their normative weighted order.
|
|||
/// </summary>
|
|||
private InlineArray8<Av1MotionVector> candidates; |
|||
|
|||
/// <summary>
|
|||
/// Stores the accumulated spatial or temporal weight corresponding to each candidate.
|
|||
/// </summary>
|
|||
private InlineArray8<ushort> weights; |
|||
|
|||
/// <summary>
|
|||
/// Stores the nearest and near references after applying AV1 fallback and precision rules.
|
|||
/// </summary>
|
|||
private InlineArray2<Av1MotionVector> references; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of valid entries in <see cref="Candidates"/> and <see cref="Weights"/>.
|
|||
/// </summary>
|
|||
public int Count { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the packed entropy context derived from adjacent, outer, and temporal candidates.
|
|||
/// </summary>
|
|||
public int ModeContext { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the derived candidates in normative nearest-region then outer-region order.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<Av1MotionVector> Candidates => this.candidates[..this.Count]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the accumulated weight corresponding to each entry in <see cref="Candidates"/>.
|
|||
/// </summary>
|
|||
public ReadOnlySpan<ushort> Weights => this.weights[..this.Count]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the nearest reference, or the current block's global-motion vector when no candidate exists.
|
|||
/// </summary>
|
|||
public Av1MotionVector Nearest => this.references[0]; |
|||
|
|||
/// <summary>
|
|||
/// Derives all single-reference motion-vector candidates for the current block.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and decoded spatial neighbors.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="frameInfo">The frame-wide spatial map and projected temporal motion field.</param>
|
|||
/// <param name="sequenceHeader">The sequence-level superblock and order-hint configuration.</param>
|
|||
/// <param name="frameHeader">The frame-level global-motion and motion-vector precision configuration.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
public void Build( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
Av1FrameInfo frameInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame) |
|||
{ |
|||
Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; |
|||
int width = blockSize.Get4x4WideCount(); |
|||
int height = blockSize.Get4x4HighCount(); |
|||
int row = partitionInfo.RowIndex; |
|||
int column = partitionInfo.ColumnIndex; |
|||
int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0; |
|||
int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0; |
|||
int maximumRowOffset = 0; |
|||
int maximumColumnOffset = 0; |
|||
|
|||
this.Count = 0; |
|||
this.ModeContext = 0; |
|||
|
|||
if (partitionInfo.AvailableAbove) |
|||
{ |
|||
maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment; |
|||
maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1); |
|||
} |
|||
|
|||
if (partitionInfo.AvailableLeft) |
|||
{ |
|||
maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment; |
|||
maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1); |
|||
} |
|||
|
|||
Av1GlobalMotionParameters globalMotion = frameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1]; |
|||
Av1MotionVector globalMotionVector = globalMotion.GetMotionVector( |
|||
frameHeader.AllowHighPrecisionMotionVector, |
|||
blockSize, |
|||
new Point(column, row), |
|||
frameHeader.ForceIntegerMotionVector); |
|||
|
|||
int processedRows = 0; |
|||
int processedColumns = 0; |
|||
int rowMatchCount = 0; |
|||
int columnMatchCount = 0; |
|||
int newMotionVectorCount = 0; |
|||
|
|||
// Immediate above and left scans form a distinct high-priority region. Their direction-level match counts,
|
|||
// rather than their number of unique vectors, drive the packed inter-mode entropy context.
|
|||
if (Math.Abs(maximumRowOffset) >= 1) |
|||
{ |
|||
this.ScanRow( |
|||
ref partitionInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
-1, |
|||
maximumRowOffset, |
|||
ref rowMatchCount, |
|||
ref newMotionVectorCount, |
|||
ref processedRows); |
|||
} |
|||
|
|||
if (Math.Abs(maximumColumnOffset) >= 1) |
|||
{ |
|||
this.ScanColumn( |
|||
ref partitionInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
-1, |
|||
maximumColumnOffset, |
|||
ref columnMatchCount, |
|||
ref newMotionVectorCount, |
|||
ref processedColumns); |
|||
} |
|||
|
|||
if (partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize)) |
|||
{ |
|||
this.AddSpatialBlock( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
-1, |
|||
width, |
|||
ref rowMatchCount, |
|||
ref newMotionVectorCount); |
|||
} |
|||
|
|||
int nearestMatch = (rowMatchCount > 0 ? 1 : 0) + (columnMatchCount > 0 ? 1 : 0); |
|||
int nearestCandidateCount = this.Count; |
|||
for (int index = 0; index < nearestCandidateCount; index++) |
|||
{ |
|||
this.weights[index] += NearestCandidateWeight; |
|||
} |
|||
|
|||
if (frameHeader.UseReferenceFrameMotionVectors) |
|||
{ |
|||
this.AddTemporalCandidates( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader.OrderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector); |
|||
} |
|||
|
|||
int ignoredNewMotionVectorCount = 0; |
|||
|
|||
// The top-left block begins the lower-priority outer region. Candidate deduplication still spans both
|
|||
// regions, while the two independent stable sorts below preserve the normative nearest-before-outer order.
|
|||
this.AddSpatialBlock( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
-1, |
|||
-1, |
|||
ref rowMatchCount, |
|||
ref ignoredNewMotionVectorCount); |
|||
|
|||
for (int index = 2; index <= ReferenceSearchDistance; index++) |
|||
{ |
|||
int rowOffset = -(index << 1) + 1 + rowAdjustment; |
|||
int columnOffset = -(index << 1) + 1 + columnAdjustment; |
|||
if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows) |
|||
{ |
|||
this.ScanRow( |
|||
ref partitionInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
rowOffset, |
|||
maximumRowOffset, |
|||
ref rowMatchCount, |
|||
ref ignoredNewMotionVectorCount, |
|||
ref processedRows); |
|||
} |
|||
|
|||
if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns) |
|||
{ |
|||
this.ScanColumn( |
|||
ref partitionInfo, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
columnOffset, |
|||
maximumColumnOffset, |
|||
ref columnMatchCount, |
|||
ref ignoredNewMotionVectorCount, |
|||
ref processedColumns); |
|||
} |
|||
} |
|||
|
|||
int referenceMatchCount = (rowMatchCount > 0 ? 1 : 0) + (columnMatchCount > 0 ? 1 : 0); |
|||
this.ModeContext |= nearestMatch switch |
|||
{ |
|||
0 => (referenceMatchCount >= 1 ? 1 : 0) | |
|||
(referenceMatchCount == 1 ? 1 << ReferenceMotionVectorContextOffset : |
|||
referenceMatchCount >= 2 ? 2 << ReferenceMotionVectorContextOffset : 0), |
|||
1 => (newMotionVectorCount > 0 ? 2 : 3) | |
|||
(referenceMatchCount == 1 ? 3 << ReferenceMotionVectorContextOffset : |
|||
referenceMatchCount >= 2 ? 4 << ReferenceMotionVectorContextOffset : 0), |
|||
_ => (newMotionVectorCount >= 1 ? 4 : 5) | (5 << ReferenceMotionVectorContextOffset), |
|||
}; |
|||
|
|||
this.SortByWeight(0, nearestCandidateCount); |
|||
this.SortByWeight(nearestCandidateCount, this.Count); |
|||
|
|||
int frameWidth = frameHeader.ModeInfoColumnCount; |
|||
int frameHeight = frameHeader.ModeInfoRowCount; |
|||
int modeInfoWidth = Math.Min(Math.Min(MaximumSearchBlockSize, width), frameWidth - column); |
|||
int modeInfoHeight = Math.Min(Math.Min(MaximumSearchBlockSize, height), frameHeight - row); |
|||
int extensionLength = Math.Min(modeInfoWidth, modeInfoHeight); |
|||
|
|||
// When the direct stack has fewer than two entries, AV1 extends it from every inter reference on the
|
|||
// immediate above and left blocks. Opposite temporal directions are sign-reversed into the target role.
|
|||
for (int index = 0; Math.Abs(maximumRowOffset) >= 1 && index < extensionLength && this.Count < 2;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1)); |
|||
this.AddExtensionCandidate(candidate, frameInfo, referenceFrame); |
|||
index += candidate.BlockSize.Get4x4WideCount(); |
|||
} |
|||
|
|||
for (int index = 0; Math.Abs(maximumColumnOffset) >= 1 && index < extensionLength && this.Count < 2;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index)); |
|||
this.AddExtensionCandidate(candidate, frameInfo, referenceFrame); |
|||
index += candidate.BlockSize.Get4x4HighCount(); |
|||
} |
|||
|
|||
for (int index = 0; index < this.Count; index++) |
|||
{ |
|||
this.candidates[index] = this.candidates[index].ClampReference( |
|||
blockSize.GetWidth(), |
|||
blockSize.GetHeight(), |
|||
partitionInfo.ModeBlockToLeftEdge, |
|||
partitionInfo.ModeBlockToRightEdge, |
|||
partitionInfo.ModeBlockToTopEdge, |
|||
partitionInfo.ModeBlockToBottomEdge); |
|||
} |
|||
|
|||
// The two-element reference list is separate from the full DRL stack. Missing entries use global motion,
|
|||
// and both entries undergo the same precision reduction as libaom's av1_find_best_ref_mvs output.
|
|||
this.references[0] = (this.Count > 0 ? this.candidates[0] : globalMotionVector).LowerPrecision( |
|||
frameHeader.AllowHighPrecisionMotionVector, |
|||
frameHeader.ForceIntegerMotionVector); |
|||
|
|||
this.references[1] = (this.Count > 1 ? this.candidates[1] : globalMotionVector).LowerPrecision( |
|||
frameHeader.AllowHighPrecisionMotionVector, |
|||
frameHeader.ForceIntegerMotionVector); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the near reference selected by a decoded dynamic-reference-list index.
|
|||
/// </summary>
|
|||
/// <param name="referenceMotionVectorIndex">The decoded zero-based dynamic-reference-list index.</param>
|
|||
/// <returns>The selected near motion vector.</returns>
|
|||
public Av1MotionVector GetNearReference(int referenceMotionVectorIndex) |
|||
=> referenceMotionVectorIndex == 0 ? this.references[1] : this.candidates[referenceMotionVectorIndex + 1]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the differential reference used to decode a new motion vector.
|
|||
/// </summary>
|
|||
/// <param name="referenceMotionVectorIndex">The decoded zero-based dynamic-reference-list index.</param>
|
|||
/// <returns>The selected stack candidate, or the nearest fallback when the stack contains one or no entries.</returns>
|
|||
public Av1MotionVector GetNewReference(int referenceMotionVectorIndex) |
|||
=> this.Count > 1 ? this.candidates[referenceMotionVectorIndex] : this.references[0]; |
|||
|
|||
/// <summary>
|
|||
/// Scans one spatial row using AV1's block-size-dependent steps and weights.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide mode map.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotion">The selected reference's global-motion model.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="rowOffset">The signed row offset from the current block in 4x4 units.</param>
|
|||
/// <param name="maximumRowOffset">The farthest permitted row offset inside the tile.</param>
|
|||
/// <param name="referenceMatchCount">Accumulates matching reference labels found in this scan direction.</param>
|
|||
/// <param name="newMotionVectorCount">Accumulates matching neighbors whose inter mode contains a new vector.</param>
|
|||
/// <param name="processedRows">Receives the spatial depth covered by block-height weighting.</param>
|
|||
private void ScanRow( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
in Av1GlobalMotionParameters globalMotion, |
|||
Av1MotionVector globalMotionVector, |
|||
int rowOffset, |
|||
int maximumRowOffset, |
|||
ref int referenceMatchCount, |
|||
ref int newMotionVectorCount, |
|||
ref int processedRows) |
|||
{ |
|||
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); |
|||
int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), MaximumSearchBlockSize); |
|||
int columnOffset = 0; |
|||
if (Math.Abs(rowOffset) > 1) |
|||
{ |
|||
columnOffset = 1; |
|||
if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2) |
|||
{ |
|||
columnOffset--; |
|||
} |
|||
} |
|||
|
|||
bool useFourUnitStep = width >= 4; |
|||
for (int index = 0; index < end;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( |
|||
new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset)); |
|||
|
|||
int candidateWidth = candidate.BlockSize.Get4x4WideCount(); |
|||
int length = Math.Min(width, candidateWidth); |
|||
if (useFourUnitStep) |
|||
{ |
|||
length = Math.Max(4, length); |
|||
} |
|||
else if (Math.Abs(rowOffset) > 1) |
|||
{ |
|||
length = Math.Max(2, length); |
|||
} |
|||
|
|||
int weight = 2; |
|||
if (width >= 2 && width <= candidateWidth) |
|||
{ |
|||
int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount()); |
|||
weight = Math.Max(weight, increment); |
|||
processedRows = increment - rowOffset - 1; |
|||
} |
|||
|
|||
this.AddCandidate( |
|||
candidate, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
length * weight, |
|||
ref referenceMatchCount, |
|||
ref newMotionVectorCount); |
|||
|
|||
index += length; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scans one spatial column using AV1's block-size-dependent steps and weights.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide mode map.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotion">The selected reference's global-motion model.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="columnOffset">The signed column offset from the current block in 4x4 units.</param>
|
|||
/// <param name="maximumColumnOffset">The farthest permitted column offset inside the tile.</param>
|
|||
/// <param name="referenceMatchCount">Accumulates matching reference labels found in this scan direction.</param>
|
|||
/// <param name="newMotionVectorCount">Accumulates matching neighbors whose inter mode contains a new vector.</param>
|
|||
/// <param name="processedColumns">Receives the spatial depth covered by block-width weighting.</param>
|
|||
private void ScanColumn( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
in Av1GlobalMotionParameters globalMotion, |
|||
Av1MotionVector globalMotionVector, |
|||
int columnOffset, |
|||
int maximumColumnOffset, |
|||
ref int referenceMatchCount, |
|||
ref int newMotionVectorCount, |
|||
ref int processedColumns) |
|||
{ |
|||
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); |
|||
int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), MaximumSearchBlockSize); |
|||
int rowOffset = 0; |
|||
if (Math.Abs(columnOffset) > 1) |
|||
{ |
|||
rowOffset = 1; |
|||
if ((partitionInfo.RowIndex & 1) != 0 && height < 2) |
|||
{ |
|||
rowOffset--; |
|||
} |
|||
} |
|||
|
|||
bool useFourUnitStep = height >= 4; |
|||
for (int index = 0; index < end;) |
|||
{ |
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( |
|||
new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index)); |
|||
|
|||
int candidateHeight = candidate.BlockSize.Get4x4HighCount(); |
|||
int length = Math.Min(height, candidateHeight); |
|||
if (useFourUnitStep) |
|||
{ |
|||
length = Math.Max(4, length); |
|||
} |
|||
else if (Math.Abs(columnOffset) > 1) |
|||
{ |
|||
length = Math.Max(2, length); |
|||
} |
|||
|
|||
int weight = 2; |
|||
if (height >= 2 && height <= candidateHeight) |
|||
{ |
|||
int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount()); |
|||
weight = Math.Max(weight, increment); |
|||
processedColumns = increment - columnOffset - 1; |
|||
} |
|||
|
|||
this.AddCandidate( |
|||
candidate, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
length * weight, |
|||
ref referenceMatchCount, |
|||
ref newMotionVectorCount); |
|||
|
|||
index += length; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds the candidate at one tile-relative spatial search position.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry and frame-wide mode map.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotion">The selected reference's global-motion model.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="rowOffset">The signed row offset from the current block in 4x4 units.</param>
|
|||
/// <param name="columnOffset">The signed column offset from the current block in 4x4 units.</param>
|
|||
/// <param name="referenceMatchCount">Accumulates matching reference labels at the search position.</param>
|
|||
/// <param name="newMotionVectorCount">Accumulates matching neighbors whose inter mode contains a new vector.</param>
|
|||
private void AddSpatialBlock( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
in Av1GlobalMotionParameters globalMotion, |
|||
Av1MotionVector globalMotionVector, |
|||
int rowOffset, |
|||
int columnOffset, |
|||
ref int referenceMatchCount, |
|||
ref int newMotionVectorCount) |
|||
{ |
|||
int row = partitionInfo.RowIndex + rowOffset; |
|||
int column = partitionInfo.ColumnIndex + columnOffset; |
|||
if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || |
|||
column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row)); |
|||
this.AddCandidate( |
|||
candidate, |
|||
referenceFrame, |
|||
in globalMotion, |
|||
globalMotionVector, |
|||
4, |
|||
ref referenceMatchCount, |
|||
ref newMotionVectorCount); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Accumulates matching references from one decoded inter block.
|
|||
/// </summary>
|
|||
/// <param name="candidate">The decoded neighboring block.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotion">The selected reference's global-motion model.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="weight">The spatial weight contributed by each matching reference.</param>
|
|||
/// <param name="referenceMatchCount">Accumulates matching reference labels in the active scan direction.</param>
|
|||
/// <param name="newMotionVectorCount">Accumulates matching neighbors whose inter mode contains a new vector.</param>
|
|||
private void AddCandidate( |
|||
Av1BlockModeInfo candidate, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
in Av1GlobalMotionParameters globalMotion, |
|||
Av1MotionVector globalMotionVector, |
|||
int weight, |
|||
ref int referenceMatchCount, |
|||
ref int newMotionVectorCount) |
|||
{ |
|||
Span<Av1ReferenceFrameType> candidateReferences = candidate.ReferenceFrames; |
|||
if (candidateReferences[0] <= Av1ReferenceFrameType.Intra) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Span<Av1MotionVector> candidateMotionVectors = candidate.MotionVectors; |
|||
for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) |
|||
{ |
|||
if (candidateReferences[referenceIndex] != referenceFrame) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// A non-translational global block has no independent translational candidate at the neighbor. AV1
|
|||
// therefore evaluates the selected reference's global model at the current block and contributes that
|
|||
// vector, but only for blocks large enough to use affine global prediction.
|
|||
bool useGlobalMotion = |
|||
(candidate.YMode is Av1PredictionMode.GlobalMotionVector or Av1PredictionMode.GlobalGlobalMotionVector) && |
|||
globalMotion.Type > Av1GlobalMotionType.Translation && |
|||
Math.Min(candidate.BlockSize.GetWidth(), candidate.BlockSize.GetHeight()) >= 8; |
|||
|
|||
Av1MotionVector motionVector = useGlobalMotion |
|||
? globalMotionVector |
|||
: candidateMotionVectors[referenceIndex]; |
|||
|
|||
this.AddUnique(motionVector, weight); |
|||
|
|||
// Every matching reference in a neighbor carrying a NEW component contributes to the adjacent NEWMV
|
|||
// context even when its vector deduplicates against an earlier stack entry.
|
|||
if (candidate.YMode is Av1PredictionMode.NewMotionVector or |
|||
Av1PredictionMode.NewNewMotionVector or |
|||
Av1PredictionMode.NearestNewMotionVector or |
|||
Av1PredictionMode.NewNearestMotionVector or |
|||
Av1PredictionMode.NearNewMotionVector or |
|||
Av1PredictionMode.NewNearMotionVector) |
|||
{ |
|||
newMotionVectorCount++; |
|||
} |
|||
|
|||
referenceMatchCount++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds projected temporal candidates over the current block and its permitted extension positions.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="frameInfo">The projected temporal motion field.</param>
|
|||
/// <param name="orderHintInfo">The sequence modulo order-hint configuration.</param>
|
|||
/// <param name="frameHeader">The frame-level motion-vector precision configuration.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
private void AddTemporalCandidates( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
Av1FrameInfo frameInfo, |
|||
ObuOrderHintInfo orderHintInfo, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
Av1MotionVector globalMotionVector) |
|||
{ |
|||
int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); |
|||
int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); |
|||
int verticalOffset = Math.Max(2, height); |
|||
int horizontalOffset = Math.Max(2, width); |
|||
int blockRowEnd = Math.Min(height, MaximumSearchBlockSize); |
|||
int blockColumnEnd = Math.Min(width, MaximumSearchBlockSize); |
|||
int rowStep = height >= MaximumSearchBlockSize ? 4 : 2; |
|||
int columnStep = width >= MaximumSearchBlockSize ? 4 : 2; |
|||
bool firstSampleAvailable = false; |
|||
|
|||
for (int blockRow = 0; blockRow < blockRowEnd; blockRow += rowStep) |
|||
{ |
|||
for (int blockColumn = 0; blockColumn < blockColumnEnd; blockColumn += columnStep) |
|||
{ |
|||
bool available = this.AddTemporalCandidate( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
orderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector, |
|||
blockRow, |
|||
blockColumn); |
|||
|
|||
if (blockRow == 0 && blockColumn == 0) |
|||
{ |
|||
firstSampleAvailable = available; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (!firstSampleAvailable) |
|||
{ |
|||
this.ModeContext |= GlobalMotionContextBit; |
|||
} |
|||
|
|||
bool allowExtension = height >= 2 && height < MaximumSearchBlockSize && width >= 2 && width < MaximumSearchBlockSize; |
|||
if (!allowExtension) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// These three positions extend the temporal search below-left, below-right, and above-right. The 64x64
|
|||
// boundary test is normative even when the sequence uses 128x128 superblocks.
|
|||
this.AddTemporalExtension( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
orderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector, |
|||
verticalOffset, |
|||
-2); |
|||
|
|||
this.AddTemporalExtension( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
orderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector, |
|||
verticalOffset, |
|||
horizontalOffset); |
|||
|
|||
this.AddTemporalExtension( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
orderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector, |
|||
verticalOffset - 2, |
|||
horizontalOffset); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds one optional temporal extension candidate after applying the normative 64x64 boundary rule.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="frameInfo">The projected temporal motion field.</param>
|
|||
/// <param name="orderHintInfo">The sequence modulo order-hint configuration.</param>
|
|||
/// <param name="frameHeader">The frame-level motion-vector precision configuration.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="blockRow">The temporal sample row relative to the current block in 4x4 units.</param>
|
|||
/// <param name="blockColumn">The temporal sample column relative to the current block in 4x4 units.</param>
|
|||
private void AddTemporalExtension( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
Av1FrameInfo frameInfo, |
|||
ObuOrderHintInfo orderHintInfo, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
Av1MotionVector globalMotionVector, |
|||
int blockRow, |
|||
int blockColumn) |
|||
{ |
|||
int rowWithinBlock64 = partitionInfo.RowIndex & (MaximumSearchBlockSize - 1); |
|||
int columnWithinBlock64 = partitionInfo.ColumnIndex & (MaximumSearchBlockSize - 1); |
|||
if (rowWithinBlock64 + blockRow < 0 || rowWithinBlock64 + blockRow >= MaximumSearchBlockSize || |
|||
columnWithinBlock64 + blockColumn < 0 || columnWithinBlock64 + blockColumn >= MaximumSearchBlockSize) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_ = this.AddTemporalCandidate( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
orderHintInfo, |
|||
frameHeader, |
|||
referenceFrame, |
|||
globalMotionVector, |
|||
blockRow, |
|||
blockColumn); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Projects and accumulates one temporal motion-field sample.
|
|||
/// </summary>
|
|||
/// <param name="partitionInfo">The current block geometry.</param>
|
|||
/// <param name="tileInfo">The active tile boundaries.</param>
|
|||
/// <param name="frameInfo">The projected temporal motion field.</param>
|
|||
/// <param name="orderHintInfo">The sequence modulo order-hint configuration.</param>
|
|||
/// <param name="frameHeader">The frame-level motion-vector precision configuration.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
/// <param name="globalMotionVector">The selected reference's global-motion vector at the current block.</param>
|
|||
/// <param name="blockRow">The temporal sample row relative to the current block in 4x4 units.</param>
|
|||
/// <param name="blockColumn">The temporal sample column relative to the current block in 4x4 units.</param>
|
|||
/// <returns><see langword="true"/> when the projected motion field covers the requested position.</returns>
|
|||
private bool AddTemporalCandidate( |
|||
ref Av1PartitionInfo partitionInfo, |
|||
Av1TileInfo tileInfo, |
|||
Av1FrameInfo frameInfo, |
|||
ObuOrderHintInfo orderHintInfo, |
|||
ObuFrameHeader frameHeader, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
Av1MotionVector globalMotionVector, |
|||
int blockRow, |
|||
int blockColumn) |
|||
{ |
|||
int rowOffset = (partitionInfo.RowIndex & 1) != 0 ? blockRow : blockRow + 1; |
|||
int columnOffset = (partitionInfo.ColumnIndex & 1) != 0 ? blockColumn : blockColumn + 1; |
|||
int row = partitionInfo.RowIndex + rowOffset; |
|||
int column = partitionInfo.ColumnIndex + columnOffset; |
|||
if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || |
|||
column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (!frameInfo.TryGetProjectedTemporalMotionVector( |
|||
row, |
|||
column, |
|||
referenceFrame, |
|||
orderHintInfo, |
|||
frameHeader.AllowHighPrecisionMotionVector, |
|||
frameHeader.ForceIntegerMotionVector, |
|||
out Av1MotionVector motionVector)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (blockRow == 0 && blockColumn == 0 && |
|||
(Math.Abs(motionVector.Row - globalMotionVector.Row) >= 16 || |
|||
Math.Abs(motionVector.Column - globalMotionVector.Column) >= 16)) |
|||
{ |
|||
// The packed global-motion context records whether the first temporal sample is absent or differs from
|
|||
// global motion by at least two full samples in either one-eighth-sample component.
|
|||
this.ModeContext |= GlobalMotionContextBit; |
|||
} |
|||
|
|||
this.AddUnique(motionVector, 2); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Extends a short stack with inter vectors from a neighboring block, correcting their temporal direction.
|
|||
/// </summary>
|
|||
/// <param name="candidate">The decoded neighboring block.</param>
|
|||
/// <param name="frameInfo">The reference-side classification for the current frame.</param>
|
|||
/// <param name="referenceFrame">The canonical inter reference selected for the current block.</param>
|
|||
private void AddExtensionCandidate( |
|||
Av1BlockModeInfo candidate, |
|||
Av1FrameInfo frameInfo, |
|||
Av1ReferenceFrameType referenceFrame) |
|||
{ |
|||
Span<Av1ReferenceFrameType> candidateReferences = candidate.ReferenceFrames; |
|||
Span<Av1MotionVector> candidateMotionVectors = candidate.MotionVectors; |
|||
bool targetSignBias = frameInfo.IsReferenceSignBiased(referenceFrame); |
|||
|
|||
for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) |
|||
{ |
|||
Av1ReferenceFrameType candidateReference = candidateReferences[referenceIndex]; |
|||
if (candidateReference <= Av1ReferenceFrameType.Intra) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
Av1MotionVector motionVector = candidateMotionVectors[referenceIndex]; |
|||
if (frameInfo.IsReferenceSignBiased(candidateReference) != targetSignBias) |
|||
{ |
|||
motionVector = new Av1MotionVector(-motionVector.Row, -motionVector.Column); |
|||
} |
|||
|
|||
int candidateIndex; |
|||
for (candidateIndex = 0; candidateIndex < this.Count; candidateIndex++) |
|||
{ |
|||
if (this.candidates[candidateIndex] == motionVector) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (candidateIndex == this.Count && this.Count < CandidateCapacity) |
|||
{ |
|||
// AV1's outer spatial extension only initializes a new stack entry. Unlike the weighted nearest and
|
|||
// temporal scans, finding an existing vector here must not change its previously accumulated rank.
|
|||
this.candidates[this.Count] = motionVector; |
|||
this.weights[this.Count] = 2; |
|||
this.Count++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds a unique candidate or accumulates the weight of an existing candidate.
|
|||
/// </summary>
|
|||
/// <param name="motionVector">The candidate vector in one-eighth-sample units.</param>
|
|||
/// <param name="weight">The spatial or temporal weight contributed by this occurrence.</param>
|
|||
private void AddUnique(Av1MotionVector motionVector, int weight) |
|||
{ |
|||
for (int index = 0; index < this.Count; index++) |
|||
{ |
|||
if (this.candidates[index] == motionVector) |
|||
{ |
|||
this.weights[index] += (ushort)weight; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
if (this.Count < CandidateCapacity) |
|||
{ |
|||
this.candidates[this.Count] = motionVector; |
|||
this.weights[this.Count] = (ushort)weight; |
|||
this.Count++; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sorts one candidate region by descending accumulated weight while retaining scan order for equal weights.
|
|||
/// </summary>
|
|||
/// <param name="start">The inclusive first candidate index in the region.</param>
|
|||
/// <param name="end">The exclusive end candidate index in the region.</param>
|
|||
private void SortByWeight(int start, int end) |
|||
{ |
|||
int length = end; |
|||
while (length > start) |
|||
{ |
|||
int lastSwap = start; |
|||
for (int index = start + 1; index < length; index++) |
|||
{ |
|||
if (this.weights[index - 1] < this.weights[index]) |
|||
{ |
|||
Av1MotionVector candidate = this.candidates[index - 1]; |
|||
this.candidates[index - 1] = this.candidates[index]; |
|||
this.candidates[index] = candidate; |
|||
|
|||
ushort weight = this.weights[index - 1]; |
|||
this.weights[index - 1] = this.weights[index]; |
|||
this.weights[index] = weight; |
|||
lastSwap = index; |
|||
} |
|||
} |
|||
|
|||
length = lastSwap; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides fixed storage for AV1's eight reference-motion-vector candidates.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The motion-vector or weight type stored in the inline buffer.</typeparam>
|
|||
[InlineArray(CandidateCapacity)] |
|||
private struct InlineArray8<T> |
|||
{ |
|||
/// <summary>
|
|||
/// The first element in the compiler-expanded inline buffer.
|
|||
/// </summary>
|
|||
private T element; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides fixed storage for the nearest and near motion-vector references.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The motion-vector type stored in the inline buffer.</typeparam>
|
|||
[InlineArray(2)] |
|||
private struct InlineArray2<T> |
|||
{ |
|||
/// <summary>
|
|||
/// The first element in the compiler-expanded inline buffer.
|
|||
/// </summary>
|
|||
private T element; |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies AV1 global-motion-vector derivation against the fixed-point rules used by the reference decoder.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1GlobalMotionParametersTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies that an identity model produces no displacement at every block position.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void IdentityModelProducesZeroMotionVector() |
|||
{ |
|||
Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; |
|||
|
|||
Av1MotionVector actual = parameters.GetMotionVector( |
|||
allowHighPrecisionMotionVector: true, |
|||
Av1BlockSize.Block128x128, |
|||
new Point(31, 17), |
|||
forceIntegerMotionVector: false); |
|||
|
|||
Assert.Equal(default, actual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies the published AV1 translation-component ordering and optional integer precision reduction.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(false, 19, -21)] |
|||
[InlineData(true, 16, -24)] |
|||
public void TranslationModelMatchesNormativeComponentOrdering(bool forceIntegerMotionVector, int expectedRow, int expectedColumn) |
|||
{ |
|||
Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; |
|||
parameters.Type = Av1GlobalMotionType.Translation; |
|||
|
|||
// Translation parameters retain sixteen fractional bits; the derived vector retains three.
|
|||
parameters[0] = 19 << 13; |
|||
parameters[1] = -21 << 13; |
|||
|
|||
Av1MotionVector actual = parameters.GetMotionVector( |
|||
allowHighPrecisionMotionVector: true, |
|||
Av1BlockSize.Block16x16, |
|||
new Point(4, 7), |
|||
forceIntegerMotionVector); |
|||
|
|||
Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies affine evaluation at the AV1 block center for high- and low-precision vector output.
|
|||
/// </summary>
|
|||
[Theory] |
|||
[InlineData(true, 1, 3)] |
|||
[InlineData(false, 0, 2)] |
|||
public void AffineModelEvaluatesBlockCenter(bool allowHighPrecisionMotionVector, int expectedRow, int expectedColumn) |
|||
{ |
|||
Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; |
|||
parameters.Type = Av1GlobalMotionType.Affine; |
|||
|
|||
// The 8x8 block at mode-info position (2, 3) has center (11, 15). These deltas produce horizontal and
|
|||
// vertical fixed-point offsets that exercise signed rounding at the selected output precision.
|
|||
parameters[0] = 2048; |
|||
parameters[1] = -1024; |
|||
parameters[2] = Av1GlobalMotionParameters.ModelScale + 1024; |
|||
parameters[3] = 512; |
|||
parameters[4] = -256; |
|||
parameters[5] = Av1GlobalMotionParameters.ModelScale + 768; |
|||
|
|||
Av1MotionVector actual = parameters.GetMotionVector( |
|||
allowHighPrecisionMotionVector, |
|||
Av1BlockSize.Block8x8, |
|||
new Point(2, 3), |
|||
forceIntegerMotionVector: false); |
|||
|
|||
Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
// 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.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the entropy state and block-size groups used by the AV1 inter-intra prediction flag.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1InterIntraEntropyTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies the four block-size-group distributions against libaom's forward Q15 defaults.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void DefaultsMatchLibaom() |
|||
{ |
|||
ReadOnlySpan<uint> forwardThresholds = [16384, 26887, 27597, 30237]; |
|||
Av1Distribution[] distributions = Av1DefaultDistributions.InterIntra; |
|||
|
|||
Assert.Equal(forwardThresholds.Length, distributions.Length); |
|||
for (int group = 0; group < distributions.Length; group++) |
|||
{ |
|||
// Av1Distribution stores inverse cumulative thresholds, so convert libaom's forward threshold before
|
|||
// comparing the exact Q15 state consumed by the range decoder.
|
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[group]; |
|||
|
|||
Assert.Equal(expected, distributions[group][0]); |
|||
Assert.Equal(2, distributions[group].NumberOfSymbols); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies every AV1 block size against the normative size-group conversion table.
|
|||
/// </summary>
|
|||
/// <param name="blockSizeValue">The AV1 block-size enumeration value.</param>
|
|||
/// <param name="expectedGroup">The normative zero-based size group.</param>
|
|||
[Theory] |
|||
[MemberData(nameof(GetBlockSizeGroups))] |
|||
public void GetSizeGroupMatchesNormativeTable(int blockSizeValue, int expectedGroup) |
|||
{ |
|||
Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; |
|||
|
|||
Assert.Equal(expectedGroup, blockSize.GetSizeGroup()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that the inter-intra flag reader selects and adapts the distribution for each size group.
|
|||
/// </summary>
|
|||
/// <param name="blockSizeValue">A block-size enumeration value representing one size group.</param>
|
|||
/// <param name="sizeGroup">The expected zero-based size group.</param>
|
|||
[Theory] |
|||
[InlineData((int)Av1BlockSize.Block4x4, 0)] |
|||
[InlineData((int)Av1BlockSize.Block8x8, 1)] |
|||
[InlineData((int)Av1BlockSize.Block16x16, 2)] |
|||
[InlineData((int)Av1BlockSize.Block32x32, 3)] |
|||
public void ReaderUsesBlockSizeGroup(int blockSizeValue, int sizeGroup) |
|||
{ |
|||
bool[] expected = [false, true, true, false, true, false, false, true]; |
|||
Av1Distribution writerDistribution = Av1DefaultDistributions.InterIntra[sizeGroup]; |
|||
using Av1SymbolWriter writer = new(Configuration.Default, expected.Length, updateCdf: true); |
|||
|
|||
foreach (bool value in expected) |
|||
{ |
|||
writer.WriteSymbol(value, writerDistribution); |
|||
} |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); |
|||
Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; |
|||
|
|||
foreach (bool value in expected) |
|||
{ |
|||
Assert.Equal(value, decoder.ReadIsInterIntra(blockSize)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that frame-context copies retain adapted inter-intra state without sharing mutable distributions.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyCopyRetainsIndependentState() |
|||
{ |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext destination = new(0); |
|||
source.InterIntra[2].Update(1); |
|||
|
|||
destination.CopyFrom(source); |
|||
|
|||
Assert.NotSame(source.InterIntra[2], destination.InterIntra[2]); |
|||
Assert.Equal(source.InterIntra[2][0], destination.InterIntra[2][0]); |
|||
|
|||
source.InterIntra[2].Update(0); |
|||
|
|||
Assert.NotEqual(source.InterIntra[2][0], destination.InterIntra[2][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that resetting a frame context restores the default threshold and adaptation state.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyResetRestoresDefaultState() |
|||
{ |
|||
Av1FrameEntropyContext context = new(0); |
|||
Av1FrameEntropyContext expected = new(0); |
|||
context.InterIntra[3].Update(1); |
|||
|
|||
context.ResetToDefaults(0); |
|||
|
|||
Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); |
|||
|
|||
// Applying the same next observation proves that reset restored the update-rate history as well as the visible
|
|||
// threshold; otherwise two equal thresholds would diverge because their adaptation rates differ.
|
|||
context.InterIntra[3].Update(0); |
|||
expected.InterIntra[3].Update(0); |
|||
|
|||
Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that a published frame snapshot preserves adapted thresholds and resets their update-rate history.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropySnapshotResetsUpdateCount() |
|||
{ |
|||
const int updateCount = 20; |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext snapshot = new(0); |
|||
|
|||
for (int i = 0; i < updateCount; i++) |
|||
{ |
|||
source.InterIntra[1].Update(1); |
|||
} |
|||
|
|||
source.SnapshotTo(snapshot); |
|||
|
|||
Assert.Equal(source.InterIntra[1][0], snapshot.InterIntra[1][0]); |
|||
|
|||
// The source retains twenty observations while the published snapshot restarts at zero. Their next identical
|
|||
// observation must therefore move the shared starting threshold by different update rates.
|
|||
source.InterIntra[1].Update(0); |
|||
snapshot.InterIntra[1].Update(0); |
|||
|
|||
Assert.NotEqual(source.InterIntra[1][0], snapshot.InterIntra[1][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides the normative AV1 size-group table in block-size enumeration order.
|
|||
/// </summary>
|
|||
/// <returns>Every decoded block size paired with its size group.</returns>
|
|||
public static TheoryData<int, int> GetBlockSizeGroups() |
|||
{ |
|||
// These are the explicit Size_Group values from AV1 section 9.3 and libaom common_data.h. The test keeps the
|
|||
// expected table independent from the production geometry formula so a shared calculation cannot mask errors.
|
|||
int[] sizeGroups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; |
|||
TheoryData<int, int> result = []; |
|||
|
|||
for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) |
|||
{ |
|||
result.Add(blockSize, sizeGroups[blockSize]); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,223 @@ |
|||
// 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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,314 @@ |
|||
// 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.Prediction.Inter; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the adaptive distributions and spatial contexts used by AV1 switchable interpolation filters.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1InterpolationFilterEntropyTests |
|||
{ |
|||
/// <summary>
|
|||
/// The number of reference, direction, and neighbor-state combinations represented by the distribution table.
|
|||
/// </summary>
|
|||
private const int SwitchableInterpolationContextCount = 16; |
|||
|
|||
/// <summary>
|
|||
/// The interpolation-filter direction index for vertical prediction.
|
|||
/// </summary>
|
|||
private const int VerticalDirection = 0; |
|||
|
|||
/// <summary>
|
|||
/// The interpolation-filter direction index for horizontal prediction.
|
|||
/// </summary>
|
|||
private const int HorizontalDirection = 1; |
|||
|
|||
/// <summary>
|
|||
/// Gets libaom's forward Q15 switchable interpolation-filter thresholds in context order.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<ushort> ForwardThresholds => |
|||
[ |
|||
31935, 32720, |
|||
5568, 32719, |
|||
422, 2938, |
|||
28244, 32608, |
|||
31206, 31953, |
|||
4862, 32121, |
|||
770, 1152, |
|||
20889, 25637, |
|||
31910, 32724, |
|||
4120, 32712, |
|||
305, 2247, |
|||
27403, 32636, |
|||
31022, 32009, |
|||
2963, 32093, |
|||
601, 943, |
|||
14969, 21398, |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Verifies every normative switchable interpolation-filter distribution against libaom's forward Q15 defaults.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void DefaultsMatchLibaom() |
|||
{ |
|||
const int thresholdCount = 2; |
|||
ReadOnlySpan<ushort> forwardThresholds = ForwardThresholds; |
|||
Av1Distribution[] distributions = Av1DefaultDistributions.SwitchableInterpolation; |
|||
|
|||
Assert.Equal(SwitchableInterpolationContextCount, distributions.Length); |
|||
for (int context = 0; context < distributions.Length; context++) |
|||
{ |
|||
Assert.Equal(thresholdCount + 1, distributions[context].NumberOfSymbols); |
|||
|
|||
for (int threshold = 0; threshold < thresholdCount; threshold++) |
|||
{ |
|||
// Av1Distribution stores inverse cumulative thresholds. Complement each published forward value by
|
|||
// the same Q15 probability top used during production construction before comparing exact state.
|
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[(context * thresholdCount) + threshold]; |
|||
|
|||
Assert.Equal(expected, distributions[context][threshold]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that the symbol reader selects and adapts each of the sixteen switchable interpolation contexts.
|
|||
/// </summary>
|
|||
/// <param name="context">The reference, direction, and neighbor filter context.</param>
|
|||
[Theory] |
|||
[MemberData(nameof(GetContexts))] |
|||
public void ReaderUsesRequestedContext(int context) |
|||
{ |
|||
Av1InterpolationFilter[] expected = |
|||
[ |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Sharp, |
|||
Av1InterpolationFilter.Smooth, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Smooth, |
|||
Av1InterpolationFilter.Sharp, |
|||
]; |
|||
|
|||
Av1Distribution writerDistribution = Av1DefaultDistributions.SwitchableInterpolation[context]; |
|||
using Av1SymbolWriter writer = new(Configuration.Default, expected.Length, updateCdf: true); |
|||
|
|||
foreach (Av1InterpolationFilter filter in expected) |
|||
{ |
|||
writer.WriteSymbol((int)filter, writerDistribution); |
|||
} |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); |
|||
|
|||
foreach (Av1InterpolationFilter filter in expected) |
|||
{ |
|||
Assert.Equal(filter, decoder.ReadSwitchableInterpolationFilter(context)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies all sixteen combinations of reference type, direction, and contributing neighbor filter state.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ContextLayoutMatchesLibaom() |
|||
{ |
|||
Av1BlockModeInfo single = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Regular); |
|||
|
|||
Av1BlockModeInfo compound = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.Backward, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Regular); |
|||
|
|||
Av1BlockModeInfo regular = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Regular); |
|||
|
|||
Av1BlockModeInfo smooth = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Smooth, |
|||
Av1InterpolationFilter.Smooth); |
|||
|
|||
Av1BlockModeInfo sharp = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Sharp, |
|||
Av1InterpolationFilter.Sharp); |
|||
|
|||
// Contexts zero through three are single-reference vertical contexts. Compound prediction adds four, while
|
|||
// horizontal prediction adds eight. The mixed Regular/Smooth pair selects the fourth neighbor state.
|
|||
Assert.Equal(0, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, null, VerticalDirection)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, smooth, null, VerticalDirection)); |
|||
Assert.Equal(2, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, sharp, null, VerticalDirection)); |
|||
Assert.Equal(3, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, smooth, VerticalDirection)); |
|||
Assert.Equal(4, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, null, VerticalDirection)); |
|||
Assert.Equal(5, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, smooth, null, VerticalDirection)); |
|||
Assert.Equal(6, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, sharp, null, VerticalDirection)); |
|||
Assert.Equal(7, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, smooth, VerticalDirection)); |
|||
Assert.Equal(8, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, null, HorizontalDirection)); |
|||
Assert.Equal(9, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, smooth, null, HorizontalDirection)); |
|||
Assert.Equal(10, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, sharp, null, HorizontalDirection)); |
|||
Assert.Equal(11, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, smooth, HorizontalDirection)); |
|||
Assert.Equal(12, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, null, HorizontalDirection)); |
|||
Assert.Equal(13, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, smooth, null, HorizontalDirection)); |
|||
Assert.Equal(14, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, sharp, null, HorizontalDirection)); |
|||
Assert.Equal(15, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, smooth, HorizontalDirection)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that only neighbors sharing the current primary reference contribute their directional filter.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ContextUsesMatchingPrimaryOrSecondaryNeighborReference() |
|||
{ |
|||
Av1BlockModeInfo current = CreateModeInfo( |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Regular); |
|||
|
|||
Av1BlockModeInfo secondaryMatch = CreateModeInfo( |
|||
Av1ReferenceFrameType.Golden, |
|||
Av1ReferenceFrameType.Last, |
|||
Av1InterpolationFilter.Smooth, |
|||
Av1InterpolationFilter.Sharp); |
|||
|
|||
Av1BlockModeInfo mismatch = CreateModeInfo( |
|||
Av1ReferenceFrameType.Golden, |
|||
Av1ReferenceFrameType.None, |
|||
Av1InterpolationFilter.Regular, |
|||
Av1InterpolationFilter.Regular); |
|||
|
|||
Assert.Equal(1, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, secondaryMatch, mismatch, VerticalDirection)); |
|||
Assert.Equal(10, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, secondaryMatch, mismatch, HorizontalDirection)); |
|||
Assert.Equal(3, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, mismatch, null, VerticalDirection)); |
|||
Assert.Equal(11, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, null, null, HorizontalDirection)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that frame-context copies retain interpolation adaptation without sharing mutable distributions.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyCopyRetainsIndependentInterpolationState() |
|||
{ |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext destination = new(0); |
|||
source.SwitchableInterpolation[15].Update((int)Av1InterpolationFilter.Sharp); |
|||
|
|||
destination.CopyFrom(source); |
|||
|
|||
Assert.Equal(source.SwitchableInterpolation[15][0], destination.SwitchableInterpolation[15][0]); |
|||
|
|||
source.SwitchableInterpolation[15].Update((int)Av1InterpolationFilter.Regular); |
|||
|
|||
Assert.NotEqual(source.SwitchableInterpolation[15][0], destination.SwitchableInterpolation[15][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that restoring frame defaults replaces adapted interpolation thresholds and update history.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyResetRestoresInterpolationDefaults() |
|||
{ |
|||
const int updateCount = 20; |
|||
Av1FrameEntropyContext context = new(0); |
|||
|
|||
for (int i = 0; i < updateCount; i++) |
|||
{ |
|||
context.SwitchableInterpolation[5].Update((int)Av1InterpolationFilter.Sharp); |
|||
} |
|||
|
|||
context.ResetToDefaults(0); |
|||
|
|||
Av1Distribution expected = Av1DefaultDistributions.SwitchableInterpolation[5]; |
|||
|
|||
Assert.Equal(expected[0], context.SwitchableInterpolation[5][0]); |
|||
Assert.Equal(expected[1], context.SwitchableInterpolation[5][1]); |
|||
|
|||
expected.Update((int)Av1InterpolationFilter.Smooth); |
|||
context.SwitchableInterpolation[5].Update((int)Av1InterpolationFilter.Smooth); |
|||
|
|||
Assert.Equal(expected[0], context.SwitchableInterpolation[5][0]); |
|||
Assert.Equal(expected[1], context.SwitchableInterpolation[5][1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that publishing frame state resets interpolation update history while retaining adapted thresholds.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropySnapshotResetsInterpolationUpdateCounts() |
|||
{ |
|||
const int updateCount = 20; |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext snapshot = new(0); |
|||
|
|||
for (int i = 0; i < updateCount; i++) |
|||
{ |
|||
source.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Smooth); |
|||
} |
|||
|
|||
source.SnapshotTo(snapshot); |
|||
|
|||
Assert.Equal(source.SwitchableInterpolation[7][0], snapshot.SwitchableInterpolation[7][0]); |
|||
|
|||
// The source retains its observations while the snapshot restarts at zero. Applying the same next symbol moves
|
|||
// identical thresholds by different amounts only when the new distribution participates in snapshot reset.
|
|||
source.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Regular); |
|||
snapshot.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Regular); |
|||
|
|||
Assert.NotEqual(source.SwitchableInterpolation[7][0], snapshot.SwitchableInterpolation[7][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides every switchable interpolation-filter context.
|
|||
/// </summary>
|
|||
/// <returns>The sixteen zero-based context indices.</returns>
|
|||
public static TheoryData<int> GetContexts() |
|||
{ |
|||
TheoryData<int> result = []; |
|||
|
|||
for (int context = 0; context < SwitchableInterpolationContextCount; context++) |
|||
{ |
|||
result.Add(context); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates decoded block state with the requested references and directional interpolation filters.
|
|||
/// </summary>
|
|||
/// <param name="primaryReference">The primary reference label.</param>
|
|||
/// <param name="secondaryReference">The optional secondary reference label.</param>
|
|||
/// <param name="verticalFilter">The vertical interpolation filter.</param>
|
|||
/// <param name="horizontalFilter">The horizontal interpolation filter.</param>
|
|||
/// <returns>The initialized block mode state.</returns>
|
|||
private static Av1BlockModeInfo CreateModeInfo( |
|||
Av1ReferenceFrameType primaryReference, |
|||
Av1ReferenceFrameType secondaryReference, |
|||
Av1InterpolationFilter verticalFilter, |
|||
Av1InterpolationFilter horizontalFilter) |
|||
{ |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); |
|||
modeInfo.ReferenceFrames[0] = primaryReference; |
|||
modeInfo.ReferenceFrames[1] = secondaryReference; |
|||
modeInfo.InterpolationFilters[0] = verticalFilter; |
|||
modeInfo.InterpolationFilters[1] = horizontalFilter; |
|||
return modeInfo; |
|||
} |
|||
} |
|||
@ -0,0 +1,223 @@ |
|||
// 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.Prediction.Inter; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the entropy defaults, lifecycle, and range-decoder alignment used by AV1 motion-mode syntax.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1MotionModeEntropyTests |
|||
{ |
|||
/// <summary>
|
|||
/// Gets libaom's forward Q15 Simple Translation, OBMC, and Warped thresholds in block-size order.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<ushort> MotionModeForwardThresholds => |
|||
[ |
|||
10923, 21845, |
|||
10923, 21845, |
|||
10923, 21845, |
|||
7651, 24760, |
|||
4738, 24765, |
|||
5391, 25528, |
|||
19419, 26810, |
|||
5123, 23606, |
|||
11606, 24308, |
|||
26260, 29116, |
|||
20360, 28062, |
|||
21679, 26830, |
|||
29516, 30701, |
|||
28898, 30397, |
|||
30878, 31335, |
|||
32507, 32558, |
|||
10923, 21845, |
|||
10923, 21845, |
|||
28799, 31390, |
|||
26431, 30774, |
|||
28973, 31594, |
|||
29742, 31203, |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Gets libaom's forward Q15 Simple Translation and OBMC thresholds in block-size order.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<ushort> ObmcForwardThresholds => |
|||
[ |
|||
16384, 16384, 16384, 10437, 9371, 9301, 17432, 14423, 15142, 25817, 22823, |
|||
22083, 30128, 31014, 31560, 32638, 16384, 16384, 23664, 20901, 24008, 26879, |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Verifies all twenty-two ternary and binary motion-mode distributions against libaom's forward Q15 defaults.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void DefaultsMatchLibaomForEveryBlockSize() |
|||
{ |
|||
const int blockSizeCount = (int)Av1BlockSize.AllSizes; |
|||
const int ternaryThresholdCount = 2; |
|||
ReadOnlySpan<ushort> motionModeForwardThresholds = MotionModeForwardThresholds; |
|||
ReadOnlySpan<ushort> obmcForwardThresholds = ObmcForwardThresholds; |
|||
Av1Distribution[] motionMode = Av1DefaultDistributions.MotionMode; |
|||
Av1Distribution[] obmc = Av1DefaultDistributions.Obmc; |
|||
|
|||
Assert.Equal(blockSizeCount * ternaryThresholdCount, motionModeForwardThresholds.Length); |
|||
Assert.Equal(blockSizeCount, obmcForwardThresholds.Length); |
|||
Assert.Equal(blockSizeCount, motionMode.Length); |
|||
Assert.Equal(blockSizeCount, obmc.Length); |
|||
|
|||
for (int blockSize = 0; blockSize < blockSizeCount; blockSize++) |
|||
{ |
|||
Assert.Equal(3, motionMode[blockSize].NumberOfSymbols); |
|||
Assert.Equal(2, obmc[blockSize].NumberOfSymbols); |
|||
|
|||
for (int threshold = 0; threshold < ternaryThresholdCount; threshold++) |
|||
{ |
|||
// Av1Distribution stores inverse cumulative thresholds, so complement libaom's published forward
|
|||
// Q15 values before comparing the exact state consumed by the range decoder.
|
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - |
|||
motionModeForwardThresholds[(blockSize * ternaryThresholdCount) + threshold]; |
|||
|
|||
Assert.Equal(expected, motionMode[blockSize][threshold]); |
|||
} |
|||
|
|||
uint expectedObmc = (uint)Av1Distribution.ProbabilityTop - obmcForwardThresholds[blockSize]; |
|||
|
|||
Assert.Equal(expectedObmc, obmc[blockSize][0]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that newly created frame contexts and explicit copies own independent motion-mode distributions.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyContextsDeepCopyAndCopyFromMotionModeState() |
|||
{ |
|||
int blockSize = (int)Av1BlockSize.Block16x16; |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext destination = new(0); |
|||
|
|||
Assert.NotSame(source.MotionMode[blockSize], destination.MotionMode[blockSize]); |
|||
Assert.NotSame(source.Obmc[blockSize], destination.Obmc[blockSize]); |
|||
|
|||
source.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); |
|||
source.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); |
|||
|
|||
Assert.NotEqual(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); |
|||
Assert.NotEqual(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); |
|||
|
|||
destination.CopyFrom(source); |
|||
|
|||
Assert.Equal(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); |
|||
Assert.Equal(source.MotionMode[blockSize][1], destination.MotionMode[blockSize][1]); |
|||
Assert.Equal(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); |
|||
|
|||
source.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
source.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
|
|||
Assert.NotEqual(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); |
|||
Assert.NotEqual(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that resetting a frame context restores both motion-mode thresholds and adaptation history.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyResetRestoresMotionModeState() |
|||
{ |
|||
const int updateCount = 20; |
|||
int blockSize = (int)Av1BlockSize.Block32x16; |
|||
Av1FrameEntropyContext context = new(0); |
|||
Av1FrameEntropyContext expected = new(0); |
|||
|
|||
for (int update = 0; update < updateCount; update++) |
|||
{ |
|||
context.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); |
|||
context.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); |
|||
} |
|||
|
|||
context.ResetToDefaults(0); |
|||
|
|||
Assert.Equal(expected.MotionMode[blockSize][0], context.MotionMode[blockSize][0]); |
|||
Assert.Equal(expected.MotionMode[blockSize][1], context.MotionMode[blockSize][1]); |
|||
Assert.Equal(expected.Obmc[blockSize][0], context.Obmc[blockSize][0]); |
|||
|
|||
// Equal thresholds can still carry different observation counts. Applying the same next symbols proves that
|
|||
// ResetToDefaults restored the update-rate history as well as the visible probability thresholds.
|
|||
context.MotionMode[blockSize].Update((int)Av1MotionMode.Obmc); |
|||
expected.MotionMode[blockSize].Update((int)Av1MotionMode.Obmc); |
|||
context.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
expected.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
|
|||
Assert.Equal(expected.MotionMode[blockSize][0], context.MotionMode[blockSize][0]); |
|||
Assert.Equal(expected.MotionMode[blockSize][1], context.MotionMode[blockSize][1]); |
|||
Assert.Equal(expected.Obmc[blockSize][0], context.Obmc[blockSize][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that a published frame snapshot retains adapted motion-mode thresholds but resets update counts.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropySnapshotResetsMotionModeUpdateCounts() |
|||
{ |
|||
const int updateCount = 20; |
|||
int blockSize = (int)Av1BlockSize.Block16x32; |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext snapshot = new(0); |
|||
|
|||
for (int update = 0; update < updateCount; update++) |
|||
{ |
|||
source.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); |
|||
source.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); |
|||
} |
|||
|
|||
source.SnapshotTo(snapshot); |
|||
|
|||
Assert.Equal(source.MotionMode[blockSize][0], snapshot.MotionMode[blockSize][0]); |
|||
Assert.Equal(source.MotionMode[blockSize][1], snapshot.MotionMode[blockSize][1]); |
|||
Assert.Equal(source.Obmc[blockSize][0], snapshot.Obmc[blockSize][0]); |
|||
|
|||
// The source retains twenty observations while the snapshot restarts at zero. Identical next observations
|
|||
// therefore move their equal starting thresholds by different update rates.
|
|||
source.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
snapshot.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
source.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
snapshot.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); |
|||
|
|||
Assert.NotEqual(source.MotionMode[blockSize][0], snapshot.MotionMode[blockSize][0]); |
|||
Assert.NotEqual(source.Obmc[blockSize][0], snapshot.Obmc[blockSize][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that both motion-mode alphabets leave the range decoder aligned for the immediately following filter symbol.
|
|||
/// </summary>
|
|||
/// <param name="allowWarpedMotion">Whether the motion-mode symbol uses the ternary rather than binary distribution.</param>
|
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void ReadMotionModePreservesFollowingInterpolationSymbolAlignment(bool allowWarpedMotion) |
|||
{ |
|||
Av1BlockSize blockSize = Av1BlockSize.Block16x16; |
|||
const int interpolationContext = 3; |
|||
Av1Distribution motionModeDistribution = allowWarpedMotion |
|||
? Av1DefaultDistributions.MotionMode[(int)blockSize] |
|||
: Av1DefaultDistributions.Obmc[(int)blockSize]; |
|||
|
|||
using Av1SymbolWriter writer = new(Configuration.Default, 2, updateCdf: true); |
|||
writer.WriteSymbol((int)Av1MotionMode.SimpleTranslation, motionModeDistribution); |
|||
writer.WriteSymbol( |
|||
(int)Av1InterpolationFilter.Sharp, |
|||
Av1DefaultDistributions.SwitchableInterpolation[interpolationContext]); |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); |
|||
|
|||
Assert.Equal(Av1MotionMode.SimpleTranslation, decoder.ReadMotionMode(blockSize, allowWarpedMotion)); |
|||
Assert.Equal(Av1InterpolationFilter.Sharp, decoder.ReadSwitchableInterpolationFilter(interpolationContext)); |
|||
} |
|||
} |
|||
@ -0,0 +1,206 @@ |
|||
// 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.Motion; |
|||
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.Prediction.Inter; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies inter-intra, motion-mode, and interpolation-filter syntax ordering in inter-frame mode parsing.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1MotionModeInfoTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies that an extended 8x32 rectangle omits inter-intra syntax and reads the following interpolation filter.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ReadInterFrameModeInfoOmitsInterIntraFlagForExtendedRectangle() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
sequenceHeader.EnableInterIntraCompound = true; |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
ConfigureForcedTranslationalGlobalMotion(frameHeader); |
|||
|
|||
using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x32, Point.Empty); |
|||
Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); |
|||
|
|||
using Av1SymbolWriter writer = new(Configuration.Default, 2, updateCdf: true); |
|||
writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); |
|||
|
|||
// With no matching above or left filter, a single-reference vertical filter uses context three. Writing the
|
|||
// filter immediately after Skip makes any accidental extended-rectangle inter-intra read desynchronize it.
|
|||
writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]); |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); |
|||
|
|||
tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); |
|||
|
|||
Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode); |
|||
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); |
|||
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that a false inter-intra flag continues through omitted, binary, and ternary Simple Translation syntax into interpolation.
|
|||
/// </summary>
|
|||
/// <param name="isMotionModeSwitchable">Whether the frame enables per-block motion-mode syntax.</param>
|
|||
/// <param name="allowWarpedMotion">Whether the eligible block uses the ternary rather than binary motion-mode distribution.</param>
|
|||
[Theory] |
|||
[InlineData(false, false)] |
|||
[InlineData(true, false)] |
|||
[InlineData(true, true)] |
|||
public void ReadInterFrameModeInfoContinuesFromFalseInterIntraThroughMotionModeIntoInterpolation( |
|||
bool isMotionModeSwitchable, |
|||
bool allowWarpedMotion) |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
sequenceHeader.EnableInterIntraCompound = true; |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
frameHeader.IsMotionModeSwitchable = isMotionModeSwitchable; |
|||
frameHeader.AllowWarpedMotion = allowWarpedMotion; |
|||
ConfigureForcedTranslationalGlobalMotion(frameHeader); |
|||
|
|||
using Av1ReferenceFrameStore referenceFrames = new(); |
|||
using Av1FrameInfo retainedFrameInfo = new(sequenceHeader); |
|||
Av1ReferenceFrame retainedFrame = new( |
|||
new Av1FrameBuffer<byte>(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), |
|||
CreateFrameHeader(), |
|||
retainedFrameInfo); |
|||
|
|||
Assert.True(referenceFrames.Commit(1, retainedFrame, showFrame: false)); |
|||
|
|||
Av1FrameEntropyContexts entropyContexts = new(0); |
|||
using Av1TileReader tileReader = new( |
|||
Configuration.Default, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
entropyContexts, |
|||
null, |
|||
referenceFrames); |
|||
|
|||
Av1SuperblockInfo superblockInfo = tileReader.FrameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block8x8, Point.Empty) |
|||
{ |
|||
YMode = Av1PredictionMode.NearestMotionVector, |
|||
}; |
|||
|
|||
aboveModeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; |
|||
aboveModeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; |
|||
aboveModeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Regular); |
|||
tileReader.FrameInfo.UpdateModeInfo(aboveModeInfo, superblockInfo); |
|||
superblockInfo.BlockCount++; |
|||
|
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(0, 2)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 0, |
|||
RowIndex = 2, |
|||
AvailableAbove = true, |
|||
AboveModeInfo = aboveModeInfo, |
|||
}; |
|||
|
|||
using Av1SymbolWriter writer = new(Configuration.Default, 4, updateCdf: true); |
|||
writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); |
|||
writer.WriteSymbol(false, Av1DefaultDistributions.InterIntra[Av1BlockSize.Block8x8.GetSizeGroup()]); |
|||
|
|||
if (isMotionModeSwitchable) |
|||
{ |
|||
Av1Distribution motionModeDistribution = allowWarpedMotion |
|||
? Av1DefaultDistributions.MotionMode[(int)Av1BlockSize.Block8x8] |
|||
: Av1DefaultDistributions.Obmc[(int)Av1BlockSize.Block8x8]; |
|||
|
|||
writer.WriteSymbol((int)Av1MotionMode.SimpleTranslation, motionModeDistribution); |
|||
} |
|||
|
|||
// The matching regular above neighbor selects vertical context zero. Sharp is deliberately non-default so the
|
|||
// assertion proves that every preceding conditional symbol consumed exactly its own range-coded interval.
|
|||
writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[0]); |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); |
|||
|
|||
tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); |
|||
|
|||
Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]); |
|||
Assert.Equal(Av1ReferenceFrameType.None, modeInfo.ReferenceFrames[1]); |
|||
Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode); |
|||
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); |
|||
Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the monochrome 64x64 sequence geometry used by direct inter-mode syntax tests.
|
|||
/// </summary>
|
|||
/// <returns>The initialized sequence header.</returns>
|
|||
private static ObuSequenceHeader CreateSequenceHeader() |
|||
=> new() |
|||
{ |
|||
MaxFrameWidth = 64, |
|||
MaxFrameHeight = 64, |
|||
Use128x128Superblock = false, |
|||
EnableDualFilter = false, |
|||
EnableCdef = false, |
|||
EnableFilterIntra = false, |
|||
ColorConfig = new ObuColorConfig |
|||
{ |
|||
IsMonochrome = true, |
|||
BitDepth = Av1BitDepth.EightBit, |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates an inter-frame header whose one tile and coded dimensions cover the complete test frame.
|
|||
/// </summary>
|
|||
/// <returns>The initialized frame header.</returns>
|
|||
private static ObuFrameHeader CreateFrameHeader() |
|||
=> new() |
|||
{ |
|||
FrameType = ObuFrameType.InterFrame, |
|||
ModeInfoColumnCount = 16, |
|||
ModeInfoRowCount = 16, |
|||
CodedLossless = true, |
|||
AllowScreenContentTools = false, |
|||
InterpolationFilter = Av1InterpolationFilter.Switchable, |
|||
FrameSize = new ObuFrameSize |
|||
{ |
|||
FrameWidth = 64, |
|||
FrameHeight = 64, |
|||
SuperResolutionUpscaledWidth = 64, |
|||
RenderWidth = 64, |
|||
RenderHeight = 64, |
|||
}, |
|||
TilesInfo = new ObuTileGroupHeader |
|||
{ |
|||
TileColumnCount = 1, |
|||
TileRowCount = 1, |
|||
TileColumnStartModeInfo = [0, 16], |
|||
TileRowStartModeInfo = [0, 16], |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Forces segment zero to a translational global-motion mode that omits reference and inter-mode symbols but still carries interpolation.
|
|||
/// </summary>
|
|||
/// <param name="frameHeader">The frame header to configure.</param>
|
|||
private static void ConfigureForcedTranslationalGlobalMotion(ObuFrameHeader frameHeader) |
|||
{ |
|||
ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; |
|||
segmentationParameters.Enabled = true; |
|||
segmentationParameters.FeatureEnabled[0, (int)ObuSegmentationLevelFeature.GlobalMotionVector] = true; |
|||
frameHeader.GetGlobalMotionParameters()[0].Type = Av1GlobalMotionType.Translation; |
|||
frameHeader.GetReferenceFrameIndices()[0] = 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,392 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the spatial neighbor and projection-sample rules used to select an AV1 motion mode.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1MotionVariationCandidatesTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies that overlap detection exhausts the complete above edge before falling back to the complete left edge.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildScansCompleteAboveEdgeThenFallsBackToLeftEdge() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
|
|||
for (int offset = 0; offset < 8; offset += 2) |
|||
{ |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(4 + offset, 2), |
|||
Av1BlockSize.Block8x8, |
|||
Av1ReferenceFrameType.Intra, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
|
|||
Av1ReferenceFrameType leftReference = offset == 6 |
|||
? Av1ReferenceFrameType.Last |
|||
: Av1ReferenceFrameType.Intra; |
|||
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(2, 4 + offset), |
|||
Av1BlockSize.Block8x8, |
|||
leftReference, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
} |
|||
|
|||
Av1PartitionInfo partitionInfo = CreatePartitionInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 4), |
|||
Av1BlockSize.Block32x32, |
|||
availableAbove: true, |
|||
availableLeft: true); |
|||
|
|||
Av1MotionVariationCandidates candidates = new(); |
|||
|
|||
candidates.Build( |
|||
ref partitionInfo, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.True(candidates.HasOverlappableNeighbor); |
|||
Assert.Equal(1, candidates.Count); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that 4x4 neighbors use the second mode record of each horizontal or vertical 8-sample pair.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildUsesSecondCellForFourSampleNeighborPairs() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
Av1FrameInfo horizontalFrameInfo = new(sequenceHeader); |
|||
AddModeInfo( |
|||
horizontalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 3), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Intra, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
|
|||
AddModeInfo( |
|||
horizontalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(5, 3), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
|
|||
Av1PartitionInfo horizontalPartition = CreatePartitionInfo( |
|||
horizontalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 4), |
|||
Av1BlockSize.Block8x8, |
|||
availableAbove: true, |
|||
availableLeft: false); |
|||
|
|||
Av1MotionVariationCandidates horizontalCandidates = new(); |
|||
horizontalCandidates.Build( |
|||
ref horizontalPartition, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Av1FrameInfo verticalFrameInfo = new(sequenceHeader); |
|||
AddModeInfo( |
|||
verticalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(3, 4), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Intra, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
|
|||
AddModeInfo( |
|||
verticalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(3, 5), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Last, |
|||
Av1ReferenceFrameType.None, |
|||
default); |
|||
|
|||
Av1PartitionInfo verticalPartition = CreatePartitionInfo( |
|||
verticalFrameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 4), |
|||
Av1BlockSize.Block8x8, |
|||
availableAbove: false, |
|||
availableLeft: true); |
|||
|
|||
Av1MotionVariationCandidates verticalCandidates = new(); |
|||
verticalCandidates.Build( |
|||
ref verticalPartition, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.True(horizontalCandidates.HasOverlappableNeighbor); |
|||
Assert.True(verticalCandidates.HasOverlappableNeighbor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that projection samples require a matching single reference and stop at the normative capacity of eight.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildCollectsMatchingSingleReferenceSamplesUpToCapacity() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
|
|||
// The first candidate has the wrong primary reference and the second is compound. Ten following candidates
|
|||
// are eligible, so the retained range must begin at offset two and stop after eight samples at offset nine.
|
|||
for (int offset = 0; offset < 12; offset++) |
|||
{ |
|||
Av1ReferenceFrameType primaryReference = offset == 0 |
|||
? Av1ReferenceFrameType.Golden |
|||
: Av1ReferenceFrameType.Last; |
|||
|
|||
Av1ReferenceFrameType secondaryReference = offset == 1 |
|||
? Av1ReferenceFrameType.Golden |
|||
: Av1ReferenceFrameType.None; |
|||
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(8 + offset, 15), |
|||
Av1BlockSize.Block4x4, |
|||
primaryReference, |
|||
secondaryReference, |
|||
new Av1MotionVector(offset, offset + 1)); |
|||
} |
|||
|
|||
Av1PartitionInfo partitionInfo = CreatePartitionInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(8, 16), |
|||
Av1BlockSize.Block64x64, |
|||
availableAbove: true, |
|||
availableLeft: false); |
|||
|
|||
Av1MotionVariationCandidates candidates = new(); |
|||
|
|||
candidates.Build( |
|||
ref partitionInfo, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(8, candidates.Count); |
|||
|
|||
// Positions are Q3 neighbor centers relative to the current block. Reference points add the corresponding
|
|||
// Q3 motion vector without rounding, which also proves that the rejected first two candidates were skipped.
|
|||
Assert.Equal(new Point(72, -24), candidates.SourcePoints[0]); |
|||
Assert.Equal(new Point(75, -22), candidates.ReferencePoints[0]); |
|||
Assert.Equal(new Point(296, -24), candidates.SourcePoints[7]); |
|||
Assert.Equal(new Point(306, -15), candidates.ReferencePoints[7]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that eligible top-left and top-right diagonal blocks contribute after the direct edge neighbors.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildIncludesEligibleTopLeftAndTopRightSamples() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(4, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(2, 4), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(2, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(6, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
|
|||
Av1PartitionInfo partitionInfo = CreatePartitionInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 4), |
|||
Av1BlockSize.Block8x8, |
|||
availableAbove: true, |
|||
availableLeft: true); |
|||
|
|||
Av1MotionVariationCandidates candidates = new(); |
|||
|
|||
candidates.Build( |
|||
ref partitionInfo, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(4, candidates.Count); |
|||
Assert.Equal(new Point(24, -40), candidates.SourcePoints[0]); |
|||
Assert.Equal(new Point(-40, 24), candidates.SourcePoints[1]); |
|||
Assert.Equal(new Point(-40, -40), candidates.SourcePoints[2]); |
|||
Assert.Equal(new Point(88, -40), candidates.SourcePoints[3]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that edge blocks already covering the diagonal positions suppress duplicate top-left and top-right samples.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildSuppressesDiagonalSamplesCoveredByEdgeNeighbors() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(); |
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
|
|||
// The aligned 16x8 above block covers the top-right position. The 8x16 left block begins two mode-info rows
|
|||
// above the current block and therefore covers its top-left position.
|
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(4, 4), Av1BlockSize.Block16x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(2, 4), Av1BlockSize.Block8x16, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); |
|||
|
|||
Av1PartitionInfo partitionInfo = CreatePartitionInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 6), |
|||
Av1BlockSize.Block8x8, |
|||
availableAbove: true, |
|||
availableLeft: true); |
|||
|
|||
Av1MotionVariationCandidates candidates = new(); |
|||
|
|||
candidates.Build( |
|||
ref partitionInfo, |
|||
new Av1TileInfo(0, 0, frameHeader), |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(2, candidates.Count); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the monochrome 128x128 sequence geometry used by spatial motion-mode tests.
|
|||
/// </summary>
|
|||
/// <returns>The initialized sequence header.</returns>
|
|||
private static ObuSequenceHeader CreateSequenceHeader() |
|||
=> new() |
|||
{ |
|||
MaxFrameWidth = 128, |
|||
MaxFrameHeight = 128, |
|||
Use128x128Superblock = false, |
|||
ColorConfig = new ObuColorConfig |
|||
{ |
|||
IsMonochrome = true, |
|||
BitDepth = Av1BitDepth.EightBit, |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates a single-tile frame covering the complete sequence geometry.
|
|||
/// </summary>
|
|||
/// <returns>The initialized frame header.</returns>
|
|||
private static ObuFrameHeader CreateFrameHeader() |
|||
=> new() |
|||
{ |
|||
FrameType = ObuFrameType.InterFrame, |
|||
ModeInfoColumnCount = 32, |
|||
ModeInfoRowCount = 32, |
|||
TilesInfo = new ObuTileGroupHeader |
|||
{ |
|||
TileColumnCount = 1, |
|||
TileRowCount = 1, |
|||
TileColumnStartModeInfo = [0, 32], |
|||
TileRowStartModeInfo = [0, 32], |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates one current partition at a frame-relative mode-information position.
|
|||
/// </summary>
|
|||
/// <param name="frameInfo">The frame map containing the neighboring mode records.</param>
|
|||
/// <param name="sequenceHeader">The sequence geometry defining superblock-relative addressing.</param>
|
|||
/// <param name="position">The frame-relative block origin in 4x4 units.</param>
|
|||
/// <param name="blockSize">The current block geometry.</param>
|
|||
/// <param name="availableAbove">Whether the above edge is available.</param>
|
|||
/// <param name="availableLeft">Whether the left edge is available.</param>
|
|||
/// <returns>The initialized partition state.</returns>
|
|||
private static Av1PartitionInfo CreatePartitionInfo( |
|||
Av1FrameInfo frameInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
Point position, |
|||
Av1BlockSize blockSize, |
|||
bool availableAbove, |
|||
bool availableLeft) |
|||
{ |
|||
int superblockSize = sequenceHeader.SuperblockModeInfoSize; |
|||
Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); |
|||
Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); |
|||
Av1BlockModeInfo modeInfo = new(blockSize, relativePosition); |
|||
|
|||
return new Av1PartitionInfo(modeInfo, frameInfo.GetSuperblock(superblockPosition), false, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = position.X, |
|||
RowIndex = position.Y, |
|||
AvailableAbove = availableAbove, |
|||
AvailableLeft = availableLeft, |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates and maps one decoded neighbor at a frame-relative mode-information position.
|
|||
/// </summary>
|
|||
/// <param name="frameInfo">The frame map that owns the neighbor.</param>
|
|||
/// <param name="sequenceHeader">The sequence geometry defining superblock-relative addressing.</param>
|
|||
/// <param name="position">The frame-relative block origin in 4x4 units.</param>
|
|||
/// <param name="blockSize">The neighboring block geometry.</param>
|
|||
/// <param name="primaryReference">The primary prediction reference.</param>
|
|||
/// <param name="secondaryReference">The optional secondary prediction reference.</param>
|
|||
/// <param name="motionVector">The primary motion vector in one-eighth-sample units.</param>
|
|||
private static void AddModeInfo( |
|||
Av1FrameInfo frameInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
Point position, |
|||
Av1BlockSize blockSize, |
|||
Av1ReferenceFrameType primaryReference, |
|||
Av1ReferenceFrameType secondaryReference, |
|||
Av1MotionVector motionVector) |
|||
{ |
|||
int superblockSize = sequenceHeader.SuperblockModeInfoSize; |
|||
Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); |
|||
Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); |
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(superblockPosition); |
|||
Av1BlockModeInfo modeInfo = new(blockSize, relativePosition) |
|||
{ |
|||
YMode = primaryReference == Av1ReferenceFrameType.Intra |
|||
? Av1PredictionMode.DC |
|||
: Av1PredictionMode.NearestMotionVector, |
|||
}; |
|||
|
|||
modeInfo.ReferenceFrames[0] = primaryReference; |
|||
modeInfo.ReferenceFrames[1] = secondaryReference; |
|||
modeInfo.MotionVectors[0] = motionVector; |
|||
frameInfo.UpdateModeInfo(modeInfo, superblockInfo); |
|||
superblockInfo.BlockCount++; |
|||
} |
|||
} |
|||
@ -0,0 +1,345 @@ |
|||
// 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.Motion; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the adaptive AV1 normal and displacement motion-vector entropy contexts.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1MotionVectorEntropyTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies both motion-vector contexts against every normative forward Q15 default from libaom.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void MotionVectorDefaultsMatchLibaom() |
|||
{ |
|||
Av1FrameEntropyContext context = new(0); |
|||
|
|||
Assert.NotSame(context.MotionVector, context.DisplacementVector); |
|||
AssertContextDefaults(context.MotionVector); |
|||
AssertContextDefaults(context.DisplacementVector); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies integer, quarter-sample, and eighth-sample syntax consumption and reconstruction.
|
|||
/// </summary>
|
|||
/// <param name="precisionValue">The numeric motion-vector precision.</param>
|
|||
/// <param name="horizontal">Indicates whether the coded delta occupies the horizontal component.</param>
|
|||
/// <param name="magnitudeClass">The coded magnitude class.</param>
|
|||
/// <param name="integerOffset">The coded integer magnitude offset.</param>
|
|||
/// <param name="fractional">The coded fractional symbol, or negative when omitted.</param>
|
|||
/// <param name="highPrecision">The coded eighth-sample symbol, or negative when omitted.</param>
|
|||
/// <param name="expectedMagnitude">The expected positive component in one-eighth-sample units.</param>
|
|||
[Theory] |
|||
[InlineData((int)Av1MotionVectorPrecision.Integer, false, 0, 1, -1, -1, 16)] |
|||
[InlineData((int)Av1MotionVectorPrecision.QuarterSample, true, 0, 0, 2, -1, 6)] |
|||
[InlineData((int)Av1MotionVectorPrecision.EighthSample, false, 0, 1, 1, 0, 11)] |
|||
[InlineData((int)Av1MotionVectorPrecision.EighthSample, true, 1, 1, 3, 1, 32)] |
|||
public void ReadMotionVectorUsesRequestedPrecision( |
|||
int precisionValue, |
|||
bool horizontal, |
|||
int magnitudeClass, |
|||
int integerOffset, |
|||
int fractional, |
|||
int highPrecision, |
|||
int expectedMagnitude) |
|||
{ |
|||
Av1MotionVectorPrecision precision = (Av1MotionVectorPrecision)precisionValue; |
|||
Av1MotionVectorContext writerContext = new(); |
|||
Av1MotionVectorContext.Component component = horizontal ? writerContext.Horizontal : writerContext.Vertical; |
|||
Av1Distribution trailingDistribution = Av1DefaultDistributions.Drl[1]; |
|||
using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); |
|||
|
|||
writer.WriteSymbol(horizontal ? 1 : 2, writerContext.Joint); |
|||
writer.WriteSymbol(false, component.Sign); |
|||
writer.WriteSymbol(magnitudeClass, component.MagnitudeClass); |
|||
|
|||
if (magnitudeClass == 0) |
|||
{ |
|||
writer.WriteSymbol(integerOffset, component.ClassZero); |
|||
} |
|||
else |
|||
{ |
|||
// CLASS0_BITS is one, so a nonzero class transmits exactly magnitudeClass integer-offset bits.
|
|||
for (int bit = 0; bit < magnitudeClass; bit++) |
|||
{ |
|||
writer.WriteSymbol((integerOffset >> bit) & 1, component.OffsetBits[bit]); |
|||
} |
|||
} |
|||
|
|||
if (precision != Av1MotionVectorPrecision.Integer) |
|||
{ |
|||
Av1Distribution fractionalDistribution = magnitudeClass == 0 |
|||
? component.ClassZeroFractional[integerOffset] |
|||
: component.Fractional; |
|||
|
|||
writer.WriteSymbol(fractional, fractionalDistribution); |
|||
} |
|||
|
|||
if (precision == Av1MotionVectorPrecision.EighthSample) |
|||
{ |
|||
Av1Distribution highPrecisionDistribution = magnitudeClass == 0 |
|||
? component.ClassZeroHighPrecision |
|||
: component.HighPrecision; |
|||
|
|||
writer.WriteSymbol(highPrecision, highPrecisionDistribution); |
|||
} |
|||
|
|||
// The trailing decision detects either an omitted precision symbol being consumed or a required one being skipped.
|
|||
writer.WriteSymbol(true, trailingDistribution); |
|||
|
|||
using IMemoryOwner<byte> encoded = writer.Exit(); |
|||
Av1FrameEntropyContext decoderContext = new(0); |
|||
uint normalJoint = decoderContext.MotionVector.Joint[0]; |
|||
uint displacementJoint = decoderContext.DisplacementVector.Joint[0]; |
|||
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, decoderContext, updateCdf: true); |
|||
Av1MotionVector reference = new(27, -11); |
|||
|
|||
Av1MotionVector actual = decoder.ReadMotionVector(reference, precision); |
|||
Av1MotionVector expected = horizontal |
|||
? new Av1MotionVector(reference.Row, reference.Column + expectedMagnitude) |
|||
: new Av1MotionVector(reference.Row + expectedMagnitude, reference.Column); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
Assert.NotEqual(normalJoint, decoderContext.MotionVector.Joint[0]); |
|||
Assert.Equal(displacementJoint, decoderContext.DisplacementVector.Joint[0]); |
|||
Assert.True(decoder.ReadDrl(1)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that normal and displacement motion vectors never share adaptive distribution state.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void NormalAndDisplacementContextsAdaptIndependently() |
|||
{ |
|||
Av1FrameEntropyContext context = new(0); |
|||
uint displacementJoint = context.DisplacementVector.Joint[0]; |
|||
uint normalFractional = context.MotionVector.Vertical.Fractional[0]; |
|||
|
|||
context.MotionVector.Joint.Update(3); |
|||
context.DisplacementVector.Vertical.Fractional.Update(2); |
|||
|
|||
Assert.NotEqual(displacementJoint, context.MotionVector.Joint[0]); |
|||
Assert.Equal(displacementJoint, context.DisplacementVector.Joint[0]); |
|||
Assert.Equal(normalFractional, context.MotionVector.Vertical.Fractional[0]); |
|||
Assert.NotEqual(normalFractional, context.DisplacementVector.Vertical.Fractional[0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that frame-context copies retain complete motion-vector state without sharing it.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyCopyRetainsIndependentMotionVectorState() |
|||
{ |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext destination = new(0); |
|||
UpdateState(source.MotionVector, 1, 5); |
|||
UpdateState(source.DisplacementVector, 1, 7); |
|||
|
|||
destination.CopyFrom(source); |
|||
|
|||
AssertStateEqual(source.MotionVector, destination.MotionVector); |
|||
AssertStateEqual(source.DisplacementVector, destination.DisplacementVector); |
|||
|
|||
UpdateState(source.MotionVector, 0, 1); |
|||
UpdateState(source.DisplacementVector, 0, 1); |
|||
|
|||
AssertStateNotEqual(source.MotionVector, destination.MotionVector); |
|||
AssertStateNotEqual(source.DisplacementVector, destination.DisplacementVector); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that publishing frame state resets every motion-vector update count.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropySnapshotResetsMotionVectorUpdateCounts() |
|||
{ |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext snapshot = new(0); |
|||
UpdateState(source.MotionVector, 1, 20); |
|||
UpdateState(source.DisplacementVector, 1, 20); |
|||
|
|||
source.SnapshotTo(snapshot); |
|||
|
|||
AssertStateEqual(source.MotionVector, snapshot.MotionVector); |
|||
AssertStateEqual(source.DisplacementVector, snapshot.DisplacementVector); |
|||
|
|||
// The source retains twenty observations while the snapshot restarts at zero. The same next symbol therefore
|
|||
// moves identical thresholds by different amounts only when every new distribution participates in reset.
|
|||
UpdateState(source.MotionVector, 0, 1); |
|||
UpdateState(snapshot.MotionVector, 0, 1); |
|||
UpdateState(source.DisplacementVector, 0, 1); |
|||
UpdateState(snapshot.DisplacementVector, 0, 1); |
|||
|
|||
AssertStateNotEqual(source.MotionVector, snapshot.MotionVector); |
|||
AssertStateNotEqual(source.DisplacementVector, snapshot.DisplacementVector); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies one complete motion-vector context against the normative defaults.
|
|||
/// </summary>
|
|||
/// <param name="context">The context under test.</param>
|
|||
private static void AssertContextDefaults(Av1MotionVectorContext context) |
|||
{ |
|||
Assert.NotSame(context.Vertical, context.Horizontal); |
|||
AssertDistribution(context.Joint, [4096, 11264, 19328]); |
|||
AssertComponentDefaults(context.Vertical); |
|||
AssertComponentDefaults(context.Horizontal); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies one component's complete set of normative defaults.
|
|||
/// </summary>
|
|||
/// <param name="component">The component under test.</param>
|
|||
private static void AssertComponentDefaults(Av1MotionVectorContext.Component component) |
|||
{ |
|||
AssertDistribution(component.MagnitudeClass, [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767]); |
|||
Assert.Equal(2, component.ClassZeroFractional.Length); |
|||
AssertDistribution(component.ClassZeroFractional[0], [16384, 24576, 26624]); |
|||
AssertDistribution(component.ClassZeroFractional[1], [12288, 21248, 24128]); |
|||
AssertDistribution(component.Fractional, [8192, 17408, 21248]); |
|||
AssertDistribution(component.Sign, [16384]); |
|||
AssertDistribution(component.ClassZeroHighPrecision, [20480]); |
|||
AssertDistribution(component.HighPrecision, [16384]); |
|||
AssertDistribution(component.ClassZero, [27648]); |
|||
|
|||
ReadOnlySpan<uint> offsetThresholds = [17408, 17920, 18944, 20480, 22528, 24576, 28672, 29952, 29952, 30720]; |
|||
|
|||
Assert.Equal(offsetThresholds.Length, component.OffsetBits.Length); |
|||
for (int bit = 0; bit < offsetThresholds.Length; bit++) |
|||
{ |
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - offsetThresholds[bit]; |
|||
|
|||
Assert.Equal(2, component.OffsetBits[bit].NumberOfSymbols); |
|||
Assert.Equal(expected, component.OffsetBits[bit][0]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies one distribution after conversion from forward to inverse cumulative thresholds.
|
|||
/// </summary>
|
|||
/// <param name="distribution">The distribution under test.</param>
|
|||
/// <param name="forwardThresholds">The normative forward Q15 thresholds.</param>
|
|||
private static void AssertDistribution(Av1Distribution distribution, ReadOnlySpan<uint> forwardThresholds) |
|||
{ |
|||
Assert.Equal(forwardThresholds.Length + 1, distribution.NumberOfSymbols); |
|||
for (int threshold = 0; threshold < forwardThresholds.Length; threshold++) |
|||
{ |
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[threshold]; |
|||
|
|||
Assert.Equal(expected, distribution[threshold]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies the same observations to every distribution in a motion-vector context.
|
|||
/// </summary>
|
|||
/// <param name="context">The context to adapt.</param>
|
|||
/// <param name="symbol">The coded symbol used for each observation.</param>
|
|||
/// <param name="count">The number of observations.</param>
|
|||
private static void UpdateState(Av1MotionVectorContext context, int symbol, int count) |
|||
{ |
|||
for (int update = 0; update < count; update++) |
|||
{ |
|||
context.Joint.Update(symbol); |
|||
UpdateState(context.Vertical, symbol); |
|||
UpdateState(context.Horizontal, symbol); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies one observation to every distribution in one motion-vector component.
|
|||
/// </summary>
|
|||
/// <param name="component">The component to adapt.</param>
|
|||
/// <param name="symbol">The coded symbol used for the observation.</param>
|
|||
private static void UpdateState(Av1MotionVectorContext.Component component, int symbol) |
|||
{ |
|||
component.MagnitudeClass.Update(symbol); |
|||
component.ClassZeroFractional[0].Update(symbol); |
|||
component.ClassZeroFractional[1].Update(symbol); |
|||
component.Fractional.Update(symbol); |
|||
component.Sign.Update(symbol); |
|||
component.ClassZeroHighPrecision.Update(symbol); |
|||
component.HighPrecision.Update(symbol); |
|||
component.ClassZero.Update(symbol); |
|||
|
|||
for (int bit = 0; bit < component.OffsetBits.Length; bit++) |
|||
{ |
|||
component.OffsetBits[bit].Update(symbol); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies equal adaptive thresholds across two motion-vector contexts.
|
|||
/// </summary>
|
|||
/// <param name="expected">The expected context.</param>
|
|||
/// <param name="actual">The actual context.</param>
|
|||
private static void AssertStateEqual(Av1MotionVectorContext expected, Av1MotionVectorContext actual) |
|||
{ |
|||
Assert.Equal(expected.Joint[0], actual.Joint[0]); |
|||
AssertStateEqual(expected.Vertical, actual.Vertical); |
|||
AssertStateEqual(expected.Horizontal, actual.Horizontal); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies equal adaptive thresholds across two motion-vector components.
|
|||
/// </summary>
|
|||
/// <param name="expected">The expected component.</param>
|
|||
/// <param name="actual">The actual component.</param>
|
|||
private static void AssertStateEqual(Av1MotionVectorContext.Component expected, Av1MotionVectorContext.Component actual) |
|||
{ |
|||
Assert.Equal(expected.MagnitudeClass[0], actual.MagnitudeClass[0]); |
|||
Assert.Equal(expected.ClassZeroFractional[0][0], actual.ClassZeroFractional[0][0]); |
|||
Assert.Equal(expected.ClassZeroFractional[1][0], actual.ClassZeroFractional[1][0]); |
|||
Assert.Equal(expected.Fractional[0], actual.Fractional[0]); |
|||
Assert.Equal(expected.Sign[0], actual.Sign[0]); |
|||
Assert.Equal(expected.ClassZeroHighPrecision[0], actual.ClassZeroHighPrecision[0]); |
|||
Assert.Equal(expected.HighPrecision[0], actual.HighPrecision[0]); |
|||
Assert.Equal(expected.ClassZero[0], actual.ClassZero[0]); |
|||
|
|||
for (int bit = 0; bit < expected.OffsetBits.Length; bit++) |
|||
{ |
|||
Assert.Equal(expected.OffsetBits[bit][0], actual.OffsetBits[bit][0]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies independently adaptive thresholds across two motion-vector contexts.
|
|||
/// </summary>
|
|||
/// <param name="expected">The independently adapted context.</param>
|
|||
/// <param name="actual">The copied or reset context.</param>
|
|||
private static void AssertStateNotEqual(Av1MotionVectorContext expected, Av1MotionVectorContext actual) |
|||
{ |
|||
Assert.NotEqual(expected.Joint[0], actual.Joint[0]); |
|||
AssertStateNotEqual(expected.Vertical, actual.Vertical); |
|||
AssertStateNotEqual(expected.Horizontal, actual.Horizontal); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies independently adaptive thresholds across two motion-vector components.
|
|||
/// </summary>
|
|||
/// <param name="expected">The independently adapted component.</param>
|
|||
/// <param name="actual">The copied or reset component.</param>
|
|||
private static void AssertStateNotEqual(Av1MotionVectorContext.Component expected, Av1MotionVectorContext.Component actual) |
|||
{ |
|||
Assert.NotEqual(expected.MagnitudeClass[0], actual.MagnitudeClass[0]); |
|||
Assert.NotEqual(expected.ClassZeroFractional[0][0], actual.ClassZeroFractional[0][0]); |
|||
Assert.NotEqual(expected.ClassZeroFractional[1][0], actual.ClassZeroFractional[1][0]); |
|||
Assert.NotEqual(expected.Fractional[0], actual.Fractional[0]); |
|||
Assert.NotEqual(expected.Sign[0], actual.Sign[0]); |
|||
Assert.NotEqual(expected.ClassZeroHighPrecision[0], actual.ClassZeroHighPrecision[0]); |
|||
Assert.NotEqual(expected.HighPrecision[0], actual.HighPrecision[0]); |
|||
Assert.NotEqual(expected.ClassZero[0], actual.ClassZero[0]); |
|||
|
|||
for (int bit = 0; bit < expected.OffsetBits.Length; bit++) |
|||
{ |
|||
Assert.NotEqual(expected.OffsetBits[bit][0], actual.OffsetBits[bit][0]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies AV1 motion-vector precision, range, spatial-clamp, and temporal-projection semantics.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1MotionVectorTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies that high-precision vectors retain their one-eighth-sample components unchanged.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void LowerPrecisionRetainsHighPrecisionComponents() |
|||
{ |
|||
Av1MotionVector vector = new(15, -15); |
|||
|
|||
Assert.Equal(vector, vector.LowerPrecision(allowHighPrecision: true, forceInteger: false)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that low precision removes odd one-eighth-sample components toward zero.
|
|||
/// </summary>
|
|||
/// <param name="row">The original vertical component.</param>
|
|||
/// <param name="column">The original horizontal component.</param>
|
|||
/// <param name="expectedRow">The expected low-precision vertical component.</param>
|
|||
/// <param name="expectedColumn">The expected low-precision horizontal component.</param>
|
|||
[Theory] |
|||
[InlineData(15, -15, 14, -14)] |
|||
[InlineData(14, -14, 14, -14)] |
|||
[InlineData(1, -1, 0, 0)] |
|||
public void LowerPrecisionReducesOddComponentsTowardZero(int row, int column, int expectedRow, int expectedColumn) |
|||
{ |
|||
Av1MotionVector actual = new Av1MotionVector(row, column).LowerPrecision(allowHighPrecision: false, forceInteger: false); |
|||
|
|||
Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies AV1 integer-sample rounding, including half-sample ties toward zero on both signs.
|
|||
/// </summary>
|
|||
/// <param name="component">The original component in one-eighth-sample units.</param>
|
|||
/// <param name="expected">The expected integer-precision component.</param>
|
|||
[Theory] |
|||
[InlineData(3, 0)] |
|||
[InlineData(4, 0)] |
|||
[InlineData(5, 8)] |
|||
[InlineData(11, 8)] |
|||
[InlineData(12, 8)] |
|||
[InlineData(13, 16)] |
|||
[InlineData(16, 16)] |
|||
[InlineData(-3, 0)] |
|||
[InlineData(-4, 0)] |
|||
[InlineData(-5, -8)] |
|||
[InlineData(-11, -8)] |
|||
[InlineData(-12, -8)] |
|||
[InlineData(-13, -16)] |
|||
[InlineData(-16, -16)] |
|||
public void LowerPrecisionRoundsIntegerHalfTiesTowardZero(int component, int expected) |
|||
{ |
|||
Av1MotionVector actual = new Av1MotionVector(component, -component).LowerPrecision( |
|||
allowHighPrecision: true, |
|||
forceInteger: true); |
|||
|
|||
Assert.Equal(new Av1MotionVector(expected, -expected), actual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that AV1 reserves both signed motion-vector endpoints.
|
|||
/// </summary>
|
|||
/// <param name="row">The vertical component.</param>
|
|||
/// <param name="column">The horizontal component.</param>
|
|||
/// <param name="expected">The expected validity.</param>
|
|||
[Theory] |
|||
[InlineData(-16383, 16383, true)] |
|||
[InlineData(-16384, 0, false)] |
|||
[InlineData(-16385, 0, false)] |
|||
[InlineData(16384, 0, false)] |
|||
[InlineData(16385, 0, false)] |
|||
[InlineData(0, -16384, false)] |
|||
[InlineData(0, 16384, false)] |
|||
public void IsValidUsesExclusiveMotionVectorEndpoints(int row, int column, bool expected) |
|||
=> Assert.Equal(expected, new Av1MotionVector(row, column).IsValid); |
|||
|
|||
/// <summary>
|
|||
/// Verifies the complete-block and sixteen-sample borders used to clamp spatial reference candidates.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ClampReferenceMatchesLibaomSpatialLimits() |
|||
{ |
|||
const int blockWidth = 16; |
|||
const int blockHeight = 8; |
|||
const int blockToLeftEdge = -256; |
|||
const int blockToRightEdge = 512; |
|||
const int blockToTopEdge = -128; |
|||
const int blockToBottomEdge = 384; |
|||
|
|||
Av1MotionVector upper = new Av1MotionVector(1000, 1000).ClampReference( |
|||
blockWidth, |
|||
blockHeight, |
|||
blockToLeftEdge, |
|||
blockToRightEdge, |
|||
blockToTopEdge, |
|||
blockToBottomEdge); |
|||
|
|||
Av1MotionVector lower = new Av1MotionVector(-1000, -1000).ClampReference( |
|||
blockWidth, |
|||
blockHeight, |
|||
blockToLeftEdge, |
|||
blockToRightEdge, |
|||
blockToTopEdge, |
|||
blockToBottomEdge); |
|||
|
|||
Av1MotionVector inside = new Av1MotionVector(48, -64).ClampReference( |
|||
blockWidth, |
|||
blockHeight, |
|||
blockToLeftEdge, |
|||
blockToRightEdge, |
|||
blockToTopEdge, |
|||
blockToBottomEdge); |
|||
|
|||
Assert.Equal(new Av1MotionVector(576, 768), upper); |
|||
Assert.Equal(new Av1MotionVector(-320, -512), lower); |
|||
Assert.Equal(new Av1MotionVector(48, -64), inside); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies AV1 fixed-point temporal projection, distance limiting, symmetric rounding, and endpoint clamping.
|
|||
/// </summary>
|
|||
/// <param name="row">The source vertical component.</param>
|
|||
/// <param name="column">The source horizontal component.</param>
|
|||
/// <param name="numerator">The signed source-to-target frame distance.</param>
|
|||
/// <param name="denominator">The positive source-to-reference frame distance.</param>
|
|||
/// <param name="expectedRow">The expected projected vertical component.</param>
|
|||
/// <param name="expectedColumn">The expected projected horizontal component.</param>
|
|||
[Theory] |
|||
[InlineData(64, -96, 2, 4, 32, -48)] |
|||
[InlineData(64, -96, -2, 4, -32, 48)] |
|||
[InlineData(2, -2, 1, 3, 1, -1)] |
|||
[InlineData(31, -31, 40, 40, 31, -31)] |
|||
[InlineData(4095, -4095, 31, 1, 16383, -16383)] |
|||
public void ProjectTemporalMatchesLibaom( |
|||
int row, |
|||
int column, |
|||
int numerator, |
|||
int denominator, |
|||
int expectedRow, |
|||
int expectedColumn) |
|||
{ |
|||
Av1MotionVector actual = new Av1MotionVector(row, column).ProjectTemporal(numerator, denominator); |
|||
|
|||
Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); |
|||
} |
|||
} |
|||
@ -0,0 +1,498 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the spatial, temporal, global, and extension rules used to derive single-reference AV1 motion vectors.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1ReferenceMotionVectorsTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies adjacent-direction counting, duplicate weighting, stable ordering, and the nearest, near, and new-reference accessors.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildOrdersAdjacentCandidatesAndPacksModeContext() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); |
|||
using Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
FillFrameWithIntraBlocks(frameInfo, sequenceHeader); |
|||
|
|||
Av1MotionVector above = new(24, -10); |
|||
Av1MotionVector left = new(-14, 30); |
|||
AddModeInfo(frameInfo, sequenceHeader, new Point(8, 4), Av1BlockSize.Block16x16, Av1ReferenceFrameType.Last, above, Av1PredictionMode.NewMotionVector); |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(4, 8), |
|||
Av1BlockSize.Block16x16, |
|||
Av1ReferenceFrameType.Last, |
|||
left, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(12, 7), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Last, |
|||
above, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 8, |
|||
RowIndex = 8, |
|||
}; |
|||
|
|||
Av1TileInfo tileInfo = new(0, 0, frameHeader); |
|||
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); |
|||
Av1ReferenceMotionVectors referenceMotionVectors = new(); |
|||
|
|||
referenceMotionVectors.Build( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(2, referenceMotionVectors.Count); |
|||
Assert.Equal(84, referenceMotionVectors.ModeContext); |
|||
Assert.Equal(above, referenceMotionVectors.Candidates[0]); |
|||
Assert.Equal(left, referenceMotionVectors.Candidates[1]); |
|||
Assert.Equal((ushort)660, referenceMotionVectors.Weights[0]); |
|||
Assert.Equal((ushort)656, referenceMotionVectors.Weights[1]); |
|||
Assert.Equal(above, referenceMotionVectors.Nearest); |
|||
Assert.Equal(left, referenceMotionVectors.GetNearReference(0)); |
|||
Assert.Equal(above, referenceMotionVectors.GetNewReference(0)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that outer candidates are weight-sorted independently without crossing the nearest-region boundary.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildSortsOuterCandidatesInsideTheirOwnRegion() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); |
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
FillFrameWithIntraBlocks(frameInfo, sequenceHeader); |
|||
|
|||
Av1MotionVector nearest = new(8, 16); |
|||
Av1MotionVector topLeft = new(24, 32); |
|||
Av1MotionVector outerRow = new(40, 48); |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(8, 7), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Last, |
|||
nearest, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
// A 4x4 intra neighbor keeps the adjacent scan from marking the deeper row as covered by a large background block.
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(9, 7), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Intra, |
|||
default, |
|||
Av1PredictionMode.DC); |
|||
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(7, 7), |
|||
Av1BlockSize.Block4x4, |
|||
Av1ReferenceFrameType.Last, |
|||
topLeft, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(9, 3), |
|||
Av1BlockSize.Block8x16, |
|||
Av1ReferenceFrameType.Last, |
|||
outerRow, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(8, 8)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 8, |
|||
RowIndex = 8, |
|||
}; |
|||
|
|||
Av1TileInfo tileInfo = new(0, 0, frameHeader); |
|||
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); |
|||
Av1ReferenceMotionVectors referenceMotionVectors = new(); |
|||
|
|||
referenceMotionVectors.Build( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(3, referenceMotionVectors.Count); |
|||
Assert.Equal(nearest, referenceMotionVectors.Candidates[0]); |
|||
Assert.Equal(outerRow, referenceMotionVectors.Candidates[1]); |
|||
Assert.Equal(topLeft, referenceMotionVectors.Candidates[2]); |
|||
Assert.Equal((ushort)642, referenceMotionVectors.Weights[0]); |
|||
Assert.Equal((ushort)8, referenceMotionVectors.Weights[1]); |
|||
Assert.Equal((ushort)4, referenceMotionVectors.Weights[2]); |
|||
Assert.Equal(51, referenceMotionVectors.ModeContext); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that an affine global-motion neighbor contributes the current block's global vector while extension retains its decoded vector.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildSubstitutesAffineGlobalMotionForDirectCandidate() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); |
|||
Av1GlobalMotionParameters globalMotion = Av1GlobalMotionParameters.Identity; |
|||
globalMotion.Type = Av1GlobalMotionType.Affine; |
|||
globalMotion[0] = 4096; |
|||
globalMotion[1] = -2048; |
|||
globalMotion[2] = Av1GlobalMotionParameters.ModelScale + 512; |
|||
globalMotion[5] = Av1GlobalMotionParameters.ModelScale; |
|||
frameHeader.GetGlobalMotionParameters()[0] = globalMotion; |
|||
|
|||
Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
FillFrameWithIntraBlocks(frameInfo, sequenceHeader); |
|||
Av1MotionVector decoded = new(40, -24); |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(8, 4), |
|||
Av1BlockSize.Block16x16, |
|||
Av1ReferenceFrameType.Last, |
|||
decoded, |
|||
Av1PredictionMode.GlobalMotionVector); |
|||
|
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 8, |
|||
RowIndex = 8, |
|||
}; |
|||
|
|||
Av1TileInfo tileInfo = new(0, 0, frameHeader); |
|||
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); |
|||
Av1ReferenceMotionVectors referenceMotionVectors = new(); |
|||
|
|||
referenceMotionVectors.Build( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Av1MotionVector expectedGlobal = globalMotion.GetMotionVector( |
|||
frameHeader.AllowHighPrecisionMotionVector, |
|||
modeInfo.BlockSize, |
|||
new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex), |
|||
frameHeader.ForceIntegerMotionVector); |
|||
|
|||
Assert.Equal(2, referenceMotionVectors.Count); |
|||
Assert.Equal(expectedGlobal, referenceMotionVectors.Candidates[0]); |
|||
Assert.Equal(decoded, referenceMotionVectors.Candidates[1]); |
|||
Assert.Equal((ushort)656, referenceMotionVectors.Weights[0]); |
|||
Assert.Equal((ushort)2, referenceMotionVectors.Weights[1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that stack extension reverses an opposite-side vector without reweighting a candidate already in the
|
|||
/// direct stack.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildReversesOppositeDirectionExtensionCandidate() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); |
|||
sequenceHeader.OrderHintInfo.EnableOrderHint = true; |
|||
sequenceHeader.OrderHintInfo.OrderHintBits = 5; |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 10, useReferenceFrameMotionVectors: false); |
|||
frameHeader.GetReferenceFrameIndices()[0] = 0; |
|||
frameHeader.GetReferenceFrameIndices()[4] = 1; |
|||
|
|||
using Av1ReferenceFrameStore referenceFrames = new(); |
|||
Av1ReferenceFrame past = CreateReferenceFrame(sequenceHeader, orderHint: 8); |
|||
Av1ReferenceFrame future = CreateReferenceFrame(sequenceHeader, orderHint: 12); |
|||
Assert.True(referenceFrames.Commit(1, past, showFrame: false)); |
|||
Assert.True(referenceFrames.Commit(2, future, showFrame: false)); |
|||
|
|||
using Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
frameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, frameHeader, referenceFrames); |
|||
FillFrameWithIntraBlocks(frameInfo, sequenceHeader); |
|||
Av1MotionVector direct = new(16, 24); |
|||
Av1BlockModeInfo candidate = AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(8, 4), |
|||
Av1BlockSize.Block16x16, |
|||
Av1ReferenceFrameType.Last, |
|||
direct, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
|
|||
// The direct scan adds the first reference with its normative adjacent weight. Extension visits both entries:
|
|||
// it must ignore that duplicate and append only the sign-corrected backward-reference vector.
|
|||
candidate.ReferenceFrames[1] = Av1ReferenceFrameType.Backward; |
|||
candidate.MotionVectors[1] = new Av1MotionVector(40, -24); |
|||
|
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 8, |
|||
RowIndex = 8, |
|||
}; |
|||
|
|||
Av1TileInfo tileInfo = new(0, 0, frameHeader); |
|||
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); |
|||
Av1ReferenceMotionVectors referenceMotionVectors = new(); |
|||
|
|||
referenceMotionVectors.Build( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Av1MotionVector expected = new(-40, 24); |
|||
Assert.Equal(2, referenceMotionVectors.Count); |
|||
Assert.Equal(direct, referenceMotionVectors.Candidates[0]); |
|||
Assert.Equal(expected, referenceMotionVectors.Candidates[1]); |
|||
Assert.Equal((ushort)656, referenceMotionVectors.Weights[0]); |
|||
Assert.Equal((ushort)2, referenceMotionVectors.Weights[1]); |
|||
Assert.Equal(direct, referenceMotionVectors.Nearest); |
|||
Assert.Equal(direct, referenceMotionVectors.GetNewReference(0)); |
|||
Assert.Equal(expected, referenceMotionVectors.GetNearReference(0)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies temporal field sampling, candidate deduplication, accumulated weight, and the global-motion context bit.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void BuildAccumulatesProjectedTemporalCandidates() |
|||
{ |
|||
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: true); |
|||
sequenceHeader.OrderHintInfo.EnableOrderHint = true; |
|||
sequenceHeader.OrderHintInfo.OrderHintBits = 5; |
|||
|
|||
using Av1ReferenceFrameStore priorReferences = new(); |
|||
Av1ReferenceFrame prior = CreateReferenceFrame(sequenceHeader, orderHint: 6); |
|||
Assert.True(priorReferences.Commit(1, prior, showFrame: false)); |
|||
|
|||
ObuFrameHeader sourceHeader = CreateFrameHeader(orderHint: 8, useReferenceFrameMotionVectors: false); |
|||
using Av1FrameInfo sourceFrameInfo = new(sequenceHeader); |
|||
sourceFrameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, sourceHeader, priorReferences); |
|||
FillFrameWithInterBlocks(sourceFrameInfo, sequenceHeader, Av1ReferenceFrameType.Last, default); |
|||
|
|||
using Av1ReferenceFrameStore sourceReferences = new(); |
|||
Av1ReferenceFrame source = new( |
|||
new Av1FrameBuffer<byte>(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), |
|||
sourceHeader, |
|||
sourceFrameInfo); |
|||
|
|||
Assert.True(sourceReferences.Commit(1, source, showFrame: false)); |
|||
|
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 10, useReferenceFrameMotionVectors: true); |
|||
using Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
frameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, frameHeader, sourceReferences); |
|||
FillFrameWithIntraBlocks(frameInfo, sequenceHeader); |
|||
|
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); |
|||
Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) |
|||
{ |
|||
ColumnIndex = 8, |
|||
RowIndex = 8, |
|||
}; |
|||
|
|||
Av1TileInfo tileInfo = new(0, 0, frameHeader); |
|||
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); |
|||
Av1ReferenceMotionVectors referenceMotionVectors = new(); |
|||
|
|||
referenceMotionVectors.Build( |
|||
ref partitionInfo, |
|||
tileInfo, |
|||
frameInfo, |
|||
sequenceHeader, |
|||
frameHeader, |
|||
Av1ReferenceFrameType.Last); |
|||
|
|||
Assert.Equal(1, referenceMotionVectors.Count); |
|||
Assert.Equal(default, referenceMotionVectors.Candidates[0]); |
|||
Assert.Equal((ushort)14, referenceMotionVectors.Weights[0]); |
|||
Assert.Equal(0, referenceMotionVectors.ModeContext); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates the monochrome 128-by-128 sequence geometry shared by reference-motion-vector tests.
|
|||
/// </summary>
|
|||
/// <param name="enableTemporalMotionVectors">Whether projected reference-frame motion vectors are enabled.</param>
|
|||
/// <returns>The configured sequence header.</returns>
|
|||
private static ObuSequenceHeader CreateSequenceHeader(bool enableTemporalMotionVectors) |
|||
=> new() |
|||
{ |
|||
MaxFrameWidth = 128, |
|||
MaxFrameHeight = 128, |
|||
Use128x128Superblock = false, |
|||
ColorConfig = new ObuColorConfig |
|||
{ |
|||
IsMonochrome = true, |
|||
BitDepth = Av1BitDepth.EightBit, |
|||
}, |
|||
OrderHintInfo = new ObuOrderHintInfo |
|||
{ |
|||
EnableReferenceFrameMotionVectors = enableTemporalMotionVectors, |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates one inter-frame header whose single tile covers the complete test frame.
|
|||
/// </summary>
|
|||
/// <param name="orderHint">The frame's modulo display-order hint.</param>
|
|||
/// <param name="useReferenceFrameMotionVectors">Whether this frame consumes its projected temporal motion field.</param>
|
|||
/// <returns>The configured frame header.</returns>
|
|||
private static ObuFrameHeader CreateFrameHeader(uint orderHint, bool useReferenceFrameMotionVectors) |
|||
=> new() |
|||
{ |
|||
FrameType = ObuFrameType.InterFrame, |
|||
OrderHint = orderHint, |
|||
ModeInfoColumnCount = 32, |
|||
ModeInfoRowCount = 32, |
|||
AllowHighPrecisionMotionVector = true, |
|||
UseReferenceFrameMotionVectors = useReferenceFrameMotionVectors, |
|||
TilesInfo = new ObuTileGroupHeader |
|||
{ |
|||
TileColumnCount = 1, |
|||
TileRowCount = 1, |
|||
TileColumnStartModeInfo = [0, 32], |
|||
TileRowStartModeInfo = [0, 32], |
|||
}, |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Maps one intra block over each 64-by-64 superblock so every spatial search position has initialized mode information.
|
|||
/// </summary>
|
|||
/// <param name="frameInfo">The frame map to initialize.</param>
|
|||
/// <param name="sequenceHeader">The sequence geometry defining the superblock grid.</param>
|
|||
private static void FillFrameWithIntraBlocks(Av1FrameInfo frameInfo, ObuSequenceHeader sequenceHeader) |
|||
{ |
|||
for (int row = 0; row < 32; row += sequenceHeader.SuperblockModeInfoSize) |
|||
{ |
|||
for (int column = 0; column < 32; column += sequenceHeader.SuperblockModeInfoSize) |
|||
{ |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(column, row), |
|||
Av1BlockSize.Block64x64, |
|||
Av1ReferenceFrameType.Intra, |
|||
default, |
|||
Av1PredictionMode.DC); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps one inter block over each 64-by-64 superblock and publishes its vector to the retained motion field.
|
|||
/// </summary>
|
|||
/// <param name="frameInfo">The frame map and retained field to initialize.</param>
|
|||
/// <param name="sequenceHeader">The sequence geometry defining the superblock grid.</param>
|
|||
/// <param name="referenceFrame">The canonical reference selected by each block.</param>
|
|||
/// <param name="motionVector">The retained motion vector.</param>
|
|||
private static void FillFrameWithInterBlocks( |
|||
Av1FrameInfo frameInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
Av1MotionVector motionVector) |
|||
{ |
|||
for (int row = 0; row < 32; row += sequenceHeader.SuperblockModeInfoSize) |
|||
{ |
|||
for (int column = 0; column < 32; column += sequenceHeader.SuperblockModeInfoSize) |
|||
{ |
|||
AddModeInfo( |
|||
frameInfo, |
|||
sequenceHeader, |
|||
new Point(column, row), |
|||
Av1BlockSize.Block64x64, |
|||
referenceFrame, |
|||
motionVector, |
|||
Av1PredictionMode.NearestMotionVector); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates and maps one mode-information block at a frame-relative position.
|
|||
/// </summary>
|
|||
/// <param name="frameInfo">The frame map that owns the block.</param>
|
|||
/// <param name="sequenceHeader">The sequence geometry defining superblock-relative addressing.</param>
|
|||
/// <param name="position">The block origin in frame-relative 4x4 units.</param>
|
|||
/// <param name="blockSize">The block geometry.</param>
|
|||
/// <param name="referenceFrame">The primary prediction reference.</param>
|
|||
/// <param name="motionVector">The primary motion vector.</param>
|
|||
/// <param name="predictionMode">The decoded luma or inter prediction mode.</param>
|
|||
/// <returns>The mapped mode-information block.</returns>
|
|||
private static Av1BlockModeInfo AddModeInfo( |
|||
Av1FrameInfo frameInfo, |
|||
ObuSequenceHeader sequenceHeader, |
|||
Point position, |
|||
Av1BlockSize blockSize, |
|||
Av1ReferenceFrameType referenceFrame, |
|||
Av1MotionVector motionVector, |
|||
Av1PredictionMode predictionMode) |
|||
{ |
|||
int superblockSize = sequenceHeader.SuperblockModeInfoSize; |
|||
Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); |
|||
Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); |
|||
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(superblockPosition); |
|||
Av1BlockModeInfo modeInfo = new(blockSize, relativePosition) |
|||
{ |
|||
YMode = predictionMode, |
|||
}; |
|||
|
|||
modeInfo.ReferenceFrames[0] = referenceFrame; |
|||
modeInfo.MotionVectors[0] = motionVector; |
|||
frameInfo.UpdateModeInfo(modeInfo, superblockInfo); |
|||
superblockInfo.BlockCount++; |
|||
return modeInfo; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a retained monochrome frame at one display-order hint.
|
|||
/// </summary>
|
|||
/// <param name="sequenceHeader">The sequence geometry used by the retained frame.</param>
|
|||
/// <param name="orderHint">The retained frame's modulo display-order hint.</param>
|
|||
/// <returns>A frame owner whose sample buffer and mode state are ready for reference-map ownership.</returns>
|
|||
private static Av1ReferenceFrame CreateReferenceFrame(ObuSequenceHeader sequenceHeader, uint orderHint) |
|||
{ |
|||
ObuFrameHeader frameHeader = CreateFrameHeader(orderHint, useReferenceFrameMotionVectors: false); |
|||
Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); |
|||
using Av1FrameInfo frameInfo = new(sequenceHeader); |
|||
return new Av1ReferenceFrame(frameBuffer, frameHeader, frameInfo); |
|||
} |
|||
} |
|||
@ -0,0 +1,330 @@ |
|||
// 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.Tiling; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the adaptive distributions and spatial contexts used to select an AV1 inter block's reference mode and frame.
|
|||
/// </summary>
|
|||
[Trait("Format", "Avif")] |
|||
public class Av1SingleReferenceEntropyTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies all eighteen normative single-reference distributions against libaom's forward Q15 defaults.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void SingleReferenceDefaultsMatchLibaom() |
|||
{ |
|||
uint[][] forwardThresholds = |
|||
[ |
|||
[4897, 1555, 4236, 8650, 904, 1444], |
|||
[16973, 16751, 19647, 24773, 11014, 15087], |
|||
[29744, 30279, 31194, 31895, 26875, 30304], |
|||
]; |
|||
|
|||
Av1Distribution[][] distributions = Av1DefaultDistributions.SingleReference; |
|||
|
|||
Assert.Equal(forwardThresholds.Length, distributions.Length); |
|||
for (int context = 0; context < distributions.Length; context++) |
|||
{ |
|||
Assert.Equal(forwardThresholds[context].Length, distributions[context].Length); |
|||
for (int decision = 0; decision < distributions[context].Length; decision++) |
|||
{ |
|||
// Av1Distribution stores inverse cumulative thresholds. Convert each published forward default by the
|
|||
// same Q15 complement used by production construction before comparing the exact value.
|
|||
uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[context][decision]; |
|||
|
|||
Assert.Equal(expected, distributions[context][decision][0]); |
|||
Assert.Equal(2, distributions[context][decision].NumberOfSymbols); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies the five normative block reference-mode distributions against libaom's forward Q15 defaults.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void CompInterDefaultsMatchLibaom() |
|||
{ |
|||
uint[] forwardThresholds = [26828, 24035, 12031, 10640, 2901]; |
|||
Av1Distribution[] distributions = Av1DefaultDistributions.CompInter; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that every semantic reader selects its exact context row and single-reference tree column.
|
|||
/// </summary>
|
|||
/// <param name="decision">The zero-based single-reference tree decision.</param>
|
|||
/// <param name="context">The neighboring reference-vote context.</param>
|
|||
[Theory] |
|||
[MemberData(nameof(GetReaderCases))] |
|||
public void SingleReferenceReadersUseRequestedDistribution(int decision, int context) |
|||
{ |
|||
bool[] expected = [false, true, true, false, true, false, false, true]; |
|||
Av1Distribution writerDistribution = Av1DefaultDistributions.SingleReference[context][decision]; |
|||
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, ReadDecision(ref decoder, decision, context)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that the reference-mode reader selects each of the five spatial-context distributions.
|
|||
/// </summary>
|
|||
/// <param name="context">The block reference-mode context.</param>
|
|||
[Theory] |
|||
[InlineData(0)] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
[InlineData(3)] |
|||
[InlineData(4)] |
|||
public void ReferenceModeReaderUsesRequestedContext(int context) |
|||
{ |
|||
bool[] expected = [false, true, true, false, true, false, false, true]; |
|||
Av1Distribution writerDistribution = Av1DefaultDistributions.CompInter[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.ReadIsCompoundReference(context)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies one-pass neighbor collection, compound-neighbor votes, clearing, and intra-neighbor exclusion.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void CollectNeighborReferenceCountsMatchesLibaom() |
|||
{ |
|||
Av1BlockModeInfo above = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None); |
|||
Av1BlockModeInfo left = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.Alternate); |
|||
InlineArray8<byte> referenceCountStorage = default; |
|||
Span<byte> referenceCounts = referenceCountStorage; |
|||
referenceCounts.Fill(7); |
|||
|
|||
Av1SymbolContextHelper.CollectNeighborReferenceCounts(above, left, referenceCounts); |
|||
|
|||
ReadOnlySpan<byte> expected = [0, 1, 0, 0, 0, 1, 0, 1]; |
|||
|
|||
for (int reference = 0; reference < referenceCounts.Length; reference++) |
|||
{ |
|||
Assert.Equal(expected[reference], referenceCounts[reference]); |
|||
} |
|||
|
|||
Av1BlockModeInfo intra = CreateModeInfo(Av1ReferenceFrameType.Intra, Av1ReferenceFrameType.None); |
|||
Av1SymbolContextHelper.CollectNeighborReferenceCounts(intra, null, referenceCounts); |
|||
|
|||
for (int reference = 0; reference < referenceCounts.Length; reference++) |
|||
{ |
|||
Assert.Equal((byte)0, referenceCounts[reference]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that the six context functions aggregate the exact reference groups used by libaom.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void SingleReferenceContextsAggregateNormativeReferenceGroups() |
|||
{ |
|||
InlineArray8<byte> referenceCountStorage = default; |
|||
Span<byte> referenceCounts = referenceCountStorage; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Last] = 5; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Last2] = 1; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Last3] = 2; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Golden] = 2; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Backward] = 3; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Alternate2] = 3; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Alternate] = 6; |
|||
|
|||
Assert.Equal(0, Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceAlternateContext(referenceCounts)); |
|||
Assert.Equal(2, Av1SymbolContextHelper.GetSingleReferenceLast3OrGoldenContext(referenceCounts)); |
|||
Assert.Equal(2, Av1SymbolContextHelper.GetSingleReferenceLast2Context(referenceCounts)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceGoldenContext(referenceCounts)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceAlternate2Context(referenceCounts)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies every branch of libaom's five-state single-versus-compound reference-mode context.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ReferenceModeContextMatchesLibaom() |
|||
{ |
|||
Av1BlockModeInfo singleForward = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None); |
|||
Av1BlockModeInfo singleBackward = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.None); |
|||
Av1BlockModeInfo intra = CreateModeInfo(Av1ReferenceFrameType.Intra, Av1ReferenceFrameType.None); |
|||
Av1BlockModeInfo compound = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.Backward); |
|||
Av1BlockModeInfo secondCompound = CreateModeInfo(Av1ReferenceFrameType.Last2, Av1ReferenceFrameType.Alternate); |
|||
|
|||
Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(null, null)); |
|||
Assert.Equal(0, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, null)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(singleBackward, null)); |
|||
Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(compound, null)); |
|||
Assert.Equal(0, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, singleForward)); |
|||
Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, singleBackward)); |
|||
Assert.Equal(2, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, compound)); |
|||
Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(intra, compound)); |
|||
Assert.Equal(2, Av1SymbolContextHelper.GetReferenceModeContext(compound, singleForward)); |
|||
Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(compound, singleBackward)); |
|||
Assert.Equal(4, Av1SymbolContextHelper.GetReferenceModeContext(compound, secondCompound)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies the tied, symbol-one-majority, and symbol-zero-majority context states.
|
|||
/// </summary>
|
|||
/// <param name="forwardCount">The votes for the forward branch represented by symbol zero.</param>
|
|||
/// <param name="backwardCount">The votes for the backward branch represented by symbol one.</param>
|
|||
/// <param name="expected">The expected context.</param>
|
|||
[Theory] |
|||
[InlineData(1, 1, 1)] |
|||
[InlineData(1, 2, 0)] |
|||
[InlineData(2, 1, 2)] |
|||
public void SingleReferenceContextReflectsNeighborVoteBalance(byte forwardCount, byte backwardCount, int expected) |
|||
{ |
|||
InlineArray8<byte> referenceCountStorage = default; |
|||
Span<byte> referenceCounts = referenceCountStorage; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Last] = forwardCount; |
|||
referenceCounts[(int)Av1ReferenceFrameType.Backward] = backwardCount; |
|||
|
|||
int actual = Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts); |
|||
|
|||
Assert.Equal(expected, actual); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that frame-context copies retain reference-selection adaptation without sharing mutable distributions.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropyCopyRetainsIndependentReferenceSelectionState() |
|||
{ |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext destination = new(0); |
|||
source.SingleReference[2][5].Update(1); |
|||
source.CompInter[4].Update(1); |
|||
|
|||
destination.CopyFrom(source); |
|||
|
|||
Assert.Equal(source.SingleReference[2][5][0], destination.SingleReference[2][5][0]); |
|||
Assert.Equal(source.CompInter[4][0], destination.CompInter[4][0]); |
|||
|
|||
source.SingleReference[2][5].Update(0); |
|||
source.CompInter[4].Update(0); |
|||
|
|||
Assert.NotEqual(source.SingleReference[2][5][0], destination.SingleReference[2][5][0]); |
|||
Assert.NotEqual(source.CompInter[4][0], destination.CompInter[4][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that publishing frame state resets the reference-selection distributions' update-rate history.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void FrameEntropySnapshotResetsReferenceSelectionUpdateCounts() |
|||
{ |
|||
const int updateCount = 20; |
|||
Av1FrameEntropyContext source = new(0); |
|||
Av1FrameEntropyContext snapshot = new(0); |
|||
|
|||
for (int i = 0; i < updateCount; i++) |
|||
{ |
|||
source.SingleReference[1][3].Update(1); |
|||
source.CompInter[2].Update(1); |
|||
} |
|||
|
|||
source.SnapshotTo(snapshot); |
|||
|
|||
Assert.Equal(source.SingleReference[1][3][0], snapshot.SingleReference[1][3][0]); |
|||
Assert.Equal(source.CompInter[2][0], snapshot.CompInter[2][0]); |
|||
|
|||
// The source retains twenty observations while the snapshot restarts at zero. The same next symbol therefore
|
|||
// moves identical thresholds by different amounts only when the new distribution participates in reset.
|
|||
source.SingleReference[1][3].Update(0); |
|||
snapshot.SingleReference[1][3].Update(0); |
|||
source.CompInter[2].Update(0); |
|||
snapshot.CompInter[2].Update(0); |
|||
|
|||
Assert.NotEqual(source.SingleReference[1][3][0], snapshot.SingleReference[1][3][0]); |
|||
Assert.NotEqual(source.CompInter[2][0], snapshot.CompInter[2][0]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Provides every context and decision pairing in the single-reference distribution matrix.
|
|||
/// </summary>
|
|||
/// <returns>The eighteen context and decision combinations.</returns>
|
|||
public static TheoryData<int, int> GetReaderCases() |
|||
{ |
|||
TheoryData<int, int> result = []; |
|||
|
|||
for (int decision = 0; decision < 6; decision++) |
|||
{ |
|||
for (int context = 0; context < 3; context++) |
|||
{ |
|||
result.Add(decision, context); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads one semantic single-reference decision through its production entry point.
|
|||
/// </summary>
|
|||
/// <param name="decoder">The tile symbol decoder.</param>
|
|||
/// <param name="decision">The zero-based single-reference tree decision.</param>
|
|||
/// <param name="context">The neighboring reference-vote context.</param>
|
|||
/// <returns>The decoded binary decision.</returns>
|
|||
private static bool ReadDecision(ref Av1SymbolDecoder decoder, int decision, int context) |
|||
=> decision switch |
|||
{ |
|||
0 => decoder.ReadSingleReferenceIsBackward(context), |
|||
1 => decoder.ReadSingleReferenceIsAlternate(context), |
|||
2 => decoder.ReadSingleReferenceIsLast3OrGolden(context), |
|||
3 => decoder.ReadSingleReferenceIsLast2(context), |
|||
4 => decoder.ReadSingleReferenceIsGolden(context), |
|||
_ => decoder.ReadSingleReferenceIsAlternate2(context), |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates decoded block-mode state with the requested primary and secondary reference labels.
|
|||
/// </summary>
|
|||
/// <param name="primary">The primary reference label.</param>
|
|||
/// <param name="secondary">The optional secondary reference label.</param>
|
|||
/// <returns>The initialized block mode state.</returns>
|
|||
private static Av1BlockModeInfo CreateModeInfo(Av1ReferenceFrameType primary, Av1ReferenceFrameType secondary) |
|||
{ |
|||
Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block4x4, Point.Empty); |
|||
modeInfo.ReferenceFrames[0] = primary; |
|||
modeInfo.ReferenceFrames[1] = secondary; |
|||
return modeInfo; |
|||
} |
|||
} |
|||
@ -1,135 +1,56 @@ |
|||
# AV1 reconstruction conformance fixtures |
|||
|
|||
The original AVIF and Y4M source files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. Derived fixtures retain the licenses recorded in libavif's `tests/data/README.md`: the Kodak image is released for unrestricted use, the Cosmos Laundromat frame uses CC BY 3.0, and the libavif color animation is distributed with the libavif test corpus under its BSD-2-Clause license. |
|||
These fixtures provide independent reference output for AV1 reconstruction and AVIF presentation tests. ImageSharp output is compared exactly with the retained native YUV planes and presented PNG files; the tests do not use a tolerance. |
|||
|
|||
The 8- and 10-bit `.bit` files contain the exact AV1 item payloads from the corresponding AVIF files. Each still file has one item occupying the complete `mdat` payload. The genuine 12-bit libavif sequence is retained for container, presentation, alpha, and metadata coverage, but its first color frame disables deblocking and therefore cannot prove the 12-bit filter path. |
|||
## Provenance |
|||
|
|||
`libaom-cosmos1650-12b.bit` was encoded from libavif's real 10-bit 4:4:4 Cosmos Laundromat Y4M source with the pinned libaom encoder. libaom promotes the input samples to a 12-bit AV1 profile-2 still-picture stream. The constant-quality level is deliberately lossy so the frame signals nonzero loop-filter levels. The material command options were `--usage=2 --passes=1 --limit=1 --obu --bit-depth=12 --input-bit-depth=10 --profile=2 --end-usage=q --cq-level=30 --cpu-used=6 --threads=1 --lag-in-frames=0 --full-still-picture-hdr`. |
|||
The source images and original AVIF files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. Their licenses are recorded in libavif's `tests/data/README.md` and continue to apply to the derived fixtures. This includes the unrestricted Kodak image, the CC BY 3.0 Cosmos Laundromat frame, and files distributed under libavif's BSD-2-Clause license. |
|||
|
|||
The `_libaom.yuv` files were decoded from those exact payloads with `aomdec` built from libaom commit `03087864cf4bea6abb0d28f95cf7843511413d8f`. The reference build used `AOM_TARGET_CPU=generic`, so these files come from libaom's scalar decoder rather than ImageSharp or an architecture-specific implementation. |
|||
Reference files were generated with scalar builds of: |
|||
|
|||
The `libaom-cdef-*` elementary streams were encoded separately with the same pinned generic libaom build so CDEF could be verified independently of the original corpus. The 8-bit stream uses `kodim23_yuv420_8bpc.y4m`; the 10- and 12-bit streams use `cosmos1650_yuv444_10bpc_p3pq.y4m`. Both source files are retained in libavif's test data at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. |
|||
- libaom commit `03087864cf4bea6abb0d28f95cf7843511413d8f`; |
|||
- libavif 1.4.2 from commit `062e582e8afda88e6baf988fdcf046a801efa0f5`, linked to that libaom build. |
|||
|
|||
The material encoder options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=1 --enable-restoration=0`. Each command also supplied the matching `--bit-depth`, `--input-bit-depth`, and `--profile` values. The 12-bit stream promotes the 10-bit 4:4:4 input through libaom's native 12-bit pipeline. Loop restoration is explicitly disabled so exact output equality exercises deblocking followed by active CDEF without a later restoration stage changing those samples. |
|||
The reference builds use `AOM_TARGET_CPU=generic` and disable libyuv. Native reconstruction therefore comes from libaom, and AVIF presentation comes from libavif's own conversion path, without architecture-specific SIMD or ImageSharp code. |
|||
|
|||
The `libavif-cdef-*` AVIF files were independently encoded with `avifenc` 1.4.2 from libavif commit `062e582e8afda88e6baf988fdcf046a801efa0f5` and its pinned libaom 3.14.1 dependency. The material options were `-j 1 -s 4 -q 60`, `enable-cdef=1`, and `enable-restoration=0`. The 8-bit 4:2:0 file uses CICP 1/13/6 and the Kodak Y4M source. The 10-bit 4:4:4 file uses CICP 12/16/12 and the Cosmos Laundromat Y4M source. The 12-bit 4:4:4 input wraps the pinned 12-bit scalar-libaom reference planes as `C444p12` Y4M and also uses CICP 12/16/12. |
|||
## File conventions |
|||
|
|||
The matching `.png` files were produced by `avifdec` from the same scalar build with `-j 1 -d 8`; the 8-bit 4:2:0 reference additionally selected bilinear chroma upsampling. The build uses `AOM_TARGET_CPU=generic` and `AVIF_LIBYUV=OFF`, so both AV1 reconstruction and YUV-to-RGB presentation come from the pinned scalar libaom/libavif paths. ImageSharp compares every presented RGBA byte exactly, without a tolerance. |
|||
- `.avif` files exercise the complete container and presentation path. |
|||
- `.bit` files contain the exact AV1 elementary-stream payload used by reconstruction tests. |
|||
- `-libaom.yuv` files contain headerless planar Y, U, and V reference samples. Samples above eight bits are stored as little-endian 16-bit values. |
|||
- `-libaom-y4m.yuv` files retain the Y4M header together with the native planar frame. |
|||
- `.png` files contain the eight-bit RGBA presentation reference produced by the pinned scalar libavif build. |
|||
|
|||
The native reference layouts are: |
|||
## Coverage |
|||
|
|||
- `libavif-kodim23-8b-libaom.yuv`: 768x512, 8-bit YUV 4:2:0, planar Y/U/V. |
|||
- `libavif-cosmos1650-10b-libaom.yuv`: 1024x428, 10-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
- `libaom-cosmos1650-12b-libaom.yuv`: 1024x428, 12-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
- `libaom-cdef-kodim23-8b-libaom.yuv`: 768x512, 8-bit YUV 4:2:0, planar Y/U/V. |
|||
- `libaom-cdef-cosmos-10b-libaom.yuv`: 1024x428, 10-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
- `libaom-cdef-cosmos-12b-libaom.yuv`: 1024x428, 12-bit YUV 4:4:4, planar Y/U/V with little-endian 16-bit samples. |
|||
| Fixture family | Coverage | |
|||
| --- | --- | |
|||
| `libavif-kodim23`, `libavif-cosmos1650`, `libaom-cosmos1650` | Baseline 8-, 10-, and 12-bit reconstruction, chroma subsampling, and active deblocking | |
|||
| `*-cdef-*` | Active CDEF with loop restoration disabled | |
|||
| `*-superres-*` | Active horizontal super-resolution with CDEF and restoration disabled | |
|||
| `*-restoration-*` | Wiener and self-guided loop restoration | |
|||
| `*-restoration-superres-*` | Restoration after super-resolution, including 10-bit 4:2:2 clipped-edge transform coverage | |
|||
| `libavif-profile-*` | The 8-, 10-, and 12-bit matrix across monochrome, 4:2:0, 4:2:2, and 4:4:4 | |
|||
| `*-palette-*` | Luma and chroma palette prediction | |
|||
| `*-intrabc-*` | Intra-block copy at every supported bit depth | |
|||
| `*-lossless-*` | Lossless quantization, reversible transforms, and exact presentation | |
|||
| `*-film-grain-*` | Full and restricted range, monochrome, identity matrix, 8/10/12-bit synthesis, overlap, and odd frame dimensions | |
|||
| `libavif-progressive-draw-points-8b` | A real two-layer color item whose final frame uses single-reference inter reconstruction, plus its progressive auxiliary alpha item | |
|||
|
|||
The conformance tests compare every visible reconstructed sample with these files. The deblocking corpus verifies nonzero loop-filter levels. The CDEF corpus additionally verifies sequence-level CDEF enablement, a selected nonzero frame strength, and disabled loop restoration, so a disabled or bypassed CDEF stage cannot satisfy the exact native-plane comparison accidentally. |
|||
The corresponding tests also assert the syntax required by each family before comparing output. This prevents an inactive tool or an incorrectly substituted stream from passing solely because its final pixels happen to match. |
|||
|
|||
Across the three active-CDEF elementary streams, the decoded mode records select every terminal AV1 partition shape. Their nested blocks also require recursive square splits, which produce no terminal mode record of their own. The tests verify that complete ten-type coverage before relying on the exact native-plane comparisons. |
|||
## Progressive dependent-frame fixture |
|||
|
|||
The `libaom-superres-*` streams were encoded from the same Kodak and Cosmos sources with the pinned generic libaom build. Their material options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=0 --enable-restoration=0 --superres-mode=1 --superres-denominator=12 --superres-kf-denominator=12`, together with the matching input depth, output depth, and profile. Disabling CDEF and restoration isolates the normative horizontal upscaling result, while the tests separately require a coded width smaller than the displayed width so an unscaled stream cannot satisfy the reference comparison. |
|||
|
|||
The matching `libaom-superres-*-libaom.yuv` files were decoded by `aomdec --rawvideo` from that exact generic build. They retain the displayed 768x512 8-bit YUV 4:2:0 and 1024x428 10/12-bit YUV 4:4:4 layouts described above. |
|||
|
|||
The `libavif-superres-*` containers retain the matching libavif-generated 8-, 10-, and 12-bit restoration container layouts described below. Each container's sole AV1 item was replaced mechanically with the corresponding active-super-resolution payload. Only the single `iloc` extent length and terminal `mdat` box size changed; the libavif-generated codec configuration, dimensions, CICP properties, item relationships, and remaining container layout were retained. |
|||
|
|||
The matching `libavif-superres-*.png` files were decoded from those exact containers with the pinned generic `avifdec -j 1 -d 8`; the 8-bit 4:2:0 reference additionally selected bilinear chroma upsampling. Tests decode the complete `mdat` payload to require a coded width smaller than the displayed width, then compare every presented RGBA byte with the scalar-libavif PNG exactly and without a tolerance. |
|||
|
|||
The `libaom-restoration-*` streams were encoded from the same Kodak and Cosmos sources with the pinned generic libaom build. Their material options were `--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 --threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=0 --enable-restoration=1 --superres-mode=0`, together with the matching input depth, output depth, and profile. The matching `*-libaom.yuv` files were decoded by that build's `aomdec --rawvideo` and retain the 768x512 8-bit YUV 4:2:0 and 1024x428 10/12-bit YUV 4:4:4 layouts. The tests require at least one signaled restoration unit and compare every resulting native sample exactly. |
|||
|
|||
The `libavif-restoration-*` container templates were encoded from the same sources with the pinned generic libavif build. Pinned libavif forcibly disables restoration for 12-bit libaom encoding, and its default all-intra settings did not select active restoration for the other templates. Each template's sole AV1 item was therefore replaced mechanically with the matching active-restoration payload above. Only the single `iloc` extent length and terminal `mdat` box size changed; the libavif-generated codec configuration, dimensions, CICP properties, item relationships, and remaining container layout were retained. |
|||
|
|||
The matching `libavif-restoration-*.png` files were decoded from those exact AVIF containers with the pinned generic `avifdec -j 1 -d 8`; the 8-bit 4:2:0 reference additionally selected bilinear chroma upsampling. The tests first decode each container's actual `mdat` payload to require both Wiener and self-guided unit selection, then compare every presented RGBA byte with the scalar-libavif PNG exactly and without a tolerance. |
|||
|
|||
The `libaom-restoration-superres-*` streams combine active restoration with a coded width reduced by super-resolution denominator 12. They use the same pinned generic libaom build and material encoder options as the restoration streams, with `--superres-mode=1 --superres-denominator=12 --superres-kf-denominator=12`. The 8-bit fixture is 768x512 YUV 4:2:0, the 10-bit fixture is 512x256 YUV 4:2:2, and the 12-bit fixture is 1024x428 YUV 4:4:4. Their matching `*-libaom.yuv` files were decoded from the exact payloads by the pinned generic `aomdec --rawvideo` build. |
|||
|
|||
The 10-bit 4:2:2 source was produced from libavif's `abc.png` with pinned generic `avifenc` using `-j 1 -s 8 -q 100 -d 10 -y 422`, then decoded to Y4M before the combined libaom encode. Its clipped rightmost 128x128 coding block crosses a second 64x64 residual region. This independently exercises the required conversion of the luma-region cursor to the subsampled chroma transform grid instead of relying only on full-width 4:4:4 blocks. |
|||
|
|||
## AV1 profile matrix |
|||
|
|||
The `libavif-profile-*` fixtures were generated from `tests/data/abc.png` at the pinned libavif revision. The source SHA-256 is `5561862FBD409A3F86B02DB73EBB8572D0E2A307EB45ECF9017A1B2137B9F729`; libavif's test-data manifest licenses it under the libavif license. Alpha was deliberately ignored so the matrix isolates the color planes. |
|||
|
|||
The tools were the retained generic `avifenc` and `avifdec` 1.4.2 builds linked to libaom 3.14.1 at commit `03087864cf4bea6abb0d28f95cf7843511413d8f`. The libaom build used `AOM_TARGET_CPU=generic` with its encoder and decoder enabled; its generated configuration disables AVX, AVX2, AVX-512, MMX, Neon, SSE, SSE2, SSE3, SSSE3, SSE4.1, and SSE4.2. The static libavif build used that `aom.lib`, disabled libyuv, and received `WITH_SIMD=OFF`, so the native and presentation references do not depend on ImageSharp or an architecture-specific decode path. |
|||
|
|||
The complete generation loop was: |
|||
|
|||
```powershell |
|||
foreach ($depth in 8, 10, 12) { |
|||
foreach ($format in 400, 420, 422, 444) { |
|||
$stem = "libavif-profile-${depth}b-${format}" |
|||
& $encoder -j 1 -s 6 -q 60 --ignore-alpha -d $depth -y $format --cicp 1/13/6 -a enable-palette=0 -a enable-intrabc=0 $source "$matrixDirectory\$stem.avif" |
|||
& $decoder -j 1 "$matrixDirectory\$stem.avif" "$matrixDirectory\$stem-libaom.y4m" |
|||
& $decoder -j 1 -d 8 "$matrixDirectory\$stem.avif" "$matrixDirectory\$stem.png" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The six subsampled PNG references were then regenerated with explicit bilinear chroma reconstruction: |
|||
|
|||
```powershell |
|||
foreach ($depth in 8, 10, 12) { |
|||
foreach ($format in 420, 422) { |
|||
$stem = "libavif-profile-${depth}b-${format}" |
|||
& $decoder -j 1 -d 8 -u bilinear "$matrixDirectory\$stem.avif" "$matrixDirectory\$stem.png" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Each AVIF is a lossy 512x256 opaque still image with full-range CICP 1/13/6 signaling and no ICC, XMP, or Exif payload. The retained Y4M output is a complete container decode with its native monochrome, 4:2:0, 4:2:2, or 4:4:4 header and 8-, 10-, or 12-bit planes. It is stored in the test corpus with `-libaom-y4m.yuv` replacing the generated `-libaom.y4m` suffix. SHA-256 comparison confirms that every committed AVIF, Y4M, and PNG is byte-identical to its retained generation artifact. |
|||
|
|||
## Palette coverage |
|||
|
|||
The palette fixture was encoded independently from ImageSharp using `tests/data/draw_points.png` from the pinned libavif revision. The source is a 33x11 flat-color image whose AV1 item selects both luma and chroma palette prediction. The pinned generic `avifenc` command used `-j 1 -s 0 -q 100 --ignore-alpha -y 444 --cicp 12/16/12 -a enable-palette=1 -a enable-intrabc=0 -a tune-content=screen`. |
|||
|
|||
`libaom-palette-draw-points-8b-444.bit` is the exact sole AV1 item extracted from `libavif-palette-draw-points-8b.avif`. The matching native YUV reference was decoded from that payload by the pinned scalar `aomdec --rawvideo` build. The presented PNG was decoded from the complete AVIF container by the pinned scalar `avifdec -j 1 -d 8` build. The tests require both palette planes to be selected, compare every native YUV sample exactly, and compare every presented RGBA byte exactly across the available vector widths and scalar fallback. No tolerance is used. |
|||
|
|||
## Intra-block-copy coverage |
|||
|
|||
The intra-block-copy fixtures were encoded independently from ImageSharp using `tests/data/abc.png` from the pinned libavif revision. This real 512x256 screen-content image provides repeated glyph and background regions beyond AV1's required 256-pixel reconstruction delay. Each AVIF is opaque YUV 4:4:4 with palette prediction disabled, so selected screen-content reuse must traverse intra-block-copy syntax and prediction rather than palette reconstruction. |
|||
|
|||
The common pinned generic `avifenc` options were `-j 1 -s 0 -l --ignore-alpha -y 444 -a enable-palette=0 -a enable-intrabc=1 -a tune-content=screen`. The 8-bit fixture uses `--cicp 1/13/0`; the 10- and 12-bit fixtures add the matching `-d` value and use `--cicp 12/16/0`. The high-depth encodes promote the 8-bit source, so `-l` configures lossless codec quantization but does not claim reversible conversion back to the original 8-bit PNG. |
|||
|
|||
The matching Y4M files were decoded from the complete AVIF containers with the pinned generic `avifdec -j 1` build. Their retained headers record the 512x256 full-range YUV 4:4:4 layouts at 8, 10, and 12 bits, followed by one planar frame. The matching PNG files were decoded with `avifdec -j 1 -d 8`. The build uses `AOM_TARGET_CPU=generic` and `AVIF_LIBYUV=OFF`, so the native planes and presented pixels come from the pinned scalar libaom/libavif paths. |
|||
|
|||
Tests require the frame header to allow intra-block copy and at least one final coding block to select it. They then compare every native Y, U, and V sample and every presented RGBA byte exactly under normal hardware dispatch, with AVX-512 disabled, with AVX disabled, and with all hardware intrinsics disabled. Displacement-vector entropy, spatial reference derivation, legal reconstruction order, inter transform selection, and prediction must therefore agree with the independent decoder for all three supported bit depths. No tolerance is used. |
|||
|
|||
## Lossless coverage |
|||
|
|||
The lossless fixtures were encoded independently from ImageSharp using `tests/data/circle-trns-after-plte.png` from the pinned libavif revision. Alpha was intentionally ignored so the native references isolate color-plane reconstruction. The 8-bit input uses CICP 1/13/0; the 10- and 12-bit YUV 4:4:4 inputs use CICP 12/16/0. The material `avifenc` options were `-j 1 -s 0 -l --ignore-alpha -y 444 -a enable-palette=0 -a enable-intrabc=0`, together with the matching depth and CICP values. Disabling palette and intra-block copy ensures the exact result traverses ordinary prediction, coefficient decoding, inverse quantization, and the reversible lossless transform. |
|||
|
|||
The `libavif-lossless-circle-*-444-libaom.yuv` files contain the headerless native planes decoded from the complete AVIF containers by the pinned generic `avifdec -j 1` build. Each file stores one 100x60 full-range YUV 4:4:4 frame at 8, 10, or 12 bits. The matching PNG files were decoded by the same build with `-d 8`. Tests require coded and complete losslessness, base quantizer zero, identity matrix coefficients, disabled palette and intra-block copy, and at least one coded residual. Every native Y, U, and V sample and every presented RGBA byte is compared exactly across normal hardware dispatch and the scalar fallback. No tolerance is used. |
|||
|
|||
## Film-grain coverage |
|||
|
|||
The film-grain pairs were generated independently from ImageSharp. Each `.bit` file is an AV1 still-picture OBU stream, and the matching `-libaom.yuv` file is the exact visible planar output from the pinned scalar libaom decoder. |
|||
|
|||
The source images are `tests/data/circle-trns-after-plte.png` and `tests/data/draw_points.png` from the pinned libavif revision above. The streams and native references use the same pinned libaom revision. Intermediate Y4M inputs were produced with libavif 1.4.2 linked to that libaom revision. |
|||
|
|||
| Stream | libaom vector | Native layout | Range | Covered behavior | |
|||
| --- | ---: | --- | --- | --- | |
|||
| `libaom-film-grain-circle-8b-420.bit` | 2 | 8-bit 4:2:0 | Full | Lag-three templates, boundary overlap, and independent luma and chroma scaling | |
|||
| `libaom-film-grain-circle-10b-422.bit` | 15 | 10-bit 4:2:2 | Full | Lag-two templates, boundary overlap, and chroma scaling derived from luma | |
|||
| `libaom-film-grain-circle-12b-444.bit` | 16 | 12-bit 4:4:4 | Full | Lag-three templates, boundary overlap, high-depth interpolation, and grain scale shift two | |
|||
| `libaom-film-grain-circle-8b-420-limited.bit` | 1 | 8-bit 4:2:0 | Restricted | Independent restricted luma and chroma endpoints | |
|||
| `libaom-film-grain-circle-8b-400-limited.bit` | 3 | 8-bit monochrome | Restricted | Monochrome synthesis, overlap, and restricted luma clipping | |
|||
| `libaom-film-grain-circle-12b-444-identity-limited.bit` | 14 | 12-bit 4:4:4 identity | Restricted | High-depth identity-matrix clipping, including luma endpoints for all three planes | |
|||
| `libaom-film-grain-draw-points-8b-420-odd.bit` | 2 | 8-bit 4:2:0, 33×11 | Full | Odd-width and odd-height extension, a partial final block, and overlap at the visible frame edge | |
|||
|
|||
The common libaom encoder options were: |
|||
The `libavif-progressive-draw-points-8b.avif` fixture is the unmodified `tests/data/draw_points_idat_progressive.avif` file from the pinned libavif tree. Its SHA-256 is `077AB2AD1E46DD912A973E4F024CB1EB242A08298BE2DBF1A52A058E88C48A4A`. It was generated with: |
|||
|
|||
```text |
|||
--usage=2 --passes=1 --limit=1 --obu --end-usage=q --cq-level=30 --cpu-used=4 |
|||
--threads=1 --lag-in-frames=0 --full-still-picture-hdr --enable-cdef=0 --enable-restoration=0 |
|||
./avifenc -q 100 --progressive ../tests/data/draw_points.png ../tests/data/draw_points_idat_progressive.avif |
|||
``` |
|||
|
|||
Each stream adds the bit depth, input bit depth, profile, monochrome or identity-matrix flag where applicable, and the `--film-grain-test` value shown above. The twelve-bit streams use a ten-bit Y4M input and `--bit-depth=12 --input-bit-depth=10`; this is the supported high-depth promotion path in the pinned generic aomenc build. |
|||
The primary color item's `a1lx` property divides its logical 72-byte AV1 payload into a 55-byte base layer and a 17-byte dependent layer. The container stores those layers in separate `iloc` extents at AVIF offsets 511 and 583. The `.bit` fixture concatenates those two logical color extents; it does not copy the physically adjacent auxiliary-alpha extent between them. |
|||
|
|||
References were decoded with: |
|||
Exact pinned libaom decodes the corrected logical payload into two 33x11 YUV444 frames. Both frames' 1,089 color samples match the corresponding first three planes of the pinned libavif YUV444-alpha outputs exactly. The retained Y4M contains both progressive YUV444-alpha frames, and the PNG contains pinned libavif's final RGBA presentation. The production-path test selects the second native frame, requires inter-coded blocks in the final ImageSharp frame, and compares both native color and final presentation without a tolerance. |
|||
|
|||
```text |
|||
aomdec --rawvideo --output=<reference>.yuv <stream>.bit |
|||
``` |
|||
## Updating fixtures |
|||
|
|||
Tests compare every visible native Y, U, and V sample exactly. No tolerant image comparison is used. |
|||
Do not create conformance references with ImageSharp. Generate both the native-plane and presentation references with an independent decoder, record the exact upstream revisions and source license, and preserve exact comparisons. A new tool-specific fixture should demonstrate that the relevant syntax is active and should be no larger than required to cover that behavior. |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:cc70e15d31a25289492469bb637ca7e00f11de24889de5a9c6b9bd3f20be4b7a |
|||
size 2986 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:077ab2ad1e46dd912a973e4f024cb1eb242a08298be2dbf1a52a058e88c48a4a |
|||
size 600 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:93ff341e1a7a4c849c3f713c4d20589fce93ddd7154106bce574106395e1498e |
|||
size 72 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0758c17dc36e38aee9f4389a335c2bf332ab91e4c79d7b0b22994fddd0fd1605 |
|||
size 186 |
|||
Loading…
Reference in new issue