From 479b5fa7e0d4c5bf51e49bff467cd4bacdbfcb64 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Nov 2015 17:39:18 +1100 Subject: [PATCH] Fix Gaussian blur Former-commit-id: 496e8a9a7d2e3bd994329d0514004e243ce33812 Former-commit-id: 5ab1b37f8bcd2a55d9e6a2f00fee695ced150e2f Former-commit-id: a68f819af644bae5e87da1af62759f471e7a09ce --- .../Common/Helpers/ImageMaths.cs | 20 +++ .../Convolution/Convolution2DFilter.cs | 9 +- .../Convolution/Convolution2PassFilter.cs | 124 ++++++++++++++++++ .../Filters/Convolution/GuassianBlur.cs | 43 ++---- src/ImageProcessor/ImageProcessor.csproj | 1 + .../project.lock.json.REMOVED.git-id | 2 +- .../Processors/Filters/FilterTests.cs | 2 +- .../Processors/ProcessorTestBase.cs | 8 +- .../Formats/Jpg/china.jpg.REMOVED.git-id | 1 + .../Formats/Png/cballs.png.REMOVED.git-id | 1 + 10 files changed, 172 insertions(+), 39 deletions(-) create mode 100644 src/ImageProcessor/Filters/Convolution/Convolution2PassFilter.cs create mode 100644 tests/ImageProcessor.Tests/TestImages/Formats/Jpg/china.jpg.REMOVED.git-id create mode 100644 tests/ImageProcessor.Tests/TestImages/Formats/Png/cballs.png.REMOVED.git-id diff --git a/src/ImageProcessor/Common/Helpers/ImageMaths.cs b/src/ImageProcessor/Common/Helpers/ImageMaths.cs index b2bf0dede..933421f53 100644 --- a/src/ImageProcessor/Common/Helpers/ImageMaths.cs +++ b/src/ImageProcessor/Common/Helpers/ImageMaths.cs @@ -18,6 +18,26 @@ namespace ImageProcessor // ReSharper disable once InconsistentNaming public const float PI = 3.1415926535897931f; + /// + /// Implementation of 1D Gaussian G(x) function + /// + /// The x provided to G(x). + /// The spread of the blur. + /// The Gaussian G(x) + public static float Gaussian(float x, float sigma) + { + const float Numerator = 1.0f; + float denominator = (float)(Math.Sqrt(2 * PI) * sigma); + + float exponentNumerator = -x * x; + float exponentDenominator = (float)(2 * Math.Pow(sigma, 2)); + + float left = Numerator / denominator; + float right = (float)Math.Exp(exponentNumerator / exponentDenominator); + + return left * right; + } + /// /// Returns the result of a B-C filter against the given value. /// diff --git a/src/ImageProcessor/Filters/Convolution/Convolution2DFilter.cs b/src/ImageProcessor/Filters/Convolution/Convolution2DFilter.cs index c50bc4e45..b9852b804 100644 --- a/src/ImageProcessor/Filters/Convolution/Convolution2DFilter.cs +++ b/src/ImageProcessor/Filters/Convolution/Convolution2DFilter.cs @@ -31,7 +31,7 @@ namespace ImageProcessor.Filters int kernelYHeight = kernelY.GetLength(0); int kernelYWidth = kernelY.GetLength(1); int kernelXHeight = kernelX.GetLength(0); - int kernelXWidth = kernelX.GetLength(0); + int kernelXWidth = kernelX.GetLength(1); int radiusY = kernelYHeight >> 1; int radiusX = kernelXWidth >> 1; @@ -58,8 +58,8 @@ namespace ImageProcessor.Filters float gY = 0; float bY = 0; - // Apply each matrix multiplier to the color components for each pixel. - for (int fy = 0; fy < kernelYHeight; fy++) + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelYHeight; fy++) { int fyr = fy - radiusY; int offsetY = y + fyr; @@ -98,7 +98,8 @@ namespace ImageProcessor.Filters float green = (float)Math.Sqrt((gX * gX) + (gY * gY)); float blue = (float)Math.Sqrt((bX * bX) + (bY * bY)); - target[x, y] = new Color(red, green, blue); + Color targetColor = target[x, y]; + target[x, y] = new Color(red, green, blue, targetColor.A); } } }); diff --git a/src/ImageProcessor/Filters/Convolution/Convolution2PassFilter.cs b/src/ImageProcessor/Filters/Convolution/Convolution2PassFilter.cs new file mode 100644 index 000000000..2f374b1d8 --- /dev/null +++ b/src/ImageProcessor/Filters/Convolution/Convolution2PassFilter.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Filters +{ + using System.Threading.Tasks; + + /// + /// Defines a filter that uses a matrix to perform convolution across two dimensions against an image. + /// + public abstract class Convolution2PassFilter : ParallelImageProcessor + { + /// + /// Gets the horizontal gradient operator. + /// + public abstract float[,] KernelX { get; } + + /// + /// Gets the vertical gradient operator. + /// + public abstract float[,] KernelY { get; } + + /// + protected override void Apply( + ImageBase target, + ImageBase source, + Rectangle targetRectangle, + Rectangle sourceRectangle, + int startY, + int endY) + { + float[,] kernelX = this.KernelX; + float[,] kernelY = this.KernelY; + + ImageBase firstPass = new Image(source.Width, source.Height); + this.ApplyConvolution(firstPass, source, sourceRectangle, startY, endY, kernelX); + this.ApplyConvolution(target, firstPass, sourceRectangle, startY, endY, kernelY); + } + + /// + /// Applies the process to the specified portion of the specified at the specified location + /// and with the specified size. + /// + /// Target image to apply the process to. + /// The source image. Cannot be null. + /// + /// The structure that specifies the portion of the image object to draw. + /// + /// The index of the row within the source image to start processing. + /// The index of the row within the source image to end processing. + /// The kernel operator. + private void ApplyConvolution( + ImageBase target, + ImageBase source, + Rectangle sourceRectangle, + int startY, + int endY, + float[,] kernel) + { + int kernelHeight = kernel.GetLength(0); + int kernelWidth = kernel.GetLength(1); + int radiusY = kernelHeight >> 1; + int radiusX = kernelWidth >> 1; + + int sourceY = sourceRectangle.Y; + int sourceBottom = sourceRectangle.Bottom; + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + int maxY = sourceBottom - 1; + int maxX = endX - 1; + + Parallel.For( + startY, + endY, + y => + { + if (y >= sourceY && y < sourceBottom) + { + for (int x = startX; x < endX; x++) + { + float rX = 0; + float gX = 0; + float bX = 0; + + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelHeight; fy++) + { + int fyr = fy - radiusY; + int offsetY = y + fyr; + + offsetY = offsetY.Clamp(0, maxY); + + for (int fx = 0; fx < kernelWidth; fx++) + { + int fxr = fx - radiusX; + int offsetX = x + fxr; + + offsetX = offsetX.Clamp(0, maxX); + + Color currentColor = source[offsetX, offsetY]; + float r = currentColor.R; + float g = currentColor.G; + float b = currentColor.B; + + rX += kernel[fy, fx] * r; + gX += kernel[fy, fx] * g; + bX += kernel[fy, fx] * b; + } + } + + float red = rX; + float green = gX; + float blue = bX; + + Color targetColor = target[x, y]; + target[x, y] = new Color(red, green, blue, targetColor.A); + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs b/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs index 44df243ee..c142b62cd 100644 --- a/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs +++ b/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs @@ -9,9 +9,8 @@ namespace ImageProcessor.Filters /// /// Applies a Gaussian blur to the image. - /// TODO: Something is not right here. The output blur is more like a motion blur. /// - public class GuassianBlur : Convolution2DFilter + public class GuassianBlur : Convolution2PassFilter { /// /// The maximum size of the kernal in either direction. @@ -19,9 +18,9 @@ namespace ImageProcessor.Filters private readonly int kernelSize; /// - /// The standard deviation (weight) + /// The spread of the blur. /// - private readonly float standardDeviation; + private readonly float sigma; /// /// The vertical kernel @@ -36,13 +35,13 @@ namespace ImageProcessor.Filters /// /// Initializes a new instance of the class. /// - /// - /// The standard deviation 'sigma' value for calculating Gaussian curves. + /// + /// The 'sigma' value representing the weight of the blur. /// - public GuassianBlur(float standardDeviation = 3f) + public GuassianBlur(float sigma = 3f) { - this.kernelSize = ((int)Math.Ceiling(standardDeviation) * 2) + 1; - this.standardDeviation = standardDeviation; + this.kernelSize = ((int)Math.Ceiling(sigma) * 2) + 1; + this.sigma = sigma; } /// @@ -51,6 +50,9 @@ namespace ImageProcessor.Filters /// public override float[,] KernelY => this.kernelY; + /// + public override int Parallelism => 1; + /// protected override void OnApply(Rectangle targetRectangle, Rectangle sourceRectangle) { @@ -73,6 +75,7 @@ namespace ImageProcessor.Filters private float[,] CreateGaussianKernel(bool horizontal) { int size = this.kernelSize; + float weight = this.sigma; float[,] kernel = horizontal ? new float[1, size] : new float[size, 1]; float sum = 0.0f; @@ -80,7 +83,7 @@ namespace ImageProcessor.Filters for (int i = 0; i < size; i++) { float x = i - midpoint; - float gx = this.Gaussian(x); + float gx = ImageMaths.Gaussian(x, weight); sum += gx; if (horizontal) { @@ -110,25 +113,5 @@ namespace ImageProcessor.Filters return kernel; } - - /// - /// Implementation of 1D Gaussian G(x) function - /// - /// The x provided to G(x) - /// The Gaussian G(x) - private float Gaussian(float x) - { - const float Numerator = 1.0f; - float deviation = this.standardDeviation; - float denominator = (float)(Math.Sqrt(2 * Math.PI) * deviation); - - float exponentNumerator = -x * x; - float exponentDenominator = (float)(2 * Math.Pow(deviation, 2)); - - float left = Numerator / denominator; - float right = (float)Math.Exp(exponentNumerator / exponentDenominator); - - return left * right; - } } } diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index c56f3b26e..19f6584e8 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -50,6 +50,7 @@ + diff --git a/src/ImageProcessor/project.lock.json.REMOVED.git-id b/src/ImageProcessor/project.lock.json.REMOVED.git-id index dba2656f5..24339fed2 100644 --- a/src/ImageProcessor/project.lock.json.REMOVED.git-id +++ b/src/ImageProcessor/project.lock.json.REMOVED.git-id @@ -1 +1 @@ -eb00c54ee74016c2b70f81963e7e8f83cb2dd54b \ No newline at end of file +3f05708641eb3ed085d4689aae4a960eb067fd16 \ No newline at end of file diff --git a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs index 517738f10..4db94c522 100644 --- a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs @@ -38,7 +38,7 @@ namespace ImageProcessor.Tests //{ "RobertsCross", new RobertsCross() }, //{ "Scharr", new Scharr() }, //{ "Sobel", new Sobel() }, - { "GuassianBlur", new GuassianBlur(5) } + { "GuassianBlur", new GuassianBlur(10) } }; [Theory] diff --git a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs index 45339591e..c6393156d 100644 --- a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs +++ b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs @@ -21,18 +21,20 @@ namespace ImageProcessor.Tests { //"../../TestImages/Formats/Jpg/Backdrop.jpg", //"../../TestImages/Formats/Jpg/Calliphora.jpg", - "../../TestImages/Formats/Jpg/ant.jpg", - "../../TestImages/Formats/Jpg/parachute.jpg", + "../../TestImages/Formats/Jpg/china.jpg", + //"../../TestImages/Formats/Jpg/ant.jpg", + //"../../TestImages/Formats/Jpg/parachute.jpg", //"../../TestImages/Formats/Jpg/lomo.jpg", //"../../TestImages/Formats/Jpg/shaftesbury.jpg", //"../../TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", //"../../TestImages/Formats/Jpg/greyscale.jpg", //"../../TestImages/Formats/Bmp/Car.bmp", + "../../TestImages/Formats/Png/cballs.png", //"../../TestImages/Formats/Png/cmyk.png", //"../../TestImages/Formats/Png/gamma-1.0-or-2.2.png", //"../../TestImages/Formats/Png/splash.png", //"../../TestImages/Formats/Gif/leaf.gif", - "../../TestImages/Formats/Gif/ben2.gif", + //"../../TestImages/Formats/Gif/ben2.gif", //"../../TestImages/Formats/Gif/rings.gif", //"../../TestImages/Formats/Gif/ani2.gif", //"../../TestImages/Formats/Gif/giphy.gif" diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/china.jpg.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/china.jpg.REMOVED.git-id new file mode 100644 index 000000000..b8a8b4908 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/china.jpg.REMOVED.git-id @@ -0,0 +1 @@ +8cee90e94f9f09e04697bee0477bb5059b85c63f \ No newline at end of file diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Png/cballs.png.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Png/cballs.png.REMOVED.git-id new file mode 100644 index 000000000..55cb73356 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Png/cballs.png.REMOVED.git-id @@ -0,0 +1 @@ +58c3318271250961bfac9fe6e9c8b0053e1b8e28 \ No newline at end of file