Browse Source

Refactor dithering and quantizers.

af/octree-no-pixelmap
James Jackson-South 6 years ago
parent
commit
eca230340c
  1. 4
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 2
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 85
      src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs
  4. 21
      src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs
  5. 97
      src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs
  6. 21
      src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs
  7. 58
      src/ImageSharp/Processing/KnownDiffusers.cs
  8. 57
      src/ImageSharp/Processing/KnownDitherers.cs
  9. 76
      src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs
  10. 84
      src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs
  11. 58
      src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs
  12. 82
      src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs
  13. 9
      src/ImageSharp/Processing/Processors/Dithering/AtkinsonDither.cs
  14. 9
      src/ImageSharp/Processing/Processors/Dithering/BurksDither.cs
  15. 21
      src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs
  16. 65
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs
  17. 85
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs
  18. 64
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  19. 85
      src/ImageSharp/Processing/Processors/Dithering/EuclideanPixelMap{TPixel}.cs
  20. 9
      src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDither.cs
  21. 43
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  22. 28
      src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs
  23. 27
      src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs
  24. 9
      src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDither.cs
  25. 68
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  26. 6
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs
  27. 40
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs
  28. 83
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs
  29. 28
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs
  30. 139
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  31. 9
      src/ImageSharp/Processing/Processors/Dithering/Sierra2Dither.cs
  32. 9
      src/ImageSharp/Processing/Processors/Dithering/Sierra3Dither.cs
  33. 9
      src/ImageSharp/Processing/Processors/Dithering/SierraLiteDither.cs
  34. 9
      src/ImageSharp/Processing/Processors/Dithering/StevensonArceDither.cs
  35. 9
      src/ImageSharp/Processing/Processors/Dithering/StuckiDither.cs
  36. 166
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs
  37. 10
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  38. 8
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs
  39. 113
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  40. 16
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  41. 83
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  42. 16
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  43. 2
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  44. 2
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs
  45. 716
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  46. 16
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  47. 4
      tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs
  48. 4
      tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs
  49. 12
      tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs
  50. 50
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs
  51. 6
      tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs
  52. 6
      tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs
  53. 6
      tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs

4
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -136,11 +136,11 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileDithering<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
var test = new FloydSteinbergDiffuser();
var test = new FloydSteinbergDither();
TPixel pixel = default;
using (var image = new ImageFrame<TPixel>(Configuration.Default, 1, 1))
{
test.Dither(image, pixel, pixel, 0, 0, 0, 0, 0);
test.Dither(image, default, pixel, pixel, 0, 0, 0);
}
}

2
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
else
{
using (IFrameQuantizer<TPixel> paletteFrameQuantizer =
new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Diffuser, quantized.Palette))
new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Dither, quantized.Palette))
{
using (IQuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame))
{

85
src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs

@ -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
{
/// <summary>
/// Defines extension methods to apply binary diffusion on an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class BinaryDiffuseExtensions
{
/// <summary>
/// Dithers the image reducing it to two colors using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext BinaryDiffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold) =>
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold));
/// <summary>
/// Dithers the image reducing it to two colors using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext BinaryDiffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
Rectangle rectangle) =>
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold), rectangle);
/// <summary>
/// Dithers the image reducing it to two colors using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
/// <param name="lowerColor">The color to use for pixels that are below the threshold</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext BinaryDiffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
Color upperColor,
Color lowerColor) =>
source.ApplyProcessor(new BinaryErrorDiffusionProcessor(diffuser, threshold, upperColor, lowerColor));
/// <summary>
/// Dithers the image reducing it to two colors using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
/// <param name="lowerColor">The color to use for pixels that are below the threshold</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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);
}
}

21
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
/// <param name="dither">The ordered ditherer.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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);
/// <summary>
/// Dithers the image reducing it to two colors using ordered dithering.
@ -32,10 +31,10 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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 }));
/// <summary>
/// Dithers the image reducing it to two colors using ordered dithering.
@ -48,9 +47,9 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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);
/// <summary>
/// Dithers the image reducing it to two colors using ordered dithering.
@ -65,10 +64,10 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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);
}
}
}

97
src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs

@ -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
{
/// <summary>
/// Defines extension methods to apply diffusion to an <see cref="Image"/>
/// using Mutate/Clone.
/// </summary>
public static class DiffuseExtensions
{
/// <summary>
/// Dithers the image reducing it to a web-safe palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(this IImageProcessingContext source) =>
Diffuse(source, KnownDiffusers.FloydSteinberg, .5F);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(this IImageProcessingContext source, float threshold) =>
Diffuse(source, KnownDiffusers.FloydSteinberg, threshold);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold));
/// <summary>
/// Dithers the image reducing it to a web-safe palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
Rectangle rectangle) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
ReadOnlyMemory<Color> palette) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette));
/// <summary>
/// Dithers the image reducing it to the given palette using error diffusion.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="diffuser">The diffusion algorithm to apply.</param>
/// <param name="threshold">The threshold to apply binarization of the image. Must be between 0 and 1.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Diffuse(
this IImageProcessingContext source,
IErrorDiffuser diffuser,
float threshold,
ReadOnlyMemory<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new ErrorDiffusionPaletteProcessor(diffuser, threshold, palette), rectangle);
}
}

21
src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs

@ -1,8 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing
@ -27,8 +26,8 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
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));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
@ -39,9 +38,9 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
IDither dither,
ReadOnlyMemory<Color> palette) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette));
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette));
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
@ -54,9 +53,9 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
IDither dither,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle);
source.ApplyProcessor(new PaletteDitherProcessor(dither), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
@ -70,9 +69,9 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IOrderedDither dither,
IDither dither,
ReadOnlyMemory<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle);
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle);
}
}
}

58
src/ImageSharp/Processing/KnownDiffusers.cs

@ -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
{
/// <summary>
/// Contains reusable static instances of known error diffusion algorithms
/// </summary>
public static class KnownDiffusers
{
/// <summary>
/// Gets the error diffuser that implements the Atkinson algorithm.
/// </summary>
public static IErrorDiffuser Atkinson { get; } = new AtkinsonDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Burks algorithm.
/// </summary>
public static IErrorDiffuser Burks { get; } = new BurksDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Floyd-Steinberg algorithm.
/// </summary>
public static IErrorDiffuser FloydSteinberg { get; } = new FloydSteinbergDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Jarvis-Judice-Ninke algorithm.
/// </summary>
public static IErrorDiffuser JarvisJudiceNinke { get; } = new JarvisJudiceNinkeDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Sierra-2 algorithm.
/// </summary>
public static IErrorDiffuser Sierra2 { get; } = new Sierra2Diffuser();
/// <summary>
/// Gets the error diffuser that implements the Sierra-3 algorithm.
/// </summary>
public static IErrorDiffuser Sierra3 { get; } = new Sierra3Diffuser();
/// <summary>
/// Gets the error diffuser that implements the Sierra-Lite algorithm.
/// </summary>
public static IErrorDiffuser SierraLite { get; } = new SierraLiteDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Stevenson-Arce algorithm.
/// </summary>
public static IErrorDiffuser StevensonArce { get; } = new StevensonArceDiffuser();
/// <summary>
/// Gets the error diffuser that implements the Stucki algorithm.
/// </summary>
public static IErrorDiffuser Stucki { get; } = new StuckiDiffuser();
}
}

57
src/ImageSharp/Processing/KnownDitherers.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -13,21 +13,66 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets the order ditherer using the 2x2 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither2x2 { get; } = new BayerDither2x2();
public static IDither BayerDither2x2 { get; } = new BayerDither2x2();
/// <summary>
/// Gets the order ditherer using the 3x3 dithering matrix
/// </summary>
public static IOrderedDither OrderedDither3x3 { get; } = new OrderedDither3x3();
public static IDither OrderedDither3x3 { get; } = new OrderedDither3x3();
/// <summary>
/// Gets the order ditherer using the 4x4 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither4x4 { get; } = new BayerDither4x4();
public static IDither BayerDither4x4 { get; } = new BayerDither4x4();
/// <summary>
/// Gets the order ditherer using the 8x8 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither8x8 { get; } = new BayerDither8x8();
public static IDither BayerDither8x8 { get; } = new BayerDither8x8();
/// <summary>
/// Gets the error Dither that implements the Atkinson algorithm.
/// </summary>
public static IDither Atkinson { get; } = new AtkinsonDither();
/// <summary>
/// Gets the error Dither that implements the Burks algorithm.
/// </summary>
public static IDither Burks { get; } = new BurksDither();
/// <summary>
/// Gets the error Dither that implements the Floyd-Steinberg algorithm.
/// </summary>
public static IDither FloydSteinberg { get; } = new FloydSteinbergDither();
/// <summary>
/// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm.
/// </summary>
public static IDither JarvisJudiceNinke { get; } = new JarvisJudiceNinkeDither();
/// <summary>
/// Gets the error Dither that implements the Sierra-2 algorithm.
/// </summary>
public static IDither Sierra2 { get; } = new Sierra2Dither();
/// <summary>
/// Gets the error Dither that implements the Sierra-3 algorithm.
/// </summary>
public static IDither Sierra3 { get; } = new Sierra3Dither();
/// <summary>
/// Gets the error Dither that implements the Sierra-Lite algorithm.
/// </summary>
public static IDither SierraLite { get; } = new SierraLiteDither();
/// <summary>
/// Gets the error Dither that implements the Stevenson-Arce algorithm.
/// </summary>
public static IDither StevensonArce { get; } = new StevensonArceDither();
/// <summary>
/// Gets the error Dither that implements the Stucki algorithm.
/// </summary>
public static IDither Stucki { get; } = new StuckiDither();
}
}
}

76
src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs

