From 5fde593c32d7b123883562b3a721d237e44eac2b Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Sun, 7 Oct 2018 00:55:10 +0200 Subject: [PATCH] Better names: WeightsWindow -> ResizeKernel, WeightsBuffer -> KernelMap --- .../Processors/Transforms/KernelMap.cs | 130 ++++++++++++++++++ .../{WeightsWindow.cs => ResizeKernel.cs} | 24 ++-- .../Processors/Transforms/ResizeProcessor.cs | 109 +++------------ .../Processors/Transforms/WeightsBuffer.cs | 56 -------- .../Processors/Transforms/KernelMapTests.cs | 61 ++++++++ .../Transforms/ResizeProfilingBenchmarks.cs | 38 ----- .../Processors/Transforms/ResizeTests.cs | 1 + 7 files changed, 225 insertions(+), 194 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs rename src/ImageSharp/Processing/Processors/Transforms/{WeightsWindow.cs => ResizeKernel.cs} (86%) delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs new file mode 100644 index 0000000000..277be53fff --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs @@ -0,0 +1,130 @@ +// 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/WeightsWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs similarity index 86% rename from src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs rename to src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs index 56c665c30d..f149523fc7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs @@ -13,9 +13,9 @@ 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 WeightsWindow + internal struct ResizeKernel { /// /// The local left index position @@ -38,14 +38,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly Memory buffer; /// - /// Initializes a new instance of the struct. + /// 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(MethodImplOptions.AggressiveInlining)] - internal WeightsWindow(int index, int left, Buffer2D buffer, int length) + internal ResizeKernel(int index, int left, Buffer2D buffer, int length) { this.flatStartIndex = index * buffer.Width; this.Left = left; @@ -65,20 +65,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } /// - /// Gets the span representing the portion of the that this window covers + /// Gets the span representing the portion of the that this window covers /// /// The [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetWindowSpan() => this.buffer.Span.Slice(this.flatStartIndex, this.Length); + public Span GetSpan() => this.buffer.Span.Slice(this.flatStartIndex, this.Length); /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// 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(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedRowSum(Span rowSpan, int sourceX) + public Vector4 ConvolvePremultipliedRows(Span rowSpan, int sourceX) { ref float horizontalValues = ref this.GetStartReference(); int left = this.Left; @@ -98,14 +98,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms } /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. /// Applies to all input vectors. /// /// The input span of vectors /// The source row position. /// The weighted sum [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeExpandedWeightedRowSum(Span rowSpan, int sourceX) + public Vector4 ConvolvePremultipliedExpandedRows(Span rowSpan, int sourceX) { ref float horizontalValues = ref this.GetStartReference(); int left = this.Left; @@ -126,14 +126,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x', - /// weighted by weight values, pointed by this instance. + /// weighted by weight values, pointed by this instance. /// /// The buffer of input vectors in row first order /// The row position /// The source column position. /// The weighted sum [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedColumnSum(Buffer2D firstPassPixels, int x, int sourceY) + public Vector4 ConvolveColumnsAndUnPremultiply(Buffer2D firstPassPixels, int x, int sourceY) { ref float verticalValues = ref this.GetStartReference(); int left = this.Left; diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs index 9b757f6e1b..52ed222cad 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/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 WeightsBuffer horizontalWeights; - private WeightsBuffer verticalWeights; + private KernelMap horizontalKernelMap; + private KernelMap verticalKernelMap; /// /// Initializes a new instance of the class. @@ -142,75 +142,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// public bool Compand { get; } - /// - /// Computes the weights to apply at each pixel when resizing. - /// - /// The to use for buffer allocations - /// The destination size - /// The source size - /// The - // TODO: Made internal to simplify experimenting with weights data. Make it private when finished figuring out how to optimize all the stuff! - internal WeightsBuffer PrecomputeWeights(MemoryAllocator memoryAllocator, int destinationSize, int sourceSize) - { - float ratio = (float)sourceSize / destinationSize; - float scale = ratio; - - if (scale < 1F) - { - scale = 1F; - } - - IResampler sampler = this.Sampler; - float radius = MathF.Ceiling(scale * sampler.Radius); - var result = new WeightsBuffer(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; - - WeightsWindow ws = result.GetWeightsWindow(i, left, right); - result.Weights[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; - } /// protected override Image CreateDestination(Image source, Rectangle sourceRectangle) @@ -229,15 +160,17 @@ 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.horizontalWeights = this.PrecomputeWeights( - memoryAllocator, + this.horizontalKernelMap = KernelMap.Calculate( + this.Sampler, this.ResizeRectangle.Width, - sourceRectangle.Width); + sourceRectangle.Width, + memoryAllocator); - this.verticalWeights = this.PrecomputeWeights( - memoryAllocator, + this.verticalKernelMap = KernelMap.Calculate( + this.Sampler, this.ResizeRectangle.Height, - sourceRectangle.Height); + sourceRectangle.Height, + memoryAllocator); } } @@ -326,18 +259,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { for (int x = minX; x < maxX; x++) { - WeightsWindow window = this.horizontalWeights.Weights[x - startX]; + ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX]; Unsafe.Add(ref firstPassRow, x) = - window.ComputeExpandedWeightedRowSum(tempRowSpan, sourceX); + window.ConvolvePremultipliedExpandedRows(tempRowSpan, sourceX); } } else { for (int x = minX; x < maxX; x++) { - WeightsWindow window = this.horizontalWeights.Weights[x - startX]; + ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX]; Unsafe.Add(ref firstPassRow, x) = - window.ComputeWeightedRowSum(tempRowSpan, sourceX); + window.ConvolvePremultipliedRows(tempRowSpan, sourceX); } } } @@ -354,7 +287,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int y = rows.Min; y < rows.Max; y++) { // Ensure offsets are normalized for cropping and padding. - WeightsWindow window = this.verticalWeights.Weights[y - startY]; + ResizeKernel window = this.verticalKernelMap.Kernels[y - startY]; ref TPixel targetRow = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); if (this.Compand) @@ -362,7 +295,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { // Destination color components - Vector4 destinationVector = window.ComputeWeightedColumnSum( + Vector4 destinationVector = window.ConvolveColumnsAndUnPremultiply( firstPassPixels, x, sourceY); @@ -377,7 +310,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { // Destination color components - Vector4 destinationVector = window.ComputeWeightedColumnSum( + Vector4 destinationVector = window.ConvolveColumnsAndUnPremultiply( firstPassPixels, x, sourceY); @@ -396,10 +329,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms base.AfterImageApply(source, destination, sourceRectangle); // TODO: An exception in the processing chain can leave these buffers undisposed. We should consider making image processors IDisposable! - this.horizontalWeights?.Dispose(); - this.horizontalWeights = null; - this.verticalWeights?.Dispose(); - this.verticalWeights = null; + this.horizontalKernelMap?.Dispose(); + this.horizontalKernelMap = null; + this.verticalKernelMap?.Dispose(); + this.verticalKernelMap = null; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs deleted file mode 100644 index 6acf38d119..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -using SixLabors.ImageSharp.Memory; -using SixLabors.Memory; - -namespace SixLabors.ImageSharp.Processing.Processors.Transforms -{ - /// - /// Holds the values in an optimized contigous memory region. - /// - internal class WeightsBuffer : IDisposable - { - private readonly Buffer2D dataBuffer; - - /// - /// Initializes a new instance of the class. - /// - /// The to use for allocations. - /// The size of the destination window - /// The radius of the kernel - public WeightsBuffer(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) - { - int width = (int)Math.Ceiling(kernelRadius * 2); - this.dataBuffer = memoryAllocator.Allocate2D(width, destinationSize, AllocationOptions.Clean); - this.Weights = new WeightsWindow[destinationSize]; - } - - /// - /// Gets the calculated values. - /// - public WeightsWindow[] Weights { get; } - - /// - /// Disposes instance releasing it's backing buffer. - /// - public void Dispose() - { - this.dataBuffer.Dispose(); - } - - /// - /// 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 - public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx) - { - return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1); - } - } -} \ 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 new file mode 100644 index 0000000000..b60853a80e --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs @@ -0,0 +1,61 @@ +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] + [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/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs index 4a35d63020..f0062d7146 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs @@ -40,43 +40,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }); } - [Theory] - [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 PrintWeightsData(int srcSize, int destSize, string resamplerName) - { - var size = new Size(srcSize, srcSize); - var resampler = (IResampler) typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); - var proc = new ResizeProcessor(resampler, destSize, destSize, size); - - WeightsBuffer weights = proc.PrecomputeWeights(Configuration.Default.MemoryAllocator, proc.Width, size.Width); - - var bld = new StringBuilder(); - - foreach (WeightsWindow window in weights.Weights) - { - Span span = window.GetWindowSpan(); - 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.PrintWeightsData)); - 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/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index a84adbe1c6..1e0f86dcb8 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; + namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { public class ResizeTests : FileTestBase