// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Heif.Hevc; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; /// /// Verifies HEVC inverse-DCT, inverse-DST, rectangular-block, dynamic-range, and reconstruction behavior. /// [Trait("Format", "Heic")] public class HevcInverseTransformerTests { /// /// Verifies the two normative normalization stages with a pure DC coefficient and a strided prediction block. /// [Fact] public void DctDcCoefficientProducesUniformReconstruction() { const int size = 4; const int stride = 7; int[] coefficients = new int[size * size]; coefficients[0] = 1024; ushort[] destination = new ushort[stride * size]; destination.AsSpan().Fill(100); int[] scratch = new int[HevcInverseTransformer.GetScratchLength(2, 2)]; HevcInverseTransformer.TransformAdd(coefficients, destination, stride, 2, 2, 8, 15, false, scratch); for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { Assert.Equal((ushort)108, destination[(y * stride) + x]); } } } /// /// Verifies a hand-derived four-by-four inverse-DST result from a pure lowest-frequency coefficient. /// [Fact] public void DstLowestFrequencyMatchesFixedResult() { int[] coefficients = new int[16]; coefficients[0] = 1024; int[] actual = new int[16]; int[] scratch = new int[HevcInverseTransformer.GetScratchLength(2, 2)]; int[] expected = [ 2, 3, 4, 5, 3, 6, 8, 9, 4, 8, 11, 12, 5, 9, 12, 14 ]; HevcInverseTransformer.Transform(coefficients, actual, 2, 2, 8, 15, true, scratch); Assert.True(expected.AsSpan().SequenceEqual(actual)); } /// /// Compares factorized SIMD transforms with a dense specification-shaped oracle across supported dimensions and precisions. /// /// The base-two logarithm of the tested block width. /// The base-two logarithm of the tested block height. /// The reconstructed component precision. /// The transform dynamic range excluding its sign bit. /// Whether the tested four-by-four block uses the inverse DST. [Theory] [InlineData(2, 2, 8, 15, false)] [InlineData(2, 2, 8, 15, true)] [InlineData(2, 3, 10, 15, false)] [InlineData(3, 2, 10, 15, false)] [InlineData(3, 3, 10, 15, false)] [InlineData(4, 3, 12, 18, false)] [InlineData(3, 4, 12, 18, false)] [InlineData(5, 4, 12, 18, false)] [InlineData(4, 5, 12, 18, false)] [InlineData(5, 5, 12, 15, false)] public void TransformMatchesDenseOracle( int log2Width, int log2Height, int bitDepth, int maxTransformDynamicRange, bool useDiscreteSineTransform) { int width = 1 << log2Width; int height = 1 << log2Height; int sampleCount = width * height; int[] coefficients = new int[sampleCount]; int coefficientMaximum = (1 << maxTransformDynamicRange) - 1; for (int i = 0; i < coefficients.Length; i++) { // Alternating values across the complete dequantized range exercise both intermediate clipping bounds // as well as every frequency group used by the factorized transform. int unit = (((i * 37) + (width * 11) + height) % 127) - 63; coefficients[i] = (unit * coefficientMaximum) / 63; } int[] expected = new int[sampleCount]; int[] actual = new int[sampleCount]; int[] oracleScratch = new int[sampleCount]; int[] transformScratch = new int[HevcInverseTransformer.GetScratchLength(log2Width, log2Height)]; TransformDenseOracle( coefficients, expected, oracleScratch, width, height, bitDepth, maxTransformDynamicRange, useDiscreteSineTransform); HevcInverseTransformer.Transform( coefficients, actual, log2Width, log2Height, bitDepth, maxTransformDynamicRange, useDiscreteSineTransform, transformScratch); Assert.True(expected.AsSpan().SequenceEqual(actual), $"The {width}x{height} transform at {bitDepth} bits did not match the dense HEVC oracle."); } /// /// Verifies that residual addition clips both negative and positive reconstruction overflow at the component range. /// [Fact] public void TransformAddClipsToComponentRange() { const int size = 4; int[] positiveCoefficients = new int[size * size]; int[] negativeCoefficients = new int[size * size]; positiveCoefficients[0] = 32767; negativeCoefficients[0] = -32768; ushort[] positive = new ushort[size * size]; ushort[] negative = new ushort[size * size]; positive.AsSpan().Fill(4090); negative.AsSpan().Fill(5); int[] scratch = new int[HevcInverseTransformer.GetScratchLength(2, 2)]; HevcInverseTransformer.TransformAdd(positiveCoefficients, positive, size, 2, 2, 12, 18, false, scratch); HevcInverseTransformer.TransformAdd(negativeCoefficients, negative, size, 2, 2, 12, 18, false, scratch); Assert.All(positive, value => Assert.Equal((ushort)4095, value)); Assert.All(negative, value => Assert.Equal((ushort)0, value)); } /// /// Verifies that SIMD residual reconstruction preserves each predicted lane for an extracted HEVC transform block. /// [Fact] public void AddResidualMatchesExtractedHevcBlock() { int[] residual = [-6, 0, 2, 4, -9, -25, -15, -2]; ushort[] destination = [154, 154, 154, 154, 154, 154, 154, 154]; ushort[] expected = [148, 154, 156, 158, 145, 129, 139, 152]; HevcInverseTransformer.AddResidual(residual, destination, destination.Length, destination.Length, 1, 8); Assert.True(expected.AsSpan().SequenceEqual(destination)); } /// /// Applies both inverse-transform dimensions using direct matrix products and normative rounding points. /// /// The dequantized coefficient block. /// The destination signed residual block. /// The full-block intermediate buffer. /// The transform-block width. /// The transform-block height. /// The reconstructed component precision. /// The transform dynamic range excluding its sign bit. /// Whether both dimensions use the four-point inverse DST. private static void TransformDenseOracle( ReadOnlySpan coefficients, Span residual, Span intermediate, int width, int height, int bitDepth, int maxTransformDynamicRange, bool useDiscreteSineTransform) { int dynamicMinimum = -(1 << maxTransformDynamicRange); int dynamicMaximum = (1 << maxTransformDynamicRange) - 1; for (int y = 0; y < height; y++) { for (int xFrequency = 0; xFrequency < width; xFrequency++) { int sum = 0; for (int yFrequency = 0; yFrequency < height; yFrequency++) { sum += coefficients[(yFrequency * width) + xFrequency] * GetInverseCoefficient(height, yFrequency, y, useDiscreteSineTransform); } intermediate[(y * width) + xFrequency] = Math.Clamp((sum + 64) >> 7, dynamicMinimum, dynamicMaximum); } } int secondShift = maxTransformDynamicRange + 5 - bitDepth; int secondRounding = 1 << (secondShift - 1); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int sum = 0; for (int xFrequency = 0; xFrequency < width; xFrequency++) { sum += intermediate[(y * width) + xFrequency] * GetInverseCoefficient(width, xFrequency, x, useDiscreteSineTransform); } residual[(y * width) + x] = Math.Clamp((sum + secondRounding) >> secondShift, short.MinValue, short.MaxValue); } } } /// /// Gets one coefficient from the normative HEVC inverse-DCT or four-point inverse-DST matrix. /// /// The transform dimension. /// The frequency-domain coordinate. /// The spatial-domain coordinate. /// Whether the four-point inverse DST is selected. /// The signed matrix coefficient. private static int GetInverseCoefficient(int size, int frequency, int position, bool useDiscreteSineTransform) { if (useDiscreteSineTransform) { ReadOnlySpan sine = [ 29, 55, 74, 84, 74, 74, 0, -74, 84, -29, -74, 55, 55, -84, 74, -29 ]; return sine[(frequency * 4) + position]; } if (frequency == 0) { return 64; } ReadOnlySpan magnitudes = [ 90, 90, 90, 90, 89, 88, 87, 85, 83, 82, 80, 78, 75, 73, 70, 67, 64, 61, 57, 54, 50, 46, 43, 38, 36, 31, 25, 22, 18, 13, 9, 4, 0 ]; int angle = (((2 * position) + 1) * frequency * (32 / size)) & 127; if (angle > 64) { angle = 128 - angle; } return angle > 32 ? -magnitudes[64 - angle] : magnitudes[angle]; } }