@ -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
{
/// <summary>
/// Performs binary threshold filtering against an image using error diffusion.
/// </summary>
public class BinaryErrorDiffusionProcessor : IImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser)
: this(diffuser, .5F)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
public BinaryErrorDiffusionProcessor(IErrorDiffuser diffuser, float threshold)
: this(diffuser, threshold, Color.White, Color.Black)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
/// <param name="lowerColor">The color to use for pixels that are below the threshold.</param>
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;
}
/// <summary>
/// Gets the error diffuser.
/// </summary>
public IErrorDiffuser Diffuser { get; }
/// <summary>
/// Gets the threshold value.
/// </summary>
public float Threshold { get; }
/// <summary>
/// Gets the color to use for pixels that are above the threshold.
/// </summary>
public Color UpperColor { get; }
/// <summary>
/// Gets the color to use for pixels that fall below the threshold.
/// </summary>
public Color LowerColor { get; }
/// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel>
=> new BinaryErrorDiffusionProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}

84
src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs

@ -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
{
/// <summary>
/// Performs binary threshold filtering against an image using error diffusion.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class BinaryErrorDiffusionProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly BinaryErrorDiffusionProcessor definition;
/// <summary>
/// Initializes a new instance of the <see cref="BinaryErrorDiffusionProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="BinaryErrorDiffusionProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public BinaryErrorDiffusionProcessor(Configuration configuration, BinaryErrorDiffusionProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
TPixel upperColor = this.definition.UpperColor.ToPixel<TPixel>();
TPixel lowerColor = this.definition.LowerColor.ToPixel<TPixel>();
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<TPixel> 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);
}
}
}
}
}

58
src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs

@ -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
{
/// <summary>
/// Defines a binary threshold filtering using ordered dithering.
/// </summary>
public class BinaryOrderedDitherProcessor : IImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
public BinaryOrderedDitherProcessor(IOrderedDither dither)
: this(dither, Color.White, Color.Black)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="upperColor">The color to use for pixels that are above the threshold.</param>
/// <param name="lowerColor">The color to use for pixels that are below the threshold.</param>
public BinaryOrderedDitherProcessor(IOrderedDither dither, Color upperColor, Color lowerColor)
{
this.Dither = dither ?? throw new ArgumentNullException(nameof(dither));
this.UpperColor = upperColor;
this.LowerColor = lowerColor;
}
/// <summary>
/// Gets the ditherer.
/// </summary>
public IOrderedDither Dither { get; }
/// <summary>
/// Gets the color to use for pixels that are above the threshold.
/// </summary>
public Color UpperColor { get; }
/// <summary>
/// Gets the color to use for pixels that fall below the threshold.
/// </summary>
public Color LowerColor { get; }
/// <inheritdoc />
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel>
=> new BinaryOrderedDitherProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}

82
src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs

@ -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
{
/// <summary>
/// Performs binary threshold filtering against an image using ordered dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class BinaryOrderedDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly BinaryOrderedDitherProcessor definition;
/// <summary>
/// Initializes a new instance of the <see cref="BinaryOrderedDitherProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="BinaryErrorDiffusionProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public BinaryOrderedDitherProcessor(Configuration configuration, BinaryOrderedDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.definition = definition;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
IOrderedDither dither = this.definition.Dither;
TPixel upperColor = this.definition.UpperColor.ToPixel<TPixel>();
TPixel lowerColor = this.definition.LowerColor.ToPixel<TPixel>();
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<TPixel> 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);
}
}
}
}
}

9
src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/AtkinsonDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Atkinson image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class AtkinsonDiffuser : ErrorDiffuser
public sealed class AtkinsonDither : ErrorDither
{
private const float Divisor = 8F;
private const int Offset = 1;
/// <summary>
/// The diffusion matrix
@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="AtkinsonDiffuser"/> class.
/// Initializes a new instance of the <see cref="AtkinsonDither"/> class.
/// </summary>
public AtkinsonDiffuser()
: base(AtkinsonMatrix)
public AtkinsonDither()
: base(AtkinsonMatrix, Offset)
{
}
}

9
src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/BurksDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Burks image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class BurksDiffuser : ErrorDiffuser
public sealed class BurksDither : ErrorDither
{
private const float Divisor = 32F;
private const int Offset = 2;
/// <summary>
/// The diffusion matrix
@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="BurksDiffuser"/> class.
/// Initializes a new instance of the <see cref="BurksDither"/> class.
/// </summary>
public BurksDiffuser()
: base(BurksMatrix)
public BurksDither()
: base(BurksMatrix, Offset)
{
}
}

21
src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Enumerates the possible dithering algorithm transform behaviors.
/// </summary>
public enum DitherTransformColorBehavior
{
/// <summary>
/// The transformed color should be precalulated and passed to the dithering algorithm.
/// </summary>
PreOperation,
/// <summary>
/// The transformed color should be calculated as a result of the dithering algorithm.
/// </summary>
PostOperation
}
}

65
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs

@ -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
{
/// <summary>
/// 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.
/// </summary>
public sealed class ErrorDiffusionPaletteProcessor : PaletteDitherProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser)
: this(diffuser, .5F)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold)
: this(diffuser, threshold, Color.WebSafePalette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor"/> class.
/// </summary>
/// <param name="diffuser">The error diffuser</param>
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
public ErrorDiffusionPaletteProcessor(IErrorDiffuser diffuser, float threshold, ReadOnlyMemory<Color> palette)
: base(palette)
{
Guard.NotNull(diffuser, nameof(diffuser));
Guard.MustBeBetweenOrEqualTo(threshold, 0, 1, nameof(threshold));
this.Diffuser = diffuser;
this.Threshold = threshold;
}
/// <summary>
/// Gets the error diffuser.
/// </summary>
public IErrorDiffuser Diffuser { get; }
/// <summary>
/// Gets the threshold value.
/// </summary>
public float Threshold { get; }
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
{
return new ErrorDiffusionPaletteProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}
}

85
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs

@ -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
{
/// <summary>
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class ErrorDiffusionPaletteProcessor<TPixel> : PaletteDitherProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffusionPaletteProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="ErrorDiffusionPaletteProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public ErrorDiffusionPaletteProcessor(Configuration configuration, ErrorDiffusionPaletteProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, definition, source, sourceRectangle)
{
}
private new ErrorDiffusionPaletteProcessor Definition => (ErrorDiffusionPaletteProcessor)base.Definition;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> 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<TPixel> 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<TPixel> 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);
}
}
}
}
}

64
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -3,78 +3,60 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// The base class for performing error diffusion based dithering.
/// The base class of all error diffusion dithering implementations.
/// </summary>
public abstract class ErrorDiffuser : IErrorDiffuser
public abstract class ErrorDither : IDither
{
private readonly int offset;
private readonly DenseMatrix<float> matrix;
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffuser"/> class.
/// Initializes a new instance of the <see cref="ErrorDither"/> class.
/// </summary>
/// <param name="matrix">The dithering matrix.</param>
internal ErrorDiffuser(in DenseMatrix<float> matrix)
/// <param name="matrix">The diffusion matrix.</param>
/// <param name="offset">The starting offset within the matrix.</param>
protected ErrorDither(in DenseMatrix<float> matrix, int offset)
{
// Calculate the offset position of the pixel relative to
// the diffusion matrix.
this.offset = 0;
for (int col = 0; col < matrix.Columns; col++)
{
if (matrix[0, col] != 0)
{
this.offset = col - 1;
break;
}
}
this.matrix = matrix;
this.offset = offset;
}
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY)
/// <inheritdoc/>
public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PreOperation;
/// <inheritdoc/>
public TPixel Dither<TPixel>(
ImageFrame<TPixel> image,
Rectangle bounds,
TPixel source,
TPixel transformed,
int x,
int y,
int bitDepth)
where TPixel : struct, IPixel<TPixel>
{
image[x, y] = transformed;
// Equal? Break out as there's no error to pass.
if (source.Equals(transformed))
{
return;
return transformed;
}
// Calculate the error
Vector4 error = source.ToVector4() - transformed.ToVector4();
if (Vector4.Dot(error, error) > 16F / 255F)
{
error *= .75F;
}
this.DoDither(image, x, y, minX, maxX, maxY, error);
}
[MethodImpl(InliningOptions.ShortMethod)]
private void DoDither<TPixel>(ImageFrame<TPixel> image, int x, int y, int minX, int maxX, int maxY, Vector4 error)
where TPixel : struct, IPixel<TPixel>
{
int offset = this.offset;
DenseMatrix<float> matrix = this.matrix;
// Loop through and distribute the error amongst neighboring pixels.
for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++)
{
// TODO: Quantize rectangle.
if (targetY >= maxY)
if (targetY >= bounds.Bottom)
{
continue;
}
@ -84,7 +66,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
for (int col = 0; col < matrix.Columns; col++)
{
int targetX = x + (col - offset);
if (targetX < minX || targetX >= maxX)
if (targetX < bounds.Left || targetX >= bounds.Right)
{
continue;
}
@ -102,6 +84,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
pixel.FromVector4(result);
}
}
return transformed;
}
}
}

85
src/ImageSharp/Processing/Processors/Dithering/EuclideanPixelMap{TPixel}.cs

