From 199a9e24311282cf6978b1f5ec6c326fea05fc6f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 13 Mar 2022 10:05:16 +0100 Subject: [PATCH] Implement border wrapping modes --- .../Convolution/BorderWrappingMode.cs | 21 ++ .../Convolution/KernelSamplingMap.cs | 126 +++++-- .../Convolution/KernelSamplingMapTest.cs | 324 ++++++++++++++++++ 3 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs create mode 100644 tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs 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); + } + } +}