Browse Source

Merge branch 'master' into feature/adaptiveHistogramEqualization

pull/673/head
Brian Popow 7 years ago
committed by GitHub
parent
commit
d397620856
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 47
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 34
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  3. 24
      src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs
  4. 33
      src/ImageSharp/Primitives/DenseMatrix{T}.cs
  5. 18
      src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs
  6. 51
      src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs
  7. 86
      src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs
  8. 43
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  9. 24
      tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs
  10. 5
      tests/ImageSharp.Tests/TestImages.cs
  11. 5
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  12. 2
      tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs
  13. BIN
      tests/Images/Input/Bmp/pal8-0.bmp
  14. BIN
      tests/Images/Input/Bmp/pal8os2v1_winv2.bmp
  15. BIN
      tests/Images/Input/Bmp/pal8os2v2-16.bmp

47
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Dithering;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -15,6 +18,17 @@ namespace SixLabors.ImageSharp.Advanced
/// </summary> /// </summary>
public static class AotCompilerTools public static class AotCompilerTools
{ {
static AotCompilerTools()
{
System.Runtime.CompilerServices.Unsafe.SizeOf<long>();
System.Runtime.CompilerServices.Unsafe.SizeOf<short>();
System.Runtime.CompilerServices.Unsafe.SizeOf<float>();
System.Runtime.CompilerServices.Unsafe.SizeOf<double>();
System.Runtime.CompilerServices.Unsafe.SizeOf<byte>();
System.Runtime.CompilerServices.Unsafe.SizeOf<Block8x8>();
System.Runtime.CompilerServices.Unsafe.SizeOf<Vector4>();
}
/// <summary> /// <summary>
/// Seeds the compiler using the given pixel format. /// Seeds the compiler using the given pixel format.
/// </summary> /// </summary>
@ -27,6 +41,13 @@ namespace SixLabors.ImageSharp.Advanced
AotCompileWuQuantizer<TPixel>(); AotCompileWuQuantizer<TPixel>();
AotCompileDithering<TPixel>(); AotCompileDithering<TPixel>();
System.Runtime.CompilerServices.Unsafe.SizeOf<TPixel>();
AotCodec<TPixel>(new Formats.Png.PngDecoder(), new Formats.Png.PngEncoder());
AotCodec<TPixel>(new Formats.Bmp.BmpDecoder(), new Formats.Bmp.BmpEncoder());
AotCodec<TPixel>(new Formats.Gif.GifDecoder(), new Formats.Gif.GifEncoder());
AotCodec<TPixel>(new Formats.Jpeg.JpegDecoder(), new Formats.Jpeg.JpegEncoder());
// TODO: Do the discovery work to figure out what works and what doesn't. // TODO: Do the discovery work to figure out what works and what doesn't.
} }
@ -99,5 +120,31 @@ namespace SixLabors.ImageSharp.Advanced
TPixel pixel = default; TPixel pixel = default;
test.Dither<TPixel>(new ImageFrame<TPixel>(Configuration.Default, 1, 1), pixel, pixel, 0, 0, 0, 0, 0, 0); test.Dither<TPixel>(new ImageFrame<TPixel>(Configuration.Default, 1, 1), pixel, pixel, 0, 0, 0, 0, 0, 0);
} }
/// <summary>
/// This method pre-seeds the decoder and encoder for a given pixel format in the AoT compiler for iOS.
/// </summary>
/// <param name="decoder">The image decoder to seed.</param>
/// <param name="encoder">The image encoder to seed.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private static void AotCodec<TPixel>(IImageDecoder decoder, IImageEncoder encoder)
where TPixel : struct, IPixel<TPixel>
{
try
{
decoder.Decode<TPixel>(Configuration.Default, null);
}
catch
{
}
try
{
encoder.Encode<TPixel>(null, null);
}
catch
{
}
}
} }
} }

