Browse Source

Merge pull request #781 from SixLabors/af/resizekernel-optimizations

Memory-optimized ResizeKernelMap
af/merge-core
Anton Firsov 7 years ago
committed by GitHub
parent
commit
700b07eeba
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  2. 101
      src/ImageSharp/Common/Helpers/TolerantMath.cs
  3. 4
      src/ImageSharp/ImageSharp.csproj.DotSettings
  4. 130
      src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs
  5. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs
  6. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs
  7. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs
  8. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs
  9. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs
  10. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs
  11. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs
  12. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs
  13. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs
  14. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs
  15. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs
  16. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs
  17. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs
  18. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs
  19. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs
  20. 85
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  21. 81
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
  22. 247
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  23. 22
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
  24. 4
      tests/ImageSharp.Sandbox46/Program.cs
  25. 3
      tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
  26. 168
      tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
  27. 61
      tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
  28. 111
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
  29. 241
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
  30. 9
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  31. 14
      tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
  32. 4
      tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
  33. 4
      tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs
  34. 11
      tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs
  35. 13
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
  36. 2
      tests/Images/External

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

@ -5,6 +5,7 @@ using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp namespace SixLabors.ImageSharp
@ -100,7 +101,6 @@ namespace SixLabors.ImageSharp
/// <summary> /// <summary>
/// Determine the Least Common Multiple (LCM) of two numbers. /// Determine the Least Common Multiple (LCM) of two numbers.
/// TODO: This method might be useful for building a more compact <see cref="Processing.Processors.Transforms.KernelMap"/>
/// </summary> /// </summary>
public static int LeastCommonMultiple(int a, int b) public static int LeastCommonMultiple(int a, int b)
{ {

101
src/ImageSharp/Common/Helpers/TolerantMath.cs

@ -0,0 +1,101 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Implements basic math operations using tolerant comparison
/// whenever an equality check is needed.
/// </summary>
internal readonly struct TolerantMath
{
private readonly double epsilon;
private readonly double negEpsilon;
public TolerantMath(double epsilon)
{
DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon));
this.epsilon = epsilon;
this.negEpsilon = -epsilon;
}
public static TolerantMath Default { get; } = new TolerantMath(1e-8);
/// <summary>
/// <paramref name="a"/> == 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon;
/// <summary>
/// <paramref name="a"/> &gt; 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsPositive(double a) => a > this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt; 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsNegative(double a) => a < this.negEpsilon;
/// <summary>
/// <paramref name="a"/> == <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool AreEqual(double a, double b) => this.IsZero(a - b);
/// <summary>
/// <paramref name="a"/> &gt; <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsGreater(double a, double b) => a > b + this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt; <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsLess(double a, double b) => a < b - this.epsilon;
/// <summary>
/// <paramref name="a"/> &gt;= <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt;= <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon;
[MethodImpl(InliningOptions.ShortMethod)]
public double Ceiling(double a)
{
double rem = Math.IEEERemainder(a, 1);
if (this.IsZero(rem))
{
return Math.Round(a);
}
return Math.Ceiling(a);
}
[MethodImpl(InliningOptions.ShortMethod)]
public double Floor(double a)
{
double rem = Math.IEEERemainder(a, 1);
if (this.IsZero(rem))
{
return Math.Round(a);
}
return Math.Floor(a);
}
}
}

4
src/ImageSharp/ImageSharp.csproj.DotSettings

@ -5,4 +5,6 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpackedpixels/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpackedpixels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixelimplementations/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixelimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixeltypes/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixeltypes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cutils/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cutils/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=processing_005Cprocessors_005Ctransforms_005Cresamplers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=processing_005Cprocessors_005Ctransforms_005Cresize/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

130
src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs

@ -1,130 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Holds the <see cref="ResizeKernel"/> values in an optimized contigous memory region.
/// </summary>
internal class KernelMap : IDisposable
{
private readonly Buffer2D<float> data;
/// <summary>
/// Initializes a new instance of the <see cref="KernelMap"/> class.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for allocations.</param>
/// <param name="destinationSize">The size of the destination window</param>
/// <param name="kernelRadius">The radius of the kernel</param>
public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius)
{
int width = (int)Math.Ceiling(kernelRadius * 2);
this.data = memoryAllocator.Allocate2D<float>(width, destinationSize, AllocationOptions.Clean);
this.Kernels = new ResizeKernel[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Kernels"/> values.
/// </summary>
public ResizeKernel[] Kernels { get; }
/// <summary>
/// Disposes <see cref="KernelMap"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.data.Dispose();
}
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="sampler">The <see cref="IResampler"/></param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <returns>The <see cref="KernelMap"/></returns>
public static KernelMap Calculate(
IResampler sampler,
int destinationSize,
int sourceSize,
MemoryAllocator memoryAllocator)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
float radius = MathF.Ceiling(scale * sampler.Radius);
var result = new KernelMap(memoryAllocator, destinationSize, radius);
for (int i = 0; i < destinationSize; i++)
{
float center = ((i + .5F) * ratio) - .5F;
// Keep inside bounds.
int left = (int)MathF.Ceiling(center - radius);
if (left < 0)
{
left = 0;
}
int right = (int)MathF.Floor(center + radius);
if (right > sourceSize - 1)
{
right = sourceSize - 1;
}
float sum = 0;
ResizeKernel ws = result.CreateKernel(i, left, right);
result.Kernels[i] = ws;
ref float weightsBaseRef = ref ws.GetStartReference();
for (int j = left; j <= right; j++)
{
float weight = sampler.GetValue((j - center) / scale);
sum += weight;
// weights[j - left] = weight:
Unsafe.Add(ref weightsBaseRef, j - left) = weight;
}
// Normalize, best to do it here rather than in the pixel loop later on.
if (sum > 0)
{
for (int w = 0; w < ws.Length; w++)
{
// weights[w] = weights[w] / sum:
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w);
wRef /= sum;
}
}
}
return result;
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx)
{
return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1);
}
}
}

0
src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs

85
src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs → src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -2,82 +2,62 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
/// <summary> /// <summary>
/// Points to a collection of of weights allocated in <see cref="KernelMap"/>. /// Points to a collection of of weights allocated in <see cref="ResizeKernelMap"/>.
/// </summary> /// </summary>
internal struct ResizeKernel internal readonly unsafe struct ResizeKernel
{ {
/// <summary> private readonly float* bufferPtr;
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Memory<float> buffer;
/// <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="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel(int index, int left, Buffer2D<float> buffer, int length) internal ResizeKernel(int left, float* bufferPtr, int length)
{ {
int flatStartIndex = index * buffer.Width;
this.Left = left; this.Left = left;
this.buffer = buffer.MemorySource.Memory.Slice(flatStartIndex, length); this.bufferPtr = bufferPtr;
this.Length = length; this.Length = length;
} }
/// <summary> /// <summary>
/// Gets a reference to the first item of the window. /// Gets the left index for the destination row
/// </summary> /// </summary>
/// <returns>The reference to the first item of the window</returns> public int Left { get; }
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetStartReference()
{
Span<float> span = this.buffer.Span;
return ref span[0];
}
/// <summary> /// <summary>
/// Gets the span representing the portion of the <see cref="KernelMap"/> that this window covers /// Gets the the length of the kernel
/// </summary> /// </summary>
/// <returns>The <see cref="Span{T}"/></returns> public int Length { get; }
[MethodImpl(InliningOptions.ShortMethod)]
public Span<float> GetSpan() => this.buffer.Span; /// <summary>
/// Gets the span representing the portion of the <see cref="ResizeKernelMap"/> that this window covers
/// </summary>
/// <value>The <see cref="Span{T}"/>
/// </value>
public Span<float> Values
{
[MethodImpl(InliningOptions.ShortMethod)]
get => new Span<float>(this.bufferPtr, this.Length);
}
/// <summary> /// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance. /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance.
/// </summary> /// </summary>
/// <param name="rowSpan">The input span of vectors</param> /// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns> /// <returns>The weighted sum</returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public Vector4 Convolve(Span<Vector4> rowSpan, int sourceX) public Vector4 Convolve(Span<Vector4> rowSpan)
{ {
ref float horizontalValues = ref this.GetStartReference(); ref float horizontalValues = ref Unsafe.AsRef<float>(this.bufferPtr);
int left = this.Left; int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left + sourceX); ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left);
// Destination color components // Destination color components
Vector4 result = Vector4.Zero; Vector4 result = Vector4.Zero;
@ -91,5 +71,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return result; return result;
} }
/// <summary>
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="Left"/>
/// to the value <paramref name="left"/>.
/// </summary>
internal ResizeKernel AlterLeftValue(int left)
{
return new ResizeKernel(left, this.bufferPtr, this.Length);
}
internal void Fill(Span<double> values)
{
DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!");
for (int i = 0; i < this.Length; i++)
{
this.Values[i] = (float)values[i];
}
}
} }
} }

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

