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/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..10fd5a5d65 --- /dev/null +++ b/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 +{ + /// + /// Applies an median filter. + /// + 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); + + 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( + interest, + targetPixels, + source.PixelBuffer, + map, + kernelSize, + this.Configuration, + this.definition.PreserveAlpha); + + ParallelRowIterator.IterateRows, Vector4>( + this.Configuration, + operationBounds, + in operation); + + Buffer2D.SwapOrCopyContent(source.PixelBuffer, targetPixels); + } + + private void ProcessSingleRow(PixelAccessor access, KernelSamplingMap map, int y, Span dest) + { + int kernelSize = (2 * this.definition.Radius) + 1; + int kernelCount = kernelSize * kernelSize; + using var vectorsBuffer = this.Configuration.MemoryAllocator.Allocate(kernelCount); + var vectorsSpan = vectorsBuffer.Memory.Span; + using var rowBuffer = this.Configuration.MemoryAllocator.Allocate(dest.Length); + var rowSpan = rowBuffer.Memory.Span; + using var componentsBuffer = this.Configuration.MemoryAllocator.Allocate(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.Instance.FromVector4Destructive(this.Configuration, rowSpan, dest); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian3(Span span, Span xs, Span ys, Span zs, Span 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 span, Span xs, Span ys, Span zs, Span 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]); + } + } +} 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..0678652ec9 --- /dev/null +++ b/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 +{ + /// + /// Applies an median filter. + /// + 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 boundsLeft = this.bounds.Left; + int boundsWidth = this.bounds.Width; + int boundsRight = this.bounds.Right; + int kernelCount = this.kernelSize * this.kernelSize; + Span kernelBuffer = span.Slice(0, kernelCount); + Span channelVectorBuffer = span.Slice(kernelCount, kernelCount); + Span targetBuffer = span.Slice(kernelCount << 1, boundsWidth); + + // Stack 4 channels of floats in the space of Vector4's. + Span channelBuffer = MemoryMarshal.Cast(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 targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsLeft, boundsWidth); + PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRowSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Vector4 FindMedian3(Span kernelSpan, Span xChannel, Span yChannel, Span 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 kernelSpan, Span xChannel, Span yChannel, Span zChannel, Span 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]); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/MedianBlurTest.cs new file mode 100644 index 0000000000..db1cbefb47 --- /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.Equal(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.Equal(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