diff --git a/src/ImageSharp.Processing/Binarization/Dither.cs b/src/ImageSharp.Processing/Binarization/Dither.cs
index f481ac4dfb..6a4f7f0057 100644
--- a/src/ImageSharp.Processing/Binarization/Dither.cs
+++ b/src/ImageSharp.Processing/Binarization/Dither.cs
@@ -16,7 +16,39 @@ namespace ImageSharp
public static partial class ImageExtensions
{
///
- /// Alters the alpha component of the image.
+ /// Dithers the image reducing it to two colors using ordered dithering.
+ ///
+ /// The pixel format.
+ /// The image this method extends.
+ /// The ordered ditherer.
+ /// The component index to test the threshold against. Must range from 0 to 3.
+ /// The .
+ public static Image Dither(this Image source, IOrderedDither dither, int index = 0)
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ return Dither(source, dither, source.Bounds, index);
+ }
+
+ ///
+ /// Dithers the image reducing it to two colors using ordered dithering.
+ ///
+ /// The pixel format.
+ /// The image this method extends.
+ /// The ordered ditherer.
+ ///
+ /// The structure that specifies the portion of the image object to alter.
+ ///
+ /// The component index to test the threshold against. Must range from 0 to 3.
+ /// The .
+ public static Image Dither(this Image source, IOrderedDither dither, Rectangle rectangle, int index = 0)
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ source.ApplyProcessor(new OrderedDitherProcessor(dither, index), rectangle);
+ return source;
+ }
+
+ ///
+ /// Dithers the image reducing it to two colors using error diffusion.
///
/// The pixel format.
/// The image this method extends.
@@ -30,7 +62,7 @@ namespace ImageSharp
}
///
- /// Alters the alpha component of the image.
+ /// Dithers the image reducing it to two colors using error diffusion.
///
/// The pixel format.
/// The image this method extends.
diff --git a/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs b/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs
index c5b78b6390..6429ce6f08 100644
--- a/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs
+++ b/src/ImageSharp.Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs
@@ -25,9 +25,6 @@ namespace ImageSharp.Processing.Processors
{
Guard.NotNull(diffuser, nameof(diffuser));
- // TODO: Check thresholding limit. Colors should probably have Max/Min/Middle properties.
- Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold));
-
this.Diffuser = diffuser;
this.Threshold = threshold;
diff --git a/src/ImageSharp.Processing/Processors/Binarization/OrderedDitherProcessor.cs b/src/ImageSharp.Processing/Processors/Binarization/OrderedDitherProcessor.cs
new file mode 100644
index 0000000000..b2a1e9a228
--- /dev/null
+++ b/src/ImageSharp.Processing/Processors/Binarization/OrderedDitherProcessor.cs
@@ -0,0 +1,119 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Processing.Processors
+{
+ using System;
+ using System.Buffers;
+
+ using ImageSharp.Dithering;
+
+ ///
+ /// An that dithers an image using error diffusion.
+ ///
+ /// The pixel format.
+ public class OrderedDitherProcessor : ImageProcessor
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ordered ditherer.
+ /// The component index to test the threshold against. Must range from 0 to 3.
+ public OrderedDitherProcessor(IOrderedDither dither, int index)
+ {
+ Guard.NotNull(dither, nameof(dither));
+ Guard.MustBeBetweenOrEqualTo(index, 0, 3, nameof(index));
+
+ // Alpha8 only stores the pixel data in the alpha channel.
+ if (typeof(TColor) == typeof(Alpha8))
+ {
+ index = 3;
+ }
+
+ this.Dither = dither;
+ this.Index = index;
+
+ // Default to white/black for upper/lower.
+ TColor upper = default(TColor);
+ upper.PackFromBytes(255, 255, 255, 255);
+ this.UpperColor = upper;
+
+ TColor lower = default(TColor);
+ lower.PackFromBytes(0, 0, 0, 255);
+ this.LowerColor = lower;
+ }
+
+ ///
+ /// Gets the ditherer.
+ ///
+ public IOrderedDither Dither { get; }
+
+ ///
+ /// Gets the component index to test the threshold against.
+ ///
+ public int Index { get; }
+
+ ///
+ /// Gets or sets the color to use for pixels that are above the threshold.
+ ///
+ public TColor UpperColor { get; set; }
+
+ ///
+ /// Gets or sets the color to use for pixels that fall below the threshold.
+ ///
+ public TColor LowerColor { get; set; }
+
+ ///
+ protected override void BeforeApply(ImageBase source, Rectangle sourceRectangle)
+ {
+ new GrayscaleBt709Processor().Apply(source, sourceRectangle);
+ }
+
+ ///
+ protected override void OnApply(ImageBase source, Rectangle sourceRectangle)
+ {
+ int startY = sourceRectangle.Y;
+ int endY = sourceRectangle.Bottom;
+ int startX = sourceRectangle.X;
+ int endX = sourceRectangle.Right;
+
+ // Align start/end positions.
+ int minX = Math.Max(0, startX);
+ int maxX = Math.Min(source.Width, endX);
+ int minY = Math.Max(0, startY);
+ int maxY = Math.Min(source.Height, endY);
+
+ // Reset offset if necessary.
+ if (minX > 0)
+ {
+ startX = 0;
+ }
+
+ if (minY > 0)
+ {
+ startY = 0;
+ }
+
+ using (PixelAccessor sourcePixels = source.Lock())
+ {
+ for (int y = minY; y < maxY; y++)
+ {
+ int offsetY = y - startY;
+ byte[] bytes = ArrayPool.Shared.Rent(4);
+
+ for (int x = minX; x < maxX; x++)
+ {
+ int offsetX = x - startX;
+ TColor sourceColor = sourcePixels[offsetX, offsetY];
+ this.Dither.Dither(sourcePixels, sourceColor, this.UpperColor, this.LowerColor, bytes, this.Index, offsetX, offsetY, maxX, maxY);
+ }
+
+ ArrayPool.Shared.Return(bytes);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Colors/PackedPixel/Alpha8.cs b/src/ImageSharp/Colors/PackedPixel/Alpha8.cs
index 95e620df01..5642e62010 100644
--- a/src/ImageSharp/Colors/PackedPixel/Alpha8.cs
+++ b/src/ImageSharp/Colors/PackedPixel/Alpha8.cs
@@ -10,7 +10,7 @@ namespace ImageSharp
using System.Runtime.CompilerServices;
///
- /// Packed pixel type containing a single 8 bit normalized W values that is ranging from 0 to 1.
+ /// Packed pixel type containing a single 8 bit normalized W values ranging from 0 to 1.
///
public struct Alpha8 : IPackedPixel, IEquatable
{
diff --git a/src/ImageSharp/Dithering/Atkinson.cs b/src/ImageSharp/Dithering/ErrorDiffusion/Atkinson.cs
similarity index 100%
rename from src/ImageSharp/Dithering/Atkinson.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/Atkinson.cs
diff --git a/src/ImageSharp/Dithering/Burks.cs b/src/ImageSharp/Dithering/ErrorDiffusion/Burks.cs
similarity index 100%
rename from src/ImageSharp/Dithering/Burks.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/Burks.cs
diff --git a/src/ImageSharp/Dithering/ErrorDiffuser.cs b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs
similarity index 100%
rename from src/ImageSharp/Dithering/ErrorDiffuser.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs
diff --git a/src/ImageSharp/Dithering/FloydSteinberg.cs b/src/ImageSharp/Dithering/ErrorDiffusion/FloydSteinberg.cs
similarity index 100%
rename from src/ImageSharp/Dithering/FloydSteinberg.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/FloydSteinberg.cs
diff --git a/src/ImageSharp/Dithering/IErrorDiffuser.cs b/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs
similarity index 100%
rename from src/ImageSharp/Dithering/IErrorDiffuser.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs
diff --git a/src/ImageSharp/Dithering/JarvisJudiceNinke.cs b/src/ImageSharp/Dithering/ErrorDiffusion/JarvisJudiceNinke.cs
similarity index 100%
rename from src/ImageSharp/Dithering/JarvisJudiceNinke.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/JarvisJudiceNinke.cs
diff --git a/src/ImageSharp/Dithering/Sierra2.cs b/src/ImageSharp/Dithering/ErrorDiffusion/Sierra2.cs
similarity index 100%
rename from src/ImageSharp/Dithering/Sierra2.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/Sierra2.cs
diff --git a/src/ImageSharp/Dithering/Sierra3.cs b/src/ImageSharp/Dithering/ErrorDiffusion/Sierra3.cs
similarity index 100%
rename from src/ImageSharp/Dithering/Sierra3.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/Sierra3.cs
diff --git a/src/ImageSharp/Dithering/SierraLite.cs b/src/ImageSharp/Dithering/ErrorDiffusion/SierraLite.cs
similarity index 100%
rename from src/ImageSharp/Dithering/SierraLite.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/SierraLite.cs
diff --git a/src/ImageSharp/Dithering/Stucki.cs b/src/ImageSharp/Dithering/ErrorDiffusion/Stucki.cs
similarity index 100%
rename from src/ImageSharp/Dithering/Stucki.cs
rename to src/ImageSharp/Dithering/ErrorDiffusion/Stucki.cs
diff --git a/src/ImageSharp/Dithering/Ordered/Bayer.cs b/src/ImageSharp/Dithering/Ordered/Bayer.cs
new file mode 100644
index 0000000000..dc731cf894
--- /dev/null
+++ b/src/ImageSharp/Dithering/Ordered/Bayer.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Dithering.Ordered
+{
+ using System;
+
+ ///
+ /// Applies error diffusion based dithering using the 4x4 Bayer dithering matrix.
+ ///
+ ///
+ public class Bayer : IOrderedDither
+ {
+ ///
+ /// The threshold matrix.
+ /// This is calculated by multiplying each value in the original matrix by 16 and subtracting 1
+ ///
+ private static readonly byte[,] ThresholdMatrix = {
+ { 15, 143, 47, 175 },
+ { 207, 79, 239, 111 },
+ { 63, 191, 31, 159 },
+ { 255, 127, 223, 95 }
+ };
+
+ ///
+ public byte[,] Matrix { get; } = ThresholdMatrix;
+
+ ///
+ public void Dither(PixelAccessor pixels, TColor source, TColor upper, TColor lower, byte[] bytes, int index, int x, int y, int width, int height)
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ source.ToXyzwBytes(bytes, 0);
+ pixels[x, y] = ThresholdMatrix[x % 3, y % 3] >= bytes[index] ? upper : lower;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs b/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs
new file mode 100644
index 0000000000..910b275f94
--- /dev/null
+++ b/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs
@@ -0,0 +1,37 @@
+//
+// 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 ordered dithering on an image.
+ ///
+ public interface IOrderedDither
+ {
+ ///
+ /// 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 color to apply to the pixels above the threshold.
+ /// The color to apply to the pixels below the threshold.
+ /// The byte array to pack/unpack to. Must have a length of 4. Bytes are unpacked to Xyzw order.
+ /// The component index to test the threshold against. Must range from 0 to 3.
+ /// The column index.
+ /// The row index.
+ /// The image width.
+ /// The image height.
+ /// The pixel format.
+ void Dither(PixelAccessor pixels, TColor source, TColor upper, TColor lower, byte[] bytes, int index, int x, int y, int width, int height)
+ where TColor : struct, IPackedPixel, IEquatable;
+ }
+}
diff --git a/src/ImageSharp/Dithering/Ordered/Ordered.cs b/src/ImageSharp/Dithering/Ordered/Ordered.cs
new file mode 100644
index 0000000000..0cfc532443
--- /dev/null
+++ b/src/ImageSharp/Dithering/Ordered/Ordered.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Dithering.Ordered
+{
+ using System;
+
+ ///
+ /// Applies error diffusion based dithering using the 4x4 ordered dithering matrix.
+ ///
+ ///
+ public class Ordered : IOrderedDither
+ {
+ ///
+ /// The threshold matrix.
+ /// This is calculated by multiplying each value in the original matrix by 16
+ ///
+ private static readonly byte[,] ThresholdMatrix = {
+ { 0, 128, 32, 160 },
+ { 192, 64, 224, 96 },
+ { 48, 176, 16, 144 },
+ { 240, 112, 208, 80 }
+ };
+
+ ///
+ public byte[,] Matrix { get; } = ThresholdMatrix;
+
+ ///
+ public void Dither(PixelAccessor pixels, TColor source, TColor upper, TColor lower, byte[] bytes, int index, int x, int y, int width, int height)
+ where TColor : struct, IPackedPixel, IEquatable
+ {
+ source.ToXyzwBytes(bytes, 0);
+ pixels[x, y] = ThresholdMatrix[x % 3, y % 3] >= bytes[index] ? upper : lower;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs b/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs
index a79ef620ef..3e2b6fcd5c 100644
--- a/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs
+++ b/tests/ImageSharp.Tests/Colors/PackedPixelTests.cs
@@ -6,6 +6,7 @@
namespace ImageSharp.Tests.Colors
{
using System;
+ using System.Diagnostics;
using System.Numerics;
using Xunit;
diff --git a/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs b/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs
index 3de2481e5d..db473901d3 100644
--- a/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs
+++ b/tests/ImageSharp.Tests/Processors/Filters/DitherTest.cs
@@ -8,6 +8,7 @@ namespace ImageSharp.Tests
using System.IO;
using ImageSharp.Dithering;
+ using ImageSharp.Dithering.Ordered;
using Xunit;
@@ -16,14 +17,14 @@ namespace ImageSharp.Tests
[Fact]
public void ImageShouldApplyDitherFilter()
{
- string path = this.CreateOutputDirectory("Dither");
+ string path = this.CreateOutputDirectory("Dither", "Dither");
foreach (TestFile file in Files)
{
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{file.FileName}"))
{
- image.Dither(new SierraLite(), .5F).Save(output);
+ image.Dither(new Bayer()).Save(output);
}
}
}
@@ -31,7 +32,38 @@ namespace ImageSharp.Tests
[Fact]
public void ImageShouldApplyDitherFilterInBox()
{
- string path = this.CreateOutputDirectory("Dither");
+ string path = this.CreateOutputDirectory("Dither", "Dither");
+
+ foreach (TestFile file in Files)
+ {
+ string filename = file.GetFileName("-InBox");
+ using (Image image = file.CreateImage())
+ using (FileStream output = File.OpenWrite($"{path}/{filename}"))
+ {
+ image.Dither(new Bayer(), new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
+ }
+ }
+ }
+
+ [Fact]
+ public void ImageShouldApplyDiffusionFilter()
+ {
+ string path = this.CreateOutputDirectory("Dither", "Diffusion");
+
+ foreach (TestFile file in Files)
+ {
+ using (Image image = file.CreateImage())
+ using (FileStream output = File.OpenWrite($"{path}/{file.FileName}"))
+ {
+ image.Dither(new SierraLite(), .5F).Save(output);
+ }
+ }
+ }
+
+ [Fact]
+ public void ImageShouldApplyDiffusionFilterInBox()
+ {
+ string path = this.CreateOutputDirectory("Dither", "Diffusion");
foreach (TestFile file in Files)
{