@ -0,0 +1,81 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <content>
/// Contains <see cref="PeriodicKernelMap"/>
/// </content>
internal partial class ResizeKernelMap
{
/// <summary>
/// Memory-optimized <see cref="ResizeKernelMap"/> where repeating rows are stored only once.
/// </summary>
private sealed class PeriodicKernelMap : ResizeKernelMap
{
private readonly int period;
private readonly int cornerInterval;
public PeriodicKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
double ratio,
double scale,
int radius,
int period,
int cornerInterval)
: base(
memoryAllocator,
sampler,
sourceLength,
destinationLength,
(cornerInterval * 2) + period,
ratio,
scale,
radius)
{
this.cornerInterval = cornerInterval;
this.period = period;
}
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}";
protected override void Initialize()
{
// Build top corner data + one period of the mosaic data:
int startOfFirstRepeatedMosaic = this.cornerInterval + this.period;
for (int i = 0; i < startOfFirstRepeatedMosaic; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
}
// Copy the mosaics:
int bottomStartDest = this.DestinationLength - this.cornerInterval;
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];
this.kernels[i] = kernel.AlterLeftValue(left);
}
// Build bottom corner data:
int bottomStartData = this.cornerInterval + this.period;
for (int i = 0; i < this.cornerInterval; i++)
{
ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i);
this.kernels[bottomStartDest + i] = kernel;
}
}
}
}
}

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

@ -0,0 +1,247 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Provides <see cref="ResizeKernel"/> values from an optimized,
/// contiguous memory region.
/// </summary>
internal partial class ResizeKernelMap : IDisposable
{
private static readonly TolerantMath TolerantMath = TolerantMath.Default;
private readonly IResampler sampler;
private readonly int sourceLength;
private readonly double ratio;
private readonly double scale;
private readonly int radius;
private readonly MemoryHandle pinHandle;
private readonly Buffer2D<float> data;
private readonly ResizeKernel[] kernels;
// To avoid both GC allocations, and MemoryAllocator ceremony:
private readonly double[] tempValues;
private ResizeKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
int bufferHeight,
double ratio,
double scale,
int radius)
{
this.sampler = sampler;
this.ratio = ratio;
this.scale = scale;
this.radius = radius;
this.sourceLength = sourceLength;
this.DestinationLength = destinationLength;
int maxWidth = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[maxWidth];
}
/// <summary>
/// Gets the length of the destination row/column
/// </summary>
public int DestinationLength { get; }
/// <summary>
/// Gets a string of information to help debugging
/// </summary>
internal virtual string Info =>
$"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}";
/// <summary>
/// Disposes <see cref="ResizeKernelMap"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.pinHandle.Dispose();
this.data.Dispose();
}
/// <summary>
/// Returns a <see cref="ResizeKernel"/> for an index value between 0 and DestinationSize - 1.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx];
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="sampler">The <see cref="IResampler"/></param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <returns>The <see cref="ResizeKernelMap"/></returns>
public static ResizeKernelMap Calculate(
IResampler sampler,
int destinationSize,
int sourceSize,
MemoryAllocator memoryAllocator)
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;
if (scale < 1)
{
scale = 1;
}
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius);
// 'ratio' is a rational number.
// Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
// This value is determining the length of the periods in repeating kernel map rows.
int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize;
// the center position at i == 0:
double center0 = (ratio - 1) * 0.5;
double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio;
// The number of rows building a "stairway" at the top and the bottom of the kernel map
// corresponding to the corners of the image.
// If we do not normalize the kernel values, these rows also fit the periodic logic,
// however, it's just simpler to calculate them separately.
int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal);
// If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1
// instead of Ceiling:
if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval))
{
cornerInterval++;
}
// If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization.
// If we don't have at least 2 periods, we go with the basic implementation:
bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize;
ResizeKernelMap result = hasAtLeast2Periods
? new PeriodicKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
ratio,
scale,
radius,
period,
cornerInterval)
: new ResizeKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
destinationSize,
ratio,
scale,
radius);
result.Initialize();
return result;
}
protected virtual void Initialize()
{
for (int i = 0; i < this.DestinationLength; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
}
}
/// <summary>
/// Builds a <see cref="ResizeKernel"/> for the row <paramref name="destRowIndex"/> (in <see cref="kernels"/>)
/// referencing the data at row <paramref name="dataRowIndex"/> within <see cref="data"/>,
/// so the data reusable by other data rows.
/// </summary>
private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex)
{
double center = ((destRowIndex + .5) * this.ratio) - .5;
// Keep inside bounds.
int left = (int)TolerantMath.Ceiling(center - this.radius);
if (left < 0)
{
left = 0;
}
int right = (int)TolerantMath.Floor(center + this.radius);
if (right > this.sourceLength - 1)
{
right = this.sourceLength - 1;
}
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
Span<double> kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length);
double sum = 0;
for (int j = left; j <= right; j++)
{
double value = this.sampler.GetValue((float)((j - center) / this.scale));
sum += value;
kernelValues[j - left] = value;
}
// Normalize, best to do it here rather than in the pixel loop later on.
if (sum > 0)
{
for (int j = 0; j < kernel.Length; j++)
{
// weights[w] = weights[w] / sum:
ref double kRef = ref kernelValues[j];
kRef /= sum;
}
}
kernel.Fill(kernelValues);
return kernel;
}
/// <summary>
/// Returns a <see cref="ResizeKernel"/> referencing values of <see cref="data"/>
/// at row <paramref name="dataRowIndex"/>.
/// </summary>
private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right)
{
int length = right - left + 1;
if (length > this.data.Width)
{
throw new InvalidOperationException(
$"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width");
}
Span<float> rowSpan = this.data.GetRowSpan(dataRowIndex);
ref float rowReference = ref MemoryMarshal.GetReference(rowSpan);
float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference);
return new ResizeKernel(left, rowPtr, length);
}
}
}

