Browse Source

introduce TolerantMath

af/merge-core
Anton Firszov 8 years ago
parent
commit
7fcae0351f
  1. 75
      src/ImageSharp/Common/Helpers/TolerantMath.cs
  2. 47
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  3. 3
      tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
  4. 130
      tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
  5. 6
      tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs
  6. 15
      tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs

75
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
{
/// <summary>
/// Implements math operations using tolerant comparison.
/// </summary>
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);
/// <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;
}
}

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

@ -17,8 +17,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary> /// </summary>
internal partial class ResizeKernelMap : IDisposable internal partial class ResizeKernelMap : IDisposable
{ {
private readonly MemoryAllocator memoryAllocator;
private readonly IResampler sampler; private readonly IResampler sampler;
private readonly int sourceLength; private readonly int sourceLength;
@ -35,6 +33,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ResizeKernel[] kernels; private readonly ResizeKernel[] kernels;
// To avoid both GC allocations, and MemoryAllocator ceremony:
private readonly double[] tempValues;
private ResizeKernelMap( private ResizeKernelMap(
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
IResampler sampler, IResampler sampler,
@ -45,7 +46,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
double scale, double scale,
int radius) int radius)
{ {
this.memoryAllocator = memoryAllocator;
this.sampler = sampler; this.sampler = sampler;
this.ratio = ratio; this.ratio = ratio;
this.scale = scale; this.scale = scale;
@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean); this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin(); this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength]; this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[maxWidth];
} }
/// <summary> /// <summary>
@ -113,7 +114,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal); int cornerInterval = (int)Math.Ceiling(firstNonNegativeLeftVal);
// corner case for cornerInteval: // corner case for cornerInteval:
if (firstNonNegativeLeftVal == cornerInterval) // TODO: Implement library-wide utils for tolerant comparison
if (Math.Abs(firstNonNegativeLeftVal - cornerInterval) < 1e-8)
{ {
cornerInterval++; cornerInterval++;
} }
@ -179,33 +181,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
using (IMemoryOwner<double> tempBuffer = this.memoryAllocator.Allocate<double>(kernel.Length)) Span<double> kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length);
{ double sum = 0;
Span<double> kernelValues = tempBuffer.GetSpan();
double sum = 0;
for (int j = left; j <= right; j++) for (int j = left; j <= right; j++)
{ {
double value = this.sampler.GetValue((float)((j - center) / this.scale)); double value = this.sampler.GetValue((float)((j - center) / this.scale));
sum += value; sum += value;
kernelValues[j - left] = value; kernelValues[j - left] = value;
} }
// 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++)
{ {
for (int j = 0; j < kernel.Length; j++) // weights[w] = weights[w] / sum:
{ ref double kRef = ref kernelValues[j];
// weights[w] = weights[w] / sum: kRef /= sum;
ref double kRef = ref kernelValues[j];
kRef /= sum;
}
} }
kernel.Fill(kernelValues);
} }
kernel.Fill(kernelValues);
return kernel; return kernel;
} }

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]

130
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));
}
}
}

6
tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.ReferenceKernelMap.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -40,6 +41,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
for (int i = 0; i < destinationSize; i++) for (int i = 0; i < destinationSize; i++)
{ {
if (i == 21 || i == 64)
{
Debug.Print("lol");
}
double center = ((i + .5) * ratio) - .5; double center = ((i + .5) * ratio) - .5;
// Keep inside bounds. // Keep inside bounds.

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

@ -104,22 +104,23 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[MemberData(nameof(KernelMapData))] [MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) 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 // Comprehensive but expensive tests, for ResizeKernelMap.
// Enabling them can kill your IDE: // 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 #if false
[Theory] [Theory]
[MemberData(nameof(GeneratedImageResizeData))] [MemberData(nameof(GeneratedImageResizeData))]
public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize)
{ {
VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
} }
#endif #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); 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); var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG #if DEBUG
// 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
var comparer = new ApproximateFloatComparer(1e-6f); var comparer = new ApproximateFloatComparer(1e-6f);

Loading…
Cancel
Save