Browse Source

Use correct Kernel dimensions and optimize. Fix #786

af/merge-core
James Jackson-South 7 years ago
parent
commit
8fc5320f0d
  1. 33
      src/ImageSharp/Primitives/DenseMatrix{T}.cs
  2. 18
      src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs
  3. 51
      src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs
  4. 86
      src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs
  5. 24
      tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs

33
src/ImageSharp/Primitives/DenseMatrix{T}.cs

@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Primitives
/// <returns>The <see typeparam="T"/> at the specified position.</returns>
public ref T this[int row, int column]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get
{
this.CheckCoordinates(row, column);
@ -125,7 +125,7 @@ namespace SixLabors.ImageSharp.Primitives
/// <returns>
/// The <see cref="DenseMatrix{T}"/> representation on the source data.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public static implicit operator DenseMatrix<T>(T[,] data) => new DenseMatrix<T>(data);
/// <summary>
@ -135,9 +135,9 @@ namespace SixLabors.ImageSharp.Primitives
/// <returns>
/// The <see cref="T:T[,]"/> representation on the source data.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
#pragma warning disable SA1008 // Opening parenthesis should be spaced correctly
public static implicit operator T[,] (DenseMatrix<T> data)
public static implicit operator T[,] (in DenseMatrix<T> data)
#pragma warning restore SA1008 // Opening parenthesis should be spaced correctly
{
var result = new T[data.Rows, data.Columns];
@ -154,17 +154,38 @@ namespace SixLabors.ImageSharp.Primitives
return result;
}
/// <summary>
/// Transposes the rows and columns of the dense matrix.
/// </summary>
/// <returns>The <see cref="DenseMatrix{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public DenseMatrix<T> Transpose()
{
var result = new DenseMatrix<T>(this.Rows, this.Columns);
for (int y = 0; y < this.Rows; y++)
{
for (int x = 0; x < this.Columns; x++)
{
ref T value = ref result[x, y];
value = this[y, x];
}
}
return result;
}
/// <summary>
/// Fills the matrix with the given value
/// </summary>
/// <param name="value">The value to fill each item with</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public void Fill(T value) => this.Span.Fill(value);
/// <summary>
/// Clears the matrix setting each value to the default value for the element type
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public void Clear() => this.Span.Clear();
/// <summary>

18
src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs

@ -29,8 +29,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
this.Radius = radius;
this.kernelSize = (radius * 2) + 1;
this.KernelX = this.CreateBoxKernel(true);
this.KernelY = this.CreateBoxKernel(false);
this.KernelX = this.CreateBoxKernel();
this.KernelY = this.KernelX.Transpose();
}
/// <summary>
@ -49,24 +49,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
public DenseMatrix<float> KernelY { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
}
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) => new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
/// <summary>
/// Create a 1 dimensional Box kernel.
/// </summary>
/// <param name="horizontal">Whether to calculate a horizontal kernel.</param>
/// <returns>The <see cref="DenseMatrix{T}"/></returns>
private DenseMatrix<float> CreateBoxKernel(bool horizontal)
private DenseMatrix<float> CreateBoxKernel()
{
int size = this.kernelSize;
DenseMatrix<float> kernel = horizontal
? new DenseMatrix<float>(size, 1)
: new DenseMatrix<float>(1, size);
var kernel = new DenseMatrix<float>(size, 1);
kernel.Fill(1.0F / size);
kernel.Fill(1F / size);
return kernel;
}

51
src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs

@ -4,7 +4,6 @@
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
using SixLabors.ImageSharp.Processing.Processors;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution
@ -26,11 +25,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// </summary>
/// <param name="sigma">The 'sigma' value representing the weight of the blur.</param>
public GaussianBlurProcessor(float sigma = 3F)
: this(sigma, (int)MathF.Ceiling(sigma * 3))
{
this.kernelSize = ((int)Math.Ceiling(sigma) * 2) + 1;
this.Sigma = sigma;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
// Kernel radius is calculated using the minimum viable value.
// http://chemaguerra.com/gaussian-filter-radius/
}
/// <summary>
@ -40,11 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The 'radius' value representing the size of the area to sample.
/// </param>
public GaussianBlurProcessor(int radius)
: this(radius / 3F, radius)
{
this.kernelSize = (radius * 2) + 1;
this.Sigma = radius;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
}
/// <summary>
@ -61,8 +56,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
this.kernelSize = (radius * 2) + 1;
this.Sigma = sigma;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
this.KernelX = this.CreateGaussianKernel();
this.KernelY = this.KernelX.Transpose();
}
/// <summary>
@ -82,22 +77,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
}
=> new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
/// <summary>
/// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function
/// </summary>
/// <param name="horizontal">Whether to calculate a horizontal kernel.</param>
/// <returns>The <see cref="DenseMatrix{T}"/></returns>
private DenseMatrix<float> CreateGaussianKernel(bool horizontal)
private DenseMatrix<float> CreateGaussianKernel()
{
int size = this.kernelSize;
float weight = this.Sigma;
DenseMatrix<float> kernel = horizontal
? new DenseMatrix<float>(size, 1)
: new DenseMatrix<float>(1, size);
var kernel = new DenseMatrix<float>(size, 1);
float sum = 0F;
float midpoint = (size - 1) / 2F;
@ -107,30 +97,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
float x = i - midpoint;
float gx = ImageMaths.Gaussian(x, weight);
sum += gx;
if (horizontal)
{
kernel[0, i] = gx;
}
else
{
kernel[i, 0] = gx;
}
kernel[0, i] = gx;
}
// Normalize 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++)
{
for (int i = 0; i < size; i++)
{
kernel[i, 0] = kernel[i, 0] / sum;
}
kernel[0, i] /= sum;
}
return kernel;

