Browse Source

Merge pull request #2793 from SixLabors/js/resize-map-optimizations

Speed improvements to resize convolution (no vpermps w/ FMA)
pull/3061/head
James Jackson-South 8 months ago
committed by GitHub
parent
commit
ad816ed988
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      README.md
  2. 2
      shared-infrastructure
  3. 43
      src/ImageSharp/Common/Helpers/Numerics.cs
  4. 119
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  5. 13
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
  6. 75
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  7. 31
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
  8. 155
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

2
README.md

@ -8,7 +8,7 @@ SixLabors.ImageSharp
<div align="center"> <div align="center">
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions) [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) [![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) [![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947 Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d

43
src/ImageSharp/Common/Helpers/Numerics.cs

@ -1080,4 +1080,47 @@ internal static class Numerics
public static nuint Vector512Count<TVector>(int length) public static nuint Vector512Count<TVector>(int length)
where TVector : struct where TVector : struct
=> (uint)length / (uint)Vector512<TVector>.Count; => (uint)length / (uint)Vector512<TVector>.Count;
/// <summary>
/// Normalizes the values in a given <see cref="Span{T}"/>.
/// </summary>
/// <param name="span">The sequence of <see cref="float"/> values to normalize.</param>
/// <param name="sum">The sum of the values in <paramref name="span"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span<float> span, float sum)
{
if (Vector256.IsHardwareAccelerated)
{
ref float startRef = ref MemoryMarshal.GetReference(span);
ref float endRef = ref Unsafe.Add(ref startRef, span.Length & ~7);
Vector256<float> sum256 = Vector256.Create(sum);
while (Unsafe.IsAddressLessThan(ref startRef, ref endRef))
{
Unsafe.As<float, Vector256<float>>(ref startRef) /= sum256;
startRef = ref Unsafe.Add(ref startRef, (nuint)8);
}
if ((span.Length & 7) >= 4)
{
Unsafe.As<float, Vector128<float>>(ref startRef) /= sum256.GetLower();
startRef = ref Unsafe.Add(ref startRef, (nuint)4);
}
endRef = ref Unsafe.Add(ref startRef, span.Length & 3);
while (Unsafe.IsAddressLessThan(ref startRef, ref endRef))
{
startRef /= sum;
startRef = ref Unsafe.Add(ref startRef, (nuint)1);
}
}
else
{
for (int i = 0; i < span.Length; i++)
{
span[i] /= sum;
}
}
}
} }

119
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -5,7 +5,7 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -14,11 +14,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
/// </summary> /// </summary>
internal readonly unsafe struct ResizeKernel internal readonly unsafe struct ResizeKernel
{ {
/// <summary>
/// The buffer with the convolution factors.
/// Note that when FMA is supported, this is of size 4x that reported in <see cref="Length"/>.
/// </summary>
private readonly float* bufferPtr; private readonly float* bufferPtr;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResizeKernel"/> struct. /// Initializes a new instance of the <see cref="ResizeKernel"/> struct.
/// </summary> /// </summary>
/// <param name="startIndex">The starting index for the destination row.</param>
/// <param name="bufferPtr">The pointer to the buffer with the convolution factors.</param>
/// <param name="length">The length of the kernel.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel(int startIndex, float* bufferPtr, int length) internal ResizeKernel(int startIndex, float* bufferPtr, int length)
{ {
@ -27,6 +34,15 @@ internal readonly unsafe struct ResizeKernel
this.Length = length; this.Length = length;
} }
/// <summary>
/// Gets a value indicating whether vectorization is supported.
/// </summary>
public static bool IsHardwareAccelerated
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Vector256.IsHardwareAccelerated;
}
/// <summary> /// <summary>
/// Gets the start index for the destination row. /// Gets the start index for the destination row.
/// </summary> /// </summary>
@ -53,7 +69,15 @@ internal readonly unsafe struct ResizeKernel
public Span<float> Values public Span<float> Values
{ {
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
get => new(this.bufferPtr, this.Length); get
{
if (Vector256.IsHardwareAccelerated)
{
return new(this.bufferPtr, this.Length * 4);
}
return new(this.bufferPtr, this.Length);
}
} }
/// <summary> /// <summary>
@ -68,73 +92,45 @@ internal readonly unsafe struct ResizeKernel
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public Vector4 ConvolveCore(ref Vector4 rowStartRef) public Vector4 ConvolveCore(ref Vector4 rowStartRef)
{ {
if (Avx2.IsSupported && Fma.IsSupported) if (IsHardwareAccelerated)
{ {
float* bufferStart = this.bufferPtr; float* bufferStart = this.bufferPtr;
float* bufferEnd = bufferStart + (this.Length & ~3); ref Vector4 rowEndRef = ref Unsafe.Add(ref rowStartRef, this.Length & ~3);
Vector256<float> result256_0 = Vector256<float>.Zero; Vector256<float> result256_0 = Vector256<float>.Zero;
Vector256<float> result256_1 = Vector256<float>.Zero; Vector256<float> result256_1 = Vector256<float>.Zero;
ReadOnlySpan<byte> maskBytes =
[
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0
];
Vector256<int> mask = Unsafe.ReadUnaligned<Vector256<int>>(ref MemoryMarshal.GetReference(maskBytes));
while (bufferStart < bufferEnd) while (Unsafe.IsAddressLessThan(ref rowStartRef, ref rowEndRef))
{ {
// It is important to use a single expression here so that the JIT will correctly use vfmadd231ps Vector256<float> pixels256_0 = Unsafe.As<Vector4, Vector256<float>>(ref rowStartRef);
// for the FMA operation, and execute it directly on the target register and reading directly from Vector256<float> pixels256_1 = Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref rowStartRef, (nuint)2));
// memory for the first parameter. This skips initializing a SIMD register, and an extra copy.
// The code below should compile in the following assembly on .NET 5 x64: result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0);
// result256_1 = Vector256_.MultiplyAdd(result256_1, Vector256.Load(bufferStart + 8), pixels256_1);
// vmovsd xmm2, [rax] ; load *(double*)bufferStart into xmm2 as [ab, _]
// vpermps ymm2, ymm1, ymm2 ; permute as a float YMM register to [a, a, a, a, b, b, b, b] bufferStart += 16;
// vfmadd231ps ymm0, ymm2, [r8] ; result256_0 = FMA(pixels, factors) + result256_0 rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)4);
//
// For tracking the codegen issue with FMA, see: https://github.com/dotnet/runtime/issues/12212.
// Additionally, we're also unrolling two computations per each loop iterations to leverage the
// fact that most CPUs have two ports to schedule multiply operations for FMA instructions.
result256_0 = Fma.MultiplyAdd(
Unsafe.As<Vector4, Vector256<float>>(ref rowStartRef),
Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)bufferStart).AsSingle(), mask),
result256_0);
result256_1 = Fma.MultiplyAdd(
Unsafe.As<Vector4, Vector256<float>>(ref Unsafe.Add(ref rowStartRef, 2)),
Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)(bufferStart + 2)).AsSingle(), mask),
result256_1);
bufferStart += 4;
rowStartRef = ref Unsafe.Add(ref rowStartRef, 4);
} }
result256_0 = Avx.Add(result256_0, result256_1); result256_0 += result256_1;
if ((this.Length & 3) >= 2) if ((this.Length & 3) >= 2)
{ {
result256_0 = Fma.MultiplyAdd( Vector256<float> pixels256_0 = Unsafe.As<Vector4, Vector256<float>>(ref rowStartRef);
Unsafe.As<Vector4, Vector256<float>>(ref rowStartRef), result256_0 = Vector256_.MultiplyAdd(result256_0, Vector256.Load(bufferStart), pixels256_0);
Avx2.PermuteVar8x32(Vector256.CreateScalarUnsafe(*(double*)bufferStart).AsSingle(), mask),
result256_0);
bufferStart += 2; bufferStart += 8;
rowStartRef = ref Unsafe.Add(ref rowStartRef, 2); rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)2);
} }
Vector128<float> result128 = Sse.Add(result256_0.GetLower(), result256_0.GetUpper()); Vector128<float> result128 = result256_0.GetLower() + result256_0.GetUpper();
if ((this.Length & 1) != 0) if ((this.Length & 1) != 0)
{ {
result128 = Fma.MultiplyAdd( Vector128<float> pixels128 = Unsafe.As<Vector4, Vector128<float>>(ref rowStartRef);
Unsafe.As<Vector4, Vector128<float>>(ref rowStartRef), result128 = Vector128_.MultiplyAdd(result128, Vector128.Load(bufferStart), pixels128);
Vector128.Create(*bufferStart),
result128);
} }
return *(Vector4*)&result128; return result128.AsVector4();
} }
else else
{ {
@ -149,7 +145,7 @@ internal readonly unsafe struct ResizeKernel
result += rowStartRef * *bufferStart; result += rowStartRef * *bufferStart;
bufferStart++; bufferStart++;
rowStartRef = ref Unsafe.Add(ref rowStartRef, 1); rowStartRef = ref Unsafe.Add(ref rowStartRef, (nuint)1);
} }
return result; return result;
@ -160,17 +156,32 @@ internal readonly unsafe struct ResizeKernel
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="StartIndex"/> /// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="StartIndex"/>
/// to the value <paramref name="left"/>. /// to the value <paramref name="left"/>.
/// </summary> /// </summary>
/// <param name="left">The new value for <see cref="StartIndex"/>.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel AlterLeftValue(int left) internal ResizeKernel AlterLeftValue(int left)
=> new(left, this.bufferPtr, this.Length); => new(left, this.bufferPtr, this.Length);
internal void Fill(Span<double> values) internal void FillOrCopyAndExpand(Span<float> values)
{ {
DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!"); DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!");
for (int i = 0; i < this.Length; i++) if (IsHardwareAccelerated)
{
Vector4* bufferStart = (Vector4*)this.bufferPtr;
ref float valuesStart = ref MemoryMarshal.GetReference(values);
ref float valuesEnd = ref Unsafe.Add(ref valuesStart, values.Length);
while (Unsafe.IsAddressLessThan(ref valuesStart, ref valuesEnd))
{
*bufferStart = new Vector4(valuesStart);
bufferStart++;
valuesStart = ref Unsafe.Add(ref valuesStart, (nuint)1);
}
}
else
{ {
this.Values[i] = (float)values[i]; values.CopyTo(this.Values);
} }
} }
} }

13
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs

@ -16,6 +16,8 @@ internal partial class ResizeKernelMap
private readonly int cornerInterval; private readonly int cornerInterval;
private readonly int sourcePeriod;
public PeriodicKernelMap( public PeriodicKernelMap(
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
int sourceLength, int sourceLength,
@ -24,7 +26,8 @@ internal partial class ResizeKernelMap
double scale, double scale,
int radius, int radius,
int period, int period,
int cornerInterval) int cornerInterval,
int sourcePeriod)
: base( : base(
memoryAllocator, memoryAllocator,
sourceLength, sourceLength,
@ -36,6 +39,7 @@ internal partial class ResizeKernelMap
{ {
this.cornerInterval = cornerInterval; this.cornerInterval = cornerInterval;
this.period = period; this.period = period;
this.sourcePeriod = sourcePeriod;
} }
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}";
@ -54,10 +58,11 @@ internal partial class ResizeKernelMap
int bottomStartDest = this.DestinationLength - this.cornerInterval; int bottomStartDest = this.DestinationLength - this.cornerInterval;
for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++)
{ {
double center = ((i + .5) * this.ratio) - .5;
int left = (int)TolerantMath.Ceiling(center - this.radius);
ResizeKernel kernel = this.kernels[i - this.period]; ResizeKernel kernel = this.kernels[i - this.period];
this.kernels[i] = kernel.AlterLeftValue(left);
// Shift the kernel start index by the source-side period so the same weights align to the
// next repeated sampling window in the source image.
this.kernels[i] = kernel.AlterLeftValue(kernel.StartIndex + this.sourcePeriod);
} }
// Build bottom corner data: // Build bottom corner data:

75
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -33,7 +33,7 @@ internal partial class ResizeKernelMap : IDisposable
private bool isDisposed; private bool isDisposed;
// To avoid both GC allocations, and MemoryAllocator ceremony: // To avoid both GC allocations, and MemoryAllocator ceremony:
private readonly double[] tempValues; private readonly float[] tempValues;
private ResizeKernelMap( private ResizeKernelMap(
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
@ -50,10 +50,12 @@ internal partial class ResizeKernelMap : IDisposable
this.sourceLength = sourceLength; this.sourceLength = sourceLength;
this.DestinationLength = destinationLength; this.DestinationLength = destinationLength;
this.MaxDiameter = (radius * 2) + 1; this.MaxDiameter = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, preferContiguosImageBuffers: true, AllocationOptions.Clean);
int diameter = ResizeKernel.IsHardwareAccelerated ? this.MaxDiameter * 4 : this.MaxDiameter;
this.data = memoryAllocator.Allocate2D<float>(diameter, bufferHeight, preferContiguosImageBuffers: true);
this.pinHandle = this.data.DangerousGetSingleMemory().Pin(); this.pinHandle = this.data.DangerousGetSingleMemory().Pin();
this.kernels = new ResizeKernel[destinationLength]; this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[this.MaxDiameter]; this.tempValues = new float[this.MaxDiameter];
} }
/// <summary> /// <summary>
@ -135,9 +137,13 @@ internal partial class ResizeKernelMap : IDisposable
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius);
// 'ratio' is a rational number. // 'ratio' is a rational number.
// Multiplying it by destSize/GCD(sourceSize, destSize) will result in a whole number "again". // Multiplying it by destSize/GCD(sourceSize, destinationSize) yields an integer, so every `period` rows
// This value is determining the length of the periods in repeating kernel map rows. // the destination-space sampling centers repeat their fractional alignment. `period` is the repeat length
int period = destinationSize / Numerics.GreatestCommonDivisor(sourceSize, destinationSize); // in destination rows, while `sourcePeriod` is the corresponding integer offset in source pixels that
// must be added to the kernel's left index when we reuse the same weights from a previous period.
int gcd = Numerics.GreatestCommonDivisor(sourceSize, destinationSize);
int period = destinationSize / gcd;
int sourcePeriod = sourceSize / gcd;
// the center position at i == 0: // the center position at i == 0:
double center0 = (ratio - 1) * 0.5; double center0 = (ratio - 1) * 0.5;
@ -161,23 +167,24 @@ internal partial class ResizeKernelMap : IDisposable
bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize;
ResizeKernelMap result = hasAtLeast2Periods ResizeKernelMap result = hasAtLeast2Periods
? new PeriodicKernelMap( ? new PeriodicKernelMap(
memoryAllocator, memoryAllocator,
sourceSize, sourceSize,
destinationSize, destinationSize,
ratio, ratio,
scale, scale,
radius, radius,
period, period,
cornerInterval) cornerInterval,
: new ResizeKernelMap( sourcePeriod)
memoryAllocator, : new ResizeKernelMap(
sourceSize, memoryAllocator,
destinationSize, sourceSize,
destinationSize, destinationSize,
ratio, destinationSize,
scale, ratio,
radius); scale,
radius);
result.Initialize(in sampler); result.Initialize(in sampler);
@ -205,6 +212,7 @@ internal partial class ResizeKernelMap : IDisposable
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ {
double center = ((destRowIndex + .5) * this.ratio) - .5; double center = ((destRowIndex + .5) * this.ratio) - .5;
double scale = this.scale;
// Keep inside bounds. // Keep inside bounds.
int left = (int)TolerantMath.Ceiling(center - this.radius); int left = (int)TolerantMath.Ceiling(center - this.radius);
@ -220,30 +228,25 @@ internal partial class ResizeKernelMap : IDisposable
} }
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
Span<float> kernelValues = this.tempValues.AsSpan(0, kernel.Length);
Span<double> kernelValues = this.tempValues.AsSpan(0, kernel.Length); ref float kernelStart = ref MemoryMarshal.GetReference(kernelValues);
double sum = 0; float sum = 0;
for (int j = left; j <= right; j++) for (int j = left; j <= right; j++)
{ {
double value = sampler.GetValue((float)((j - center) / this.scale)); float value = sampler.GetValue((float)((j - center) / scale));
sum += value; sum += value;
kernelStart = value;
kernelValues[j - left] = value; kernelStart = ref Unsafe.Add(ref kernelStart, 1);
} }
// Normalize, best to do it here rather than in the pixel loop later on. // Normalize, best to do it here rather than in the pixel loop later on.
if (sum > 0) if (sum > 0)
{ {
for (int j = 0; j < kernel.Length; j++) Numerics.Normalize(kernelValues, sum);
{
// weights[w] = weights[w] / sum:
ref double kRef = ref kernelValues[j];
kRef /= sum;
}
} }
kernel.Fill(kernelValues); kernel.FillOrCopyAndExpand(kernelValues);
return kernel; return kernel;
} }

