diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs
new file mode 100644
index 0000000000..b9b3b8ea13
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/TolerantMath.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp
+{
+ ///
+ /// Implements math operations using tolerant comparison.
+ ///
+ internal 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);
+
+ ///
+ /// == 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon;
+
+ ///
+ /// > 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsPositive(double a) => a > this.epsilon;
+
+ ///
+ /// < 0
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsNegative(double a) => a < this.negEpsilon;
+
+ ///
+ /// ==
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool AreEqual(double a, double b) => this.IsZero(a - b);
+
+ ///
+ /// >
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsGreater(double a, double b) => a > b + this.epsilon;
+
+ ///
+ /// <
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsLess(double a, double b) => a < b - this.epsilon;
+
+ ///
+ /// >=
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon;
+
+ ///
+ /// <=
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon;
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
index 4cd9928d30..468e0d8447 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
@@ -17,8 +17,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
///
internal partial class ResizeKernelMap : IDisposable
{
- private readonly MemoryAllocator memoryAllocator;
-
private readonly IResampler sampler;
private readonly int sourceLength;
@@ -35,6 +33,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ResizeKernel[] kernels;
+ // To avoid both GC allocations, and MemoryAllocator ceremony:
+ private readonly double[] tempValues;
+
private ResizeKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
@@ -45,7 +46,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
double scale,
int radius)
{
- this.memoryAllocator = memoryAllocator;
this.sampler = sampler;
this.ratio = ratio;
this.scale = scale;
@@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.data = memoryAllocator.Allocate2D(maxWidth, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength];
+ this.tempValues = new double[maxWidth];
}
///
@@ -113,7 +114,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal);
// corner case for cornerInteval:
- if (firstNonNegativeLeftVal == cornerInterval)
+ // TODO: Implement library-wide utils for tolerant comparison
+ if (Math.Abs(firstNonNegativeLeftVal - cornerInterval) < 1e-8)
{
cornerInterval++;
}
@@ -179,33 +181,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
- using (IMemoryOwner tempBuffer = this.memoryAllocator.Allocate(kernel.Length))
- {
- Span kernelValues = tempBuffer.GetSpan();
- double sum = 0;
+ Span 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;
+ for (int j = left; j <= right; j++)
+ {
+ double value = this.sampler.GetValue((float)((j - center) / this.scale));
+ sum += value;
- kernelValues[j - left] = value;
- }
+ kernelValues[j - left] = value;
+ }
- // Normalize, best to do it here rather than in the pixel loop later on.
- if (sum > 0)
+ // 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++)
{
- for (int j = 0; j < kernel.Length; j++)
- {
- // weights[w] = weights[w] / sum:
- ref double kRef = ref kernelValues[j];
- kRef /= sum;
- }
+ // weights[w] = weights[w] / sum:
+ ref double kRef = ref kernelValues[j];
+ kRef /= sum;
}
-
- kernel.Fill(kernelValues);
}
+ kernel.Fill(kernelValues);
+
return kernel;
}
diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
index 75ef611a5c..018fabd982 100644
--- a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
+++ b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
@@ -2,11 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers
{
- using Xunit;
-
public class ImageMathsTests
{
[Theory]
diff --git a/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
new file mode 100644
index 0000000000..d488d6491d
--- /dev/null
+++ b/tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
@@ -0,0 +1,130 @@
+// 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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs
index 9a7052b5a8..31907b06d3 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
@@ -40,6 +41,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
for (int i = 0; i < destinationSize; i++)
{
+ if (i == 21 || i == 64)
+ {
+ Debug.Print("lol");
+ }
+
double center = ((i + .5) * ratio) - .5;
// Keep inside bounds.
diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
index d0ff62a911..dc7a441e99 100644
--- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
@@ -104,22 +104,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
- VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
+ this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
- // Comprehensive but expensive tests, for KernelMap generation
- // Enabling them can kill your IDE:
+ // 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)
{
- VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
+ this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
#endif
- private static void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
+ private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
@@ -127,8 +128,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
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");
+ 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);