22
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs

@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
// The following fields are not immutable but are optionally created on demand. // The following fields are not immutable but are optionally created on demand.
private KernelMap horizontalKernelMap; private ResizeKernelMap horizontalKernelMap;
private KernelMap verticalKernelMap; private ResizeKernelMap verticalKernelMap;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="ResizeProcessor{TPixel}"/> class.
@ -165,13 +165,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
// Since all image frame dimensions have to be the same we can calculate this for all frames. // Since all image frame dimensions have to be the same we can calculate this for all frames.
MemoryAllocator memoryAllocator = source.GetMemoryAllocator(); MemoryAllocator memoryAllocator = source.GetMemoryAllocator();
this.horizontalKernelMap = KernelMap.Calculate( this.horizontalKernelMap = ResizeKernelMap.Calculate(
this.Sampler, this.Sampler,
this.ResizeRectangle.Width, this.ResizeRectangle.Width,
sourceRectangle.Width, sourceRectangle.Width,
memoryAllocator); memoryAllocator);
this.verticalKernelMap = KernelMap.Calculate( this.verticalKernelMap = ResizeKernelMap.Calculate(
this.Sampler, this.Sampler,
this.ResizeRectangle.Height, this.ResizeRectangle.Height,
sourceRectangle.Height, sourceRectangle.Height,
@ -254,8 +254,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{ {
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> sourceRow = source.GetPixelRowSpan(y); Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(sourceX);
Span<Vector4> tempRowSpan = tempRowBuffer.Span; Span<Vector4> tempRowSpan = tempRowBuffer.Span.Slice(sourceX);
PixelOperations<TPixel>.Instance.ToVector4(configuration, sourceRow, tempRowSpan); PixelOperations<TPixel>.Instance.ToVector4(configuration, sourceRow, tempRowSpan);
Vector4Utils.Premultiply(tempRowSpan); Vector4Utils.Premultiply(tempRowSpan);
@ -269,9 +269,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int x = minX; x < maxX; x++) for (int x = minX; x < maxX; x++)
{ {
ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX]; ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX);
Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) = Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) =
window.Convolve(tempRowSpan, sourceX); kernel.Convolve(tempRowSpan);
} }
} }
}); });
@ -289,16 +289,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
// Ensure offsets are normalized for cropping and padding. // Ensure offsets are normalized for cropping and padding.
ResizeKernel window = this.verticalKernelMap.Kernels[y - startY]; ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY);
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan); ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan);
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
Span<Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x); Span<Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY);
// Destination color components // Destination color components
Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn, sourceY); Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn);
} }
Vector4Utils.UnPremultiply(tempRowSpan); Vector4Utils.UnPremultiply(tempRowSpan);

4
tests/ImageSharp.Sandbox46/Program.cs

