// 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; /// /// Verifies the adaptive AV1 normal and displacement motion-vector entropy contexts. /// [Trait("Format", "Avif")] public class Av1MotionVectorEntropyTests { /// /// Verifies both motion-vector contexts against every normative forward Q15 default from the reference decoder. /// [Fact] public void MotionVectorDefaultsMatchReference() { Av1FrameEntropyContext context = new(0); Assert.NotSame(context.MotionVector, context.DisplacementVector); AssertContextDefaults(context.MotionVector); AssertContextDefaults(context.DisplacementVector); } /// /// Verifies integer, quarter-sample, and eighth-sample syntax consumption and reconstruction. /// /// The numeric motion-vector precision. /// Indicates whether the coded delta occupies the horizontal component. /// The coded magnitude class. /// The coded integer magnitude offset. /// The coded fractional symbol, or negative when omitted. /// The coded eighth-sample symbol, or negative when omitted. /// The expected positive component in one-eighth-sample units. [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 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)); } /// /// Verifies that normal and displacement motion vectors never share adaptive distribution state. /// [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]); } /// /// Verifies that frame-context copies retain complete motion-vector state without sharing it. /// [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); } /// /// Verifies that publishing frame state resets every motion-vector update count. /// [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); } /// /// Verifies one complete motion-vector context against the normative defaults. /// /// The context under test. private static void AssertContextDefaults(Av1MotionVectorContext context) { Assert.NotSame(context.Vertical, context.Horizontal); AssertDistribution(context.Joint, [4096, 11264, 19328]); AssertComponentDefaults(context.Vertical); AssertComponentDefaults(context.Horizontal); } /// /// Verifies one component's complete set of normative defaults. /// /// The component under test. 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 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]); } } /// /// Verifies one distribution after conversion from forward to inverse cumulative thresholds. /// /// The distribution under test. /// The normative forward Q15 thresholds. private static void AssertDistribution(Av1Distribution distribution, ReadOnlySpan 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]); } } /// /// Applies the same observations to every distribution in a motion-vector context. /// /// The context to adapt. /// The coded symbol used for each observation. /// The number of observations. 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); } } /// /// Applies one observation to every distribution in one motion-vector component. /// /// The component to adapt. /// The coded symbol used for the observation. 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); } } /// /// Verifies equal adaptive thresholds across two motion-vector contexts. /// /// The expected context. /// The actual context. 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); } /// /// Verifies equal adaptive thresholds across two motion-vector components. /// /// The expected component. /// The actual component. 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]); } } /// /// Verifies independently adaptive thresholds across two motion-vector contexts. /// /// The independently adapted context. /// The copied or reset context. 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); } /// /// Verifies independently adaptive thresholds across two motion-vector components. /// /// The independently adapted component. /// The copied or reset component. 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]); } } }