@ -0,0 +1,85 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Gets the closest color to the supplied color based upon the Eucladean distance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class EuclideanPixelMap<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly ReadOnlyMemory<TPixel> palette;
private readonly ConcurrentDictionary<int, Vector4> vectorCache = new ConcurrentDictionary<int, Vector4>();
private readonly ConcurrentDictionary<TPixel, byte> distanceCache = new ConcurrentDictionary<TPixel, byte>();
public EuclideanPixelMap(ReadOnlyMemory<TPixel> palette)
{
this.palette = palette;
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
for (int i = 0; i < paletteSpan.Length; i++)
{
this.vectorCache[i] = paletteSpan[i].ToScaledVector4();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetClosestColor(TPixel color, out TPixel match)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
// Check if the color is in the lookup table
if (this.distanceCache.TryGetValue(color, out byte index))
{
match = paletteSpan[index];
return index;
}
return this.GetClosestColorSlow(color, paletteSpan, out match);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private byte GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
Vector4 vector = color.ToScaledVector4();
for (int i = 0; i < palette.Length; i++)
{
Vector4 candidate = this.vectorCache[i];
float distance = Vector4.DistanceSquared(vector, candidate);
// Greater... Move on.
if (leastDistance < distance)
{
continue;
}
index = i;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance == 0)
{
break;
}
}
// Now I have the index, pop it into the cache for next time
var result = (byte)index;
this.distanceCache[color] = result;
match = palette[index];
return result;
}
}
}

9
src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class FloydSteinbergDiffuser : ErrorDiffuser
public sealed class FloydSteinbergDither : ErrorDither
{
private const float Divisor = 16F;
private const int Offset = 1;
/// <summary>
/// The diffusion matrix
@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="FloydSteinbergDiffuser"/> class.
/// Initializes a new instance of the <see cref="FloydSteinbergDither"/> class.
/// </summary>
public FloydSteinbergDiffuser()
: base(FloydSteinbergMatrix)
public FloydSteinbergDither()
: base(FloydSteinbergMatrix, Offset)
{
}
}

43
src/ImageSharp/Processing/Processors/Dithering/IDither.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Defines the contract for types that apply dithering to images.
/// </summary>
public interface IDither
{
/// <summary>
/// Gets the <see cref="DitherTransformColorBehavior"/> which determines whether the
/// transformed color should be calculated and supplied to the algorithm.
/// </summary>
public DitherTransformColorBehavior TransformColorBehavior { get; }
/// <summary>
/// Transforms the image applying a dither matrix.
/// When <see cref="TransformColorBehavior"/> is <see cref="DitherTransformColorBehavior.PreOperation"/> this
/// this method is destructive and will alter the input pixels.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="bounds">The region of interest bounds.</param>
/// <param name="source">The source pixel</param>
/// <param name="transformed">The transformed pixel</param>
/// <param name="x">The column index.</param>
/// <param name="y">The row index.</param>
/// <param name="bitDepth">The bit depth of the target palette.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The dithered result for the source pixel.</returns>
TPixel Dither<TPixel>(
ImageFrame<TPixel> image,
Rectangle bounds,
TPixel source,
TPixel transformed,
int x,
int y,
int bitDepth)
where TPixel : struct, IPixel<TPixel>;
}
}

28
src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs

@ -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
{
/// <summary>
/// Encapsulates properties and methods required to perform diffused error dithering on an image.
/// </summary>
public interface IErrorDiffuser
{
/// <summary>
/// Transforms the image applying the dither matrix. This method alters the input pixels array
/// </summary>
/// <param name="image">The image</param>
/// <param name="source">The source pixel</param>
/// <param name="transformed">The transformed pixel</param>
/// <param name="x">The column index.</param>
/// <param name="y">The row index.</param>
/// <param name="minX">The minimum column value.</param>
/// <param name="maxX">The maximum column value.</param>
/// <param name="maxY">The maximum row value.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel transformed, int x, int y, int minX, int maxX, int maxY)
where TPixel : struct, IPixel<TPixel>;
}
}

27
src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs

@ -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
{
/// <summary>
/// Encapsulates properties and methods required to perform ordered dithering on an image.
/// </summary>
public interface IOrderedDither
{
/// <summary>
/// Transforms the image applying the dither matrix. This method alters the input pixels array
/// </summary>
/// <param name="image">The image</param>
/// <param name="source">The source pixel</param>
/// <param name="upper">The color to apply to the pixels above the threshold.</param>
/// <param name="lower">The color to apply to the pixels below the threshold.</param>
/// <param name="threshold">The threshold to split the image. Must be between 0 and 1.</param>
/// <param name="x">The column index.</param>
/// <param name="y">The row index.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y)
where TPixel : struct, IPixel<TPixel>;
}
}

9
src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the JarvisJudiceNinke image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class JarvisJudiceNinkeDiffuser : ErrorDiffuser
public sealed class JarvisJudiceNinkeDither : ErrorDither
{
private const float Divisor = 48F;
private const int Offset = 2;
/// <summary>
/// The diffusion matrix
@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="JarvisJudiceNinkeDiffuser"/> class.
/// Initializes a new instance of the <see cref="JarvisJudiceNinkeDither"/> class.
/// </summary>
public JarvisJudiceNinkeDiffuser()
: base(JarvisJudiceNinkeMatrix)
public JarvisJudiceNinkeDither()
: base(JarvisJudiceNinkeMatrix, Offset)
{
}
}

68
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
@ -8,9 +9,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <summary>
/// An ordered dithering matrix with equal sides of arbitrary length
/// </summary>
public class OrderedDither : IOrderedDither
public class OrderedDither : IDither
{
private readonly DenseMatrix<uint> thresholdMatrix;
private readonly DenseMatrix<float> thresholdMatrix;
private readonly int modulusX;
private readonly int modulusY;
@ -21,29 +22,62 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public OrderedDither(uint length)
{
DenseMatrix<uint> ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length);
this.modulusX = ditherMatrix.Columns;
this.modulusY = ditherMatrix.Rows;
// Adjust the matrix range for 0-255
// TODO: It looks like it's actually possible to dither an image using it's own colors. We should investigate for V2
// https://stackoverflow.com/questions/12422407/monochrome-dithering-in-javascript-bayer-atkinson-floyd-steinberg
int multiplier = 256 / ditherMatrix.Count;
for (int y = 0; y < ditherMatrix.Rows; y++)
// Create a new matrix to run against, that pre-thresholds the values.
// We don't want to adjust the original matrix generation code as that
// creates known, easy to test values.
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
var thresholdMatrix = new DenseMatrix<float>((int)length);
float m2 = length * length;
for (int y = 0; y < length; y++)
{
for (int x = 0; x < ditherMatrix.Columns; x++)
for (int x = 0; x < length; y++)
{
ditherMatrix[y, x] = (uint)((ditherMatrix[y, x] + 1) * multiplier) - 1;
thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F;
}
}
this.thresholdMatrix = ditherMatrix;
this.modulusX = ditherMatrix.Columns;
this.modulusY = ditherMatrix.Rows;
this.thresholdMatrix = thresholdMatrix;
}
/// <inheritdoc />
public void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y)
/// <inheritdoc/>
public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PostOperation;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel Dither<TPixel>(
ImageFrame<TPixel> image,
Rectangle bounds,
TPixel source,
TPixel transformed,
int x,
int y,
int bitDepth)
where TPixel : struct, IPixel<TPixel>
{
image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper;
// TODO: Should we consider a pixel format with a larger coror range?
Rgba32 rgba = default;
source.ToRgba32(ref rgba);
Rgba32 attempt;
// Srpead assumes an even colorspace distribution and precision.
// Calculated as 0-255/component count. 256 / bitDepth
// https://bisqwit.iki.fi/story/howto/dither/jy/
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
int spread = 256 / bitDepth;
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX];
attempt.R = (byte)(rgba.R + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.G = (byte)(rgba.G + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.B = (byte)(rgba.B + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.A = (byte)(rgba.A + factor).Clamp(byte.MinValue, byte.MaxValue);
TPixel result = default;
result.FromRgba32(attempt);
return result;
}
}
}
}

6
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;
}
}
}
}

40
src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs

@ -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
{
/// <summary>
/// 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.
/// </summary>
public sealed class OrderedDitherPaletteProcessor : PaletteDitherProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
public OrderedDitherPaletteProcessor(IOrderedDither dither)
: this(dither, Color.WebSafePalette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
public OrderedDitherPaletteProcessor(IOrderedDither dither, ReadOnlyMemory<Color> palette)
: base(palette) => this.Dither = dither ?? throw new ArgumentNullException(nameof(dither));
/// <summary>
/// Gets the ditherer.
/// </summary>
public IOrderedDither Dither { get; }
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
=> new OrderedDitherPaletteProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}

83
src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs

@ -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
{
/// <summary>
/// An <see cref="IImageProcessor{TPixel}"/> that dithers an image using error diffusion.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class OrderedDitherPaletteProcessor<TPixel> : PaletteDitherProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDitherPaletteProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="OrderedDitherPaletteProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public OrderedDitherPaletteProcessor(Configuration configuration, OrderedDitherPaletteProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, definition, source, sourceRectangle)
{
}
private new OrderedDitherPaletteProcessor Definition => (OrderedDitherPaletteProcessor)base.Definition;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> 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<TPixel> 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<TPixel> 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);
}
}
}
}
}

28
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs

@ -2,32 +2,48 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// The base class for dither and diffusion processors that consume a palette.
/// Allows the consumption a palette to dither an image.
/// </summary>
public abstract class PaletteDitherProcessor : IImageProcessor
public sealed class PaletteDitherProcessor : IImageProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
public PaletteDitherProcessor(IDither dither)
: this(dither, Color.WebSafePalette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The dithering algorithm.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
protected PaletteDitherProcessor(ReadOnlyMemory<Color> palette)
public PaletteDitherProcessor(IDither dither, ReadOnlyMemory<Color> palette)
{
this.Dither = dither ?? throw new ArgumentNullException(nameof(dither));
this.Palette = palette;
}
/// <summary>
/// Gets the dithering algorithm.
/// </summary>
public IDither Dither { get; }
/// <summary>
/// Gets the palette to select substitute colors from.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public abstract IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel>;
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : struct, IPixel<TPixel>
=> new PaletteDitherProcessor<TPixel>(configuration, this, source, sourceRectangle);
}
}

139
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -3,25 +3,24 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// The base class for dither and diffusion processors that consume a palette.
/// Allows the consumption a palette to dither an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Dictionary<TPixel, PixelPair<TPixel>> cache = new Dictionary<TPixel, PixelPair<TPixel>>();
private readonly int paletteLength;
private readonly int bitDepth;
private readonly IDither dither;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private IMemoryOwner<Vector4> paletteVector;
private bool palleteVectorMapped;
private EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
/// <summary>
@ -31,34 +30,67 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <param name="definition">The <see cref="PaletteDitherProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
protected PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.Definition = definition;
this.palette = this.Configuration.MemoryAllocator.Allocate<TPixel>(definition.Palette.Length);
this.paletteVector = this.Configuration.MemoryAllocator.Allocate<Vector4>(definition.Palette.Length);
this.paletteLength = definition.Palette.Span.Length;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(this.paletteLength);
this.dither = definition.Dither;
this.sourcePalette = definition.Palette;
}
protected PaletteDitherProcessor Definition { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
// Error diffusion. The difference between the source and transformed color
// is spread to neighboring pixels.
if (this.dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation)
{
for (int y = interest.Top; y < interest.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = interest.Left; x < interest.Right; x++)
{
TPixel sourcePixel = row[x];
this.pixelMap.GetClosestColor(sourcePixel, out TPixel transformed);
this.dither.Dither(source, interest, sourcePixel, transformed, x, y, this.bitDepth);
row[x] = transformed;
}
}
return;
}
// TODO: This can be parallel.
// Ordered dithering. We are only operating on a single pixel.
for (int y = interest.Top; y < interest.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = interest.Left; x < interest.Right; x++)
{
TPixel dithered = this.dither.Dither(source, interest, row[x], default, x, y, this.bitDepth);
this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
row[x] = transformed;
}
}
}
/// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source)
{
// Lazy init palettes:
if (!this.palleteVectorMapped)
if (this.pixelMap is null)
{
ReadOnlySpan<Color> sourcePalette = this.Definition.Palette.Span;
this.palette = this.Configuration.MemoryAllocator.Allocate<TPixel>(this.paletteLength);
ReadOnlySpan<Color> sourcePalette = this.sourcePalette.Span;
Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span);
PixelOperations<TPixel>.Instance.ToVector4(
this.Configuration,
this.palette.Memory.Span,
this.paletteVector.Memory.Span,
PixelConversionModifiers.Scale);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.palette.Memory);
}
this.palleteVectorMapped = true;
base.BeforeFrameApply(source);
}
@ -73,71 +105,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
if (disposing)
{
this.palette?.Dispose();
this.paletteVector?.Dispose();
}
this.palette = null;
this.paletteVector = null;
this.isDisposed = true;
base.Dispose(disposing);
}
/// <summary>
/// Returns the two closest colors from the palette calculated via Euclidean distance in the Rgba space.
/// </summary>
/// <param name="pixel">The source color to match.</param>
/// <returns>The <see cref="PixelPair{TPixel}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected PixelPair<TPixel> GetClosestPixelPair(ref TPixel pixel)
{
// Check if the color is in the lookup table
if (this.cache.TryGetValue(pixel, out PixelPair<TPixel> value))
{
return value;
}
return this.GetClosestPixelPairSlow(ref pixel);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private PixelPair<TPixel> 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<TPixel> paletteSpan = this.palette.Memory.Span;
ref TPixel paletteSpanBase = ref MemoryMarshal.GetReference(paletteSpan);
Span<Vector4> 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<TPixel>(closest, secondClosest);
this.cache.Add(pixel, pair);
return pair;
}
}
}

9
src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs → src/ImageSharp/Processing/Processors/Dithering/Sierra2Dither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Sierra2 image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class Sierra2Diffuser : ErrorDiffuser
public sealed class Sierra2Dither : ErrorDither
{
private const float Divisor = 16F;
private const int Offset = 2;
/// <summary>
/// The diffusion matrix
@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="Sierra2Diffuser"/> class.
/// Initializes a new instance of the <see cref="Sierra2Dither"/> class.
/// </summary>
public Sierra2Diffuser()
: base(Sierra2Matrix)
public Sierra2Dither()
: base(Sierra2Matrix, Offset)
{
}
}

9
src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs → src/ImageSharp/Processing/Processors/Dithering/Sierra3Dither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Sierra3 image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class Sierra3Diffuser : ErrorDiffuser
public sealed class Sierra3Dither : ErrorDither
{
private const float Divisor = 32F;
private const int Offset = 2;
/// <summary>
/// The diffusion matrix
@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="Sierra3Diffuser"/> class.
/// Initializes a new instance of the <see cref="Sierra3Dither"/> class.
/// </summary>
public Sierra3Diffuser()
: base(Sierra3Matrix)
public Sierra3Dither()
: base(Sierra3Matrix, Offset)
{
}
}

9
src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/SierraLiteDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the SierraLite image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class SierraLiteDiffuser : ErrorDiffuser
public sealed class SierraLiteDither : ErrorDither
{
private const float Divisor = 4F;
private const int Offset = 1;
/// <summary>
/// The diffusion matrix
@ -22,10 +23,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="SierraLiteDiffuser"/> class.
/// Initializes a new instance of the <see cref="SierraLiteDither"/> class.
/// </summary>
public SierraLiteDiffuser()
: base(SierraLiteMatrix)
public SierraLiteDither()
: base(SierraLiteMatrix, Offset)
{
}
}

9
src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/StevensonArceDither.cs

@ -6,9 +6,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <summary>
/// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm.
/// </summary>
public sealed class StevensonArceDiffuser : ErrorDiffuser
public sealed class StevensonArceDither : ErrorDither
{
private const float Divisor = 200F;
private const int Offset = 3;
/// <summary>
/// The diffusion matrix
@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="StevensonArceDiffuser"/> class.
/// Initializes a new instance of the <see cref="StevensonArceDither"/> class.
/// </summary>
public StevensonArceDiffuser()
: base(StevensonArceMatrix)
public StevensonArceDither()
: base(StevensonArceMatrix, Offset)
{
}
}

9
src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs → src/ImageSharp/Processing/Processors/Dithering/StuckiDither.cs

@ -7,9 +7,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Applies error diffusion based dithering using the Stucki image dithering algorithm.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public sealed class StuckiDiffuser : ErrorDiffuser
public sealed class StuckiDither : ErrorDither
{
private const float Divisor = 42F;
private const int Offset = 2;
/// <summary>
/// The diffusion matrix
@ -23,10 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
};
/// <summary>
/// Initializes a new instance of the <see cref="StuckiDiffuser"/> class.
/// Initializes a new instance of the <see cref="StuckiDither"/> class.
/// </summary>
public StuckiDiffuser()
: base(StuckiMatrix)
public StuckiDither()
: base(StuckiMatrix, Offset)
{
}
}

166
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs

@ -2,11 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -20,21 +17,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public abstract class FrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// A lookup table for colors
/// </summary>
private readonly Dictionary<TPixel, byte> distanceCache = new Dictionary<TPixel, byte>();
/// <summary>
/// Flag used to indicate whether a single pass or two passes are needed for quantization.
/// </summary>
private readonly bool singlePass;
/// <summary>
/// The vector representation of the image palette.
/// </summary>
private IMemoryOwner<Vector4> paletteVector;
private EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
/// <summary>
@ -55,8 +43,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Guard.NotNull(quantizer, nameof(quantizer));
this.Configuration = configuration;
this.Diffuser = quantizer.Diffuser;
this.Dither = this.Diffuser != null;
this.Dither = quantizer.Dither;
this.DoDither = this.Dither != null;
this.singlePass = singlePass;
}
@ -73,19 +61,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// only call the <see cref="SecondPass(ImageFrame{TPixel}, Span{byte}, ReadOnlySpan{TPixel}, int, int)"/> method.
/// If two passes are required, the code will also call <see cref="FirstPass(ImageFrame{TPixel}, int, int)"/>.
/// </remarks>
protected FrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, bool singlePass)
protected FrameQuantizer(Configuration configuration, IDither diffuser, bool singlePass)
{
this.Configuration = configuration;
this.Diffuser = diffuser;
this.Dither = this.Diffuser != null;
this.Dither = diffuser;
this.DoDither = this.Dither != null;
this.singlePass = singlePass;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
public IDither Dither { get; }
/// <inheritdoc />
public bool Dither { get; }
public bool DoDither { get; }
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.
@ -119,18 +107,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
// Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = this.GetPalette();
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
this.paletteVector = memoryAllocator.Allocate<Vector4>(palette.Length);
PixelOperations<TPixel>.Instance.ToVector4(
this.Configuration,
palette.Span,
this.paletteVector.Memory.Span,
PixelConversionModifiers.Scale);
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, width, height, palette);
Span<byte> pixelSpan = quantizedFrame.GetWritablePixelSpan();
if (this.Dither)
if (this.DoDither)
{
// We clone the image as we don't want to alter the original via dithering.
using (ImageFrame<TPixel> clone = image.Clone())
@ -157,13 +139,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return;
}
if (disposing)
{
this.paletteVector?.Dispose();
}
this.paletteVector = null;
this.isDisposed = true;
}
@ -178,22 +153,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <summary>
/// Returns the closest color from the palette to the given color by calculating the
/// Euclidean distance in the Rgba colorspace.
/// Returns the index and color from the quantized palette corresponding to the give to the given color.
/// </summary>
/// <param name="pixel">The color.</param>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected byte GetClosestPixel(ref TPixel pixel)
{
// Check if the color is in the lookup table
if (this.distanceCache.TryGetValue(pixel, out byte value))
{
return value;
}
return this.GetClosestPixelSlow(ref pixel);
}
[MethodImpl(InliningOptions.ShortMethod)]
protected virtual byte GetQuantizedColor(TPixel color, out TPixel match)
=> this.pixelMap.GetClosestColor(color, out match);
/// <summary>
/// Retrieve the palette for the quantized image.
@ -203,32 +170,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </returns>
protected abstract ReadOnlyMemory<TPixel> GetPalette();
/// <summary>
/// Returns the index of the first instance of the transparent color in the palette.
/// </summary>
/// <returns>The <see cref="int"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected byte GetTransparentIndex()
{
// Transparent pixels are much more likely to be found at the end of a palette.
Span<Vector4> 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;
}
/// <summary>
/// Execute a second pass through the image to assign the pixels to a palette entry.
/// </summary>
@ -237,49 +178,66 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The output color palette.</param>
/// <param name="width">The width in pixels of the image.</param>
/// <param name="height">The height in pixels of the image.</param>
protected abstract void SecondPass(
protected virtual void SecondPass(
ImageFrame<TPixel> source,
Span<byte> output,
ReadOnlySpan<TPixel> palette,
int width,
int height);
[MethodImpl(MethodImplOptions.NoInlining)]
private byte GetClosestPixelSlow(ref TPixel pixel)
int height)
{
// Loop through the palette and find the nearest match.
int colorIndex = 0;
float leastDistance = float.MaxValue;
Vector4 vector = pixel.ToScaledVector4();
float epsilon = Constants.EpsilonSquared;
Span<Vector4> paletteVectorSpan = this.paletteVector.Memory.Span;
ref Vector4 paletteVectorSpanBase = ref MemoryMarshal.GetReference(paletteVectorSpan);
Rectangle interest = source.Bounds();
int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length);
for (int index = 0; index < paletteVectorSpan.Length; index++)
if (!this.DoDither)
{
ref Vector4 candidate = ref Unsafe.Add(ref paletteVectorSpanBase, index);
float distance = Vector4.DistanceSquared(vector, candidate);
// Greater... Move on.
if (!(distance < leastDistance))
// TODO: This can be parallel.
for (int y = interest.Top; y < interest.Bottom; y++)
{
continue;
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
for (int x = interest.Left; x < interest.Right; x++)
{
output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _);
}
}
colorIndex = index;
leastDistance = distance;
return;
}
// And if it's an exact match, exit the loop
if (distance < epsilon)
// Error diffusion. The difference between the source and transformed color
// is spread to neighboring pixels.
if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation)
{
for (int y = interest.Top; y < interest.Bottom; y++)
{
break;
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
for (int x = interest.Left; x < interest.Right; x++)
{
TPixel sourcePixel = row[x];
output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth);
}
}
return;
}
// Now I have the index, pop it into the cache for next time
byte result = (byte)colorIndex;
this.distanceCache.Add(pixel, result);
return result;
// TODO: This can be parallel.
// Ordered dithering. We are only operating on a single pixel.
for (int y = interest.Top; y < interest.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
for (int x = interest.Left; x < interest.Right; x++)
{
TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth);
output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _);
}
}
}
}
}

10
src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -17,12 +17,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Gets a value indicating whether to apply dithering to the output image.
/// </summary>
bool Dither { get; }
bool DoDither { get; }
/// <summary>
/// Gets the error diffusion algorithm to apply to the output image.
/// Gets the algorithm to apply to the output image.
/// </summary>
IErrorDiffuser Diffuser { get; }
IDither Dither { get; }
/// <summary>
/// Quantize an image frame and return the resulting output pixels.
@ -33,4 +33,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </returns>
IQuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image);
}
}
}

8
src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public interface IQuantizer
{
/// <summary>
/// Gets the error diffusion algorithm to apply to the output image.
/// Gets the dithering algorithm to apply to the output image.
/// </summary>
IErrorDiffuser Diffuser { get; }
IDither Dither { get; }
/// <summary>
/// Creates the generic frame quantizer
@ -35,4 +35,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
where TPixel : struct, IPixel<TPixel>;
}
}
}

113
src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs

@ -29,10 +29,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private readonly Octree octree;
/// <summary>
/// The transparent index
/// </summary>
private byte transparentIndex;
private TPixel[] palette;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
@ -86,92 +83,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <inheritdoc/>
protected override void SecondPass(
ImageFrame<TPixel> source,
Span<byte> output,
ReadOnlySpan<TPixel> palette,
int width,
int height)
[MethodImpl(InliningOptions.ShortMethod)]
protected override byte GetQuantizedColor(TPixel color, out TPixel match)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
this.transparentIndex = this.GetTransparentIndex();
byte pixelValue = this.QuantizePixel(ref sourcePixel);
TPixel transformedPixel = palette[pixelValue];
for (int y = 0; y < height; y++)
if (!this.DoDither)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
// And loop through each column
for (int x = 0; x < width; x++)
{
// Get the pixel.
sourcePixel = row[x];
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
{
// Quantize the pixel
pixelValue = this.QuantizePixel(ref sourcePixel);
// And setup the previous pointer
previousPixel = sourcePixel;
if (this.Dither)
{
transformedPixel = palette[pixelValue];
}
}
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height);
}
output[(y * source.Width) + x] = pixelValue;
}
var index = (byte)this.octree.GetPaletteIndex(ref color);
match = this.GetPalette().Span[index];
return index;
}
return base.GetQuantizedColor(color, out match);
}
internal ReadOnlyMemory<TPixel> AotGetPalette() => this.GetPalette();
/// <inheritdoc/>
protected override ReadOnlyMemory<TPixel> GetPalette() => this.octree.Palletize(this.colors);
/// <summary>
/// Process the pixel in the second pass of the algorithm.
/// </summary>
/// <param name="pixel">The pixel to quantize.</param>
/// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte QuantizePixel(ref TPixel pixel)
{
if (this.Dither)
{
// The colors have changed so we need to use Euclidean distance calculation to
// find the closest value.
return this.GetClosestPixel(ref pixel);
}
Rgba32 rgba = default;
pixel.ToRgba32(ref rgba);
if (rgba.Equals(default))
{
return this.transparentIndex;
}
return (byte)this.octree.GetPaletteIndex(ref pixel);
}
[MethodImpl(InliningOptions.ShortMethod)]
protected override ReadOnlyMemory<TPixel> GetPalette()
=> this.palette ?? (this.palette = this.octree.Palletize(this.colors));
/// <summary>
/// Class which does the actual quantization
/// </summary>
private class Octree
private sealed class Octree
{
/// <summary>
/// Mask used when getting the appropriate pixels for a given node
@ -220,10 +155,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public int Leaves
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
set;
}
@ -232,7 +167,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private OctreeNode[] ReducibleNodes
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
@ -272,7 +207,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount)
{
while (this.Leaves > colorCount - 1)
@ -297,7 +232,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>
/// The <see cref="int"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public int GetPaletteIndex(ref TPixel pixel) => this.root.GetPaletteIndex(ref pixel, 0);
/// <summary>
@ -306,8 +241,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="node">
/// The node last quantized
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void TrackPrevious(OctreeNode node) => this.previousNode = node;
[MethodImpl(InliningOptions.ShortMethod)]
public void TrackPrevious(OctreeNode node) => this.previousNode = node;
/// <summary>
/// Reduce the depth of the tree
@ -336,7 +271,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Class which encapsulates each node in the tree
/// </summary>
protected class OctreeNode
public sealed class OctreeNode
{
/// <summary>
/// Pointers to any child nodes
@ -414,7 +349,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public OctreeNode NextReducible
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
@ -530,6 +465,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(MethodImplOptions.NoInlining)]
public int GetPaletteIndex(ref TPixel pixel, int level)
{
// TODO: pass index around so we can do this in parallel.
int index = this.paletteIndex;
if (!this.leaf)
@ -549,6 +485,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
else
{
// TODO: Throw helper.
throw new Exception($"Cannot retrieve a pixel at the given index {pixelIndex}.");
}
}
@ -560,7 +497,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Increment the pixel count and add to the color information
/// </summary>
/// <param name="pixel">The pixel to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public void Increment(ref TPixel pixel)
{
Rgba32 rgba = default;

16
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Allows the quantization of images pixels using Octrees.
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
/// <para>
/// By default the quantizer uses <see cref="KnownDiffusers.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// By default the quantizer uses <see cref="KnownDitherers.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// </para>
/// </summary>
public class OctreeQuantizer : IQuantizer
@ -54,8 +54,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image.</param>
public OctreeQuantizer(IErrorDiffuser diffuser)
/// <param name="diffuser">The dithering algorithm, if any, to apply to the output image.</param>
public OctreeQuantizer(IDither diffuser)
: this(diffuser, QuantizerConstants.MaxColors)
{
}
@ -63,16 +63,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image.</param>
/// <param name="dither">The dithering algorithm, if any, to apply to the output image.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors)
public OctreeQuantizer(IDither dither, int maxColors)
{
this.Diffuser = diffuser;
this.Dither = dither;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
public IDither Dither { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
@ -93,6 +93,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return new OctreeFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null;
}
}

83
src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs

@ -3,7 +3,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -29,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="diffuser">The palette quantizer.</param>
/// <param name="colors">An array of all colors in the palette.</param>
public PaletteFrameQuantizer(Configuration configuration, IErrorDiffuser diffuser, ReadOnlyMemory<TPixel> colors)
public PaletteFrameQuantizer(Configuration configuration, IDither diffuser, ReadOnlyMemory<TPixel> colors)
: base(configuration, diffuser, true) => this.palette = colors;
/// <inheritdoc/>
@ -40,47 +39,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int width,
int height)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(ref sourcePixel);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette);
TPixel transformedPixel = Unsafe.Add(ref paletteRef, pixelValue);
Rectangle interest = source.Bounds();
int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length);
for (int y = 0; y < height; y++)
if (!this.DoDither)
{
ref TPixel rowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
// And loop through each column
for (int x = 0; x < width; x++)
// TODO: This can be parallel.
for (int y = interest.Top; y < interest.Bottom; y++)
{
// Get the pixel.
sourcePixel = Unsafe.Add(ref rowRef, x);
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
for (int x = interest.Left; x < interest.Right; x++)
{
// Quantize the pixel
pixelValue = this.QuantizePixel(ref sourcePixel);
output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _);
}
}
// And setup the previous pointer
previousPixel = sourcePixel;
return;
}
if (this.Dither)
{
transformedPixel = Unsafe.Add(ref paletteRef, pixelValue);
}
}
// Error diffusion. The difference between the source and transformed color
// is spread to neighboring pixels.
if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation)
{
for (int y = interest.Top; y < interest.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
if (this.Dither)
for (int x = interest.Left; x < interest.Right; x++)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height);
TPixel sourcePixel = row[x];
output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth);
}
}
output[(y * source.Width) + x] = pixelValue;
return;
}
// TODO: This can be parallel.
// Ordered dithering. We are only operating on a single pixel.
for (int y = interest.Top; y < interest.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
int offset = y * width;
for (int x = interest.Left; x < interest.Right; x++)
{
TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth);
output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _);
}
}
}
@ -88,15 +97,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override ReadOnlyMemory<TPixel> GetPalette() => this.palette;
/// <summary>
/// Process the pixel in the second pass of the algorithm
/// </summary>
/// <param name="pixel">The pixel to quantize</param>
/// <returns>
/// The quantized value
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte QuantizePixel(ref TPixel pixel) => this.GetClosestPixel(ref pixel);
}
}