31
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs

@ -16,9 +16,7 @@ public partial class ResizeKernelMapTests
private readonly ReferenceKernel[] kernels; private readonly ReferenceKernel[] kernels;
public ReferenceKernelMap(ReferenceKernel[] kernels) public ReferenceKernelMap(ReferenceKernel[] kernels)
{ => this.kernels = kernels;
this.kernels = kernels;
}
public int DestinationSize => this.kernels.Length; public int DestinationSize => this.kernels.Length;
@ -28,16 +26,16 @@ public partial class ResizeKernelMapTests
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ {
double ratio = (double)sourceSize / destinationSize; double ratio = (double)sourceSize / destinationSize;
double scale = ratio; double scaleD = ratio;
if (scale < 1F) if (scaleD < 1)
{ {
scale = 1F; scaleD = 1;
} }
TolerantMath tolerantMath = TolerantMath.Default; TolerantMath tolerantMath = TolerantMath.Default;
double radius = tolerantMath.Ceiling(scale * sampler.Radius); double radius = tolerantMath.Ceiling(scaleD * sampler.Radius);
List<ReferenceKernel> result = []; List<ReferenceKernel> result = [];
@ -58,15 +56,14 @@ public partial class ResizeKernelMapTests
right = sourceSize - 1; right = sourceSize - 1;
} }
double sum = 0; float sum = 0;
double[] values = new double[right - left + 1]; float[] values = new float[right - left + 1];
for (int j = left; j <= right; j++) for (int j = left; j <= right; j++)
{ {
double weight = sampler.GetValue((float)((j - center) / scale)); float weight = sampler.GetValue((float)((j - center) / scaleD));
sum += weight; sum += weight;
values[j - left] = weight; values[j - left] = weight;
} }
@ -78,16 +75,14 @@ public partial class ResizeKernelMapTests
} }
} }
float[] floatVals = values.Select(v => (float)v).ToArray(); result.Add(new ReferenceKernel(left, values));
result.Add(new ReferenceKernel(left, floatVals));
} }
return new ReferenceKernelMap(result.ToArray()); return new ReferenceKernelMap([.. result]);
} }
} }
internal struct ReferenceKernel internal readonly struct ReferenceKernel
{ {
public ReferenceKernel(int left, float[] values) public ReferenceKernel(int left, float[] values)
{ {
@ -102,8 +97,6 @@ public partial class ResizeKernelMapTests
public int Length => this.Values.Length; public int Length => this.Values.Length;
public static implicit operator ReferenceKernel(ResizeKernel orig) public static implicit operator ReferenceKernel(ResizeKernel orig)
{ => new(orig.StartIndex, orig.Values.ToArray());
return new ReferenceKernel(orig.StartIndex, orig.Values.ToArray());
}
} }
} }

