From 4af75b3e61bb2cc7a805419437a7b64248726d7d Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 30 Aug 2022 21:25:01 +0200 Subject: [PATCH 01/10] Median Blur --- .../Convolution/MedianBlurExtensions.cs | 41 +++++ .../Convolution/MedianBlurProcessor.cs | 53 +++++++ .../MedianBlurProcessor{TPixel}.cs | 147 ++++++++++++++++++ .../Convolution/MedianRowOperation{TPixel}.cs | 140 +++++++++++++++++ .../Processing/Convolution/MedianBlurTest.cs | 34 ++++ .../Processors/Convolution/MedianBlurTest.cs | 18 +++ .../InBox_Rgba32_CalliphoraPartial_3.png | 3 + .../InBox_Rgba32_CalliphoraPartial_5.png | 3 + .../MedianBlurTest/InBox_Rgba32_Car_3.png | 3 + .../MedianBlurTest/InBox_Rgba32_Car_5.png | 3 + .../MedianBlurTest/InBox_Rgba32_blur_3.png | 3 + .../MedianBlurTest/InBox_Rgba32_blur_5.png | 3 + ...OnFullImage_Rgba32_CalliphoraPartial_3.png | 3 + ...OnFullImage_Rgba32_CalliphoraPartial_5.png | 3 + .../OnFullImage_Rgba32_Car_3.png | 3 + .../OnFullImage_Rgba32_Car_5.png | 3 + .../OnFullImage_Rgba32_blur_3.png | 3 + .../OnFullImage_Rgba32_blur_5.png | 3 + 18 files changed, 469 insertions(+) create mode 100644 src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs create mode 100644 src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs create mode 100644 src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs create mode 100644 tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs create mode 100644 tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_5.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_5.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_5.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_5.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_5.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_3.png create mode 100644 tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_5.png diff --git a/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs new file mode 100644 index 000000000..a80a11900 --- /dev/null +++ b/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Processing +{ + /// + /// Defines extensions that allow the applying of the median blur on an + /// using Mutate/Clone. + /// + public static class MedianBlurExtensions + { + /// + /// Applies a median blur on the image. + /// + /// The image this method extends. + /// The radius of the area to find the median for. + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha) + => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha)); + + /// + /// Applies a median blur on the image. + /// + /// The image this method extends. + /// The radius of the area to find the median for. + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha, Rectangle rectangle) + => source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs new file mode 100644 index 000000000..a3819f5e9 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Applies an median filter. + /// + public sealed class MedianBlurProcessor : IImageProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to filter over. + /// + /// + /// Whether the filter is applied to alpha as well as the color channels. + /// + public MedianBlurProcessor(int radius, bool preserveAlpha) + { + this.Radius = radius; + this.PreserveAlpha = preserveAlpha; + } + + /// + /// Gets the size of the area to find the median of. + /// + public int Radius { get; } + + /// + /// Gets a value indicating whether the filter is applied to alpha as well as the color channels. + /// + public bool PreserveAlpha { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new MedianBlurProcessor(configuration, this, source, sourceRectangle); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs new file mode 100644 index 000000000..10fd5a5d6 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Applies an median filter. + /// + internal sealed class MedianBlurProcessor : ImageProcessor + where TPixel : unmanaged, IPixel + { + private readonly MedianBlurProcessor definition; + + public MedianBlurProcessor(Configuration configuration, MedianBlurProcessor definition, Image source, Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + } + + protected override void OnFrameApply(ImageFrame source) + { + int kernelSize = (2 * this.definition.Radius) + 1; + + MemoryAllocator allocator = this.Configuration.MemoryAllocator; + using Buffer2D targetPixels = allocator.Allocate2D(source.Width, source.Height); + + source.CopyTo(targetPixels); + + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + + // We use a rectangle with width set to 2 * kernelSize^2 + width, to allocate a buffer big enough + // for kernel source and target bulk pixel conversion. + var operationBounds = new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + interest.Width, interest.Height); + + using var map = new KernelSamplingMap(this.Configuration.MemoryAllocator); + map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); + + var operation = new MedianRowOperation( + interest, + targetPixels, + source.PixelBuffer, + map, + kernelSize, + this.Configuration, + this.definition.PreserveAlpha); + + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + operationBounds, + in operation); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + + private void ProcessSingleRow(PixelAccessor access, KernelSamplingMap map, int y, Span dest) + { + int kernelSize = (2 * this.definition.Radius) + 1; + int kernelCount = kernelSize * kernelSize; + using var vectorsBuffer = this.Configuration.MemoryAllocator.Allocate(kernelCount); + var vectorsSpan = vectorsBuffer.Memory.Span; + using var rowBuffer = this.Configuration.MemoryAllocator.Allocate(dest.Length); + var rowSpan = rowBuffer.Memory.Span; + using var componentsBuffer = this.Configuration.MemoryAllocator.Allocate(kernelCount << 2); + var componentsSpan = componentsBuffer.Memory.Span; + var xs = componentsSpan.Slice(0, kernelCount); + var ys = componentsSpan.Slice(kernelCount, kernelCount); + var zs = componentsSpan.Slice(kernelCount << 1, kernelCount); + var ws = componentsSpan.Slice(kernelCount * 3, kernelCount); + var xOffsets = map.GetColumnOffsetSpan(); + var yOffsets = map.GetRowOffsetSpan(); + var baseYOffsetIndex = y * kernelSize; + for (var x = 0; x < access.Width; x++) + { + var baseXOffsetIndex = x * kernelSize; + var index = 0; + for (var w = 0; w < kernelSize; w++) + { + var j = yOffsets[baseYOffsetIndex + w]; + var row = access.GetRowSpan(j); + for (var z = 0; z < kernelSize; z++) + { + var k = xOffsets[baseXOffsetIndex + z]; + var pixel = row[k]; + vectorsSpan[index + z] = pixel.ToVector4(); + } + + index += kernelSize; + } + + rowSpan[x] = this.FindMedian4(vectorsSpan, xs, ys, zs, ws, kernelCount); + } + + PixelOperations.Instance.FromVector4Destructive(this.Configuration, rowSpan, dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian3(Span span, Span xs, Span ys, Span zs, Span ws, int stride) + { + Vector4 found = new Vector4(0f, 0f, 0f, 0f); + int halfLength = (span.Length + 1) >> 1; + + // Find median of X component. + for (int i = 0; i < xs.Length; i++) + { + xs[i] = span[i].X; + ys[i] = span[i].Y; + zs[i] = span[i].Z; + } + + xs.Sort(); + ys.Sort(); + zs.Sort(); + + return new Vector4(xs[halfLength], ys[halfLength], zs[halfLength], span[halfLength].W); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian4(Span span, Span xs, Span ys, Span zs, Span ws, int stride) + { + Vector4 found = new Vector4(0f, 0f, 0f, 0f); + int halfLength = (span.Length + 1) >> 1; + + // Find median of X component. + for (int i = 0; i < xs.Length; i++) + { + xs[i] = span[i].X; + ys[i] = span[i].Y; + zs[i] = span[i].Z; + ws[i] = span[i].W; + } + + xs.Sort(); + ys.Sort(); + zs.Sort(); + ws.Sort(); + + return new Vector4(xs[halfLength], ys[halfLength], zs[halfLength], ws[halfLength]); + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs new file mode 100644 index 000000000..0678652ec --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Applies an median filter. + /// + internal readonly struct MedianRowOperation : IRowOperation + where TPixel : unmanaged, IPixel + { + private readonly int yChannelStart; + private readonly int zChannelStart; + private readonly int wChannelStart; + private readonly Configuration configuration; + private readonly Rectangle bounds; + private readonly Buffer2D targetPixels; + private readonly Buffer2D sourcePixels; + private readonly KernelSamplingMap map; + private readonly int kernelSize; + private readonly bool preserveAlpha; + + public MedianRowOperation(Rectangle bounds, Buffer2D targetPixels, Buffer2D sourcePixels, KernelSamplingMap map, int kernelSize, Configuration configuration, bool preserveAlpha) + { + this.bounds = bounds; + this.configuration = configuration; + this.targetPixels = targetPixels; + this.sourcePixels = sourcePixels; + this.map = map; + this.kernelSize = kernelSize; + this.preserveAlpha = preserveAlpha; + int kernelCount = this.kernelSize * this.kernelSize; + this.yChannelStart = kernelCount; + this.zChannelStart = this.yChannelStart + kernelCount; + this.wChannelStart = this.zChannelStart + kernelCount; + } + + public void Invoke(int y, Span span) + { + // Span has kernelSize^2 followed by bound width. + int boundsLeft = this.bounds.Left; + int boundsWidth = this.bounds.Width; + int boundsRight = this.bounds.Right; + int kernelCount = this.kernelSize * this.kernelSize; + Span kernelBuffer = span.Slice(0, kernelCount); + Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); + Span targetBuffer = span.Slice(kernelCount << 1, boundsWidth); + + // Stack 4 channels of floats in the space of Vector4's. + Span channelBuffer = MemoryMarshal.Cast(channelVectorBuffer); + var xChannel = channelBuffer.Slice(0, kernelCount); + var yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); + var zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); + var wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); + + var xOffsets = this.map.GetColumnOffsetSpan(); + var yOffsets = this.map.GetRowOffsetSpan(); + var baseXOffsetIndex = 0; + var baseYOffsetIndex = (y - this.bounds.Top) * this.kernelSize; + + for (var x = boundsLeft; x < boundsRight; x++) + { + var index = 0; + for (var w = 0; w < this.kernelSize; w++) + { + var j = yOffsets[baseYOffsetIndex + w]; + var row = this.sourcePixels.DangerousGetRowSpan(j); + for (var z = 0; z < this.kernelSize; z++) + { + var k = xOffsets[baseXOffsetIndex + z]; + var pixel = row[k]; + kernelBuffer[index + z] = pixel.ToVector4(); + } + + index += this.kernelSize; + } + + targetBuffer[x - boundsLeft] = this.preserveAlpha ? + this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount) : + this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); + baseXOffsetIndex += this.kernelSize; + } + + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsLeft, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian3(Span kernelSpan, Span xChannel, Span yChannel, Span zChannel, int stride) + { + int halfLength = (kernelSpan.Length + 1) >> 1; + + // Split color channels + for (int i = 0; i < xChannel.Length; i++) + { + xChannel[i] = kernelSpan[i].X; + yChannel[i] = kernelSpan[i].Y; + zChannel[i] = kernelSpan[i].Z; + } + + // Sort each channel serarately. + xChannel.Sort(); + yChannel.Sort(); + zChannel.Sort(); + + return new Vector4(xChannel[halfLength], yChannel[halfLength], zChannel[halfLength], kernelSpan[halfLength].W); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian4(Span kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel, int stride) + { + int halfLength = (kernelSpan.Length + 1) >> 1; + + // Split color channels + for (int i = 0; i < xChannel.Length; i++) + { + xChannel[i] = kernelSpan[i].X; + yChannel[i] = kernelSpan[i].Y; + zChannel[i] = kernelSpan[i].Z; + wChannel[i] = kernelSpan[i].W; + } + + // Sort each channel serarately. + xChannel.Sort(); + yChannel.Sort(); + zChannel.Sort(); + wChannel.Sort(); + + return new Vector4(xChannel[halfLength], yChannel[halfLength], zChannel[halfLength], wChannel[halfLength]); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs new file mode 100644 index 000000000..db1cbefb4 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Convolution +{ + [Trait("Category", "Processors")] + public class MedianBlurTest : BaseImageOperationsExtensionTest + { + [Fact] + public void Median_radius_MedianProcessorDefaultsSet() + { + this.operations.MedianBlur(3, true); + var processor = this.Verify(); + + Assert.Equal(3, processor.Radius); + Assert.Equal(true, processor.PreserveAlpha); + } + + [Fact] + public void Median_radius_rect_MedianProcessorDefaultsSet() + { + this.operations.MedianBlur(5, false, this.rect); + var processor = this.Verify(this.rect); + + Assert.Equal(5, processor.Radius); + Assert.Equal(false, processor.PreserveAlpha); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs new file mode 100644 index 000000000..f63fbbcf2 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution +{ + [Trait("Category", "Processors")] + [GroupOutput("Convolution")] + public class MedianBlurTest : Basic1ParameterConvolutionTests + { + protected override void Apply(IImageProcessingContext ctx, int value) => ctx.MedianBlur(value, true); + + protected override void Apply(IImageProcessingContext ctx, int value, Rectangle bounds) => + ctx.MedianBlur(value, true, bounds); + } +} diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_3.png new file mode 100644 index 000000000..a81fa6d08 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea5c7e0191cd4cf12067b462f13a7466fac94e94c12fa9d9b291f3d9677a14b4 +size 331353 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_5.png new file mode 100644 index 000000000..cdb13d561 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6c5492f42eb5ffaeb7878b47b7654bb2a08bf91f13fe4ca8186892a849ba516 +size 322752 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_3.png new file mode 100644 index 000000000..647e65ee7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1fe499ba5b6f4f9ffa73d898be45e2ee13fc7c7c65b5f3366569a280546cb49 +size 234179 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_5.png new file mode 100644 index 000000000..99230049e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fab69dad4a26739fe7dd2167d4b5ec9581b9d1bd9fef9b3df0cf2a195d03efc7 +size 227933 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_3.png new file mode 100644 index 000000000..c3f8c111c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c06cefc194ab21aabaf256d8a65b42d62b3a22c1a141f8d706a4c2958cec22e +size 194915 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_5.png new file mode 100644 index 000000000..a01e8b8bf --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a7ec3721962f469ad9c057a464f2c79ee64c583e227c3956d27be7240fa0ab7 +size 194709 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_3.png new file mode 100644 index 000000000..c95d21317 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b12f2df171142c9ccb368673c1809a6efa2cf0c4a2bb685ca9012e02b54532 +size 225209 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_5.png new file mode 100644 index 000000000..8edc0a647 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5c532fee91fd812fb36f4ae2f05552de4d66f863ee7c1becb33e6e26e6fb6ba +size 189577 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_3.png new file mode 100644 index 000000000..999da06ee --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c9adda439083b795e163d3a54108750ee921010fe03ef9b39bec794a26f8fe4 +size 207101 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_5.png new file mode 100644 index 000000000..9de4f0b2c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0a86d9ec30d609756b402bed229e38bbcd30878c49224d6f6821a240d460608 +size 192432 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_3.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_3.png new file mode 100644 index 000000000..1d5b97ee2 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48cbc90a9fb90c12882e52d2999f1d41da89230c18d9b0a9d06ee2917acee1b8 +size 149396 diff --git a/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_5.png b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_5.png new file mode 100644 index 000000000..bb6cfb065 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cfddcda78110c29f8ddf0cc8cc8089e77bef8f13c468c8fee5eda18779defb4 +size 143547 From 626488e17b09e388826253ca4ae520d51b9f2fff Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 30 Aug 2022 21:29:24 +0200 Subject: [PATCH 02/10] Preserve alpha in separate loop --- .../Convolution/MedianRowOperation{TPixel}.cs | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index 0678652ec..c3f32a717 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -59,34 +59,58 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution var xChannel = channelBuffer.Slice(0, kernelCount); var yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); var zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); - var wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); var xOffsets = this.map.GetColumnOffsetSpan(); var yOffsets = this.map.GetRowOffsetSpan(); var baseXOffsetIndex = 0; var baseYOffsetIndex = (y - this.bounds.Top) * this.kernelSize; - for (var x = boundsLeft; x < boundsRight; x++) + if (this.preserveAlpha) { - var index = 0; - for (var w = 0; w < this.kernelSize; w++) + for (var x = boundsLeft; x < boundsRight; x++) { - var j = yOffsets[baseYOffsetIndex + w]; - var row = this.sourcePixels.DangerousGetRowSpan(j); - for (var z = 0; z < this.kernelSize; z++) + var index = 0; + for (var w = 0; w < this.kernelSize; w++) { - var k = xOffsets[baseXOffsetIndex + z]; - var pixel = row[k]; - kernelBuffer[index + z] = pixel.ToVector4(); + var j = yOffsets[baseYOffsetIndex + w]; + var row = this.sourcePixels.DangerousGetRowSpan(j); + for (var z = 0; z < this.kernelSize; z++) + { + var k = xOffsets[baseXOffsetIndex + z]; + var pixel = row[k]; + kernelBuffer[index + z] = pixel.ToVector4(); + } + + index += this.kernelSize; } - index += this.kernelSize; + targetBuffer[x - boundsLeft] = this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount); + baseXOffsetIndex += this.kernelSize; } + } + else + { + var wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); + for (var x = boundsLeft; x < boundsRight; x++) + { + var index = 0; + for (var w = 0; w < this.kernelSize; w++) + { + var j = yOffsets[baseYOffsetIndex + w]; + var row = this.sourcePixels.DangerousGetRowSpan(j); + for (var z = 0; z < this.kernelSize; z++) + { + var k = xOffsets[baseXOffsetIndex + z]; + var pixel = row[k]; + kernelBuffer[index + z] = pixel.ToVector4(); + } + + index += this.kernelSize; + } - targetBuffer[x - boundsLeft] = this.preserveAlpha ? - this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount) : - this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); - baseXOffsetIndex += this.kernelSize; + targetBuffer[x - boundsLeft] = this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); + baseXOffsetIndex += this.kernelSize; + } } Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsLeft, boundsWidth); From eb3d3a56411250d7f329586ef0554fe9a86ee881 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 30 Aug 2022 21:35:17 +0200 Subject: [PATCH 03/10] Clean up dead code --- .../MedianBlurProcessor{TPixel}.cs | 87 ------------------- 1 file changed, 87 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index 10fd5a5d6..c9b6abbc7 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -1,9 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -58,90 +56,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); } - - private void ProcessSingleRow(PixelAccessor access, KernelSamplingMap map, int y, Span dest) - { - int kernelSize = (2 * this.definition.Radius) + 1; - int kernelCount = kernelSize * kernelSize; - using var vectorsBuffer = this.Configuration.MemoryAllocator.Allocate(kernelCount); - var vectorsSpan = vectorsBuffer.Memory.Span; - using var rowBuffer = this.Configuration.MemoryAllocator.Allocate(dest.Length); - var rowSpan = rowBuffer.Memory.Span; - using var componentsBuffer = this.Configuration.MemoryAllocator.Allocate(kernelCount << 2); - var componentsSpan = componentsBuffer.Memory.Span; - var xs = componentsSpan.Slice(0, kernelCount); - var ys = componentsSpan.Slice(kernelCount, kernelCount); - var zs = componentsSpan.Slice(kernelCount << 1, kernelCount); - var ws = componentsSpan.Slice(kernelCount * 3, kernelCount); - var xOffsets = map.GetColumnOffsetSpan(); - var yOffsets = map.GetRowOffsetSpan(); - var baseYOffsetIndex = y * kernelSize; - for (var x = 0; x < access.Width; x++) - { - var baseXOffsetIndex = x * kernelSize; - var index = 0; - for (var w = 0; w < kernelSize; w++) - { - var j = yOffsets[baseYOffsetIndex + w]; - var row = access.GetRowSpan(j); - for (var z = 0; z < kernelSize; z++) - { - var k = xOffsets[baseXOffsetIndex + z]; - var pixel = row[k]; - vectorsSpan[index + z] = pixel.ToVector4(); - } - - index += kernelSize; - } - - rowSpan[x] = this.FindMedian4(vectorsSpan, xs, ys, zs, ws, kernelCount); - } - - PixelOperations.Instance.FromVector4Destructive(this.Configuration, rowSpan, dest); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian3(Span span, Span xs, Span ys, Span zs, Span ws, int stride) - { - Vector4 found = new Vector4(0f, 0f, 0f, 0f); - int halfLength = (span.Length + 1) >> 1; - - // Find median of X component. - for (int i = 0; i < xs.Length; i++) - { - xs[i] = span[i].X; - ys[i] = span[i].Y; - zs[i] = span[i].Z; - } - - xs.Sort(); - ys.Sort(); - zs.Sort(); - - return new Vector4(xs[halfLength], ys[halfLength], zs[halfLength], span[halfLength].W); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian4(Span span, Span xs, Span ys, Span zs, Span ws, int stride) - { - Vector4 found = new Vector4(0f, 0f, 0f, 0f); - int halfLength = (span.Length + 1) >> 1; - - // Find median of X component. - for (int i = 0; i < xs.Length; i++) - { - xs[i] = span[i].X; - ys[i] = span[i].Y; - zs[i] = span[i].Z; - ws[i] = span[i].W; - } - - xs.Sort(); - ys.Sort(); - zs.Sort(); - ws.Sort(); - - return new Vector4(xs[halfLength], ys[halfLength], zs[halfLength], ws[halfLength]); - } } } From 2959db03c36193c027bf33dde90845be3b3fef40 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 30 Aug 2022 21:44:07 +0200 Subject: [PATCH 04/10] Solve build warnings --- .../ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs index db1cbefb4..aac1a1eca 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution var processor = this.Verify(); Assert.Equal(3, processor.Radius); - Assert.Equal(true, processor.PreserveAlpha); + Assert.True(processor.PreserveAlpha); } [Fact] @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution var processor = this.Verify(this.rect); Assert.Equal(5, processor.Radius); - Assert.Equal(false, processor.PreserveAlpha); + Assert.False(processor.PreserveAlpha); } } } From ea474d7499fccb86d4cd3855c030592163840048 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Thu, 1 Sep 2022 12:27:02 +0200 Subject: [PATCH 05/10] Solving IDE0008 warnings --- .../MedianBlurProcessor{TPixel}.cs | 6 +-- .../Convolution/MedianRowOperation{TPixel}.cs | 50 ++++++++++--------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index c9b6abbc7..8867c84e8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -31,13 +31,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution source.CopyTo(targetPixels); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We use a rectangle with width set to 2 * kernelSize^2 + width, to allocate a buffer big enough // for kernel source and target bulk pixel conversion. - var operationBounds = new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + interest.Width, interest.Height); + Rectangle operationBounds = new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + interest.Width, interest.Height); - using var map = new KernelSamplingMap(this.Configuration.MemoryAllocator); + using KernelSamplingMap map = new KernelSamplingMap(this.Configuration.MemoryAllocator); map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); var operation = new MedianRowOperation( diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index c3f32a717..764782f29 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -56,28 +56,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution // Stack 4 channels of floats in the space of Vector4's. Span channelBuffer = MemoryMarshal.Cast(channelVectorBuffer); - var xChannel = channelBuffer.Slice(0, kernelCount); - var yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); - var zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); + Span xChannel = channelBuffer.Slice(0, kernelCount); + Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); + Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); - var xOffsets = this.map.GetColumnOffsetSpan(); - var yOffsets = this.map.GetRowOffsetSpan(); - var baseXOffsetIndex = 0; - var baseYOffsetIndex = (y - this.bounds.Top) * this.kernelSize; + Span xOffsets = this.map.GetColumnOffsetSpan(); + Span yOffsets = this.map.GetRowOffsetSpan(); + int baseXOffsetIndex = 0; + int baseYOffsetIndex = (y - this.bounds.Top) * this.kernelSize; if (this.preserveAlpha) { - for (var x = boundsLeft; x < boundsRight; x++) + for (int x = boundsLeft; x < boundsRight; x++) { - var index = 0; - for (var w = 0; w < this.kernelSize; w++) + int index = 0; + for (int w = 0; w < this.kernelSize; w++) { - var j = yOffsets[baseYOffsetIndex + w]; - var row = this.sourcePixels.DangerousGetRowSpan(j); - for (var z = 0; z < this.kernelSize; z++) + int j = yOffsets[baseYOffsetIndex + w]; + Span row = this.sourcePixels.DangerousGetRowSpan(j); + for (int z = 0; z < this.kernelSize; z++) { - var k = xOffsets[baseXOffsetIndex + z]; - var pixel = row[k]; + int k = xOffsets[baseXOffsetIndex + z]; + TPixel pixel = row[k]; kernelBuffer[index + z] = pixel.ToVector4(); } @@ -90,18 +90,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } else { - var wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); - for (var x = boundsLeft; x < boundsRight; x++) + Span wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); + for (int x = boundsLeft; x < boundsRight; x++) { - var index = 0; - for (var w = 0; w < this.kernelSize; w++) + int index = 0; + for (int w = 0; w < this.kernelSize; w++) { - var j = yOffsets[baseYOffsetIndex + w]; - var row = this.sourcePixels.DangerousGetRowSpan(j); - for (var z = 0; z < this.kernelSize; z++) + int j = yOffsets[baseYOffsetIndex + w]; + Span row = this.sourcePixels.DangerousGetRowSpan(j); + for (int z = 0; z < this.kernelSize; z++) { - var k = xOffsets[baseXOffsetIndex + z]; - var pixel = row[k]; + int k = xOffsets[baseXOffsetIndex + z]; + TPixel pixel = row[k]; kernelBuffer[index + z] = pixel.ToVector4(); } @@ -135,6 +135,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution yChannel.Sort(); zChannel.Sort(); + // Taking the W value from the source pixels, where the middle index in the kernelSpan is by definition the resulting pixel. + // This will preserve the alpha value. return new Vector4(xChannel[halfLength], yChannel[halfLength], zChannel[halfLength], kernelSpan[halfLength].W); } From 22af95a8d58e37e6d7d5774da4b91809630030e8 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Thu, 1 Sep 2022 16:49:26 +0200 Subject: [PATCH 06/10] Minor rework --- .../Convolution/MedianRowOperation{TPixel}.cs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index 764782f29..a926abb80 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -46,9 +46,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution public void Invoke(int y, Span span) { // Span has kernelSize^2 followed by bound width. - int boundsLeft = this.bounds.Left; + int boundsX = this.bounds.X; int boundsWidth = this.bounds.Width; - int boundsRight = this.bounds.Right; int kernelCount = this.kernelSize * this.kernelSize; Span kernelBuffer = span.Slice(0, kernelCount); Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); @@ -67,53 +66,51 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution if (this.preserveAlpha) { - for (int x = boundsLeft; x < boundsRight; x++) + for (int x = 0; x < boundsWidth; x++) { int index = 0; - for (int w = 0; w < this.kernelSize; w++) + for (int kY = 0; kY < this.kernelSize; kY++) { - int j = yOffsets[baseYOffsetIndex + w]; - Span row = this.sourcePixels.DangerousGetRowSpan(j); - for (int z = 0; z < this.kernelSize; z++) + int currentY = yOffsets[baseYOffsetIndex + kY]; + Span row = this.sourcePixels.DangerousGetRowSpan(currentY); + for (int kX = 0; kX < this.kernelSize; kX++) { - int k = xOffsets[baseXOffsetIndex + z]; - TPixel pixel = row[k]; - kernelBuffer[index + z] = pixel.ToVector4(); + int currentX = xOffsets[baseXOffsetIndex + kX]; + TPixel pixel = row[currentX]; + kernelBuffer[index] = pixel.ToVector4(); + index++; } - - index += this.kernelSize; } - targetBuffer[x - boundsLeft] = this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount); + targetBuffer[x] = this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount); baseXOffsetIndex += this.kernelSize; } } else { Span wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount); - for (int x = boundsLeft; x < boundsRight; x++) + for (int x = 0; x < boundsWidth; x++) { int index = 0; - for (int w = 0; w < this.kernelSize; w++) + for (int kY = 0; kY < this.kernelSize; kY++) { - int j = yOffsets[baseYOffsetIndex + w]; + int j = yOffsets[baseYOffsetIndex + kY]; Span row = this.sourcePixels.DangerousGetRowSpan(j); - for (int z = 0; z < this.kernelSize; z++) + for (int kX = 0; kX < this.kernelSize; kX++) { - int k = xOffsets[baseXOffsetIndex + z]; - TPixel pixel = row[k]; - kernelBuffer[index + z] = pixel.ToVector4(); + int currentX = xOffsets[baseXOffsetIndex + kX]; + TPixel pixel = row[currentX]; + kernelBuffer[index] = pixel.ToVector4(); + index++; } - - index += this.kernelSize; } - targetBuffer[x - boundsLeft] = this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); + targetBuffer[x] = this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); baseXOffsetIndex += this.kernelSize; } } - Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsLeft, boundsWidth); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); } From f97a2cc22e8125bc9894b3408222aac2ad4fcc1f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 2 Sep 2022 12:37:54 +0200 Subject: [PATCH 07/10] Use ConvolutionState and Unsafe.Add --- src/ImageSharp/Primitives/DenseMatrix{T}.cs | 21 +++++ .../Processors/Convolution/Kernel.cs | 92 +++++++++++++++++++ .../Convolution/MedianConvolutionState.cs | 46 ++++++++++ .../Convolution/MedianRowOperation{TPixel}.cs | 56 ++++++----- 4 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Convolution/Kernel.cs create mode 100644 src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs diff --git a/src/ImageSharp/Primitives/DenseMatrix{T}.cs b/src/ImageSharp/Primitives/DenseMatrix{T}.cs index 471e4f6b2..de95ba348 100644 --- a/src/ImageSharp/Primitives/DenseMatrix{T}.cs +++ b/src/ImageSharp/Primitives/DenseMatrix{T}.cs @@ -96,6 +96,27 @@ namespace SixLabors.ImageSharp } } + /// + /// Initializes a new instance of the struct. + /// + /// The number of columns. + /// The number of rows. + /// The array to provide access to. + public DenseMatrix(int columns, int rows, Span data) + { + Guard.MustBeGreaterThan(rows, 0, nameof(this.Rows)); + Guard.MustBeGreaterThan(columns, 0, nameof(this.Columns)); + Guard.IsTrue(rows * columns == data.Length, nameof(data), "Length should be equal to ros * columns"); + + this.Rows = rows; + this.Columns = columns; + this.Size = new Size(columns, rows); + this.Count = this.Columns * this.Rows; + this.Data = new T[this.Columns * this.Rows]; + + data.CopyTo(this.Data); + } + /// /// Gets a span wrapping the . /// diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs new file mode 100644 index 000000000..dbf345caf --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A stack only, readonly, kernel matrix that can be indexed without + /// bounds checks when compiled in release mode. + /// + internal readonly ref struct Kernel + where T : struct, IEquatable + { + private readonly Span values; + + public Kernel(DenseMatrix matrix) + { + this.Columns = matrix.Columns; + this.Rows = matrix.Rows; + this.values = matrix.Span; + } + + public int Columns + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public int Rows + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public ReadOnlySpan Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return this.values; + } + } + + public T this[int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + this.CheckCoordinates(row, column); + ref T vBase = ref MemoryMarshal.GetReference(this.values); + return Unsafe.Add(ref vBase, (row * this.Columns) + column); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetValue(int row, int column, T value) + { + this.SetValue((row * this.Columns) + column, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetValue(int index, T value) + { + ref T vBase = ref MemoryMarshal.GetReference(this.values); + Unsafe.Add(ref vBase, index) = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + this.values.Clear(); + } + + [Conditional("DEBUG")] + private void CheckCoordinates(int row, int column) + { + if (row < 0 || row >= this.Rows) + { + throw new ArgumentOutOfRangeException(nameof(row), row, $"{row} is outside the matrix bounds."); + } + + if (column < 0 || column >= this.Columns) + { + throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outside the matrix bounds."); + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs new file mode 100644 index 000000000..f4aa68e22 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// A stack only struct used for reducing reference indirection during convolution operations. + /// + internal readonly ref struct MedianConvolutionState + { + private readonly Span rowOffsetMap; + private readonly Span columnOffsetMap; + private readonly int kernelHeight; + private readonly int kernelWidth; + + public MedianConvolutionState( + in DenseMatrix kernel, + KernelSamplingMap map) + { + this.Kernel = new Kernel(kernel); + this.kernelHeight = kernel.Rows; + this.kernelWidth = kernel.Columns; + this.rowOffsetMap = map.GetRowOffsetSpan(); + this.columnOffsetMap = map.GetColumnOffsetSpan(); + } + + public readonly Kernel Kernel + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleRow(int row) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.rowOffsetMap), row * this.kernelHeight); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly ref int GetSampleColumn(int column) + => ref Unsafe.Add(ref MemoryMarshal.GetReference(this.columnOffsetMap), column * this.kernelWidth); + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index a926abb80..4aa2ef7ec 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -59,31 +59,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); - Span xOffsets = this.map.GetColumnOffsetSpan(); - Span yOffsets = this.map.GetRowOffsetSpan(); - int baseXOffsetIndex = 0; - int baseYOffsetIndex = (y - this.bounds.Top) * this.kernelSize; + DenseMatrix kernel = new DenseMatrix(this.kernelSize, this.kernelSize, kernelBuffer); + + int row = y - this.bounds.Y; + MedianConvolutionState state = new MedianConvolutionState(in kernel, this.map); + ref int sampleRowBase = ref state.GetSampleRow(row); + ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); if (this.preserveAlpha) { for (int x = 0; x < boundsWidth; x++) { int index = 0; - for (int kY = 0; kY < this.kernelSize; kY++) + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + for (int kY = 0; kY < state.Kernel.Rows; kY++) { - int currentY = yOffsets[baseYOffsetIndex + kY]; - Span row = this.sourcePixels.DangerousGetRowSpan(currentY); - for (int kX = 0; kX < this.kernelSize; kX++) + int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); + ref TPixel sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + for (int kX = 0; kX < state.Kernel.Columns; kX++) { - int currentX = xOffsets[baseXOffsetIndex + kX]; - TPixel pixel = row[currentX]; - kernelBuffer[index] = pixel.ToVector4(); + int currentXIndex = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + TPixel pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); + state.Kernel.SetValue(index, pixel.ToVector4()); index++; } } - targetBuffer[x] = this.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount); - baseXOffsetIndex += this.kernelSize; + target = this.FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel, kernelCount); + state.Kernel.Clear(); } } else @@ -92,21 +97,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution for (int x = 0; x < boundsWidth; x++) { int index = 0; - for (int kY = 0; kY < this.kernelSize; kY++) + ref int sampleColumnBase = ref state.GetSampleColumn(x); + ref Vector4 target = ref Unsafe.Add(ref targetBase, x); + for (int kY = 0; kY < state.Kernel.Rows; kY++) { - int j = yOffsets[baseYOffsetIndex + kY]; - Span row = this.sourcePixels.DangerousGetRowSpan(j); - for (int kX = 0; kX < this.kernelSize; kX++) + int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); + ref TPixel sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + for (int kX = 0; kX < state.Kernel.Columns; kX++) { - int currentX = xOffsets[baseXOffsetIndex + kX]; - TPixel pixel = row[currentX]; - kernelBuffer[index] = pixel.ToVector4(); + int currentXIndex = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; + TPixel pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); + state.Kernel.SetValue(index, pixel.ToVector4()); index++; } } - targetBuffer[x] = this.FindMedian4(kernelBuffer, xChannel, yChannel, zChannel, wChannel, kernelCount); - baseXOffsetIndex += this.kernelSize; + target = this.FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel, kernelCount); + state.Kernel.Clear(); } } @@ -115,7 +123,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian3(Span kernelSpan, Span xChannel, Span yChannel, Span zChannel, int stride) + private Vector4 FindMedian3(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, int stride) { int halfLength = (kernelSpan.Length + 1) >> 1; @@ -138,7 +146,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian4(Span kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel, int stride) + private Vector4 FindMedian4(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel, int stride) { int halfLength = (kernelSpan.Length + 1) >> 1; From bc1162e05b32d15041015614cdc8bd7d1f966185 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 4 Sep 2022 17:30:54 +0200 Subject: [PATCH 08/10] Bulk convert source rows to Vector4 --- .../MedianBlurProcessor{TPixel}.cs | 11 +++---- .../Convolution/MedianRowOperation{TPixel}.cs | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index 8867c84e8..a44a81725 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -17,10 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private readonly MedianBlurProcessor definition; public MedianBlurProcessor(Configuration configuration, MedianBlurProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - this.definition = definition; - } + : base(configuration, source, sourceRectangle) => this.definition = definition; protected override void OnFrameApply(ImageFrame source) { @@ -31,13 +28,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution source.CopyTo(targetPixels); - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We use a rectangle with width set to 2 * kernelSize^2 + width, to allocate a buffer big enough // for kernel source and target bulk pixel conversion. - Rectangle operationBounds = new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + interest.Width, interest.Height); + var operationBounds = new Rectangle(interest.X, interest.Y, (2 * kernelSize * kernelSize) + interest.Width + (kernelSize * interest.Width), interest.Height); - using KernelSamplingMap map = new KernelSamplingMap(this.Configuration.MemoryAllocator); + using var map = new KernelSamplingMap(this.Configuration.MemoryAllocator); map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); var operation = new MedianRowOperation( diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index 4aa2ef7ec..8296e064d 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -51,7 +51,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int kernelCount = this.kernelSize * this.kernelSize; Span kernelBuffer = span.Slice(0, kernelCount); Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); - Span targetBuffer = span.Slice(kernelCount << 1, boundsWidth); + Span sourceVectorBuffer = span.Slice(kernelCount << 1, this.kernelSize * boundsWidth); + Span targetBuffer = span.Slice((kernelCount << 1) + sourceVectorBuffer.Length, boundsWidth); // Stack 4 channels of floats in the space of Vector4's. Span channelBuffer = MemoryMarshal.Cast(channelVectorBuffer); @@ -59,13 +60,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); - DenseMatrix kernel = new DenseMatrix(this.kernelSize, this.kernelSize, kernelBuffer); + var kernel = new DenseMatrix(this.kernelSize, this.kernelSize, kernelBuffer); int row = y - this.bounds.Y; - MedianConvolutionState state = new MedianConvolutionState(in kernel, this.map); + var state = new MedianConvolutionState(in kernel, this.map); ref int sampleRowBase = ref state.GetSampleRow(row); ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); + // First convert the required source rows to Vector4. + for (int i = 0; i < this.kernelSize; i++) + { + int currentYIndex = Unsafe.Add(ref sampleRowBase, i); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); + Span sourceVectorRow = sourceVectorBuffer.Slice(i * boundsWidth, boundsWidth); + PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceVectorRow); + } + if (this.preserveAlpha) { for (int x = 0; x < boundsWidth; x++) @@ -76,13 +86,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution for (int kY = 0; kY < state.Kernel.Rows; kY++) { int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); - Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); - ref TPixel sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); + ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) { int currentXIndex = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; - TPixel pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); - state.Kernel.SetValue(index, pixel.ToVector4()); + Vector4 pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); + state.Kernel.SetValue(index, pixel); index++; } } @@ -102,13 +112,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution for (int kY = 0; kY < state.Kernel.Rows; kY++) { int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); - Span sourceRow = this.sourcePixels.DangerousGetRowSpan(currentYIndex).Slice(boundsX, boundsWidth); - ref TPixel sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); + Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); + ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) { int currentXIndex = Unsafe.Add(ref sampleColumnBase, kX) - boundsX; - TPixel pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); - state.Kernel.SetValue(index, pixel.ToVector4()); + Vector4 pixel = Unsafe.Add(ref sourceRowBase, currentXIndex); + state.Kernel.SetValue(index, pixel); index++; } } From bb3acac4d2d2f8f32f7a0176b280b5354996b2c3 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 4 Sep 2022 17:44:14 +0200 Subject: [PATCH 09/10] Remove dead code --- .../Processors/Convolution/MedianBlurProcessor{TPixel}.cs | 7 ++++--- .../Processors/Convolution/MedianRowOperation{TPixel}.cs | 4 ---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index a44a81725..bc09d3bd8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -30,9 +30,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - // We use a rectangle with width set to 2 * kernelSize^2 + width, to allocate a buffer big enough - // for kernel source and target bulk pixel conversion. - var operationBounds = new Rectangle(interest.X, interest.Y, (2 * kernelSize * kernelSize) + interest.Width + (kernelSize * interest.Width), interest.Height); + // We use a rectangle with width set wider, to allocate a buffer big enough + // for kernel source, channel buffers, source rows and target bulk pixel conversion. + int operationWidth = (2 * kernelSize * kernelSize) + interest.Width + (kernelSize * interest.Width); + var operationBounds = new Rectangle(interest.X, interest.Y, operationWidth, interest.Height); using var map = new KernelSamplingMap(this.Configuration.MemoryAllocator); map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index 8296e064d..76987abf8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -85,7 +85,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution ref Vector4 target = ref Unsafe.Add(ref targetBase, x); for (int kY = 0; kY < state.Kernel.Rows; kY++) { - int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) @@ -98,7 +97,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } target = this.FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel, kernelCount); - state.Kernel.Clear(); } } else @@ -111,7 +109,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution ref Vector4 target = ref Unsafe.Add(ref targetBase, x); for (int kY = 0; kY < state.Kernel.Rows; kY++) { - int currentYIndex = Unsafe.Add(ref sampleRowBase, kY); Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) @@ -124,7 +121,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } target = this.FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel, kernelCount); - state.Kernel.Clear(); } } From b6fb196982e1e19012eb4bb108923e9cff4c3c67 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 12 Sep 2022 12:18:34 +1000 Subject: [PATCH 10/10] Update to match new build rules. --- .../Processors/Convolution/Kernel.cs | 33 +++++++++++-------- .../MedianBlurProcessor{TPixel}.cs | 9 ++--- .../Convolution/MedianRowOperation{TPixel}.cs | 21 ++++++------ 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs index dbf345caf..032a8ce44 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs @@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// A stack only, readonly, kernel matrix that can be indexed without /// bounds checks when compiled in release mode. /// + /// The type of each element in the kernel. internal readonly ref struct Kernel where T : struct, IEquatable { @@ -39,10 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution public ReadOnlySpan Span { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return this.values; - } + get => this.values; } public T this[int row, int column] @@ -54,26 +52,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution ref T vBase = ref MemoryMarshal.GetReference(this.values); return Unsafe.Add(ref vBase, (row * this.Columns) + column); } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetValue(int row, int column, T value) - { - this.SetValue((row * this.Columns) + column, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.CheckCoordinates(row, column); + ref T vBase = ref MemoryMarshal.GetReference(this.values); + Unsafe.Add(ref vBase, (row * this.Columns) + column) = value; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetValue(int index, T value) { + this.CheckIndex(index); ref T vBase = ref MemoryMarshal.GetReference(this.values); Unsafe.Add(ref vBase, index) = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() - { - this.values.Clear(); - } + public void Clear() => this.values.Clear(); [Conditional("DEBUG")] private void CheckCoordinates(int row, int column) @@ -88,5 +86,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution throw new ArgumentOutOfRangeException(nameof(column), column, $"{column} is outside the matrix bounds."); } } + + [Conditional("DEBUG")] + private void CheckIndex(int index) + { + if (index < 0 || index >= this.values.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), index, $"{index} is outside the matrix bounds."); + } + } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs index bc09d3bd8..8e2540faf 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Applies an median filter. /// + /// The type of pixel format. internal sealed class MedianBlurProcessor : ImageProcessor where TPixel : unmanaged, IPixel { @@ -28,17 +29,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution source.CopyTo(targetPixels); - var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); // We use a rectangle with width set wider, to allocate a buffer big enough // for kernel source, channel buffers, source rows and target bulk pixel conversion. int operationWidth = (2 * kernelSize * kernelSize) + interest.Width + (kernelSize * interest.Width); - var operationBounds = new Rectangle(interest.X, interest.Y, operationWidth, interest.Height); + Rectangle operationBounds = new(interest.X, interest.Y, operationWidth, interest.Height); - using var map = new KernelSamplingMap(this.Configuration.MemoryAllocator); + using KernelSamplingMap map = new(this.Configuration.MemoryAllocator); map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY); - var operation = new MedianRowOperation( + MedianRowOperation operation = new( interest, targetPixels, source.PixelBuffer, diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs index 76987abf8..90dce4dad 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs @@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// Applies an median filter. /// + /// The type of pixel format. internal readonly struct MedianRowOperation : IRowOperation where TPixel : unmanaged, IPixel { @@ -49,21 +50,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int boundsX = this.bounds.X; int boundsWidth = this.bounds.Width; int kernelCount = this.kernelSize * this.kernelSize; - Span kernelBuffer = span.Slice(0, kernelCount); + Span kernelBuffer = span[..kernelCount]; Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); Span sourceVectorBuffer = span.Slice(kernelCount << 1, this.kernelSize * boundsWidth); Span targetBuffer = span.Slice((kernelCount << 1) + sourceVectorBuffer.Length, boundsWidth); // Stack 4 channels of floats in the space of Vector4's. Span channelBuffer = MemoryMarshal.Cast(channelVectorBuffer); - Span xChannel = channelBuffer.Slice(0, kernelCount); + Span xChannel = channelBuffer[..kernelCount]; Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount); - var kernel = new DenseMatrix(this.kernelSize, this.kernelSize, kernelBuffer); + DenseMatrix kernel = new(this.kernelSize, this.kernelSize, kernelBuffer); int row = y - this.bounds.Y; - var state = new MedianConvolutionState(in kernel, this.map); + MedianConvolutionState state = new(in kernel, this.map); ref int sampleRowBase = ref state.GetSampleRow(row); ref Vector4 targetBase = ref MemoryMarshal.GetReference(targetBuffer); @@ -85,7 +86,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution ref Vector4 target = ref Unsafe.Add(ref targetBase, x); for (int kY = 0; kY < state.Kernel.Rows; kY++) { - Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); + Span sourceRow = sourceVectorBuffer[(kY * boundsWidth)..]; ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) { @@ -96,7 +97,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } } - target = this.FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel, kernelCount); + target = FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel); } } else @@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution ref Vector4 target = ref Unsafe.Add(ref targetBase, x); for (int kY = 0; kY < state.Kernel.Rows; kY++) { - Span sourceRow = sourceVectorBuffer.Slice(kY * boundsWidth); + Span sourceRow = sourceVectorBuffer[(kY * boundsWidth)..]; ref Vector4 sourceRowBase = ref MemoryMarshal.GetReference(sourceRow); for (int kX = 0; kX < state.Kernel.Columns; kX++) { @@ -120,7 +121,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } } - target = this.FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel, kernelCount); + target = FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel); } } @@ -129,7 +130,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian3(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, int stride) + private static Vector4 FindMedian3(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel) { int halfLength = (kernelSpan.Length + 1) >> 1; @@ -152,7 +153,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector4 FindMedian4(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel, int stride) + private static Vector4 FindMedian4(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel) { int halfLength = (kernelSpan.Length + 1) >> 1;