@ -63,8 +63,8 @@ namespace SixLabors.ImageSharp.Sandbox46
private static void RunDecodeJpegProfilingTests() private static void RunDecodeJpegProfilingTests()
{ {
Console.WriteLine("RunDecodeJpegProfilingTests..."); Console.WriteLine("RunDecodeJpegProfilingTests...");
var benchmarks = new JpegBenchmarks(new ConsoleOutput()); var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput());
foreach (object[] data in JpegBenchmarks.DecodeJpegData) foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData)
{ {
string fileName = (string)data[0]; string fileName = (string)data[0];
benchmarks.DecodeJpeg(fileName); benchmarks.DecodeJpeg(fileName);

3
tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs

@ -2,11 +2,10 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers namespace SixLabors.ImageSharp.Tests.Helpers
{ {
using Xunit;
public class ImageMathsTests public class ImageMathsTests
{ {
[Theory] [Theory]

168
tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs

@ -0,0 +1,168 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class TolerantMathTests
{
private readonly TolerantMath tolerantMath = new TolerantMath(0.1);
[Theory]
[InlineData(0)]
[InlineData(0.01)]
[InlineData(-0.05)]
public void IsZero_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsZero(a));
}
[Theory]
[InlineData(0.11)]
[InlineData(-0.101)]
[InlineData(42)]
public void IsZero_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsZero(a));
}
[Theory]
[InlineData(0.11)]
[InlineData(100)]
public void IsPositive_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsPositive(a));
}
[Theory]
[InlineData(0.09)]
[InlineData(-0.1)]
[InlineData(-1000)]
public void IsPositive_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsPositive(a));
}
[Theory]
[InlineData(-0.11)]
[InlineData(-100)]
public void IsNegative_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsNegative(a));
}
[Theory]
[InlineData(-0.09)]
[InlineData(0.1)]
[InlineData(1000)]
public void IsNegative_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsNegative(a));
}
[Theory]
[InlineData(4.2, 4.2)]
[InlineData(4.2, 4.25)]
[InlineData(-Math.PI, -Math.PI + 0.05)]
[InlineData(999999.2, 999999.25)]
public void AreEqual_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.AreEqual(a, b));
}
[Theory]
[InlineData(1, 2)]
[InlineData(-1000000, -1000000.2)]
public void AreEqual_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.AreEqual(a, b));
}
[Theory]
[InlineData(2, 1.8)]
[InlineData(-20, -20.2)]
[InlineData(0.1, -0.1)]
[InlineData(100, 10)]
public void IsGreater_IsLess_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.IsGreater(a, b));
Assert.True(this.tolerantMath.IsLess(b, a));
}
[Theory]
[InlineData(2, 1.95)]
[InlineData(-20, -20.02)]
[InlineData(0.01, -0.01)]
[InlineData(999999, 999999.09)]
public void IsGreater_IsLess_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.IsGreater(a, b));
Assert.False(this.tolerantMath.IsLess(b, a));
}
[Theory]
[InlineData(3, 2)]
[InlineData(3, 2.99)]
[InlineData(2.99, 3)]
[InlineData(-5, -6)]
[InlineData(-5, -5.05)]
[InlineData(-5.05, -5)]
public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b));
Assert.True(this.tolerantMath.IsLessOrEqual(b, a));
}
[Theory]
[InlineData(2, 3)]
[InlineData(2.89, 3)]
[InlineData(-3, -2.89)]
public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b));
Assert.False(this.tolerantMath.IsLessOrEqual(b, a));
}
[Theory]
[InlineData(3.5, 4.0)]
[InlineData(3.89, 4.0)]
[InlineData(4.09, 4.0)]
[InlineData(4.11, 5.0)]
[InlineData(0.11, 1)]
[InlineData(0.05, 0)]
[InlineData(-0.5, 0)]
[InlineData(-0.95, -1)]
[InlineData(-1.05, -1)]
[InlineData(-1.5, -1)]
public void Ceiling(double value, double expected)
{
double actual = this.tolerantMath.Ceiling(value);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(1, 1)]
[InlineData(0.99, 1)]
[InlineData(0.5, 0)]
[InlineData(0.01, 0)]
[InlineData(-0.09, 0)]
[InlineData(-0.11, -1)]
[InlineData(-100.11, -101)]
[InlineData(-100.09, -100)]
public void Floor(double value, double expected)
{
double plz1 = Math.IEEERemainder(1.1, 1);
double plz2 = Math.IEEERemainder(0.9, 1);
double plz3 = Math.IEEERemainder(-1.1, 1);
double plz4 = Math.IEEERemainder(-0.9, 1);
double actual = this.tolerantMath.Floor(value);
Assert.Equal(expected, actual);
}
}
}

61
tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs

