From 789e52c367fe0f2bfa257627658d6cf35564396b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 2 Feb 2018 00:53:48 +1100 Subject: [PATCH] Refactor and fix dithering API + algorithm. --- .../ErrorDiffusion/ErrorDiffuserBase.cs | 18 +-- .../ErrorDiffusion/IErrorDiffuser.cs | 24 +--- .../Dithering/Ordered/Bayer2x2Dither.cs | 19 +++ .../Dithering/Ordered/Bayer4x4Dither.cs | 19 +++ .../Dithering/Ordered/Bayer8x8Dither.cs | 19 +++ .../Dithering/Ordered/BayerDither.cs | 60 ++++++--- .../Dithering/Ordered/IOrderedDither.cs | 7 +- .../Dithering/Ordered/OrderedDither.cs | 36 ----- .../Dithering/Ordered/OrderedDitherBase.cs | 49 ++++--- src/ImageSharp/Memory/Fast2DArray{T}.cs | 11 +- src/ImageSharp/PixelFormats/ColorConstants.cs | 18 +-- .../PixelFormats/NamedColors{TPixel}.cs | 17 +++ .../Processing/Binarization/BinaryDiffuse.cs | 86 ++++++++++++ .../Processing/Binarization/BinaryDither.cs | 82 ++++++++++++ .../Binarization/BinaryThreshold.cs | 36 ++++- .../Processing/Dithering/Diffuse.cs | 84 ++++++++++++ .../{Binarization => Dithering}/Dither.cs | 58 ++------- .../BinaryErrorDiffusionProcessor.cs | 123 ++++++++++++++++++ .../BinaryOrderedDitherProcessor.cs | 103 +++++++++++++++ .../Binarization/BinaryThresholdProcessor.cs | 73 +++++------ .../ErrorDiffusionDitherProcessor.cs | 85 ------------ .../Binarization/OrderedDitherProcessor.cs | 93 ------------- .../ErrorDiffusionPaletteProcessor.cs | 113 ++++++++++++++++ .../OrderedDitherPaletteProcessor.cs | 93 +++++++++++++ .../Dithering/PaletteDitherProcessorBase.cs | 76 +++++++++++ .../Processors/Dithering/PixelPair.cs | 49 +++++++ .../Filters/GrayscaleBt709Processor.cs | 2 +- .../Quantizers/OctreeQuantizer{TPixel}.cs | 2 +- .../Quantizers/PaletteQuantizer{TPixel}.cs | 32 ++--- .../Quantizers/WuQuantizer{TPixel}.cs | 2 +- .../Binarization/BinaryThresholdTest.cs | 28 +++- .../Processing/Binarization/DitherTests.cs | 70 +++++++--- .../Processors/Binarization/DitherTests.cs | 19 +-- 33 files changed, 1150 insertions(+), 456 deletions(-) create mode 100644 src/ImageSharp/Dithering/Ordered/Bayer2x2Dither.cs create mode 100644 src/ImageSharp/Dithering/Ordered/Bayer4x4Dither.cs create mode 100644 src/ImageSharp/Dithering/Ordered/Bayer8x8Dither.cs delete mode 100644 src/ImageSharp/Dithering/Ordered/OrderedDither.cs create mode 100644 src/ImageSharp/Processing/Binarization/BinaryDiffuse.cs create mode 100644 src/ImageSharp/Processing/Binarization/BinaryDither.cs create mode 100644 src/ImageSharp/Processing/Dithering/Diffuse.cs rename src/ImageSharp/Processing/{Binarization => Dithering}/Dither.cs (50%) create mode 100644 src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs delete mode 100644 src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs diff --git a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs index 46bafcc0c..9fde27908 100644 --- a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs +++ b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuserBase.cs @@ -70,22 +70,10 @@ namespace SixLabors.ImageSharp.Dithering.Base /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dither(ImageFrame pixels, TPixel source, TPixel transformed, int x, int y, int minX, int minY, int maxX, int maxY) + public void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int minY, int maxX, int maxY) where TPixel : struct, IPixel { - this.Dither(pixels, source, transformed, x, y, minX, minY, maxX, maxY, true); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int minY, int maxX, int maxY, bool replacePixel) - where TPixel : struct, IPixel - { - if (replacePixel) - { - // Assign the transformed pixel to the array. - image[x, y] = transformed; - } + image[x, y] = transformed; // Calculate the error Vector4 error = source.ToVector4() - transformed.ToVector4(); @@ -117,6 +105,8 @@ namespace SixLabors.ImageSharp.Dithering.Base var offsetColor = pixel.ToVector4(); Vector4 result = ((error * coefficient) / this.divisorVector) + offsetColor; + + // result.W = offsetColor.W; pixel.PackFromVector4(result); } } diff --git a/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs b/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs index c538d643c..dabc4e682 100644 --- a/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs +++ b/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Dithering { /// - /// Encapsulates properties and methods required to perfom diffused error dithering on an image. + /// Encapsulates properties and methods required to perform diffused error dithering on an image. /// public interface IErrorDiffuser { @@ -25,25 +25,5 @@ namespace SixLabors.ImageSharp.Dithering /// The pixel format. void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int minY, int maxX, int maxY) where TPixel : struct, IPixel; - - /// - /// Transforms the image applying the dither matrix. This method alters the input pixels array - /// - /// The image - /// The source pixel - /// The transformed pixel - /// The column index. - /// The row index. - /// The minimum column value. - /// The minimum row value. - /// The maximum column value. - /// The maximum row value. - /// - /// Whether to replace the pixel at the given coordinates with the transformed value. - /// Generally this would be true for standard two-color dithering but when used in conjunction with color quantization this should be false. - /// - /// The pixel format. - void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int minY, int maxX, int maxY, bool replacePixel) - where TPixel : struct, IPixel; } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/Bayer2x2Dither.cs b/src/ImageSharp/Dithering/Ordered/Bayer2x2Dither.cs new file mode 100644 index 000000000..e96a9c4d3 --- /dev/null +++ b/src/ImageSharp/Dithering/Ordered/Bayer2x2Dither.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Dithering +{ + /// + /// Applies order dithering using the 2x2 Bayer dithering matrix. + /// + public sealed class Bayer2x2Dither : BayerDither + { + /// + /// Initializes a new instance of the class. + /// + public Bayer2x2Dither() + : base(1) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/Bayer4x4Dither.cs b/src/ImageSharp/Dithering/Ordered/Bayer4x4Dither.cs new file mode 100644 index 000000000..ad72c164f --- /dev/null +++ b/src/ImageSharp/Dithering/Ordered/Bayer4x4Dither.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Dithering +{ + /// + /// Applies order dithering using the 4x4 Bayer dithering matrix. + /// + public sealed class Bayer4x4Dither : BayerDither + { + /// + /// Initializes a new instance of the class. + /// + public Bayer4x4Dither() + : base(2) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/Bayer8x8Dither.cs b/src/ImageSharp/Dithering/Ordered/Bayer8x8Dither.cs new file mode 100644 index 000000000..9077dc2cc --- /dev/null +++ b/src/ImageSharp/Dithering/Ordered/Bayer8x8Dither.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Dithering +{ + /// + /// Applies order dithering using the 8x8 Bayer dithering matrix. + /// + public sealed class Bayer8x8Dither : BayerDither + { + /// + /// Initializes a new instance of the class. + /// + public Bayer8x8Dither() + : base(3) + { + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/BayerDither.cs b/src/ImageSharp/Dithering/Ordered/BayerDither.cs index 685dca5fe..3bac601ae 100644 --- a/src/ImageSharp/Dithering/Ordered/BayerDither.cs +++ b/src/ImageSharp/Dithering/Ordered/BayerDither.cs @@ -1,36 +1,60 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Dithering.Base; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Dithering { /// - /// Applies error diffusion based dithering using the 4x4 Bayer dithering matrix. - /// + /// Applies order dithering using a Bayer dithering matrix of arbitrary length. + /// /// - public sealed class BayerDither : OrderedDitherBase + public class BayerDither : OrderedDitherBase { /// - /// The threshold matrix. - /// This is calculated by multiplying each value in the original matrix by 16 and subtracting 1 + /// Initializes a new instance of the class. /// - private static readonly Fast2DArray ThresholdMatrix = - new byte[,] + /// + /// The exponent used to raise the base value (2). + /// The value given determines the dimensions of the matrix with each dimension a power of 2. e.g 2^2 = 4, 2^3 = 8 + /// + public BayerDither(uint exponent) + : base(ComputeBayer(exponent)) + { + } + + private static Fast2DArray ComputeBayer(uint order) + { + uint dimension = (uint)(1 << (int)order); + var matrix = new Fast2DArray((int)dimension); + uint i = 0; + for (int y = 0; y < dimension; y++) { - { 15, 143, 47, 175 }, - { 207, 79, 239, 111 }, - { 63, 191, 31, 159 }, - { 255, 127, 223, 95 } - }; + for (int x = 0; x < dimension; x++) + { + matrix[y, x] = Bayer(i / dimension, i % dimension, order); + i++; + } + } - /// - /// Initializes a new instance of the class. - /// - public BayerDither() - : base(ThresholdMatrix) + return matrix; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Bayer(uint x, uint y, uint order) { + uint res = 0; + for (uint i = 0; i < order; ++i) + { + uint xOdd_XOR_yOdd = (x & 1) ^ (y & 1); + uint xOdd = x & 1; + res = ((res << 1 | xOdd_XOR_yOdd) << 1) | xOdd; + x >>= 1; + y >>= 1; + } + + return res; } } } \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs b/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs index 689c9a85b..5d05be370 100644 --- a/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs +++ b/src/ImageSharp/Dithering/Ordered/IOrderedDither.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Dithering { /// - /// Encapsulates properties and methods required to perfom ordered dithering on an image. + /// Encapsulates properties and methods required to perform ordered dithering on an image. /// public interface IOrderedDither { @@ -17,12 +17,11 @@ namespace SixLabors.ImageSharp.Dithering /// The source pixel /// The color to apply to the pixels above the threshold. /// The color to apply to the pixels below the threshold. - /// The to pack/unpack to. - /// The component index to test the threshold against. Must range from 0 to 3. + /// The threshold to split the image. Must be between 0 and 1. /// The column index. /// The row index. /// The pixel format. - void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, ref Rgba32 rgba, int index, int x, int y) + void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, byte threshold, int x, int y) where TPixel : struct, IPixel; } } \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/OrderedDither.cs b/src/ImageSharp/Dithering/Ordered/OrderedDither.cs deleted file mode 100644 index 12968914d..000000000 --- a/src/ImageSharp/Dithering/Ordered/OrderedDither.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Dithering.Base; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Dithering -{ - /// - /// Applies error diffusion based dithering using the 4x4 ordered dithering matrix. - /// - /// - public sealed class OrderedDither : OrderedDitherBase - { - /// - /// The threshold matrix. - /// This is calculated by multiplying each value in the original matrix by 16 - /// - private static readonly Fast2DArray ThresholdMatrix = - new byte[,] - { - { 0, 128, 32, 160 }, - { 192, 64, 224, 96 }, - { 48, 176, 16, 144 }, - { 240, 112, 208, 80 } - }; - - /// - /// Initializes a new instance of the class. - /// - public OrderedDither() - : base(ThresholdMatrix) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Dithering/Ordered/OrderedDitherBase.cs b/src/ImageSharp/Dithering/Ordered/OrderedDitherBase.cs index 818a24d5d..cf7a14239 100644 --- a/src/ImageSharp/Dithering/Ordered/OrderedDitherBase.cs +++ b/src/ImageSharp/Dithering/Ordered/OrderedDitherBase.cs @@ -1,53 +1,48 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Dithering.Base +namespace SixLabors.ImageSharp.Dithering { /// - /// The base class for performing ordered dithering using a 4x4 matrix. + /// The base class for performing ordered dithering using a dither matrix. /// public abstract class OrderedDitherBase : IOrderedDither { - /// - /// The dithering matrix - /// - private Fast2DArray matrix; + private readonly Fast2DArray matrix; + private readonly Fast2DArray thresholdMatrix; + private readonly int modulusX; + private readonly int modulusY; /// /// Initializes a new instance of the class. /// /// The thresholding matrix. - internal OrderedDitherBase(Fast2DArray matrix) + internal OrderedDitherBase(Fast2DArray matrix) { this.matrix = matrix; + this.modulusX = matrix.Width; + this.modulusY = matrix.Height; + this.thresholdMatrix = new Fast2DArray(matrix.Width, matrix.Height); + + // Adjust the matrix range for 0-255 + int multiplier = 256 / (this.modulusX * this.modulusY); + for (int y = 0; y < matrix.Height; y++) + { + for (int x = 0; x < matrix.Width; x++) + { + this.thresholdMatrix[y, x] = (uint)((matrix[y, x] + 1) * multiplier) - 1; + } + } } /// - public void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, ref Rgba32 rgba, int index, int x, int y) + public void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, byte threshold, int x, int y) where TPixel : struct, IPixel { - source.ToRgba32(ref rgba); - switch (index) - { - case 0: - image[x, y] = this.matrix[y % 3, x % 3] >= rgba.R ? lower : upper; - return; - case 1: - image[x, y] = this.matrix[y % 3, x % 3] >= rgba.G ? lower : upper; - return; - case 2: - image[x, y] = this.matrix[y % 3, x % 3] >= rgba.B ? lower : upper; - return; - case 3: - image[x, y] = this.matrix[y % 3, x % 3] >= rgba.A ? lower : upper; - return; - } - - throw new ArgumentOutOfRangeException(nameof(index), "Index should be between 0 and 3 inclusive."); + image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper; } } } \ No newline at end of file diff --git a/src/ImageSharp/Memory/Fast2DArray{T}.cs b/src/ImageSharp/Memory/Fast2DArray{T}.cs index 14ac58baf..e0384d208 100644 --- a/src/ImageSharp/Memory/Fast2DArray{T}.cs +++ b/src/ImageSharp/Memory/Fast2DArray{T}.cs @@ -28,6 +28,15 @@ namespace SixLabors.ImageSharp.Memory /// public int Height; + /// + /// Initializes a new instance of the struct. + /// + /// The length of each dimension. + public Fast2DArray(int length) + : this(length, length) + { + } + /// /// Initializes a new instance of the struct. /// @@ -96,7 +105,7 @@ namespace SixLabors.ImageSharp.Memory /// /// The source array. /// - /// The represenation on the source data. + /// The representation on the source data. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Fast2DArray(T[,] data) diff --git a/src/ImageSharp/PixelFormats/ColorConstants.cs b/src/ImageSharp/PixelFormats/ColorConstants.cs index f97d3b190..bac05c53d 100644 --- a/src/ImageSharp/PixelFormats/ColorConstants.cs +++ b/src/ImageSharp/PixelFormats/ColorConstants.cs @@ -1,9 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; -using System.Collections.Generic; - namespace SixLabors.ImageSharp.PixelFormats { /// @@ -11,23 +8,17 @@ namespace SixLabors.ImageSharp.PixelFormats /// public static class ColorConstants { - /// - /// Provides a lazy, one time method of returning the colors. - /// - private static readonly Lazy SafeColors = new Lazy(GetWebSafeColors); - /// /// Gets a collection of named, web safe, colors as defined in the CSS Color Module Level 4. /// - public static Rgba32[] WebSafeColors => SafeColors.Value; + public static readonly Rgba32[] WebSafeColors = GetWebSafeColors(); /// /// Returns an array of web safe colors. /// /// The private static Rgba32[] GetWebSafeColors() - { - return new List + => new Rgba32[] { Rgba32.AliceBlue, Rgba32.AntiqueWhite, @@ -171,7 +162,6 @@ namespace SixLabors.ImageSharp.PixelFormats Rgba32.WhiteSmoke, Rgba32.Yellow, Rgba32.YellowGreen - }.ToArray(); - } + }; } -} +} \ No newline at end of file diff --git a/src/ImageSharp/PixelFormats/NamedColors{TPixel}.cs b/src/ImageSharp/PixelFormats/NamedColors{TPixel}.cs index 45050de72..ccd532bc3 100644 --- a/src/ImageSharp/PixelFormats/NamedColors{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/NamedColors{TPixel}.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.PixelFormats { /// @@ -719,5 +721,20 @@ namespace SixLabors.ImageSharp.PixelFormats /// Represents a matching the W3C definition that has an hex value of #9ACD32. /// public static readonly TPixel YellowGreen = ColorBuilder.FromRGBA(154, 205, 50, 255); + + /// + /// Represents a matching the W3C definition of web safe colors. + /// + public static readonly TPixel[] WebSafePalette = GetWebSafePalette(); + + private static TPixel[] GetWebSafePalette() + { + Rgba32[] constants = ColorConstants.WebSafeColors; + TPixel[] safe = new TPixel[constants.Length + 1]; + + Span constantsBytes = constants.AsSpan().NonPortableCast(); + PixelOperations.Instance.PackFromRgba32Bytes(constantsBytes, safe, constants.Length); + return safe; + } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Binarization/BinaryDiffuse.cs b/src/ImageSharp/Processing/Binarization/BinaryDiffuse.cs new file mode 100644 index 000000000..eb5008757 --- /dev/null +++ b/src/ImageSharp/Processing/Binarization/BinaryDiffuse.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Dithers the image reducing it to two colors using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The . + public static IImageProcessingContext BinaryDiffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold)); + return source; + } + + /// + /// Dithers the image reducing it to two colors using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryDiffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold), rectangle); + return source; + } + + /// + /// Dithers the image reducing it to two colors using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// The . + public static IImageProcessingContext BinaryDiffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, TPixel upperColor, TPixel lowerColor) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor)); + return source; + } + + /// + /// Dithers the image reducing it to two colors using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryDiffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, TPixel upperColor, TPixel lowerColor, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor), rectangle); + return source; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Binarization/BinaryDither.cs b/src/ImageSharp/Processing/Binarization/BinaryDither.cs new file mode 100644 index 000000000..715dff472 --- /dev/null +++ b/src/ImageSharp/Processing/Binarization/BinaryDither.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The pixel format. + /// The image this method extends. + /// The ordered ditherer. + /// The . + public static IImageProcessingContext BinaryDither(this IImageProcessingContext source, IOrderedDither dither) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither)); + return source; + } + + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The pixel format. + /// The image this method extends. + /// The ordered ditherer. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// The . + public static IImageProcessingContext BinaryDither(this IImageProcessingContext source, IOrderedDither dither, TPixel upperColor, TPixel lowerColor) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither, upperColor, lowerColor)); + return source; + } + + /// + /// 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 . + public static IImageProcessingContext BinaryDither(this IImageProcessingContext source, IOrderedDither dither, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither), rectangle); + return source; + } + + /// + /// Dithers the image reducing it to two colors using ordered dithering. + /// + /// The pixel format. + /// The image this method extends. + /// The ordered ditherer. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryDither(this IImageProcessingContext source, IOrderedDither dither, TPixel upperColor, TPixel lowerColor, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither, upperColor, lowerColor), rectangle); + return source; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Binarization/BinaryThreshold.cs b/src/ImageSharp/Processing/Binarization/BinaryThreshold.cs index 5a165659b..3f86528f5 100644 --- a/src/ImageSharp/Processing/Binarization/BinaryThreshold.cs +++ b/src/ImageSharp/Processing/Binarization/BinaryThreshold.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Primitives; @@ -43,5 +42,40 @@ namespace SixLabors.ImageSharp source.ApplyProcessor(new BinaryThresholdProcessor(threshold), rectangle); return source; } + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The pixel format. + /// The image this method extends. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// The . + public static IImageProcessingContext BinaryThreshold(this IImageProcessingContext source, float threshold, TPixel upperColor, TPixel lowerColor) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor)); + return source; + } + + /// + /// Applies binarization to the image splitting the pixels at the given threshold. + /// + /// The pixel format. + /// The image this method extends. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext BinaryThreshold(this IImageProcessingContext source, float threshold, TPixel upperColor, TPixel lowerColor, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new BinaryThresholdProcessor(threshold, upperColor, lowerColor), rectangle); + return source; + } } } diff --git a/src/ImageSharp/Processing/Dithering/Diffuse.cs b/src/ImageSharp/Processing/Dithering/Diffuse.cs new file mode 100644 index 000000000..e6b82d313 --- /dev/null +++ b/src/ImageSharp/Processing/Dithering/Diffuse.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Dithers the image reducing it to a web-safe palette using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The . + public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold)); + return source; + } + + /// + /// Dithers the image reducing it to a web-safe palette using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold), rectangle); + return source; + } + + /// + /// Dithers the image reducing it to the given palette using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The palette to select substitute colors from. + /// The . + public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, TPixel[] palette) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette)); + return source; + } + + /// + /// Dithers the image reducing it to the given palette using error diffusion. + /// + /// The pixel format. + /// The image this method extends. + /// The diffusion algorithm to apply. + /// The threshold to apply binarization of the image. Must be between 0 and 1. + /// The palette to select substitute colors from. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The . + public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, TPixel[] palette, Rectangle rectangle) + where TPixel : struct, IPixel + { + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle); + return source; + } + } +} diff --git a/src/ImageSharp/Processing/Binarization/Dither.cs b/src/ImageSharp/Processing/Dithering/Dither.cs similarity index 50% rename from src/ImageSharp/Processing/Binarization/Dither.cs rename to src/ImageSharp/Processing/Dithering/Dither.cs index f21ccf0bd..85fdef24b 100644 --- a/src/ImageSharp/Processing/Binarization/Dither.cs +++ b/src/ImageSharp/Processing/Dithering/Dither.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using System; using SixLabors.ImageSharp.Dithering; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; @@ -15,7 +14,7 @@ namespace SixLabors.ImageSharp public static partial class ImageExtensions { /// - /// Dithers the image reducing it to two colors using ordered dithering. + /// Dithers the image reducing it to a web-safe palette using ordered dithering. /// /// The pixel format. /// The image this method extends. @@ -24,27 +23,27 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither) where TPixel : struct, IPixel { - source.ApplyProcessor(new OrderedDitherProcessor(dither, 0)); + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither)); return source; } /// - /// Dithers the image reducing it to two colors using ordered dithering. + /// Dithers the image reducing it to the given palette 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 palette to select substitute colors from. /// The . - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, int index) + public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, TPixel[] palette) where TPixel : struct, IPixel { - source.ApplyProcessor(new OrderedDitherProcessor(dither, index)); + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette)); return source; } /// - /// Dithers the image reducing it to two colors using ordered dithering. + /// Dithers the image reducing it to a web-safe palette using ordered dithering. /// /// The pixel format. /// The image this method extends. @@ -56,58 +55,25 @@ namespace SixLabors.ImageSharp public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, Rectangle rectangle) where TPixel : struct, IPixel { - source.ApplyProcessor(new OrderedDitherProcessor(dither, 0), rectangle); + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle); return source; } /// - /// Dithers the image reducing it to two colors using ordered dithering. + /// Dithers the image reducing it to the given palette using ordered dithering. /// /// The pixel format. /// The image this method extends. /// The ordered ditherer. + /// The palette to select substitute colors from. /// /// 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 IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, Rectangle rectangle, int index) + public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, TPixel[] palette, Rectangle rectangle) where TPixel : struct, IPixel { - 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. - /// The diffusion algorithm to apply. - /// The threshold to apply binarization of the image. Must be between 0 and 1. - /// The . - public static IImageProcessingContext Dither(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold) - where TPixel : struct, IPixel - { - source.ApplyProcessor(new ErrorDiffusionDitherProcessor(diffuser, threshold)); - return source; - } - - /// - /// Dithers the image reducing it to two colors using error diffusion. - /// - /// The pixel format. - /// The image this method extends. - /// The diffusion algorithm to apply. - /// The threshold to apply binarization of the image. Must be between 0 and 1. - /// - /// The structure that specifies the portion of the image object to alter. - /// - /// The . - public static IImageProcessingContext Dither(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle) - where TPixel : struct, IPixel - { - source.ApplyProcessor(new ErrorDiffusionDitherProcessor(diffuser, threshold), rectangle); + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle); return source; } } diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs new file mode 100644 index 000000000..70d903f31 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Performs binary threshold filtering against an image using error diffusion. + /// + /// The pixel format. + internal class BinaryErrorDiffusionProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser) + : this(diffuser, .5F) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + /// The threshold to split the image. Must be between 0 and 1. + public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser, float threshold) + : this(diffuser, threshold, NamedColors.White, NamedColors.Black) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + /// The threshold to split the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold. + public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser, float threshold, TPixel upperColor, TPixel lowerColor) + { + Guard.NotNull(diffuser, nameof(diffuser)); + Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); + + this.Diffuser = diffuser; + this.Threshold = threshold; + this.UpperColor = upperColor; + this.LowerColor = lowerColor; + } + + /// + /// Gets the error diffuser. + /// + public IErrorDiffuser Diffuser { get; } + + /// + /// Gets the threshold value. + /// + public float Threshold { get; } + + /// + /// Gets the color to use for pixels that are above the threshold. + /// + public TPixel UpperColor { get; } + + /// + /// Gets the color to use for pixels that fall below the threshold. + /// + public TPixel LowerColor { get; } + + /// + protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + float threshold = this.Threshold * 255F; + var rgba = default(Rgba32); + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + sourcePixel.ToRgba32(ref rgba); + + // Convert to grayscale using ITU-R Recommendation BT.709 if required + byte luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + for (int y = startY; y < endY; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = startX; x < endX; x++) + { + sourcePixel = row[x]; + + // Check if this is the same as the last pixel. If so use that value + // rather than calculating it again. This is an inexpensive optimization. + if (!previousPixel.Equals(sourcePixel)) + { + sourcePixel.ToRgba32(ref rgba); + luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + // Setup the previous pointer + previousPixel = sourcePixel; + } + + TPixel transformedPixel = luminance >= threshold ? this.UpperColor : this.LowerColor; + this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs new file mode 100644 index 000000000..3cabe378a --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Performs binary threshold filtering against an image using ordered dithering. + /// + /// The pixel format. + internal class BinaryOrderedDitherProcessor : ImageProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + public BinaryOrderedDitherProcessor(IOrderedDither dither) + : this(dither, NamedColors.White, NamedColors.Black) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold. + public BinaryOrderedDitherProcessor(IOrderedDither dither, TPixel upperColor, TPixel lowerColor) + { + Guard.NotNull(dither, nameof(dither)); + + this.Dither = dither; + this.UpperColor = upperColor; + this.LowerColor = lowerColor; + } + + /// + /// Gets the ditherer. + /// + public IOrderedDither Dither { get; } + + /// + /// Gets the color to use for pixels that are above the threshold. + /// + public TPixel UpperColor { get; } + + /// + /// Gets the color to use for pixels that fall below the threshold. + /// + public TPixel LowerColor { get; } + + /// + protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + var rgba = default(Rgba32); + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + sourcePixel.ToRgba32(ref rgba); + + // Convert to grayscale using ITU-R Recommendation BT.709 if required + byte luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + for (int y = startY; y < endY; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = startX; x < endX; x++) + { + sourcePixel = row[x]; + + // Check if this is the same as the last pixel. If so use that value + // rather than calculating it again. This is an inexpensive optimization. + if (!previousPixel.Equals(sourcePixel)) + { + sourcePixel.ToRgba32(ref rgba); + luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + // Setup the previous pointer + previousPixel = sourcePixel; + } + + this.Dither.Dither(source, sourcePixel, this.UpperColor, this.LowerColor, luminance, x, y); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs index 434ed0269..609b09092 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor.cs @@ -4,14 +4,14 @@ using System; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Helpers; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// An to perform binary threshold filtering against an - /// . The image will be converted to grayscale before thresholding occurs. + /// Performs simple binary threshold filtering against an image. /// /// The pixel format. internal class BinaryThresholdProcessor : ImageProcessor @@ -22,14 +22,22 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// The threshold to split the image. Must be between 0 and 1. public BinaryThresholdProcessor(float threshold) + : this(threshold, NamedColors.White, NamedColors.Black) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The threshold to split the image. Must be between 0 and 1. + /// The color to use for pixels that are above the threshold. + /// The color to use for pixels that are below the threshold. + public BinaryThresholdProcessor(float threshold, TPixel upperColor, TPixel lowerColor) { - // TODO: Check thresholding limit. Colors should probably have Max/Min/Middle properties. Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); this.Threshold = threshold; - - // Default to white/black for upper/lower. - this.UpperColor = NamedColors.White; - this.LowerColor = NamedColors.Black; + this.UpperColor = upperColor; + this.LowerColor = lowerColor; } /// @@ -47,55 +55,38 @@ namespace SixLabors.ImageSharp.Processing.Processors /// public TPixel LowerColor { get; set; } - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); - } - /// protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - float threshold = this.Threshold; + float threshold = this.Threshold * 255F; TPixel upper = this.UpperColor; TPixel lower = this.LowerColor; - 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; - } + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; - if (minY > 0) - { - startY = 0; - } + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); Parallel.For( - minY, - maxY, + startY, + endY, configuration.ParallelOptions, y => { - Span row = source.GetPixelRowSpan(y - startY); + Span row = source.GetPixelRowSpan(y); + var rgba = default(Rgba32); - for (int x = minX; x < maxX; x++) + for (int x = startX; x < endX; x++) { - ref TPixel color = ref row[x - startX]; + ref TPixel color = ref row[x]; + color.ToRgba32(ref rgba); - // Any channel will do since it's Grayscale. - color = color.ToVector4().X >= threshold ? upper : lower; + // Convert to grayscale using ITU-R Recommendation BT.709 if required + float luminance = isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B); + color = luminance >= threshold ? upper : lower; } }); } diff --git a/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs deleted file mode 100644 index 01cba15c4..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/ErrorDiffusionDitherProcessor.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Dithering; -using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An that dithers an image using error diffusion. - /// - /// The pixel format. - internal class ErrorDiffusionDitherProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The error diffuser - /// The threshold to split the image. Must be between 0 and 1. - public ErrorDiffusionDitherProcessor(IErrorDiffuser diffuser, float threshold) - { - Guard.NotNull(diffuser, nameof(diffuser)); - - this.Diffuser = diffuser; - this.Threshold = threshold; - - // Default to white/black for upper/lower. - this.UpperColor = NamedColors.White; - this.LowerColor = NamedColors.Black; - } - - /// - /// Gets the error diffuser. - /// - public IErrorDiffuser Diffuser { get; } - - /// - /// Gets the threshold value. - /// - public float Threshold { get; } - - /// - /// Gets or sets the color to use for pixels that are above the threshold. - /// - public TPixel UpperColor { get; set; } - - /// - /// Gets or sets the color to use for pixels that fall below the threshold. - /// - public TPixel LowerColor { get; set; } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); - } - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - int startY = interest.Y; - int endY = interest.Bottom; - int startX = interest.X; - int endX = interest.Right; - - for (int y = startY; y < endY; y++) - { - Span row = source.GetPixelRowSpan(y); - - for (int x = startX; x < endX; x++) - { - TPixel sourceColor = row[x]; - TPixel transformedColor = sourceColor.ToVector4().X >= this.Threshold ? this.UpperColor : this.LowerColor; - this.Diffuser.Dither(source, sourceColor, transformedColor, x, y, startX, startY, endX, endY); - } - } - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs deleted file mode 100644 index a37d12f18..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/OrderedDitherProcessor.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Dithering; -using SixLabors.ImageSharp.Helpers; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// An that dithers an image using error diffusion. - /// - /// The pixel format. - internal class OrderedDitherProcessor : ImageProcessor - where TPixel : struct, IPixel - { - /// - /// 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(TPixel) == typeof(Alpha8)) - { - index = 3; - } - - this.Dither = dither; - this.Index = index; - - // Default to white/black for upper/lower. - this.UpperColor = NamedColors.White; - this.LowerColor = NamedColors.Black; - } - - /// - /// 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 TPixel UpperColor { get; set; } - - /// - /// Gets or sets the color to use for pixels that fall below the threshold. - /// - public TPixel LowerColor { get; set; } - - /// - protected override void BeforeApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - new GrayscaleBt709Processor(1F).Apply(source, sourceRectangle, configuration); - } - - /// - protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - int startY = interest.Y; - int endY = interest.Bottom; - int startX = interest.X; - int endX = interest.Right; - - var rgba = default(Rgba32); - for (int y = startY; y < endY; y++) - { - Span row = source.GetPixelRowSpan(y); - - for (int x = startX; x < endX; x++) - { - TPixel sourceColor = row[x]; - this.Dither.Dither(source, sourceColor, this.UpperColor, this.LowerColor, ref rgba, this.Index, x, y); - } - } - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs new file mode 100644 index 000000000..f8ff475d1 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// An that dithers an image using error diffusion. + /// + /// The pixel format. + internal class ErrorDiffusionPaletteProcessor : PaletteDitherProcessorBase + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser) + : this(diffuser, .5F) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + /// The threshold to split the image. Must be between 0 and 1. + public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold) + : this(diffuser, threshold, NamedColors.WebSafePalette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error diffuser + /// The threshold to split the image. Must be between 0 and 1. + /// The palette to select substitute colors from. + public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, TPixel[] palette) + : base(palette) + { + Guard.NotNull(diffuser, nameof(diffuser)); + Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold)); + + this.Diffuser = diffuser; + this.Threshold = threshold; + } + + /// + /// Gets the error diffuser. + /// + public IErrorDiffuser Diffuser { get; } + + /// + /// Gets the threshold value. + /// + public float Threshold { get; } + + /// + protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + float threshold = this.Threshold * 255F; + var rgba = default(Rgba32); + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + PixelPair pair = this.GetClosestPixelPair(ref sourcePixel, this.Palette); + sourcePixel.ToRgba32(ref rgba); + + // Convert to grayscale using ITU-R Recommendation BT.709 if required + byte luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + for (int y = startY; y < endY; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = startX; x < endX; x++) + { + sourcePixel = row[x]; + + // Check if this is the same as the last pixel. If so use that value + // rather than calculating it again. This is an inexpensive optimization. + if (!previousPixel.Equals(sourcePixel)) + { + pair = this.GetClosestPixelPair(ref sourcePixel, this.Palette); + sourcePixel.ToRgba32(ref rgba); + luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + // Setup the previous pointer + previousPixel = sourcePixel; + } + + TPixel transformedPixel = luminance >= threshold ? pair.First : pair.Second; + this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs new file mode 100644 index 000000000..49455928a --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Dithering; +using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// An that dithers an image using error diffusion. + /// If no palette is given this will default to the web safe colors defined in the CSS Color Module Level 4. + /// + /// The pixel format. + internal class OrderedDitherPaletteProcessor : PaletteDitherProcessorBase + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + public OrderedDitherPaletteProcessor(IOrderedDither dither) + : this(dither, NamedColors.WebSafePalette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + /// The palette to select substitute colors from. + public OrderedDitherPaletteProcessor(IOrderedDither dither, TPixel[] palette) + : base(palette) + { + Guard.NotNull(dither, nameof(dither)); + this.Dither = dither; + } + + /// + /// Gets the ditherer. + /// + public IOrderedDither Dither { get; } + + /// + protected override void OnApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + var rgba = default(Rgba32); + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + PixelPair pair = this.GetClosestPixelPair(ref sourcePixel, this.Palette); + sourcePixel.ToRgba32(ref rgba); + + // Convert to grayscale using ITU-R Recommendation BT.709 if required + byte luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + for (int y = startY; y < endY; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = startX; x < endX; x++) + { + sourcePixel = row[x]; + + // Check if this is the same as the last pixel. If so use that value + // rather than calculating it again. This is an inexpensive optimization. + if (!previousPixel.Equals(sourcePixel)) + { + pair = this.GetClosestPixelPair(ref sourcePixel, this.Palette); + sourcePixel.ToRgba32(ref rgba); + luminance = (byte)(isAlphaOnly ? rgba.A : (.2126F * rgba.R) + (.7152F * rgba.G) + (.0722F * rgba.B)).Clamp(0, 255); + + // Setup the previous pointer + previousPixel = sourcePixel; + } + + this.Dither.Dither(source, sourcePixel, pair.First, pair.Second, luminance, x, y); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs new file mode 100644 index 000000000..c6b80293c --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// The base class for dither and diffusion processors that consume a palette. + /// + internal abstract class PaletteDitherProcessorBase : ImageProcessor + where TPixel : struct, IPixel + { + private readonly Dictionary> cache = new Dictionary>(); + + /// + /// Initializes a new instance of the class. + /// + /// The palette to select substitute colors from. + public PaletteDitherProcessorBase(TPixel[] palette) + { + Guard.NotNull(palette, nameof(palette)); + this.Palette = palette; + } + + /// + /// Gets the palette to select substitute colors from. + /// + public TPixel[] Palette { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected PixelPair GetClosestPixelPair(ref TPixel pixel, TPixel[] colorPalette) + { + // Check if the color is in the lookup table + if (this.cache.ContainsKey(pixel)) + { + return this.cache[pixel]; + } + + // Not found - loop through the palette and find the nearest match. + float leastDistance = int.MaxValue; + float secondLeastDistance = int.MaxValue; + var vector = pixel.ToVector4(); + + var closest = default(TPixel); + var secondClosest = default(TPixel); + for (int index = 0; index < colorPalette.Length; index++) + { + TPixel temp = colorPalette[index]; + var tempVector = temp.ToVector4(); + float distance = Vector4.Distance(vector, tempVector); + + if (distance < leastDistance) + { + leastDistance = distance; + secondClosest = closest; + closest = temp; + } + else if (distance < secondLeastDistance) + { + secondLeastDistance = distance; + secondClosest = temp; + } + } + + // Pop it into the cache for next time + var pair = new PixelPair(closest, secondClosest); + this.cache.Add(pixel, pair); + + return pair; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs b/src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs new file mode 100644 index 000000000..e3b9c11bd --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Represents a composite pair of pixels. Used for caching color distance lookups. + /// + /// The pixel format. + internal struct PixelPair : IEquatable> + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + /// The first pixel color + /// The second pixel color + public PixelPair(TPixel first, TPixel second) + { + this.First = first; + this.Second = second; + } + + /// + /// Gets the first pixel color + /// + public TPixel First { get; } + + /// + /// Gets the second pixel color + /// + public TPixel Second { get; } + + /// + public bool Equals(PixelPair other) + => this.First.Equals(other.First) && this.Second.Equals(other.Second); + + /// + public override bool Equals(object obj) + => obj is PixelPair other && this.First.Equals(other.First) && this.Second.Equals(other.Second); + + /// + public override int GetHashCode() + => HashHelpers.Combine(this.First.GetHashCode(), this.Second.GetHashCode()); + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs index 2d97f6584..fcd7b2e8f 100644 --- a/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs +++ b/src/ImageSharp/Processing/Processors/Filters/GrayscaleBt709Processor.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors { /// - /// Applies a greyscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.709 + /// Applies a grayscale filter matrix using the given amount and the formula as specified by ITU-R Recommendation BT.709 /// /// The pixel format. internal class GrayscaleBt709Processor : FilterProcessor diff --git a/src/ImageSharp/Quantizers/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Quantizers/OctreeQuantizer{TPixel}.cs index d646a680e..8b8db6177 100644 --- a/src/ImageSharp/Quantizers/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Quantizers/OctreeQuantizer{TPixel}.cs @@ -107,7 +107,7 @@ namespace SixLabors.ImageSharp.Quantizers if (this.Dither) { // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height, false); + this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height); } output[(y * source.Width) + x] = pixelValue; diff --git a/src/ImageSharp/Quantizers/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Quantizers/PaletteQuantizer{TPixel}.cs index 0b95c09a6..cd1b4b07b 100644 --- a/src/ImageSharp/Quantizers/PaletteQuantizer{TPixel}.cs +++ b/src/ImageSharp/Quantizers/PaletteQuantizer{TPixel}.cs @@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Quantizers { /// /// Encapsulates methods to create a quantized image based upon the given palette. + /// If no palette is given this will default to the web safe colors defined in the CSS Color Module Level 4. /// /// /// The pixel format. @@ -31,27 +32,20 @@ namespace SixLabors.ImageSharp.Quantizers /// /// Initializes a new instance of the class. /// - /// - /// The color palette. If none is given this will default to the web safe colors defined - /// in the CSS Color Module Level 4. - /// + public PaletteQuantizer() + : this(NamedColors.WebSafePalette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The palette to select substitute colors from. public PaletteQuantizer(TPixel[] palette = null) : base(true) { - if (palette == null) - { - Rgba32[] constants = ColorConstants.WebSafeColors; - TPixel[] safe = new TPixel[constants.Length + 1]; - - Span constantsBytes = constants.AsSpan().NonPortableCast(); - - PixelOperations.Instance.PackFromRgba32Bytes(constantsBytes, safe, constants.Length); - this.colors = safe; - } - else - { - this.colors = palette; - } + Guard.NotNull(palette, nameof(palette)); + this.colors = palette; } /// @@ -102,7 +96,7 @@ namespace SixLabors.ImageSharp.Quantizers if (this.Dither) { // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height, false); + this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height); } output[(y * source.Width) + x] = pixelValue; diff --git a/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs b/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs index f08114574..ce2a71da4 100644 --- a/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Quantizers/WuQuantizer{TPixel}.cs @@ -272,7 +272,7 @@ namespace SixLabors.ImageSharp.Quantizers if (this.Dither) { // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height, false); + this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, 0, 0, width, height); } output[(y * source.Width) + x] = pixelValue; diff --git a/tests/ImageSharp.Tests/Processing/Binarization/BinaryThresholdTest.cs b/tests/ImageSharp.Tests/Processing/Binarization/BinaryThresholdTest.cs index 221b4a9bf..488b3d18b 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/BinaryThresholdTest.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/BinaryThresholdTest.cs @@ -15,16 +15,40 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void BinaryThreshold_CorrectProcessor() { this.operations.BinaryThreshold(.23f); - var p = this.Verify>(); + BinaryThresholdProcessor p = this.Verify>(); Assert.Equal(.23f, p.Threshold); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); } [Fact] public void BinaryThreshold_rect_CorrectProcessor() { this.operations.BinaryThreshold(.93f, this.rect); - var p = this.Verify>(this.rect); + BinaryThresholdProcessor p = this.Verify>(this.rect); Assert.Equal(.93f, p.Threshold); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); + } + + [Fact] + public void BinaryThreshold_CorrectProcessorWithUpperLower() + { + this.operations.BinaryThreshold(.23f, NamedColors.HotPink, NamedColors.Yellow); + BinaryThresholdProcessor p = this.Verify>(); + Assert.Equal(.23f, p.Threshold); + Assert.Equal(NamedColors.HotPink, p.UpperColor); + Assert.Equal(NamedColors.Yellow, p.LowerColor); + } + + [Fact] + public void BinaryThreshold_rect_CorrectProcessorWithUpperLower() + { + this.operations.BinaryThreshold(.93f, NamedColors.HotPink, NamedColors.Yellow, this.rect); + BinaryThresholdProcessor p = this.Verify>(this.rect); + Assert.Equal(.93f, p.Threshold); + Assert.Equal(NamedColors.HotPink, p.UpperColor); + Assert.Equal(NamedColors.Yellow, p.LowerColor); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Binarization/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Binarization/DitherTests.cs index 94241d007..ba5cb0cf3 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/DitherTests.cs @@ -5,7 +5,6 @@ using SixLabors.ImageSharp.Dithering; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors; using Moq; -using SixLabors.Primitives; using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Binarization @@ -23,55 +22,84 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void Dither_CorrectProcessor() { - this.operations.Dither(orderedDither); - var p = this.Verify>(); + this.operations.BinaryDither(this.orderedDither); + BinaryOrderedDitherProcessor p = this.Verify>(); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(0, p.Index); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); } [Fact] public void Dither_rect_CorrectProcessor() { - this.operations.Dither(orderedDither, this.rect); - var p = this.Verify>(this.rect); + this.operations.BinaryDither(this.orderedDither, this.rect); + BinaryOrderedDitherProcessor p = this.Verify>(this.rect); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(0, p.Index); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); } [Fact] public void Dither_index_CorrectProcessor() { - this.operations.Dither(orderedDither, 2); - var p = this.Verify>(); + this.operations.BinaryDither(this.orderedDither, NamedColors.Yellow, NamedColors.HotPink); + BinaryOrderedDitherProcessor p = this.Verify>(); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(2, p.Index); + Assert.Equal(NamedColors.Yellow, p.UpperColor); + Assert.Equal(NamedColors.HotPink, p.LowerColor); } [Fact] public void Dither_index_rect_CorrectProcessor() { - this.operations.Dither(orderedDither, this.rect, 2); - var p = this.Verify>(this.rect); + this.operations.BinaryDither(this.orderedDither, NamedColors.Yellow, NamedColors.HotPink, this.rect); + BinaryOrderedDitherProcessor p = this.Verify>(this.rect); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(2, p.Index); + Assert.Equal(NamedColors.HotPink, p.LowerColor); } [Fact] - public void Dither_ErrorDifuser_CorrectProcessor() + public void Dither_ErrorDiffuser_CorrectProcessor() { - this.operations.Dither(errorDiffuser, 4); - var p = this.Verify>(); + this.operations.BinaryDiffuse(this.errorDiffuser, .4F); + BinaryErrorDiffusionProcessor p = this.Verify>(); Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(4, p.Threshold); + Assert.Equal(.4F, p.Threshold); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); } [Fact] - public void Dither_ErrorDifuser_rect_CorrectProcessor() + public void Dither_ErrorDiffuser_rect_CorrectProcessor() { - this.operations.Dither(this.errorDiffuser, 3, this.rect); - var p = this.Verify>(this.rect); + this.operations.BinaryDiffuse(this.errorDiffuser, .3F, this.rect); + BinaryErrorDiffusionProcessor p = this.Verify>(this.rect); Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(3, p.Threshold); + Assert.Equal(.3F, p.Threshold); + Assert.Equal(NamedColors.White, p.UpperColor); + Assert.Equal(NamedColors.Black, p.LowerColor); + } + + [Fact] + public void Dither_ErrorDiffuser_CorrectProcessorWithColors() + { + this.operations.BinaryDiffuse(this.errorDiffuser, .5F, NamedColors.HotPink, NamedColors.Yellow); + BinaryErrorDiffusionProcessor p = this.Verify>(); + Assert.Equal(this.errorDiffuser, p.Diffuser); + Assert.Equal(.5F, p.Threshold); + Assert.Equal(NamedColors.HotPink, p.UpperColor); + Assert.Equal(NamedColors.Yellow, p.LowerColor); + } + + [Fact] + public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors() + { + this.operations.BinaryDiffuse(this.errorDiffuser, .5F, NamedColors.HotPink, NamedColors.Yellow, this.rect); + BinaryErrorDiffusionProcessor p = this.Verify>(this.rect); + Assert.Equal(this.errorDiffuser, p.Diffuser); + Assert.Equal(.5F, p.Threshold); + Assert.Equal(NamedColors.HotPink, p.UpperColor); + Assert.Equal(NamedColors.Yellow, p.LowerColor); } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Binarization/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Binarization/DitherTests.cs index 9a6d24226..3ddf9d0fe 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Binarization/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Binarization/DitherTests.cs @@ -22,8 +22,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization public static readonly TheoryData Ditherers = new TheoryData { - { "Ordered", new OrderedDither() }, - { "Bayer", new BayerDither() } + { "Bayer8x8", new Bayer8x8Dither() }, + { "Bayer4x4", new Bayer4x4Dither() }, + { "Bayer2x2", new Bayer2x2Dither() } }; public static readonly TheoryData ErrorDiffusers = new TheoryData @@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization }; - private static IOrderedDither DefaultDitherer => new OrderedDither(); + private static IOrderedDither DefaultDitherer => new Bayer4x4Dither(); private static IErrorDiffuser DefaultErrorDiffuser => new AtkinsonDiffuser(); @@ -51,7 +52,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Dither(ditherer)); + image.Mutate(x => x.BinaryDither(ditherer)); image.DebugSave(provider, name); } } @@ -64,7 +65,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Dither(diffuser, .5F)); + image.Mutate(x => x.BinaryDiffuse(diffuser, .5F)); image.DebugSave(provider, name); } } @@ -76,7 +77,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Dither(DefaultDitherer)); + image.Mutate(x => x.BinaryDither(DefaultDitherer)); image.DebugSave(provider); } } @@ -88,7 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.Dither(DefaultErrorDiffuser, 0.5f)); + image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, 0.5f)); image.DebugSave(provider); } } @@ -103,7 +104,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Dither(DefaultDitherer, bounds)); + image.Mutate(x => x.BinaryDither(DefaultDitherer, bounds)); image.DebugSave(provider); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); @@ -120,7 +121,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.Dither(DefaultErrorDiffuser, .5F, bounds)); + image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, .5F, bounds)); image.DebugSave(provider); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds);