mirror of https://github.com/SixLabors/ImageSharp
18 changed files with 469 additions and 0 deletions
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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]); |
|||
} |
|||
} |
|||
} |
|||
@ -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]); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:ea5c7e0191cd4cf12067b462f13a7466fac94e94c12fa9d9b291f3d9677a14b4 |
|||
size 331353 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b6c5492f42eb5ffaeb7878b47b7654bb2a08bf91f13fe4ca8186892a849ba516 |
|||
size 322752 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a1fe499ba5b6f4f9ffa73d898be45e2ee13fc7c7c65b5f3366569a280546cb49 |
|||
size 234179 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fab69dad4a26739fe7dd2167d4b5ec9581b9d1bd9fef9b3df0cf2a195d03efc7 |
|||
size 227933 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6c06cefc194ab21aabaf256d8a65b42d62b3a22c1a141f8d706a4c2958cec22e |
|||
size 194915 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8a7ec3721962f469ad9c057a464f2c79ee64c583e227c3956d27be7240fa0ab7 |
|||
size 194709 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:14b12f2df171142c9ccb368673c1809a6efa2cf0c4a2bb685ca9012e02b54532 |
|||
size 225209 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:d5c532fee91fd812fb36f4ae2f05552de4d66f863ee7c1becb33e6e26e6fb6ba |
|||
size 189577 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1c9adda439083b795e163d3a54108750ee921010fe03ef9b39bec794a26f8fe4 |
|||
size 207101 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b0a86d9ec30d609756b402bed229e38bbcd30878c49224d6f6821a240d460608 |
|||
size 192432 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:48cbc90a9fb90c12882e52d2999f1d41da89230c18d9b0a9d06ee2917acee1b8 |
|||
size 149396 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6cfddcda78110c29f8ddf0cc8cc8089e77bef8f13c468c8fee5eda18779defb4 |
|||
size 143547 |
|||
Loading…
Reference in new issue