diff --git a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs
new file mode 100644
index 0000000000..0dd5e59439
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Processing.Processors.Convolution
+{
+ ///
+ /// Wrapping mode for the border pixels in convolution processing.
+ ///
+ public enum BorderWrappingMode : byte
+ {
+ /// Repeat the border pixel value: aaaaaa|abcdefgh|hhhhhhh
+ Repeat = 0,
+
+ /// Take values from the opposite edge: cdefgh|abcdefgh|abcdefg
+ Wrap = 1,
+
+ /// Mirror the last few border values: fedcb|abcdefgh|gfedcb
+ /// Please note this mode doe not repeat the very border pixel, as this gives better image quality.
+ Mirror = 2
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs
index 904b599f7c..5c0a25befa 100644
--- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs
+++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs
@@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The convolution kernel.
/// The source bounds.
public void BuildSamplingOffsetMap(DenseMatrix kernel, Rectangle bounds)
- => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds);
+ => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);
///
/// Builds a map of the sampling offsets for the kernel clamped by the given bounds.
@@ -40,6 +40,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The width (number of columns) of the convolution kernel to use.
/// The source bounds.
public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds)
+ => this.BuildSamplingOffsetMap(kernelHeight, kernelWidth, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat);
+
+ ///
+ /// Builds a map of the sampling offsets for the kernel clamped by the given bounds.
+ ///
+ /// The height (number of rows) of the convolution kernel to use.
+ /// The width (number of columns) of the convolution kernel to use.
+ /// The source bounds.
+ /// The wrapping mode on the horizontal borders.
+ /// The wrapping mode on the vertical borders.
+ public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode)
{
this.yOffsets = this.allocator.Allocate(bounds.Height * kernelHeight);
this.xOffsets = this.allocator.Allocate(bounds.Width * kernelWidth);
@@ -49,43 +60,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
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 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 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)]
@@ -105,5 +81,79 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
this.isDisposed = true;
}
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void BuildOffsets(IMemoryOwner offsets, int boundsSize, int kernelSize, int min, int max, BorderWrappingMode borderMode)
+ {
+ int radius = kernelSize >> 1;
+ Span 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 span, int kernelSize, int min, int max, BorderWrappingMode borderMode)
+ {
+ var affectedSize = (kernelSize >> 1) * kernelSize;
+ 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:
+ for (int i = 0; i < affectedSize; i++)
+ {
+ var value = span[i];
+ if (value < min)
+ {
+ span[i] = min - value + min;
+ }
+ }
+
+ for (int i = span.Length - affectedSize; i < span.Length; i++)
+ {
+ var value = span[i];
+ if (value > max)
+ {
+ span[i] = max - value + max;
+ }
+ }
+
+ break;
+ case BorderWrappingMode.Wrap:
+ for (int i = 0; i < affectedSize; i++)
+ {
+ var value = span[i];
+ if (value < min)
+ {
+ span[i] = max - min + value + 1;
+ }
+ }
+
+ for (int i = span.Length - affectedSize; i < span.Length; i++)
+ {
+ var value = span[i];
+ if (value > max)
+ {
+ span[i] = min + value - max - 1;
+ }
+ }
+
+ break;
+ }
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs
new file mode 100644
index 0000000000..68af58b596
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs
@@ -0,0 +1,324 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Processing.Processors.Convolution;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Processing.Convolution
+{
+ [Trait("Category", "Processors")]
+ public class KernelSamplingMapTest
+ {
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7RepeatBorder()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Repeat;
+ int[] expected =
+ {
+ 0, 0, 0, 1, 2,
+ 0, 0, 1, 2, 3,
+ 0, 1, 2, 3, 4,
+ 1, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 6,
+ 4, 5, 6, 6, 6,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7MirrorBorder()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Mirror;
+ int[] expected =
+ {
+ 2, 1, 0, 1, 2,
+ 1, 0, 1, 2, 3,
+ 0, 1, 2, 3, 4,
+ 1, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 5,
+ 4, 5, 6, 5, 4,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7WrapBorder()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Wrap;
+ int[] expected =
+ {
+ 5, 6, 0, 1, 2,
+ 6, 0, 1, 2, 3,
+ 0, 1, 2, 3, 4,
+ 1, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 0,
+ 4, 5, 6, 0, 1,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image9x9MirrorBorder()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(1, 1, 9, 9);
+ var mode = BorderWrappingMode.Mirror;
+ int[] expected =
+ {
+ 3, 2, 1, 2, 3,
+ 2, 1, 2, 3, 4,
+ 1, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 7,
+ 4, 5, 6, 7, 8,
+ 5, 6, 7, 8, 9,
+ 6, 7, 8, 9, 8,
+ 7, 8, 9, 8, 7,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image9x9WrapBorder()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(1, 1, 9, 9);
+ var mode = BorderWrappingMode.Wrap;
+ int[] expected =
+ {
+ 8, 9, 1, 2, 3,
+ 9, 1, 2, 3, 4,
+ 1, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 7,
+ 4, 5, 6, 7, 8,
+ 5, 6, 7, 8, 9,
+ 6, 7, 8, 9, 1,
+ 7, 8, 9, 1, 2,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7RepeatBorderTile()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Repeat;
+ int[] expected =
+ {
+ 2, 2, 2, 3, 4,
+ 2, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 7,
+ 4, 5, 6, 7, 8,
+ 5, 6, 7, 8, 8,
+ 6, 7, 8, 8, 8,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7MirrorBorderTile()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Mirror;
+ int[] expected =
+ {
+ 4, 3, 2, 3, 4,
+ 3, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 7,
+ 4, 5, 6, 7, 8,
+ 5, 6, 7, 8, 7,
+ 6, 7, 8, 7, 6,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel5Image7x7WrapBorderTile()
+ {
+ var kernelSize = new Size(5, 5);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Wrap;
+ int[] expected =
+ {
+ 7, 8, 2, 3, 4,
+ 8, 2, 3, 4, 5,
+ 2, 3, 4, 5, 6,
+ 3, 4, 5, 6, 7,
+ 4, 5, 6, 7, 8,
+ 5, 6, 7, 8, 2,
+ 6, 7, 8, 2, 3,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x7RepeatBorder()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Repeat;
+ int[] expected =
+ {
+ 0, 0, 1,
+ 0, 1, 2,
+ 1, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 6,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x7MirrorBorder()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Mirror;
+ int[] expected =
+ {
+ 1, 0, 1,
+ 0, 1, 2,
+ 1, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 5,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x7WrapBorder()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(0, 0, 7, 7);
+ var mode = BorderWrappingMode.Wrap;
+ int[] expected =
+ {
+ 6, 0, 1,
+ 0, 1, 2,
+ 1, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 0,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x7RepeatBorderTile()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Repeat;
+ int[] expected =
+ {
+ 2, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 7,
+ 6, 7, 8,
+ 7, 8, 8,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7MirrorBorderTile()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Mirror;
+ int[] expected =
+ {
+ 3, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 7,
+ 6, 7, 8,
+ 7, 8, 7,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x7WrapBorderTile()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(2, 2, 7, 7);
+ var mode = BorderWrappingMode.Wrap;
+ int[] expected =
+ {
+ 8, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 7,
+ 6, 7, 8,
+ 7, 8, 2,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected);
+ }
+
+ [Fact]
+ public void KernalSamplingMap_Kernel3Image7x5WrapBorderTile()
+ {
+ var kernelSize = new Size(3, 3);
+ var bounds = new Rectangle(2, 2, 7, 5);
+ var mode = BorderWrappingMode.Wrap;
+ int[] xExpected =
+ {
+ 8, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 7,
+ 6, 7, 8,
+ 7, 8, 2,
+ };
+ int[] yExpected =
+ {
+ 6, 2, 3,
+ 2, 3, 4,
+ 3, 4, 5,
+ 4, 5, 6,
+ 5, 6, 2,
+ };
+ this.AssertOffsets(kernelSize, bounds, mode, mode, xExpected, yExpected);
+ }
+
+ private void AssertOffsets(Size kernelSize, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode, int[] xExpected, int[] yExpected)
+ {
+ // Arrange
+ var map = new KernelSamplingMap(Configuration.Default.MemoryAllocator);
+
+ // Act
+ map.BuildSamplingOffsetMap(kernelSize.Height, kernelSize.Width, bounds, xBorderMode, yBorderMode);
+
+ // Assert
+ var xOffsets = map.GetColumnOffsetSpan().ToArray();
+ Assert.Equal(xExpected, xOffsets);
+ var yOffsets = map.GetRowOffsetSpan().ToArray();
+ Assert.Equal(yExpected, yOffsets);
+ }
+ }
+}