Browse Source

Use tensor negation for sharpen kernels

pull/3161/head
James Jackson-South 3 weeks ago
parent
commit
c1dc1aedb4
  1. 2
      src/ImageSharp/Common/Helpers/TensorPrimitives.cs
  2. 276
      src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
  3. 20
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs
  4. 36
      tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
  5. 35
      tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs

2
src/ImageSharp/Common/Helpers/TensorPrimitives.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Common.Helpers;
/// implementation when ImageSharp no longer supports target frameworks that predate it.
/// </remarks>
#pragma warning disable SA1649 // File name should match first type name
internal static class TensorPrimitives_
internal static partial class TensorPrimitives_
#pragma warning restore SA1649 // File name should match first type name
{
/// <summary>

276
src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs

@ -0,0 +1,276 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Common.Helpers;
internal static partial class TensorPrimitives_
{
/// <summary>
/// Defines an element-wise unary operation.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private interface IUnaryOperator<T>
{
/// <summary>
/// Gets a value indicating whether the operation supports vector execution.
/// </summary>
public static abstract bool Vectorizable { get; }
/// <summary>
/// Applies the operation to a scalar value.
/// </summary>
/// <param name="x">The input value.</param>
/// <returns>The operation result.</returns>
public static abstract T Invoke(T x);
/// <summary>
/// Applies the operation to a 128-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector128<T> Invoke(Vector128<T> x);
/// <summary>
/// Applies the operation to a 256-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector256<T> Invoke(Vector256<T> x);
/// <summary>
/// Applies the operation to a 512-bit vector.
/// </summary>
/// <param name="x">The input vector.</param>
/// <returns>The operation result.</returns>
public static abstract Vector512<T> Invoke(Vector512<T> x);
}
/// <summary>
/// Computes the element-wise negation of the values in <paramref name="x"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="x">The values to negate.</param>
/// <param name="destination">The destination for the negated values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Negate<T>(ReadOnlySpan<T> x, Span<T> destination)
where T : IUnaryNegationOperators<T, T>
=> InvokeSpanIntoSpan<T, NegateOperator<T>>(x, destination);
/// <summary>
/// Performs an element-wise unary operation over a span.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="x">The input values.</param>
/// <param name="destination">The destination values.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeSpanIntoSpan<T, TOperator>(ReadOnlySpan<T> x, Span<T> destination)
where TOperator : struct, IUnaryOperator<T>
{
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
// The dispatch matches the other compatibility pipelines: AVX-512 is reserved for large spans because
// its setup cost is not recovered by the short image-processing buffers that dominate ImageSharp.
if (TOperator.Vectorizable
&& Vector512.IsHardwareAccelerated
&& Vector512<T>.IsSupported
&& length >= 512)
{
InvokeUnaryVectorized512<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector256.IsHardwareAccelerated && Vector256<T>.IsSupported && length >= (uint)Vector256<T>.Count)
{
InvokeUnaryVectorized256<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
if (TOperator.Vectorizable && Vector128.IsHardwareAccelerated && Vector128<T>.IsSupported && length >= (uint)Vector128<T>.Count)
{
InvokeUnaryVectorized128<T, TOperator>(ref xRef, ref destinationRef, length);
return;
}
for (nuint i = 0; i < length; i++)
{
Unsafe.Add(ref destinationRef, i) = TOperator.Invoke(Unsafe.Add(ref xRef, i));
}
}
/// <summary>
/// Applies a unary operation with 128-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized128<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector128<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
// The final vector overlaps the preceding store when the length is not a vector multiple. Loading it
// before any stores preserves same-start in-place operation because it captures the original tail.
Vector128<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector128.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a unary operation with 256-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized256<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector256<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector256<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector256.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Applies a unary operation with 512-bit vectors.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <typeparam name="TOperator">The operation to apply.</typeparam>
/// <param name="xRef">The first input element.</param>
/// <param name="destinationRef">The first destination element.</param>
/// <param name="length">The number of elements to process.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InvokeUnaryVectorized512<T, TOperator>(ref T xRef, ref T destinationRef, nuint length)
where TOperator : struct, IUnaryOperator<T>
{
nuint vectorCount = (uint)Vector512<T>.Count;
nuint vectorsPerLoop = vectorCount * 8;
nuint index = 0;
Vector512<T> end = default;
if ((length % vectorCount) != 0)
{
end = TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, length - vectorCount));
}
while ((length - index) >= vectorsPerLoop)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 0))).StoreUnsafe(ref destinationRef, index + (vectorCount * 0));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 1))).StoreUnsafe(ref destinationRef, index + (vectorCount * 1));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 2))).StoreUnsafe(ref destinationRef, index + (vectorCount * 2));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 3))).StoreUnsafe(ref destinationRef, index + (vectorCount * 3));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 4))).StoreUnsafe(ref destinationRef, index + (vectorCount * 4));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 5))).StoreUnsafe(ref destinationRef, index + (vectorCount * 5));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 6))).StoreUnsafe(ref destinationRef, index + (vectorCount * 6));
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index + (vectorCount * 7))).StoreUnsafe(ref destinationRef, index + (vectorCount * 7));
index += vectorsPerLoop;
}
while ((length - index) >= vectorCount)
{
TOperator.Invoke(Vector512.LoadUnsafe(ref xRef, index)).StoreUnsafe(ref destinationRef, index);
index += vectorCount;
}
if (index != length)
{
end.StoreUnsafe(ref destinationRef, length - vectorCount);
}
}
/// <summary>
/// Implements element-wise negation for scalar and SIMD inputs.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
private readonly struct NegateOperator<T> : IUnaryOperator<T>
where T : IUnaryNegationOperators<T, T>
{
/// <inheritdoc />
public static bool Vectorizable => true;
/// <inheritdoc />
public static T Invoke(T x) => -x;
/// <inheritdoc />
public static Vector128<T> Invoke(Vector128<T> x) => -x;
/// <inheritdoc />
public static Vector256<T> Invoke(Vector256<T> x) => -x;
/// <inheritdoc />
public static Vector512<T> Invoke(Vector512<T> x) => -x;
}
}

