-
-
Notifications
You must be signed in to change notification settings - Fork 887
Implement Median Blur processor #2219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
8bbec22
4af75b3
626488e
eb3d3a5
2959db0
3572f51
f53f5f0
cd25825
0e03797
ea474d7
a6daaf9
22af95a
f97a2cc
bc1162e
bb3acac
d59618b
c359b53
b6fb196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // Copyright (c) Six Labors. | ||
| // Licensed under the Six Labors Split License. | ||
|
|
||
| using System.Numerics; | ||
| 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); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // 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); | ||
ynse01 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount); | ||
| var 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; | ||
|
|
||
| if (this.preserveAlpha) | ||
| { | ||
| 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.FindMedian3(kernelBuffer, xChannel, yChannel, zChannel, kernelCount); | ||
ynse01 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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.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]); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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.False(processor.PreserveAlpha); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.