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;
}