@ -1,61 +0,0 @@
using System;
using System.IO;
using System.Text;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class KernelMapTests
{
private ITestOutputHelper Output { get; }
public KernelMapTests(ITestOutputHelper output)
{
this.Output = output;
}
[Theory(Skip = "TODO: Add asserionts")]
[InlineData(500, 200, nameof(KnownResamplers.Bicubic))]
[InlineData(50, 40, nameof(KnownResamplers.Bicubic))]
[InlineData(40, 30, nameof(KnownResamplers.Bicubic))]
[InlineData(500, 200, nameof(KnownResamplers.Lanczos8))]
[InlineData(100, 80, nameof(KnownResamplers.Lanczos8))]
[InlineData(100, 10, nameof(KnownResamplers.Lanczos8))]
[InlineData(10, 100, nameof(KnownResamplers.Lanczos8))]
public void PrintKernelMap(int srcSize, int destSize, string resamplerName)
{
var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null);
var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
var bld = new StringBuilder();
foreach (ResizeKernel window in kernelMap.Kernels)
{
Span<float> span = window.GetSpan();
for (int i = 0; i < window.Length; i++)
{
float value = span[i];
bld.Append($"{value,7:F4}");
bld.Append("| ");
}
bld.AppendLine();
}
string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap));
string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD";
File.WriteAllText(fileName, bld.ToString());
this.Output.WriteLine(bld.ToString());
}
}
}

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

@ -0,0 +1,111 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public partial class ResizeKernelMapTests
{
/// <summary>
/// Simplified reference implementation for <see cref="ResizeKernelMap"/> functionality.
/// </summary>
internal class ReferenceKernelMap
{
private readonly ReferenceKernel[] kernels;
public ReferenceKernelMap(ReferenceKernel[] kernels)
{
this.kernels = kernels;
}
public int DestinationSize => this.kernels.Length;
public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex];
public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true)
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
TolerantMath tolerantMath = TolerantMath.Default;
double radius = tolerantMath.Ceiling(scale * sampler.Radius);
var result = new List<ReferenceKernel>();
for (int i = 0; i < destinationSize; i++)
{
double center = ((i + .5) * ratio) - .5;
// Keep inside bounds.
int left = (int)tolerantMath.Ceiling(center - radius);
if (left < 0)
{
left = 0;
}
int right = (int)tolerantMath.Floor(center + radius);
if (right > sourceSize - 1)
{
right = sourceSize - 1;
}
double sum = 0;
double[] values = new double[right - left + 1];
for (int j = left; j <= right; j++)
{
double weight = sampler.GetValue((float)((j - center) / scale));
sum += weight;
values[j - left] = weight;
}
if (sum > 0 && normalize)
{
for (int w = 0; w < values.Length; w++)
{
values[w] /= sum;
}
}
float[] floatVals = values.Select(v => (float)v).ToArray();
result.Add(new ReferenceKernel(left, floatVals));
}
return new ReferenceKernelMap(result.ToArray());
}
}
internal struct ReferenceKernel
{
public ReferenceKernel(int left, float[] values)
{
this.Left = left;
this.Values = values;
}
public int Left { get; }
public float[] Values { get; }
public int Length => this.Values.Length;
public static implicit operator ReferenceKernel(ResizeKernel orig)
{
return new ReferenceKernel(orig.Left, orig.Values.ToArray());
}
}
}
}

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

