From d4073f87cd1db308b68dd75eb773611d1ff1a6d5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Nov 2015 07:55:29 +1100 Subject: [PATCH] Playing with 1D Gaussian transforms Former-commit-id: 491e73ef2b4c46b935abbd9d2778a1c09388a6b8 Former-commit-id: 147535c84f3593676be6830ed231e3f11e43d42e Former-commit-id: aaec7537352e24c285afe67248f8be0df97df371 --- .../Convolution/Convolution2DFilter - Copy.cs | 99 ++++++++++++ .../Filters/Convolution/GuassianBlur.cs | 145 ++++++++++++------ .../project.lock.json.REMOVED.git-id | 2 +- .../Processors/Filters/FilterTests.cs | 53 ++++--- .../Processors/ProcessorTestBase.cs | 29 ++-- .../Formats/Jpg/ant.jpg.REMOVED.git-id | 1 + 6 files changed, 244 insertions(+), 85 deletions(-) create mode 100644 src/ImageProcessor/Filters/Convolution/Convolution2DFilter - Copy.cs create mode 100644 tests/ImageProcessor.Tests/TestImages/Formats/Jpg/ant.jpg.REMOVED.git-id diff --git a/src/ImageProcessor/Filters/Convolution/Convolution2DFilter - Copy.cs b/src/ImageProcessor/Filters/Convolution/Convolution2DFilter - Copy.cs new file mode 100644 index 000000000..d67b86462 --- /dev/null +++ b/src/ImageProcessor/Filters/Convolution/Convolution2DFilter - Copy.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor.Filters +{ + using System; + using System.Threading.Tasks; + + /// + /// Defines a filter that uses a matrix to perform convolution across two dimensions against an image. + /// + public abstract class Convolution2DFilter : 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; + int kernelYLength = kernelY.GetLength(0); + int kernelXLength = kernelX.GetLength(0); + int radiusY = kernelYLength >> 1; + int radiusX = kernelXLength >> 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; + float rY = 0; + float gY = 0; + float bY = 0; + + // Apply each matrix multiplier to the color components for each pixel. + for (int fy = 0; fy < kernelYLength; fy++) + { + int fyr = fy - radiusY; + int offsetY = y + fyr; + + offsetY = offsetY.Clamp(0, maxY); + + for (int fx = 0; fx < kernelXLength; 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 += kernelX[fx, fy] * r; + gX += kernelX[fx, fy] * g; + bX += kernelX[fx, fy] * b; + + rY += kernelY[fy, fx] * r; + gY += kernelY[fy, fx] * g; + bY += kernelY[fy, fx] * b; + } + } + + float red = (float)Math.Sqrt((rX * rX) + (rY * rY)); + 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); + } + } + }); + } + } +} diff --git a/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs b/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs index 37fa90173..d760221e3 100644 --- a/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs +++ b/src/ImageProcessor/Filters/Convolution/GuassianBlur.cs @@ -1,14 +1,16 @@ namespace ImageProcessor.Filters.Convolution { using System; - using System.Runtime.CompilerServices; - public class GuassianBlur : ConvolutionFilter + public class GuassianBlur : Convolution2DFilter { private int kernelSize; private float standardDeviation; + private float[,] kernelY; + private float[,] kernelX; + /// /// Initializes a new instance of the class. /// @@ -18,81 +20,132 @@ /// /// The standard deviation 'sigma' value for calculating Gaussian curves. /// - public GuassianBlur(int size, float standardDeviation) + public GuassianBlur(int size, float standardDeviation = 1.4f) { this.kernelSize = size; this.standardDeviation = standardDeviation; } - public override float[,] KernelX { get; } + /// + public override float[,] KernelX => this.kernelX; + + /// + public override float[,] KernelY => this.kernelY; + + /// + protected override void OnApply(Rectangle targetRectangle, Rectangle sourceRectangle) + { + if (this.kernelY == null) + { + this.kernelY = this.CreateGaussianKernel(false); + } + + if (this.kernelX == null) + { + this.kernelX = this.CreateGaussianKernel(true); + } + } /// - /// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function for - /// blurring images. + /// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function /// - /// Kernel Size - /// A Gaussian Kernel with the given size. - private float[,] CreateGuassianBlurFilter() + private void CreateGaussianKernel2D() { - // Create kernel int size = this.kernelSize; - float[,] kernel = this.CreateGaussianKernel2D(size); - float min = kernel[0, 0]; - - // Convert to integer blurring kernel. First of all the integer kernel is calculated from Kernel2D - // by dividing all elements by the element with the smallest value. - float[,] intKernel = new float[size, size]; - int divider = 0; + float[,] kernel = new float[size, size]; + int midpoint = size / 2; + float sum = 0; for (int i = 0; i < size; i++) { + int x = i - midpoint; + for (int j = 0; j < size; j++) { - float v = kernel[i, j] / min; - - if (v > ushort.MaxValue) - { - v = ushort.MaxValue; - } - - intKernel[i, j] = (int)v; - - // Collect the divider - divider += (int)intKernel[i, j]; + int y = j - midpoint; + float gxy = this.Gaussian2D(x, y); + sum += gxy; + kernel[i, j] = gxy; } } - // Update filter - //this.Divider = divider; - return intKernel; + // Normalise kernel so that the sum of all weights equals 1 + //for (int i = 0; i < size; i++) + //{ + // for (int j = 0; j < size; j++) + // { + // kernel[i, 0] = kernel[i, j] / sum; + // } + //} + + this.kernelY = kernel; } /// - /// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function + /// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function /// - /// Kernel Size - /// A Gaussian Kernel with the given size and deviation. - public float[,] CreateGaussianKernel2D(int kernelSize) + /// The + private float[,] CreateGaussianKernel(bool horizontal) { - float[,] kernel = new float[kernelSize, kernelSize]; - - int midpoint = kernelSize / 2; + int size = this.kernelSize; + float[,] kernel = horizontal ? new float[1, size] : new float[size, 1]; + float sum = 0.0f; - for (int i = 0; i < kernelSize; i++) + int midpoint = size / 2; + for (int i = 0; i < size; i++) { int x = i - midpoint; + float gx = this.Gaussian(x); + sum += gx; + if (horizontal) + { + kernel[0, i] = gx; + } + else + { + kernel[i, 0] = gx; + } + } - for (int j = 0; j < kernelSize; j++) + // Normalise kernel so that the sum of all weights equals 1 + if (horizontal) + { + for (int i = 0; i < size; i++) { - int y = j - midpoint; - float gxy = this.Gaussian2D(x, y); - kernel[i, j] = gxy; + kernel[0, i] = kernel[0, i] / sum; + } + } + else + { + for (int i = 0; i < size; i++) + { + kernel[i, 0] = kernel[i, 0] / sum; } } 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; + } + /// /// Implementation of 2D Gaussian G(x) function /// @@ -102,10 +155,12 @@ private float Gaussian2D(float x, float y) { const float Numerator = 1.0f; - float denominator = (float)((2 * Math.PI) * Math.Pow(this.standardDeviation, 2)); + float deviation = this.standardDeviation; + double pow = Math.Pow(deviation, 2); + float denominator = (float)((2 * Math.PI) * pow); float exponentNumerator = (-x * x) + (-y * y); - float exponentDenominator = (float)(2 * Math.Pow(this.standardDeviation, 2)); + float exponentDenominator = (float)(2 * pow); float left = Numerator / denominator; float right = (float)Math.Exp(exponentNumerator / exponentDenominator); 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 c5fb051d7..204210249 100644 --- a/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs +++ b/tests/ImageProcessor.Tests/Processors/Filters/FilterTests.cs @@ -3,8 +3,10 @@ namespace ImageProcessor.Tests { using System.Diagnostics; using System.IO; + using System.Numerics; using ImageProcessor.Filters; + using ImageProcessor.Filters.Convolution; using Xunit; @@ -12,31 +14,32 @@ namespace ImageProcessor.Tests { public static readonly TheoryData Filters = new TheoryData { - { "Brightness-50", new Brightness(50) }, - { "Brightness--50", new Brightness(-50) }, - { "Contrast-50", new Contrast(50) }, - { "Contrast--50", new Contrast(-50) }, - { "Blend", new Blend(new Image(File.OpenRead("../../TestImages/Formats/Bmp/Car.bmp")),15)}, - { "Saturation-50", new Saturation(50) }, - { "Saturation--50", new Saturation(-50) }, - { "Alpha--50", new Alpha(50) }, - { "Invert", new Invert() }, - { "Sepia", new Sepia() }, - { "BlackWhite", new BlackWhite() }, - { "Lomograph", new Lomograph() }, - { "Polaroid", new Polaroid() }, - { "Kodachrome", new Kodachrome() }, - { "GreyscaleBt709", new GreyscaleBt709() }, - { "GreyscaleBt601", new GreyscaleBt601() }, - { "Kayyali", new Kayyali() }, - { "Kirsch", new Kirsch() }, - { "Laplacian3X3", new Laplacian3X3() }, - { "Laplacian5X5", new Laplacian5X5() }, - { "LaplacianOfGaussian", new LaplacianOfGaussian() }, - { "Prewitt", new Prewitt() }, - { "RobertsCross", new RobertsCross() }, - { "Scharr", new Scharr() }, - { "Sobel", new Sobel() } + //{ "Brightness-50", new Brightness(50) }, + //{ "Brightness--50", new Brightness(-50) }, + //{ "Contrast-50", new Contrast(50) }, + //{ "Contrast--50", new Contrast(-50) }, + //{ "Blend", new Blend(new Image(File.OpenRead("../../TestImages/Formats/Bmp/Car.bmp")),15)}, + //{ "Saturation-50", new Saturation(50) }, + //{ "Saturation--50", new Saturation(-50) }, + //{ "Alpha--50", new Alpha(50) }, + //{ "Invert", new Invert() }, + //{ "Sepia", new Sepia() }, + //{ "BlackWhite", new BlackWhite() }, + //{ "Lomograph", new Lomograph() }, + //{ "Polaroid", new Polaroid() }, + //{ "Kodachrome", new Kodachrome() }, + //{ "GreyscaleBt709", new GreyscaleBt709() }, + //{ "GreyscaleBt601", new GreyscaleBt601() }, + //{ "Kayyali", new Kayyali() }, + //{ "Kirsch", new Kirsch() }, + //{ "Laplacian3X3", new Laplacian3X3() }, + //{ "Laplacian5X5", new Laplacian5X5() }, + //{ "LaplacianOfGaussian", new LaplacianOfGaussian() }, + //{ "Prewitt", new Prewitt() }, + //{ "RobertsCross", new RobertsCross() }, + //{ "Scharr", new Scharr() }, + //{ "Sobel", new Sobel() }, + { "GuassianBlur", new GuassianBlur(20) } }; [Theory] diff --git a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs index 0e33d5438..7ba500e5e 100644 --- a/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs +++ b/tests/ImageProcessor.Tests/Processors/ProcessorTestBase.cs @@ -19,20 +19,21 @@ namespace ImageProcessor.Tests /// public static readonly List Files = new List { - "../../TestImages/Formats/Jpg/Backdrop.jpg", - "../../TestImages/Formats/Jpg/Calliphora.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/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/rings.gif", - "../../TestImages/Formats/Gif/ani2.gif", - "../../TestImages/Formats/Gif/giphy.gif" + //"../../TestImages/Formats/Jpg/Backdrop.jpg", + //"../../TestImages/Formats/Jpg/Calliphora.jpg", + "../../TestImages/Formats/Jpg/ant.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/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/rings.gif", + //"../../TestImages/Formats/Gif/ani2.gif", + //"../../TestImages/Formats/Gif/giphy.gif" }; } } diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/ant.jpg.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/ant.jpg.REMOVED.git-id new file mode 100644 index 000000000..1485b2316 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Jpg/ant.jpg.REMOVED.git-id @@ -0,0 +1 @@ +1da0ed59ed220f32d72b01e6cbb9c73a7f3b9b35 \ No newline at end of file