diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs index 97371b376b..eda267bb97 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; +using System.ComponentModel; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; @@ -12,11 +12,11 @@ internal class Av1Inverse2dTransformer /// /// SVT: inv_txfm2d_add_c /// - internal static void InverseTransform2dAdd( + internal static void Transform2dAdd( Span input, - Span outputForRead, + Span outputForRead, int strideForRead, - Span outputForWrite, + Span outputForWrite, int strideForWrite, Av1Transform2dFlipConfiguration config, Span transformFunctionBuffer, @@ -78,8 +78,8 @@ internal class Av1Inverse2dTransformer } Av1InverseTransformMath.RoundShiftArray(bufPtr, transformWidth, -shift[0]); - input.Slice(transformWidth); - bufPtr.Slice(transformWidth); + input = input[transformWidth..]; + bufPtr = bufPtr.Slice(transformWidth); } // Columns @@ -87,17 +87,21 @@ internal class Av1Inverse2dTransformer { if (!config.FlipLeftToRight) { + int t = c; for (r = 0; r < transformHeight; ++r) { - tempIn[r] = buf[(r * transformWidth) + c]; + tempIn[r] = buf[t]; + t += transformWidth; } } else { // flip left right + int t = transformWidth - c - 1; for (r = 0; r < transformHeight; ++r) { - tempIn[r] = buf[(r * transformWidth) + (transformWidth - c - 1)]; + tempIn[r] = buf[t]; + t += transformWidth; } } @@ -106,19 +110,29 @@ internal class Av1Inverse2dTransformer Av1InverseTransformMath.RoundShiftArray(tempOut, transformHeight, -shift[1]); if (!config.FlipUpsideDown) { + int indexForWrite = c; + int indexForRead = c; for (r = 0; r < transformHeight; ++r) { - outputForWrite[(r * strideForWrite) + c] = - Av1InverseTransformMath.ClipPixelAdd(outputForRead[(r * strideForRead) + c], tempOut[r], bitDepth); + outputForWrite[indexForWrite] = + Av1InverseTransformMath.ClipPixelAdd(outputForRead[indexForRead], tempOut[r], bitDepth); + indexForWrite += strideForWrite; + indexForRead += strideForRead; } } else { // flip upside down + int indexForWrite = c; + int indexForRead = c; + int indexTemp = transformHeight - 1; for (r = 0; r < transformHeight; ++r) { - outputForWrite[(r * strideForWrite) + c] = Av1InverseTransformMath.ClipPixelAdd( - outputForRead[(r * strideForRead) + c], tempOut[transformHeight - r - 1], bitDepth); + outputForWrite[indexForWrite] = Av1InverseTransformMath.ClipPixelAdd( + outputForRead[indexForRead], tempOut[indexTemp], bitDepth); + indexForWrite += strideForWrite; + indexForRead += strideForRead; + indexTemp--; } } } @@ -127,7 +141,7 @@ internal class Av1Inverse2dTransformer /// /// SVT: inv_txfm2d_add_c /// - internal static void InverseTransform2dAdd( + internal static void Transform2dAdd( Span input, Span outputForRead, int strideForRead, @@ -240,6 +254,7 @@ internal class Av1Inverse2dTransformer } } + /* /// /// SVT: highbd_iwht4x4_add /// @@ -260,8 +275,8 @@ internal class Av1Inverse2dTransformer /// private static void InverseWhalshHadamard4x4Add16(ref int input, ref byte destinationForRead, int strideForRead, ref byte destinationForWrite, int strideForWrite, int bitDepth) { - /* 4-point reversible, orthonormal inverse Walsh-Hadamard in 3.5 adds, - 0.5 shifts per pixel. */ + // 4-point reversible, orthonormal inverse Walsh-Hadamard in 3.5 adds, + // 0.5 shifts per pixel. int i; Span output = stackalloc ushort[16]; ushort a1, b1, c1, d1, e1; @@ -311,7 +326,7 @@ internal class Av1Inverse2dTransformer range_check_value(b1, (int8_t)(bd + 1)); range_check_value(c1, (int8_t)(bd + 1)); range_check_value(d1, (int8_t)(bd + 1)); - */ + // destForWrite = Av1InverseTransformMath.ClipPixelAdd(destForRead, a1, bitDepth); Unsafe.Add(ref destForWrite, strideForWrite) = Av1InverseTransformMath.ClipPixelAdd(Unsafe.Add(ref destForRead, strideForRead), b1, bitDepth); @@ -360,4 +375,5 @@ internal class Av1Inverse2dTransformer destForWrite = ref Unsafe.Add(ref destForWrite, 1); } } + */ } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs index c8c7a04d3e..4a4cae4564 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs @@ -157,23 +157,23 @@ internal static class Av1InverseTransformMath return (byte)ClipPixelHighBitDepth(dest + trans, 8); } - public static ushort ClipPixelAdd(ushort dest, long trans, int bitDepth) + public static short ClipPixelAdd(short dest, long trans, int bitDepth) { trans = CheckRange(trans, bitDepth); return ClipPixelHighBitDepth(dest + trans, bitDepth); } - private static ushort ClipPixelHighBitDepth(long val, int bd) + private static short ClipPixelHighBitDepth(long val, int bd) { switch (bd) { case 8: default: - return (ushort)Av1Math.Clamp(val, 0, 255); + return (short)Av1Math.Clamp(val, 0, 255); case 10: - return (ushort)Av1Math.Clamp(val, 0, 1023); + return (short)Av1Math.Clamp(val, 0, 1023); case 12: - return (ushort)Av1Math.Clamp(val, 0, 4095); + return (short)Av1Math.Clamp(val, 0, 4095); } } @@ -234,4 +234,19 @@ internal static class Av1InverseTransformMath int int_min = -int_max - 1; return Av1Math.Clamp(input, int_min, int_max); } + + internal static int GetMaxEndOfBuffer(Av1TransformSize transformSize) + { + if (transformSize is Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64) + { + return 1024; + } + + if (transformSize is Av1TransformSize.Size16x64 or Av1TransformSize.Size64x16) + { + return 512; + } + + return transformSize.GetSize2d(); + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs index 1f73edae1b..474364df8c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs @@ -15,17 +15,17 @@ internal static class Av1InverseTransformerFactory int height = transformFunctionParameters.TransformSize.GetHeight(); Span buffer = new int[(width * height) + (2 * Math.Max(width, height))]; Av1Transform2dFlipConfiguration config = new(transformFunctionParameters.TransformType, transformFunctionParameters.TransformSize); - Av1Inverse2dTransformer.InverseTransform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, config, buffer); + Av1Inverse2dTransformer.Transform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, config, buffer); } - public static unsafe void InverseTransformAdd(Span coefficients, Span readBuffer, int readStride, Span writeBuffer, int writeStride, Av1TransformFunctionParameters transformFunctionParameters) + public static unsafe void InverseTransformAdd(Span coefficients, Span readBuffer, int readStride, Span writeBuffer, int writeStride, Av1TransformFunctionParameters transformFunctionParameters) { Guard.IsTrue(transformFunctionParameters.Is16BitPipeline, nameof(transformFunctionParameters), "Calling 16-bit pipeline while 8-bit is requested."); int width = transformFunctionParameters.TransformSize.GetWidth(); int height = transformFunctionParameters.TransformSize.GetHeight(); Span buffer = new int[(width * height) + (2 * Math.Max(width, height))]; Av1Transform2dFlipConfiguration config = new(transformFunctionParameters.TransformType, transformFunctionParameters.TransformSize); - Av1Inverse2dTransformer.InverseTransform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, config, buffer, transformFunctionParameters.BitDepth); + Av1Inverse2dTransformer.Transform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, config, buffer, transformFunctionParameters.BitDepth); } internal static IAv1Forward1dTransformer? GetTransformer(Av1TransformFunctionType type) => type switch diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs index f25082b4d7..c84531a76a 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Inverse; @@ -74,6 +75,151 @@ public class Av1InverseTransformTests public void AccuracyOfEchoTransformSize4Test() => AssertAccuracy1d(Av1TransformType.Identity, Av1TransformSize.Size4x4, 0, new EchoTestTransformer(), new EchoTestTransformer()); + [Fact] + public void FlipNothingTest() + { + // Arrange + int[] input = [ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16]; + short[] expected = [ + 2, 4, 6, 8, + 10, 12, 14, 16, + 18, 20, 22, 24, + 26, 28, 30, 32]; + int[] temp = new int[16 + 8]; + short[] actual = new short[16]; + Av1Transform2dFlipConfiguration config = new(Av1TransformType.Identity, Av1TransformSize.Size4x4); + config.GenerateStageRange(8); + config.SetFlip(false, false); + config.SetShift(0, 0, 0); + IAv1Forward1dTransformer transformer = new EchoTestTransformer(); + + // Act + Av1Inverse2dTransformer.Transform2dAdd( + input, + actual, + 4, + actual, + 4, + config, + temp, + 8); + + // Assert + Assert.True(CompareWithError(expected, actual, 1)); + } + + [Fact] + public void FlipHorizontalTest() + { + // Arrange + short[] expected = [ + 8, 6, 4, 2, + 16, 14, 12, 10, + 24, 22, 20, 18, + 32, 30, 28, 26]; + int[] input = [ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16]; + int[] temp = new int[16 + 8]; + short[] actual = new short[16]; + Av1Transform2dFlipConfiguration config = new(Av1TransformType.Identity, Av1TransformSize.Size4x4); + config.SetFlip(false, true); + config.SetShift(0, 0, 0); + IAv1Forward1dTransformer transformer = new EchoTestTransformer(); + + // Act + Av1Inverse2dTransformer.Transform2dAdd( + input, + actual, + 4, + actual, + 4, + config, + temp, + 8); + + // Assert + Assert.True(CompareWithError(expected, actual, 1)); + } + + [Fact] + public void FlipVerticalTest() + { + // Arrange + short[] expected = [ + 26, 28, 30, 32, + 18, 20, 22, 24, + 10, 12, 14, 16, + 2, 4, 6, 8]; + int[] input = [ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16]; + int[] temp = new int[16 + 8]; + short[] actual = new short[16]; + Av1Transform2dFlipConfiguration config = new(Av1TransformType.Identity, Av1TransformSize.Size4x4); + config.SetFlip(true, false); + config.SetShift(0, 0, 0); + IAv1Forward1dTransformer transformer = new EchoTestTransformer(); + + // Act + Av1Inverse2dTransformer.Transform2dAdd( + input, + actual, + 4, + actual, + 4, + config, + temp, + 8); + + // Assert + Assert.True(CompareWithError(expected, actual, 1)); + } + + [Fact] + public void FlipHorizontalAndVerticalTest() + { + // Arrange + short[] expected = [ + 32, 30, 28, 26, + 24, 22, 20, 18, + 16, 14, 12, 10, + 8, 6, 4, 2]; + int[] input = [ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16]; + int[] temp = new int[16 + 8]; + short[] actual = new short[16]; + Av1Transform2dFlipConfiguration config = new(Av1TransformType.Identity, Av1TransformSize.Size4x4); + config.SetFlip(true, true); + config.SetShift(0, 0, 0); + IAv1Forward1dTransformer transformer = new EchoTestTransformer(); + + // Act + Av1Inverse2dTransformer.Transform2dAdd( + input, + actual, + 4, + actual, + 4, + config, + temp, + 8); + + // Assert + Assert.True(CompareWithError(expected, actual, 1)); + } + private static void AssertAccuracy1d( Av1TransformType transformType, Av1TransformSize transformSize, @@ -129,10 +275,160 @@ public class Av1InverseTransformTests config.StageRangeColumn); // Assert - Assert.True(CompareWithError(inputOfTest, outputOfTest.Select(x => x >> scaleLog2).ToArray(), allowedError), $"Error: {GetMaximumError(inputOfTest, outputOfTest)}"); + Assert.True(CompareWithError(inputOfTest, outputOfTest.Select(x => x >> scaleLog2).ToArray(), allowedError), $"Error: {GetMaximumError(inputOfTest, outputOfTest)}"); + } + } + + // [Theory] + // [MemberData(nameof(Generate2dCombinations))] + public void Test2dTransformAdd(int txSize, int txType, bool isLossless) + { + const int bitDepth = 8; + Av1TransformType transformType = (Av1TransformType)txType; + Av1TransformSize transformSize = (Av1TransformSize)txSize; + Av1TransformFunctionParameters transformFunctionParams = new() + { + BitDepth = bitDepth, + IsLossless = isLossless, + TransformSize = transformSize, + EndOfBuffer = Av1InverseTransformMath.GetMaxEndOfBuffer(transformSize) + }; + + if (bitDepth > 8 && !isLossless) + { + // Not support 10 bit with not lossless + return; + } + + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + uint stride = (uint)width; + short[] input = new short[width * height]; + int[] referenceOutput = new int[width * height]; + short[] outputOfTest = new short[width * height]; + int[] transformActual = new int[width * height]; + int[] tempBuffer = new int[(width * height) + 128]; + + transformFunctionParams.TransformType = transformType; + Av1Transform2dFlipConfiguration config = new(transformType, transformSize); + config.GenerateStageRange(bitDepth); + + const int loops = 1; // Initially: 10; + for (int k = 0; k < loops; k++) + { + PopulateWithRandomValues(input, bitDepth); + + Av1ForwardTransformer.Transform2d( + input, + referenceOutput, + stride, + transformType, + transformSize, + bitDepth); + Av1Inverse2dTransformer.Transform2dAdd( + referenceOutput.Select(x => x >> 3).ToArray(), + outputOfTest, + width, + outputOfTest, + width, + config, + tempBuffer, + bitDepth); + Av1ForwardTransformer.Transform2d( + outputOfTest.Select(x => (short)(x >> 1)).ToArray(), + transformActual, + stride, + transformType, + transformSize, + bitDepth); + + Assert.True(CompareWithError(referenceOutput, transformActual, 1), $"Error: {GetMaximumError(referenceOutput, transformActual)}"); + } + } + + private static void DivideArray(Span list, int factor) + { + for (int i = 0; i < list.Length; i++) + { + list[i] = list[i] / factor; + } + } + + private static void DivideArray(Span list, int factor) + { + for (int i = 0; i < list.Length; i++) + { + list[i] = (short)(list[i] / factor); } } + public static TheoryData Generate2dCombinations() + { + int[][] transformFunctionSupportMatrix = [ + + // [Size][type]" // O - No; 1 - lossless; 2 - !lossless; 3 - any + /*0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15*/ + [3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], // 0 TX_4X4, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 1 TX_8X8, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 2 TX_16X16, + [3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0], // 3 TX_32X32, + [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // 4 TX_64X64, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 5 TX_4X8, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 6 TX_8X4, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 7 TX_8X16, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 8 TX_16X8, + [3, 1, 3, 1, 1, 3, 1, 1, 1, 3, 3, 3, 1, 3, 1, 3], // 9 TX_16X32, + [3, 3, 1, 1, 3, 1, 1, 1, 1, 3, 3, 3, 3, 1, 3, 1], // 10 TX_32X16, + [3, 0, 1, 0, 0, 1, 0, 0, 0, 3, 3, 3, 0, 1, 0, 1], // 11 TX_32X64, + [3, 1, 0, 0, 1, 0, 0, 0, 0, 3, 3, 3, 1, 0, 1, 0], // 12 TX_64X32, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 13 TX_4X16, + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], // 14 TX_16X4, + [3, 1, 3, 1, 1, 3, 1, 1, 1, 3, 3, 3, 1, 3, 1, 3], // 15 TX_8X32, + [3, 3, 1, 1, 3, 1, 1, 1, 1, 3, 3, 3, 3, 1, 3, 1], // 16 TX_32X8, + [3, 0, 3, 0, 0, 3, 0, 0, 0, 3, 3, 3, 0, 3, 0, 3], // 17 TX_16X64, + [3, 3, 0, 0, 3, 0, 0, 0, 0, 3, 3, 3, 3, 0, 3, 0], // 18 TX_64X16, + /*0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15*/ + ]; + + TheoryData data = []; + for (int size = 0; size < (int)Av1TransformSize.AllSizes; size++) + { + for (int type = 0; type < (int)Av1TransformType.AllTransformTypes; type++) + { + for (int i = 0; i < 2; i++) + { + bool isLossless = i == 1; + if ((isLossless && ((transformFunctionSupportMatrix[size][type] & 1) == 0)) || + (!isLossless && ((transformFunctionSupportMatrix[size][type] & 2) == 0))) + { + continue; + } + + if (IsTransformTypeImplemented((Av1TransformType)type, (Av1TransformSize)size)) + { + data.Add(size, type, isLossless); + } + } + } + } + + return data; + } + + private static void PopulateWithRandomValues(Span input, int bitDepth) + { + Random rnd = new(42); + int maxValue = (1 << (bitDepth - 1)) - 1; + int minValue = -maxValue; + for (int i = 0; i < input.Length; i++) + { + input[i] = (short)rnd.Next(minValue, maxValue); + } + } + + private static bool IsTransformTypeImplemented(Av1TransformType transformType, Av1TransformSize transformSize) + => transformSize == Av1TransformSize.Size4x4; + private static IAv1Forward1dTransformer GetForwardTransformer(Av1TransformFunctionType func) => func switch { @@ -175,20 +471,21 @@ public class Av1InverseTransformTests _ => null, }; - private static bool CompareWithError(Span expected, Span actual, int allowedError) + private static bool CompareWithError(Span expected, Span actual, int allowedError) + where T : unmanaged { // compare for the result is within accuracy int maximumErrorInTest = GetMaximumError(expected, actual); return maximumErrorInTest <= allowedError; } - private static int GetMaximumError(Span expected, Span actual) + private static int GetMaximumError(Span expected, Span actual) { int maximumErrorInTest = 0; int count = Math.Min(expected.Length, 32); for (int ni = 0; ni < count; ++ni) { - maximumErrorInTest = Math.Max(maximumErrorInTest, Math.Abs(actual[ni] - expected[ni])); + maximumErrorInTest = Math.Max(maximumErrorInTest, Math.Abs(Convert.ToInt32(actual[ni]) - Convert.ToInt32(expected[ni]))); } return maximumErrorInTest;