mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
426 lines
16 KiB
426 lines
16 KiB
// --------------------------------------------------------------------------------------------------------------------
|
|
// <copyright file="Convolution.cs" company="James South">
|
|
// Copyright (c) James South.
|
|
// Licensed under the Apache License, Version 2.0.
|
|
// </copyright>
|
|
// <summary>
|
|
// Provides methods for applying blurring and sharpening effects to an image..
|
|
// </summary>
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
namespace ImageProcessor.Imaging
|
|
{
|
|
using System;
|
|
using System.Drawing;
|
|
using System.Threading.Tasks;
|
|
|
|
using ImageProcessor.Common.Extensions;
|
|
|
|
/// <summary>
|
|
/// Provides methods for applying blurring and sharpening effects to an image..
|
|
/// </summary>
|
|
public class Convolution
|
|
{
|
|
/// <summary>
|
|
/// The standard deviation 'sigma' value for calculating Gaussian curves.
|
|
/// </summary>
|
|
private readonly double standardDeviation = 1.4;
|
|
|
|
/// <summary>
|
|
/// Whether to use dynamic divider for edges.
|
|
/// </summary>
|
|
private bool useDynamicDividerForEdges = true;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Convolution"/> class.
|
|
/// </summary>
|
|
public Convolution()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Convolution"/> class.
|
|
/// </summary>
|
|
/// <param name="standardDeviation">
|
|
/// The standard deviation.
|
|
/// </param>
|
|
public Convolution(double standardDeviation)
|
|
{
|
|
this.standardDeviation = standardDeviation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the threshold to add to the weighted sum.
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Specifies the threshold value, which is added to each weighted
|
|
/// sum of pixels.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// </summary>
|
|
public int Threshold { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the value used to divide convolution; the weighted sum
|
|
/// of pixels is divided by this value.
|
|
/// <remarks>
|
|
/// <para>If not set this value will be automatically calculated.</para>
|
|
/// </remarks>
|
|
/// </summary>
|
|
public double Divider { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to use the dynamic divider for edges.
|
|
/// <remarks>
|
|
/// <para>
|
|
/// If it is set to <see langword="false" />, then the same divider, specified by <see cref="Divider" />
|
|
/// property or calculated automatically, will be applied both for non-edge regions
|
|
/// and for edge regions. If the value is set to <see langword="true" />, then the dynamically
|
|
/// calculated divider will be used for edge regions. This is calculated from the sum of the kernel
|
|
/// elements used at that region, which are taken into account for particular processed pixel
|
|
/// (elements, which are not outside image).
|
|
/// </para>
|
|
/// <para>Default value is set to <see langword="true" />.</para>
|
|
/// </remarks>
|
|
/// </summary>
|
|
public bool UseDynamicDividerForEdges
|
|
{
|
|
get
|
|
{
|
|
return this.useDynamicDividerForEdges;
|
|
}
|
|
|
|
set
|
|
{
|
|
this.useDynamicDividerForEdges = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function
|
|
/// </summary>
|
|
/// <param name="kernelSize">Kernel Size</param>
|
|
/// <returns>A Gaussian Kernel with the given size and deviation.</returns>
|
|
public double[,] CreateGaussianKernel(int kernelSize)
|
|
{
|
|
double[,] kernel = new double[kernelSize, 1];
|
|
double sum = 0.0d;
|
|
|
|
int midpoint = kernelSize / 2;
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
int x = i - midpoint;
|
|
double gx = this.Gaussian(x);
|
|
sum += gx;
|
|
kernel[i, 0] = gx;
|
|
}
|
|
|
|
// Normalise kernel so that the sum of all weights equals 1
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
kernel[i, 0] = kernel[i, 0] / sum;
|
|
}
|
|
|
|
return kernel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function
|
|
/// </summary>
|
|
/// <param name="kernelSize">Kernel Size</param>
|
|
/// <returns>A Gaussian Kernel with the given size and deviation.</returns>
|
|
public double[,] CreateGaussianKernel2D(int kernelSize)
|
|
{
|
|
double[,] kernel = new double[kernelSize, kernelSize];
|
|
|
|
int midpoint = kernelSize / 2;
|
|
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
int x = i - midpoint;
|
|
|
|
for (int j = 0; j < kernelSize; j++)
|
|
{
|
|
int y = j - midpoint;
|
|
double gxy = this.Gaussian2D(x, y);
|
|
kernel[i, j] = gxy;
|
|
}
|
|
}
|
|
|
|
return kernel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function for
|
|
/// blurring images.
|
|
/// </summary>
|
|
/// <param name="kernelSize">Kernel Size</param>
|
|
/// <returns>A Gaussian Kernel with the given size.</returns>
|
|
public double[,] CreateGuassianBlurFilter(int kernelSize)
|
|
{
|
|
// Create kernel
|
|
double[,] kernel = this.CreateGaussianKernel2D(kernelSize);
|
|
double 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.
|
|
double[,] intKernel = new double[kernelSize, kernelSize];
|
|
int divider = 0;
|
|
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
for (int j = 0; j < kernelSize; j++)
|
|
{
|
|
double 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];
|
|
}
|
|
}
|
|
|
|
// Update filter
|
|
this.Divider = divider;
|
|
return intKernel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a 2 dimensional Gaussian kernel using the Gaussian G(x y) function for
|
|
/// sharpening images.
|
|
/// </summary>
|
|
/// <param name="kernelSize">Kernel Size</param>
|
|
/// <returns>A Gaussian Kernel with the given size.</returns>
|
|
public double[,] CreateGuassianSharpenFilter(int kernelSize)
|
|
{
|
|
// Create kernel
|
|
double[,] kernel = this.CreateGaussianKernel2D(kernelSize);
|
|
double min = kernel[0, 0];
|
|
|
|
// integer kernel
|
|
double[,] intKernel = new double[kernelSize, kernelSize];
|
|
int sum = 0;
|
|
int divider = 0;
|
|
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
for (int j = 0; j < kernelSize; j++)
|
|
{
|
|
double v = kernel[i, j] / min;
|
|
|
|
if (v > ushort.MaxValue)
|
|
{
|
|
v = ushort.MaxValue;
|
|
}
|
|
|
|
intKernel[i, j] = (int)v;
|
|
|
|
// Collect the sum.
|
|
sum += (int)intKernel[i, j];
|
|
}
|
|
}
|
|
|
|
// Recalculate the kernel.
|
|
int center = kernelSize >> 1;
|
|
|
|
for (int i = 0; i < kernelSize; i++)
|
|
{
|
|
for (int j = 0; j < kernelSize; j++)
|
|
{
|
|
if ((i == center) && (j == center))
|
|
{
|
|
// Calculate central value
|
|
intKernel[i, j] = (2 * sum) - intKernel[i, j];
|
|
}
|
|
else
|
|
{
|
|
// invert value
|
|
intKernel[i, j] = -intKernel[i, j];
|
|
}
|
|
|
|
// Collect the divider
|
|
divider += (int)intKernel[i, j];
|
|
}
|
|
}
|
|
|
|
// Update filter
|
|
this.Divider = divider;
|
|
return intKernel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes the given kernel to produce an array of pixels representing a bitmap.
|
|
/// </summary>
|
|
/// <param name="source">The image to process.</param>
|
|
/// <param name="kernel">The Gaussian kernel to use when performing the method</param>
|
|
/// <returns>A processed bitmap.</returns>
|
|
public Bitmap ProcessKernel(Bitmap source, double[,] kernel)
|
|
{
|
|
int width = source.Width;
|
|
int height = source.Height;
|
|
Bitmap destination = new Bitmap(width, height);
|
|
destination.SetResolution(source.HorizontalResolution, source.VerticalResolution);
|
|
|
|
using (FastBitmap sourceBitmap = new FastBitmap(source))
|
|
{
|
|
using (FastBitmap destinationBitmap = new FastBitmap(destination))
|
|
{
|
|
int kernelLength = kernel.GetLength(0);
|
|
int radius = kernelLength >> 1;
|
|
int kernelSize = kernelLength * kernelLength;
|
|
int threshold = this.Threshold;
|
|
|
|
// For each line
|
|
Parallel.For(
|
|
0,
|
|
height,
|
|
y =>
|
|
{
|
|
// For each pixel
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
// The number of kernel elements taken into account
|
|
int processedKernelSize;
|
|
|
|
// Colour sums
|
|
double blue;
|
|
double alpha;
|
|
double divider;
|
|
double green;
|
|
double red = green = blue = alpha = divider = processedKernelSize = 0;
|
|
|
|
// For each kernel row
|
|
for (int i = 0; i < kernelLength; i++)
|
|
{
|
|
int ir = i - radius;
|
|
int offsetY = y + ir;
|
|
|
|
// Skip the current row
|
|
if (offsetY < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Outwith the current bounds so break.
|
|
if (offsetY >= height)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// For each kernel column
|
|
for (int j = 0; j < kernelLength; j++)
|
|
{
|
|
int jr = j - radius;
|
|
int offsetX = x + jr;
|
|
|
|
// Skip the column
|
|
if (offsetX < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (offsetX < width)
|
|
{
|
|
// ReSharper disable once AccessToDisposedClosure
|
|
Color color = sourceBitmap.GetPixel(offsetX, offsetY);
|
|
double k = kernel[i, j];
|
|
divider += k;
|
|
|
|
red += k * color.R;
|
|
green += k * color.G;
|
|
blue += k * color.B;
|
|
alpha += k * color.A;
|
|
|
|
processedKernelSize++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to see if all kernel elements were processed
|
|
if (processedKernelSize == kernelSize)
|
|
{
|
|
// All kernel elements are processed; we are not on the edge.
|
|
divider = this.Divider;
|
|
}
|
|
else
|
|
{
|
|
// We are on an edge; do we need to use dynamic divider or not?
|
|
if (!this.UseDynamicDividerForEdges)
|
|
{
|
|
// Apply the set divider.
|
|
divider = this.Divider;
|
|
}
|
|
}
|
|
|
|
// Check and apply the divider
|
|
if ((long)divider != 0)
|
|
{
|
|
red /= divider;
|
|
green /= divider;
|
|
blue /= divider;
|
|
alpha /= divider;
|
|
}
|
|
|
|
// Add any applicable threshold.
|
|
red += threshold;
|
|
green += threshold;
|
|
blue += threshold;
|
|
alpha += threshold;
|
|
|
|
// ReSharper disable once AccessToDisposedClosure
|
|
destinationBitmap.SetPixel(x, y, Color.FromArgb(alpha.ToByte(), red.ToByte(), green.ToByte(), blue.ToByte()));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return destination;
|
|
}
|
|
|
|
#region Private
|
|
/// <summary>
|
|
/// Implementation of 1D Gaussian G(x) function
|
|
/// </summary>
|
|
/// <param name="x">The x provided to G(x)</param>
|
|
/// <returns>The Gaussian G(x)</returns>
|
|
private double Gaussian(double x)
|
|
{
|
|
const double Numerator = 1.0;
|
|
double denominator = Math.Sqrt(2 * Math.PI) * this.standardDeviation;
|
|
|
|
double exponentNumerator = -x * x;
|
|
double exponentDenominator = 2 * Math.Pow(this.standardDeviation, 2);
|
|
|
|
double left = Numerator / denominator;
|
|
double right = Math.Exp(exponentNumerator / exponentDenominator);
|
|
|
|
return left * right;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implementation of 2D Gaussian G(x) function
|
|
/// </summary>
|
|
/// <param name="x">The x provided to G(x y)</param>
|
|
/// <param name="y">The y provided to G(x y)</param>
|
|
/// <returns>The Gaussian G(x y)</returns>
|
|
private double Gaussian2D(double x, double y)
|
|
{
|
|
const double Numerator = 1.0;
|
|
double denominator = (2 * Math.PI) * Math.Pow(this.standardDeviation, 2);
|
|
|
|
double exponentNumerator = (-x * x) + (-y * y);
|
|
double exponentDenominator = 2 * Math.Pow(this.standardDeviation, 2);
|
|
|
|
double left = Numerator / denominator;
|
|
double right = Math.Exp(exponentNumerator / exponentDenominator);
|
|
|
|
return left * right;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
|