Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we optimize this? Would be useful to assign a buffer large enough to allow the bulk operation like we do in the ConvolutionProcessor<T>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the z loops runs over neighboring rows, this buffer would need to contain at least several rows of the source image.

Do you feel that is worth the memory?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can still read/convert the source row slice one at a time though if you're sampling one kernel row at a time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see in the comments below how this is done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should Make ReadOnlyKernel Kernel and add setters. That way you can use a DenseMatrix<T> and use all the existing APIs for Kernel access.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ConvolutionProcessor uses a clever trick, where it builds up the target value row-by-row. This is allowed for ReadOnlyKernel type of RowOperation classes, where we're calculating weighted sums basically. These weighted sums can indeed be split arbitrary, as is done by clearing the target buffer before the loop and using += in line 152
However, the operation of finding a median, cannot be split up like this (or at least not without lots of caching). It mandates the traversal of the entire kernel in one pass, to get a single Span of pixel values to Sort. Hence the different logic here, compared to ConvolutionProcessor.

I'll update the code to use Unsafe.Add iso indexers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no, I didn't mean the processors themselves I just meant the ConvolutionState struct. It's designed to track all the offsets you're manually tracking yourself. ReadOnlyKernel could become Kernel with a read/write indexer (maybe ref based).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a separate MedianConvolutionState struct for this, as it's using Vector4 iso float.

}

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.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 changes: 34 additions & 0 deletions tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs
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);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading