diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 142ea3f3e..c8c8568e4 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -82,6 +84,7 @@ namespace SixLabors.ImageSharp.Advanced // This is we actually call all the individual methods you need to seed. AotCompileOctreeQuantizer(); AotCompileWuQuantizer(); + AotCompilePaletteQuantizer(); AotCompileDithering(); AotCompilePixelOperations(); @@ -109,9 +112,10 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileOctreeQuantizer() where TPixel : struct, IPixel { - using (var test = new OctreeFrameQuantizer(Configuration.Default, new OctreeQuantizer(false))) + using (var test = new OctreeFrameQuantizer(Configuration.Default, new OctreeQuantizer().Options)) { - test.AotGetPalette(); + var frame = new ImageFrame(Configuration.Default, 1, 1); + test.QuantizeFrame(frame, frame.Bounds()); } } @@ -122,10 +126,24 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileWuQuantizer() where TPixel : struct, IPixel { - using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer(false))) + using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer().Options)) { - test.QuantizeFrame(new ImageFrame(Configuration.Default, 1, 1)); - test.AotGetPalette(); + var frame = new ImageFrame(Configuration.Default, 1, 1); + test.QuantizeFrame(frame, frame.Bounds()); + } + } + + /// + /// This method pre-seeds the PaletteQuantizer in the AoT compiler for iOS. + /// + /// The pixel format. + private static void AotCompilePaletteQuantizer() + where TPixel : struct, IPixel + { + using (var test = (PaletteFrameQuantizer)new PaletteQuantizer(Array.Empty()).CreateFrameQuantizer(Configuration.Default)) + { + var frame = new ImageFrame(Configuration.Default, 1, 1); + test.QuantizeFrame(frame, frame.Bounds()); } } @@ -136,11 +154,13 @@ namespace SixLabors.ImageSharp.Advanced private static void AotCompileDithering() where TPixel : struct, IPixel { - var test = new FloydSteinbergDiffuser(); + ErrorDither errorDither = ErrorDither.FloydSteinberg; + OrderedDither orderedDither = OrderedDither.Bayer2x2; TPixel pixel = default; using (var image = new ImageFrame(Configuration.Default, 1, 1)) { - test.Dither(image, pixel, pixel, 0, 0, 0, 0, 0); + errorDither.Dither(image, image.Bounds(), pixel, pixel, 0, 0, 0); + orderedDither.Dither(pixel, 0, 0, 0, 0); } } diff --git a/src/ImageSharp/Color/Color.NamedColors.cs b/src/ImageSharp/Color/Color.NamedColors.cs index 8eb3fbcaf..240ce304d 100644 --- a/src/ImageSharp/Color/Color.NamedColors.cs +++ b/src/ImageSharp/Color/Color.NamedColors.cs @@ -8,6 +8,7 @@ namespace SixLabors.ImageSharp { /// /// Contains static named color values. + /// /// public readonly partial struct Color { @@ -719,9 +720,9 @@ namespace SixLabors.ImageSharp public static readonly Color Tomato = FromRgba(255, 99, 71, 255); /// - /// Represents a matching the W3C definition that has an hex value of #FFFFFF. + /// Represents a matching the W3C definition that has an hex value of #00000000. /// - public static readonly Color Transparent = FromRgba(255, 255, 255, 0); + public static readonly Color Transparent = FromRgba(0, 0, 0, 0); /// /// Represents a matching the W3C definition that has an hex value of #40E0D0. diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 1c7c606ca..1b3e0228a 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp @@ -87,7 +88,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; this.writeV4Header = options.SupportTransparency; - this.quantizer = options.Quantizer ?? new OctreeQuantizer(dither: true, maxColors: 256); + this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; } /// @@ -335,36 +336,36 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : struct, IPixel { - using (IQuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image)) + using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration); + using QuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); + + ReadOnlySpan quantizedColors = quantized.Palette.Span; + var color = default(Rgba32); + + // TODO: Use bulk conversion here for better perf + int idx = 0; + foreach (TPixel quantizedColor in quantizedColors) { - ReadOnlySpan quantizedColors = quantized.Palette.Span; - var color = default(Rgba32); + quantizedColor.ToRgba32(ref color); + colorPalette[idx] = color.B; + colorPalette[idx + 1] = color.G; + colorPalette[idx + 2] = color.R; - // TODO: Use bulk conversion here for better perf - int idx = 0; - foreach (TPixel quantizedColor in quantizedColors) - { - quantizedColor.ToRgba32(ref color); - colorPalette[idx] = color.B; - colorPalette[idx + 1] = color.G; - colorPalette[idx + 2] = color.R; - - // Padding byte, always 0. - colorPalette[idx + 3] = 0; - idx += 4; - } + // Padding byte, always 0. + colorPalette[idx + 3] = 0; + idx += 4; + } + + stream.Write(colorPalette); - stream.Write(colorPalette); + for (int y = image.Height - 1; y >= 0; y--) + { + ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); + stream.Write(pixelSpan); - for (int y = image.Height - 1; y >= 0; y--) + for (int i = 0; i < this.padding; i++) { - ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); - stream.Write(pixelSpan); - - for (int i = 0; i < this.padding; i++) - { - stream.WriteByte(0); - } + stream.WriteByte(0); } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index a691e527e..3a0fa5169 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -6,8 +6,6 @@ using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -28,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Configuration bound to the encoding operation. /// - private Configuration configuration; + private readonly Configuration configuration; /// /// A reusable buffer used to reduce allocations. @@ -81,10 +79,10 @@ namespace SixLabors.ImageSharp.Formats.Gif bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; // Quantize the image returning a palette. - IQuantizedFrame quantized; + QuantizedFrame quantized; using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { - quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); } // Get the number of bits. @@ -127,7 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Gif stream.WriteByte(GifConstants.EndIntroducer); } - private void EncodeGlobal(Image image, IQuantizedFrame quantized, int transparencyIndex, Stream stream) + private void EncodeGlobal(Image image, QuantizedFrame quantized, int transparencyIndex, Stream stream) where TPixel : struct, IPixel { for (int i = 0; i < image.Frames.Count; i++) @@ -144,19 +142,16 @@ namespace SixLabors.ImageSharp.Formats.Gif } else { - using (IFrameQuantizer paletteFrameQuantizer = - new PaletteFrameQuantizer(this.configuration, this.quantizer.Diffuser, quantized.Palette)) + using (var paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Options, quantized.Palette)) + using (QuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) { - using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame)) - { - this.WriteImageData(paletteQuantized, stream); - } + this.WriteImageData(paletteQuantized, stream); } } } } - private void EncodeLocal(Image image, IQuantizedFrame quantized, Stream stream) + private void EncodeLocal(Image image, QuantizedFrame quantized, Stream stream) where TPixel : struct, IPixel { ImageFrame previousFrame = null; @@ -171,16 +166,23 @@ namespace SixLabors.ImageSharp.Formats.Gif if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength && frameMetadata.ColorTableLength > 0) { - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, frameMetadata.ColorTableLength)) + var options = new QuantizerOptions + { + Dither = this.quantizer.Options.Dither, + DitherScale = this.quantizer.Options.DitherScale, + MaxColors = frameMetadata.ColorTableLength + }; + + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, options)) { - quantized = frameQuantizer.QuantizeFrame(frame); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } else { using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { - quantized = frameQuantizer.QuantizeFrame(frame); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } } @@ -206,7 +208,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The . /// - private int GetTransparentIndex(IQuantizedFrame quantized) + private int GetTransparentIndex(QuantizedFrame quantized) where TPixel : struct, IPixel { // Transparent pixels are much more likely to be found at the end of a palette @@ -435,7 +437,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// The pixel format. /// The to encode. /// The stream to write to. - private void WriteColorTable(IQuantizedFrame image, Stream stream) + private void WriteColorTable(QuantizedFrame image, Stream stream) where TPixel : struct, IPixel { // The maximum number of colors for the bit depth @@ -457,9 +459,9 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Writes the image pixel data to the stream. /// /// The pixel format. - /// The containing indexed pixels. + /// The containing indexed pixels. /// The stream to write to. - private void WriteImageData(IQuantizedFrame image, Stream stream) + private void WriteImageData(QuantizedFrame image, Stream stream) where TPixel : struct, IPixel { using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 69a80e024..5f14d483b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -146,7 +146,7 @@ namespace SixLabors.ImageSharp.Formats.Png ImageMetadata metadata = image.Metadata; PngMetadata pngMetadata = metadata.GetPngMetadata(); PngEncoderOptionsHelpers.AdjustOptions(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); - IQuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); + QuantizedFrame quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized); stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); @@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The row span. /// The quantized pixels. Can be null. /// The row. - private void CollectPixelBytes(ReadOnlySpan rowSpan, IQuantizedFrame quantized, int row) + private void CollectPixelBytes(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) where TPixel : struct, IPixel { switch (this.options.ColorType) @@ -440,7 +440,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, IQuantizedFrame quantized, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) where TPixel : struct, IPixel { this.CollectPixelBytes(rowSpan, quantized, row); @@ -546,7 +546,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixel format. /// The containing image data. /// The quantized frame. - private void WritePaletteChunk(Stream stream, IQuantizedFrame quantized) + private void WritePaletteChunk(Stream stream, QuantizedFrame quantized) where TPixel : struct, IPixel { if (quantized == null) @@ -783,7 +783,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The image. /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, IQuantizedFrame quantized, Stream stream) + private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream) where TPixel : struct, IPixel { byte[] buffer; @@ -881,7 +881,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The pixels. /// The quantized pixels span. /// The deflate stream. - private void EncodePixels(ImageFrame pixels, IQuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodePixels(ImageFrame pixels, QuantizedFrame quantized, ZlibDeflateStream deflateStream) where TPixel : struct, IPixel { int bytesPerScanline = this.CalculateScanlineLength(this.width); @@ -960,7 +960,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The type of the pixel. /// The quantized. /// The deflate stream. - private void EncodeAdam7IndexedPixels(IQuantizedFrame quantized, ZlibDeflateStream deflateStream) + private void EncodeAdam7IndexedPixels(QuantizedFrame quantized, ZlibDeflateStream deflateStream) where TPixel : struct, IPixel { int width = quantized.Width; diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index b494c164f..172b6208a 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The type of the pixel. /// The options. /// The image. - public static IQuantizedFrame CreateQuantizedFrame( + public static QuantizedFrame CreateQuantizedFrame( PngEncoderOptions options, Image image) where TPixel : struct, IPixel @@ -72,13 +72,15 @@ namespace SixLabors.ImageSharp.Formats.Png // Use the metadata to determine what quantization depth to use if no quantizer has been set. if (options.Quantizer is null) { - options.Quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits)); + var maxColors = ImageMaths.GetColorCountForBitDepth(bits); + options.Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = maxColors }); } // Create quantized frame returning the palette and set the bit depth. using (IFrameQuantizer frameQuantizer = options.Quantizer.CreateFrameQuantizer(image.GetConfiguration())) { - return frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + ImageFrame frame = image.Frames.RootFrame; + return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } @@ -92,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png public static byte CalculateBitDepth( PngEncoderOptions options, Image image, - IQuantizedFrame quantizedFrame) + QuantizedFrame quantizedFrame) where TPixel : struct, IPixel { byte bitDepth; diff --git a/src/ImageSharp/Primitives/DenseMatrix{T}.cs b/src/ImageSharp/Primitives/DenseMatrix{T}.cs index 4229e69e7..3fda03b77 100644 --- a/src/ImageSharp/Primitives/DenseMatrix{T}.cs +++ b/src/ImageSharp/Primitives/DenseMatrix{T}.cs @@ -136,7 +136,7 @@ namespace SixLabors.ImageSharp /// [MethodImpl(InliningOptions.ShortMethod)] #pragma warning disable SA1008 // Opening parenthesis should be spaced correctly - public static implicit operator T[,] (in DenseMatrix data) + public static implicit operator T[,](in DenseMatrix data) #pragma warning restore SA1008 // Opening parenthesis should be spaced correctly { var result = new T[data.Rows, data.Columns]; @@ -153,6 +153,24 @@ namespace SixLabors.ImageSharp return result; } + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(DenseMatrix left, DenseMatrix right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(DenseMatrix left, DenseMatrix right) + => !(left == right); + /// /// Transposes the rows and columns of the dense matrix. /// @@ -210,15 +228,32 @@ namespace SixLabors.ImageSharp } /// - public override bool Equals(object obj) => obj is DenseMatrix other && this.Equals(other); + public override bool Equals(object obj) + => obj is DenseMatrix other && this.Equals(other); /// + [MethodImpl(InliningOptions.ShortMethod)] public bool Equals(DenseMatrix other) => this.Columns == other.Columns && this.Rows == other.Rows && this.Span.SequenceEqual(other.Span); /// - public override int GetHashCode() => this.Data.GetHashCode(); + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() + { + HashCode code = default; + + code.Add(this.Columns); + code.Add(this.Rows); + + Span span = this.Span; + for (int i = 0; i < span.Length; i++) + { + code.Add(span[i]); + } + + return code.ToHashCode(); + } } } 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..a04aa0df8 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 @@ -14,12 +13,12 @@ namespace SixLabors.ImageSharp.Processing public static class DitherExtensions { /// - /// Dithers the image reducing it to a web-safe palette using Bayer4x4 ordered dithering. + /// Dithers the image reducing it to a web-safe palette using . /// /// The image this method extends. /// The to allow chaining of operations. public static IImageProcessingContext Dither(this IImageProcessingContext source) => - Dither(source, KnownDitherers.BayerDither4x4); + Dither(source, KnownDitherings.Bayer8x8); /// /// Dithers the image reducing it to a web-safe palette using ordered dithering. @@ -27,21 +26,62 @@ 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 a web-safe palette using ordered dithering. + /// + /// The image this method extends. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale)); + + /// + /// Dithers the image reducing it to the given palette using ordered dithering. + /// + /// The image this method extends. + /// The ordered ditherer. + /// The palette to select substitute colors from. + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + ReadOnlyMemory palette) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette)); /// /// Dithers the image reducing it to the given palette using ordered dithering. /// /// The image this method extends. /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. /// The palette to select substitute colors from. /// The to allow chaining of operations. public static IImageProcessingContext Dither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, + float ditherScale, ReadOnlyMemory palette) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette)); + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette)); + + /// + /// Dithers the image reducing it to a web-safe palette using . + /// + /// The image this method extends. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext Dither(this IImageProcessingContext source, Rectangle rectangle) => + Dither(source, KnownDitherings.Bayer8x8, rectangle); /// /// Dithers the image reducing it to a web-safe palette using ordered dithering. @@ -54,15 +94,50 @@ 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 PaletteDitherProcessor(dither), rectangle); + + /// + /// Dithers the image reducing it to a web-safe palette using ordered dithering. + /// + /// The image this method extends. + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + float ditherScale, + Rectangle rectangle) => + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale), rectangle); + + /// + /// Dithers the image reducing it to the given palette using ordered dithering. + /// + /// The image this method extends. + /// The ordered ditherer. + /// The palette to select substitute colors from. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IDither dither, + ReadOnlyMemory palette, Rectangle rectangle) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle); + source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle); /// /// Dithers the image reducing it to the given palette using ordered dithering. /// /// The image this method extends. /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. /// The palette to select substitute colors from. /// /// The structure that specifies the portion of the image object to alter. @@ -70,9 +145,10 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Dither( this IImageProcessingContext source, - IOrderedDither dither, + IDither dither, + float ditherScale, ReadOnlyMemory palette, Rectangle rectangle) => - source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle); + source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette), rectangle); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs index 3410ee6be..86ccddd85 100644 --- a/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.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.Quantization; @@ -27,5 +27,28 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Quantize(this IImageProcessingContext source, IQuantizer quantizer) => source.ApplyProcessor(new QuantizeProcessor(quantizer)); + + /// + /// Applies quantization to the image using the . + /// + /// The image this method extends. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext Quantize(this IImageProcessingContext source, Rectangle rectangle) => + Quantize(source, KnownQuantizers.Octree, rectangle); + + /// + /// Applies quantization to the image. + /// + /// The image this method extends. + /// The quantizer to apply to perform the operation. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext Quantize(this IImageProcessingContext source, IQuantizer quantizer, Rectangle rectangle) => + source.ApplyProcessor(new QuantizeProcessor(quantizer), 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 deleted file mode 100644 index dad5bb38c..000000000 --- a/src/ImageSharp/Processing/KnownDitherers.cs +++ /dev/null @@ -1,33 +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 ordered dither matrices - /// - public static class KnownDitherers - { - /// - /// Gets the order ditherer using the 2x2 Bayer dithering matrix - /// - public static IOrderedDither BayerDither2x2 { get; } = new BayerDither2x2(); - - /// - /// Gets the order ditherer using the 3x3 dithering matrix - /// - public static IOrderedDither OrderedDither3x3 { get; } = new OrderedDither3x3(); - - /// - /// Gets the order ditherer using the 4x4 Bayer dithering matrix - /// - public static IOrderedDither BayerDither4x4 { get; } = new BayerDither4x4(); - - /// - /// Gets the order ditherer using the 8x8 Bayer dithering matrix - /// - public static IOrderedDither BayerDither8x8 { get; } = new BayerDither8x8(); - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/KnownDitherings.cs b/src/ImageSharp/Processing/KnownDitherings.cs new file mode 100644 index 000000000..bb968d2ef --- /dev/null +++ b/src/ImageSharp/Processing/KnownDitherings.cs @@ -0,0 +1,78 @@ +// 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 dithering algorithms. + /// + public static class KnownDitherings + { + /// + /// Gets the order ditherer using the 2x2 Bayer dithering matrix + /// + public static IDither Bayer2x2 { get; } = OrderedDither.Bayer2x2; + + /// + /// Gets the order ditherer using the 3x3 dithering matrix + /// + public static IDither Ordered3x3 { get; } = OrderedDither.Ordered3x3; + + /// + /// Gets the order ditherer using the 4x4 Bayer dithering matrix + /// + public static IDither Bayer4x4 { get; } = OrderedDither.Bayer4x4; + + /// + /// Gets the order ditherer using the 8x8 Bayer dithering matrix + /// + public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8; + + /// + /// Gets the error Dither that implements the Atkinson algorithm. + /// + public static IDither Atkinson { get; } = ErrorDither.Atkinson; + + /// + /// Gets the error Dither that implements the Burks algorithm. + /// + public static IDither Burks { get; } = ErrorDither.Burkes; + + /// + /// Gets the error Dither that implements the Floyd-Steinberg algorithm. + /// + public static IDither FloydSteinberg { get; } = ErrorDither.FloydSteinberg; + + /// + /// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm. + /// + public static IDither JarvisJudiceNinke { get; } = ErrorDither.JarvisJudiceNinke; + + /// + /// Gets the error Dither that implements the Sierra-2 algorithm. + /// + public static IDither Sierra2 { get; } = ErrorDither.Sierra2; + + /// + /// Gets the error Dither that implements the Sierra-3 algorithm. + /// + public static IDither Sierra3 { get; } = ErrorDither.Sierra3; + + /// + /// Gets the error Dither that implements the Sierra-Lite algorithm. + /// + public static IDither SierraLite { get; } = ErrorDither.SierraLite; + + /// + /// Gets the error Dither that implements the Stevenson-Arce algorithm. + /// + public static IDither StevensonArce { get; } = ErrorDither.StevensonArce; + + /// + /// Gets the error Dither that implements the Stucki algorithm. + /// + public static IDither Stucki { get; } = ErrorDither.Stucki; + } +} 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/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index 1ebd6476e..36d36223a 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -22,26 +21,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution internal class BokehBlurProcessor : ImageProcessor where TPixel : struct, IPixel { - /// - /// The kernel radius. - /// - private readonly int radius; - /// /// The gamma highlight factor to use when applying the effect /// private readonly float gamma; - /// - /// The maximum size of the kernel in either direction - /// - private readonly int kernelSize; - - /// - /// The number of components to use when applying the bokeh blur - /// - private readonly int componentsCount; - /// /// The kernel parameters to use for the current instance (a: X, b: Y, A: Z, B: W) /// @@ -52,16 +36,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// private readonly Complex64[][] kernels; - /// - /// The scaling factor for kernel values - /// - private readonly float kernelsScale; - - /// - /// The mapping of initialized complex kernels and parameters, to speed up the initialization of new instances - /// - private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); - /// /// Initializes a new instance of the class. /// @@ -72,29 +46,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution public BokehBlurProcessor(Configuration configuration, BokehBlurProcessor definition, Image source, Rectangle sourceRectangle) : base(configuration, source, sourceRectangle) { - this.radius = definition.Radius; - this.kernelSize = (this.radius * 2) + 1; - this.componentsCount = definition.Components; this.gamma = definition.Gamma; - // Reuse the initialized values from the cache, if possible - var parameters = new BokehBlurParameters(this.radius, this.componentsCount); - if (Cache.TryGetValue(parameters, out BokehBlurKernelData info)) - { - this.kernelParameters = info.Parameters; - this.kernelsScale = info.Scale; - this.kernels = info.Kernels; - } - else - { - // Initialize the complex kernels and parameters with the current arguments - (this.kernelParameters, this.kernelsScale) = this.GetParameters(); - this.kernels = this.CreateComplexKernels(); - this.NormalizeKernels(); + // Get the bokeh blur data + BokehBlurKernelData data = BokehBlurKernelDataProvider.GetBokehBlurKernelData( + definition.Radius, + (definition.Radius * 2) + 1, + definition.Components); - // Store them in the cache for future use - Cache.TryAdd(parameters, new BokehBlurKernelData(this.kernelParameters, this.kernelsScale, this.kernels)); - } + this.kernelParameters = data.Parameters; + this.kernels = data.Kernels; } /// @@ -107,163 +68,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public IReadOnlyList KernelParameters => this.kernelParameters; - /// - /// Gets the kernel scales to adjust the component values in each kernel - /// - private static IReadOnlyList KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; - - /// - /// Gets the available bokeh blur kernel parameters - /// - private static IReadOnlyList KernelComponents { get; } = new[] - { - // 1 component - new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) }, - - // 2 components - new[] - { - new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f), - new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f) - }, - - // 3 components - new[] - { - new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f), - new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f), - new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f) - }, - - // 4 components - new[] - { - new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f), - new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f), - new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f), - new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f) - }, - - // 5 components - new[] - { - new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f), - new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f), - new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f), - new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f), - new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f) - }, - - // 6 components - new[] - { - new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f), - new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f), - new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f), - new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f), - new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f), - new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f) - } - }; - - /// - /// Gets the kernel parameters and scaling factor for the current count value in the current instance - /// - private (Vector4[] Parameters, float Scale) GetParameters() - { - // Prepare the kernel components - int index = Math.Max(0, Math.Min(this.componentsCount - 1, KernelComponents.Count)); - return (KernelComponents[index], KernelScales[index]); - } - - /// - /// Creates the collection of complex 1D kernels with the specified parameters - /// - private Complex64[][] CreateComplexKernels() - { - var kernels = new Complex64[this.kernelParameters.Length][]; - ref Vector4 baseRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); - for (int i = 0; i < this.kernelParameters.Length; i++) - { - ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i); - kernels[i] = this.CreateComplex1DKernel(paramsRef.X, paramsRef.Y); - } - - return kernels; - } - - /// - /// Creates a complex 1D kernel with the specified parameters - /// - /// The exponential parameter for each complex component - /// The angle component for each complex component - private Complex64[] CreateComplex1DKernel(float a, float b) - { - var kernel = new Complex64[this.kernelSize]; - ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan()); - int r = this.radius, n = -r; - - for (int i = 0; i < this.kernelSize; i++, n++) - { - // Incrementally compute the range values - float value = n * this.kernelsScale * (1f / r); - value *= value; - - // Fill in the complex kernel values - Unsafe.Add(ref baseRef, i) = new Complex64( - MathF.Exp(-a * value) * MathF.Cos(b * value), - MathF.Exp(-a * value) * MathF.Sin(b * value)); - } - - return kernel; - } - - /// - /// Normalizes the kernels with respect to A * real + B * imaginary - /// - private void NormalizeKernels() - { - // Calculate the complex weighted sum - float total = 0; - Span kernelsSpan = this.kernels.AsSpan(); - ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan); - ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan()); - - for (int i = 0; i < this.kernelParameters.Length; i++) - { - ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, i); - int length = kernelRef.Length; - ref Complex64 valueRef = ref kernelRef[0]; - ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, i); - - for (int j = 0; j < length; j++) - { - for (int k = 0; k < length; k++) - { - ref Complex64 jRef = ref Unsafe.Add(ref valueRef, j); - ref Complex64 kRef = ref Unsafe.Add(ref valueRef, k); - total += - (paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary))) - + (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real))); - } - } - } - - // Normalize the kernels - float scalar = 1f / MathF.Sqrt(total); - for (int i = 0; i < kernelsSpan.Length; i++) - { - ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, i); - int length = kernelsRef.Length; - ref Complex64 valueRef = ref kernelsRef[0]; - - for (int j = 0; j < length; j++) - { - Unsafe.Add(ref valueRef, j) *= scalar; - } - } - } - /// protected override void OnFrameApply(ImageFrame source) { diff --git a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs index 5f03396ba..561892683 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.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.Numerics; @@ -15,11 +15,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters /// public readonly Vector4[] Parameters; - /// - /// The scaling factor for the kernel values - /// - public readonly float Scale; - /// /// The kernel components to apply the bokeh blur effect /// @@ -29,12 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters /// Initializes a new instance of the struct. /// /// The kernel parameters - /// The kernel scale factor /// The complex kernel components - public BokehBlurKernelData(Vector4[] parameters, float scale, Complex64[][] kernels) + public BokehBlurKernelData(Vector4[] parameters, Complex64[][] kernels) { this.Parameters = parameters; - this.Scale = scale; this.Kernels = kernels; } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs new file mode 100644 index 000000000..f7828fa9e --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs @@ -0,0 +1,228 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters +{ + /// + /// Provides parameters to be used in the . + /// + internal static class BokehBlurKernelDataProvider + { + /// + /// The mapping of initialized complex kernels and parameters, to speed up the initialization of new instances + /// + private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + + /// + /// Gets the kernel scales to adjust the component values in each kernel + /// + private static IReadOnlyList KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f }; + + /// + /// Gets the available bokeh blur kernel parameters + /// + private static IReadOnlyList KernelComponents { get; } = new[] + { + // 1 component + new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) }, + + // 2 components + new[] + { + new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f), + new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f) + }, + + // 3 components + new[] + { + new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f), + new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f), + new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f) + }, + + // 4 components + new[] + { + new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f), + new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f), + new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f), + new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f) + }, + + // 5 components + new[] + { + new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f), + new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f), + new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f), + new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f), + new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f) + }, + + // 6 components + new[] + { + new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f), + new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f), + new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f), + new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f), + new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f), + new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f) + } + }; + + /// + /// Gets the bokeh blur kernel data for the specified parameters. + /// + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The number of components to use to approximate the original 2D bokeh blur convolution kernel. + /// A instance with the kernel data for the current parameters. + public static BokehBlurKernelData GetBokehBlurKernelData( + int radius, + int kernelSize, + int componentsCount) + { + // Reuse the initialized values from the cache, if possible + var parameters = new BokehBlurParameters(radius, componentsCount); + if (!Cache.TryGetValue(parameters, out BokehBlurKernelData info)) + { + // Initialize the complex kernels and parameters with the current arguments + (Vector4[] kernelParameters, float kernelsScale) = GetParameters(componentsCount); + Complex64[][] kernels = CreateComplexKernels(kernelParameters, radius, kernelSize, kernelsScale); + NormalizeKernels(kernels, kernelParameters); + + // Store them in the cache for future use + info = new BokehBlurKernelData(kernelParameters, kernels); + Cache.TryAdd(parameters, info); + } + + return info; + } + + /// + /// Gets the kernel parameters and scaling factor for the current count value in the current instance + /// + private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount) + { + // Prepare the kernel components + int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Count)); + + return (KernelComponents[index], KernelScales[index]); + } + + /// + /// Creates the collection of complex 1D kernels with the specified parameters + /// + /// The parameters to use to normalize the kernels + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The scale factor for each kernel. + private static Complex64[][] CreateComplexKernels( + Vector4[] kernelParameters, + int radius, + int kernelSize, + float kernelsScale) + { + var kernels = new Complex64[kernelParameters.Length][]; + ref Vector4 baseRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); + for (int i = 0; i < kernelParameters.Length; i++) + { + ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i); + kernels[i] = CreateComplex1DKernel(radius, kernelSize, kernelsScale, paramsRef.X, paramsRef.Y); + } + + return kernels; + } + + /// + /// Creates a complex 1D kernel with the specified parameters + /// + /// The value representing the size of the area to sample. + /// The size of each kernel to compute. + /// The scale factor for each kernel. + /// The exponential parameter for each complex component + /// The angle component for each complex component + private static Complex64[] CreateComplex1DKernel( + int radius, + int kernelSize, + float kernelsScale, + float a, + float b) + { + var kernel = new Complex64[kernelSize]; + ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan()); + int r = radius, n = -r; + + for (int i = 0; i < kernelSize; i++, n++) + { + // Incrementally compute the range values + float value = n * kernelsScale * (1f / r); + value *= value; + + // Fill in the complex kernel values + Unsafe.Add(ref baseRef, i) = new Complex64( + MathF.Exp(-a * value) * MathF.Cos(b * value), + MathF.Exp(-a * value) * MathF.Sin(b * value)); + } + + return kernel; + } + + /// + /// Normalizes the kernels with respect to A * real + B * imaginary + /// + /// The current convolution kernels to normalize + /// The parameters to use to normalize the kernels + private static void NormalizeKernels(Complex64[][] kernels, Vector4[] kernelParameters) + { + // Calculate the complex weighted sum + float total = 0; + Span kernelsSpan = kernels.AsSpan(); + ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan); + ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan()); + + for (int i = 0; i < kernelParameters.Length; i++) + { + ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, i); + int length = kernelRef.Length; + ref Complex64 valueRef = ref kernelRef[0]; + ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, i); + + for (int j = 0; j < length; j++) + { + for (int k = 0; k < length; k++) + { + ref Complex64 jRef = ref Unsafe.Add(ref valueRef, j); + ref Complex64 kRef = ref Unsafe.Add(ref valueRef, k); + total += + (paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary))) + + (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real))); + } + } + } + + // Normalize the kernels + float scalar = 1f / MathF.Sqrt(total); + for (int i = 0; i < kernelsSpan.Length; i++) + { + ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, i); + int length = kernelsRef.Length; + ref Complex64 valueRef = ref kernelsRef[0]; + + for (int j = 0; j < length; j++) + { + Unsafe.Add(ref valueRef, j) *= scalar; + } + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs deleted file mode 100644 index 9cf10ce59..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Atkinson image dithering algorithm. - /// - /// - public sealed class AtkinsonDiffuser : ErrorDiffuser - { - private const float Divisor = 8F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix AtkinsonMatrix = - new float[,] - { - { 0, 0, 1 / Divisor, 1 / Divisor }, - { 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 }, - { 0, 1 / Divisor, 0, 0 } - }; - - /// - /// Initializes a new instance of the class. - /// - public AtkinsonDiffuser() - : base(AtkinsonMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/BayerDither2x2.cs b/src/ImageSharp/Processing/Processors/Dithering/BayerDither2x2.cs deleted file mode 100644 index b7fdfbfe5..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/BayerDither2x2.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies order dithering using the 2x2 Bayer dithering matrix. - /// - public sealed class BayerDither2x2 : OrderedDither - { - /// - /// Initializes a new instance of the class. - /// - public BayerDither2x2() - : base(2) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/BayerDither4x4.cs b/src/ImageSharp/Processing/Processors/Dithering/BayerDither4x4.cs deleted file mode 100644 index 4f6d5dd07..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/BayerDither4x4.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies order dithering using the 4x4 Bayer dithering matrix. - /// - public sealed class BayerDither4x4 : OrderedDither - { - /// - /// Initializes a new instance of the class. - /// - public BayerDither4x4() - : base(4) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/BayerDither8x8.cs b/src/ImageSharp/Processing/Processors/Dithering/BayerDither8x8.cs deleted file mode 100644 index 8d0c23aa3..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/BayerDither8x8.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies order dithering using the 8x8 Bayer dithering matrix. - /// - public sealed class BayerDither8x8 : OrderedDither - { - /// - /// Initializes a new instance of the class. - /// - public BayerDither8x8() - : base(8) - { - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs deleted file mode 100644 index 152704ec2..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Burks image dithering algorithm. - /// - /// - public sealed class BurksDiffuser : ErrorDiffuser - { - private const float Divisor = 32F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix BurksMatrix = - new float[,] - { - { 0, 0, 0, 8 / Divisor, 4 / Divisor }, - { 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public BurksDiffuser() - : base(BurksMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErroDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/ErroDither.KnownTypes.cs new file mode 100644 index 000000000..d39237a2c --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/ErroDither.KnownTypes.cs @@ -0,0 +1,188 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// An error diffusion dithering implementation. + /// + public readonly partial struct ErrorDither + { + /// + /// Applies error diffusion based dithering using the Atkinson image dithering algorithm. + /// + public static ErrorDither Atkinson = CreateAtkinson(); + + /// + /// Applies error diffusion based dithering using the Burks image dithering algorithm. + /// + public static ErrorDither Burkes = CreateBurks(); + + /// + /// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm. + /// + public static ErrorDither FloydSteinberg = CreateFloydSteinberg(); + + /// + /// Applies error diffusion based dithering using the Jarvis, Judice, Ninke image dithering algorithm. + /// + public static ErrorDither JarvisJudiceNinke = CreateJarvisJudiceNinke(); + + /// + /// Applies error diffusion based dithering using the Sierra2 image dithering algorithm. + /// + public static ErrorDither Sierra2 = CreateSierra2(); + + /// + /// Applies error diffusion based dithering using the Sierra3 image dithering algorithm. + /// + public static ErrorDither Sierra3 = CreateSierra3(); + + /// + /// Applies error diffusion based dithering using the Sierra Lite image dithering algorithm. + /// + public static ErrorDither SierraLite = CreateSierraLite(); + + /// + /// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm. + /// + public static ErrorDither StevensonArce = CreateStevensonArce(); + + /// + /// Applies error diffusion based dithering using the Stucki image dithering algorithm. + /// + public static ErrorDither Stucki = CreateStucki(); + + private static ErrorDither CreateAtkinson() + { + const float Divisor = 8F; + const int Offset = 1; + + var matrix = new float[,] + { + { 0, 0, 1 / Divisor, 1 / Divisor }, + { 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 }, + { 0, 1 / Divisor, 0, 0 } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateBurks() + { + const float Divisor = 32F; + const int Offset = 2; + + var matrix = new float[,] + { + { 0, 0, 0, 8 / Divisor, 4 / Divisor }, + { 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateFloydSteinberg() + { + const float Divisor = 16F; + const int Offset = 1; + + var matrix = new float[,] + { + { 0, 0, 7 / Divisor }, + { 3 / Divisor, 5 / Divisor, 1 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateJarvisJudiceNinke() + { + const float Divisor = 48F; + const int Offset = 2; + + var matrix = new float[,] + { + { 0, 0, 0, 7 / Divisor, 5 / Divisor }, + { 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor }, + { 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateSierra2() + { + const float Divisor = 16F; + const int Offset = 2; + + var matrix = new float[,] + { + { 0, 0, 0, 4 / Divisor, 3 / Divisor }, + { 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateSierra3() + { + const float Divisor = 32F; + const int Offset = 2; + + var matrix = new float[,] + { + { 0, 0, 0, 5 / Divisor, 3 / Divisor }, + { 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor }, + { 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateSierraLite() + { + const float Divisor = 4F; + const int Offset = 1; + + var matrix = new float[,] + { + { 0, 0, 2 / Divisor }, + { 1 / Divisor, 1 / Divisor, 0 } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateStevensonArce() + { + const float Divisor = 200F; + const int Offset = 3; + + var matrix = new float[,] + { + { 0, 0, 0, 0, 0, 32 / Divisor, 0 }, + { 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor }, + { 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 }, + { 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + + private static ErrorDither CreateStucki() + { + const float Divisor = 42F; + const int Offset = 2; + + var matrix = new float[,] + { + { 0, 0, 0, 8 / Divisor, 4 / Divisor }, + { 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }, + { 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor } + }; + + return new ErrorDither(matrix, Offset); + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs deleted file mode 100644 index d6ccfb369..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.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. - /// - public abstract class ErrorDiffuser : IErrorDiffuser - { - private readonly int offset; - private readonly DenseMatrix matrix; - - /// - /// Initializes a new instance of the class. - /// - /// The dithering matrix. - internal ErrorDiffuser(in DenseMatrix matrix) - { - // 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; - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void Dither(ImageFrame image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY) - where TPixel : struct, IPixel - { - image[x, y] = transformed; - - // Equal? Break out as there's no error to pass. - if (source.Equals(transformed)) - { - return; - } - - // Calculate the error - Vector4 error = source.ToVector4() - transformed.ToVector4(); - 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 && targetY < maxY; row++, targetY++) - { - Span rowSpan = image.GetPixelRowSpan(targetY); - - for (int col = 0; col < matrix.Columns; col++) - { - int targetX = x + (col - offset); - if (targetX >= minX && targetX < maxX) - { - float coefficient = matrix[row, col]; - if (coefficient == 0) - { - continue; - } - - ref TPixel pixel = ref rowSpan[targetX]; - var result = pixel.ToVector4(); - - result += error * coefficient; - pixel.FromVector4(result); - } - } - } - } - } -} 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/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs new file mode 100644 index 000000000..079a22ecc --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// An error diffusion dithering implementation. + /// + /// + public readonly partial struct ErrorDither : IDither, IEquatable, IEquatable + { + private readonly int offset; + private readonly DenseMatrix matrix; + + /// + /// Initializes a new instance of the struct. + /// + /// The diffusion matrix. + /// The starting offset within the matrix. + [MethodImpl(InliningOptions.ShortMethod)] + public ErrorDither(in DenseMatrix matrix, int offset) + { + this.matrix = matrix; + this.offset = offset; + } + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(IDither left, ErrorDither right) + => right == left; + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(IDither left, ErrorDither right) + => !(right == left); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(ErrorDither left, IDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(ErrorDither left, IDither right) + => !(left == right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(ErrorDither left, ErrorDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(ErrorDither left, ErrorDither right) + => !(left == right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ReadOnlyMemory palette, + ImageFrame source, + Memory output, + Rectangle bounds) + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + Span outputSpan = output.Span; + ReadOnlySpan paletteSpan = palette.Span; + int width = bounds.Width; + int offsetY = bounds.Top; + int offsetX = bounds.Left; + float scale = quantizer.Options.DitherScale; + + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + int rowStart = (y - offsetY) * width; + + for (int x = bounds.Left; x < bounds.Right; x++) + { + TPixel sourcePixel = row[x]; + outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); + this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); + } + } + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyPaletteDither( + Configuration configuration, + ReadOnlyMemory palette, + ImageFrame source, + Rectangle bounds, + float scale) + where TPixel : struct, IPixel + { + var pixelMap = new EuclideanPixelMap(palette); + + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y); + for (int x = bounds.Left; x < bounds.Right; x++) + { + TPixel sourcePixel = row[x]; + pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); + this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); + row[x] = transformed; + } + } + } + + // Internal for AOT + [MethodImpl(InliningOptions.ShortMethod)] + internal TPixel Dither( + ImageFrame image, + Rectangle bounds, + TPixel source, + TPixel transformed, + int x, + int y, + float scale) + where TPixel : struct, IPixel + { + // Equal? Break out as there's no error to pass. + if (source.Equals(transformed)) + { + return transformed; + } + + // Calculate the error + Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale; + + 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++) + { + if (targetY >= bounds.Bottom) + { + continue; + } + + Span rowSpan = image.GetPixelRowSpan(targetY); + + for (int col = 0; col < matrix.Columns; col++) + { + int targetX = x + (col - offset); + if (targetX < bounds.Left || targetX >= bounds.Right) + { + continue; + } + + float coefficient = matrix[row, col]; + if (coefficient == 0) + { + continue; + } + + ref TPixel pixel = ref rowSpan[targetX]; + var result = pixel.ToVector4(); + + result += error * coefficient; + pixel.FromVector4(result); + } + } + + return transformed; + } + + /// + public override bool Equals(object obj) + => obj is ErrorDither dither && this.Equals(dither); + + /// + public bool Equals(ErrorDither other) + => this.offset == other.offset && this.matrix.Equals(other.matrix); + + /// + public bool Equals(IDither other) + => this.Equals((object)other); + + /// + public override int GetHashCode() + => HashCode.Combine(this.offset, this.matrix); + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs deleted file mode 100644 index b3137337b..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm. - /// - /// - public sealed class FloydSteinbergDiffuser : ErrorDiffuser - { - private const float Divisor = 16F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix FloydSteinbergMatrix = - new float[,] - { - { 0, 0, 7 / Divisor }, - { 3 / Divisor, 5 / Divisor, 1 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public FloydSteinbergDiffuser() - : base(FloydSteinbergMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs new file mode 100644 index 000000000..fc8ee32f7 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -0,0 +1,53 @@ +// 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.Quantization; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// Defines the contract for types that apply dithering to images. + /// + public interface IDither + { + /// + /// Transforms the quantized image frame applying a dither matrix. + /// This method should be treated as destructive, altering the input pixels. + /// + /// The type of frame quantizer. + /// The pixel format. + /// The frame quantizer. + /// The quantized palette. + /// The source image. + /// The output target + /// The region of interest bounds. + void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ReadOnlyMemory palette, + ImageFrame source, + Memory output, + Rectangle bounds) + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel; + + /// + /// Transforms the image frame applying a dither matrix. + /// This method should be treated as destructive, altering the input pixels. + /// + /// The pixel format. + /// The configuration. + /// The quantized palette. + /// The source image. + /// The region of interest bounds. + /// The dithering scale used to adjust the amount of dither. Range 0..1. + void ApplyPaletteDither( + Configuration configuration, + ReadOnlyMemory palette, + ImageFrame source, + Rectangle bounds, + float scale) + 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/JarvisJudiceNinkeDiffuser.cs deleted file mode 100644 index 40cf79266..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the JarvisJudiceNinke image dithering algorithm. - /// - /// - public sealed class JarvisJudiceNinkeDiffuser : ErrorDiffuser - { - private const float Divisor = 48F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix JarvisJudiceNinkeMatrix = - new float[,] - { - { 0, 0, 0, 7 / Divisor, 5 / Divisor }, - { 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor }, - { 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public JarvisJudiceNinkeDiffuser() - : base(JarvisJudiceNinkeMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs new file mode 100644 index 000000000..d3e710782 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// An ordered dithering matrix with equal sides of arbitrary length + /// + public readonly partial struct OrderedDither : IDither + { + /// + /// Applies order dithering using the 2x2 Bayer dithering matrix. + /// + public static OrderedDither Bayer2x2 = new OrderedDither(2); + + /// + /// Applies order dithering using the 4x4 Bayer dithering matrix. + /// + public static OrderedDither Bayer4x4 = new OrderedDither(4); + + /// + /// Applies order dithering using the 8x8 Bayer dithering matrix. + /// + public static OrderedDither Bayer8x8 = new OrderedDither(8); + + /// + /// Applies order dithering using the 3x3 ordered dithering matrix. + /// + public static OrderedDither Ordered3x3 = new OrderedDither(3); + } +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 34eff4fe9..69e323bd5 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -1,49 +1,303 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// /// An ordered dithering matrix with equal sides of arbitrary length /// - public class OrderedDither : IOrderedDither + public readonly partial struct OrderedDither : IDither, IEquatable, IEquatable { - private readonly DenseMatrix thresholdMatrix; + private readonly DenseMatrix thresholdMatrix; private readonly int modulusX; private readonly int modulusY; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The length of the matrix sides + [MethodImpl(InliningOptions.ShortMethod)] public OrderedDither(uint length) { DenseMatrix ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length); + + // 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 < length; x++) + { + thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F; + } + } + this.modulusX = ditherMatrix.Columns; this.modulusY = ditherMatrix.Rows; + this.thresholdMatrix = thresholdMatrix; + } + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(IDither left, OrderedDither right) + => right == left; + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(IDither left, OrderedDither right) + => !(right == left); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(OrderedDither left, IDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(OrderedDither left, IDither right) + => !(left == right); + + /// + /// Compares the two instances to determine whether they are equal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator ==(OrderedDither left, OrderedDither right) + => left.Equals(right); + + /// + /// Compares the two instances to determine whether they are unequal. + /// + /// The first source instance. + /// The second source instance. + /// The . + public static bool operator !=(OrderedDither left, OrderedDither right) + => !(left == right); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyQuantizationDither( + ref TFrameQuantizer quantizer, + ReadOnlyMemory palette, + ImageFrame source, + Memory output, + Rectangle bounds) + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + var ditherOperation = new QuantizeDitherRowIntervalOperation( + ref quantizer, + in Unsafe.AsRef(this), + source, + output, + bounds, + palette, + ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); + + ParallelRowIterator.IterateRows( + quantizer.Configuration, + bounds, + in ditherOperation); + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void ApplyPaletteDither( + Configuration configuration, + ReadOnlyMemory palette, + ImageFrame source, + Rectangle bounds, + float scale) + where TPixel : struct, IPixel + { + var ditherOperation = new PaletteDitherRowIntervalOperation( + in Unsafe.AsRef(this), + source, + bounds, + palette, + scale, + ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length)); - // 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++) + ParallelRowIterator.IterateRows( + configuration, + bounds, + in ditherOperation); + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal TPixel Dither( + TPixel source, + int x, + int y, + int bitDepth, + float scale) + where TPixel : struct, IPixel + { + Rgba32 rgba = default; + source.ToRgba32(ref rgba); + Rgba32 attempt; + + // Spread 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] * scale; + + 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; + } + + /// + public override bool Equals(object obj) + => obj is OrderedDither dither && this.Equals(dither); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public bool Equals(OrderedDither other) + => this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY; + + /// + public bool Equals(IDither other) + => this.Equals((object)other); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public override int GetHashCode() + => HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); + + private readonly struct QuantizeDitherRowIntervalOperation : IRowIntervalOperation + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + private readonly TFrameQuantizer quantizer; + private readonly OrderedDither dither; + private readonly ImageFrame source; + private readonly Memory output; + private readonly Rectangle bounds; + private readonly ReadOnlyMemory palette; + private readonly int bitDepth; + + [MethodImpl(InliningOptions.ShortMethod)] + public QuantizeDitherRowIntervalOperation( + ref TFrameQuantizer quantizer, + in OrderedDither dither, + ImageFrame source, + Memory output, + Rectangle bounds, + ReadOnlyMemory palette, + int bitDepth) + { + this.quantizer = quantizer; + this.dither = dither; + this.source = source; + this.output = output; + this.bounds = bounds; + this.palette = palette; + this.bitDepth = bitDepth; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) { - for (int x = 0; x < ditherMatrix.Columns; x++) + ReadOnlySpan paletteSpan = this.palette.Span; + Span outputSpan = this.output.Span; + int width = this.bounds.Width; + int offsetY = this.bounds.Top; + int offsetX = this.bounds.Left; + float scale = this.quantizer.Options.DitherScale; + + for (int y = rows.Min; y < rows.Max; y++) { - ditherMatrix[y, x] = (uint)((ditherMatrix[y, x] + 1) * multiplier) - 1; + Span row = this.source.GetPixelRowSpan(y); + int rowStart = (y - offsetY) * width; + + // TODO: This can be a bulk operation. + for (int x = this.bounds.Left; x < this.bounds.Right; x++) + { + TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale); + outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); + } } } - - this.thresholdMatrix = ditherMatrix; } - /// - public void Dither(ImageFrame image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y) + private readonly struct PaletteDitherRowIntervalOperation : IRowIntervalOperation where TPixel : struct, IPixel { - image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper; + private readonly OrderedDither dither; + private readonly ImageFrame source; + private readonly Rectangle bounds; + private readonly EuclideanPixelMap pixelMap; + private readonly float scale; + private readonly int bitDepth; + + [MethodImpl(InliningOptions.ShortMethod)] + public PaletteDitherRowIntervalOperation( + in OrderedDither dither, + ImageFrame source, + Rectangle bounds, + ReadOnlyMemory palette, + float scale, + int bitDepth) + { + this.dither = dither; + this.source = source; + this.bounds = bounds; + this.pixelMap = new EuclideanPixelMap(palette); + this.scale = scale; + this.bitDepth = bitDepth; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + for (int y = rows.Min; y < rows.Max; y++) + { + Span row = this.source.GetPixelRowSpan(y); + + for (int x = this.bounds.Left; x < this.bounds.Right; x++) + { + TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale); + this.pixelMap.GetClosestColor(dithered, out TPixel transformed); + row[x] = transformed; + } + } + } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither3x3.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither3x3.cs deleted file mode 100644 index 93bce0578..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither3x3.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies order dithering using the 3x3 dithering matrix. - /// - public sealed class OrderedDither3x3 : OrderedDither - { - /// - /// Initializes a new instance of the class. - /// - public OrderedDither3x3() - : base(3) - { - } - } -} \ 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..6217535c5 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs @@ -2,32 +2,78 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; 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, QuantizerConstants.MaxDitherScale) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ordered ditherer. + /// The dithering scale used to adjust the amount of dither. + public PaletteDitherProcessor(IDither dither, float ditherScale) + : this(dither, ditherScale, 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, QuantizerConstants.MaxDitherScale, palette) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dithering algorithm. + /// The dithering scale used to adjust the amount of dither. + /// The palette to select substitute colors from. + public PaletteDitherProcessor(IDither dither, float ditherScale, ReadOnlyMemory palette) + { + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + Guard.NotNull(dither, nameof(dither)); + this.Dither = dither; + this.DitherScale = ditherScale.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); this.Palette = palette; } + /// + /// Gets the dithering algorithm to apply to the output image. + /// + public IDither Dither { get; } + + /// + /// Gets the dithering scale used to adjust the amount of dither. Range 0..1. + /// + public float DitherScale { 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..118352ec3 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -3,25 +3,22 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; 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 IDither dither; + private readonly float ditherScale; + private readonly ReadOnlyMemory sourcePalette; private IMemoryOwner palette; - private IMemoryOwner paletteVector; - private bool palleteVectorMapped; private bool isDisposed; /// @@ -31,34 +28,39 @@ 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.dither = definition.Dither; + this.ditherScale = definition.DitherScale; + this.sourcePalette = definition.Palette; } - protected PaletteDitherProcessor Definition { get; } + /// + protected override void OnFrameApply(ImageFrame source) + { + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + + this.dither.ApplyPaletteDither( + this.Configuration, + this.palette.Memory, + source, + interest, + this.ditherScale); + } /// protected override void BeforeFrameApply(ImageFrame source) { // Lazy init palettes: - if (!this.palleteVectorMapped) + if (this.palette 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.palleteVectorMapped = true; - base.BeforeFrameApply(source); } @@ -73,71 +75,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/PixelPair.cs b/src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs deleted file mode 100644 index 13660d30a..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Represents a composite pair of pixels. Used for caching color distance lookups. - /// - /// The pixel format. - internal readonly struct PixelPair : IEquatable> - where TPixel : struct, IPixel - { - /// - /// Initializes a new instance of the struct. - /// - /// The first pixel color - /// The second pixel color - public PixelPair(TPixel first, TPixel second) - { - this.First = first; - this.Second = second; - } - - /// - /// Gets the first pixel color - /// - public TPixel First { get; } - - /// - /// Gets the second pixel color - /// - public TPixel Second { get; } - - /// - public bool Equals(PixelPair other) - => this.First.Equals(other.First) && this.Second.Equals(other.Second); - - /// - public override bool Equals(object obj) - => obj is PixelPair other && this.First.Equals(other.First) && this.Second.Equals(other.Second); - - /// - public override int GetHashCode() => HashCode.Combine(this.First, this.Second); - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs deleted file mode 100644 index 001df19af..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Sierra2 image dithering algorithm. - /// - /// - public sealed class Sierra2Diffuser : ErrorDiffuser - { - private const float Divisor = 16F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix Sierra2Matrix = - new float[,] - { - { 0, 0, 0, 4 / Divisor, 3 / Divisor }, - { 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public Sierra2Diffuser() - : base(Sierra2Matrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs deleted file mode 100644 index 3e56c63b3..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Sierra3 image dithering algorithm. - /// - /// - public sealed class Sierra3Diffuser : ErrorDiffuser - { - private const float Divisor = 32F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix Sierra3Matrix = - new float[,] - { - { 0, 0, 0, 5 / Divisor, 3 / Divisor }, - { 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor }, - { 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 } - }; - - /// - /// Initializes a new instance of the class. - /// - public Sierra3Diffuser() - : base(Sierra3Matrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs deleted file mode 100644 index 763695d66..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the SierraLite image dithering algorithm. - /// - /// - public sealed class SierraLiteDiffuser : ErrorDiffuser - { - private const float Divisor = 4F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix SierraLiteMatrix = - new float[,] - { - { 0, 0, 2 / Divisor }, - { 1 / Divisor, 1 / Divisor, 0 } - }; - - /// - /// Initializes a new instance of the class. - /// - public SierraLiteDiffuser() - : base(SierraLiteMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs deleted file mode 100644 index 72ff30c11..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm. - /// - public sealed class StevensonArceDiffuser : ErrorDiffuser - { - private const float Divisor = 200F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix StevensonArceMatrix = - new float[,] - { - { 0, 0, 0, 0, 0, 32 / Divisor, 0 }, - { 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor }, - { 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 }, - { 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public StevensonArceDiffuser() - : base(StevensonArceMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs b/src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs deleted file mode 100644 index 78e8fb4e4..000000000 --- a/src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Processing.Processors.Dithering -{ - /// - /// Applies error diffusion based dithering using the Stucki image dithering algorithm. - /// - /// - public sealed class StuckiDiffuser : ErrorDiffuser - { - private const float Divisor = 42F; - - /// - /// The diffusion matrix - /// - private static readonly DenseMatrix StuckiMatrix = - new float[,] - { - { 0, 0, 0, 8 / Divisor, 4 / Divisor }, - { 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }, - { 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor } - }; - - /// - /// Initializes a new instance of the class. - /// - public StuckiDiffuser() - : base(StuckiMatrix) - { - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf b/src/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf new file mode 100644 index 000000000..42fb22c95 Binary files /dev/null and b/src/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf differ diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs new file mode 100644 index 000000000..a5e8d70b0 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -0,0 +1,106 @@ +// 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.Quantization +{ + /// + /// Gets the closest color to the supplied color based upon the Eucladean distance. + /// TODO: Expose this somehow. + /// + /// The pixel format. + internal readonly struct EuclideanPixelMap : IPixelMap, IEquatable> + where TPixel : struct, IPixel + { + private readonly ConcurrentDictionary vectorCache; + private readonly ConcurrentDictionary distanceCache; + + /// + /// Initializes a new instance of the struct. + /// + /// The color palette to map from. + public EuclideanPixelMap(ReadOnlyMemory palette) + { + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + + this.Palette = palette; + ReadOnlySpan paletteSpan = this.Palette.Span; + this.vectorCache = new ConcurrentDictionary(); + this.distanceCache = new ConcurrentDictionary(); + + for (int i = 0; i < paletteSpan.Length; i++) + { + this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); + } + } + + /// + public ReadOnlyMemory Palette { get; } + + /// + public override bool Equals(object obj) + => obj is EuclideanPixelMap map && this.Equals(map); + + /// + public bool Equals(EuclideanPixelMap other) + => this.Palette.Equals(other.Palette); + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public int 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 int index)) + { + match = paletteSpan[index]; + return index; + } + + return this.GetClosestColorSlow(color, paletteSpan, out match); + } + + /// + public override int GetHashCode() + => this.vectorCache.GetHashCode(); + + [MethodImpl(InliningOptions.ShortMethod)] + private int 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); + + // Less than... assign. + if (distance < leastDistance) + { + 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 + this.distanceCache[color] = index; + match = palette[index]; + return index; + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs new file mode 100644 index 000000000..5b49fe9e8 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// Contains extension methods for frame quantizers. + /// + public static class FrameQuantizerExtensions + { + /// + /// Quantizes an image frame and return the resulting output pixels. + /// + /// The type of frame quantizer. + /// The pixel format. + /// The frame + /// The source image frame to quantize. + /// The bounds within the frame to quantize. + /// + /// A representing a quantized version of the source frame pixels. + /// + public static QuantizedFrame QuantizeFrame( + ref TFrameQuantizer quantizer, + ImageFrame source, + Rectangle bounds) + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + Guard.NotNull(source, nameof(source)); + var interest = Rectangle.Intersect(source.Bounds(), bounds); + + // Collect the palette. Required before the second pass runs. + ReadOnlyMemory palette = quantizer.BuildPalette(source, interest); + MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator; + + var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); + Memory output = quantizedFrame.GetWritablePixelMemory(); + + if (quantizer.Options.Dither is null) + { + SecondPass(ref quantizer, source, interest, output, palette); + } + else + { + // We clone the image as we don't want to alter the original via error diffusion based dithering. + using (ImageFrame clone = source.Clone()) + { + SecondPass(ref quantizer, clone, interest, output, palette); + } + } + + return quantizedFrame; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static void SecondPass( + ref TFrameQuantizer quantizer, + ImageFrame source, + Rectangle bounds, + Memory output, + ReadOnlyMemory palette) + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + IDither dither = quantizer.Options.Dither; + + if (dither is null) + { + var operation = new RowIntervalOperation(quantizer, source, output, bounds, palette); + ParallelRowIterator.IterateRows( + quantizer.Configuration, + bounds, + in operation); + + return; + } + + dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); + } + + private readonly struct RowIntervalOperation : IRowIntervalOperation + where TFrameQuantizer : struct, IFrameQuantizer + where TPixel : struct, IPixel + { + private readonly TFrameQuantizer quantizer; + private readonly ImageFrame source; + private readonly Memory output; + private readonly Rectangle bounds; + private readonly ReadOnlyMemory palette; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + in TFrameQuantizer quantizer, + ImageFrame source, + Memory output, + Rectangle bounds, + ReadOnlyMemory palette) + { + this.quantizer = quantizer; + this.source = source; + this.output = output; + this.bounds = bounds; + this.palette = palette; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) + { + ReadOnlySpan paletteSpan = this.palette.Span; + Span outputSpan = this.output.Span; + int width = this.bounds.Width; + int offsetY = this.bounds.Top; + int offsetX = this.bounds.Left; + + for (int y = rows.Min; y < rows.Max; y++) + { + Span row = this.source.GetPixelRowSpan(y); + int rowStart = (y - offsetY) * width; + + // TODO: This can be a bulk operation. + for (int x = this.bounds.Left; x < this.bounds.Right; x++) + { + outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _); + } + } + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs deleted file mode 100644 index eb3838d21..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// 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.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// The base class for all implementations - /// - /// The pixel format. - 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 bool isDisposed; - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The quantizer - /// - /// If true, the quantization process only needs to loop through the source pixels once - /// - /// - /// If you construct this class with a true for , then the code will - /// only call the method. - /// If two passes are required, the code will also call . - /// - protected FrameQuantizer(Configuration configuration, IQuantizer quantizer, bool singlePass) - { - Guard.NotNull(quantizer, nameof(quantizer)); - - this.Configuration = configuration; - this.Diffuser = quantizer.Diffuser; - this.Dither = this.Diffuser != null; - this.singlePass = singlePass; - } - - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The diffuser - /// - /// If true, the quantization process only needs to loop through the source pixels once - /// - /// - /// If you construct this class with a true for , then the code will - /// only call the method. - /// If two passes are required, the code will also call . - /// - protected FrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, bool singlePass) - { - this.Configuration = configuration; - this.Diffuser = diffuser; - this.Dither = this.Diffuser != null; - this.singlePass = singlePass; - } - - /// - public IErrorDiffuser Diffuser { get; } - - /// - public bool Dither { get; } - - /// - /// Gets the configuration which allows altering default behaviour or extending the library. - /// - protected Configuration Configuration { get; } - - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - public IQuantizedFrame QuantizeFrame(ImageFrame image) - { - Guard.NotNull(image, nameof(image)); - - // Get the size of the source image - int height = image.Height; - int width = image.Width; - - // Call the FirstPass function if not a single pass algorithm. - // For something like an Octree quantizer, this will run through - // all image pixels, build a data structure, and create a palette. - if (!this.singlePass) - { - this.FirstPass(image, width, height); - } - - // 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); - - var quantizedFrame = new QuantizedFrame(memoryAllocator, width, height, palette); - - Span pixelSpan = quantizedFrame.GetWritablePixelSpan(); - if (this.Dither) - { - // We clone the image as we don't want to alter the original via dithering. - using (ImageFrame clone = image.Clone()) - { - this.SecondPass(clone, pixelSpan, palette.Span, width, height); - } - } - else - { - this.SecondPass(image, pixelSpan, palette.Span, width, height); - } - - return quantizedFrame; - } - - /// - /// Disposes the object and frees resources for the Garbage Collector. - /// - /// Whether to dispose managed and unmanaged objects. - protected virtual void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - if (disposing) - { - this.paletteVector?.Dispose(); - } - - this.paletteVector = null; - - this.isDisposed = true; - } - - /// - /// Execute the first pass through the pixels in the image to create the palette. - /// - /// The source data. - /// The width in pixels of the image. - /// The height in pixels of the image. - protected virtual void FirstPass(ImageFrame source, int width, int height) - { - } - - /// - /// Returns the closest color from the palette to the given color by calculating the - /// Euclidean distance in the Rgba colorspace. - /// - /// The 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); - } - - /// - /// Retrieve the palette for the quantized image. - /// - /// - /// - /// - 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. - /// - /// The source image. - /// The output pixel array. - /// The output color palette. - /// The width in pixels of the image. - /// The height in pixels of the image. - protected abstract void SecondPass( - ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height); - - [MethodImpl(MethodImplOptions.NoInlining)] - private byte GetClosestPixelSlow(ref TPixel pixel) - { - // 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); - - for (int index = 0; index < paletteVectorSpan.Length; index++) - { - ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, index); - float distance = Vector4.DistanceSquared(vector, candidate); - - // Greater... Move on. - if (!(distance < leastDistance)) - { - continue; - } - - colorIndex = index; - leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance < epsilon) - { - break; - } - } - - // Now I have the index, pop it into the cache for next time - byte result = (byte)colorIndex; - this.distanceCache.Add(pixel, result); - return result; - } - } -} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 54dabab0a..d3091c3b0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -1,9 +1,8 @@ -// 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.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { @@ -15,22 +14,45 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization where TPixel : struct, IPixel { /// - /// Gets a value indicating whether to apply dithering to the output image. + /// Gets the configuration. /// - bool Dither { get; } + Configuration Configuration { get; } /// - /// Gets the error diffusion algorithm to apply to the output image. + /// Gets the quantizer options defining quantization rules. /// - IErrorDiffuser Diffuser { get; } + QuantizerOptions Options { get; } /// - /// Quantize an image frame and return the resulting output pixels. + /// Quantizes an image frame and return the resulting output pixels. /// - /// The image to quantize. + /// The source image frame to quantize. + /// The bounds within the frame to quantize. /// - /// A representing a quantized version of the image pixels. + /// A representing a quantized version of the source frame pixels. /// - IQuantizedFrame QuantizeFrame(ImageFrame image); + QuantizedFrame QuantizeFrame( + ImageFrame source, + Rectangle bounds); + + /// + /// Builds the quantized palette from the given image frame and bounds. + /// + /// The source image frame. + /// The region of interest bounds. + /// The palette. + ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds); + + /// + /// Returns the index and color from the quantized palette corresponding to the give to the given color. + /// + /// The color to match. + /// The output color palette. + /// The matched color. + /// The index. + public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match); + + // TODO: Enable bulk operations. + // void GetQuantizedColors(ReadOnlySpan colors, ReadOnlySpan palette, Span indices, Span matches); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs new file mode 100644 index 000000000..d25f2b07d --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// Allows the mapping of input colors to colors within a given palette. + /// TODO: Expose this somehow. + /// + /// The pixel format. + internal interface IPixelMap + where TPixel : struct, IPixel + { + /// + /// Gets the color palette containing colors to match. + /// + ReadOnlyMemory Palette { get; } + + /// + /// Returns the closest color in the palette and the index of that pixel. + /// + /// The color to match. + /// The matched color. + /// The index. + int GetClosestColor(TPixel color, out TPixel match); + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/IQuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IQuantizedFrame{TPixel}.cs deleted file mode 100644 index 42016459b..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizedFrame{TPixel}.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Defines an abstraction to represent a quantized image frame where the pixels indexed by a color palette. - /// - /// The pixel format. - public interface IQuantizedFrame : IDisposable - where TPixel : struct, IPixel - { - /// - /// Gets the width of this . - /// - int Width { get; } - - /// - /// Gets the height of this . - /// - int Height { get; } - - /// - /// Gets the color palette of this . - /// - ReadOnlyMemory Palette { get; } - - /// - /// Gets the pixels of this . - /// - /// The The pixel span. - ReadOnlySpan GetPixelSpan(); - } -} \ 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..2daddf105 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IQuantizer.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 SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { @@ -12,27 +11,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization public interface IQuantizer { /// - /// Gets the error diffusion algorithm to apply to the output image. + /// Gets the quantizer options defining quantization rules. /// - IErrorDiffuser Diffuser { get; } + QuantizerOptions Options { get; } /// - /// Creates the generic frame quantizer + /// Creates the generic frame quantizer. /// /// The to configure internal operations. /// The pixel format. - /// The + /// The . IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel; /// - /// Creates the generic frame quantizer + /// Creates the generic frame quantizer. /// /// The pixel format. /// The to configure internal operations. - /// The maximum number of colors to hold in the color palette. - /// The - IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// The options to create the quantizer with. + /// The . + IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) 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..2b8ef3f0b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -2,11 +2,12 @@ // 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; namespace SixLabors.ImageSharp.Processing.Processors.Quantization @@ -16,168 +17,114 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - internal sealed class OctreeFrameQuantizer : FrameQuantizer + public struct OctreeFrameQuantizer : IFrameQuantizer where TPixel : struct, IPixel { - /// - /// Maximum allowed color depth - /// private readonly int colors; - - /// - /// Stores the tree - /// private readonly Octree octree; + private EuclideanPixelMap pixelMap; + private readonly bool isDithering; /// - /// The transparent index - /// - private byte transparentIndex; - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. - /// The octree quantizer - /// - /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, - /// the second pass quantizes a color based on the nodes in the tree - /// - public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer) - : this(configuration, quantizer, quantizer.MaxColors) + /// The quantizer options defining quantization rules. + [MethodImpl(InliningOptions.ShortMethod)] + public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions options) { - } + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The octree quantizer. - /// The maximum number of colors to hold in the color palette. - /// - /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, - /// the second pass quantizes a color based on the nodes in the tree - /// - public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer, int maxColors) - : base(configuration, quantizer, false) - { - this.colors = maxColors; + this.Configuration = configuration; + this.Options = options; + + this.colors = this.Options.MaxColors; this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); + this.pixelMap = default; + this.isDithering = !(this.Options.Dither is null); } /// - protected override void FirstPass(ImageFrame source, int width, int height) - { - // Loop through each row - for (int y = 0; y < height; y++) - { - Span row = source.GetPixelRowSpan(y); - ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row); + public Configuration Configuration { get; } - // And loop through each column - for (int x = 0; x < width; x++) - { - ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x); + /// + public QuantizerOptions Options { get; } - // Add the color to the Octree - this.octree.AddColor(ref pixel); - } - } - } + /// + [MethodImpl(InliningOptions.ShortMethod)] + public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); /// - protected override void SecondPass( - ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height) + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) { - // 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++) + using IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(bounds.Width); + Span bufferSpan = buffer.GetSpan(); + + // Loop through each row + for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y); + Span row = source.GetPixelRowSpan(y).Slice(bounds.Left, bounds.Width); + PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); - // And loop through each column - for (int x = 0; x < width; x++) + for (int x = 0; x < bufferSpan.Length; x++) { - // Get the pixel. - sourcePixel = row[x]; + Rgba32 rgba = bufferSpan[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; + // Add the color to the Octree + this.octree.AddColor(rgba); } } - } - internal ReadOnlyMemory AotGetPalette() => this.GetPalette(); + TPixel[] palette = this.octree.Palletize(this.colors); + this.pixelMap = new EuclideanPixelMap(palette); - /// - protected override ReadOnlyMemory GetPalette() => this.octree.Palletize(this.colors); + return palette; + } - /// - /// Process the pixel in the second pass of the algorithm. - /// - /// The pixel to quantize. - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte QuantizePixel(ref TPixel pixel) + /// + [MethodImpl(InliningOptions.ShortMethod)] + public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { - if (this.Dither) + // Octree only maps the RGB component of a color + // so cannot tell the difference between a fully transparent + // pixel and a black one. + if (!this.isDithering && !color.Equals(default)) { - // The colors have changed so we need to use Euclidean distance calculation to - // find the closest value. - return this.GetClosestPixel(ref pixel); + var index = (byte)this.octree.GetPaletteIndex(color); + match = palette[index]; + return index; } - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); - if (rgba.Equals(default)) - { - return this.transparentIndex; - } + return (byte)this.pixelMap.GetClosestColor(color, out match); + } - return (byte)this.octree.GetPaletteIndex(ref pixel); + /// + public void Dispose() + { } /// - /// Class which does the actual quantization + /// Class which does the actual quantization. /// - private class Octree + private sealed class Octree { /// - /// Mask used when getting the appropriate pixels for a given node + /// Mask used when getting the appropriate pixels for a given node. /// - // ReSharper disable once StaticMemberInGenericType - private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + private static readonly byte[] Mask = new byte[] + { + 0b10000000, + 0b1000000, + 0b100000, + 0b10000, + 0b1000, + 0b100, + 0b10, + 0b1 + }; /// /// The root of the Octree @@ -197,7 +144,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Cache the previous color quantized /// - private TPixel previousColor; + private Rgba32 previousColor; /// /// Initializes a new instance of the class. @@ -220,10 +167,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,36 +179,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private OctreeNode[] ReducibleNodes { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get; } /// /// Add a given color value to the Octree /// - /// The pixel data. - public void AddColor(ref TPixel pixel) + /// The color to add. + public void AddColor(Rgba32 color) { // Check if this request is for the same color as the last - if (this.previousColor.Equals(pixel)) + if (this.previousColor.Equals(color)) { - // If so, check if I have a previous node setup. This will only occur if the first color in the image + // If so, check if I have a previous node setup. + // This will only occur if the first color in the image // happens to be black, with an alpha component of zero. if (this.previousNode is null) { - this.previousColor = pixel; - this.root.AddColor(ref pixel, this.maxColorBits, 0, this); + this.previousColor = color; + this.root.AddColor(ref color, this.maxColorBits, 0, this); } else { // Just update the previous node - this.previousNode.Increment(ref pixel); + this.previousNode.Increment(ref color); } } else { - this.previousColor = pixel; - this.root.AddColor(ref pixel, this.maxColorBits, 0, this); + this.previousColor = color; + this.root.AddColor(ref color, this.maxColorBits, 0, this); } } @@ -272,7 +220,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) @@ -293,12 +241,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Get the palette index for the passed color /// - /// The pixel data. + /// The color to match. /// - /// The . + /// The index. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetPaletteIndex(ref TPixel pixel) => this.root.GetPaletteIndex(ref pixel, 0); + [MethodImpl(InliningOptions.ShortMethod)] + public int GetPaletteIndex(TPixel color) + { + Rgba32 rgba = default; + color.ToRgba32(ref rgba); + return this.root.GetPaletteIndex(ref rgba, 0); + } /// /// Keep track of the previous node that was quantized @@ -306,8 +259,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 +289,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 @@ -376,15 +329,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// - /// The level in the tree = 0 - 7 - /// - /// - /// The number of significant color bits in the image - /// - /// - /// The tree to which this node belongs - /// + /// The level in the tree = 0 - 7. + /// The number of significant color bits in the image. + /// The tree to which this node belongs. public OctreeNode(int level, int colorBits, Octree octree) { // Construct the new node @@ -414,23 +361,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// public OctreeNode NextReducible { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get; } /// /// Add a color into the tree /// - /// The pixel color - /// The number of significant color bits - /// The level in the tree - /// The tree to which this node belongs - public void AddColor(ref TPixel pixel, int colorBits, int level, Octree octree) + /// The color to add. + /// The number of significant color bits. + /// The level in the tree. + /// The tree to which this node belongs. + public void AddColor(ref Rgba32 color, int colorBits, int level, Octree octree) { // Update the color information if this is a leaf if (this.leaf) { - this.Increment(ref pixel); + this.Increment(ref color); // Setup the previous node octree.TrackPrevious(this); @@ -438,13 +385,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization else { // Go to the next level down in the tree - int shift = 7 - level; - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); - - int index = ((rgba.B & Mask[level]) >> (shift - 2)) - | ((rgba.G & Mask[level]) >> (shift - 1)) - | ((rgba.R & Mask[level]) >> shift); + int index = GetColorIndex(ref color, level); OctreeNode child = this.children[index]; if (child is null) @@ -455,7 +396,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } // Add the color to the child node - child.AddColor(ref pixel, colorBits, level + 1, octree); + child.AddColor(ref color, colorBits, level + 1, octree); } } @@ -495,7 +436,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The palette /// The current palette index - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(InliningOptions.ColdPath)] public void ConstructPalette(TPixel[] palette, ref int index) { if (this.leaf) @@ -527,29 +468,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The representing the index of the pixel in the palette. /// - [MethodImpl(MethodImplOptions.NoInlining)] - public int GetPaletteIndex(ref TPixel pixel, int level) + [MethodImpl(InliningOptions.ColdPath)] + public int GetPaletteIndex(ref Rgba32 pixel, int level) { - int index = this.paletteIndex; - - if (!this.leaf) + if (this.leaf) { - int shift = 7 - level; - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); + return this.paletteIndex; + } - int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2)) - | ((rgba.G & Mask[level]) >> (shift - 1)) - | ((rgba.R & Mask[level]) >> shift); + int colorIndex = GetColorIndex(ref pixel, level); + OctreeNode child = this.children[colorIndex]; - OctreeNode child = this.children[pixelIndex]; - if (child != null) - { - index = child.GetPaletteIndex(ref pixel, level + 1); - } - else + int index = 0; + if (child != null) + { + index = child.GetPaletteIndex(ref pixel, level + 1); + } + else + { + // Check other children. + for (int i = 0; i < this.children.Length; i++) { - throw new Exception($"Cannot retrieve a pixel at the given index {pixelIndex}."); + child = this.children[i]; + if (child != null) + { + var childIndex = child.GetPaletteIndex(ref pixel, level + 1); + if (childIndex != 0) + { + return childIndex; + } + } } } @@ -557,18 +505,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - /// Increment the pixel count and add to the color information + /// Gets the color index at the given level. + /// + /// The color. + /// The node level. + /// The index. + [MethodImpl(InliningOptions.ShortMethod)] + private static int GetColorIndex(ref Rgba32 color, int level) + { + int shift = 7 - level; + byte mask = Mask[level]; + return ((color.R & mask) >> shift) + | ((color.G & mask) >> (shift - 1)) + | ((color.B & mask) >> (shift - 2)); + } + + /// + /// Increment the color count and add to the color information /// - /// The pixel to add. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Increment(ref TPixel pixel) + /// The pixel to add. + [MethodImpl(InliningOptions.ShortMethod)] + public void Increment(ref Rgba32 color) { - Rgba32 rgba = default; - pixel.ToRgba32(ref rgba); this.pixelCount++; - this.red += rgba.R; - this.green += rgba.G; - this.blue += rgba.B; + this.red += color.R; + this.green += color.G; + this.blue += color.B; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index aaf2c42cb..a5660c43b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -2,97 +2,45 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; 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 - /// /// public class OctreeQuantizer : IQuantizer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// using the default . /// public OctreeQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of colors to hold in the color palette. - public OctreeQuantizer(int maxColors) - : this(GetDiffuser(true), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image. - public OctreeQuantizer(bool dither) - : this(GetDiffuser(dither), QuantizerConstants.MaxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of colors to hold in the color palette. - /// Whether to apply dithering to the output image. - public OctreeQuantizer(bool dither, int maxColors) - : this(GetDiffuser(dither), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error diffusion algorithm, if any, to apply to the output image. - public OctreeQuantizer(IErrorDiffuser diffuser) - : this(diffuser, QuantizerConstants.MaxColors) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The error diffusion 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) + /// The quantizer options defining quantization rules. + public OctreeQuantizer(QuantizerOptions options) { - this.Diffuser = diffuser; - this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); + Guard.NotNull(options, nameof(options)); + this.Options = options; } /// - public IErrorDiffuser Diffuser { get; } - - /// - /// Gets the maximum number of colors to hold in the color palette. - /// - public int MaxColors { get; } + public QuantizerOptions Options { get; } - /// /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - => new OctreeFrameQuantizer(configuration, this); + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel - { - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - return new OctreeFrameQuantizer(configuration, this, maxColors); - } - - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + => new OctreeFrameQuantizer(configuration, options); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 825eb6bee..b8925b664 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -3,10 +3,7 @@ using System; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; namespace SixLabors.ImageSharp.Processing.Processors.Quantization { @@ -15,88 +12,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - internal sealed class PaletteFrameQuantizer : FrameQuantizer + internal struct PaletteFrameQuantizer : IFrameQuantizer where TPixel : struct, IPixel { - /// - /// The reduced image palette. - /// private readonly ReadOnlyMemory palette; + private readonly EuclideanPixelMap pixelMap; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// 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) - : base(configuration, diffuser, true) => this.palette = colors; - - /// - protected override void SecondPass( - ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height) + /// The quantizer options defining quantization rules. + /// A containing all colors in the palette. + [MethodImpl(InliningOptions.ShortMethod)] + public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory colors) { - // 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); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); - for (int y = 0; y < height; y++) - { - ref TPixel rowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); + this.Configuration = configuration; + this.Options = options; - // And loop through each column - for (int x = 0; x < width; x++) - { - // Get the pixel. - sourcePixel = Unsafe.Add(ref rowRef, 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); + this.palette = colors; + this.pixelMap = new EuclideanPixelMap(colors); + } - // And setup the previous pointer - previousPixel = sourcePixel; + /// + public Configuration Configuration { get; } - if (this.Dither) - { - transformedPixel = Unsafe.Add(ref paletteRef, pixelValue); - } - } + /// + public QuantizerOptions Options { get; } - 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); - } + /// + [MethodImpl(InliningOptions.ShortMethod)] + public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); - output[(y * source.Width) + x] = pixelValue; - } - } - } + /// + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) + => this.palette; /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override ReadOnlyMemory GetPalette() => this.palette; + [MethodImpl(InliningOptions.ShortMethod)] + public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) + => (byte)this.pixelMap.GetClosestColor(color, out match); - /// - /// 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); + /// + public void Dispose() + { + } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index a493e6f88..c1198c58f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -2,80 +2,62 @@ // 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.Quantization { /// /// Allows the quantization of images pixels using color palettes. - /// Override this class to provide your own palette. - /// - /// By default the quantizer uses dithering. - /// /// public class PaletteQuantizer : IQuantizer { /// /// Initializes a new instance of the class. /// - /// The palette. + /// The color palette. public PaletteQuantizer(ReadOnlyMemory palette) - : this(palette, true) + : this(palette, new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The palette. - /// Whether to apply dithering to the output image - public PaletteQuantizer(ReadOnlyMemory palette, bool dither) - : this(palette, GetDiffuser(dither)) + /// The color palette. + /// The quantizer options defining quantization rules. + public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) { - } + Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); + Guard.NotNull(options, nameof(options)); - /// - /// 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) - { this.Palette = palette; - this.Diffuser = diffuser; + this.Options = options; } - /// - public IErrorDiffuser Diffuser { get; } - /// - /// Gets the palette. + /// Gets the color palette. /// public ReadOnlyMemory Palette { get; } + /// + public QuantizerOptions Options { get; } + /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - { - var palette = new TPixel[this.Palette.Length]; - Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Diffuser, palette); - } + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel { - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - int max = Math.Min(maxColors, this.Palette.Length); + Guard.NotNull(options, nameof(options)); - var palette = new TPixel[max]; - Color.ToPixel(configuration, this.Palette.Span.Slice(0, max), palette.AsSpan()); - return new PaletteFrameQuantizer(configuration, this.Diffuser, palette); - } + int length = Math.Min(this.Palette.Span.Length, options.MaxColors); + var palette = new TPixel[length]; - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); + return new PaletteFrameQuantizer(configuration, options, palette); + } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs index 8e1dffeed..93211855d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs @@ -15,9 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The quantizer used to reduce the color palette. public QuantizeProcessor(IQuantizer quantizer) - { - this.Quantizer = quantizer; - } + => this.Quantizer = quantizer; /// /// Gets the quantizer. diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 276919d60..bfcc26ae2 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -35,14 +35,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// protected override void OnFrameApply(ImageFrame source) { + var interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle); + Configuration configuration = this.Configuration; using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(configuration); - using IQuantizedFrame quantized = frameQuantizer.QuantizeFrame(source); + using QuantizedFrame quantized = frameQuantizer.QuantizeFrame(source, interest); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); ParallelRowIterator.IterateRows( configuration, - this.SourceRectangle, + interest, in operation); } @@ -50,19 +52,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { private readonly Rectangle bounds; private readonly ImageFrame source; - private readonly IQuantizedFrame quantized; - private readonly int maxPaletteIndex; + private readonly QuantizedFrame quantized; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( Rectangle bounds, ImageFrame source, - IQuantizedFrame quantized) + QuantizedFrame quantized) { this.bounds = bounds; this.source = source; this.quantized = quantized; - this.maxPaletteIndex = quantized.Palette.Length - 1; } [MethodImpl(InliningOptions.ShortMethod)] @@ -70,16 +70,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization { ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); ReadOnlySpan paletteSpan = this.quantized.Palette.Span; + int offsetY = this.bounds.Top; + int offsetX = this.bounds.Left; + int width = this.bounds.Width; for (int y = rows.Min; y < rows.Max; y++) { Span row = this.source.GetPixelRowSpan(y); - int yy = y * this.bounds.Width; + int rowStart = (y - offsetY) * width; - for (int x = this.bounds.X; x < this.bounds.Right; x++) + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - int i = x + yy; - row[x] = paletteSpan[Math.Min(this.maxPaletteIndex, quantizedPixelSpan[i])]; + int i = rowStart + x - offsetX; + row[x] = paletteSpan[quantizedPixelSpan[i]]; } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrameExtensions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrameExtensions.cs deleted file mode 100644 index fa3d36e10..000000000 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrameExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Runtime.CompilerServices; - -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Processing.Processors.Quantization -{ - /// - /// Contains extension methods for . - /// - public static class QuantizedFrameExtensions - { - /// - /// Gets the representation of the pixels as a of contiguous memory - /// at row beginning from the the first pixel on that row. - /// - /// The . - /// The row. - /// The pixel type. - /// The pixel row as a . - [MethodImpl(InliningOptions.ShortMethod)] - public static ReadOnlySpan GetRowSpan(this IQuantizedFrame frame, int rowIndex) - where TPixel : struct, IPixel - => frame.GetPixelSpan().Slice(rowIndex * frame.Width, frame.Width); - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 4938f0e12..fccc799bb 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{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; @@ -13,10 +13,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Represents a quantized image frame where the pixels indexed by a color palette. /// /// The pixel format. - public class QuantizedFrame : IQuantizedFrame + public sealed class QuantizedFrame : IDisposable where TPixel : struct, IPixel { private IMemoryOwner pixels; + private bool isDisposed; /// /// Initializes a new instance of the class. @@ -58,17 +59,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization [MethodImpl(InliningOptions.ShortMethod)] public ReadOnlySpan GetPixelSpan() => this.pixels.GetSpan(); + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// The row. + /// The pixel row as a . + [MethodImpl(InliningOptions.ShortMethod)] + public ReadOnlySpan GetRowSpan(int rowIndex) + => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); + /// public void Dispose() { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; this.pixels?.Dispose(); this.pixels = null; this.Palette = null; } /// - /// Get the non-readonly span of pixel data so can fill it. + /// Get the non-readonly memory of pixel data so can fill it. /// - internal Span GetWritablePixelSpan() => this.pixels.GetSpan(); + internal Memory GetWritablePixelMemory() => this.pixels.Memory; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs index d79a91c30..ece3777e0 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs @@ -1,12 +1,14 @@ -// 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; + namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// /// Contains color quantization specific constants. /// - internal static class QuantizerConstants + public static class QuantizerConstants { /// /// The minimum number of colors to use when quantizing an image. @@ -17,5 +19,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// The maximum number of colors to use when quantizing an image. /// public const int MaxColors = 256; + + /// + /// The minumim dithering scale used to adjust the amount of dither. + /// + public const float MinDitherScale = 0; + + /// + /// The max dithering scale used to adjust the amount of dither. + /// + public const float MaxDitherScale = 1F; + + /// + /// Gets the default dithering algorithm to use. + /// + public static IDither DefaultDither { get; } = KnownDitherings.FloydSteinberg; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs new file mode 100644 index 000000000..5c1daf183 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Dithering; + +namespace SixLabors.ImageSharp.Processing.Processors.Quantization +{ + /// + /// Defines options for quantization. + /// + public class QuantizerOptions + { + private float ditherScale = QuantizerConstants.MaxDitherScale; + private int maxColors = QuantizerConstants.MaxColors; + + /// + /// Gets or sets the algorithm to apply to the output image. + /// Defaults to ; set to for no dithering. + /// + public IDither Dither { get; set; } = QuantizerConstants.DefaultDither; + + /// + /// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1. + /// Defaults to . + /// + public float DitherScale + { + get { return this.ditherScale; } + set { this.ditherScale = value.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } + } + + /// + /// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256. + /// Defaults to . + /// + public int MaxColors + { + get { return this.maxColors; } + set { this.maxColors = value.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs index c912572f0..8aa634b9f 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.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; @@ -14,26 +14,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// public WebSafePaletteQuantizer() - : this(true) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// Whether to apply dithering to the output image - public WebSafePaletteQuantizer(bool dither) - : base(Color.WebSafePalette, dither) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error diffusion algorithm, if any, to apply to the output image - public WebSafePaletteQuantizer(IErrorDiffuser diffuser) - : base(Color.WebSafePalette, diffuser) + /// The quantizer options defining quantization rules. + public WebSafePaletteQuantizer(QuantizerOptions options) + : base(Color.WebSafePalette, options) { } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs index cd320a9a3..168c837d5 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs @@ -1,8 +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.Dithering; - namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -15,26 +13,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// public WernerPaletteQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image - public WernerPaletteQuantizer(bool dither) - : base(Color.WernerPalette, dither) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The error diffusion algorithm, if any, to apply to the output image - public WernerPaletteQuantizer(IErrorDiffuser diffuser) - : base(Color.WernerPalette, diffuser) + /// The quantizer options defining quantization rules. + public WernerPaletteQuantizer(QuantizerOptions options) + : base(Color.WernerPalette, options) { } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 2de02ebb3..396f120aa 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -5,13 +5,10 @@ using System; using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; 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 { /// @@ -34,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// The pixel format. - internal sealed class WuFrameQuantizer : FrameQuantizer + internal struct WuFrameQuantizer : IFrameQuantizer where TPixel : struct, IPixel { private readonly MemoryAllocator memoryAllocator; @@ -69,34 +66,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. @@ -108,178 +80,113 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private int colors; - /// - /// The reduced image palette - /// - private TPixel[] palette; - /// /// The color cube representing the image palette /// - private Box[] colorCube; + private readonly Box[] colorCube; + + private EuclideanPixelMap pixelMap; + + private readonly bool isDithering; private bool isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The configuration which allows altering default behaviour or extending the library. - /// The Wu quantizer - /// - /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram, - /// the second pass quantizes a color based on the position in the histogram. - /// - public WuFrameQuantizer(Configuration configuration, WuQuantizer quantizer) - : this(configuration, quantizer, quantizer.MaxColors) + /// The quantizer options defining quantization rules. + [MethodImpl(InliningOptions.ShortMethod)] + public WuFrameQuantizer(Configuration configuration, QuantizerOptions options) { - } + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(options, nameof(options)); - /// - /// Initializes a new instance of the class. - /// - /// The configuration which allows altering default behaviour or extending the library. - /// The Wu quantizer. - /// The maximum number of colors to hold in the color palette. - /// - /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram, - /// the second pass quantizes a color based on the position in the histogram. - /// - public WuFrameQuantizer(Configuration configuration, WuQuantizer quantizer, int maxColors) - : base(configuration, quantizer, false) - { + this.Configuration = configuration; + this.Options = options; 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; + this.colors = this.Options.MaxColors; + this.colorCube = new Box[this.colors]; + this.isDisposed = false; + this.pixelMap = default; + this.isDithering = this.isDithering = !(this.Options.Dither is null); } /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - if (disposing) - { - this.vwt?.Dispose(); - this.vmr?.Dispose(); - this.vmg?.Dispose(); - this.vmb?.Dispose(); - this.vma?.Dispose(); - this.m2?.Dispose(); - this.tag?.Dispose(); - } - - this.vwt = null; - this.vmr = null; - this.vmg = null; - this.vmb = null; - this.vma = null; - this.m2 = null; - this.tag = null; + public Configuration Configuration { get; } - this.isDisposed = true; - base.Dispose(true); - } + /// + public QuantizerOptions Options { get; } - internal ReadOnlyMemory AotGetPalette() => this.GetPalette(); + /// + [MethodImpl(InliningOptions.ShortMethod)] + public QuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds) + => FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); /// - protected override ReadOnlyMemory GetPalette() + public ReadOnlyMemory BuildPalette(ImageFrame source, Rectangle bounds) { - 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(); - - for (int k = 0; k < this.colors; k++) - { - this.Mark(ref this.colorCube[k], (byte)k); + this.Build3DHistogram(source, bounds); + this.Get3DMoments(this.memoryAllocator); + this.BuildCube(); - float weight = Volume(ref this.colorCube[k], vwtSpan); + var palette = new TPixel[this.colors]; + ReadOnlySpan momentsSpan = this.moments.GetSpan(); - if (MathF.Abs(weight) > Constants.Epsilon) - { - 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); + for (int k = 0; k < this.colors; k++) + { + this.Mark(ref this.colorCube[k], (byte)k); - ref TPixel color = ref this.palette[k]; - color.FromScaledVector4(new Vector4(r, g, b, a) / weight / 255F); - } + Moment moment = Volume(ref this.colorCube[k], momentsSpan); + + if (moment.Weight > 0) + { + ref TPixel color = ref palette[k]; + color.FromScaledVector4(moment.Normalize()); } } - return this.palette; + this.pixelMap = new EuclideanPixelMap(palette); + return palette; } /// - protected override void FirstPass(ImageFrame source, int width, int height) + public byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { - this.Build3DHistogram(source, width, height); - this.Get3DMoments(this.memoryAllocator); - this.BuildCube(); + if (!this.isDithering) + { + 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(); + byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; + match = palette[index]; + return index; + } + + return (byte)this.pixelMap.GetClosestColor(color, out match); } /// - protected override void SecondPass(ImageFrame source, Span output, ReadOnlySpan palette, int width, int height) + public void Dispose() { - // 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.isDisposed) { - 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; - } + return; } + + this.isDisposed = true; + this.moments?.Dispose(); + this.tag?.Dispose(); + this.moments = null; + this.tag = null; } /// @@ -290,7 +197,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 +214,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 +241,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 +302,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)); @@ -454,49 +361,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Builds a 3-D color histogram of counts, r/g/b, c^2. /// /// The source data. - /// The width in pixels of the image. - /// The height in pixels of the image. - private void Build3DHistogram(ImageFrame source, int width, int height) + /// The bounds within the source image to quantize. + private void Build3DHistogram(ImageFrame source, Rectangle bounds) { - 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); + using IMemoryOwner buffer = this.memoryAllocator.Allocate(bounds.Width); + Span bufferSpan = buffer.GetSpan(); - // And loop through each column - for (int x = 0; x < width; x++) - { - ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, 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); + for (int y = bounds.Top; y < bounds.Bottom; y++) + { + Span row = source.GetPixelRowSpan(y).Slice(bounds.Left, bounds.Width); + PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); - int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); + for (int x = 0; x < bufferSpan.Length; x++) + { + Rgba32 rgba = bufferSpan[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); - } + momentSpan[GetPaletteIndex(r, g, b, a)] += rgba; } } } @@ -507,103 +395,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 +440,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 +477,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 +527,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 +568,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 +576,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 +584,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 +592,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; @@ -841,7 +634,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private void BuildCube() { - this.colorCube = new Box[this.colors]; Span vv = stackalloc double[this.colors]; ref Box cube = ref this.colorCube[0]; @@ -857,8 +649,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 +678,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 +817,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 +834,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..b8c54f467 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs @@ -2,89 +2,44 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing.Processors.Dithering; 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 - /// /// public class WuQuantizer : IQuantizer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class + /// using the default . /// public WuQuantizer() - : this(true) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of colors to hold in the color palette - public WuQuantizer(int maxColors) - : this(GetDiffuser(true), maxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Whether to apply dithering to the output image - public WuQuantizer(bool dither) - : this(GetDiffuser(dither), QuantizerConstants.MaxColors) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error diffusion algorithm, if any, to apply to the output image - public WuQuantizer(IErrorDiffuser diffuser) - : this(diffuser, QuantizerConstants.MaxColors) + : this(new QuantizerOptions()) { } /// /// Initializes a new instance of the class. /// - /// The error diffusion 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) + /// The quantizer options defining quantization rules. + public WuQuantizer(QuantizerOptions options) { - this.Diffuser = diffuser; - this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); + Guard.NotNull(options, nameof(options)); + this.Options = options; } /// - public IErrorDiffuser Diffuser { get; } - - /// - /// Gets the maximum number of colors to hold in the color palette. - /// - public int MaxColors { get; } + public QuantizerOptions Options { get; } /// public IFrameQuantizer CreateFrameQuantizer(Configuration configuration) where TPixel : struct, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - return new WuFrameQuantizer(configuration, this); - } + => this.CreateFrameQuantizer(configuration, this.Options); - /// - public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, int maxColors) + /// + public IFrameQuantizer CreateFrameQuantizer(Configuration configuration, QuantizerOptions options) where TPixel : struct, IPixel - { - Guard.NotNull(configuration, nameof(configuration)); - maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); - return new WuFrameQuantizer(configuration, this, maxColors); - } - - private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null; + => new WuFrameQuantizer(configuration, options); } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs index 89eb63d62..5e91f98eb 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGif.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.Drawing.Imaging; @@ -6,6 +6,7 @@ using System.IO; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests; using SDImage = System.Drawing.Image; @@ -53,11 +54,15 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public void GifCore() { // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + using (var memoryStream = new MemoryStream()) { this.bmpCore.SaveAsGif(memoryStream, options); } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs index 4d93d89af..5c7a9e991 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Drawing.Imaging; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Benchmarks.Codecs @@ -23,7 +24,11 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs this.ForEachImageSharpImage((img, ms) => { // Try to get as close to System.Drawing's output as possible - var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new GifEncoder + { + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 }) + }; + img.Save(ms, options); return null; }); diff --git a/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs b/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs index 639d1594e..aedf9cd77 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.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.IO; @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new OctreeQuantizer(false) }; + var options = new PngEncoder { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(false) }; + var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } @@ -95,9 +95,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs { using (var memoryStream = new MemoryStream()) { - var options = new PngEncoder { Quantizer = new WuQuantizer(false) }; + var options = new PngEncoder { Quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }) }; this.bmpCore.SaveAsPng(memoryStream, options); } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs b/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs index 1676197d4..096167eb9 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs @@ -15,7 +15,18 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers { using (var image = new Image(Configuration.Default, 800, 800, Color.BlanchedAlmond)) { - image.Mutate(x => x.Diffuse()); + image.Mutate(x => x.Dither(KnownDitherings.FloydSteinberg)); + + return image.Size(); + } + } + + [Benchmark] + public Size DoDither() + { + using (var image = new Image(Configuration.Default, 800, 800, Color.BlanchedAlmond)) + { + image.Mutate(x => x.Dither()); return image.Size(); } @@ -48,3 +59,25 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers // |---------- |----- |-------- |----------:|----------:|----------:|------:|------:|------:|----------:| // | DoDiffuse | Clr | Clr | 124.93 ms | 33.297 ms | 1.8251 ms | - | - | - | 2 KB | // | DoDiffuse | Core | Core | 89.63 ms | 9.895 ms | 0.5424 ms | - | - | - | 1.91 KB | + +// #### 20th February 2020 #### +// +// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363 +// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// .NET Core SDK = 3.1.101 +// +// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT +// Job-OJKYBT : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT +// Job-RZWLFP : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT +// Job-NUYUQV : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT +// +// IterationCount=3 LaunchCount=1 WarmupCount=3 +// +// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +// |---------- |-------------- |----------:|----------:|----------:|------:|------:|------:|----------:| +// | DoDiffuse | .NET 4.7.2 | 30.535 ms | 19.217 ms | 1.0534 ms | - | - | - | 26.25 KB | +// | DoDither | .NET 4.7.2 | 14.174 ms | 1.625 ms | 0.0891 ms | - | - | - | 31.38 KB | +// | DoDiffuse | .NET Core 2.1 | 15.984 ms | 3.686 ms | 0.2020 ms | - | - | - | 25.98 KB | +// | DoDither | .NET Core 2.1 | 8.646 ms | 1.635 ms | 0.0896 ms | - | - | - | 28.99 KB | +// | DoDiffuse | .NET Core 3.1 | 16.235 ms | 9.612 ms | 0.5269 ms | - | - | - | 25.96 KB | +// | DoDither | .NET Core 3.1 | 8.429 ms | 1.270 ms | 0.0696 ms | - | - | - | 31.61 KB | diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 4fd1d6490..85d5ca1b8 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, - Quantizer = new WuQuantizer(256) + Quantizer = new WuQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); @@ -223,7 +223,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel8, - Quantizer = new OctreeQuantizer(256) + Quantizer = new OctreeQuantizer() }; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false); IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile); diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 95389511b..ff91c0e82 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Linq; using System.Reflection; @@ -90,7 +91,7 @@ namespace SixLabors.ImageSharp.Tests private static IQuantizer GetQuantizer(string name) { PropertyInfo property = typeof(KnownQuantizers).GetTypeInfo().GetProperty(name); - return (IQuantizer)property.GetMethod.Invoke(null, new object[0]); + return (IQuantizer)property.GetMethod.Invoke(null, Array.Empty()); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 1fc99922d..1519bc801 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { // Use the palette quantizer without dithering to ensure results // are consistent - Quantizer = new WebSafePaletteQuantizer(false) + Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif var encoder = new GifEncoder { ColorTableMode = GifColorTableMode.Global, - Quantizer = new OctreeQuantizer(false) + Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) }; // Always save as we need to compare the encoded output. @@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif var encoder = new GifEncoder { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(frameMetadata.ColorTableLength) + Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) }; image.Save(outStream, encoder); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 20a2d0233..390613256 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -448,7 +448,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FilterMethod = pngFilterMethod, CompressionLevel = compressionLevel, BitDepth = bitDepth, - Quantizer = new WuQuantizer(paletteSize), + Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }), InterlaceMethod = interlaceMode }; diff --git a/tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs b/tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs index 3e37cb30b..d515b21a9 100644 --- a/tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs +++ b/tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs @@ -116,5 +116,25 @@ namespace SixLabors.ImageSharp.Tests.Primitives Assert.Equal(2, transposed[1, 0]); Assert.Equal(3, transposed[2, 0]); } + + [Fact] + public void DenseMatrixEquality() + { + var dense = new DenseMatrix(3, 1); + var dense2 = new DenseMatrix(3, 1); + var dense3 = new DenseMatrix(1, 3); + + Assert.True(dense == dense2); + Assert.False(dense != dense2); + Assert.Equal(dense, dense2); + Assert.Equal(dense, (object)dense2); + Assert.Equal(dense.GetHashCode(), dense2.GetHashCode()); + + Assert.False(dense == dense3); + Assert.True(dense != dense3); + Assert.NotEqual(dense, dense3); + Assert.NotEqual(dense, (object)dense3); + Assert.NotEqual(dense.GetHashCode(), dense3.GetHashCode()); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs b/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs deleted file mode 100644 index f5a26dc17..000000000 --- a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Binarization; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Processing.Binarization -{ - public class BinaryDitherTest : BaseImageOperationsExtensionTest - { - private readonly IOrderedDither orderedDither; - private readonly IErrorDiffuser errorDiffuser; - - public BinaryDitherTest() - { - this.orderedDither = KnownDitherers.BayerDither4x4; - this.errorDiffuser = KnownDiffusers.FloydSteinberg; - } - - [Fact] - public void BinaryDither_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither); - BinaryOrderedDitherProcessor p = this.Verify(); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_rect_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, this.rect); - BinaryOrderedDitherProcessor p = this.Verify(this.rect); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_index_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink); - BinaryOrderedDitherProcessor p = this.Verify(); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.Yellow, p.UpperColor); - Assert.Equal(Color.HotPink, p.LowerColor); - } - - [Fact] - public void BinaryDither_index_rect_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink, this.rect); - BinaryOrderedDitherProcessor p = this.Verify(this.rect); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.HotPink, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_CorrectProcessor() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .4F); - BinaryErrorDiffusionProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.4F, p.Threshold); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_rect_CorrectProcessor() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .3F, this.rect); - BinaryErrorDiffusionProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.3F, p.Threshold); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_CorrectProcessorWithColors() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow); - BinaryErrorDiffusionProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); - Assert.Equal(Color.HotPink, p.UpperColor); - Assert.Equal(Color.Yellow, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_rect_CorrectProcessorWithColors() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow, this.rect); - BinaryErrorDiffusionProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); - Assert.Equal(Color.HotPink, p.UpperColor); - Assert.Equal(Color.Yellow, p.LowerColor); - } - } -} diff --git a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs index bb84bd4b1..0cc8db651 100644 --- a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs +++ b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs @@ -2,10 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Dithering; - using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Binarization @@ -20,8 +18,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, @@ -31,15 +29,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public DitherTest() { - this.orderedDither = KnownDitherers.BayerDither4x4; - this.errorDiffuser = KnownDiffusers.FloydSteinberg; + this.orderedDither = KnownDitherings.Bayer4x4; + this.errorDiffuser = KnownDitherings.FloydSteinberg; } [Fact] public void Dither_CorrectProcessor() { this.operations.Dither(this.orderedDither); - OrderedDitherPaletteProcessor p = this.Verify(); + PaletteDitherProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } @@ -48,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_rect_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.rect); - OrderedDitherPaletteProcessor p = this.Verify(this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } @@ -57,7 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_index_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.testPalette); - OrderedDitherPaletteProcessor p = this.Verify(); + PaletteDitherProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.testPalette, p.Palette); } @@ -66,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_index_rect_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.testPalette, this.rect); - OrderedDitherPaletteProcessor p = this.Verify(this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.testPalette, p.Palette); } @@ -74,41 +72,103 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void Dither_ErrorDiffuser_CorrectProcessor() { - this.operations.Diffuse(this.errorDiffuser, .4F); - ErrorDiffusionPaletteProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.4F, p.Threshold); + this.operations.Dither(this.errorDiffuser); + PaletteDitherProcessor p = this.Verify(); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_rect_CorrectProcessor() { - this.operations.Diffuse(this.errorDiffuser, .3F, this.rect); - ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.3F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_CorrectProcessorWithColors() { - this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette); - ErrorDiffusionPaletteProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.testPalette); + PaletteDitherProcessor p = this.Verify(); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(this.testPalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors() { - this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette, this.rect); - ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.testPalette, this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(this.testPalette, p.Palette); } + + [Fact] + public void ErrorDitherEquality() + { + IDither dither = KnownDitherings.FloydSteinberg; + ErrorDither dither2 = ErrorDither.FloydSteinberg; + ErrorDither dither3 = ErrorDither.FloydSteinberg; + + Assert.True(dither == dither2); + Assert.True(dither2 == dither); + Assert.False(dither != dither2); + Assert.False(dither2 != dither); + Assert.Equal(dither, dither2); + Assert.Equal(dither, (object)dither2); + Assert.Equal(dither.GetHashCode(), dither2.GetHashCode()); + + dither = null; + Assert.False(dither == dither2); + Assert.False(dither2 == dither); + Assert.True(dither != dither2); + Assert.True(dither2 != dither); + Assert.NotEqual(dither, dither2); + Assert.NotEqual(dither, (object)dither2); + Assert.NotEqual(dither?.GetHashCode(), dither2.GetHashCode()); + + Assert.True(dither2 == dither3); + Assert.True(dither3 == dither2); + Assert.False(dither2 != dither3); + Assert.False(dither3 != dither2); + Assert.Equal(dither2, dither3); + Assert.Equal(dither2, (object)dither3); + Assert.Equal(dither2.GetHashCode(), dither3.GetHashCode()); + } + + [Fact] + public void OrderedDitherEquality() + { + IDither dither = KnownDitherings.Bayer2x2; + OrderedDither dither2 = OrderedDither.Bayer2x2; + OrderedDither dither3 = OrderedDither.Bayer2x2; + + Assert.True(dither == dither2); + Assert.True(dither2 == dither); + Assert.False(dither != dither2); + Assert.False(dither2 != dither); + Assert.Equal(dither, dither2); + Assert.Equal(dither, (object)dither2); + Assert.Equal(dither.GetHashCode(), dither2.GetHashCode()); + + dither = null; + Assert.False(dither == dither2); + Assert.False(dither2 == dither); + Assert.True(dither != dither2); + Assert.True(dither2 != dither); + Assert.NotEqual(dither, dither2); + Assert.NotEqual(dither, (object)dither2); + Assert.NotEqual(dither?.GetHashCode(), dither2.GetHashCode()); + + Assert.True(dither2 == dither3); + Assert.True(dither3 == dither2); + Assert.False(dither2 != dither3); + Assert.False(dither3 != dither2); + Assert.Equal(dither2, dither3); + Assert.Equal(dither2, (object)dither3); + Assert.Equal(dither2.GetHashCode(), dither3.GetHashCode()); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs index 7d9e0f04b..9cb7e0409 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs @@ -18,37 +18,37 @@ 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 }, - { "Ordered3x3", KnownDitherers.OrderedDither3x3 }, - { "Bayer2x2", KnownDitherers.BayerDither2x2 } + { "Bayer8x8", KnownDitherings.Bayer8x8 }, + { "Bayer4x4", KnownDitherings.Bayer4x4 }, + { "Ordered3x3", KnownDitherings.Ordered3x3 }, + { "Bayer2x2", KnownDitherings.Bayer2x2 } }; - public static readonly TheoryData ErrorDiffusers = new TheoryData + public static readonly TheoryData ErrorDiffusers = new TheoryData { - { "Atkinson", KnownDiffusers.Atkinson }, - { "Burks", KnownDiffusers.Burks }, - { "FloydSteinberg", KnownDiffusers.FloydSteinberg }, - { "JarvisJudiceNinke", KnownDiffusers.JarvisJudiceNinke }, - { "Sierra2", KnownDiffusers.Sierra2 }, - { "Sierra3", KnownDiffusers.Sierra3 }, - { "SierraLite", KnownDiffusers.SierraLite }, - { "StevensonArce", KnownDiffusers.StevensonArce }, - { "Stucki", KnownDiffusers.Stucki }, + { "Atkinson", KnownDitherings.Atkinson }, + { "Burks", KnownDitherings.Burks }, + { "FloydSteinberg", KnownDitherings.FloydSteinberg }, + { "JarvisJudiceNinke", KnownDitherings.JarvisJudiceNinke }, + { "Sierra2", KnownDitherings.Sierra2 }, + { "Sierra3", KnownDitherings.Sierra3 }, + { "SierraLite", KnownDitherings.SierraLite }, + { "StevensonArce", KnownDitherings.StevensonArce }, + { "Stucki", KnownDitherings.Stucki }, }; public const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24; - private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4; + private static IDither DefaultDitherer => KnownDitherings.Bayer4x4; - private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDitherings.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,12 +61,12 @@ 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()) { - image.Mutate(x => x.BinaryDiffuse(diffuser, .5F)); + image.Mutate(x => x.BinaryDither(diffuser)); image.DebugSave(provider, name); } } @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, 0.5f)); + image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser)); image.DebugSave(provider); } } @@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, .5F, bounds)); + image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser, bounds)); image.DebugSave(provider); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 78481acd2..86f982118 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 + { + { KnownDitherings.Atkinson, nameof(KnownDitherings.Atkinson) }, + { KnownDitherings.Burks, nameof(KnownDitherings.Burks) }, + { KnownDitherings.FloydSteinberg, nameof(KnownDitherings.FloydSteinberg) }, + { KnownDitherings.JarvisJudiceNinke, nameof(KnownDitherings.JarvisJudiceNinke) }, + { KnownDitherings.Sierra2, nameof(KnownDitherings.Sierra2) }, + { KnownDitherings.Sierra3, nameof(KnownDitherings.Sierra3) }, + { KnownDitherings.SierraLite, nameof(KnownDitherings.SierraLite) }, + { KnownDitherings.StevensonArce, nameof(KnownDitherings.StevensonArce) }, + { KnownDitherings.Stucki, nameof(KnownDitherings.Stucki) }, + }; + + public static readonly TheoryData OrderedDitherers + = new TheoryData + { + { KnownDitherings.Bayer2x2, nameof(KnownDitherings.Bayer2x2) }, + { KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) }, + { KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) }, + { KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) } + }; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f); - private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4; + private static IDither DefaultDitherer => KnownDitherings.Bayer4x4; - private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDitherings.Atkinson; /// /// The output is visually correct old 32bit runtime, @@ -62,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization } provider.RunRectangleConstrainedValidatingProcessorTest( - (x, rect) => x.Diffuse(DefaultErrorDiffuser, .5F, rect), + (x, rect) => x.Dither(DefaultErrorDiffuser, rect), comparer: ValidatorComparer); } @@ -93,14 +95,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization // Increased tolerance because of compatibility issues on .NET 4.6.2: var comparer = ImageComparer.TolerantPercentage(1f); - provider.RunValidatingProcessorTest(x => x.Diffuse(DefaultErrorDiffuser, 0.5f), comparer: comparer); + provider.RunValidatingProcessorTest(x => x.Dither(DefaultErrorDiffuser), comparer: comparer); } [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)] public void DiffusionFilter_WorksWithAllErrorDiffusers( TestImageProvider provider, - IErrorDiffuser diffuser) + IDither diffuser, + string name) where TPixel : struct, IPixel { if (SkipAllDitherTests) @@ -109,8 +112,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization } provider.RunValidatingProcessorTest( - x => x.Diffuse(diffuser, 0.5f), - testOutputDetails: diffuser.GetType().Name, + x => x.Dither(diffuser), + testOutputDetails: name, comparer: ValidatorComparer, appendPixelTypeToFileName: false); } @@ -134,7 +137,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization [WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)] public void DitherFilter_WorksWithAllDitherers( TestImageProvider provider, - IOrderedDither ditherer) + IDither ditherer, + string name) where TPixel : struct, IPixel { if (SkipAllDitherTests) @@ -144,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization provider.RunValidatingProcessorTest( x => x.Dither(ditherer), - testOutputDetails: ditherer.GetType().Name, + testOutputDetails: name, comparer: ValidatorComparer, appendPixelTypeToFileName: false); } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs index b3900325d..bb7921d68 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.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; @@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization [Fact] public void OctreeQuantizerConstructor() { - var quantizer = new OctreeQuantizer(128); - - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); - - quantizer = new OctreeQuantizer(false); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Diffuser); - - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); - - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson, 128); - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new OctreeQuantizer(expected); + + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = null }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new OctreeQuantizer(expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] @@ -38,21 +42,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(false); + quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson); + quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index 2e9dc83dd..3c1fa11ab 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -10,61 +10,70 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization { public class PaletteQuantizerTests { - private static readonly Color[] Rgb = new Color[] { Color.Red, Color.Green, Color.Blue }; + private static readonly Color[] Palette = new Color[] { Color.Red, Color.Green, Color.Blue }; [Fact] public void PaletteQuantizerConstructor() { - var quantizer = new PaletteQuantizer(Rgb); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new PaletteQuantizer(Palette, expected); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); - quantizer = new PaletteQuantizer(Rgb, false); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Null(quantizer.Diffuser); + expected = new QuantizerOptions { Dither = null }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); - quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson); - Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new PaletteQuantizer(Palette, expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] public void PaletteQuantizerCanCreateFrameQuantizer() { - var quantizer = new PaletteQuantizer(Rgb); + var quantizer = new PaletteQuantizer(Palette); IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new PaletteQuantizer(Rgb, false); + quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson); + quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); } [Fact] public void KnownQuantizersWebSafeTests() { IQuantizer quantizer = KnownQuantizers.WebSafe; - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } [Fact] public void KnownQuantizersWernerTests() { IQuantizer quantizer = KnownQuantizers.Werner; - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs new file mode 100644 index 000000000..70a07f74f --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization +{ + public class QuantizerTests + { + /// + /// Something is causing tests to fail on NETFX in CI. + /// Could be a JIT error as everything runs well and is identical to .NET Core output. + /// Not worth investigating for now. + /// + /// + private static readonly bool SkipAllQuantizerTests = TestEnvironment.RunsOnCI && TestEnvironment.IsFramework; + + public static readonly string[] CommonTestImages = + { + TestImages.Png.CalliphoraPartial, + TestImages.Png.Bike + }; + + private static readonly QuantizerOptions NoDitherOptions = new QuantizerOptions { Dither = null }; + private static readonly QuantizerOptions DiffuserDitherOptions = new QuantizerOptions { Dither = KnownDitherings.FloydSteinberg }; + private static readonly QuantizerOptions OrderedDitherOptions = new QuantizerOptions { Dither = KnownDitherings.Bayer8x8 }; + + private static readonly QuantizerOptions Diffuser0_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = 0F + }; + + private static readonly QuantizerOptions Diffuser0_25_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .25F + }; + + private static readonly QuantizerOptions Diffuser0_5_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .5F + }; + + private static readonly QuantizerOptions Diffuser0_75_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.FloydSteinberg, + DitherScale = .75F + }; + + private static readonly QuantizerOptions Ordered0_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.Bayer8x8, + DitherScale = 0F + }; + + private static readonly QuantizerOptions Ordered0_25_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.Bayer8x8, + DitherScale = .25F + }; + + private static readonly QuantizerOptions Ordered0_5_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.Bayer8x8, + DitherScale = .5F + }; + + private static readonly QuantizerOptions Ordered0_75_ScaleDitherOptions = new QuantizerOptions + { + Dither = KnownDitherings.Bayer8x8, + DitherScale = .75F + }; + + public static readonly TheoryData Quantizers + = new TheoryData + { + // Known uses error diffusion by default. + KnownQuantizers.Octree, + KnownQuantizers.WebSafe, + KnownQuantizers.Werner, + KnownQuantizers.Wu, + new OctreeQuantizer(NoDitherOptions), + new WebSafePaletteQuantizer(NoDitherOptions), + new WernerPaletteQuantizer(NoDitherOptions), + new WuQuantizer(NoDitherOptions), + new OctreeQuantizer(OrderedDitherOptions), + new WebSafePaletteQuantizer(OrderedDitherOptions), + new WernerPaletteQuantizer(OrderedDitherOptions), + new WuQuantizer(OrderedDitherOptions) + }; + + public static readonly TheoryData DitherScaleQuantizers + = new TheoryData + { + new OctreeQuantizer(Diffuser0_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions), + new WuQuantizer(Diffuser0_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions), + new WuQuantizer(Diffuser0_25_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions), + new WuQuantizer(Diffuser0_5_ScaleDitherOptions), + + new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions), + new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions), + new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions), + new WuQuantizer(Diffuser0_75_ScaleDitherOptions), + + new OctreeQuantizer(DiffuserDitherOptions), + new WebSafePaletteQuantizer(DiffuserDitherOptions), + new WernerPaletteQuantizer(DiffuserDitherOptions), + new WuQuantizer(DiffuserDitherOptions), + + new OctreeQuantizer(Ordered0_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions), + new WuQuantizer(Ordered0_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_25_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions), + new WuQuantizer(Ordered0_25_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_5_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions), + new WuQuantizer(Ordered0_5_ScaleDitherOptions), + + new OctreeQuantizer(Ordered0_75_ScaleDitherOptions), + new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions), + new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions), + new WuQuantizer(Ordered0_75_ScaleDitherOptions), + + new OctreeQuantizer(OrderedDitherOptions), + new WebSafePaletteQuantizer(OrderedDitherOptions), + new WernerPaletteQuantizer(OrderedDitherOptions), + new WuQuantizer(OrderedDitherOptions), + }; + + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F); + + [Theory] + [WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)] + public void ApplyQuantizationInBox(TestImageProvider provider, IQuantizer quantizer) + where TPixel : struct, IPixel + { + if (SkipAllQuantizerTests) + { + return; + } + + string quantizerName = quantizer.GetType().Name; + string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither"; + string testOutputDetails = $"{quantizerName}_{ditherName}"; + + provider.RunRectangleConstrainedValidatingProcessorTest( + (x, rect) => x.Quantize(quantizer, rect), + comparer: ValidatorComparer, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: false); + } + + [Theory] + [WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)] + public void ApplyQuantization(TestImageProvider provider, IQuantizer quantizer) + where TPixel : struct, IPixel + { + if (SkipAllQuantizerTests) + { + return; + } + + string quantizerName = quantizer.GetType().Name; + string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither"; + string testOutputDetails = $"{quantizerName}_{ditherName}"; + + provider.RunValidatingProcessorTest( + x => x.Quantize(quantizer), + comparer: ValidatorComparer, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: false); + } + + [Theory] + [WithFile(TestImages.Png.David, nameof(DitherScaleQuantizers), PixelTypes.Rgba32)] + public void ApplyQuantizationWithDitheringScale(TestImageProvider provider, IQuantizer quantizer) + where TPixel : struct, IPixel + { + if (SkipAllQuantizerTests) + { + return; + } + + string quantizerName = quantizer.GetType().Name; + string ditherName = quantizer.Options.Dither.GetType().Name; + float ditherScale = quantizer.Options.DitherScale; + string testOutputDetails = FormattableString.Invariant($"{quantizerName}_{ditherName}_{ditherScale}"); + + provider.RunValidatingProcessorTest( + x => x.Quantize(quantizer), + comparer: ValidatorComparer, + testOutputDetails: testOutputDetails, + appendPixelTypeToFileName: false); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs index 625043c7f..eb9d738e9 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.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; @@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization [Fact] public void WuQuantizerConstructor() { - var quantizer = new WuQuantizer(128); - - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); - - quantizer = new WuQuantizer(false); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Diffuser); - - quantizer = new WuQuantizer(KnownDiffusers.Atkinson); - Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); - - quantizer = new WuQuantizer(KnownDiffusers.Atkinson, 128); - Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + var expected = new QuantizerOptions { MaxColors = 128 }; + var quantizer = new WuQuantizer(expected); + + Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = null }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Null(quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); + + expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 }; + quantizer = new WuQuantizer(expected); + Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors); + Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither); } [Fact] @@ -38,21 +42,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser); + Assert.NotNull(frameQuantizer.Options); + Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new WuQuantizer(false); + quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); - Assert.Null(frameQuantizer.Diffuser); + Assert.Null(frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); - quantizer = new WuQuantizer(KnownDiffusers.Atkinson); + quantizer = new WuQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson }); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser); + Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither); + frameQuantizer.Dispose(); } } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 775001709..92e14b6a1 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -22,15 +22,30 @@ namespace SixLabors.ImageSharp.Tests var octree = new OctreeQuantizer(); var wu = new WuQuantizer(); - Assert.NotNull(werner.Diffuser); - Assert.NotNull(webSafe.Diffuser); - Assert.NotNull(octree.Diffuser); - Assert.NotNull(wu.Diffuser); - - Assert.True(werner.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(webSafe.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(octree.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(wu.CreateFrameQuantizer(this.Configuration).Dither); + Assert.NotNull(werner.Options.Dither); + Assert.NotNull(webSafe.Options.Dither); + Assert.NotNull(octree.Options.Dither); + Assert.NotNull(wu.Options.Dither); + + using (IFrameQuantizer quantizer = werner.CreateFrameQuantizer(this.Configuration)) + { + Assert.NotNull(quantizer.Options.Dither); + } + + using (IFrameQuantizer quantizer = webSafe.CreateFrameQuantizer(this.Configuration)) + { + Assert.NotNull(quantizer.Options.Dither); + } + + using (IFrameQuantizer quantizer = octree.CreateFrameQuantizer(this.Configuration)) + { + Assert.NotNull(quantizer.Options.Dither); + } + + using (IFrameQuantizer quantizer = wu.CreateFrameQuantizer(this.Configuration)) + { + Assert.NotNull(quantizer.Options.Dither); + } } [Theory] @@ -43,17 +58,24 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage()) { - Assert.True(image[0, 0].Equals(default(TPixel))); + Assert.True(image[0, 0].Equals(default)); + + var options = new QuantizerOptions(); + if (!dither) + { + options.Dither = null; + } - var quantizer = new OctreeQuantizer(dither); + var quantizer = new OctreeQuantizer(options); foreach (ImageFrame frame in image.Frames) { - IQuantizedFrame quantized = - quantizer.CreateFrameQuantizer(this.Configuration).QuantizeFrame(frame); - - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) + using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + { + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.GetPixelSpan()[0]); + } } } } @@ -66,22 +88,29 @@ namespace SixLabors.ImageSharp.Tests { using (Image image = provider.GetImage()) { - Assert.True(image[0, 0].Equals(default(TPixel))); + Assert.True(image[0, 0].Equals(default)); - var quantizer = new WuQuantizer(dither); + var options = new QuantizerOptions(); + if (!dither) + { + options.Dither = null; + } + + var quantizer = new WuQuantizer(options); foreach (ImageFrame frame in image.Frames) { - IQuantizedFrame quantized = - quantizer.CreateFrameQuantizer(this.Configuration).QuantizeFrame(frame); - - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) + using (QuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + { + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.GetPixelSpan()[0]); + } } } } - private int GetTransparentIndex(IQuantizedFrame quantized) + private int GetTransparentIndex(QuantizedFrame quantized) where TPixel : struct, IPixel { // Transparent pixels are much more likely to be found at the end of a palette diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index c83adea91..d41d133fa 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -15,34 +15,38 @@ namespace SixLabors.ImageSharp.Tests.Quantization public void SinglePixelOpaque() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); - using (var image = new Image(config, 1, 1, Color.Black)) - using (IQuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) - { - Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + using var image = new Image(config, 1, 1, Color.Black); + ImageFrame frame = image.Frames.RootFrame; - Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); - } + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); } [Fact] public void SinglePixelTransparent() { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); - using (var image = new Image(config, 1, 1, default(Rgba32))) - using (IQuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) - { - Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + using var image = new Image(config, 1, 1, default(Rgba32)); + ImageFrame frame = image.Frames.RootFrame; - Assert.Equal(default, result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); - } + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(default, result.Palette.Span[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); } [Fact] @@ -63,46 +67,47 @@ namespace SixLabors.ImageSharp.Tests.Quantization [Fact] public void Palette256() { - using (var image = new Image(1, 256)) + using var image = new Image(1, 256); + + for (int i = 0; i < 256; i++) { - for (int i = 0; i < 256; i++) - { - byte r = (byte)((i % 4) * 85); - byte g = (byte)(((i / 4) % 4) * 85); - byte b = (byte)(((i / 16) % 4) * 85); - byte a = (byte)((i / 64) * 85); + byte r = (byte)((i % 4) * 85); + byte g = (byte)(((i / 4) % 4) * 85); + byte b = (byte)(((i / 16) % 4) * 85); + byte a = (byte)((i / 64) * 85); - image[0, i] = new Rgba32(r, g, b, a); - } + image[0, i] = new Rgba32(r, g, b, a); + } - Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) - { - Assert.Equal(256, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); - var actualImage = new Image(1, 256); + ImageFrame frame = image.Frames.RootFrame; - ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; - for (int y = 0; y < actualImage.Height; y++) - { - Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); - int yy = y * actualImage.Width; + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - for (int x = 0; x < actualImage.Width; x++) - { - int i = x + yy; - row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])]; - } - } + Assert.Equal(256, result.Palette.Length); + Assert.Equal(256, result.GetPixelSpan().Length); + + var actualImage = new Image(1, 256); - Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); + ReadOnlySpan paletteSpan = result.Palette.Span; + int paletteCount = result.Palette.Length - 1; + for (int y = 0; y < actualImage.Height; y++) + { + Span row = actualImage.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + int yy = y * actualImage.Width; + + for (int x = 0; x < actualImage.Width; x++) + { + int i = x + yy; + row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])]; } } + + Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); } [Theory] @@ -114,12 +119,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization using (Image image = provider.GetImage()) { Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) - { - Assert.Equal(48, result.Palette.Length); - } + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + ImageFrame frame = image.Frames.RootFrame; + + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(48, result.Palette.Length); } } @@ -142,10 +148,11 @@ namespace SixLabors.ImageSharp.Tests.Quantization } Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); + var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }); + ImageFrame frame = image.Frames.RootFrame; using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) + using (QuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ee919dc2f..b1cfec218 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Tests public const string LowColorVariance = "Png/low-variance.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string InvalidTextData = "Png/InvalidTextData.png"; + public const string David = "Png/david.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs index e6cee9a6d..626b698e1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs @@ -24,6 +24,16 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison sb.Append(Environment.NewLine); + // TODO: We should add OSX. + sb.AppendFormat("Test Environment OS : {0}", TestEnvironment.IsWindows ? "Windows" : "Linux"); + sb.Append(Environment.NewLine); + + sb.AppendFormat("Test Environment is CI : {0}", TestEnvironment.RunsOnCI); + sb.Append(Environment.NewLine); + + sb.AppendFormat("Test Environment is .NET Core : {0}", !TestEnvironment.IsFramework); + sb.Append(Environment.NewLine); + int i = 0; foreach (ImageSimilarityReport r in reports) { diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 4f89af70d..502a5bf46 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -342,10 +342,10 @@ namespace SixLabors.ImageSharp.Tests if (!File.Exists(referenceOutputFile)) { - throw new System.IO.FileNotFoundException("Reference output file missing: " + referenceOutputFile, referenceOutputFile); + throw new FileNotFoundException("Reference output file missing: " + referenceOutputFile, referenceOutputFile); } - decoder = decoder ?? TestEnvironment.GetReferenceDecoder(referenceOutputFile); + decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile); return Image.Load(referenceOutputFile, decoder); } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 05da31282..089e5805e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -294,7 +294,8 @@ namespace SixLabors.ImageSharp.Tests this TestImageProvider provider, Action process, object testOutputDetails = null, - ImageComparer comparer = null) + ImageComparer comparer = null, + bool appendPixelTypeToFileName = true) where TPixel : struct, IPixel { if (comparer == null) @@ -306,8 +307,8 @@ namespace SixLabors.ImageSharp.Tests { var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => process(x, bounds)); - image.DebugSave(provider, testOutputDetails); - image.CompareToReferenceOutput(comparer, provider, testOutputDetails); + image.DebugSave(provider, testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName); + image.CompareToReferenceOutput(comparer, provider, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName); } } diff --git a/tests/Images/External b/tests/Images/External index fbba5e2a7..2d1505d70 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit fbba5e2a78aa479c0752dc0fd91ec25b4948704a +Subproject commit 2d1505d7087d91cd83d0cda409aee213de7841ab diff --git a/tests/Images/Input/Png/bike-small.png b/tests/Images/Input/Png/bike-small.png new file mode 100644 index 000000000..8597e68e9 Binary files /dev/null and b/tests/Images/Input/Png/bike-small.png differ diff --git a/tests/Images/Input/Png/david.png b/tests/Images/Input/Png/david.png new file mode 100644 index 000000000..6cfa88480 Binary files /dev/null and b/tests/Images/Input/Png/david.png differ