diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs index 8720140e1..0c5b05180 100644 --- a/src/ImageSharp/Common/Helpers/ImageMaths.cs +++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs @@ -5,6 +5,7 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.Primitives; namespace SixLabors.ImageSharp @@ -100,7 +101,6 @@ namespace SixLabors.ImageSharp /// /// Determine the Least Common Multiple (LCM) of two numbers. - /// TODO: This method might be useful for building a more compact /// public static int LeastCommonMultiple(int a, int b) { diff --git a/src/ImageSharp/Common/Helpers/TolerantMath.cs b/src/ImageSharp/Common/Helpers/TolerantMath.cs new file mode 100644 index 000000000..5347efcc0 --- /dev/null +++ b/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 +{ + /// + /// Implements basic math operations using tolerant comparison + /// whenever an equality check is needed. + /// + 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); + + /// + /// == 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; + + [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); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/ImageSharp.csproj.DotSettings b/src/ImageSharp/ImageSharp.csproj.DotSettings index cd75f91b7..a7337240a 100644 --- a/src/ImageSharp/ImageSharp.csproj.DotSettings +++ b/src/ImageSharp/ImageSharp.csproj.DotSettings @@ -5,4 +5,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs deleted file mode 100644 index 277be53ff..000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs +++ /dev/null @@ -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 -{ - /// - /// Holds the values in an optimized contigous memory region. - /// - internal class KernelMap : IDisposable - { - private readonly Buffer2D data; - - /// - /// Initializes a new instance of the class. - /// - /// The to use for allocations. - /// The size of the destination window - /// The radius of the kernel - public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) - { - int width = (int)Math.Ceiling(kernelRadius * 2); - this.data = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); - this.Kernels = new ResizeKernel[destinationSize]; - } - - /// - /// Gets the calculated values. - /// - public ResizeKernel[] Kernels { get; } - - /// - /// Disposes instance releasing it's backing buffer. - /// - public void Dispose() - { - this.data.Dispose(); - } - - /// - /// Computes the weights to apply at each pixel when resizing. - /// - /// The - /// The destination size - /// The source size - /// The to use for buffer allocations - /// The - 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; - } - - /// - /// Slices a weights value at the given positions. - /// - /// The index in destination buffer - /// The local left index value - /// The local right index value - /// The weights - private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx) - { - return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1); - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs b/src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs similarity index 100% rename from src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs similarity index 51% rename from src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs index cc3c20453..f349634ac 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs @@ -2,82 +2,62 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; -using SixLabors.Memory; - namespace SixLabors.ImageSharp.Processing.Processors.Transforms { /// - /// Points to a collection of of weights allocated in . + /// Points to a collection of of weights allocated in . /// - internal struct ResizeKernel + internal readonly unsafe struct ResizeKernel { - /// - /// The local left index position - /// - public int Left; - - /// - /// The length of the weights window - /// - public int Length; - - /// - /// The buffer containing the weights values. - /// - private readonly Memory buffer; + private readonly float* bufferPtr; /// /// Initializes a new instance of the struct. /// - /// The destination index in the buffer - /// The local left index - /// The span - /// The length of the window [MethodImpl(InliningOptions.ShortMethod)] - internal ResizeKernel(int index, int left, Buffer2D buffer, int length) + internal ResizeKernel(int left, float* bufferPtr, int length) { - int flatStartIndex = index * buffer.Width; this.Left = left; - this.buffer = buffer.MemorySource.Memory.Slice(flatStartIndex, length); + this.bufferPtr = bufferPtr; this.Length = length; } /// - /// Gets a reference to the first item of the window. + /// Gets the left index for the destination row /// - /// The reference to the first item of the window - [MethodImpl(InliningOptions.ShortMethod)] - public ref float GetStartReference() - { - Span span = this.buffer.Span; - return ref span[0]; - } + public int Left { get; } /// - /// Gets the span representing the portion of the that this window covers + /// Gets the the length of the kernel /// - /// The - [MethodImpl(InliningOptions.ShortMethod)] - public Span GetSpan() => this.buffer.Span; + public int Length { get; } + + /// + /// Gets the span representing the portion of the that this window covers + /// + /// The + /// + public Span Values + { + [MethodImpl(InliningOptions.ShortMethod)] + get => new Span(this.bufferPtr, this.Length); + } /// /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. /// /// The input span of vectors - /// The source row position. /// The weighted sum [MethodImpl(InliningOptions.ShortMethod)] - public Vector4 Convolve(Span rowSpan, int sourceX) + public Vector4 Convolve(Span rowSpan) { - ref float horizontalValues = ref this.GetStartReference(); + ref float horizontalValues = ref Unsafe.AsRef(this.bufferPtr); 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 Vector4 result = Vector4.Zero; @@ -91,5 +71,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return result; } + + /// + /// Copy the contents of altering + /// to the value . + /// + internal ResizeKernel AlterLeftValue(int left) + { + return new ResizeKernel(left, this.bufferPtr, this.Length); + } + + internal void Fill(Span 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]; + } + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs new file mode 100644 index 000000000..4b81aaa64 --- /dev/null +++ b/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 +{ + /// + /// Contains + /// + internal partial class ResizeKernelMap + { + /// + /// Memory-optimized where repeating rows are stored only once. + /// + 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; + } + } + } + } +} \ 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 new file mode 100644 index 000000000..2ab574df2 --- /dev/null +++ b/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 +{ + /// + /// Provides values from an optimized, + /// contiguous memory region. + /// + 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 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(maxWidth, bufferHeight, AllocationOptions.Clean); + this.pinHandle = this.data.Memory.Pin(); + this.kernels = new ResizeKernel[destinationLength]; + this.tempValues = new double[maxWidth]; + } + + /// + /// Gets the length of the destination row/column + /// + public int DestinationLength { get; } + + /// + /// Gets a string of information to help debugging + /// + internal virtual string Info => + $"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}"; + + /// + /// Disposes instance releasing it's backing buffer. + /// + public void Dispose() + { + this.pinHandle.Dispose(); + this.data.Dispose(); + } + + /// + /// Returns a for an index value between 0 and DestinationSize - 1. + /// + [MethodImpl(InliningOptions.ShortMethod)] + public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx]; + + /// + /// Computes the weights to apply at each pixel when resizing. + /// + /// The + /// The destination size + /// The source size + /// The to use for buffer allocations + /// The + 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; + } + } + + /// + /// Builds a for the row (in ) + /// referencing the data at row within , + /// so the data reusable by other data rows. + /// + 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 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; + } + + /// + /// Returns a referencing values of + /// at row . + /// + 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 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); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs similarity index 95% rename from src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs rename to src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 4d4ed06ce..189e21de7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms where TPixel : struct, IPixel { // The following fields are not immutable but are optionally created on demand. - private KernelMap horizontalKernelMap; - private KernelMap verticalKernelMap; + private ResizeKernelMap horizontalKernelMap; + private ResizeKernelMap verticalKernelMap; /// /// Initializes a new instance of the 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. MemoryAllocator memoryAllocator = source.GetMemoryAllocator(); - this.horizontalKernelMap = KernelMap.Calculate( + this.horizontalKernelMap = ResizeKernelMap.Calculate( this.Sampler, this.ResizeRectangle.Width, sourceRectangle.Width, memoryAllocator); - this.verticalKernelMap = KernelMap.Calculate( + this.verticalKernelMap = ResizeKernelMap.Calculate( this.Sampler, this.ResizeRectangle.Height, sourceRectangle.Height, @@ -254,8 +254,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = source.GetPixelRowSpan(y); - Span tempRowSpan = tempRowBuffer.Span; + Span sourceRow = source.GetPixelRowSpan(y).Slice(sourceX); + Span tempRowSpan = tempRowBuffer.Span.Slice(sourceX); PixelOperations.Instance.ToVector4(configuration, sourceRow, tempRowSpan); Vector4Utils.Premultiply(tempRowSpan); @@ -269,9 +269,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms 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) = - 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++) { // 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); for (int x = 0; x < width; x++) { - Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x); + Span firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY); // Destination color components - Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn, sourceY); + Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn); } Vector4Utils.UnPremultiply(tempRowSpan); diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index c0bb25a1b..02d4f80c5 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -63,8 +63,8 @@ namespace SixLabors.ImageSharp.Sandbox46 private static void RunDecodeJpegProfilingTests() { Console.WriteLine("RunDecodeJpegProfilingTests..."); - var benchmarks = new JpegBenchmarks(new ConsoleOutput()); - foreach (object[] data in JpegBenchmarks.DecodeJpegData) + var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput()); + foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData) { string fileName = (string)data[0]; benchmarks.DecodeJpeg(fileName); diff --git a/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs b/tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs index 75ef611a5..018fabd98 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 000000000..6c7a1f275 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs deleted file mode 100644 index 1b4b3cf6a..000000000 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs +++ /dev/null @@ -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 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()); - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs new file mode 100644 index 000000000..7d842c4e1 --- /dev/null +++ b/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 + { + /// + /// Simplified reference implementation for functionality. + /// + 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(); + + 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()); + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs new file mode 100644 index 000000000..08b294913 --- /dev/null +++ b/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; + } + + /// + /// resamplerName, srcSize, destSize + /// + public static readonly TheoryData KernelMapData = new TheoryData + { + { 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 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 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 kernelMap, + Func getDestinationSize, + Func 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 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 GenerateImageResizeData() + { + var result = new TheoryData(); + + 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; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index bec64e4d3..b4aff53e8 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; using Xunit; +// ReSharper disable InconsistentNaming 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); - public static readonly TheoryData AllReSamplers = + public static readonly TheoryData AllResamplers = new TheoryData { { "Bicubic", KnownResamplers.Bicubic }, @@ -40,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }; [Theory] - [WithTestPatternImages(nameof(AllReSamplers), 100, 100, DefaultPixelType, 0.5f)] - [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.5f)] - [WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.3f)] + [WithTestPatternImages(nameof(AllResamplers), 100, 100, DefaultPixelType, 0.5f)] + [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.5f)] + [WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.3f)] public void Resize_WorksWithAllResamplers(TestImageProvider provider, string name, IResampler sampler, float ratio) where TPixel : struct, IPixel { diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs index d1d2ea077..29c51543f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { foreach (string resamplerName in ResamplerNames) { - IResampler sampler = GetResampler(resamplerName); + IResampler sampler = TestUtils.GetResampler(resamplerName); using (Image image = provider.GetImage()) { 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); - } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs similarity index 96% rename from tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs index e06d2da91..65989556d 100644 --- a/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs @@ -15,9 +15,9 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { - public class JpegBenchmarks : MeasureFixture + public class JpegProfilingBenchmarks : MeasureFixture { - public JpegBenchmarks(ITestOutputHelper output) + public JpegProfilingBenchmarks(ITestOutputHelper output) : base(output) { } diff --git a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs similarity index 89% rename from tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs index 306072276..95fe4e48f 100644 --- a/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs @@ -10,9 +10,9 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { - public class LoadResizeSaveBenchmarks : MeasureFixture + public class LoadResizeSaveProfilingBenchmarks : MeasureFixture { - public LoadResizeSaveBenchmarks(ITestOutputHelper output) + public LoadResizeSaveProfilingBenchmarks(ITestOutputHelper output) : base(output) { } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs similarity index 82% rename from tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs rename to tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs index e24458d38..8b9355938 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs @@ -7,17 +7,10 @@ using SixLabors.ImageSharp.Processing; using Xunit; using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms +namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks { public class ResizeProfilingBenchmarks : MeasureFixture { - public const string SkipText = -#if false - null; -#else - "Benchmark, enable manually!"; -#endif - private readonly Configuration configuration = Configuration.CreateDefaultInstance(); public ResizeProfilingBenchmarks(ITestOutputHelper output) @@ -28,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public int ExecutionCount { get; set; } = 50; - [Theory(Skip = SkipText)] + [Theory(Skip = ProfilingSetup.SkipProfilingTests)] [InlineData(100, 100)] [InlineData(2000, 2000)] public void ResizeBicubic(int width, int height) diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index d7755ff7a..5ea0bccf0 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.Primitives; @@ -284,5 +285,17 @@ namespace SixLabors.ImageSharp.Tests } 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); + } } } \ No newline at end of file diff --git a/tests/Images/External b/tests/Images/External index e7e0f1311..5b18d8c95 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4 +Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128