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) {