20
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessorHelpers.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution;
@ -68,19 +69,12 @@ internal static class ConvolutionProcessorHelpers
// Invert the kernel for sharpening.
int midpointRounded = (int)midpoint;
for (int i = 0; i < size; i++)
{
if (i == midpointRounded)
{
// Calculate central value
kernel[i] = (2F * sum) - kernel[i];
}
else
{
// invert value
kernel[i] = -kernel[i];
}
}
float midpointValue = kernel[midpointRounded];
TensorPrimitives_.Negate<float>(kernel, kernel);
// The sharpening kernel negates every Gaussian weight except its center. Restore that original
// center while adding twice the Gaussian sum so the complete kernel retains unit response.
kernel[midpointRounded] = (2F * sum) - midpointValue;
// Normalize kernel so that the sum of all weights equals 1
for (int i = 0; i < size; i++)

36
tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs

@ -117,6 +117,42 @@ public class TensorPrimitivesTests
Assert.Equal(expected, inPlace);
}
/// <summary>
/// Verifies that floating-point negation preserves the scalar operator's exact bit-level behavior.
/// </summary>
/// <param name="length">The input length.</param>
[Theory]
[MemberData(nameof(SpanLengths))]
public void NegateSingleMatchesScalarFormula(int length)
{
float[] values =
{
float.NaN,
-0F,
0F,
-1F,
1F,
float.NegativeInfinity,
float.PositiveInfinity
};
float[] source = new float[length];
float[] expected = new float[length];
for (int i = 0; i < source.Length; i++)
{
source[i] = values[i % values.Length];
expected[i] = -source[i];
}
float[] destination = new float[length];
TensorPrimitives_.Negate<float>(source, destination);
AssertSingleBitsEqual(expected, destination);
TensorPrimitives_.Negate<float>(source, source);
AssertSingleBitsEqual(expected, source);
}
/// <summary>
/// Verifies that integer clamping produces identical results for separate and in-place destinations.
/// </summary>

35
tests/ImageSharp.Tests/Processing/Processors/Convolution/ConvolutionProcessorHelpersTest.cs

@ -41,6 +41,41 @@ public class ConvolutionProcessorHelpersTest
}
}
/// <summary>
/// Verifies that Gaussian sharpening preserves the scalar kernel formula across scalar and SIMD lengths.
/// </summary>
/// <param name="radius">The kernel radius.</param>
[Theory]
[InlineData(1)]
[InlineData(3)]
[InlineData(9)]
[InlineData(32)]
[InlineData(80)]
public void VerifyGaussianSharpenKernel(int radius)
{
int kernelSize = (radius * 2) + 1;
float sigma = radius / 3F;
float[] expected = new float[kernelSize];
float sum = 0F;
for (int i = 0; i < kernelSize; i++)
{
float value = Numerics.Gaussian(i - radius, sigma);
expected[i] = value;
sum += value;
}
for (int i = 0; i < kernelSize; i++)
{
expected[i] = i == radius ? (2F * sum) - expected[i] : -expected[i];
expected[i] /= sum;
}
float[] actual = ConvolutionProcessorHelpers.CreateGaussianSharpenKernel(kernelSize, sigma);
Assert.Equal(expected, actual);
}
[Fact]
public void VerifyNonSeparableMatrix()
{

Loading…
Cancel
Save