diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 142ea3f3e..e02afc83e 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -136,11 +136,11 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileDithering() where TPixel : struct, IPixel { - var test = new FloydSteinbergDiffuser(); + var test = new FloydSteinbergDither(); TPixel pixel = default; using (var image = new ImageFrame(Configuration.Default, 1, 1)) { - test.Dither(image, pixel, pixel, 0, 0, 0, 0, 0); + test.Dither(image, default, pixel, pixel, 0, 0, 0); } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index a691e527e..df7953230 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Gif else { using (IFrameQuantizer paletteFrameQuantizer = - new PaletteFrameQuantizer(this.configuration, this.quantizer.Diffuser, quantized.Palette)) + new PaletteFrameQuantizer(this.configuration, this.quantizer.Dither, quantized.Palette)) { using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame)) { diff --git a/src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs b/src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs deleted file mode 100644 index 66337f669..000000000 --- a/src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Processing.Processors.Binarization; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Defines extension methods to apply binary diffusion on an - /// using Mutate/Clone. - /// - public static class BinaryDiffuseExtensions - { - /// - /// Dithers the image reducing it to two colors using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext BinaryDiffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold) => - source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold)); - - /// - /// Dithers the image reducing it to two colors using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext BinaryDiffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - Rectangle rectangle) => - source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold), rectangle); - - /// - /// Dithers the image reducing it to two colors using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext BinaryDiffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - Color upperColor, - Color lowerColor) => - source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor)); - - /// - /// Dithers the image reducing it to two colors using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext BinaryDiffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - Color upperColor, - Color lowerColor, - Rectangle rectangle) => - source.ApplyProcessor( - new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor), - rectangle); - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs b/src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs index afd4a4941..659b538fc 100644 --- a/src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs @@ -1,7 +1,6 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.Processing.Processors.Binarization; using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing @@ -19,8 +18,8 @@ namespace SixLabors.ImageSharp.Processing /// The ordered ditherer. /// The to allow chaining of operations. public static IImageProcessingContext - BinaryDither(this IImageProcessingContext source, IOrderedDither dither) => - source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither)); + BinaryDither(this IImageProcessingContext source, IDither dither) => + BinaryDither(source, dither, Color.White, Color.Black); /// /// Dithers the image reducing it to two colors using ordered dithering. @@ -32,10 +31,10 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext BinaryDither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, Color upperColor, Color lowerColor) => - source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither, upperColor, lowerColor)); + source.ApplyProcessor(new PaletteDitherProcessor(dither, new[] { upperColor, lowerColor })); /// /// Dithers the image reducing it to two colors using ordered dithering. @@ -48,9 +47,9 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext BinaryDither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, Rectangle rectangle) => - source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither), rectangle); + BinaryDither(source, dither, Color.White, Color.Black, rectangle); /// /// Dithers the image reducing it to two colors using ordered dithering. @@ -65,10 +64,10 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext BinaryDither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, Color upperColor, Color lowerColor, Rectangle rectangle) => - source.ApplyProcessor(new BinaryOrderedDitherProcessor(dither, upperColor, lowerColor), rectangle); + source.ApplyProcessor(new PaletteDitherProcessor(dither, new[] { upperColor, lowerColor }), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs b/src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs deleted file mode 100644 index 92d312fdf..000000000 --- a/src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Defines extension methods to apply diffusion to an - /// using Mutate/Clone. - /// - public static class DiffuseExtensions - { - /// - /// Dithers the image reducing it to a web-safe palette using error diffusion. - /// - /// The image this method extends. - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source) => - Diffuse(source, KnownDiffusers.FloydSteinberg, .5F); - - /// - /// Dithers the image reducing it to a web-safe palette using error diffusion. - /// - /// The image this method extends. - /// The threshold to apply binarization of the image. Must be between 0 and 1. - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source, float threshold) => - Diffuse(source, KnownDiffusers.FloydSteinberg, threshold); - - /// - /// Dithers the image reducing it to a web-safe palette using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext Diffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold) => - source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold)); - - /// - /// Dithers the image reducing it to a web-safe palette using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext Diffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - Rectangle rectangle) => - source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold), rectangle); - - /// - /// Dithers the image reducing it to the given palette using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext Diffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - ReadOnlyMemory palette) => - source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette)); - - /// - /// Dithers the image reducing it to the given palette using error diffusion. - /// - /// 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 to allow chaining of operations. - public static IImageProcessingContext Diffuse( - this IImageProcessingContext source, - IErrorDiffuser diffuser, - float threshold, - ReadOnlyMemory palette, - Rectangle rectangle) => - source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle); - } -} diff --git a/src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs b/src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs index f58b025f3..516bd5545 100644 --- a/src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs @@ -1,8 +1,7 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing @@ -27,8 +26,8 @@ namespace SixLabors.ImageSharp.Processing /// The image this method extends. /// The ordered ditherer. /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither)); + public static IImageProcessingContext Dither(this IImageProcessingContext source, IDither dither) => + source.ApplyProcessor(new PaletteDitherProcessor(dither)); /// /// Dithers the image reducing it to the given palette using ordered dithering. @@ -39,9 +38,9 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Dither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, ReadOnlyMemory palette) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette)); + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette)); /// /// Dithers the image reducing it to a web-safe palette using ordered dithering. @@ -54,9 +53,9 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Dither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, Rectangle rectangle) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle); + source.ApplyProcessor(new PaletteDitherProcessor(dither), rectangle); /// /// Dithers the image reducing it to the given palette using ordered dithering. @@ -70,9 +69,9 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Dither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, ReadOnlyMemory palette, Rectangle rectangle) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle); + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/KnownDiffusers.cs b/src/ImageSharp/Processing/KnownDiffusers.cs deleted file mode 100644 index 2b10312fe..000000000 --- a/src/ImageSharp/Processing/KnownDiffusers.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing -{ - /// - /// Contains reusable static instances of known error diffusion algorithms - /// - public static class KnownDiffusers - { - /// - /// Gets the error diffuser that implements the Atkinson algorithm. - /// - public static IErrorDiffuser Atkinson { get; } = new AtkinsonDiffuser(); - - /// - /// Gets the error diffuser that implements the Burks algorithm. - /// - public static IErrorDiffuser Burks { get; } = new BurksDiffuser(); - - /// - /// Gets the error diffuser that implements the Floyd-Steinberg algorithm. - /// - public static IErrorDiffuser FloydSteinberg { get; } = new FloydSteinbergDiffuser(); - - /// - /// Gets the error diffuser that implements the Jarvis-Judice-Ninke algorithm. - /// - public static IErrorDiffuser JarvisJudiceNinke { get; } = new JarvisJudiceNinkeDiffuser(); - - /// - /// Gets the error diffuser that implements the Sierra-2 algorithm. - /// - public static IErrorDiffuser Sierra2 { get; } = new Sierra2Diffuser(); - - /// - /// Gets the error diffuser that implements the Sierra-3 algorithm. - /// - public static IErrorDiffuser Sierra3 { get; } = new Sierra3Diffuser(); - - /// - /// Gets the error diffuser that implements the Sierra-Lite algorithm. - /// - public static IErrorDiffuser SierraLite { get; } = new SierraLiteDiffuser(); - - /// - /// Gets the error diffuser that implements the Stevenson-Arce algorithm. - /// - public static IErrorDiffuser StevensonArce { get; } = new StevensonArceDiffuser(); - - /// - /// Gets the error diffuser that implements the Stucki algorithm. - /// - public static IErrorDiffuser Stucki { get; } = new StuckiDiffuser(); - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/KnownDitherers.cs b/src/ImageSharp/Processing/KnownDitherers.cs index dad5bb38c..8e3653b52 100644 --- a/src/ImageSharp/Processing/KnownDitherers.cs +++ b/src/ImageSharp/Processing/KnownDitherers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -13,21 +13,66 @@ namespace SixLabors.ImageSharp.Processing /// /// Gets the order ditherer using the 2x2 Bayer dithering matrix /// - public static IOrderedDither BayerDither2x2 { get; } = new BayerDither2x2(); + public static IDither BayerDither2x2 { get; } = new BayerDither2x2(); /// /// Gets the order ditherer using the 3x3 dithering matrix /// - public static IOrderedDither OrderedDither3x3 { get; } = new OrderedDither3x3(); + public static IDither OrderedDither3x3 { get; } = new OrderedDither3x3(); /// /// Gets the order ditherer using the 4x4 Bayer dithering matrix /// - public static IOrderedDither BayerDither4x4 { get; } = new BayerDither4x4(); + public static IDither BayerDither4x4 { get; } = new BayerDither4x4(); /// /// Gets the order ditherer using the 8x8 Bayer dithering matrix /// - public static IOrderedDither BayerDither8x8 { get; } = new BayerDither8x8(); + public static IDither BayerDither8x8 { get; } = new BayerDither8x8(); + + /// + /// Gets the error Dither that implements the Atkinson algorithm. + /// + public static IDither Atkinson { get; } = new AtkinsonDither(); + + /// + /// Gets the error Dither that implements the Burks algorithm. + /// + public static IDither Burks { get; } = new BurksDither(); + + /// + /// Gets the error Dither that implements the Floyd-Steinberg algorithm. + /// + public static IDither FloydSteinberg { get; } = new FloydSteinbergDither(); + + /// + /// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm. + /// + public static IDither JarvisJudiceNinke { get; } = new JarvisJudiceNinkeDither(); + + /// + /// Gets the error Dither that implements the Sierra-2 algorithm. + /// + public static IDither Sierra2 { get; } = new Sierra2Dither(); + + /// + /// Gets the error Dither that implements the Sierra-3 algorithm. + /// + public static IDither Sierra3 { get; } = new Sierra3Dither(); + + /// + /// Gets the error Dither that implements the Sierra-Lite algorithm. + /// + public static IDither SierraLite { get; } = new SierraLiteDither(); + + /// + /// Gets the error Dither that implements the Stevenson-Arce algorithm. + /// + public static IDither StevensonArce { get; } = new StevensonArceDither(); + + /// + /// Gets the error Dither that implements the Stucki algorithm. + /// + public static IDither Stucki { get; } = new StuckiDither(); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs deleted file mode 100644 index 287853979..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing.Processors.Binarization -{ - /// - /// Performs binary threshold filtering against an image using error diffusion. - /// - public class BinaryErrorDiffusionProcessor : IImageProcessor - { - /// - /// 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, Color.White, Color.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, Color upperColor, Color 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 Color UpperColor { get; } - - /// - /// Gets the color to use for pixels that fall below the threshold. - /// - public Color LowerColor { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : struct, IPixel - => new BinaryErrorDiffusionProcessor(configuration, this, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs deleted file mode 100644 index 262e9d024..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs +++ /dev/null @@ -1,84 +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.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing.Processors.Binarization -{ - /// - /// Performs binary threshold filtering against an image using error diffusion. - /// - /// The pixel format. - internal sealed class BinaryErrorDiffusionProcessor : ImageProcessor - where TPixel : struct, IPixel - { - private readonly BinaryErrorDiffusionProcessor definition; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public BinaryErrorDiffusionProcessor(Configuration configuration, BinaryErrorDiffusionProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - this.definition = definition; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - TPixel upperColor = this.definition.UpperColor.ToPixel(); - TPixel lowerColor = this.definition.LowerColor.ToPixel(); - IErrorDiffuser diffuser = this.definition.Diffuser; - - byte threshold = (byte)MathF.Round(this.definition.Threshold * 255F); - bool isAlphaOnly = typeof(TPixel) == typeof(A8); - - var interest = Rectangle.Intersect(this.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; - Rgba32 rgba = default; - sourcePixel.ToRgba32(ref rgba); - - // Convert to grayscale using ITU-R Recommendation BT.709 if required - byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - 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 = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - // Setup the previous pointer - previousPixel = sourcePixel; - } - - TPixel transformedPixel = luminance >= threshold ? upperColor : lowerColor; - diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, endX, endY); - } - } - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs deleted file mode 100644 index 1626bbe80..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs +++ /dev/null @@ -1,58 +0,0 @@ -// 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.Dithering; - -namespace SixLabors.ImageSharp.Processing.Processors.Binarization -{ - /// - /// Defines a binary threshold filtering using ordered dithering. - /// - public class BinaryOrderedDitherProcessor : IImageProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// The ordered ditherer. - public BinaryOrderedDitherProcessor(IOrderedDither dither) - : this(dither, Color.White, Color.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, Color upperColor, Color lowerColor) - { - this.Dither = dither ?? throw new ArgumentNullException(nameof(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 Color UpperColor { get; } - - /// - /// Gets the color to use for pixels that fall below the threshold. - /// - public Color LowerColor { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : struct, IPixel - => new BinaryOrderedDitherProcessor(configuration, this, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs deleted file mode 100644 index 66b92d1ce..000000000 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs +++ /dev/null @@ -1,82 +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.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing.Processors.Binarization -{ - /// - /// Performs binary threshold filtering against an image using ordered dithering. - /// - /// The pixel format. - internal class BinaryOrderedDitherProcessor : ImageProcessor - where TPixel : struct, IPixel - { - private readonly BinaryOrderedDitherProcessor definition; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public BinaryOrderedDitherProcessor(Configuration configuration, BinaryOrderedDitherProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - this.definition = definition; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - IOrderedDither dither = this.definition.Dither; - TPixel upperColor = this.definition.UpperColor.ToPixel(); - TPixel lowerColor = this.definition.LowerColor.ToPixel(); - - bool isAlphaOnly = typeof(TPixel) == typeof(A8); - - var interest = Rectangle.Intersect(this.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; - Rgba32 rgba = default; - sourcePixel.ToRgba32(ref rgba); - - // Convert to grayscale using ITU-R Recommendation BT.709 if required - byte luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - 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 = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - // Setup the previous pointer - previousPixel = sourcePixel; - } - - dither.Dither(source, sourcePixel, upperColor, lowerColor, luminance, x, y); - } - } - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDither.cs similarity index 83% rename from src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/AtkinsonDither.cs index 9cf10ce59..635777bf3 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Atkinson image dithering algorithm. /// /// - public sealed class AtkinsonDiffuser : ErrorDiffuser + public sealed class AtkinsonDither : ErrorDither { private const float Divisor = 8F; + private const int Offset = 1; /// /// The diffusion matrix @@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public AtkinsonDiffuser() - : base(AtkinsonMatrix) + public AtkinsonDither() + : base(AtkinsonMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/BurksDither.cs similarity index 83% rename from src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/BurksDither.cs index 152704ec2..f7ac30e68 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/BurksDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Burks image dithering algorithm. /// /// - public sealed class BurksDiffuser : ErrorDiffuser + public sealed class BurksDither : ErrorDither { private const float Divisor = 32F; + private const int Offset = 2; /// /// The diffusion matrix @@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public BurksDiffuser() - : base(BurksMatrix) + public BurksDither() + : base(BurksMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs b/src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs new file mode 100644 index 000000000..682363064 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// Enumerates the possible dithering algorithm transform behaviors. + /// + public enum DitherTransformColorBehavior + { + /// + /// The transformed color should be precalulated and passed to the dithering algorithm. + /// + PreOperation, + + /// + /// The transformed color should be calculated as a result of the dithering algorithm. + /// + PostOperation + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs deleted file mode 100644 index 059816065..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Defines a dither operation using error diffusion. - /// If no palette is given this will default to the web safe colors defined in the CSS Color Module Level 4. - /// - public sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor - { - /// - /// 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, Color.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, ReadOnlyMemory 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; } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - { - return new ErrorDiffusionPaletteProcessor(configuration, this, source, sourceRectangle); - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs deleted file mode 100644 index f0c8610ed..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.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.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// An that dithers an image using error diffusion. - /// - /// The pixel format. - internal sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public ErrorDiffusionPaletteProcessor(Configuration configuration, ErrorDiffusionPaletteProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, definition, source, sourceRectangle) - { - } - - private new ErrorDiffusionPaletteProcessor Definition => (ErrorDiffusionPaletteProcessor)base.Definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - byte threshold = (byte)MathF.Round(this.Definition.Threshold * 255F); - var interest = Rectangle.Intersect(this.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); - Rgba32 rgba = default; - sourcePixel.ToRgba32(ref rgba); - - // Convert to grayscale using ITU-R Recommendation BT.709 if required - byte luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - 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); - - // No error to spread, exact match. - if (sourcePixel.Equals(pair.First)) - { - continue; - } - - sourcePixel.ToRgba32(ref rgba); - luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - // Setup the previous pointer - previousPixel = sourcePixel; - } - - TPixel transformedPixel = luminance >= threshold ? pair.Second : pair.First; - this.Definition.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, endX, endY); - } - } - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs similarity index 56% rename from src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index e8597bc9d..2ab570610 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -3,78 +3,60 @@ using System; using System.Numerics; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// - /// The base class for performing error diffusion based dithering. + /// The base class of all error diffusion dithering implementations. /// - public abstract class ErrorDiffuser : IErrorDiffuser + public abstract class ErrorDither : IDither { private readonly int offset; private readonly DenseMatrix matrix; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The dithering matrix. - internal ErrorDiffuser(in DenseMatrix matrix) + /// The diffusion matrix. + /// The starting offset within the matrix. + protected ErrorDither(in DenseMatrix matrix, int offset) { - // Calculate the offset position of the pixel relative to - // the diffusion matrix. - this.offset = 0; - - for (int col = 0; col < matrix.Columns; col++) - { - if (matrix[0, col] != 0) - { - this.offset = col - 1; - break; - } - } - this.matrix = matrix; + this.offset = offset; } - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY) + /// + public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PreOperation; + + /// + public TPixel Dither( + ImageFrame image, + Rectangle bounds, + TPixel source, + TPixel transformed, + int x, + int y, + int bitDepth) where TPixel : struct, IPixel { - image[x, y] = transformed; - // Equal? Break out as there's no error to pass. if (source.Equals(transformed)) { - return; + return transformed; } // Calculate the error Vector4 error = source.ToVector4() - transformed.ToVector4(); - if (Vector4.Dot(error, error) > 16F / 255F) - { - error *= .75F; - } - - this.DoDither(image, x, y, minX, maxX, maxY, error); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private void DoDither(ImageFrame image, int x, int y, int minX, int maxX, int maxY, Vector4 error) - where TPixel : struct, IPixel - { int offset = this.offset; DenseMatrix matrix = this.matrix; // Loop through and distribute the error amongst neighboring pixels. for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++) { - // TODO: Quantize rectangle. - if (targetY >= maxY) + if (targetY >= bounds.Bottom) { continue; } @@ -84,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering for (int col = 0; col < matrix.Columns; col++) { int targetX = x + (col - offset); - if (targetX < minX || targetX >= maxX) + if (targetX < bounds.Left || targetX >= bounds.Right) { continue; } @@ -102,6 +84,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering pixel.FromVector4(result); } } + + return transformed; } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/EuclideanPixelMap{TPixel}.cs new file mode 100644 index 000000000..9bbdd72c4 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/EuclideanPixelMap{TPixel}.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Concurrent; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// Gets the closest color to the supplied color based upon the Eucladean distance. + /// + /// The pixel format. + internal sealed class EuclideanPixelMap + where TPixel : struct, IPixel + { + private readonly ReadOnlyMemory palette; + private readonly ConcurrentDictionary vectorCache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary distanceCache = new ConcurrentDictionary(); + + public EuclideanPixelMap(ReadOnlyMemory palette) + { + this.palette = palette; + ReadOnlySpan paletteSpan = this.palette.Span; + + for (int i = 0; i < paletteSpan.Length; i++) + { + this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte GetClosestColor(TPixel color, out TPixel match) + { + ReadOnlySpan paletteSpan = this.palette.Span; + + // Check if the color is in the lookup table + if (this.distanceCache.TryGetValue(color, out byte index)) + { + match = paletteSpan[index]; + return index; + } + + return this.GetClosestColorSlow(color, paletteSpan, out match); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private byte GetClosestColorSlow(TPixel color, ReadOnlySpan palette, out TPixel match) + { + // Loop through the palette and find the nearest match. + int index = 0; + float leastDistance = float.MaxValue; + Vector4 vector = color.ToScaledVector4(); + + for (int i = 0; i < palette.Length; i++) + { + Vector4 candidate = this.vectorCache[i]; + float distance = Vector4.DistanceSquared(vector, candidate); + + // Greater... Move on. + if (leastDistance < distance) + { + continue; + } + + index = i; + leastDistance = distance; + + // And if it's an exact match, exit the loop + if (distance == 0) + { + break; + } + } + + // Now I have the index, pop it into the cache for next time + var result = (byte)index; + this.distanceCache[color] = result; + match = palette[index]; + return result; + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDither.cs similarity index 80% rename from src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDither.cs index b3137337b..4dc8b5441 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm. /// /// - public sealed class FloydSteinbergDiffuser : ErrorDiffuser + public sealed class FloydSteinbergDither : ErrorDither { private const float Divisor = 16F; + private const int Offset = 1; /// /// The diffusion matrix @@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public FloydSteinbergDiffuser() - : base(FloydSteinbergMatrix) + public FloydSteinbergDither() + : base(FloydSteinbergMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs new file mode 100644 index 000000000..45c9d4b58 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// Defines the contract for types that apply dithering to images. + /// + public interface IDither + { + /// + /// Gets the which determines whether the + /// transformed color should be calculated and supplied to the algorithm. + /// + public DitherTransformColorBehavior TransformColorBehavior { get; } + + /// + /// Transforms the image applying a dither matrix. + /// When is this + /// this method is destructive and will alter the input pixels. + /// + /// The image. + /// The region of interest bounds. + /// The source pixel + /// The transformed pixel + /// The column index. + /// The row index. + /// The bit depth of the target palette. + /// The pixel format. + /// The dithered result for the source pixel. + TPixel Dither( + ImageFrame image, + Rectangle bounds, + TPixel source, + TPixel transformed, + int x, + int y, + int bitDepth) + where TPixel : struct, IPixel; + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs deleted file mode 100644 index 8f4381d30..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Encapsulates properties and methods required to perform diffused error dithering on an image. - /// - public interface IErrorDiffuser - { - /// - /// 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 maximum column value. - /// The maximum row value. - /// The pixel format. - void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY) - where TPixel : struct, IPixel; - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs deleted file mode 100644 index 571929b99..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Encapsulates properties and methods required to perform ordered dithering on an image. - /// - public interface IOrderedDither - { - /// - /// Transforms the image applying the dither matrix. This method alters the input pixels array - /// - /// The image - /// The source pixel - /// The color to apply to the pixels above the threshold. - /// The color to apply to the pixels below the threshold. - /// 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, float threshold, int x, int y) - where TPixel : struct, IPixel; - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDither.cs similarity index 82% rename from src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDither.cs index 40cf79266..43431c01d 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the JarvisJudiceNinke image dithering algorithm. /// /// - public sealed class JarvisJudiceNinkeDiffuser : ErrorDiffuser + public sealed class JarvisJudiceNinkeDither : ErrorDither { private const float Divisor = 48F; + private const int Offset = 2; /// /// The diffusion matrix @@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public JarvisJudiceNinkeDiffuser() - : base(JarvisJudiceNinkeMatrix) + public JarvisJudiceNinkeDither() + : base(JarvisJudiceNinkeMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 34eff4fe9..0e15c700f 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -1,6 +1,7 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Dithering @@ -8,9 +9,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// An ordered dithering matrix with equal sides of arbitrary length /// - public class OrderedDither : IOrderedDither + public class OrderedDither : IDither { - private readonly DenseMatrix thresholdMatrix; + private readonly DenseMatrix thresholdMatrix; private readonly int modulusX; private readonly int modulusY; @@ -21,29 +22,62 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering public OrderedDither(uint length) { DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); - this.modulusX = ditherMatrix.Columns; - this.modulusY = ditherMatrix.Rows; - // Adjust the matrix range for 0-255 - // TODO: It looks like it's actually possible to dither an image using it's own colors. We should investigate for V2 - // https://stackoverflow.com/questions/12422407/monochrome-dithering-in-javascript-bayer-atkinson-floyd-steinberg - int multiplier = 256 / ditherMatrix.Count; - for (int y = 0; y < ditherMatrix.Rows; y++) + // Create a new matrix to run against, that pre-thresholds the values. + // We don't want to adjust the original matrix generation code as that + // creates known, easy to test values. + // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm + var thresholdMatrix = new DenseMatrix((int)length); + float m2 = length * length; + for (int y = 0; y < length; y++) { - for (int x = 0; x < ditherMatrix.Columns; x++) + for (int x = 0; x < length; y++) { - ditherMatrix[y, x] = (uint)((ditherMatrix[y, x] + 1) * multiplier) - 1; + thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F; } } - this.thresholdMatrix = ditherMatrix; + this.modulusX = ditherMatrix.Columns; + this.modulusY = ditherMatrix.Rows; + this.thresholdMatrix = thresholdMatrix; } - /// - public void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y) + /// + public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PostOperation; + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public TPixel Dither( + ImageFrame image, + Rectangle bounds, + TPixel source, + TPixel transformed, + int x, + int y, + int bitDepth) where TPixel : struct, IPixel { - image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper; + // TODO: Should we consider a pixel format with a larger coror range? + Rgba32 rgba = default; + source.ToRgba32(ref rgba); + Rgba32 attempt; + + // Srpead assumes an even colorspace distribution and precision. + // Calculated as 0-255/component count. 256 / bitDepth + // https://bisqwit.iki.fi/story/howto/dither/jy/ + // https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm + int spread = 256 / bitDepth; + float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX]; + + attempt.R = (byte)(rgba.R + factor).Clamp(byte.MinValue, byte.MaxValue); + attempt.G = (byte)(rgba.G + factor).Clamp(byte.MinValue, byte.MaxValue); + attempt.B = (byte)(rgba.B + factor).Clamp(byte.MinValue, byte.MaxValue); + attempt.A = (byte)(rgba.A + factor).Clamp(byte.MinValue, byte.MaxValue); + + TPixel result = default; + result.FromRgba32(attempt); + + return result; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs index f4835f421..48aaa22d6 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.Runtime.CompilerServices; @@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering { // Calculate the the logarithm of length to the base 2 uint exponent = 0; - uint bayerLength = 0; + uint bayerLength; do { exponent++; @@ -90,4 +90,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering return result; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs deleted file mode 100644 index e28c662f8..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Defines a dithering operation 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. - /// - public sealed class OrderedDitherPaletteProcessor : PaletteDitherProcessor - { - /// - /// Initializes a new instance of the class. - /// - /// The ordered ditherer. - public OrderedDitherPaletteProcessor(IOrderedDither dither) - : this(dither, Color.WebSafePalette) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The ordered ditherer. - /// The palette to select substitute colors from. - public OrderedDitherPaletteProcessor(IOrderedDither dither, ReadOnlyMemory palette) - : base(palette) => this.Dither = dither ?? throw new ArgumentNullException(nameof(dither)); - - /// - /// Gets the ditherer. - /// - public IOrderedDither Dither { get; } - - /// - public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - => new OrderedDitherPaletteProcessor(configuration, this, source, sourceRectangle); - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs deleted file mode 100644 index 29baa9750..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs +++ /dev/null @@ -1,83 +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.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// An that dithers an image using error diffusion. - /// - /// The pixel format. - internal class OrderedDitherPaletteProcessor : PaletteDitherProcessor - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. - /// The source for the current processor instance. - /// The source area to process for the current processor instance. - public OrderedDitherPaletteProcessor(Configuration configuration, OrderedDitherPaletteProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, definition, source, sourceRectangle) - { - } - - private new OrderedDitherPaletteProcessor Definition => (OrderedDitherPaletteProcessor)base.Definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - var interest = Rectangle.Intersect(this.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); - Rgba32 rgba = default; - sourcePixel.ToRgba32(ref rgba); - - // Convert to grayscale using ITU-R Recommendation BT.709 if required - byte luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - 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); - - // No error to spread, exact match. - if (sourcePixel.Equals(pair.First)) - { - continue; - } - - sourcePixel.ToRgba32(ref rgba); - luminance = ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - // Setup the previous pointer - previousPixel = sourcePixel; - } - - this.Definition.Dither.Dither(source, sourcePixel, pair.Second, pair.First, luminance, x, y); - } - } - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs index 0a1552c11..c7abb308f 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs @@ -2,32 +2,48 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// - /// The base class for dither and diffusion processors that consume a palette. + /// Allows the consumption a palette to dither an image. /// - public abstract class PaletteDitherProcessor : IImageProcessor + public sealed class PaletteDitherProcessor : IImageProcessor { /// /// Initializes a new instance of the class. /// + /// The ordered ditherer. + public PaletteDitherProcessor(IDither dither) + : this(dither, Color.WebSafePalette) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dithering algorithm. /// The palette to select substitute colors from. - protected PaletteDitherProcessor(ReadOnlyMemory palette) + public PaletteDitherProcessor(IDither dither, ReadOnlyMemory palette) { + this.Dither = dither ?? throw new ArgumentNullException(nameof(dither)); this.Palette = palette; } + /// + /// Gets the dithering algorithm. + /// + public IDither Dither { get; } + /// /// Gets the palette to select substitute colors from. /// public ReadOnlyMemory Palette { get; } /// - public abstract IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : struct, IPixel; + public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + where TPixel : struct, IPixel + => new PaletteDitherProcessor(configuration, this, source, sourceRectangle); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index c9f09fc62..ed7e3a353 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -3,25 +3,24 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// - /// The base class for dither and diffusion processors that consume a palette. + /// Allows the consumption a palette to dither an image. /// /// The pixel format. - internal abstract class PaletteDitherProcessor : ImageProcessor + internal sealed class PaletteDitherProcessor : ImageProcessor where TPixel : struct, IPixel { - private readonly Dictionary> cache = new Dictionary>(); + private readonly int paletteLength; + private readonly int bitDepth; + private readonly IDither dither; + private readonly ReadOnlyMemory sourcePalette; private IMemoryOwner palette; - private IMemoryOwner paletteVector; - private bool palleteVectorMapped; + private EuclideanPixelMap pixelMap; private bool isDisposed; /// @@ -31,34 +30,67 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// The defining the processor parameters. /// The source for the current processor instance. /// The source area to process for the current processor instance. - protected PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle) + public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.Definition = definition; - this.palette = this.Configuration.MemoryAllocator.Allocate(definition.Palette.Length); - this.paletteVector = this.Configuration.MemoryAllocator.Allocate(definition.Palette.Length); + this.paletteLength = definition.Palette.Span.Length; + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(this.paletteLength); + this.dither = definition.Dither; + this.sourcePalette = definition.Palette; } - protected PaletteDitherProcessor Definition { get; } + /// + protected override void OnFrameApply(ImageFrame source) + { + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + + // Error diffusion. The difference between the source and transformed color + // is spread to neighboring pixels. + if (this.dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) + { + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = interest.Left; x < interest.Right; x++) + { + TPixel sourcePixel = row[x]; + this.pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); + this.dither.Dither(source, interest, sourcePixel, transformed, x, y, this.bitDepth); + row[x] = transformed; + } + } + + return; + } + + // TODO: This can be parallel. + // Ordered dithering. We are only operating on a single pixel. + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + + for (int x = interest.Left; x < interest.Right; x++) + { + TPixel dithered = this.dither.Dither(source, interest, row[x], default, x, y, this.bitDepth); + this.pixelMap.GetClosestColor(dithered, out TPixel transformed); + row[x] = transformed; + } + } + } /// protected override void BeforeFrameApply(ImageFrame source) { // Lazy init palettes: - if (!this.palleteVectorMapped) + if (this.pixelMap is null) { - ReadOnlySpan sourcePalette = this.Definition.Palette.Span; + this.palette = this.Configuration.MemoryAllocator.Allocate(this.paletteLength); + ReadOnlySpan sourcePalette = this.sourcePalette.Span; Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span); - - PixelOperations.Instance.ToVector4( - this.Configuration, - this.palette.Memory.Span, - this.paletteVector.Memory.Span, - PixelConversionModifiers.Scale); + this.pixelMap = new EuclideanPixelMap(this.palette.Memory); } - this.palleteVectorMapped = true; - base.BeforeFrameApply(source); } @@ -73,71 +105,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering if (disposing) { this.palette?.Dispose(); - this.paletteVector?.Dispose(); } this.palette = null; - this.paletteVector = null; this.isDisposed = true; base.Dispose(disposing); } - - /// - /// Returns the two closest colors from the palette calculated via Euclidean distance in the Rgba space. - /// - /// The source color to match. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected PixelPair GetClosestPixelPair(ref TPixel pixel) - { - // Check if the color is in the lookup table - if (this.cache.TryGetValue(pixel, out PixelPair value)) - { - return value; - } - - return this.GetClosestPixelPairSlow(ref pixel); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private PixelPair GetClosestPixelPairSlow(ref TPixel pixel) - { - // Not found - loop through the palette and find the nearest match. - float leastDistance = float.MaxValue; - float secondLeastDistance = float.MaxValue; - var vector = pixel.ToVector4(); - - TPixel closest = default; - TPixel secondClosest = default; - Span paletteSpan = this.palette.Memory.Span; - ref TPixel paletteSpanBase = ref MemoryMarshal.GetReference(paletteSpan); - Span paletteVectorSpan = this.paletteVector.Memory.Span; - ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan); - - for (int index = 0; index < paletteVectorSpan.Length; index++) - { - ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, index); - float distance = Vector4.DistanceSquared(vector, candidate); - - if (distance < leastDistance) - { - leastDistance = distance; - secondClosest = closest; - closest = Unsafe.Add(ref paletteSpanBase, index); - } - else if (distance < secondLeastDistance) - { - secondLeastDistance = distance; - secondClosest = Unsafe.Add(ref paletteSpanBase, index); - } - } - - // Pop it into the cache for next time - var pair = new PixelPair(closest, secondClosest); - this.cache.Add(pixel, pair); - - return pair; - } } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/Sierra2Dither.cs similarity index 83% rename from src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/Sierra2Dither.cs index 001df19af..36b9577b1 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/Sierra2Dither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Sierra2 image dithering algorithm. /// /// - public sealed class Sierra2Diffuser : ErrorDiffuser + public sealed class Sierra2Dither : ErrorDither { private const float Divisor = 16F; + private const int Offset = 2; /// /// The diffusion matrix @@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public Sierra2Diffuser() - : base(Sierra2Matrix) + public Sierra2Dither() + : base(Sierra2Matrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/Sierra3Dither.cs similarity index 84% rename from src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/Sierra3Dither.cs index 3e56c63b3..25baa9b40 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/Sierra3Dither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Sierra3 image dithering algorithm. /// /// - public sealed class Sierra3Diffuser : ErrorDiffuser + public sealed class Sierra3Dither : ErrorDither { private const float Divisor = 32F; + private const int Offset = 2; /// /// The diffusion matrix @@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public Sierra3Diffuser() - : base(Sierra3Matrix) + public Sierra3Dither() + : base(Sierra3Matrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDither.cs similarity index 81% rename from src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/SierraLiteDither.cs index 763695d66..55b1a9048 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the SierraLite image dithering algorithm. /// /// - public sealed class SierraLiteDiffuser : ErrorDiffuser + public sealed class SierraLiteDither : ErrorDither { private const float Divisor = 4F; + private const int Offset = 1; /// /// The diffusion matrix @@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public SierraLiteDiffuser() - : base(SierraLiteMatrix) + public SierraLiteDither() + : base(SierraLiteMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDither.cs similarity index 83% rename from src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/StevensonArceDither.cs index 72ff30c11..e4287a53f 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDither.cs @@ -6,9 +6,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm. /// - public sealed class StevensonArceDiffuser : ErrorDiffuser + public sealed class StevensonArceDither : ErrorDither { private const float Divisor = 200F; + private const int Offset = 3; /// /// The diffusion matrix @@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public StevensonArceDiffuser() - : base(StevensonArceMatrix) + public StevensonArceDither() + : base(StevensonArceMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/StuckiDither.cs similarity index 84% rename from src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs rename to src/ImageSharp/Processing/Processors/Dithering/StuckiDither.cs index 78e8fb4e4..a50f304a4 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/StuckiDither.cs @@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// Applies error diffusion based dithering using the Stucki image dithering algorithm. /// /// - public sealed class StuckiDiffuser : ErrorDiffuser + public sealed class StuckiDither : ErrorDither { private const float Divisor = 42F; + private const int Offset = 2; /// /// The diffusion matrix @@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering }; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public StuckiDiffuser() - : base(StuckiMatrix) + public StuckiDither() + : base(StuckiMatrix, Offset) { } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs index eb3838d21..c5c729300 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs @@ -2,11 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; -using System.Collections.Generic; -using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -20,21 +17,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public abstract class FrameQuantizer : IFrameQuantizer where TPixel : struct, IPixel { - /// - /// A lookup table for colors - /// - private readonly Dictionary distanceCache = new Dictionary(); - /// /// Flag used to indicate whether a single pass or two passes are needed for quantization. /// private readonly bool singlePass; - /// - /// The vector representation of the image palette. - /// - private IMemoryOwner paletteVector; - + private EuclideanPixelMap pixelMap; private bool isDisposed; /// @@ -55,8 +43,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Guard.NotNull(quantizer, nameof(quantizer)); this.Configuration = configuration; - this.Diffuser = quantizer.Diffuser; - this.Dither = this.Diffuser != null; + this.Dither = quantizer.Dither; + this.DoDither = this.Dither != null; this.singlePass = singlePass; } @@ -73,19 +61,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// only call the method. /// If two passes are required, the code will also call . /// - protected FrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, bool singlePass) + protected FrameQuantizer(Configuration configuration, IDither diffuser, bool singlePass) { this.Configuration = configuration; - this.Diffuser = diffuser; - this.Dither = this.Diffuser != null; + this.Dither = diffuser; + this.DoDither = this.Dither != null; this.singlePass = singlePass; } /// - public IErrorDiffuser Diffuser { get; } + public IDither Dither { get; } /// - public bool Dither { get; } + public bool DoDither { get; } /// /// Gets the configuration which allows altering default behaviour or extending the library. @@ -119,18 +107,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // Collect the palette. Required before the second pass runs. ReadOnlyMemory palette = this.GetPalette(); MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; - - this.paletteVector = memoryAllocator.Allocate(palette.Length); - PixelOperations.Instance.ToVector4( - this.Configuration, - palette.Span, - this.paletteVector.Memory.Span, - PixelConversionModifiers.Scale); + this.pixelMap = new EuclideanPixelMap(palette); var quantizedFrame = new QuantizedFrame(memoryAllocator, width, height, palette); Span pixelSpan = quantizedFrame.GetWritablePixelSpan(); - if (this.Dither) + if (this.DoDither) { // We clone the image as we don't want to alter the original via dithering. using (ImageFrame clone = image.Clone()) @@ -157,13 +139,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return; } - if (disposing) - { - this.paletteVector?.Dispose(); - } - - this.paletteVector = null; - this.isDisposed = true; } @@ -178,22 +153,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - /// Returns the closest color from the palette to the given color by calculating the - /// Euclidean distance in the Rgba colorspace. + /// Returns the index and color from the quantized palette corresponding to the give to the given color. /// - /// The color. + /// The color to match. + /// The matched color. /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected byte GetClosestPixel(ref TPixel pixel) - { - // Check if the color is in the lookup table - if (this.distanceCache.TryGetValue(pixel, out byte value)) - { - return value; - } - - return this.GetClosestPixelSlow(ref pixel); - } + [MethodImpl(InliningOptions.ShortMethod)] + protected virtual byte GetQuantizedColor(TPixel color, out TPixel match) + => this.pixelMap.GetClosestColor(color, out match); /// /// Retrieve the palette for the quantized image. @@ -203,32 +170,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// protected abstract ReadOnlyMemory GetPalette(); - /// - /// Returns the index of the first instance of the transparent color in the palette. - /// - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected byte GetTransparentIndex() - { - // Transparent pixels are much more likely to be found at the end of a palette. - Span paletteVectorSpan = this.paletteVector.Memory.Span; - ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan); - - int paletteVectorLengthMinus1 = paletteVectorSpan.Length - 1; - - int index = paletteVectorLengthMinus1; - for (int i = paletteVectorLengthMinus1; i >= 0; i--) - { - ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, i); - if (candidate.Equals(default)) - { - index = i; - } - } - - return (byte)index; - } - /// /// Execute a second pass through the image to assign the pixels to a palette entry. /// @@ -237,49 +178,66 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The output color palette. /// The width in pixels of the image. /// The height in pixels of the image. - protected abstract void SecondPass( + protected virtual void SecondPass( ImageFrame source, Span output, ReadOnlySpan palette, int width, - int height); - - [MethodImpl(MethodImplOptions.NoInlining)] - private byte GetClosestPixelSlow(ref TPixel pixel) + int height) { - // Loop through the palette and find the nearest match. - int colorIndex = 0; - float leastDistance = float.MaxValue; - Vector4 vector = pixel.ToScaledVector4(); - float epsilon = Constants.EpsilonSquared; - Span paletteVectorSpan = this.paletteVector.Memory.Span; - ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan); + Rectangle interest = source.Bounds(); + int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length); - for (int index = 0; index < paletteVectorSpan.Length; index++) + if (!this.DoDither) { - ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, index); - float distance = Vector4.DistanceSquared(vector, candidate); - - // Greater... Move on. - if (!(distance < leastDistance)) + // TODO: This can be parallel. + for (int y = interest.Top; y < interest.Bottom; y++) { - continue; + Span row = source.GetPixelRowSpan(y); + int offset = y * width; + + for (int x = interest.Left; x < interest.Right; x++) + { + output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _); + } } - colorIndex = index; - leastDistance = distance; + return; + } - // And if it's an exact match, exit the loop - if (distance < epsilon) + // Error diffusion. The difference between the source and transformed color + // is spread to neighboring pixels. + if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) + { + for (int y = interest.Top; y < interest.Bottom; y++) { - break; + Span row = source.GetPixelRowSpan(y); + int offset = y * width; + + for (int x = interest.Left; x < interest.Right; x++) + { + TPixel sourcePixel = row[x]; + output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed); + this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth); + } } + + return; } - // Now I have the index, pop it into the cache for next time - byte result = (byte)colorIndex; - this.distanceCache.Add(pixel, result); - return result; + // TODO: This can be parallel. + // Ordered dithering. We are only operating on a single pixel. + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + int offset = y * width; + + for (int x = interest.Left; x < interest.Right; x++) + { + TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth); + output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _); + } + } } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 54dabab0a..4561727fb 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -17,12 +17,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Gets a value indicating whether to apply dithering to the output image. /// - bool Dither { get; } + bool DoDither { get; } /// - /// Gets the error diffusion algorithm to apply to the output image. + /// Gets the algorithm to apply to the output image. /// - IErrorDiffuser Diffuser { get; } + IDither Dither { get; } /// /// Quantize an image frame and return the resulting output pixels. @@ -33,4 +33,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// IQuantizedFrame QuantizeFrame(ImageFrame image); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs index f1490a6d2..7bf58b31f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public interface IQuantizer { /// - /// Gets the error diffusion algorithm to apply to the output image. + /// Gets the dithering algorithm to apply to the output image. /// - IErrorDiffuser Diffuser { get; } + IDither Dither { get; } /// /// Creates the generic frame quantizer @@ -35,4 +35,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) where TPixel : struct, IPixel; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 4b94c14be..20b276c74 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -29,10 +29,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private readonly Octree octree; - /// - /// The transparent index - /// - private byte transparentIndex; + private TPixel[] palette; /// /// Initializes a new instance of the class. @@ -86,92 +83,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - protected override void SecondPass( - ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height) + [MethodImpl(InliningOptions.ShortMethod)] + protected override byte GetQuantizedColor(TPixel color, out TPixel match) { - // Load up the values for the first pixel. We can use these to speed up the second - // pass of the algorithm by avoiding transforming rows of identical color. - TPixel sourcePixel = source[0, 0]; - TPixel previousPixel = sourcePixel; - this.transparentIndex = this.GetTransparentIndex(); - byte pixelValue = this.QuantizePixel(ref sourcePixel); - TPixel transformedPixel = palette[pixelValue]; - - for (int y = 0; y < height; y++) + if (!this.DoDither) { - Span row = source.GetPixelRowSpan(y); - - // And loop through each column - for (int x = 0; x < width; x++) - { - // Get the pixel. - 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)) - { - // Quantize the pixel - pixelValue = this.QuantizePixel(ref sourcePixel); - - // And setup the previous pointer - previousPixel = sourcePixel; - - if (this.Dither) - { - transformedPixel = palette[pixelValue]; - } - } - - if (this.Dither) - { - // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height); - } - - output[(y * source.Width) + x] = pixelValue; - } + var index = (byte)this.octree.GetPaletteIndex(ref color); + match = this.GetPalette().Span[index]; + return index; } + + return base.GetQuantizedColor(color, out match); } internal ReadOnlyMemory AotGetPalette() => this.GetPalette(); /// - protected override ReadOnlyMemory GetPalette() => this.octree.Palletize(this.colors); - - /// - /// Process the pixel in the second pass of the algorithm. - /// - /// The pixel to quantize. - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte QuantizePixel(ref TPixel pixel) - { - if (this.Dither) - { - // The colors have changed so we need to use Euclidean distance calculation to - // find the closest value. - return this.GetClosestPixel(ref pixel); - } - - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); - if (rgba.Equals(default)) - { - return this.transparentIndex; - } - - return (byte)this.octree.GetPaletteIndex(ref pixel); - } + [MethodImpl(InliningOptions.ShortMethod)] + protected override ReadOnlyMemory GetPalette() + => this.palette ?? (this.palette = this.octree.Palletize(this.colors)); /// /// Class which does the actual quantization /// - private class Octree + private sealed class Octree { /// /// Mask used when getting the appropriate pixels for a given node @@ -220,10 +155,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public int Leaves { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get; - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] set; } @@ -232,7 +167,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private OctreeNode[] ReducibleNodes { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get; } @@ -272,7 +207,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// An with the palletized colors /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] public TPixel[] Palletize(int colorCount) { while (this.Leaves > colorCount - 1) @@ -297,7 +232,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] public int GetPaletteIndex(ref TPixel pixel) => this.root.GetPaletteIndex(ref pixel, 0); /// @@ -306,8 +241,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The node last quantized /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected void TrackPrevious(OctreeNode node) => this.previousNode = node; + [MethodImpl(InliningOptions.ShortMethod)] + public void TrackPrevious(OctreeNode node) => this.previousNode = node; /// /// Reduce the depth of the tree @@ -336,7 +271,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Class which encapsulates each node in the tree /// - protected class OctreeNode + public sealed class OctreeNode { /// /// Pointers to any child nodes @@ -414,7 +349,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public OctreeNode NextReducible { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get; } @@ -530,6 +465,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(MethodImplOptions.NoInlining)] public int GetPaletteIndex(ref TPixel pixel, int level) { + // TODO: pass index around so we can do this in parallel. int index = this.paletteIndex; if (!this.leaf) @@ -549,6 +485,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } else { + // TODO: Throw helper. throw new Exception($"Cannot retrieve a pixel at the given index {pixelIndex}."); } } @@ -560,7 +497,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Increment the pixel count and add to the color information /// /// The pixel to add. - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] public void Increment(ref TPixel pixel) { Rgba32 rgba = default; diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index aaf2c42cb..0a932b13f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Allows the quantization of images pixels using Octrees. /// /// - /// By default the quantizer uses dithering and a color palette of a maximum length of 255 + /// By default the quantizer uses dithering and a color palette of a maximum length of 255 /// /// public class OctreeQuantizer : IQuantizer @@ -54,8 +54,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image. - public OctreeQuantizer(IErrorDiffuser diffuser) + /// The dithering algorithm, if any, to apply to the output image. + public OctreeQuantizer(IDither diffuser) : this(diffuser, QuantizerConstants.MaxColors) { } @@ -63,16 +63,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image. + /// The dithering algorithm, if any, to apply to the output image. /// The maximum number of colors to hold in the color palette. - public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors) + public OctreeQuantizer(IDither dither, int maxColors) { - this.Diffuser = diffuser; + this.Dither = dither; this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } /// - public IErrorDiffuser Diffuser { get; } + public IDither Dither { get; } /// /// Gets the maximum number of colors to hold in the color palette. @@ -93,6 +93,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return new OctreeFrameQuantizer(configuration, this, maxColors); } - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 825eb6bee..1c9c22481 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -29,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The configuration which allows altering default behaviour or extending the library. /// The palette quantizer. /// An array of all colors in the palette. - public PaletteFrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, ReadOnlyMemory colors) + public PaletteFrameQuantizer(Configuration configuration, IDither diffuser, ReadOnlyMemory colors) : base(configuration, diffuser, true) => this.palette = colors; /// @@ -40,47 +39,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization int width, int height) { - // Load up the values for the first pixel. We can use these to speed up the second - // pass of the algorithm by avoiding transforming rows of identical color. - TPixel sourcePixel = source[0, 0]; - TPixel previousPixel = sourcePixel; - byte pixelValue = this.QuantizePixel(ref sourcePixel); - ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette); - TPixel transformedPixel = Unsafe.Add(ref paletteRef, pixelValue); + Rectangle interest = source.Bounds(); + int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length); - for (int y = 0; y < height; y++) + if (!this.DoDither) { - ref TPixel rowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); - - // And loop through each column - for (int x = 0; x < width; x++) + // TODO: This can be parallel. + for (int y = interest.Top; y < interest.Bottom; y++) { - // Get the pixel. - sourcePixel = Unsafe.Add(ref rowRef, x); + Span row = source.GetPixelRowSpan(y); + int offset = y * width; - // 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)) + for (int x = interest.Left; x < interest.Right; x++) { - // Quantize the pixel - pixelValue = this.QuantizePixel(ref sourcePixel); + output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _); + } + } - // And setup the previous pointer - previousPixel = sourcePixel; + return; + } - if (this.Dither) - { - transformedPixel = Unsafe.Add(ref paletteRef, pixelValue); - } - } + // Error diffusion. The difference between the source and transformed color + // is spread to neighboring pixels. + if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) + { + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + int offset = y * width; - if (this.Dither) + for (int x = interest.Left; x < interest.Right; x++) { - // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height); + TPixel sourcePixel = row[x]; + output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed); + this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth); } + } - output[(y * source.Width) + x] = pixelValue; + return; + } + + // TODO: This can be parallel. + // Ordered dithering. We are only operating on a single pixel. + for (int y = interest.Top; y < interest.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + int offset = y * width; + + for (int x = interest.Left; x < interest.Right; x++) + { + TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth); + output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _); } } } @@ -88,15 +97,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override ReadOnlyMemory GetPalette() => this.palette; - - /// - /// Process the pixel in the second pass of the algorithm - /// - /// The pixel to quantize - /// - /// The quantized value - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte QuantizePixel(ref TPixel pixel) => this.GetClosestPixel(ref pixel); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index a493e6f88..daba7a6b7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Allows the quantization of images pixels using color palettes. /// Override this class to provide your own palette. /// - /// By default the quantizer uses dithering. + /// By default the quantizer uses dithering. /// /// public class PaletteQuantizer : IQuantizer @@ -40,15 +40,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The palette. - /// The error diffusion algorithm, if any, to apply to the output image - public PaletteQuantizer(ReadOnlyMemory palette, IErrorDiffuser diffuser) + /// The dithering algorithm, if any, to apply to the output image + public PaletteQuantizer(ReadOnlyMemory palette, IDither dither) { this.Palette = palette; - this.Diffuser = diffuser; + this.Dither = dither; } /// - public IErrorDiffuser Diffuser { get; } + public IDither Dither { get; } /// /// Gets the palette. @@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { var palette = new TPixel[this.Palette.Length]; Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Diffuser, palette); + return new PaletteFrameQuantizer(configuration, this.Dither, palette); } /// @@ -73,9 +73,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization var palette = new TPixel[max]; Color.ToPixel(configuration, this.Palette.Span.Slice(0, max), palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Diffuser, palette); + return new PaletteFrameQuantizer(configuration, this.Dither, palette); } - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index c912572f0..ff965e393 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The error diffusion algorithm, if any, to apply to the output image - public WebSafePaletteQuantizer(IErrorDiffuser diffuser) + public WebSafePaletteQuantizer(IDither diffuser) : base(Color.WebSafePalette, diffuser) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index cd320a9a3..3b48ddeda 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The error diffusion algorithm, if any, to apply to the output image - public WernerPaletteQuantizer(IErrorDiffuser diffuser) + public WernerPaletteQuantizer(IDither diffuser) : base(Color.WernerPalette, diffuser) { } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 2de02ebb3..bf37a7755 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -10,8 +10,6 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -// TODO: Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? -// (T, R, G, B, A, M2) could be grouped together! Investigate a ColorMoment struct. namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -69,34 +67,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; /// - /// Moment of P(c). + /// Color moments. /// - private IMemoryOwner vwt; - - /// - /// Moment of r*P(c). - /// - private IMemoryOwner vmr; - - /// - /// Moment of g*P(c). - /// - private IMemoryOwner vmg; - - /// - /// Moment of b*P(c). - /// - private IMemoryOwner vmb; - - /// - /// Moment of a*P(c). - /// - private IMemoryOwner vma; - - /// - /// Moment of c^2*P(c). - /// - private IMemoryOwner m2; + private IMemoryOwner moments; /// /// Color space tag. @@ -148,15 +121,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization : base(configuration, quantizer, false) { this.memoryAllocator = this.Configuration.MemoryAllocator; - - this.vwt = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmr = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmg = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmb = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vma = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.m2 = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.moments = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tag = this.memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.colors = maxColors; } @@ -170,21 +136,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (disposing) { - this.vwt?.Dispose(); - this.vmr?.Dispose(); - this.vmg?.Dispose(); - this.vmb?.Dispose(); - this.vma?.Dispose(); - this.m2?.Dispose(); + this.moments?.Dispose(); this.tag?.Dispose(); } - this.vwt = null; - this.vmr = null; - this.vmg = null; - this.vmb = null; - this.vma = null; - this.m2 = null; + this.moments = null; this.tag = null; this.isDisposed = true; @@ -199,27 +155,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization if (this.palette is null) { this.palette = new TPixel[this.colors]; - Span vwtSpan = this.vwt.GetSpan(); - Span vmrSpan = this.vmr.GetSpan(); - Span vmgSpan = this.vmg.GetSpan(); - Span vmbSpan = this.vmb.GetSpan(); - Span vmaSpan = this.vma.GetSpan(); + ReadOnlySpan momentsSpan = this.moments.GetSpan(); for (int k = 0; k < this.colors; k++) { this.Mark(ref this.colorCube[k], (byte)k); - float weight = Volume(ref this.colorCube[k], vwtSpan); + Moment moment = Volume(ref this.colorCube[k], momentsSpan); - if (MathF.Abs(weight) > Constants.Epsilon) + if (moment.Weight > 0) { - float r = Volume(ref this.colorCube[k], vmrSpan); - float g = Volume(ref this.colorCube[k], vmgSpan); - float b = Volume(ref this.colorCube[k], vmbSpan); - float a = Volume(ref this.colorCube[k], vmaSpan); - ref TPixel color = ref this.palette[k]; - color.FromScaledVector4(new Vector4(r, g, b, a) / weight / 255F); + color.FromScaledVector4(moment.Normalize()); } } } @@ -236,50 +183,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - protected override void SecondPass(ImageFrame source, Span output, ReadOnlySpan palette, int width, int height) + [MethodImpl(InliningOptions.ShortMethod)] + protected override byte GetQuantizedColor(TPixel color, out TPixel match) { - // Load up the values for the first pixel. We can use these to speed up the second - // pass of the algorithm by avoiding transforming rows of identical color. - TPixel sourcePixel = source[0, 0]; - TPixel previousPixel = sourcePixel; - byte pixelValue = this.QuantizePixel(ref sourcePixel); - TPixel transformedPixel = palette[pixelValue]; - - for (int y = 0; y < height; y++) + if (!this.DoDither) { - Span row = source.GetPixelRowSpan(y); - - // And loop through each column - for (int x = 0; x < width; x++) - { - // Get the pixel. - 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)) - { - // Quantize the pixel - pixelValue = this.QuantizePixel(ref sourcePixel); - - // And setup the previous pointer - previousPixel = sourcePixel; - - if (this.Dither) - { - transformedPixel = palette[pixelValue]; - } - } - - if (this.Dither) - { - // Apply the dithering matrix. We have to reapply the value now as the original has changed. - this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height); - } - - output[(y * source.Width) + x] = pixelValue; - } + // Expected order r->g->b->a + Rgba32 rgba = default; + color.ToRgba32(ref rgba); + + int r = rgba.R >> (8 - IndexBits); + int g = rgba.G >> (8 - IndexBits); + int b = rgba.B >> (8 - IndexBits); + int a = rgba.A >> (8 - IndexAlphaBits); + + ReadOnlySpan tagSpan = this.tag.GetSpan(); + var index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + match = this.GetPalette().Span[index]; + return index; } + + return base.GetQuantizedColor(color, out match); } /// @@ -290,7 +214,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The blue value. /// The alpha value. /// The index. - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] private static int GetPaletteIndex(int r, int g, int b, int a) { return (r << ((IndexBits * 2) + IndexAlphaBits)) @@ -307,26 +231,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Computes sum over a box of any given statistic. /// /// The cube. - /// The moment. + /// The moment. /// The result. - private static float Volume(ref Box cube, Span moment) + private static Moment Volume(ref Box cube, ReadOnlySpan moments) { - return moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + return moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; } /// @@ -334,55 +258,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The cube. /// The direction. - /// The moment. + /// The moment. /// The result. - private static long Bottom(ref Box cube, int direction, Span moment) + private static Moment Bottom(ref Box cube, int direction, ReadOnlySpan moments) { switch (direction) { // Red case 3: - return -moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + return -moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Green case 2: - return -moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + return -moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Blue case 1: - return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + return -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Alpha case 0: - return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + return -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; default: throw new ArgumentOutOfRangeException(nameof(direction)); @@ -395,55 +319,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The cube. /// The direction. /// The position. - /// The moment. + /// The moment. /// The result. - private static long Top(ref Box cube, int direction, int position, Span moment) + private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpan moments) { switch (direction) { // Red case 3: - return moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)]; + return moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)]; // Green case 2: - return moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)] - - moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)]; + return moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)]; // Blue case 1: - return moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)] - - moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)]; + return moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)]; // Alpha case 0: - return moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)] - - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)] - - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)] - + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)] - - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)] - + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)] - + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)] - - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)]; + return moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)] + - moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)] + - moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)] + + moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)] + - moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)] + + moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)] + + moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)] + - moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)]; default: throw new ArgumentOutOfRangeException(nameof(direction)); @@ -458,45 +382,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The height in pixels of the image. private void Build3DHistogram(ImageFrame source, int width, int height) { - Span vwtSpan = this.vwt.GetSpan(); - Span vmrSpan = this.vmr.GetSpan(); - Span vmgSpan = this.vmg.GetSpan(); - Span vmbSpan = this.vmb.GetSpan(); - Span vmaSpan = this.vma.GetSpan(); - Span m2Span = this.m2.GetSpan(); + Span momentSpan = this.moments.GetSpan(); // Build up the 3-D color histogram // Loop through each row - using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(source.Width)) - { - for (int y = 0; y < height; y++) - { - Span row = source.GetPixelRowSpan(y); - Span rgbaSpan = rgbaBuffer.GetSpan(); - PixelOperations.Instance.ToRgba32(source.GetConfiguration(), row, rgbaSpan); - ref Rgba32 scanBaseRef = ref MemoryMarshal.GetReference(rgbaSpan); - - // And loop through each column - for (int x = 0; x < width; x++) - { - ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x); + using IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(source.Width); + Span rgbaSpan = rgbaBuffer.GetSpan(); + ref Rgba32 scanBaseRef = ref MemoryMarshal.GetReference(rgbaSpan); - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); - int a = rgba.A >> (8 - IndexAlphaBits); + for (int y = 0; y < height; y++) + { + Span row = source.GetPixelRowSpan(y); + PixelOperations.Instance.ToRgba32(source.GetConfiguration(), row, rgbaSpan); - int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); + // And loop through each column + for (int x = 0; x < width; x++) + { + ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x); - vwtSpan[index]++; - vmrSpan[index] += rgba.R; - vmgSpan[index] += rgba.G; - vmbSpan[index] += rgba.B; - vmaSpan[index] += rgba.A; + int r = (rgba.R >> (8 - IndexBits)) + 1; + int g = (rgba.G >> (8 - IndexBits)) + 1; + int b = (rgba.B >> (8 - IndexBits)) + 1; + int a = (rgba.A >> (8 - IndexAlphaBits)) + 1; - var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A); - m2Span[index] += Vector4.Dot(vector, vector); - } + int index = GetPaletteIndex(r, g, b, a); + momentSpan[index] += rgba; } } } @@ -507,103 +417,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The memory allocator used for allocating buffers. private void Get3DMoments(MemoryAllocator memoryAllocator) { - Span vwtSpan = this.vwt.GetSpan(); - Span vmrSpan = this.vmr.GetSpan(); - Span vmgSpan = this.vmg.GetSpan(); - Span vmbSpan = this.vmb.GetSpan(); - Span vmaSpan = this.vma.GetSpan(); - Span m2Span = this.m2.GetSpan(); - - using (IMemoryOwner volume = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volumeR = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volumeG = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volumeB = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volumeA = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volume2 = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner area = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner areaR = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner areaG = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner areaB = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner areaA = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner area2 = memoryAllocator.Allocate(IndexAlphaCount)) + using IMemoryOwner volume = memoryAllocator.Allocate(IndexCount * IndexAlphaCount); + using IMemoryOwner area = memoryAllocator.Allocate(IndexAlphaCount); + + Span momentSpan = this.moments.GetSpan(); + Span volumeSpan = volume.GetSpan(); + Span areaSpan = area.GetSpan(); + int baseIndex = GetPaletteIndex(1, 0, 0, 0); + + for (int r = 1; r < IndexCount; r++) { - Span volumeSpan = volume.GetSpan(); - Span volumeRSpan = volumeR.GetSpan(); - Span volumeGSpan = volumeG.GetSpan(); - Span volumeBSpan = volumeB.GetSpan(); - Span volumeASpan = volumeA.GetSpan(); - Span volume2Span = volume2.GetSpan(); - - Span areaSpan = area.GetSpan(); - Span areaRSpan = areaR.GetSpan(); - Span areaGSpan = areaG.GetSpan(); - Span areaBSpan = areaB.GetSpan(); - Span areaASpan = areaA.GetSpan(); - Span area2Span = area2.GetSpan(); - - for (int r = 1; r < IndexCount; r++) + volumeSpan.Clear(); + + for (int g = 1; g < IndexCount; g++) { - volume.Clear(); - volumeR.Clear(); - volumeG.Clear(); - volumeB.Clear(); - volumeA.Clear(); - volume2.Clear(); - - for (int g = 1; g < IndexCount; g++) + areaSpan.Clear(); + + for (int b = 1; b < IndexCount; b++) { - area.Clear(); - areaR.Clear(); - areaG.Clear(); - areaB.Clear(); - areaA.Clear(); - area2.Clear(); - - for (int b = 1; b < IndexCount; b++) + Moment line = default; + + for (int a = 1; a < IndexAlphaCount; a++) { - long line = 0; - long lineR = 0; - long lineG = 0; - long lineB = 0; - long lineA = 0; - double line2 = 0; - - for (int a = 1; a < IndexAlphaCount; a++) - { - int ind1 = GetPaletteIndex(r, g, b, a); - - line += vwtSpan[ind1]; - lineR += vmrSpan[ind1]; - lineG += vmgSpan[ind1]; - lineB += vmbSpan[ind1]; - lineA += vmaSpan[ind1]; - line2 += m2Span[ind1]; - - areaSpan[a] += line; - areaRSpan[a] += lineR; - areaGSpan[a] += lineG; - areaBSpan[a] += lineB; - areaASpan[a] += lineA; - area2Span[a] += line2; - - int inv = (b * IndexAlphaCount) + a; - - volumeSpan[inv] += areaSpan[a]; - volumeRSpan[inv] += areaRSpan[a]; - volumeGSpan[inv] += areaGSpan[a]; - volumeBSpan[inv] += areaBSpan[a]; - volumeASpan[inv] += areaASpan[a]; - volume2Span[inv] += area2Span[a]; - - int ind2 = ind1 - GetPaletteIndex(1, 0, 0, 0); - - vwtSpan[ind1] = vwtSpan[ind2] + volumeSpan[inv]; - vmrSpan[ind1] = vmrSpan[ind2] + volumeRSpan[inv]; - vmgSpan[ind1] = vmgSpan[ind2] + volumeGSpan[inv]; - vmbSpan[ind1] = vmbSpan[ind2] + volumeBSpan[inv]; - vmaSpan[ind1] = vmaSpan[ind2] + volumeASpan[inv]; - m2Span[ind1] = m2Span[ind2] + volume2Span[inv]; - } + int ind1 = GetPaletteIndex(r, g, b, a); + line += momentSpan[ind1]; + + areaSpan[a] += line; + + int inv = (b * IndexAlphaCount) + a; + volumeSpan[inv] += areaSpan[a]; + + int ind2 = ind1 - baseIndex; + momentSpan[ind1] = momentSpan[ind2] + volumeSpan[inv]; } } } @@ -617,33 +462,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The . private double Variance(ref Box cube) { - float dr = Volume(ref cube, this.vmr.GetSpan()); - float dg = Volume(ref cube, this.vmg.GetSpan()); - float db = Volume(ref cube, this.vmb.GetSpan()); - float da = Volume(ref cube, this.vma.GetSpan()); - - Span m2Span = this.m2.GetSpan(); - - double moment = - m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] - - m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] - - m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] - + m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] - - m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] - + m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] - + m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] - - m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] - - m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] - + m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] - + m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] - - m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] - + m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] - - m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] - - m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] - + m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; - - var vector = new Vector4(dr, dg, db, da); - return moment - (Vector4.Dot(vector, vector) / Volume(ref cube, this.vwt.GetSpan())); + ReadOnlySpan momentSpan = this.moments.GetSpan(); + + Moment volume = Volume(ref cube, momentSpan); + Moment variance = + momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + momentSpan[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; + + var vector = new Vector4(volume.R, volume.G, volume.B, volume.A); + return variance.Moment2 - (Vector4.Dot(vector, vector) / volume.Weight); } /// @@ -658,60 +499,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The first position. /// The last position. /// The cutting point. - /// The whole red. - /// The whole green. - /// The whole blue. - /// The whole alpha. - /// The whole weight. + /// The whole moment. /// The . - private float Maximize(ref Box cube, int direction, int first, int last, out int cut, float wholeR, float wholeG, float wholeB, float wholeA, float wholeW) + private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) { - Span vwtSpan = this.vwt.GetSpan(); - Span vmrSpan = this.vmr.GetSpan(); - Span vmgSpan = this.vmg.GetSpan(); - Span vmbSpan = this.vmb.GetSpan(); - Span vmaSpan = this.vma.GetSpan(); - - long baseR = Bottom(ref cube, direction, vmrSpan); - long baseG = Bottom(ref cube, direction, vmgSpan); - long baseB = Bottom(ref cube, direction, vmbSpan); - long baseA = Bottom(ref cube, direction, vmaSpan); - long baseW = Bottom(ref cube, direction, vwtSpan); + ReadOnlySpan momentSpan = this.moments.GetSpan(); + Moment bottom = Bottom(ref cube, direction, momentSpan); float max = 0F; cut = -1; for (int i = first; i < last; i++) { - float halfR = baseR + Top(ref cube, direction, i, vmrSpan); - float halfG = baseG + Top(ref cube, direction, i, vmgSpan); - float halfB = baseB + Top(ref cube, direction, i, vmbSpan); - float halfA = baseA + Top(ref cube, direction, i, vmaSpan); - float halfW = baseW + Top(ref cube, direction, i, vwtSpan); + Moment half = bottom + Top(ref cube, direction, i, momentSpan); - if (MathF.Abs(halfW) < Constants.Epsilon) + if (half.Weight == 0) { continue; } - var vector = new Vector4(halfR, halfG, halfB, halfA); - float temp = Vector4.Dot(vector, vector) / halfW; + var vector = new Vector4(half.R, half.G, half.B, half.A); + float temp = Vector4.Dot(vector, vector) / half.Weight; - halfW = wholeW - halfW; + half = whole - half; - if (MathF.Abs(halfW) < Constants.Epsilon) + if (half.Weight == 0) { continue; } - halfR = wholeR - halfR; - halfG = wholeG - halfG; - halfB = wholeB - halfB; - halfA = wholeA - halfA; - - vector = new Vector4(halfR, halfG, halfB, halfA); - - temp += Vector4.Dot(vector, vector) / halfW; + vector = new Vector4(half.R, half.G, half.B, half.A); + temp += Vector4.Dot(vector, vector) / half.Weight; if (temp > max) { @@ -731,33 +549,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Returns a value indicating whether the box has been split. private bool Cut(ref Box set1, ref Box set2) { - float wholeR = Volume(ref set1, this.vmr.GetSpan()); - float wholeG = Volume(ref set1, this.vmg.GetSpan()); - float wholeB = Volume(ref set1, this.vmb.GetSpan()); - float wholeA = Volume(ref set1, this.vma.GetSpan()); - float wholeW = Volume(ref set1, this.vwt.GetSpan()); + ReadOnlySpan momentSpan = this.moments.GetSpan(); + Moment whole = Volume(ref set1, momentSpan); - float maxr = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutr, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxg = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutg, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxb = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutb, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxa = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cuta, wholeR, wholeG, wholeB, wholeA, wholeW); + float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); + float maxG = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutG, whole); + float maxB = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutB, whole); + float maxA = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cutA, whole); int dir; - if ((maxr >= maxg) && (maxr >= maxb) && (maxr >= maxa)) + if ((maxR >= maxG) && (maxR >= maxB) && (maxR >= maxA)) { dir = 3; - if (cutr < 0) + if (cutR < 0) { return false; } } - else if ((maxg >= maxr) && (maxg >= maxb) && (maxg >= maxa)) + else if ((maxG >= maxR) && (maxG >= maxB) && (maxG >= maxA)) { dir = 2; } - else if ((maxb >= maxr) && (maxb >= maxg) && (maxb >= maxa)) + else if ((maxB >= maxR) && (maxB >= maxG) && (maxB >= maxA)) { dir = 1; } @@ -775,7 +590,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { // Red case 3: - set2.RMin = set1.RMax = cutr; + set2.RMin = set1.RMax = cutR; set2.GMin = set1.GMin; set2.BMin = set1.BMin; set2.AMin = set1.AMin; @@ -783,7 +598,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // Green case 2: - set2.GMin = set1.GMax = cutg; + set2.GMin = set1.GMax = cutG; set2.RMin = set1.RMin; set2.BMin = set1.BMin; set2.AMin = set1.AMin; @@ -791,7 +606,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // Blue case 1: - set2.BMin = set1.BMax = cutb; + set2.BMin = set1.BMax = cutB; set2.RMin = set1.RMin; set2.GMin = set1.GMin; set2.AMin = set1.AMin; @@ -799,7 +614,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // Alpha case 0: - set2.AMin = set1.AMax = cuta; + set2.AMin = set1.AMax = cutA; set2.RMin = set1.RMin; set2.GMin = set1.GMin; set2.BMin = set1.BMin; @@ -857,8 +672,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ref Box currentCube = ref this.colorCube[i]; if (this.Cut(ref nextCube, ref currentCube)) { - vv[next] = nextCube.Volume > 1 ? this.Variance(ref nextCube) : 0F; - vv[i] = currentCube.Volume > 1 ? this.Variance(ref currentCube) : 0F; + vv[next] = nextCube.Volume > 1 ? this.Variance(ref nextCube) : 0D; + vv[i] = currentCube.Volume > 1 ? this.Variance(ref currentCube) : 0D; } else { @@ -886,35 +701,92 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } } - /// - /// Process the pixel in the second pass of the algorithm - /// - /// The pixel to quantize - /// - /// The quantized value - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte QuantizePixel(ref TPixel pixel) + private struct Moment { - if (this.Dither) + /// + /// Moment of r*P(c). + /// + public long R; + + /// + /// Moment of g*P(c). + /// + public long G; + + /// + /// Moment of b*P(c). + /// + public long B; + + /// + /// Moment of a*P(c). + /// + public long A; + + /// + /// Moment of P(c). + /// + public long Weight; + + /// + /// Moment of c^2*P(c). + /// + public double Moment2; + + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator +(Moment x, Moment y) { - // The colors have changed so we need to use Euclidean distance calculation to - // find the closest value. - return this.GetClosestPixel(ref pixel); + x.R += y.R; + x.G += y.G; + x.B += y.B; + x.A += y.A; + x.Weight += y.Weight; + x.Moment2 += y.Moment2; + return x; } - // Expected order r->g->b->a - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator -(Moment x, Moment y) + { + x.R -= y.R; + x.G -= y.G; + x.B -= y.B; + x.A -= y.A; + x.Weight -= y.Weight; + x.Moment2 -= y.Moment2; + return x; + } - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); - int a = rgba.A >> (8 - IndexAlphaBits); + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator -(Moment x) + { + x.R = -x.R; + x.G = -x.G; + x.B = -x.B; + x.A = -x.A; + x.Weight = -x.Weight; + x.Moment2 = -x.Moment2; + return x; + } - Span tagSpan = this.tag.GetSpan(); + [MethodImpl(InliningOptions.ShortMethod)] + public static Moment operator +(Moment x, Rgba32 y) + { + x.R += y.R; + x.G += y.G; + x.B += y.B; + x.A += y.A; + x.Weight++; + + var vector = new Vector4(y.R, y.G, y.B, y.A); + x.Moment2 += Vector4.Dot(vector, vector); + + return x; + } - return tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + [MethodImpl(InliningOptions.ShortMethod)] + public readonly Vector4 Normalize() + => new Vector4(this.R, this.G, this.B, this.A) / this.Weight / 255F; } /// @@ -968,10 +840,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public int Volume; /// - public override bool Equals(object obj) => obj is Box box && this.Equals(box); + public readonly override bool Equals(object obj) + => obj is Box box + && this.Equals(box); /// - public bool Equals(Box other) => + public readonly bool Equals(Box other) => this.RMin == other.RMin && this.RMax == other.RMax && this.GMin == other.GMin @@ -983,7 +857,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization && this.Volume == other.Volume; /// - public override int GetHashCode() + public readonly override int GetHashCode() { HashCode hash = default; hash.Add(this.RMin); diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs index 3f2deaec0..6bd432242 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Allows the quantization of images pixels using Xiaolin Wu's Color Quantizer /// - /// By default the quantizer uses dithering and a color palette of a maximum length of 255 + /// By default the quantizer uses dithering and a color palette of a maximum length of 255 /// /// public class WuQuantizer : IQuantizer @@ -43,8 +43,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image - public WuQuantizer(IErrorDiffuser diffuser) + /// The dithering algorithm, if any, to apply to the output image + public WuQuantizer(IDither diffuser) : this(diffuser, QuantizerConstants.MaxColors) { } @@ -52,16 +52,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image + /// The dithering algorithm, if any, to apply to the output image /// The maximum number of colors to hold in the color palette - public WuQuantizer(IErrorDiffuser diffuser, int maxColors) + public WuQuantizer(IDither dither, int maxColors) { - this.Diffuser = diffuser; + this.Dither = dither; this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } /// - public IErrorDiffuser Diffuser { get; } + public IDither Dither { get; } /// /// Gets the maximum number of colors to hold in the color palette. @@ -85,6 +85,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization return new WuFrameQuantizer(configuration, this, maxColors); } - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null; } } diff --git a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs b/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs index f5a26dc17..d20407be9 100644 --- a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs +++ b/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs @@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization { public class BinaryDitherTest : BaseImageOperationsExtensionTest { - private readonly IOrderedDither orderedDither; - private readonly IErrorDiffuser errorDiffuser; + private readonly IDither orderedDither; + private readonly IDither errorDiffuser; public BinaryDitherTest() { diff --git a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs index bb84bd4b1..3bdbd8e52 100644 --- a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs +++ b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs @@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization } } - private readonly IOrderedDither orderedDither; - private readonly IErrorDiffuser errorDiffuser; + private readonly IDither orderedDither; + private readonly IDither errorDiffuser; private readonly Color[] testPalette = { Color.Red, diff --git a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs index 7d9e0f04b..00eacdaf5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization TestImages.Png.CalliphoraPartial, TestImages.Png.Bike }; - public static readonly TheoryData OrderedDitherers = new TheoryData + public static readonly TheoryData OrderedDitherers = new TheoryData { { "Bayer8x8", KnownDitherers.BayerDither8x8 }, { "Bayer4x4", KnownDitherers.BayerDither4x4 }, @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { "Bayer2x2", KnownDitherers.BayerDither2x2 } }; - public static readonly TheoryData ErrorDiffusers = new TheoryData + public static readonly TheoryData ErrorDiffusers = new TheoryData { { "Atkinson", KnownDiffusers.Atkinson }, { "Burks", KnownDiffusers.Burks }, @@ -41,14 +41,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization public const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24; - private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4; + private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4; - private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson; [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)] [WithTestPatternImages(nameof(OrderedDitherers), 100, 100, PixelTypes.Rgba32)] - public void BinaryDitherFilter_WorksWithAllDitherers(TestImageProvider provider, string name, IOrderedDither ditherer) + public void BinaryDitherFilter_WorksWithAllDitherers(TestImageProvider provider, string name, IDither ditherer) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) @@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)] [WithTestPatternImages(nameof(ErrorDiffusers), 100, 100, PixelTypes.Rgba32)] - public void DiffusionFilter_WorksWithAllErrorDiffusers(TestImageProvider provider, string name, IErrorDiffuser diffuser) + public void DiffusionFilter_WorksWithAllErrorDiffusers(TestImageProvider provider, string name, IDither diffuser) where TPixel : struct, IPixel { using (Image image = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 78481acd2..94a2ec824 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -17,32 +17,34 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization public static readonly string[] CommonTestImages = { TestImages.Png.CalliphoraPartial, TestImages.Png.Bike }; - public static readonly TheoryData ErrorDiffusers = new TheoryData - { - KnownDiffusers.Atkinson, - KnownDiffusers.Burks, - KnownDiffusers.FloydSteinberg, - KnownDiffusers.JarvisJudiceNinke, - KnownDiffusers.Sierra2, - KnownDiffusers.Sierra3, - KnownDiffusers.SierraLite, - KnownDiffusers.StevensonArce, - KnownDiffusers.Stucki, - }; - - public static readonly TheoryData OrderedDitherers = new TheoryData - { - KnownDitherers.BayerDither8x8, - KnownDitherers.BayerDither4x4, - KnownDitherers.OrderedDither3x3, - KnownDitherers.BayerDither2x2 - }; + public static readonly TheoryData ErrorDiffusers + = new TheoryData + { + KnownDiffusers.Atkinson, + KnownDiffusers.Burks, + KnownDiffusers.FloydSteinberg, + KnownDiffusers.JarvisJudiceNinke, + KnownDiffusers.Sierra2, + KnownDiffusers.Sierra3, + KnownDiffusers.SierraLite, + KnownDiffusers.StevensonArce, + KnownDiffusers.Stucki, + }; + + public static readonly TheoryData OrderedDitherers + = new TheoryData + { + KnownDitherers.BayerDither8x8, + KnownDitherers.BayerDither4x4, + KnownDitherers.OrderedDither3x3, + KnownDitherers.BayerDither2x2 + }; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f); - private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4; + private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4; - private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson; /// /// The output is visually correct old 32bit runtime, @@ -100,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization [WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)] public void DiffusionFilter_WorksWithAllErrorDiffusers( TestImageProvider provider, - IErrorDiffuser diffuser) + IDither diffuser) where TPixel : struct, IPixel { if (SkipAllDitherTests) @@ -134,7 +136,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization [WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)] public void DitherFilter_WorksWithAllDitherers( TestImageProvider provider, - IOrderedDither ditherer) + IDither ditherer) where TPixel : struct, IPixel { if (SkipAllDitherTests) diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs index b3900325d..bd1efaa64 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs @@ -39,20 +39,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); quantizer = new OctreeQuantizer(false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Dither); quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index 2e9dc83dd..c21e6dc12 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -37,20 +37,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); quantizer = new PaletteQuantizer(Rgb, false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Dither); quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); } [Fact] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs index 625043c7f..8287e6e44 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs @@ -39,20 +39,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); quantizer = new WuQuantizer(false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Dither); quantizer = new WuQuantizer(KnownDiffusers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); } } }