From 16b24d4a08c736ea42b9e8b0b102e6cd41ba4eac Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Tue, 7 May 2019 00:02:54 +0200 Subject: [PATCH] refactor dithering and error diffusion to use Color --- src/ImageSharp/Color/Color.cs | 67 ++++++++------- .../Processing/DiffuseExtensions.cs | 69 ++++++++-------- src/ImageSharp/Processing/DitherExtensions.cs | 55 ++++++------- .../ErrorDiffusionPaletteProcessor.cs | 77 +++--------------- .../ErrorDiffusionPaletteProcessor{TPixel}.cs | 81 +++++++++++++++++++ .../OrderedDitherPaletteProcessor.cs | 70 +++------------- .../OrderedDitherPaletteProcessor{TPixel}.cs | 79 ++++++++++++++++++ .../Dithering/PaletteDitherProcessor.cs | 37 +++++++++ ...e.cs => PaletteDitherProcessor{TPixel}.cs} | 35 ++++---- .../Processing/Dithering/DitherTest.cs | 42 ++++++---- 10 files changed, 370 insertions(+), 242 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs create mode 100644 src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs rename src/ImageSharp/Processing/Processors/Dithering/{PaletteDitherProcessorBase.cs => PaletteDitherProcessor{TPixel}.cs} (77%) diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs index 3e20cafbd..bc97393cc 100644 --- a/src/ImageSharp/Color/Color.cs +++ b/src/ImageSharp/Color/Color.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.Globalization; using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.PixelFormats; @@ -117,6 +118,44 @@ namespace SixLabors.ImageSharp /// public override string ToString() => this.ToHex(); + public TPixel ToPixel() + where TPixel : struct, IPixel + { + TPixel pixel = default; + pixel.FromRgba64(this.data); + return pixel; + } + + public bool Equals(Color other) + { + return this.data.PackedValue == other.data.PackedValue; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + return obj is Color other && this.Equals(other); + } + + /// + public override int GetHashCode() + { + return this.data.PackedValue.GetHashCode(); + } + + internal static void ToPixel( + Configuration configuration, + ReadOnlySpan source, + Span destination) + where TPixel : struct, IPixel + { + ReadOnlySpan rgba64Span = MemoryMarshal.Cast(source); + PixelOperations.Instance.FromRgba64(Configuration.Default, rgba64Span, destination); + } + /// /// Converts the specified hex value to an rrggbbaa hex value. /// @@ -153,33 +192,5 @@ namespace SixLabors.ImageSharp return new string(new[] { r, r, g, g, b, b, a, a }); } - - public TPixel ToPixel() - where TPixel : struct, IPixel - { - TPixel pixel = default; - pixel.FromRgba64(this.data); - return pixel; - } - - public bool Equals(Color other) - { - return this.data.PackedValue == other.data.PackedValue; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - return obj is Color other && this.Equals(other); - } - - /// - public override int GetHashCode() - { - return this.data.PackedValue.GetHashCode(); - } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/DiffuseExtensions.cs b/src/ImageSharp/Processing/DiffuseExtensions.cs index 4668363e9..cd714c3da 100644 --- a/src/ImageSharp/Processing/DiffuseExtensions.cs +++ b/src/ImageSharp/Processing/DiffuseExtensions.cs @@ -1,14 +1,15 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; +using System; + using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Dithering { /// - /// Defines extension methods to apply diffusion to an + /// Defines extension methods to apply diffusion to an /// using Mutate/Clone. /// public static class DiffuseExtensions @@ -16,68 +17,68 @@ namespace SixLabors.ImageSharp.Processing.Dithering /// /// Dithers the image reducing it to a web-safe palette using error diffusion. /// - /// The pixel format. /// The image this method extends. - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source) - where TPixel : struct, IPixel - => Diffuse(source, KnownDiffusers.FloydSteinberg, .5F); + /// 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 pixel format. /// 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) - where TPixel : struct, IPixel - => Diffuse(source, KnownDiffusers.FloydSteinberg, threshold); + /// 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 pixel format. /// The image this method extends. /// The diffusion algorithm to apply. /// The threshold to apply binarization of the image. Must be between 0 and 1. - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold) - where TPixel : struct, IPixel - => source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold)); + /// 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 pixel format. /// The image this method extends. /// The diffusion algorithm to apply. /// The threshold to apply binarization of the image. Must be between 0 and 1. /// /// The structure that specifies the portion of the image object to alter. /// - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold), rectangle); + /// 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 pixel format. /// The image this method extends. /// The diffusion algorithm to apply. /// The threshold to apply binarization of the image. Must be between 0 and 1. /// The palette to select substitute colors from. - /// The to allow chaining of operations. - public static IImageProcessingContext Diffuse(this IImageProcessingContext source, IErrorDiffuser diffuser, float threshold, TPixel[] palette) - where TPixel : struct, IPixel - => source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette)); + /// The to allow chaining of operations. + public static IImageProcessingContext Diffuse( + this IImageProcessingContext source, + IErrorDiffuser diffuser, + float threshold, + ReadOnlySpan palette) => + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette)); /// /// Dithers the image reducing it to the given palette using error diffusion. /// - /// The pixel format. /// The image this method extends. /// The diffusion algorithm to apply. /// The threshold to apply binarization of the image. Must be between 0 and 1. @@ -85,9 +86,13 @@ namespace SixLabors.ImageSharp.Processing.Dithering /// /// 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, TPixel[] palette, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle); + /// The to allow chaining of operations. + public static IImageProcessingContext Diffuse( + this IImageProcessingContext source, + IErrorDiffuser diffuser, + float threshold, + ReadOnlySpan palette, + Rectangle rectangle) => + source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/DitherExtensions.cs b/src/ImageSharp/Processing/DitherExtensions.cs index aeb975d1c..55794aec2 100644 --- a/src/ImageSharp/Processing/DitherExtensions.cs +++ b/src/ImageSharp/Processing/DitherExtensions.cs @@ -1,14 +1,15 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -using SixLabors.ImageSharp.PixelFormats; +using System; + using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing { /// - /// Defines dithering extensions to apply on an + /// Defines dithering extensions to apply on an /// using Mutate/Clone. /// public static class DitherExtensions @@ -16,63 +17,63 @@ namespace SixLabors.ImageSharp.Processing /// /// Dithers the image reducing it to a web-safe palette using Bayer4x4 ordered dithering. /// - /// The pixel format. /// The image this method extends. - /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source) - where TPixel : struct, IPixel - => Dither(source, KnownDitherers.BayerDither4x4); + /// The to allow chaining of operations. + public static IImageProcessingContext Dither(this IImageProcessingContext source) => + Dither(source, KnownDitherers.BayerDither4x4); /// /// Dithers the image reducing it to a web-safe palette using ordered dithering. /// - /// The pixel format. /// The image this method extends. /// The ordered ditherer. - /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither) - where TPixel : struct, IPixel - => source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither)); + /// The to allow chaining of operations. + public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither) => + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither)); /// /// Dithers the image reducing it to the given palette using ordered dithering. /// - /// The pixel format. /// The image this method extends. /// The ordered ditherer. /// The palette to select substitute colors from. - /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, TPixel[] palette) - where TPixel : struct, IPixel - => source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette)); + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IOrderedDither dither, + ReadOnlySpan palette) => + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette)); /// /// Dithers the image reducing it to a web-safe palette using ordered dithering. /// - /// The pixel format. /// The image this method extends. /// The ordered ditherer. /// /// The structure that specifies the portion of the image object to alter. /// - /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle); + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IOrderedDither dither, + Rectangle rectangle) => + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle); /// /// Dithers the image reducing it to the given palette using ordered dithering. /// - /// The pixel format. /// The image this method extends. /// The ordered ditherer. /// The palette to select substitute colors from. /// /// The structure that specifies the portion of the image object to alter. /// - /// The to allow chaining of operations. - public static IImageProcessingContext Dither(this IImageProcessingContext source, IOrderedDither dither, TPixel[] palette, Rectangle rectangle) - where TPixel : struct, IPixel - => source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle); + /// The to allow chaining of operations. + public static IImageProcessingContext Dither( + this IImageProcessingContext source, + IOrderedDither dither, + ReadOnlySpan palette, + Rectangle rectangle) => + source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle); } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs index 911d3e8fd..5436a7733 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs @@ -2,21 +2,19 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; + +using SixLabors.ImageSharp.Processing.Processors.Binarization; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// - /// An that dithers an image using error diffusion. + /// 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. /// - /// The pixel format. - internal class ErrorDiffusionPaletteProcessor : PaletteDitherProcessorBase - where TPixel : struct, IPixel + public sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The error diffuser public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser) @@ -25,22 +23,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The error diffuser /// The threshold to split the image. Must be between 0 and 1. public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold) - : this(diffuser, threshold, NamedColors.WebSafePalette) + : this(diffuser, threshold, Color.WebSafePalette) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The error diffuser /// The threshold to split the image. Must be between 0 and 1. /// The palette to select substitute colors from. - public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, TPixel[] palette) + public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, ReadOnlySpan palette) : base(palette) { Guard.NotNull(diffuser, nameof(diffuser)); @@ -60,59 +58,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// public float Threshold { get; } - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + /// + public override IImageProcessor CreatePixelSpecificProcessor() { - byte threshold = (byte)MathF.Round(this.Threshold * 255F); - bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); - - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - int startY = interest.Y; - int endY = interest.Bottom; - int startX = interest.X; - int endX = interest.Right; - - // Collect the values before looping so we can reduce our calculation count for identical sibling pixels - TPixel sourcePixel = source[startX, startY]; - TPixel previousPixel = sourcePixel; - PixelPair pair = this.GetClosestPixelPair(ref sourcePixel); - 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)) - { - pair = this.GetClosestPixelPair(ref sourcePixel); - - // No error to spread, exact match. - if (sourcePixel.Equals(pair.First)) - { - continue; - } - - 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 ? pair.Second : pair.First; - this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY); - } - } + return new ErrorDiffusionPaletteProcessor(this); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs new file mode 100644 index 000000000..7edf287e8 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs @@ -0,0 +1,81 @@ +// 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.Primitives; + +namespace SixLabors.ImageSharp.Processing.Processors.Dithering +{ + /// + /// An that dithers an image using error diffusion. + /// + /// The pixel format. + internal class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor + where TPixel : struct, IPixel + { + public ErrorDiffusionPaletteProcessor(ErrorDiffusionPaletteProcessor definition) + : base(definition) + { + } + + private new ErrorDiffusionPaletteProcessor Definition => (ErrorDiffusionPaletteProcessor)base.Definition; + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + byte threshold = (byte)MathF.Round(this.Definition.Threshold * 255F); + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + PixelPair pair = this.GetClosestPixelPair(ref sourcePixel); + 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)) + { + pair = this.GetClosestPixelPair(ref sourcePixel); + + // No error to spread, exact match. + if (sourcePixel.Equals(pair.First)) + { + continue; + } + + 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 ? pair.Second : pair.First; + this.Definition.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, startX, startY, endX, endY); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs index 1b4910a14..ba7c1e998 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs @@ -2,35 +2,30 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Dithering { /// - /// An that dithers an image using error diffusion. + /// 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. /// - /// The pixel format. - internal class OrderedDitherPaletteProcessor : PaletteDitherProcessorBase - where TPixel : struct, IPixel + public sealed class OrderedDitherPaletteProcessor : PaletteDitherProcessor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ordered ditherer. public OrderedDitherPaletteProcessor(IOrderedDither dither) - : this(dither, NamedColors.WebSafePalette) + : this(dither, Color.WebSafePalette) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ordered ditherer. /// The palette to select substitute colors from. - public OrderedDitherPaletteProcessor(IOrderedDither dither, TPixel[] palette) + public OrderedDitherPaletteProcessor(IOrderedDither dither, ReadOnlySpan palette) : base(palette) => this.Dither = dither ?? throw new ArgumentNullException(nameof(dither)); /// @@ -38,57 +33,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// public IOrderedDither Dither { get; } - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + /// + public override IImageProcessor CreatePixelSpecificProcessor() { - bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); - - var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); - int startY = interest.Y; - int endY = interest.Bottom; - int startX = interest.X; - int endX = interest.Right; - - // Collect the values before looping so we can reduce our calculation count for identical sibling pixels - TPixel sourcePixel = source[startX, startY]; - TPixel previousPixel = sourcePixel; - PixelPair pair = this.GetClosestPixelPair(ref sourcePixel); - 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)) - { - pair = this.GetClosestPixelPair(ref sourcePixel); - - // No error to spread, exact match. - if (sourcePixel.Equals(pair.First)) - { - continue; - } - - sourcePixel.ToRgba32(ref rgba); - luminance = isAlphaOnly ? rgba.A : ImageMaths.Get8BitBT709Luminance(rgba.R, rgba.G, rgba.B); - - // Setup the previous pointer - previousPixel = sourcePixel; - } - - this.Dither.Dither(source, sourcePixel, pair.Second, pair.First, luminance, x, y); - } - } + return new OrderedDitherPaletteProcessor(this); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs new file mode 100644 index 000000000..eefa6e522 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs @@ -0,0 +1,79 @@ +// 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.Primitives; + +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 + { + public OrderedDitherPaletteProcessor(OrderedDitherPaletteProcessor definition) + : base(definition) + { + } + + private new OrderedDitherPaletteProcessor Definition => (OrderedDitherPaletteProcessor)base.Definition; + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + bool isAlphaOnly = typeof(TPixel) == typeof(Alpha8); + + var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); + int startY = interest.Y; + int endY = interest.Bottom; + int startX = interest.X; + int endX = interest.Right; + + // Collect the values before looping so we can reduce our calculation count for identical sibling pixels + TPixel sourcePixel = source[startX, startY]; + TPixel previousPixel = sourcePixel; + PixelPair pair = this.GetClosestPixelPair(ref sourcePixel); + 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)) + { + pair = this.GetClosestPixelPair(ref sourcePixel); + + // No error to spread, exact match. + if (sourcePixel.Equals(pair.First)) + { + continue; + } + + sourcePixel.ToRgba32(ref rgba); + luminance = isAlphaOnly ? rgba.A : 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); + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs new file mode 100644 index 000000000..904d02634 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs @@ -0,0 +1,37 @@ +// 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 +{ + /// + /// The base class for dither and diffusion processors that consume a palette. + /// + public abstract class PaletteDitherProcessor : IImageProcessor + { + private readonly Color[] palette; + + /// + /// Initializes a new instance of the class. + /// + /// The palette to select substitute colors from. + protected PaletteDitherProcessor(ReadOnlySpan palette) + { + // This shouldn't be a perf issue: + // these arrays are small, and created with low frequency. + this.palette = palette.ToArray(); + } + + /// + /// Gets the palette to select substitute colors from. + /// + public ReadOnlySpan Palette => this.palette; + + /// + public abstract IImageProcessor CreatePixelSpecificProcessor() + where TPixel : struct, IPixel; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs similarity index 77% rename from src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs rename to src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index 6313b74c9..334eab833 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessorBase.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -1,10 +1,11 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; + using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -14,7 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// The base class for dither and diffusion processors that consume a palette. /// /// The pixel format. - internal abstract class PaletteDitherProcessorBase : ImageProcessor + internal abstract class PaletteDitherProcessor : ImageProcessor where TPixel : struct, IPixel { private readonly Dictionary> cache = new Dictionary>(); @@ -24,19 +25,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// private Vector4[] paletteVector; + private TPixel[] palette; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The palette to select substitute colors from. - protected PaletteDitherProcessorBase(TPixel[] palette) + protected PaletteDitherProcessor(PaletteDitherProcessor definition) { - this.Palette = palette ?? throw new ArgumentNullException(nameof(palette)); + this.Definition = definition; } - /// - /// Gets the palette to select substitute colors from. - /// - public TPixel[] Palette { get; } + protected PaletteDitherProcessor Definition { get; } /// /// Returns the two closest colors from the palette calculated via Euclidean distance in the Rgba space. @@ -74,12 +73,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering { leastDistance = distance; secondClosest = closest; - closest = this.Palette[index]; + closest = this.palette[index]; } else if (distance < secondLeastDistance) { secondLeastDistance = distance; - secondClosest = this.Palette[index]; + secondClosest = this.palette[index]; } } @@ -94,11 +93,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering { base.BeforeFrameApply(source, sourceRectangle, configuration); + // Lazy init palette: + if (this.palette is null) + { + ReadOnlySpan sourcePalette = this.Definition.Palette; + this.palette = new TPixel[sourcePalette.Length]; + Color.ToPixel(configuration, sourcePalette, this.palette); + } + // Lazy init paletteVector: if (this.paletteVector is null) { - this.paletteVector = new Vector4[this.Palette.Length]; - PixelOperations.Instance.ToVector4(configuration, (ReadOnlySpan)this.Palette, (Span)this.paletteVector, PixelConversionModifiers.Scale); + this.paletteVector = new Vector4[this.palette.Length]; + PixelOperations.Instance.ToVector4(configuration, (ReadOnlySpan)this.palette, (Span)this.paletteVector, PixelConversionModifiers.Scale); } } } diff --git a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs index f393d5923..59dcde2a6 100644 --- a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs +++ b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs @@ -1,6 +1,8 @@ // 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.Dithering; @@ -12,13 +14,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization { public class DitherTest : BaseImageOperationsExtensionTest { + private class Assert : Xunit.Assert + { + public static void Equal(ReadOnlySpan a, ReadOnlySpan b) + { + Xunit.Assert.True(a.SequenceEqual(b)); + } + } + private readonly IOrderedDither orderedDither; private readonly IErrorDiffuser errorDiffuser; - private readonly Rgba32[] TestPalette = + private readonly Color[] TestPalette = { - Rgba32.Red, - Rgba32.Green, - Rgba32.Blue + Color.Red, + Color.Green, + Color.Blue }; public DitherTest() @@ -31,24 +41,24 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_CorrectProcessor() { this.operations.Dither(this.orderedDither); - OrderedDitherPaletteProcessor p = this.Verify>(); + OrderedDitherPaletteProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(NamedColors.WebSafePalette, p.Palette); + Assert.Equal(Color.WebSafePalette.ToArray(), p.Palette); } [Fact] public void Dither_rect_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.rect); - OrderedDitherPaletteProcessor p = this.Verify>(this.rect); + OrderedDitherPaletteProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(NamedColors.WebSafePalette, p.Palette); + Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_index_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.TestPalette); - OrderedDitherPaletteProcessor p = this.Verify>(); + OrderedDitherPaletteProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.TestPalette, p.Palette); } @@ -57,7 +67,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); + OrderedDitherPaletteProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.TestPalette, p.Palette); } @@ -67,27 +77,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_ErrorDiffuser_CorrectProcessor() { this.operations.Diffuse(this.errorDiffuser, .4F); - ErrorDiffusionPaletteProcessor p = this.Verify>(); + ErrorDiffusionPaletteProcessor p = this.Verify(); Assert.Equal(this.errorDiffuser, p.Diffuser); Assert.Equal(.4F, p.Threshold); - Assert.Equal(NamedColors.WebSafePalette, p.Palette); + 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); + ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); Assert.Equal(this.errorDiffuser, p.Diffuser); Assert.Equal(.3F, p.Threshold); - Assert.Equal(NamedColors.WebSafePalette, p.Palette); + Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_CorrectProcessorWithColors() { this.operations.Diffuse(this.errorDiffuser, .5F, this.TestPalette); - ErrorDiffusionPaletteProcessor p = this.Verify>(); + ErrorDiffusionPaletteProcessor p = this.Verify(); Assert.Equal(this.errorDiffuser, p.Diffuser); Assert.Equal(.5F, p.Threshold); Assert.Equal(this.TestPalette, p.Palette); @@ -97,7 +107,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors() { this.operations.Diffuse(this.errorDiffuser, .5F, this.TestPalette, this.rect); - ErrorDiffusionPaletteProcessor p = this.Verify>(this.rect); + ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); Assert.Equal(this.errorDiffuser, p.Diffuser); Assert.Equal(.5F, p.Threshold); Assert.Equal(this.TestPalette, p.Palette);