Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
/// <summary>
/// Wrapping mode for the border pixels in convolution processing.
/// </summary>
public enum BorderWrappingMode : byte
{
/// <summary>Repeat the border pixel value: aaaaaa|abcdefgh|hhhhhhh</summary>
Repeat = 0,

/// <summary>Take values from the opposite edge: cdefgh|abcdefgh|abcdefg</summary>
Wrap = 1,

/// <summary>Mirror the last few border values: fedcb|abcdefgh|gfedcb</summary>
/// <remarks>Please note this mode does not repeat the very border pixel, as this gives better image quality.</remarks>
Mirror = 2
}
}
130 changes: 92 additions & 38 deletions src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal sealed class KernelSamplingMap : IDisposable
/// <param name="kernel">The convolution kernel.</param>
/// <param name="bounds">The source bounds.</param>
public void BuildSamplingOffsetMap(DenseMatrix<float> kernel, Rectangle bounds)
=> this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds);
=> this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);

/// <summary>
/// Builds a map of the sampling offsets for the kernel clamped by the given bounds.
Expand All @@ -40,6 +40,17 @@ public void BuildSamplingOffsetMap(DenseMatrix<float> kernel, Rectangle bounds)
/// <param name="kernelWidth">The width (number of columns) of the convolution kernel to use.</param>
/// <param name="bounds">The source bounds.</param>
public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds)
=> this.BuildSamplingOffsetMap(kernelHeight, kernelWidth, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);

