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;