diff --git a/src/ImageSharp/Primitives/DenseMatrix{T}.cs b/src/ImageSharp/Primitives/DenseMatrix{T}.cs
index 7eee4a1b08..63ca6f7564 100644
--- a/src/ImageSharp/Primitives/DenseMatrix{T}.cs
+++ b/src/ImageSharp/Primitives/DenseMatrix{T}.cs
@@ -71,6 +71,27 @@ public DenseMatrix(T[,] data)
}
}
+ ///
+ /// 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 the 1D representation of the dense matrix.
///
diff --git a/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/MedianBlurExtensions.cs
new file mode 100644
index 0000000000..a80a119004
--- /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/Kernel.cs b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs
new file mode 100644
index 0000000000..032a8ce445
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Convolution/Kernel.cs
@@ -0,0 +1,99 @@
+// 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.
+ ///
+ /// The type of each element in the kernel.
+ 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 => 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)]
+ 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();
+
+ [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.");
+ }
+ }
+
+ [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.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor.cs
new file mode 100644
index 0000000000..a3819f5e95
--- /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 0000000000..8e2540faf8
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Convolution/MedianBlurProcessor{TPixel}.cs
@@ -0,0 +1,59 @@
+// 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
+{
+ ///
+ /// Applies an median filter.
+ ///
+ /// The type of pixel format.
+ 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);
+
+ 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);
+ Rectangle operationBounds = new(interest.X, interest.Y, operationWidth, interest.Height);
+
+ using KernelSamplingMap map = new(this.Configuration.MemoryAllocator);
+ map.BuildSamplingOffsetMap(kernelSize, kernelSize, interest, this.definition.BorderWrapModeX, this.definition.BorderWrapModeY);
+
+ MedianRowOperation operation = new(
+ interest,
+ targetPixels,
+ source.PixelBuffer,
+ map,
+ kernelSize,
+ this.Configuration,
+ this.definition.PreserveAlpha);
+
+ ParallelRowIterator.IterateRows, Vector4>(
+ this.Configuration,
+ operationBounds,
+ in operation);
+
+ Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels);
+ }
+ }
+}
diff --git a/src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs b/src/ImageSharp/Processing/Processors/Convolution/MedianConvolutionState.cs
new file mode 100644
index 0000000000..f4aa68e22f
--- /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
new file mode 100644
index 0000000000..90dce4dad9
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Convolution/MedianRowOperation{TPixel}.cs
@@ -0,0 +1,178 @@
+// 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.
+ ///
+ /// The type of pixel format.
+ 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 boundsX = this.bounds.X;
+ int boundsWidth = this.bounds.Width;
+ int kernelCount = this.kernelSize * this.kernelSize;
+ 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[..kernelCount];
+ Span yChannel = channelBuffer.Slice(this.yChannelStart, kernelCount);
+ Span zChannel = channelBuffer.Slice(this.zChannelStart, kernelCount);
+
+ DenseMatrix kernel = new(this.kernelSize, this.kernelSize, kernelBuffer);
+
+ int row = y - this.bounds.Y;
+ MedianConvolutionState state = new(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++)
+ {
+ int index = 0;
+ 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++)
+ {
+ Span sourceRow = sourceVectorBuffer[(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;
+ Vector4 pixel = Unsafe.Add(ref sourceRowBase, currentXIndex);
+ state.Kernel.SetValue(index, pixel);
+ index++;
+ }
+ }
+
+ target = FindMedian3(state.Kernel.Span, xChannel, yChannel, zChannel);
+ }
+ }
+ else
+ {
+ Span wChannel = channelBuffer.Slice(this.wChannelStart, kernelCount);
+ for (int x = 0; x < boundsWidth; x++)
+ {
+ int index = 0;
+ 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++)
+ {
+ Span sourceRow = sourceVectorBuffer[(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;
+ Vector4 pixel = Unsafe.Add(ref sourceRowBase, currentXIndex);
+ state.Kernel.SetValue(index, pixel);
+ index++;
+ }
+ }
+
+ target = FindMedian4(state.Kernel.Span, xChannel, yChannel, zChannel, wChannel);
+ }
+ }
+
+ Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth);
+ PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector4 FindMedian3(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel)
+ {
+ 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();
+
+ // 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);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector4 FindMedian4(ReadOnlySpan kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span wChannel)
+ {
+ 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 0000000000..aac1a1eca8
--- /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.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.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 0000000000..f63fbbcf2b
--- /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 0000000000..a81fa6d088
--- /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 0000000000..cdb13d561c
--- /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 0000000000..647e65ee78
--- /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 0000000000..99230049ee
--- /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 0000000000..c3f8c111cd
--- /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 0000000000..a01e8b8bf3
--- /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 0000000000..c95d213173
--- /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 0000000000..8edc0a6471
--- /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 0000000000..999da06eed
--- /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 0000000000..9de4f0b2c1
--- /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 0000000000..1d5b97ee25
--- /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 0000000000..bb6cfb065b
--- /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