86
src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs

@ -27,11 +27,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The 'sigma' value representing the weight of the sharpening.
/// </param>
public GaussianSharpenProcessor(float sigma = 3F)
: this(sigma, (int)MathF.Ceiling(sigma * 3))
{
this.kernelSize = ((int)Math.Ceiling(sigma) * 2) + 1;
this.Sigma = sigma;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
// Kernel radius is calculated using the minimum viable value.
// http://chemaguerra.com/gaussian-filter-radius/
}
/// <summary>
@ -41,11 +40,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// The 'radius' value representing the size of the area to sample.
/// </param>
public GaussianSharpenProcessor(int radius)
: this(radius / 3F, radius)
{
this.kernelSize = (radius * 2) + 1;
this.Sigma = radius;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
}
/// <summary>
@ -62,8 +58,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
{
this.kernelSize = (radius * 2) + 1;
this.Sigma = sigma;
this.KernelX = this.CreateGaussianKernel(true);
this.KernelY = this.CreateGaussianKernel(false);
this.KernelX = this.CreateGaussianKernel();
this.KernelY = this.KernelX.Transpose();
}
/// <summary>
@ -83,91 +79,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
}
=> new Convolution2PassProcessor<TPixel>(this.KernelX, this.KernelY).Apply(source, sourceRectangle, configuration);
/// <summary>
/// Create a 1 dimensional Gaussian kernel using the Gaussian G(x) function
/// </summary>
/// <param name="horizontal">Whether to calculate a horizontal kernel.</param>
/// <returns>The <see cref="DenseMatrix{T}"/></returns>
private DenseMatrix<float> CreateGaussianKernel(bool horizontal)
private DenseMatrix<float> CreateGaussianKernel()
{
int size = this.kernelSize;
float weight = this.Sigma;
DenseMatrix<float> kernel = horizontal
? new DenseMatrix<float>(size, 1)
: new DenseMatrix<float>(1, size);
var kernel = new DenseMatrix<float>(size, 1);
float sum = 0;
float midpoint = (size - 1) / 2f;
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;
}
kernel[0, i] = gx;
}
// Invert the kernel for sharpening.
int midpointRounded = (int)midpoint;
if (horizontal)
for (int i = 0; i < size; i++)
{
for (int i = 0; i < size; i++)
if (i == midpointRounded)
{
if (i == midpointRounded)
{
// Calculate central value
kernel[0, i] = (2F * sum) - kernel[0, i];
}
else
{
// invert value
kernel[0, i] = -kernel[0, i];
}
// Calculate central value
kernel[0, i] = (2F * sum) - kernel[0, i];
}
}
else
{
for (int i = 0; i < size; i++)
else
{
if (i == midpointRounded)
{
// Calculate central value
kernel[i, 0] = (2 * sum) - kernel[i, 0];
}
else
{
// invert value
kernel[i, 0] = -kernel[i, 0];
}
// invert value
kernel[0, i] = -kernel[0, i];
}
}
// Normalize kernel so that the sum of all weights equals 1
if (horizontal)
{
for (int i = 0; i < size; i++)
{
kernel[0, i] /= sum;
}
}
else
for (int i = 0; i < size; i++)
{
for (int i = 0; i < size; i++)
{
kernel[i, 0] /= sum;
}
kernel[0, i] /= sum;
}
return kernel;

24
tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs

@ -106,5 +106,29 @@ namespace SixLabors.ImageSharp.Tests.Primitives
Assert.Equal(0, dense.Data[i]);
}
}
[Fact]
public void DenseMatrixCorrectlyCasts()
{
float[,] actual = new DenseMatrix<float>(FloydSteinbergMatrix);
Assert.Equal(FloydSteinbergMatrix, actual);
}
[Fact]
public void DenseMatrixCanTranspose()
{
var dense = new DenseMatrix<int>(3, 1);
dense[0, 0] = 1;
dense[0, 1] = 2;
dense[0, 2] = 3;
DenseMatrix<int> transposed = dense.Transpose();
Assert.Equal(dense.Columns, transposed.Rows);
Assert.Equal(dense.Rows, transposed.Columns);
Assert.Equal(1, transposed[0, 0]);
Assert.Equal(2, transposed[1, 0]);
Assert.Equal(3, transposed[2, 0]);
}
}
}
Loading…
Cancel
Save