155
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

@ -13,10 +13,7 @@ public partial class ResizeKernelMapTests
{ {
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
public ResizeKernelMapTests(ITestOutputHelper output) public ResizeKernelMapTests(ITestOutputHelper output) => this.Output = output;
{
this.Output = output;
}
/// <summary> /// <summary>
/// resamplerName, srcSize, destSize /// resamplerName, srcSize, destSize
@ -24,71 +21,70 @@ public partial class ResizeKernelMapTests
public static readonly TheoryData<IResampler, int, int> KernelMapData public static readonly TheoryData<IResampler, int, int> KernelMapData
= new() = new()
{ {
{ KnownResamplers.Bicubic, 15, 10 }, { KnownResamplers.Bicubic, 15, 10 },
{ KnownResamplers.Bicubic, 10, 15 }, { KnownResamplers.Bicubic, 10, 15 },
{ KnownResamplers.Bicubic, 20, 20 }, { KnownResamplers.Bicubic, 20, 20 },
{ KnownResamplers.Bicubic, 50, 40 }, { KnownResamplers.Bicubic, 50, 40 },
{ KnownResamplers.Bicubic, 40, 50 }, { KnownResamplers.Bicubic, 40, 50 },
{ KnownResamplers.Bicubic, 500, 200 }, { KnownResamplers.Bicubic, 500, 200 },
{ KnownResamplers.Bicubic, 200, 500 }, { KnownResamplers.Bicubic, 200, 500 },
{ KnownResamplers.Bicubic, 3032, 400 }, { KnownResamplers.Bicubic, 3032, 400 },
{ KnownResamplers.Bicubic, 10, 25 }, { KnownResamplers.Bicubic, 10, 25 },
{ KnownResamplers.Lanczos3, 16, 12 }, { KnownResamplers.Lanczos3, 16, 12 },
{ KnownResamplers.Lanczos3, 12, 16 }, { KnownResamplers.Lanczos3, 12, 16 },
{ KnownResamplers.Lanczos3, 12, 9 }, { KnownResamplers.Lanczos3, 12, 9 },
{ KnownResamplers.Lanczos3, 9, 12 }, { KnownResamplers.Lanczos3, 9, 12 },
{ KnownResamplers.Lanczos3, 6, 8 }, { KnownResamplers.Lanczos3, 6, 8 },
{ KnownResamplers.Lanczos3, 8, 6 }, { KnownResamplers.Lanczos3, 8, 6 },
{ KnownResamplers.Lanczos3, 20, 12 }, { KnownResamplers.Lanczos3, 20, 12 },
{ KnownResamplers.Lanczos3, 5, 25 }, { KnownResamplers.Lanczos3, 5, 25 },
{ KnownResamplers.Lanczos3, 5, 50 }, { KnownResamplers.Lanczos3, 5, 50 },
{ KnownResamplers.Lanczos3, 25, 5 }, { KnownResamplers.Lanczos3, 25, 5 },
{ KnownResamplers.Lanczos3, 50, 5 }, { KnownResamplers.Lanczos3, 50, 5 },
{ KnownResamplers.Lanczos3, 49, 5 }, { KnownResamplers.Lanczos3, 49, 5 },
{ KnownResamplers.Lanczos3, 31, 5 }, { KnownResamplers.Lanczos3, 31, 5 },
{ KnownResamplers.Lanczos8, 500, 200 }, { KnownResamplers.Lanczos8, 500, 200 },
{ KnownResamplers.Lanczos8, 100, 10 }, { KnownResamplers.Lanczos8, 100, 10 },
{ KnownResamplers.Lanczos8, 100, 80 }, { KnownResamplers.Lanczos8, 100, 80 },
{ KnownResamplers.Lanczos8, 10, 100 }, { KnownResamplers.Lanczos8, 10, 100 },
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
{ KnownResamplers.Box, 378, 149 }, { KnownResamplers.Box, 378, 149 },
{ KnownResamplers.Box, 349, 174 }, { KnownResamplers.Box, 349, 174 },
// Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
{ KnownResamplers.Box, 201, 100 }, { KnownResamplers.Box, 201, 100 },
{ KnownResamplers.Box, 199, 99 }, { KnownResamplers.Box, 199, 99 },
{ KnownResamplers.Box, 10, 299 }, { KnownResamplers.Box, 10, 299 },
{ KnownResamplers.Box, 299, 10 }, { KnownResamplers.Box, 299, 10 },
{ KnownResamplers.Box, 301, 300 }, { KnownResamplers.Box, 301, 300 },
{ KnownResamplers.Box, 1180, 480 }, { KnownResamplers.Box, 1180, 480 },
{ KnownResamplers.Lanczos2, 3264, 3032 }, { KnownResamplers.Lanczos2, 3264, 3032 },
{ KnownResamplers.Bicubic, 1280, 2240 }, { KnownResamplers.Bicubic, 1280, 2240 },
{ KnownResamplers.Bicubic, 1920, 1680 }, { KnownResamplers.Bicubic, 1920, 1680 },
{ KnownResamplers.Bicubic, 3072, 2240 }, { KnownResamplers.Bicubic, 3072, 2240 },
{ KnownResamplers.Welch, 300, 2008 }, { KnownResamplers.Welch, 300, 2008 },
// ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
{ KnownResamplers.Bicubic, 10, 50 }, { KnownResamplers.Bicubic, 10, 50 },
{ KnownResamplers.Bicubic, 49, 301 }, { KnownResamplers.Bicubic, 49, 301 },
{ KnownResamplers.Bicubic, 301, 49 }, { KnownResamplers.Bicubic, 301, 49 },
{ KnownResamplers.Bicubic, 1680, 1200 }, { KnownResamplers.Bicubic, 1680, 1200 },
{ KnownResamplers.Box, 13, 299 }, { KnownResamplers.Box, 13, 299 },
{ KnownResamplers.Lanczos5, 3032, 600 }, { KnownResamplers.Lanczos5, 3032, 600 },
// Large number. https://github.com/SixLabors/ImageSharp/issues/1616 // Large number. https://github.com/SixLabors/ImageSharp/issues/1616
{ KnownResamplers.Bicubic, 207773, 51943 } { KnownResamplers.Bicubic, 207773, 51943 }
}; };
public static TheoryData<string, int, int> GeneratedImageResizeData = public static TheoryData<string, int, int> GeneratedImageResizeData = GenerateImageResizeData();
GenerateImageResizeData();
[Theory(Skip = "Only for debugging and development")] [Theory(Skip = "Only for debugging and development")]
[MemberData(nameof(KernelMapData))] [MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap<TResampler>(TResampler resampler, int srcSize, int destSize) public void PrintNonNormalizedKernelMap<TResampler>(TResampler resampler, int srcSize, int destSize)
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ {
ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate<TResampler>(in resampler, destSize, srcSize, false); ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize, false);
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
} }
@ -97,9 +93,7 @@ public partial class ResizeKernelMapTests
[MemberData(nameof(KernelMapData))] [MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect<TResampler>(TResampler resampler, int srcSize, int destSize) public void KernelMapContentIsCorrect<TResampler>(TResampler resampler, int srcSize, int destSize)
where TResampler : struct, IResampler where TResampler : struct, IResampler
{ => this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize);
this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize);
}
// Comprehensive but expensive tests, for ResizeKernelMap. // Comprehensive but expensive tests, for ResizeKernelMap.
// Enabling them can kill you, but sometimes you have to wear the burden! // Enabling them can kill you, but sometimes you have to wear the burden!
@ -124,8 +118,8 @@ public partial class ResizeKernelMapTests
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
#endif #endif
ApproximateFloatComparer comparer = new(1e-6f);
ApproximateFloatComparer comparer = new ApproximateFloatComparer(1e-6f);
for (int i = 0; i < kernelMap.DestinationLength; i++) for (int i = 0; i < kernelMap.DestinationLength; i++)
{ {
ResizeKernel kernel = kernelMap.GetKernel((uint)i); ResizeKernel kernel = kernelMap.GetKernel((uint)i);
@ -139,7 +133,23 @@ public partial class ResizeKernelMapTests
referenceKernel.Left == kernel.StartIndex, referenceKernel.Left == kernel.StartIndex,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}"); $"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.StartIndex}");
float[] expectedValues = referenceKernel.Values; float[] expectedValues = referenceKernel.Values;
Span<float> actualValues = kernel.Values; Span<float> actualValues;
if (ResizeKernel.IsHardwareAccelerated)
{
Assert.Equal(expectedValues.Length, kernel.Values.Length / 4);
actualValues = new float[expectedValues.Length];
for (int j = 0; j < expectedValues.Length; j++)
{
actualValues[j] = kernel.Values[j * 4];
}
}
else
{
actualValues = kernel.Values;
}
Assert.Equal(expectedValues.Length, actualValues.Length); Assert.Equal(expectedValues.Length, actualValues.Length);
@ -199,12 +209,13 @@ public partial class ResizeKernelMapTests
int[] dimensionVals = int[] dimensionVals =
[ [
// Arbitrary, small dimensions: // Arbitrary, small dimensions:
9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301,
// Typical image sizes: // Typical image sizes:
640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, 640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560,
1920, 3032, 2008, 3072, 2304, 3264, 2448 1920, 3032, 2008, 3072, 2304, 3264, 2448
]; ];
IOrderedEnumerable<(int S, int D)> source2Dest = dimensionVals IOrderedEnumerable<(int S, int D)> source2Dest = dimensionVals

Loading…
Cancel
Save