34
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
{ {
try try
{ {
this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
var image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metaData); var image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metaData);
@ -137,6 +137,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.infoHeader.Width, this.infoHeader.Width,
this.infoHeader.Height, this.infoHeader.Height,
this.infoHeader.BitsPerPixel, this.infoHeader.BitsPerPixel,
bytesPerColorMapEntry,
inverted); inverted);
} }
@ -329,18 +330,20 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param> /// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="bits">The number of bits per pixel.</param> /// <param name="bitsPerPixel">The number of bits per pixel.</param>
/// <param name="bytesPerColorMapEntry">Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
/// the bytes per color palette entry's can be 3 bytes instead of 4.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRgbPalette<TPixel>(Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bits, bool inverted) private void ReadRgbPalette<TPixel>(Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
// Pixels per byte (bits per pixel) // Pixels per byte (bits per pixel)
int ppb = 8 / bits; int ppb = 8 / bitsPerPixel;
int arrayWidth = (width + ppb - 1) / ppb; int arrayWidth = (width + ppb - 1) / ppb;
// Bit mask // Bit mask
int mask = 0xFF >> (8 - bits); int mask = 0xFF >> (8 - bitsPerPixel);
// Rows are aligned on 4 byte boundaries // Rows are aligned on 4 byte boundaries
int padding = arrayWidth % 4; int padding = arrayWidth % 4;
@ -366,7 +369,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
int colOffset = x * ppb; int colOffset = x * ppb;
for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++) for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++)
{ {
int colorIndex = ((rowSpan[offset] >> (8 - bits - (shift * bits))) & mask) * 4; int colorIndex = ((rowSpan[offset] >> (8 - bitsPerPixel - (shift * bitsPerPixel))) & mask) * bytesPerColorMapEntry;
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIndex])); color.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIndex]));
pixelRow[newX] = color; pixelRow[newX] = color;
@ -510,6 +513,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
// 12 bytes // 12 bytes
this.infoHeader = BmpInfoHeader.ParseCore(buffer); this.infoHeader = BmpInfoHeader.ParseCore(buffer);
} }
else if (headerSize == BmpInfoHeader.Os22ShortSize)
{
// 16 bytes
this.infoHeader = BmpInfoHeader.ParseOs22Short(buffer);
}
else if (headerSize >= BmpInfoHeader.Size) else if (headerSize >= BmpInfoHeader.Size)
{ {
// >= 40 bytes // >= 40 bytes
@ -571,7 +579,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Reads the <see cref="BmpFileHeader"/> and <see cref="BmpInfoHeader"/> from the stream and sets the corresponding fields. /// Reads the <see cref="BmpFileHeader"/> and <see cref="BmpInfoHeader"/> from the stream and sets the corresponding fields.
/// </summary> /// </summary>
private void ReadImageHeaders(Stream stream, out bool inverted, out byte[] palette) /// <returns>Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
/// the bytes per color palette entry's can be 3 bytes instead of 4.</returns>
private int ReadImageHeaders(Stream stream, out bool inverted, out byte[] palette)
{ {
this.stream = stream; this.stream = stream;
@ -591,6 +601,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
} }
int colorMapSize = -1; int colorMapSize = -1;
int bytesPerColorMapEntry = 4;
if (this.infoHeader.ClrUsed == 0) if (this.infoHeader.ClrUsed == 0)
{ {
@ -598,12 +609,15 @@ namespace SixLabors.ImageSharp.Formats.Bmp
|| this.infoHeader.BitsPerPixel == 4 || this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8) || this.infoHeader.BitsPerPixel == 8)
{ {
colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4; int colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
int colorCountForBitDepth = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
colorMapSize = colorMapSizeBytes;
} }
} }
else else
{ {
colorMapSize = this.infoHeader.ClrUsed * 4; colorMapSize = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
} }
palette = null; palette = null;
@ -622,6 +636,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
} }
this.infoHeader.VerifyDimensions(); this.infoHeader.VerifyDimensions();
return bytesPerColorMapEntry;
} }
} }
} }

24
src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs

@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary> /// </summary>
public const int CoreSize = 12; public const int CoreSize = 12;
/// <summary>
/// Defines the size of the short variant of the OS22XBITMAPHEADER data structure in the bitmap file.
/// </summary>
public const int Os22ShortSize = 16;
/// <summary> /// <summary>
/// Defines the size of the biggest supported header data structure in the bitmap file. /// Defines the size of the biggest supported header data structure in the bitmap file.
/// </summary> /// </summary>
@ -143,7 +148,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Parses the BITMAPCOREHEADER consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// Parses the BITMAPCOREHEADER consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes).
/// </summary> /// </summary>
/// <param name="data">The data to parse,</param> /// <param name="data">The data to parse.</param>
/// <returns>Parsed header</returns> /// <returns>Parsed header</returns>
/// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/> /// <seealso href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd183372.aspx"/>
public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data) public static BmpInfoHeader ParseCore(ReadOnlySpan<byte> data)
@ -156,6 +161,23 @@ namespace SixLabors.ImageSharp.Formats.Bmp
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2))); bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2)));
} }
/// <summary>
/// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height
/// are 4 bytes instead of 2.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <returns>Parsed header</returns>
/// <seealso href="https://www.fileformat.info/format/os2bmp/egff.htm"/>
public static BmpInfoHeader ParseOs22Short(ReadOnlySpan<byte> data)
{
return new BmpInfoHeader(
headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)),
width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)),
height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)),
planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)),
bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)));
}
public unsafe void WriteTo(Span<byte> buffer) public unsafe void WriteTo(Span<byte> buffer)
{ {
ref BmpInfoHeader dest = ref Unsafe.As<byte, BmpInfoHeader>(ref MemoryMarshal.GetReference(buffer)); ref BmpInfoHeader dest = ref Unsafe.As<byte, BmpInfoHeader>(ref MemoryMarshal.GetReference(buffer));

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

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

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

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

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

43
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -55,6 +55,30 @@ namespace SixLabors.ImageSharp.Tests
} }
} }
[Theory]
[WithFile(WinBmpv2, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecodeBmpv2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider, "png");
image.CompareToOriginal(provider);
}
}
[Theory]
[WithFile(Bit8Palette4, PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode4BytePerEntryPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
{
image.DebugSave(provider, "png");
image.CompareToOriginal(provider);
}
}
[Theory] [Theory]
[InlineData(Car, 24)] [InlineData(Car, 24)]
[InlineData(F, 24)] [InlineData(F, 24)]
@ -89,18 +113,17 @@ namespace SixLabors.ImageSharp.Tests
} }
[Theory] [Theory]
[MemberData(nameof(RatioFiles))] [WithFile(Os2v2Short, PixelTypes.Rgba32)]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) public void BmpDecoder_CanDecode_Os2v2XShortHeader<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{ {
var testFile = TestFile.Create(imagePath); using (Image<TPixel> image = provider.GetImage(new BmpDecoder()))
using (var stream = new MemoryStream(testFile.Bytes, false))
{ {
var decoder = new BmpDecoder(); image.DebugSave(provider, "png");
IImageInfo image = decoder.Identify(Configuration.Default, stream);
ImageMetaData meta = image.MetaData; // TODO: Neither System.Drawing not MagickReferenceDecoder
Assert.Equal(xResolution, meta.HorizontalResolution); // can correctly decode this file.
Assert.Equal(yResolution, meta.VerticalResolution); // image.CompareToOriginal(provider);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
} }
} }
} }

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

