From c8e88a4967a357f88e45461a20017177b838afc9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 29 Jul 2016 11:29:47 +1000 Subject: [PATCH] Guassian Blur/Sharpen Former-commit-id: 8a5b4cf3f2f77979e055f7244a07152f3e7911b3 Former-commit-id: 3c88eca8695868dafeb4478865530b94a4fff195 Former-commit-id: e31d42ca178b55324a3700280a67fc7acd90f0a8 --- .../Filters/GuassianBlur.cs | 60 ++++++ .../Filters/GuassianSharpen.cs | 60 ++++++ .../Convolution/Convolution2DFilter.cs | 2 + .../Convolution/Convolution2PassFilter.cs | 108 +++++++++++ .../Convolution/GuassianBlurProcessor.cs | 144 ++++++++++++++ .../Convolution/GuassianSharpenProcessor.cs | 182 ++++++++++++++++++ .../Processors/Filters/GuassianBlurTest.cs | 47 +++++ .../Processors/Filters/GuassianSharpenTest.cs | 47 +++++ 8 files changed, 650 insertions(+) create mode 100644 src/ImageProcessorCore/Filters/GuassianBlur.cs create mode 100644 src/ImageProcessorCore/Filters/GuassianSharpen.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2PassFilter.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/Convolution/GuassianBlurProcessor.cs create mode 100644 src/ImageProcessorCore/Filters/Processors/Convolution/GuassianSharpenProcessor.cs create mode 100644 tests/ImageProcessorCore.Tests/Processors/Filters/GuassianBlurTest.cs create mode 100644 tests/ImageProcessorCore.Tests/Processors/Filters/GuassianSharpenTest.cs diff --git a/src/ImageProcessorCore/Filters/GuassianBlur.cs b/src/ImageProcessorCore/Filters/GuassianBlur.cs new file mode 100644 index 0000000000..a57f43b2c2 --- /dev/null +++ b/src/ImageProcessorCore/Filters/GuassianBlur.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using Processors; + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Applies a Guassian blur to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image GuassianBlur(this Image source, float sigma = 3f, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + return GuassianBlur(source, sigma, source.Bounds, progressHandler); + } + + /// + /// Applies a Guassian blur to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image GuassianBlur(this Image source, float sigma, Rectangle rectangle, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + GuassianBlurProcessor processor = new GuassianBlurProcessor(sigma); + processor.OnProgress += progressHandler; + + try + { + return source.Process(rectangle, processor); + } + finally + { + processor.OnProgress -= progressHandler; + } + } + } +} diff --git a/src/ImageProcessorCore/Filters/GuassianSharpen.cs b/src/ImageProcessorCore/Filters/GuassianSharpen.cs new file mode 100644 index 0000000000..6fb888cdfc --- /dev/null +++ b/src/ImageProcessorCore/Filters/GuassianSharpen.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using Processors; + + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Applies a Guassian sharpening filter to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image GuassianSharpen(this Image source, float sigma = 3f, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + return GuassianSharpen(source, sigma, source.Bounds, progressHandler); + } + + /// + /// Applies a Guassian sharpening filter to the image. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// A delegate which is called as progress is made processing the image. + /// The . + public static Image GuassianSharpen(this Image source, float sigma, Rectangle rectangle, ProgressEventHandler progressHandler = null) + where T : IPackedVector + where TP : struct + { + GuassianSharpenProcessor processor = new GuassianSharpenProcessor(sigma); + processor.OnProgress += progressHandler; + + try + { + return source.Process(rectangle, processor); + } + finally + { + processor.OnProgress -= progressHandler; + } + } + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2DFilter.cs b/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2DFilter.cs index cc8ac82e35..3638c1384f 100644 --- a/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2DFilter.cs +++ b/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2DFilter.cs @@ -12,6 +12,8 @@ namespace ImageProcessorCore.Processors /// /// Defines a filter that uses two one-dimensional matrices to perform convolution against an image. /// + /// The pixel format. + /// The packed format. long, float. public abstract class Convolution2DFilter : ImageProcessor where T : IPackedVector where TP : struct diff --git a/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2PassFilter.cs b/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2PassFilter.cs new file mode 100644 index 0000000000..231929bc2a --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/Convolution/Convolution2PassFilter.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System.Numerics; + using System.Threading.Tasks; + + /// + /// Defines a filter that uses two one-dimensional matrices to perform two-pass convolution against an image. + /// + /// The pixel format. + /// The packed format. long, float. + public abstract class Convolution2PassFilter : ImageProcessor + where T : IPackedVector + where TP : struct + { + /// + /// 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 sourceBottom = sourceRectangle.Bottom; + int startX = sourceRectangle.X; + int endX = sourceRectangle.Right; + int maxY = sourceBottom - 1; + int maxX = endX - 1; + + using (IPixelAccessor sourcePixels = source.Lock()) + using (IPixelAccessor targetPixels = target.Lock()) + { + Parallel.For( + startY, + endY, + y => + { + for (int x = startX; x < endX; x++) + { + Vector4 destination = new Vector4(); + + // 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); + + Vector4 currentColor = sourcePixels[offsetX, offsetY].ToVector4(); + destination += kernel[fy, fx] * currentColor; + } + } + + T packed = default(T); + packed.PackVector(destination); + targetPixels[x, y] = packed; + } + + this.OnRowProcessed(); + }); + } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianBlurProcessor.cs b/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianBlurProcessor.cs new file mode 100644 index 0000000000..23d6d94378 --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianBlurProcessor.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System; + + /// + /// Applies a Gaussian blur filter to the image. + /// + /// The pixel format. + /// The packed format. long, float. + public class GuassianBlurProcessor : Convolution2PassFilter + where T : IPackedVector + where TP : struct + { + /// + /// The maximum size of the kernal in either direction. + /// + private readonly int kernelSize; + + /// + /// The spread of the blur. + /// + private readonly float sigma; + + /// + /// The vertical kernel + /// + private float[,] kernelY; + + /// + /// The horizontal kernel + /// + private float[,] kernelX; + + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + public GuassianBlurProcessor(float sigma = 3f) + { + this.kernelSize = ((int)Math.Ceiling(sigma) * 2) + 1; + this.sigma = sigma; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public GuassianBlurProcessor(int radius) + { + this.kernelSize = (radius * 2) + 1; + this.sigma = radius; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + public GuassianBlurProcessor(float sigma, int radius) + { + this.kernelSize = (radius * 2) + 1; + this.sigma = sigma; + } + + /// + public override float[,] KernelX => this.kernelX; + + /// + public override float[,] KernelY => this.kernelY; + + /// + protected override void OnApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle) + { + if (this.kernelY == null) + { + this.kernelY = this.CreateGaussianKernel(false); + } + + if (this.kernelX == null) + { + this.kernelX = this.CreateGaussianKernel(true); + } + } + + /// + /// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function + /// + /// Whether to calculate a horizontal kernel. + /// The + 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; + + float midpoint = (size - 1) / 2f; + for (int i = 0; i < size; i++) + { + float x = i - midpoint; + float gx = ImageMaths.Gaussian(x, weight); + sum += gx; + if (horizontal) + { + kernel[0, i] = gx; + } + else + { + kernel[i, 0] = gx; + } + } + + // Normalise kernel so that the sum of all weights equals 1 + if (horizontal) + { + for (int i = 0; i < size; i++) + { + kernel[0, i] = kernel[0, i] / sum; + } + } + else + { + for (int i = 0; i < size; i++) + { + kernel[i, 0] = kernel[i, 0] / sum; + } + } + + return kernel; + } + } +} diff --git a/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianSharpenProcessor.cs b/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianSharpenProcessor.cs new file mode 100644 index 0000000000..edfe594513 --- /dev/null +++ b/src/ImageProcessorCore/Filters/Processors/Convolution/GuassianSharpenProcessor.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Processors +{ + using System; + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The pixel format. + /// The packed format. long, float. + public class GuassianSharpenProcessor : Convolution2PassFilter + where T : IPackedVector + where TP : struct + { + /// + /// The maximum size of the kernal in either direction. + /// + private readonly int kernelSize; + + /// + /// The spread of the blur. + /// + private readonly float sigma; + + /// + /// The vertical kernel + /// + private float[,] kernelY; + + /// + /// The horizontal kernel + /// + private float[,] kernelX; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the sharpening. + /// + public GuassianSharpenProcessor(float sigma = 3f) + { + this.kernelSize = ((int)Math.Ceiling(sigma) * 2) + 1; + this.sigma = sigma; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public GuassianSharpenProcessor(int radius) + { + this.kernelSize = (radius * 2) + 1; + this.sigma = radius; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the sharpen. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + public GuassianSharpenProcessor(float sigma, int radius) + { + this.kernelSize = (radius * 2) + 1; + this.sigma = sigma; + } + + /// + public override float[,] KernelX => this.kernelX; + + /// + public override float[,] KernelY => this.kernelY; + + /// + protected override void OnApply(ImageBase target, ImageBase source, Rectangle targetRectangle, Rectangle sourceRectangle) + { + if (this.kernelY == null) + { + this.kernelY = this.CreateGaussianKernel(false); + } + + if (this.kernelX == null) + { + this.kernelX = this.CreateGaussianKernel(true); + } + } + + /// + /// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function + /// + /// Whether to calculate a horizontal kernel. + /// The + 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; + + float midpoint = (size - 1) / 2f; + for (int i = 0; i < size; i++) + { + float x = i - midpoint; + float gx = ImageMaths.Gaussian(x, weight); + sum += gx; + if (horizontal) + { + kernel[0, i] = gx; + } + else + { + kernel[i, 0] = gx; + } + } + + // Invert the kernel for sharpening. + int midpointRounded = (int)midpoint; + + if (horizontal) + { + for (int i = 0; i < size; i++) + { + if (i == midpointRounded) + { + // Calculate central value + kernel[0, i] = (2f * sum) - kernel[0, i]; + } + else + { + // invert value + kernel[0, i] = -kernel[0, i]; + } + } + } + else + { + for (int i = 0; i < size; i++) + { + if (i == midpointRounded) + { + // Calculate central value + kernel[i, 0] = (2 * sum) - kernel[i, 0]; + } + else + { + // invert value + kernel[i, 0] = -kernel[i, 0]; + } + } + } + + // Normalise kernel so that the sum of all weights equals 1 + if (horizontal) + { + for (int i = 0; i < size; i++) + { + kernel[0, i] = kernel[0, i] / sum; + } + } + else + { + for (int i = 0; i < size; i++) + { + kernel[i, 0] = kernel[i, 0] / sum; + } + } + + return kernel; + } + } +} diff --git a/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianBlurTest.cs b/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianBlurTest.cs new file mode 100644 index 0000000000..667fac55c5 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianBlurTest.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System.IO; + + using Xunit; + + public class GuassianBlurTest : FileTestBase + { + public static readonly TheoryData GuassianBlurValues + = new TheoryData + { + 3 , + 5 , + }; + + [Theory] + [MemberData("GuassianBlurValues")] + public void ImageShouldApplyGuassianBlurFilter(int value) + { + const string path = "TestOutput/GuassianBlur"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + string filename = Path.GetFileNameWithoutExtension(file) + "-" + value + Path.GetExtension(file); + + Image image = new Image(stream); + using (FileStream output = File.OpenWrite($"{path}/{filename}")) + { + image.GuassianBlur(value) + .Save(output); + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianSharpenTest.cs b/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianSharpenTest.cs new file mode 100644 index 0000000000..47316e59e9 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Processors/Filters/GuassianSharpenTest.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using System.IO; + + using Xunit; + + public class GuassianSharpenTest : FileTestBase + { + public static readonly TheoryData GuassianSharpenValues + = new TheoryData + { + 3 , + 5 , + }; + + [Theory] + [MemberData("GuassianSharpenValues")] + public void ImageShouldApplyGuassianSharpenFilter(int value) + { + const string path = "TestOutput/GuassianSharpen"; + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + foreach (string file in Files) + { + using (FileStream stream = File.OpenRead(file)) + { + string filename = Path.GetFileNameWithoutExtension(file) + "-" + value + Path.GetExtension(file); + + Image image = new Image(stream); + using (FileStream output = File.OpenWrite($"{path}/{filename}")) + { + image.GuassianSharpen(value) + .Save(output); + } + } + } + } + } +} \ No newline at end of file