@ -0,0 +1,241 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public partial class ResizeKernelMapTests
{
private ITestOutputHelper Output { get; }
public ResizeKernelMapTests(ITestOutputHelper output)
{
this.Output = output;
}
/// <summary>
/// resamplerName, srcSize, destSize
/// </summary>
public static readonly TheoryData<string, int, int> KernelMapData = new TheoryData<string, int, int>
{
{ nameof(KnownResamplers.Bicubic), 15, 10 },
{ nameof(KnownResamplers.Bicubic), 10, 15 },
{ nameof(KnownResamplers.Bicubic), 20, 20 },
{ nameof(KnownResamplers.Bicubic), 50, 40 },
{ nameof(KnownResamplers.Bicubic), 40, 50 },
{ nameof(KnownResamplers.Bicubic), 500, 200 },
{ nameof(KnownResamplers.Bicubic), 200, 500 },
{ nameof(KnownResamplers.Bicubic), 10, 25 },
{ nameof(KnownResamplers.Lanczos3), 16, 12 },
{ nameof(KnownResamplers.Lanczos3), 12, 16 },
{ nameof(KnownResamplers.Lanczos3), 12, 9 },
{ nameof(KnownResamplers.Lanczos3), 9, 12 },
{ nameof(KnownResamplers.Lanczos3), 6, 8 },
{ nameof(KnownResamplers.Lanczos3), 8, 6 },
{ nameof(KnownResamplers.Lanczos3), 20, 12 },
{ nameof(KnownResamplers.Lanczos3), 5, 25 },
{ nameof(KnownResamplers.Lanczos3), 5, 50 },
{ nameof(KnownResamplers.Lanczos3), 25, 5 },
{ nameof(KnownResamplers.Lanczos3), 50, 5 },
{ nameof(KnownResamplers.Lanczos3), 49, 5 },
{ nameof(KnownResamplers.Lanczos3), 31, 5 },
{ nameof(KnownResamplers.Lanczos8), 500, 200 },
{ nameof(KnownResamplers.Lanczos8), 100, 10 },
{ nameof(KnownResamplers.Lanczos8), 100, 80 },
{ nameof(KnownResamplers.Lanczos8), 10, 100 },
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
{ nameof(KnownResamplers.Box), 378, 149 },
{ nameof(KnownResamplers.Box), 349, 174 },
// Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Box), 201, 100 },
{ nameof(KnownResamplers.Box), 199, 99 },
{ nameof(KnownResamplers.Box), 10, 299 },
{ nameof(KnownResamplers.Box), 299, 10 },
{ nameof(KnownResamplers.Box), 301, 300 },
{ nameof(KnownResamplers.Box), 1180, 480 },
{ nameof(KnownResamplers.Lanczos2), 3264, 3032 },
{ nameof(KnownResamplers.Bicubic), 1280, 2240 },
{ nameof(KnownResamplers.Bicubic), 1920, 1680 },
{ nameof(KnownResamplers.Bicubic), 3072, 2240 },
{ nameof(KnownResamplers.Welch), 300, 2008 },
// ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Bicubic), 10, 50 },
{ nameof(KnownResamplers.Bicubic), 49, 301 },
{ nameof(KnownResamplers.Bicubic), 301, 49 },
{ nameof(KnownResamplers.Bicubic), 1680, 1200 },
{ nameof(KnownResamplers.Box), 13, 299 },
{ nameof(KnownResamplers.Lanczos5), 3032, 600 },
};
public static TheoryData<string, int, int> GeneratedImageResizeData =
GenerateImageResizeData();
[Theory(Skip = "Only for debugging and development")]
[MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize)
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false);
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
}
[Theory]
[MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
// Comprehensive but expensive tests, for ResizeKernelMap.
// Enabling them can kill you, but sometimes you have to wear the burden!
// AppVeyor will never follow you to these shadows of Mordor.
#if false
[Theory]
[MemberData(nameof(GeneratedImageResizeData))]
public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize)
{
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
#endif
private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
#endif
var comparer = new ApproximateFloatComparer(1e-6f);
for (int i = 0; i < kernelMap.DestinationLength; i++)
{
ResizeKernel kernel = kernelMap.GetKernel(i);
ReferenceKernel referenceKernel = referenceMap.GetKernel(i);
Assert.True(
referenceKernel.Length == kernel.Length,
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}");
Assert.True(
referenceKernel.Left == kernel.Left,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}");
float[] expectedValues = referenceKernel.Values;
Span<float> actualValues = kernel.Values;
Assert.Equal(expectedValues.Length, actualValues.Length);
for (int x = 0; x < expectedValues.Length; x++)
{
Assert.True(
comparer.Equals(expectedValues[x], actualValues[x]),
$"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})");
}
}
}
private static string PrintKernelMap(ResizeKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i));
private static string PrintKernelMap(ReferenceKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i));
private static string PrintKernelMap<TKernelMap>(
TKernelMap kernelMap,
Func<TKernelMap, int> getDestinationSize,
Func<TKernelMap, int, ReferenceKernel> getKernel)
{
var bld = new StringBuilder();
if (kernelMap is ResizeKernelMap actualMap)
{
bld.AppendLine(actualMap.Info);
}
int destinationSize = getDestinationSize(kernelMap);
for (int i = 0; i < destinationSize; i++)
{
ReferenceKernel kernel = getKernel(kernelMap, i);
bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || ");
Span<float> span = kernel.Values;
for (int j = 0; j < kernel.Length; j++)
{
float value = span[j];
bld.Append($"{value,8:F5}");
bld.Append(" | ");
}
bld.AppendLine();
}
return bld.ToString();
}
private static TheoryData<string, int, int> GenerateImageResizeData()
{
var result = new TheoryData<string, int, int>();
string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static)
.Select(p => p.Name)
.Where(name => name != nameof(KnownResamplers.NearestNeighbor))
.ToArray();
int[] dimensionVals =
{
// Arbitrary, small dimensions:
9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301,
// Typical image sizes:
640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560,
1920, 3032, 2008, 3072, 2304, 3264, 2448
};
IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals
.SelectMany(s => dimensionVals.Select(d => (s, d)))
.OrderBy(x => x.s + x.d);
foreach (string resampler in resamplerNames)
{
foreach ((int s, int d) x in source2Dest)
{
result.Add(resampler, x.s, x.d);
}
}
return result;
}
}
}