16
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Allows the quantization of images pixels using color palettes.
/// Override this class to provide your own palette.
/// <para>
/// By default the quantizer uses <see cref="KnownDiffusers.FloydSteinberg"/> dithering.
/// By default the quantizer uses <see cref="KnownDitherers.FloydSteinberg"/> dithering.
/// </para>
/// </summary>
public class PaletteQuantizer : IQuantizer
@ -40,15 +40,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, IErrorDiffuser diffuser)
/// <param name="dither">The dithering algorithm, if any, to apply to the output image</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, IDither dither)
{
this.Palette = palette;
this.Diffuser = diffuser;
this.Dither = dither;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
public IDither Dither { get; }
/// <summary>
/// Gets the palette.
@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
var palette = new TPixel[this.Palette.Length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, this.Diffuser, palette);
return new PaletteFrameQuantizer<TPixel>(configuration, this.Dither, palette);
}
/// <inheritdoc/>
@ -73,9 +73,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var palette = new TPixel[max];
Color.ToPixel(configuration, this.Palette.Span.Slice(0, max), palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, this.Diffuser, palette);
return new PaletteFrameQuantizer<TPixel>(configuration, this.Dither, palette);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null;
}
}

2
src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs

@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WebSafePaletteQuantizer(IErrorDiffuser diffuser)
public WebSafePaletteQuantizer(IDither diffuser)
: base(Color.WebSafePalette, diffuser)
{
}

2
src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs

@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WernerPaletteQuantizer(IErrorDiffuser diffuser)
public WernerPaletteQuantizer(IDither diffuser)
: base(Color.WernerPalette, diffuser)
{
}

716
src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs

@ -10,8 +10,6 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
// TODO: Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case?
// (T, R, G, B, A, M2) could be grouped together! Investigate a ColorMoment struct.
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
@ -69,34 +67,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// Moment of <c>P(c)</c>.
/// Color moments.
/// </summary>
private IMemoryOwner<long> vwt;
/// <summary>
/// Moment of <c>r*P(c)</c>.
/// </summary>
private IMemoryOwner<long> vmr;
/// <summary>
/// Moment of <c>g*P(c)</c>.
/// </summary>
private IMemoryOwner<long> vmg;
/// <summary>
/// Moment of <c>b*P(c)</c>.
/// </summary>
private IMemoryOwner<long> vmb;
/// <summary>
/// Moment of <c>a*P(c)</c>.
/// </summary>
private IMemoryOwner<long> vma;
/// <summary>
/// Moment of <c>c^2*P(c)</c>.
/// </summary>
private IMemoryOwner<double> m2;
private IMemoryOwner<Moment> moments;
/// <summary>
/// Color space tag.
@ -148,15 +121,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
: base(configuration, quantizer, false)
{
this.memoryAllocator = this.Configuration.MemoryAllocator;
this.vwt = this.memoryAllocator.Allocate<long>(TableLength, AllocationOptions.Clean);
this.vmr = this.memoryAllocator.Allocate<long>(TableLength, AllocationOptions.Clean);
this.vmg = this.memoryAllocator.Allocate<long>(TableLength, AllocationOptions.Clean);
this.vmb = this.memoryAllocator.Allocate<long>(TableLength, AllocationOptions.Clean);
this.vma = this.memoryAllocator.Allocate<long>(TableLength, AllocationOptions.Clean);
this.m2 = this.memoryAllocator.Allocate<double>(TableLength, AllocationOptions.Clean);
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = maxColors;
}
@ -170,21 +136,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (disposing)
{
this.vwt?.Dispose();
this.vmr?.Dispose();
this.vmg?.Dispose();
this.vmb?.Dispose();
this.vma?.Dispose();
this.m2?.Dispose();
this.moments?.Dispose();
this.tag?.Dispose();
}
this.vwt = null;
this.vmr = null;
this.vmg = null;
this.vmb = null;
this.vma = null;
this.m2 = null;
this.moments = null;
this.tag = null;
this.isDisposed = true;
@ -199,27 +155,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (this.palette is null)
{
this.palette = new TPixel[this.colors];
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan();
for (int k = 0; k < this.colors; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
float weight = Volume(ref this.colorCube[k], vwtSpan);
Moment moment = Volume(ref this.colorCube[k], momentsSpan);
if (MathF.Abs(weight) > Constants.Epsilon)
if (moment.Weight > 0)
{
float r = Volume(ref this.colorCube[k], vmrSpan);
float g = Volume(ref this.colorCube[k], vmgSpan);
float b = Volume(ref this.colorCube[k], vmbSpan);
float a = Volume(ref this.colorCube[k], vmaSpan);
ref TPixel color = ref this.palette[k];
color.FromScaledVector4(new Vector4(r, g, b, a) / weight / 255F);
color.FromScaledVector4(moment.Normalize());
}
}
}
@ -236,50 +183,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <inheritdoc/>
protected override void SecondPass(ImageFrame<TPixel> source, Span<byte> output, ReadOnlySpan<TPixel> palette, int width, int height)
[MethodImpl(InliningOptions.ShortMethod)]
protected override byte GetQuantizedColor(TPixel color, out TPixel match)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(ref sourcePixel);
TPixel transformedPixel = palette[pixelValue];
for (int y = 0; y < height; y++)
if (!this.DoDither)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
// And loop through each column
for (int x = 0; x < width; x++)
{
// Get the pixel.
sourcePixel = row[x];
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
{
// Quantize the pixel
pixelValue = this.QuantizePixel(ref sourcePixel);
// And setup the previous pointer
previousPixel = sourcePixel;
if (this.Dither)
{
transformedPixel = palette[pixelValue];
}
}
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height);
}
output[(y * source.Width) + x] = pixelValue;
}
// Expected order r->g->b->a
Rgba32 rgba = default;
color.ToRgba32(ref rgba);
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
ReadOnlySpan<byte> tagSpan = this.tag.GetSpan();
var index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
match = this.GetPalette().Span[index];
return index;
}
return base.GetQuantizedColor(color, out match);
}
/// <summary>
@ -290,7 +214,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="b">The blue value.</param>
/// <param name="a">The alpha value.</param>
/// <returns>The index.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetPaletteIndex(int r, int g, int b, int a)
{
return (r << ((IndexBits * 2) + IndexAlphaBits))
@ -307,26 +231,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Computes sum over a box of any given statistic.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="moment">The moment.</param>
/// <param name="moments">The moment.</param>
/// <returns>The result.</returns>
private static float Volume(ref Box cube, Span<long> moment)
private static Moment Volume(ref Box cube, ReadOnlySpan<Moment> 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)];
}
/// <summary>
@ -334,55 +258,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="direction">The direction.</param>
/// <param name="moment">The moment.</param>
/// <param name="moments">The moment.</param>
/// <returns>The result.</returns>
private static long Bottom(ref Box cube, int direction, Span<long> moment)
private static Moment Bottom(ref Box cube, int direction, ReadOnlySpan<Moment> moments)
{
switch (direction)
{
// Red
case 3:
return -moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
return -moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
// Green
case 2:
return -moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
return -moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
// Blue
case 1:
return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
return -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
// Alpha
case 0:
return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
return -moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)];
default:
throw new ArgumentOutOfRangeException(nameof(direction));
@ -395,55 +319,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="cube">The cube.</param>
/// <param name="direction">The direction.</param>
/// <param name="position">The position.</param>
/// <param name="moment">The moment.</param>
/// <param name="moments">The moment.</param>
/// <returns>The result.</returns>
private static long Top(ref Box cube, int direction, int position, Span<long> moment)
private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpan<Moment> moments)
{
switch (direction)
{
// Red
case 3:
return moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)]
- moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)]
- moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)]
+ moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)];
return moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)]
- moments[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)]
- moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)]
+ moments[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)];
// Green
case 2:
return moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)]
- moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)]
- moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)];
return moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)]
- moments[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)]
- moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)];
// Blue
case 1:
return moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)]
- moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)]
- moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)]
- moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)];
return moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)]
- moments[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)]
- moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)]
- moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)];
// Alpha
case 0:
return moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)]
- moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)]
- moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)]
+ moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)]
- moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)]
+ moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)]
- moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)];
return moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)]
- moments[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)]
- moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)]
+ moments[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)]
- moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)]
+ moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)]
- moments[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)];
default:
throw new ArgumentOutOfRangeException(nameof(direction));
@ -458,45 +382,31 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="height">The height in pixels of the image.</param>
private void Build3DHistogram(ImageFrame<TPixel> source, int width, int height)
{
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
Span<double> m2Span = this.m2.GetSpan();
Span<Moment> momentSpan = this.moments.GetSpan();
// Build up the 3-D color histogram
// Loop through each row
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(source.Width))
{
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(source.GetConfiguration(), row, rgbaSpan);
ref Rgba32 scanBaseRef = ref MemoryMarshal.GetReference(rgbaSpan);
// And loop through each column
for (int x = 0; x < width; x++)
{
ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x);
using IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(source.Width);
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 scanBaseRef = ref MemoryMarshal.GetReference(rgbaSpan);
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgba32(source.GetConfiguration(), row, rgbaSpan);
int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
// And loop through each column
for (int x = 0; x < width; x++)
{
ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x);
vwtSpan[index]++;
vmrSpan[index] += rgba.R;
vmgSpan[index] += rgba.G;
vmbSpan[index] += rgba.B;
vmaSpan[index] += rgba.A;
int r = (rgba.R >> (8 - IndexBits)) + 1;
int g = (rgba.G >> (8 - IndexBits)) + 1;
int b = (rgba.B >> (8 - IndexBits)) + 1;
int a = (rgba.A >> (8 - IndexAlphaBits)) + 1;
var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A);
m2Span[index] += Vector4.Dot(vector, vector);
}
int index = GetPaletteIndex(r, g, b, a);
momentSpan[index] += rgba;
}
}
}
@ -507,103 +417,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator)
{
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
Span<double> m2Span = this.m2.GetSpan();
using (IMemoryOwner<long> volume = memoryAllocator.Allocate<long>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<long> volumeR = memoryAllocator.Allocate<long>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<long> volumeG = memoryAllocator.Allocate<long>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<long> volumeB = memoryAllocator.Allocate<long>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<long> volumeA = memoryAllocator.Allocate<long>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<double> volume2 = memoryAllocator.Allocate<double>(IndexCount * IndexAlphaCount))
using (IMemoryOwner<long> area = memoryAllocator.Allocate<long>(IndexAlphaCount))
using (IMemoryOwner<long> areaR = memoryAllocator.Allocate<long>(IndexAlphaCount))
using (IMemoryOwner<long> areaG = memoryAllocator.Allocate<long>(IndexAlphaCount))
using (IMemoryOwner<long> areaB = memoryAllocator.Allocate<long>(IndexAlphaCount))
using (IMemoryOwner<long> areaA = memoryAllocator.Allocate<long>(IndexAlphaCount))
using (IMemoryOwner<double> area2 = memoryAllocator.Allocate<double>(IndexAlphaCount))
using IMemoryOwner<Moment> volume = memoryAllocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = memoryAllocator.Allocate<Moment>(IndexAlphaCount);
Span<Moment> momentSpan = this.moments.GetSpan();
Span<Moment> volumeSpan = volume.GetSpan();
Span<Moment> areaSpan = area.GetSpan();
int baseIndex = GetPaletteIndex(1, 0, 0, 0);
for (int r = 1; r < IndexCount; r++)
{
Span<long> volumeSpan = volume.GetSpan();
Span<long> volumeRSpan = volumeR.GetSpan();
Span<long> volumeGSpan = volumeG.GetSpan();
Span<long> volumeBSpan = volumeB.GetSpan();
Span<long> volumeASpan = volumeA.GetSpan();
Span<double> volume2Span = volume2.GetSpan();
Span<long> areaSpan = area.GetSpan();
Span<long> areaRSpan = areaR.GetSpan();
Span<long> areaGSpan = areaG.GetSpan();
Span<long> areaBSpan = areaB.GetSpan();
Span<long> areaASpan = areaA.GetSpan();
Span<double> area2Span = area2.GetSpan();
for (int r = 1; r < IndexCount; r++)
volumeSpan.Clear();
for (int g = 1; g < IndexCount; g++)
{
volume.Clear();
volumeR.Clear();
volumeG.Clear();
volumeB.Clear();
volumeA.Clear();
volume2.Clear();
for (int g = 1; g < IndexCount; g++)
areaSpan.Clear();
for (int b = 1; b < IndexCount; b++)
{
area.Clear();
areaR.Clear();
areaG.Clear();
areaB.Clear();
areaA.Clear();
area2.Clear();
for (int b = 1; b < IndexCount; b++)
Moment line = default;
for (int a = 1; a < IndexAlphaCount; a++)
{
long line = 0;
long lineR = 0;
long lineG = 0;
long lineB = 0;
long lineA = 0;
double line2 = 0;
for (int a = 1; a < IndexAlphaCount; a++)
{
int ind1 = GetPaletteIndex(r, g, b, a);
line += vwtSpan[ind1];
lineR += vmrSpan[ind1];
lineG += vmgSpan[ind1];
lineB += vmbSpan[ind1];
lineA += vmaSpan[ind1];
line2 += m2Span[ind1];
areaSpan[a] += line;
areaRSpan[a] += lineR;
areaGSpan[a] += lineG;
areaBSpan[a] += lineB;
areaASpan[a] += lineA;
area2Span[a] += line2;
int inv = (b * IndexAlphaCount) + a;
volumeSpan[inv] += areaSpan[a];
volumeRSpan[inv] += areaRSpan[a];
volumeGSpan[inv] += areaGSpan[a];
volumeBSpan[inv] += areaBSpan[a];
volumeASpan[inv] += areaASpan[a];
volume2Span[inv] += area2Span[a];
int ind2 = ind1 - GetPaletteIndex(1, 0, 0, 0);
vwtSpan[ind1] = vwtSpan[ind2] + volumeSpan[inv];
vmrSpan[ind1] = vmrSpan[ind2] + volumeRSpan[inv];
vmgSpan[ind1] = vmgSpan[ind2] + volumeGSpan[inv];
vmbSpan[ind1] = vmbSpan[ind2] + volumeBSpan[inv];
vmaSpan[ind1] = vmaSpan[ind2] + volumeASpan[inv];
m2Span[ind1] = m2Span[ind2] + volume2Span[inv];
}
int ind1 = GetPaletteIndex(r, g, b, a);
line += momentSpan[ind1];
areaSpan[a] += line;
int inv = (b * IndexAlphaCount) + a;
volumeSpan[inv] += areaSpan[a];
int ind2 = ind1 - baseIndex;
momentSpan[ind1] = momentSpan[ind2] + volumeSpan[inv];
}
}
}
@ -617,33 +462,29 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns>
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<double> 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<Moment> 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);
}
/// <summary>
@ -658,60 +499,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="first">The first position.</param>
/// <param name="last">The last position.</param>
/// <param name="cut">The cutting point.</param>
/// <param name="wholeR">The whole red.</param>
/// <param name="wholeG">The whole green.</param>
/// <param name="wholeB">The whole blue.</param>
/// <param name="wholeA">The whole alpha.</param>
/// <param name="wholeW">The whole weight.</param>
/// <param name="whole">The whole moment.</param>
/// <returns>The <see cref="float"/>.</returns>
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<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> 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<Moment> momentSpan = this.moments.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan);
float max = 0F;
cut = -1;
for (int i = first; i < last; i++)
{
float halfR = baseR + Top(ref cube, direction, i, vmrSpan);
float halfG = baseG + Top(ref cube, direction, i, vmgSpan);
float halfB = baseB + Top(ref cube, direction, i, vmbSpan);
float halfA = baseA + Top(ref cube, direction, i, vmaSpan);
float halfW = baseW + Top(ref cube, direction, i, vwtSpan);
Moment half = bottom + Top(ref cube, direction, i, momentSpan);
if (MathF.Abs(halfW) < Constants.Epsilon)
if (half.Weight == 0)
{
continue;
}
var vector = new Vector4(halfR, halfG, halfB, halfA);
float temp = Vector4.Dot(vector, vector) / halfW;
var vector = new Vector4(half.R, half.G, half.B, half.A);
float temp = Vector4.Dot(vector, vector) / half.Weight;
halfW = wholeW - halfW;
half = whole - half;
if (MathF.Abs(halfW) < Constants.Epsilon)
if (half.Weight == 0)
{
continue;
}
halfR = wholeR - halfR;
halfG = wholeG - halfG;
halfB = wholeB - halfB;
halfA = wholeA - halfA;
vector = new Vector4(halfR, halfG, halfB, halfA);
temp += Vector4.Dot(vector, vector) / halfW;
vector = new Vector4(half.R, half.G, half.B, half.A);
temp += Vector4.Dot(vector, vector) / half.Weight;
if (temp > max)
{
@ -731,33 +549,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>Returns a value indicating whether the box has been split.</returns>
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<Moment> momentSpan = this.moments.GetSpan();
Moment whole = Volume(ref set1, momentSpan);
float maxr = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutr, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxg = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutg, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxb = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutb, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxa = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cuta, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole);
float maxG = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutG, whole);
float maxB = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutB, whole);
float maxA = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cutA, whole);
int dir;
if ((maxr >= maxg) && (maxr >= maxb) && (maxr >= maxa))
if ((maxR >= maxG) && (maxR >= maxB) && (maxR >= maxA))
{
dir = 3;
if (cutr < 0)
if (cutR < 0)
{
return false;
}
}
else if ((maxg >= maxr) && (maxg >= maxb) && (maxg >= maxa))
else if ((maxG >= maxR) && (maxG >= maxB) && (maxG >= maxA))
{
dir = 2;
}
else if ((maxb >= maxr) && (maxb >= maxg) && (maxb >= maxa))
else if ((maxB >= maxR) && (maxB >= maxG) && (maxB >= maxA))
{
dir = 1;
}
@ -775,7 +590,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
// Red
case 3:
set2.RMin = set1.RMax = cutr;
set2.RMin = set1.RMax = cutR;
set2.GMin = set1.GMin;
set2.BMin = set1.BMin;
set2.AMin = set1.AMin;
@ -783,7 +598,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
// Green
case 2:
set2.GMin = set1.GMax = cutg;
set2.GMin = set1.GMax = cutG;
set2.RMin = set1.RMin;
set2.BMin = set1.BMin;
set2.AMin = set1.AMin;
@ -791,7 +606,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
// Blue
case 1:
set2.BMin = set1.BMax = cutb;
set2.BMin = set1.BMax = cutB;
set2.RMin = set1.RMin;
set2.GMin = set1.GMin;
set2.AMin = set1.AMin;
@ -799,7 +614,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
// Alpha
case 0:
set2.AMin = set1.AMax = cuta;
set2.AMin = set1.AMax = cutA;
set2.RMin = set1.RMin;
set2.GMin = set1.GMin;
set2.BMin = set1.BMin;
@ -857,8 +672,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ref Box currentCube = ref this.colorCube[i];
if (this.Cut(ref nextCube, ref currentCube))
{
vv[next] = nextCube.Volume > 1 ? this.Variance(ref nextCube) : 0F;
vv[i] = currentCube.Volume > 1 ? this.Variance(ref currentCube) : 0F;
vv[next] = nextCube.Volume > 1 ? this.Variance(ref nextCube) : 0D;
vv[i] = currentCube.Volume > 1 ? this.Variance(ref currentCube) : 0D;
}
else
{
@ -886,35 +701,92 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
/// <summary>
/// Process the pixel in the second pass of the algorithm
/// </summary>
/// <param name="pixel">The pixel to quantize</param>
/// <returns>
/// The quantized value
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte QuantizePixel(ref TPixel pixel)
private struct Moment
{
if (this.Dither)
/// <summary>
/// Moment of <c>r*P(c)</c>.
/// </summary>
public long R;
/// <summary>
/// Moment of <c>g*P(c)</c>.
/// </summary>
public long G;
/// <summary>
/// Moment of <c>b*P(c)</c>.
/// </summary>
public long B;
/// <summary>
/// Moment of <c>a*P(c)</c>.
/// </summary>
public long A;
/// <summary>
/// Moment of <c>P(c)</c>.
/// </summary>
public long Weight;
/// <summary>
/// Moment of <c>c^2*P(c)</c>.
/// </summary>
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<byte> 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;
}
/// <summary>
@ -968,10 +840,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public int Volume;
/// <inheritdoc/>
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);
/// <inheritdoc/>
public bool Equals(Box other) =>
public readonly bool Equals(Box other) =>
this.RMin == other.RMin
&& this.RMax == other.RMax
&& this.GMin == other.GMin
@ -983,7 +857,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
&& this.Volume == other.Volume;
/// <inheritdoc/>
public override int GetHashCode()
public readonly override int GetHashCode()
{
HashCode hash = default;
hash.Add(this.RMin);

16
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Allows the quantization of images pixels using Xiaolin Wu's Color Quantizer <see href="http://www.ece.mcmaster.ca/~xwu/cq.c"/>
/// <para>
/// By default the quantizer uses <see cref="KnownDiffusers.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// By default the quantizer uses <see cref="KnownDitherers.FloydSteinberg"/> dithering and a color palette of a maximum length of <value>255</value>
/// </para>
/// </summary>
public class WuQuantizer : IQuantizer
@ -43,8 +43,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WuQuantizer(IErrorDiffuser diffuser)
/// <param name="diffuser">The dithering algorithm, if any, to apply to the output image</param>
public WuQuantizer(IDither diffuser)
: this(diffuser, QuantizerConstants.MaxColors)
{
}
@ -52,16 +52,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
/// <param name="dither">The dithering algorithm, if any, to apply to the output image</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(IErrorDiffuser diffuser, int maxColors)
public WuQuantizer(IDither dither, int maxColors)
{
this.Diffuser = diffuser;
this.Dither = dither;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
public IDither Dither { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
@ -85,6 +85,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return new WuFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
private static IDither GetDiffuser(bool dither) => dither ? KnownDitherers.FloydSteinberg : null;
}
}

4
tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs

@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
{
public class BinaryDitherTest : BaseImageOperationsExtensionTest
{
private readonly IOrderedDither orderedDither;
private readonly IErrorDiffuser errorDiffuser;
private readonly IDither orderedDither;
private readonly IDither errorDiffuser;
public BinaryDitherTest()
{

4
tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs

@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
}
}
private readonly IOrderedDither orderedDither;
private readonly IErrorDiffuser errorDiffuser;
private readonly IDither orderedDither;
private readonly IDither errorDiffuser;
private readonly Color[] testPalette =
{
Color.Red,

12
tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
TestImages.Png.CalliphoraPartial, TestImages.Png.Bike
};
public static readonly TheoryData<string, IOrderedDither> OrderedDitherers = new TheoryData<string, IOrderedDither>
public static readonly TheoryData<string, IDither> OrderedDitherers = new TheoryData<string, IDither>
{
{ "Bayer8x8", KnownDitherers.BayerDither8x8 },
{ "Bayer4x4", KnownDitherers.BayerDither4x4 },
@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
{ "Bayer2x2", KnownDitherers.BayerDither2x2 }
};
public static readonly TheoryData<string, IErrorDiffuser> ErrorDiffusers = new TheoryData<string, IErrorDiffuser>
public static readonly TheoryData<string, IDither> ErrorDiffusers = new TheoryData<string, IDither>
{
{ "Atkinson", KnownDiffusers.Atkinson },
{ "Burks", KnownDiffusers.Burks },
@ -41,14 +41,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
public const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24;
private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson;
private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson;
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(OrderedDitherers), 100, 100, PixelTypes.Rgba32)]
public void BinaryDitherFilter_WorksWithAllDitherers<TPixel>(TestImageProvider<TPixel> provider, string name, IOrderedDither ditherer)
public void BinaryDitherFilter_WorksWithAllDitherers<TPixel>(TestImageProvider<TPixel> provider, string name, IDither ditherer)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(ErrorDiffusers), 100, 100, PixelTypes.Rgba32)]
public void DiffusionFilter_WorksWithAllErrorDiffusers<TPixel>(TestImageProvider<TPixel> provider, string name, IErrorDiffuser diffuser)
public void DiffusionFilter_WorksWithAllErrorDiffusers<TPixel>(TestImageProvider<TPixel> provider, string name, IDither diffuser)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())

50
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<IErrorDiffuser> ErrorDiffusers = new TheoryData<IErrorDiffuser>
{
KnownDiffusers.Atkinson,
KnownDiffusers.Burks,
KnownDiffusers.FloydSteinberg,
KnownDiffusers.JarvisJudiceNinke,
KnownDiffusers.Sierra2,
KnownDiffusers.Sierra3,
KnownDiffusers.SierraLite,
KnownDiffusers.StevensonArce,
KnownDiffusers.Stucki,
};
public static readonly TheoryData<IOrderedDither> OrderedDitherers = new TheoryData<IOrderedDither>
{
KnownDitherers.BayerDither8x8,
KnownDitherers.BayerDither4x4,
KnownDitherers.OrderedDither3x3,
KnownDitherers.BayerDither2x2
};
public static readonly TheoryData<IDither> ErrorDiffusers
= new TheoryData<IDither>
{
KnownDiffusers.Atkinson,
KnownDiffusers.Burks,
KnownDiffusers.FloydSteinberg,
KnownDiffusers.JarvisJudiceNinke,
KnownDiffusers.Sierra2,
KnownDiffusers.Sierra3,
KnownDiffusers.SierraLite,
KnownDiffusers.StevensonArce,
KnownDiffusers.Stucki,
};
public static readonly TheoryData<IDither> OrderedDitherers
= new TheoryData<IDither>
{
KnownDitherers.BayerDither8x8,
KnownDitherers.BayerDither4x4,
KnownDitherers.OrderedDither3x3,
KnownDitherers.BayerDither2x2
};
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f);
private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson;
private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson;
/// <summary>
/// The output is visually correct old 32bit runtime,
@ -100,7 +102,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
[WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)]
public void DiffusionFilter_WorksWithAllErrorDiffusers<TPixel>(
TestImageProvider<TPixel> provider,
IErrorDiffuser diffuser)
IDither diffuser)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllDitherTests)
@ -134,7 +136,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
[WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)]
public void DitherFilter_WorksWithAllDitherers<TPixel>(
TestImageProvider<TPixel> provider,
IOrderedDither ditherer)
IDither ditherer)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllDitherTests)

6
tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs

@ -39,20 +39,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither);
quantizer = new OctreeQuantizer(false);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Dither);
quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither);
}
}
}

6
tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs

@ -37,20 +37,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither);
quantizer = new PaletteQuantizer(Rgb, false);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Dither);
quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither);
}
[Fact]

6
tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs

@ -39,20 +39,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither);
quantizer = new WuQuantizer(false);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Dither);
quantizer = new WuQuantizer(KnownDiffusers.Atkinson);
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither);
}
}
}

Loading…
Cancel
Save