Browse Source

Better names: WeightsWindow -> ResizeKernel, WeightsBuffer -> KernelMap

af/merge-core
Anton Firszov 8 years ago
parent
commit
5fde593c32
  1. 130
      src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs
  2. 24
      src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs
  3. 109
      src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
  4. 56
      src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs
  5. 61
      tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
  6. 38
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs
  7. 1
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

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

24
src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs → src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs

@ -13,9 +13,9 @@ using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="WeightsBuffer"/>.
/// Points to a collection of of weights allocated in <see cref="KernelMap"/>.
/// </summary>
internal struct WeightsWindow
internal struct ResizeKernel
{
/// <summary>
/// The local left index position
@ -38,14 +38,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly Memory<float> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsWindow"/> struct.
/// Initializes a new instance of the <see cref="ResizeKernel"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal WeightsWindow(int index, int left, Buffer2D<float> buffer, int length)
internal ResizeKernel(int index, int left, Buffer2D<float> buffer, int length)
{
this.flatStartIndex = index * buffer.Width;
this.Left = left;
@ -65,20 +65,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
/// <summary>
/// Gets the span representing the portion of the <see cref="WeightsBuffer"/> that this window covers
/// Gets the span representing the portion of the <see cref="KernelMap"/> that this window covers
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<float> GetWindowSpan() => this.buffer.Span.Slice(this.flatStartIndex, this.Length);
public Span<float> GetSpan() => this.buffer.Span.Slice(this.flatStartIndex, this.Length);
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
public Vector4 ConvolvePremultipliedRows(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
@ -98,14 +98,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance.
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(Span<Vector4> rowSpan, int sourceX)
public Vector4 ConvolvePremultipliedExpandedRows(Span<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.GetStartReference();
int left = this.Left;
@ -126,14 +126,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> firstPassPixels, int x, int sourceY)
public Vector4 ConvolveColumnsAndUnPremultiply(Buffer2D<Vector4> firstPassPixels, int x, int sourceY)
{
ref float verticalValues = ref this.GetStartReference();
int left = this.Left;

109
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs

@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
where TPixel : struct, IPixel<TPixel>
{
// The following fields are not immutable but are optionally created on demand.
private WeightsBuffer horizontalWeights;
private WeightsBuffer verticalWeights;
private KernelMap horizontalKernelMap;
private KernelMap verticalKernelMap;
/// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor{TPixel}"/> class.
@ -142,75 +142,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary>
public bool Compand { get; }
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <returns>The <see cref="WeightsBuffer"/></returns>
// 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;
}
/// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> 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;
}
}
}

56
src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs

@ -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
{
/// <summary>
/// Holds the <see cref="WeightsWindow"/> values in an optimized contigous memory region.
/// </summary>
internal class WeightsBuffer : IDisposable
{
private readonly Buffer2D<float> dataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsBuffer"/> class.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for allocations.</param>
/// <param name="destinationSize">The size of the destination window</param>
/// <param name="kernelRadius">The radius of the kernel</param>
public WeightsBuffer(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius)
{
int width = (int)Math.Ceiling(kernelRadius * 2);
this.dataBuffer = memoryAllocator.Allocate2D<float>(width, destinationSize, AllocationOptions.Clean);
this.Weights = new WeightsWindow[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Weights"/> values.
/// </summary>
public WeightsWindow[] Weights { get; }
/// <summary>
/// Disposes <see cref="WeightsBuffer"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.dataBuffer.Dispose();
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx)
{
return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1);
}
}
}

61
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<float> span = window.GetSpan();
for (int i = 0; i < window.Length; i++)
{
float value = span[i];
bld.Append($"{value,7:F4}");
bld.Append("| ");
}
bld.AppendLine();
}
string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap));
string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD";
File.WriteAllText(fileName, bld.ToString());
this.Output.WriteLine(bld.ToString());
}
}
}

38
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<Rgba32>(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<float> 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());
}
}
}

1
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

Loading…
Cancel
Save