/// <summary>
/// Builds a map of the sampling offsets for the kernel clamped by the given bounds.
/// </summary>
/// <param name="kernelHeight">The height (number of rows) of the convolution kernel to use.</param>
/// <param name="kernelWidth">The width (number of columns) of the convolution kernel to use.</param>
/// <param name="bounds">The source bounds.</param>
/// <param name="xBorderMode">The wrapping mode on the horizontal borders.</param>
/// <param name="yBorderMode">The wrapping mode on the vertical borders.</param>
public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode)
{
this.yOffsets = this.allocator.Allocate<int>(bounds.Height * kernelHeight);
this.xOffsets = this.allocator.Allocate<int>(bounds.Width * kernelWidth);
Expand All @@ -49,43 +60,8 @@ public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle
int minX = bounds.X;
int maxX = bounds.Right - 1;

int radiusY = kernelHeight >> 1;
int radiusX = kernelWidth >> 1;

// Calculate the y and x sampling offsets clamped to the given rectangle.
// While this isn't a hotpath we still dip into unsafe to avoid the span bounds
// checks as the can potentially be looping over large arrays.
Span<int> ySpan = this.yOffsets.GetSpan();
ref int ySpanBase = ref MemoryMarshal.GetReference(ySpan);
for (int row = 0; row < bounds.Height; row++)
{
int rowBase = row * kernelHeight;
for (int y = 0; y < kernelHeight; y++)
{
Unsafe.Add(ref ySpanBase, rowBase + y) = row + y + minY - radiusY;
}
}

if (kernelHeight > 1)
{
Numerics.Clamp(ySpan, minY, maxY);
}

Span<int> xSpan = this.xOffsets.GetSpan();
ref int xSpanBase = ref MemoryMarshal.GetReference(xSpan);
for (int column = 0; column < bounds.Width; column++)
{
int columnBase = column * kernelWidth;
for (int x = 0; x < kernelWidth; x++)
{
Unsafe.Add(ref xSpanBase, columnBase + x) = column + x + minX - radiusX;
}
}

if (kernelWidth > 1)
{
Numerics.Clamp(xSpan, minX, maxX);
}
this.BuildOffsets(this.yOffsets, bounds.Height, kernelHeight, minY, maxY, yBorderMode);
this.BuildOffsets(this.xOffsets, bounds.Width, kernelWidth, minX, maxX, xBorderMode);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -105,5 +81,83 @@ public void Dispose()
this.isDisposed = true;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BuildOffsets(IMemoryOwner<int> offsets, int boundsSize, int kernelSize, int min, int max, BorderWrappingMode borderMode)
{
int radius = kernelSize >> 1;
Span<int> span = offsets.GetSpan();
ref int spanBase = ref MemoryMarshal.GetReference(span);
for (int chunk = 0; chunk < boundsSize; chunk++)
{
int chunkBase = chunk * kernelSize;
for (int i = 0; i < kernelSize; i++)
{
Unsafe.Add(ref spanBase, chunkBase + i) = chunk + i + min - radius;
}
}

this.CorrectBorder(span, kernelSize, min, max, borderMode);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CorrectBorder(Span<int> span, int kernelSize, int min, int max, BorderWrappingMode borderMode)
{
var affectedSize = (kernelSize >> 1) * kernelSize;
ref int spanBase = ref MemoryMarshal.GetReference(span);
if (affectedSize > 0)
{
switch (borderMode)
{
case BorderWrappingMode.Repeat:
Numerics.Clamp(span.Slice(0, affectedSize), min, max);
Numerics.Clamp(span.Slice(span.Length - affectedSize), min, max);
break;
case BorderWrappingMode.Mirror:
var min2 = min + min;
for (int i = 0; i < affectedSize; i++)
{
var value = span[i];
if (value < min)
{
span[i] = min2 - value;
}
}

var max2 = max + max;
for (int i = span.Length - affectedSize; i < span.Length; i++)
{
var value = span[i];
if (value > max)
{
span[i] = max2 - value;
}
}

break;
case BorderWrappingMode.Wrap:
var diff = max - min + 1;
for (int i = 0; i < affectedSize; i++)
{
var value = span[i];
if (value < min)
{
span[i] = diff + value;
}
}

for (int i = span.Length - affectedSize; i < span.Length; i++)
{
var value = span[i];
if (value > max)
{
span[i] = value - diff;
}
}

break;
}
}
}
}
}
2 changes: 1 addition & 1 deletion tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
// TODO: Scatter test cases into multiple test classes
[Trait("Format", "Jpg")]
[Trait("Format", "Jpg")]
public partial class JpegDecoderTests
{
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector;
Expand Down
22 changes: 11 additions & 11 deletions tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ public partial class Buffer2DTests
{
public class SwapOrCopyContent
{
private readonly TestMemoryAllocator MemoryAllocator = new TestMemoryAllocator();
private readonly TestMemoryAllocator memoryAllocator = new TestMemoryAllocator();

[Fact]
public void SwapOrCopyContent_WhenBothAllocated()
{
using (Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(10, 5, AllocationOptions.Clean))
using (Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(3, 7, AllocationOptions.Clean))
using (Buffer2D<int> a = this.memoryAllocator.Allocate2D<int>(10, 5, AllocationOptions.Clean))
using (Buffer2D<int> b = this.memoryAllocator.Allocate2D<int>(3, 7, AllocationOptions.Clean))
{
a[1, 3] = 666;
b[1, 3] = 444;
Expand All @@ -46,7 +46,7 @@ public void SwapOrCopyContent_WhenDestinationIsOwned_ShouldNotSwapInDisposedSour
using var destData = MemoryGroup<int>.Wrap(new int[100]);
using var dest = new Buffer2D<int>(destData, 10, 10);

using (Buffer2D<int> source = this.MemoryAllocator.Allocate2D<int>(10, 10, AllocationOptions.Clean))
using (Buffer2D<int> source = this.memoryAllocator.Allocate2D<int>(10, 10, AllocationOptions.Clean))
{
source[0, 0] = 1;
dest[0, 0] = 2;
Expand All @@ -68,9 +68,9 @@ public void SwapOrCopyContent_WhenDestinationIsOwned_ShouldNotSwapInDisposedSour
[Fact]
public void WhenBothAreMemoryOwners_ShouldSwap()
{
this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * 50;
using Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(48, 2);
using Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(50, 2);
this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * 50;
using Buffer2D<int> a = this.memoryAllocator.Allocate2D<int>(48, 2);
using Buffer2D<int> b = this.memoryAllocator.Allocate2D<int>(50, 2);

Memory<int> a0 = a.FastMemoryGroup[0];
Memory<int> a1 = a.FastMemoryGroup[1];
Expand All @@ -90,8 +90,8 @@ public void WhenBothAreMemoryOwners_ShouldSwap()
[Fact]
public void WhenBothAreMemoryOwners_ShouldReplaceViews()
{
using Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(100, 1);
using Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(100, 2);
using Buffer2D<int> a = this.memoryAllocator.Allocate2D<int>(100, 1);
using Buffer2D<int> b = this.memoryAllocator.Allocate2D<int>(100, 2);

a.FastMemoryGroup[0].Span[42] = 1;
b.FastMemoryGroup[0].Span[33] = 2;
Expand Down Expand Up @@ -121,7 +121,7 @@ public void WhenDestIsNotAllocated_SameSize_ShouldCopy(bool sourceIsAllocated)
using var destOwner = new TestMemoryManager<Rgba32>(data);
using var dest = new Buffer2D<Rgba32>(MemoryGroup<Rgba32>.Wrap(destOwner.Memory), 21, 1);

using Buffer2D<Rgba32> source = this.MemoryAllocator.Allocate2D<Rgba32>(21, 1);
using Buffer2D<Rgba32> source = this.memoryAllocator.Allocate2D<Rgba32>(21, 1);

source.FastMemoryGroup[0].Span[10] = color;

Expand All @@ -145,7 +145,7 @@ public void WhenDestIsNotMemoryOwner_DifferentSize_Throws(bool sourceIsOwner)
using var destOwner = new TestMemoryManager<Rgba32>(data);
using var dest = new Buffer2D<Rgba32>(MemoryGroup<Rgba32>.Wrap(destOwner.Memory), 21, 1);

using Buffer2D<Rgba32> source = this.MemoryAllocator.Allocate2D<Rgba32>(22, 1);
using Buffer2D<Rgba32> source = this.memoryAllocator.Allocate2D<Rgba32>(22, 1);

source.FastMemoryGroup[0].Span[10] = color;

Expand Down
Loading