9
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives; using SixLabors.Primitives;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
@ -20,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F); private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
public static readonly TheoryData<string, IResampler> AllReSamplers = public static readonly TheoryData<string, IResampler> AllResamplers =
new TheoryData<string, IResampler> new TheoryData<string, IResampler>
{ {
{ "Bicubic", KnownResamplers.Bicubic }, { "Bicubic", KnownResamplers.Bicubic },
@ -40,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}; };
[Theory] [Theory]
[WithTestPatternImages(nameof(AllReSamplers), 100, 100, DefaultPixelType, 0.5f)] [WithTestPatternImages(nameof(AllResamplers), 100, 100, DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.5f)] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.3f)] [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.3f)]
public void Resize_WorksWithAllResamplers<TPixel>(TestImageProvider<TPixel> provider, string name, IResampler sampler, float ratio) public void Resize_WorksWithAllResamplers<TPixel>(TestImageProvider<TPixel> provider, string name, IResampler sampler, float ratio)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {

14
tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs

@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ {
foreach (string resamplerName in ResamplerNames) foreach (string resamplerName in ResamplerNames)
{ {
IResampler sampler = GetResampler(resamplerName); IResampler sampler = TestUtils.GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
image.Mutate(i => i.Skew(x, y, sampler)); image.Mutate(i => i.Skew(x, y, sampler));
@ -68,17 +68,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
} }
} }
} }
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property is null)
{
throw new Exception($"No resampler named '{name}");
}
return (IResampler)property.GetValue(null);
}
} }
} }

4
tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs

@ -15,9 +15,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{ {
public class JpegBenchmarks : MeasureFixture public class JpegProfilingBenchmarks : MeasureFixture
{ {
public JpegBenchmarks(ITestOutputHelper output) public JpegProfilingBenchmarks(ITestOutputHelper output)
: base(output) : base(output)
{ {
} }

4
tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs

@ -10,9 +10,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{ {
public class LoadResizeSaveBenchmarks : MeasureFixture public class LoadResizeSaveProfilingBenchmarks : MeasureFixture
{ {
public LoadResizeSaveBenchmarks(ITestOutputHelper output) public LoadResizeSaveProfilingBenchmarks(ITestOutputHelper output)
: base(output) : base(output)
{ {
} }

11
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs

@ -7,17 +7,10 @@ using SixLabors.ImageSharp.Processing;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{ {
public class ResizeProfilingBenchmarks : MeasureFixture public class ResizeProfilingBenchmarks : MeasureFixture
{ {
public const string SkipText =
#if false
null;
#else
"Benchmark, enable manually!";
#endif
private readonly Configuration configuration = Configuration.CreateDefaultInstance(); private readonly Configuration configuration = Configuration.CreateDefaultInstance();
public ResizeProfilingBenchmarks(ITestOutputHelper output) public ResizeProfilingBenchmarks(ITestOutputHelper output)
@ -28,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public int ExecutionCount { get; set; } = 50; public int ExecutionCount { get; set; } = 50;
[Theory(Skip = SkipText)] [Theory(Skip = ProfilingSetup.SkipProfilingTests)]
[InlineData(100, 100)] [InlineData(100, 100)]
[InlineData(2000, 2000)] [InlineData(2000, 2000)]
public void ResizeBicubic(int width, int height) public void ResizeBicubic(int width, int height)

13
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives; using SixLabors.Primitives;
@ -284,5 +285,17 @@ namespace SixLabors.ImageSharp.Tests
} }
public static string AsInvariantString(this FormattableString formattable) => System.FormattableString.Invariant(formattable); public static string AsInvariantString(this FormattableString formattable) => System.FormattableString.Invariant(formattable);
public static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property is null)
{
throw new Exception($"No resampler named '{name}");
}
return (IResampler)property.GetValue(null);
}
} }
} }

2
tests/Images/External

@ -1 +1 @@
Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4 Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128
Loading…
Cancel
Save