diff --git a/src/ImageSharp/Dithering/Atkinson.cs b/src/ImageSharp/Dithering/Atkinson.cs new file mode 100644 index 000000000..6d1580171 --- /dev/null +++ b/src/ImageSharp/Dithering/Atkinson.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Atkinson image dithering algorithm. + /// + /// + public class Atkinson : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] AtkinsonMatrix = + { + { 0, 0, 1, 1 }, + { 1, 1, 1, 0 }, + { 0, 1, 0, 0 } + }; + + /// + /// Initializes a new instance of the class. + /// + public Atkinson() + : base(AtkinsonMatrix, 8) + { + } + } +} diff --git a/src/ImageSharp/Dithering/Burks.cs b/src/ImageSharp/Dithering/Burks.cs new file mode 100644 index 000000000..3e2d19e57 --- /dev/null +++ b/src/ImageSharp/Dithering/Burks.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Burks image dithering algorithm. + /// + /// + public class Burks : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] BurksMatrix = + { + { 0, 0, 0, 8, 4 }, + { 2, 4, 8, 4, 2 } + }; + + /// + /// Initializes a new instance of the class. + /// + public Burks() + : base(BurksMatrix, 32) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/ErrorDiffusion.cs b/src/ImageSharp/Dithering/ErrorDiffusion.cs new file mode 100644 index 000000000..481e8b4a6 --- /dev/null +++ b/src/ImageSharp/Dithering/ErrorDiffusion.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + using System; + using System.Numerics; + using System.Runtime.CompilerServices; + + /// + /// The base class for performing effor diffusion based dithering. + /// + public abstract class ErrorDiffusion : IErrorDiffusion + { + /// + /// The vector to perform division. + /// + private readonly Vector4 divisorVector; + + /// + /// The matrix width + /// + private readonly byte matrixHeight; + + /// + /// The matrix height + /// + private readonly byte matrixWidth; + + /// + /// The offset at which to start the dithering operation. + /// + private readonly int startingOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The dithering matrix. + /// The divisor. + protected ErrorDiffusion(byte[,] matrix, byte divisor) + { + Guard.NotNull(matrix, nameof(matrix)); + Guard.MustBeGreaterThan(divisor, 0, nameof(divisor)); + + this.Matrix = matrix; + this.matrixWidth = (byte)(matrix.GetUpperBound(1) + 1); + this.matrixHeight = (byte)(matrix.GetUpperBound(0) + 1); + this.divisorVector = new Vector4(divisor); + + this.startingOffset = 0; + for (int i = 0; i < this.matrixWidth; i++) + { + if (matrix[0, i] != 0) + { + this.startingOffset = (byte)(i - 1); + break; + } + } + } + + /// + public byte[,] Matrix { get; } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height) + where TColor : struct, IPackedPixel, IEquatable + { + // Assign the transformed pixel to the array. + pixels[x, y] = transformed; + + // Calculate the error + Vector4 error = source.ToVector4() - transformed.ToVector4(); + + // Loop through and distribute the error amongst neighbouring pixels. + for (int row = 0; row < this.matrixHeight; row++) + { + int matrixY = y + row; + + for (int col = 0; col < this.matrixWidth; col++) + { + int matrixX = x + (col - this.startingOffset); + + if (matrixX > 0 && matrixX < width && matrixY > 0 && matrixY < height) + { + byte coefficient = this.Matrix[row, col]; + if (coefficient == 0) + { + continue; + } + + Vector4 coefficientVector = new Vector4(this.Matrix[row, col]); + Vector4 offsetColor = pixels[matrixX, matrixY].ToVector4(); + Vector4 result = ((error * coefficientVector) / this.divisorVector) + offsetColor; + result.W = offsetColor.W; + + TColor packed = default(TColor); + packed.PackFromVector4(result); + pixels[matrixX, matrixY] = packed; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/FloydSteinberg.cs b/src/ImageSharp/Dithering/FloydSteinberg.cs new file mode 100644 index 000000000..a87421b95 --- /dev/null +++ b/src/ImageSharp/Dithering/FloydSteinberg.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm. + /// + /// + public class FloydSteinberg : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] FloydSteinbergMatrix = + { + { 0, 0, 7 }, + { 3, 5, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + public FloydSteinberg() + : base(FloydSteinbergMatrix, 16) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/IErrorDiffusion.cs b/src/ImageSharp/Dithering/IErrorDiffusion.cs new file mode 100644 index 000000000..cd38cd1cd --- /dev/null +++ b/src/ImageSharp/Dithering/IErrorDiffusion.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + using System; + + /// + /// Encapsulates properties and methods required to perfom diffused error dithering on an image. + /// + public interface IErrorDiffusion + { + /// + /// Gets the dithering matrix + /// + byte[,] Matrix { get; } + + /// + /// Transforms the image applying the dither matrix. This method alters the input pixels array + /// + /// The pixel accessor + /// The source pixel + /// The transformed pixel + /// The column index. + /// The row index. + /// The image width. + /// The image height. + /// The pixel format. + void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height) + where TColor : struct, IPackedPixel, IEquatable; + } +} diff --git a/src/ImageSharp/Dithering/JarvisJudiceNinke.cs b/src/ImageSharp/Dithering/JarvisJudiceNinke.cs new file mode 100644 index 000000000..a495f6001 --- /dev/null +++ b/src/ImageSharp/Dithering/JarvisJudiceNinke.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the JarvisJudiceNinke image dithering algorithm. + /// + /// + public class JarvisJudiceNinke : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] JarvisJudiceNinkeMatrix = + { + { 0, 0, 0, 7, 5 }, + { 3, 5, 7, 5, 3 }, + { 1, 3, 5, 3, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + public JarvisJudiceNinke() + : base(JarvisJudiceNinkeMatrix, 48) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Sierra2.cs b/src/ImageSharp/Dithering/Sierra2.cs new file mode 100644 index 000000000..a2a6db36d --- /dev/null +++ b/src/ImageSharp/Dithering/Sierra2.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Sierra2 image dithering algorithm. + /// + /// + public class Sierra2 : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] Sierra2Matrix = + { + { 0, 0, 0, 4, 3 }, + { 1, 2, 3, 2, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + public Sierra2() + : base(Sierra2Matrix, 16) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Sierra3.cs b/src/ImageSharp/Dithering/Sierra3.cs new file mode 100644 index 000000000..8ab9279f3 --- /dev/null +++ b/src/ImageSharp/Dithering/Sierra3.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Sierra3 image dithering algorithm. + /// + /// + public class Sierra3 : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] Sierra3Matrix = + { + { 0, 0, 0, 5, 3 }, + { 2, 4, 5, 4, 2 }, + { 0, 2, 3, 2, 0 } + }; + + /// + /// Initializes a new instance of the class. + /// + public Sierra3() + : base(Sierra3Matrix, 32) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/SierraLite.cs b/src/ImageSharp/Dithering/SierraLite.cs new file mode 100644 index 000000000..217b6ac5f --- /dev/null +++ b/src/ImageSharp/Dithering/SierraLite.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the SierraLite image dithering algorithm. + /// + /// + public class SierraLite : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] SierraLiteMatrix = + { + { 0, 0, 2 }, + { 1, 1, 0 } + }; + + /// + /// Initializes a new instance of the class. + /// + public SierraLite() + : base(SierraLiteMatrix, 4) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Stucki.cs b/src/ImageSharp/Dithering/Stucki.cs new file mode 100644 index 000000000..0b9b40f73 --- /dev/null +++ b/src/ImageSharp/Dithering/Stucki.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Dithering +{ + /// + /// Applies error diffusion based dithering using the Stucki image dithering algorithm. + /// + /// + public class Stucki : ErrorDiffusion + { + /// + /// The diffusion matrix + /// + private static readonly byte[,] StuckiMatrix = + { + { 0, 0, 0, 8, 4 }, + { 2, 4, 8, 4, 2 }, + { 1, 2, 4, 2, 1 } + }; + + /// + /// Initializes a new instance of the class. + /// + public Stucki() + : base(StuckiMatrix, 4) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Quantizers/IQuantizer.cs b/src/ImageSharp/Quantizers/IQuantizer.cs index 878e9775b..a027ca94c 100644 --- a/src/ImageSharp/Quantizers/IQuantizer.cs +++ b/src/ImageSharp/Quantizers/IQuantizer.cs @@ -7,6 +7,8 @@ namespace ImageSharp.Quantizers { using System; + using ImageSharp.Dithering; + /// /// Provides methods for allowing quantization of images pixels. /// @@ -25,6 +27,24 @@ namespace ImageSharp.Quantizers QuantizedImage Quantize(ImageBase image, int maxColors); } + /// + /// Provides methods for allowing dithering of quantized image pixels. + /// + /// The pixel format. + public interface IDitheredQuantizer : IQuantizer + where TColor : struct, IPackedPixel, IEquatable + { + /// + /// Gets or sets a value indicating whether to apply dithering to the output image. + /// + bool Dither { get; set; } + + /// + /// Gets or sets the dithering algorithm to apply to the output image. + /// + IErrorDiffusion DitherType { get; set; } + } + /// /// Provides methods for allowing quantization of images pixels. /// diff --git a/src/ImageSharp/Quantizers/Octree/Quantizer.cs b/src/ImageSharp/Quantizers/Octree/Quantizer.cs index 74aa6aade..65a9d1ede 100644 --- a/src/ImageSharp/Quantizers/Octree/Quantizer.cs +++ b/src/ImageSharp/Quantizers/Octree/Quantizer.cs @@ -6,12 +6,16 @@ namespace ImageSharp.Quantizers { using System; + using System.Numerics; + using System.Runtime.CompilerServices; + + using ImageSharp.Dithering; /// /// Encapsulates methods to calculate the color palette of an image. /// /// The pixel format. - public abstract class Quantizer : IQuantizer + public abstract class Quantizer : IDitheredQuantizer where TColor : struct, IPackedPixel, IEquatable { /// @@ -19,6 +23,11 @@ namespace ImageSharp.Quantizers /// private readonly bool singlePass; + /// + /// The reduced image palette + /// + private TColor[] palette; + /// /// Initializes a new instance of the class. /// @@ -35,6 +44,12 @@ namespace ImageSharp.Quantizers this.singlePass = singlePass; } + /// + public bool Dither { get; set; } = true; + + /// + public IErrorDiffusion DitherType { get; set; } = new SierraLite(); + /// public virtual QuantizedImage Quantize(ImageBase image, int maxColors) { @@ -44,7 +59,6 @@ namespace ImageSharp.Quantizers int height = image.Height; int width = image.Width; byte[] quantizedPixels = new byte[width * height]; - TColor[] palette; using (PixelAccessor pixels = image.Lock()) { @@ -57,12 +71,24 @@ namespace ImageSharp.Quantizers } // Get the palette - palette = this.GetPalette(); + this.palette = this.GetPalette(); - this.SecondPass(pixels, quantizedPixels, width, height); + if (this.Dither) + { + // We clone the image as we don't want to alter the original. + using (Image clone = new Image(image)) + using (PixelAccessor clonedPixels = clone.Lock()) + { + this.SecondPass(clonedPixels, quantizedPixels, width, height); + } + } + else + { + this.SecondPass(pixels, quantizedPixels, width, height); + } } - return new QuantizedImage(width, height, palette, quantizedPixels); + return new QuantizedImage(width, height, this.palette, quantizedPixels); } /// @@ -99,6 +125,14 @@ namespace ImageSharp.Quantizers // And loop through each column for (int x = 0; x < width; x++) { + if (this.Dither) + { + // Apply the dithering matrix + TColor sourcePixel = source[x, y]; + TColor transformedPixel = this.palette[GetClosestColor(sourcePixel, this.palette)]; + this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height); + } + output[(y * source.Width) + x] = this.QuantizePixel(source[x, y]); } } @@ -129,8 +163,41 @@ namespace ImageSharp.Quantizers /// Retrieve the palette for the quantized image /// /// - /// The new color palette + /// /// protected abstract TColor[] GetPalette(); + + /// + /// Returns the closest color from the palette to the given color by calculating the Euclidean distance. + /// + /// The color. + /// The color palette. + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetClosestColor(TColor pixel, TColor[] palette) + { + float leastDistance = int.MaxValue; + Vector4 vector = pixel.ToVector4(); + + byte colorIndex = 0; + for (int index = 0; index < palette.Length; index++) + { + float distance = Vector4.Distance(vector, palette[index].ToVector4()); + + if (distance < leastDistance) + { + colorIndex = (byte)index; + leastDistance = distance; + + // And if it's an exact match, exit the loop + if (Math.Abs(distance) < Constants.Epsilon) + { + break; + } + } + } + + return colorIndex; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs b/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs index 6edb7801b..abf1e5dc5 100644 --- a/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs +++ b/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs @@ -97,7 +97,7 @@ namespace ImageSharp.Quantizers leastDistance = distance; // And if it's an exact match, exit the loop - if (Math.Abs(distance) < .0001F) + if (Math.Abs(distance) < Constants.Epsilon) { break; }