@ -106,5 +106,29 @@ namespace SixLabors.ImageSharp.Tests.Primitives
Assert.Equal(0, dense.Data[i]); 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]);
}
} }
} }

5
tests/ImageSharp.Tests/TestImages.cs

@ -201,6 +201,11 @@ namespace SixLabors.ImageSharp.Tests
public const string Bit16 = "Bmp/test16.bmp"; public const string Bit16 = "Bmp/test16.bmp";
public const string Bit16Inverted = "Bmp/test16-inverted.bmp"; public const string Bit16Inverted = "Bmp/test16-inverted.bmp";
public const string Bit32Rgb = "Bmp/rgb32.bmp"; public const string Bit32Rgb = "Bmp/rgb32.bmp";
// Note: This format can be called OS/2 BMPv1, or Windows BMPv2
public const string WinBmpv2 = "Bmp/pal8os2v1_winv2.bmp";
public const string Bit8Palette4 = "Bmp/pal8-0.bmp";
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
public static readonly string[] All public static readonly string[] All
= { = {

5
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -34,8 +34,7 @@ namespace SixLabors.ImageSharp.Tests
{ {
string extension = Path.GetExtension(filePath); string extension = Path.GetExtension(filePath);
IImageFormat format = Configuration.ImageFormatsManager.FindFormatByFileExtension(extension); return Configuration.ImageFormatsManager.FindFormatByFileExtension(extension);
return format;
} }
private static void ConfigureCodecs( private static void ConfigureCodecs(
@ -69,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests
cfg.ConfigureCodecs( cfg.ConfigureCodecs(
BmpFormat.Instance, BmpFormat.Instance,
SystemDrawingReferenceDecoder.Instance, IsWindows ? (IImageDecoder)SystemDrawingReferenceDecoder.Instance : MagickReferenceDecoder.Instance,
bmpEncoder, bmpEncoder,
new BmpImageFormatDetector()); new BmpImageFormatDetector());

2
tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs

@ -127,7 +127,7 @@ namespace SixLabors.ImageSharp.Tests
[Theory] [Theory]
[InlineData("lol/foo.png", typeof(MagickReferenceDecoder))] [InlineData("lol/foo.png", typeof(MagickReferenceDecoder))]
[InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceDecoder))] [InlineData("lol/Rofl.bmp", typeof(MagickReferenceDecoder))]
[InlineData("lol/Baz.JPG", typeof(JpegDecoder))] [InlineData("lol/Baz.JPG", typeof(JpegDecoder))]
[InlineData("lol/Baz.gif", typeof(GifDecoder))] [InlineData("lol/Baz.gif", typeof(GifDecoder))]
public void GetReferenceDecoder_ReturnsCorrectDecoders_Linux(string fileName, Type expectedDecoderType) public void GetReferenceDecoder_ReturnsCorrectDecoders_Linux(string fileName, Type expectedDecoderType)

BIN
tests/Images/Input/Bmp/pal8-0.bmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
tests/Images/Input/Bmp/pal8os2v1_winv2.bmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
tests/Images/Input/Bmp/pal8os2v2-16.bmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Loading…
Cancel
Save