// 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; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; /// /// Verifies the entropy state and spatial contexts used by intra-coded blocks inside AV1 inter frames. /// [Trait("Format", "Avif")] public class Av1InterFrameIntraEntropyTests { /// /// Gets libaom's four forward Q15 luma-mode CDF rows in block-size-group order. /// private static ReadOnlySpan FrameYModeForwardThresholds => [ 22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349, 18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852, 19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659, 20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049, ]; /// /// Verifies the four normative intra/inter distributions against libaom's forward Q15 defaults. /// [Fact] public void IntraInterDefaultsMatchLibaom() { uint[] forwardThresholds = [806, 16662, 20186, 26538]; Av1Distribution[] distributions = Av1DefaultDistributions.IntraInter; Assert.Equal(forwardThresholds.Length, distributions.Length); for (int context = 0; context < distributions.Length; context++) { // Av1Distribution stores inverse cumulative thresholds, so compare each libaom default after the same // forward-to-inverse conversion performed by its constructor. Assert.Equal((uint)Av1Distribution.ProbabilityTop - forwardThresholds[context], distributions[context][0]); Assert.Equal(2, distributions[context].NumberOfSymbols); } } /// /// Verifies every inter-frame intra luma-mode threshold against libaom's forward Q15 defaults. /// [Fact] public void FrameYModeDefaultsMatchLibaom() { const int thresholdsPerGroup = 12; ReadOnlySpan forwardThresholds = FrameYModeForwardThresholds; Av1Distribution[] distributions = Av1DefaultDistributions.FrameYMode; Assert.Equal(4, distributions.Length); for (int group = 0; group < distributions.Length; group++) { Assert.Equal(thresholdsPerGroup + 1, distributions[group].NumberOfSymbols); for (int threshold = 0; threshold < thresholdsPerGroup; threshold++) { uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[(group * thresholdsPerGroup) + threshold]; Assert.Equal(expected, distributions[group][threshold]); } } } /// /// Verifies that the intra/inter reader selects and adapts each of the four spatial-context distributions. /// /// The intra/inter spatial context. [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] [InlineData(3)] public void ReadIsInterUsesRequestedContext(int context) { bool[] expected = [false, true, true, false, true, false, false, true]; Av1Distribution writerDistribution = Av1DefaultDistributions.IntraInter[context]; using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); foreach (bool value in expected) { writer.WriteSymbol(value, writerDistribution); } using IMemoryOwner encoded = writer.Exit(); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); foreach (bool value in expected) { Assert.Equal(value, decoder.ReadIsInter(context)); } } /// /// Verifies that inter-frame intra luma modes use the normative size group for every AV1 block size. /// /// The AV1 block-size enumeration value. /// The normative size group from AV1 section 9.3. [Theory] [MemberData(nameof(GetBlockSizeGroups))] public void ReadInterFrameYModeUsesNormativeSizeGroup(int blockSizeValue, int sizeGroup) { Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; Av1PredictionMode[] expected = [ Av1PredictionMode.DC, Av1PredictionMode.Directional45Degrees, Av1PredictionMode.Smooth, Av1PredictionMode.Paeth, Av1PredictionMode.Horizontal, Av1PredictionMode.Directional157Degrees, ]; Av1Distribution writerDistribution = Av1DefaultDistributions.FrameYMode[sizeGroup]; using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); foreach (Av1PredictionMode mode in expected) { writer.WriteSymbol((int)mode, writerDistribution); } using IMemoryOwner encoded = writer.Exit(); Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); foreach (Av1PredictionMode mode in expected) { Assert.Equal(mode, decoder.ReadInterFrameYMode(blockSize)); } } /// /// Verifies that all four intra/inter contexts follow the normative above-and-left neighbor classification. /// /// Whether the above block is available. /// Whether the available above block uses inter prediction. /// Whether the left block is available. /// Whether the available left block uses inter prediction. /// The expected intra/inter context. [Theory] [InlineData(false, false, false, false, 0)] [InlineData(true, true, false, false, 0)] [InlineData(true, false, false, false, 2)] [InlineData(false, false, true, true, 0)] [InlineData(false, false, true, false, 2)] [InlineData(true, true, true, true, 0)] [InlineData(true, false, true, true, 1)] [InlineData(true, true, true, false, 1)] [InlineData(true, false, true, false, 3)] public void IntraInterContextMatchesNeighborPredictionTypes( bool hasAbove, bool aboveIsInter, bool hasLeft, bool leftIsInter, int expected) { Av1BlockModeInfo above = hasAbove ? CreateModeInfo(aboveIsInter) : null; Av1BlockModeInfo left = hasLeft ? CreateModeInfo(leftIsInter) : null; int actual = Av1SymbolContextHelper.GetIntraInterContext(above, left); Assert.Equal(expected, actual); } /// /// Verifies that frame-context copies retain adapted inter-frame intra state without sharing mutable distributions. /// [Fact] public void FrameEntropyCopyRetainsIndependentInterFrameIntraState() { Av1FrameEntropyContext source = new(0); Av1FrameEntropyContext destination = new(0); source.FrameYMode[2].Update((int)Av1PredictionMode.Smooth); source.IntraInter[3].Update(1); destination.CopyFrom(source); Assert.Equal(source.FrameYMode[2][0], destination.FrameYMode[2][0]); Assert.Equal(source.IntraInter[3][0], destination.IntraInter[3][0]); source.FrameYMode[2].Update((int)Av1PredictionMode.Paeth); source.IntraInter[3].Update(0); Assert.NotEqual(source.FrameYMode[2][0], destination.FrameYMode[2][0]); Assert.NotEqual(source.IntraInter[3][0], destination.IntraInter[3][0]); } /// /// Verifies that a published frame snapshot preserves adapted thresholds but resets their update-rate history. /// [Fact] public void FrameEntropySnapshotResetsInterFrameIntraUpdateCounts() { const int updateCount = 20; Av1FrameEntropyContext source = new(0); Av1FrameEntropyContext snapshot = new(0); for (int i = 0; i < updateCount; i++) { source.FrameYMode[1].Update((int)Av1PredictionMode.Vertical); source.IntraInter[1].Update(1); } source.SnapshotTo(snapshot); Assert.Equal(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); Assert.Equal(source.IntraInter[1][0], snapshot.IntraInter[1][0]); // The source retains twenty observations while the published snapshot restarts at zero. Applying the same // symbol therefore moves identical thresholds by different update rates only when reset wiring is complete. source.FrameYMode[1].Update((int)Av1PredictionMode.DC); snapshot.FrameYMode[1].Update((int)Av1PredictionMode.DC); source.IntraInter[1].Update(0); snapshot.IntraInter[1].Update(0); Assert.NotEqual(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); Assert.NotEqual(source.IntraInter[1][0], snapshot.IntraInter[1][0]); } /// /// Provides the normative AV1 size-group table in block-size enumeration order. /// /// Every decoded block size paired with its luma-mode size group. public static TheoryData GetBlockSizeGroups() { // This is size_group_lookup from AV1 section 9.3 and libaom common_data.h. Keeping expected values explicit // ensures that the test does not reproduce the production formula it is intended to verify. 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 result = []; for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) { result.Add(blockSize, sizeGroups[blockSize]); } return result; } /// /// Creates decoded neighbor state with either an intra or inter primary reference. /// /// Whether the neighbor uses inter prediction. /// The initialized block mode state. private static Av1BlockModeInfo CreateModeInfo(bool isInter) { Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block4x4, Point.Empty); modeInfo.ReferenceFrames[0] = isInter ? Av1ReferenceFrameType.Last : Av1ReferenceFrameType.Intra; modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; return modeInfo; } }