Browse Source

Median Blur

pull/2219/head
Ynse Hoornenborg 4 years ago
parent
commit
4af75b3e61
  1. 41
      src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs
  2. 53
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs
  3. 147
      src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
  4. 140
      src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs
  5. 34
      tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs
  6. 18
      tests/ImageSharp.Tests/Processing/Processors/Convolution/MedianBlurTest.cs
  7. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_3.png
  8. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_CalliphoraPartial_5.png
  9. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_3.png
  10. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_Car_5.png
  11. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_3.png
  12. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/InBox_Rgba32_blur_5.png
  13. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_3.png
  14. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_CalliphoraPartial_5.png
  15. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_3.png
  16. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_Car_5.png
  17. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_3.png
  18. 3
      tests/Images/External/ReferenceOutput/Convolution/MedianBlurTest/OnFullImage_Rgba32_blur_5.png

41
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
{
/// <summary>
/// Defines extensions that allow the applying of the median blur on an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class MedianBlurExtensions
{
/// <summary>
/// Applies a median blur on the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="radius">The radius of the area to find the median for.</param>
/// <param name="preserveAlpha">
/// Whether the filter is applied to alpha as well as the color channels.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha)
=> source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha));
/// <summary>
/// Applies a median blur on the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="radius">The radius of the area to find the median for.</param>
/// <param name="preserveAlpha">
/// Whether the filter is applied to alpha as well as the color channels.
/// </param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext MedianBlur(this IImageProcessingContext source, int radius, bool preserveAlpha, Rectangle rectangle)
=> source.ApplyProcessor(new MedianBlurProcessor(radius, preserveAlpha), rectangle);
}
}

53
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
{
/// <summary>
/// Applies an median filter.
/// </summary>
public sealed class MedianBlurProcessor : IImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="MedianBlurProcessor"/> class.
/// </summary>
/// <param name="radius">
/// The 'radius' value representing the size of the area to filter over.
/// </param>
/// <param name="preserveAlpha">
/// Whether the filter is applied to alpha as well as the color channels.
/// </param>
public MedianBlurProcessor(int radius, bool preserveAlpha)
{
this.Radius = radius;
this.PreserveAlpha = preserveAlpha;
}
/// <summary>
/// Gets the size of the area to find the median of.
/// </summary>
public int Radius { get; }
/// <summary>
/// Gets a value indicating whether the filter is applied to alpha as well as the color channels.
/// </summary>
public bool PreserveAlpha { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in X direction.
/// </summary>
public BorderWrappingMode BorderWrapModeX { get; }
/// <summary>
/// Gets the <see cref="BorderWrappingMode"/> to use when mapping the pixels outside of the border, in Y direction.
/// </summary>
public BorderWrappingMode BorderWrapModeY { get; }
/// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged, IPixel<TPixel>
=> new MedianBlurProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}

147
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
{
/// <summary>
/// Applies an median filter.
/// </summary>
internal sealed class MedianBlurProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly MedianBlurProcessor definition;
public MedianBlurProcessor(Configuration configuration, MedianBlurProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
}
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
int kernelSize = (2 * this.definition.Radius) + 1;
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
using Buffer2D<TPixel> targetPixels = allocator.Allocate2D<TPixel>(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<TPixel>(
interest,
targetPixels,
source.PixelBuffer,
map,
kernelSize,
this.Configuration,
this.definition.PreserveAlpha);
ParallelRowIterator.IterateRows<MedianRowOperation<TPixel>, Vector4>(
this.Configuration,
operationBounds,
in operation);
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
private void ProcessSingleRow(PixelAccessor<TPixel> access, KernelSamplingMap map, int y, Span<TPixel> dest)
{
int kernelSize = (2 * this.definition.Radius) + 1;
int kernelCount = kernelSize * kernelSize;
using var vectorsBuffer = this.Configuration.MemoryAllocator.Allocate<Vector4>(kernelCount);
var vectorsSpan = vectorsBuffer.Memory.Span;
using var rowBuffer = this.Configuration.MemoryAllocator.Allocate<Vector4>(dest.Length);
var rowSpan = rowBuffer.Memory.Span;
using var componentsBuffer = this.Configuration.MemoryAllocator.Allocate<float>(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<TPixel>.Instance.FromVector4Destructive(this.Configuration, rowSpan, dest);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Vector4 FindMedian3(Span<Vector4> span, Span<float> xs, Span<float> ys, Span<float> zs, Span<float> 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<Vector4> span, Span<float> xs, Span<float> ys, Span<float> zs, Span<float> 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]);
}
}
}

140
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
{
/// <summary>
/// Applies an median filter.
/// </summary>
internal readonly struct MedianRowOperation<TPixel> : IRowOperation<Vector4>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int yChannelStart;
private readonly int zChannelStart;
private readonly int wChannelStart;
private readonly Configuration configuration;
private readonly Rectangle bounds;
private readonly Buffer2D<TPixel> targetPixels;
private readonly Buffer2D<TPixel> sourcePixels;
private readonly KernelSamplingMap map;
private readonly int kernelSize;
private readonly bool preserveAlpha;
public MedianRowOperation(Rectangle bounds, Buffer2D<TPixel> targetPixels, Buffer2D<TPixel> 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<Vector4> 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<Vector4> kernelBuffer = span.Slice(0, kernelCount);
Span<Vector4> channelVectorBuffer = span.Slice(kernelCount, kernelCount);
Span<Vector4> targetBuffer = span.Slice(kernelCount << 1, boundsWidth);
// Stack 4 channels of floats in the space of Vector4's.
Span<float> channelBuffer = MemoryMarshal.Cast<Vector4, float>(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<TPixel> targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsLeft, boundsWidth);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Vector4 FindMedian3(Span<Vector4> kernelSpan, Span<float> xChannel, Span<float> yChannel, Span<float> 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<Vector4> kernelSpan, Span<float> xChannel, Span<float> yChannel, Span<float> zChannel, Span<float> 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]);
}
}
}

34
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<MedianBlurProcessor>();
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<MedianBlurProcessor>(this.rect);
Assert.Equal(5, processor.Radius);
Assert.Equal(false, processor.PreserveAlpha);
}
}
}

18
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);
}
}

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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
Loading…
Cancel
Save