Browse Source

Merge branch 'master' into af/disco-buffers

pull/1109/head
Anton Firszov 6 years ago
parent
commit
9b815cf7a7
  1. 34
      src/ImageSharp/Advanced/AotCompilerTools.cs
  2. 5
      src/ImageSharp/Color/Color.NamedColors.cs
  3. 53
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  4. 42
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  5. 14
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 10
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  7. 41
      src/ImageSharp/Primitives/DenseMatrix{T}.cs
  8. 85
      src/ImageSharp/Processing/Extensions/Binarization/BinaryDiffuseExtensions.cs
  9. 21
      src/ImageSharp/Processing/Extensions/Binarization/BinaryDitherExtensions.cs
  10. 97
      src/ImageSharp/Processing/Extensions/Dithering/DiffuseExtensions.cs
  11. 102
      src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs
  12. 27
      src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs
  13. 58
      src/ImageSharp/Processing/KnownDiffusers.cs
  14. 33
      src/ImageSharp/Processing/KnownDitherers.cs
  15. 78
      src/ImageSharp/Processing/KnownDitherings.cs
  16. 76
      src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor.cs
  17. 84
      src/ImageSharp/Processing/Processors/Binarization/BinaryErrorDiffusionProcessor{TPixel}.cs
  18. 58
      src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor.cs
  19. 82
      src/ImageSharp/Processing/Processors/Binarization/BinaryOrderedDitherProcessor{TPixel}.cs
  20. 210
      src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs
  21. 11
      src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs
  22. 228
      src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs
  23. 33
      src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs
  24. 19
      src/ImageSharp/Processing/Processors/Dithering/BayerDither2x2.cs
  25. 19
      src/ImageSharp/Processing/Processors/Dithering/BayerDither4x4.cs
  26. 19
      src/ImageSharp/Processing/Processors/Dithering/BayerDither8x8.cs
  27. 32
      src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs
  28. 188
      src/ImageSharp/Processing/Processors/Dithering/ErroDither.KnownTypes.cs
  29. 93
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs
  30. 65
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor.cs
  31. 85
      src/ImageSharp/Processing/Processors/Dithering/ErrorDiffusionPaletteProcessor{TPixel}.cs
  32. 220
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  33. 32
      src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs
  34. 53
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  35. 28
      src/ImageSharp/Processing/Processors/Dithering/IErrorDiffuser.cs
  36. 27
      src/ImageSharp/Processing/Processors/Dithering/IOrderedDither.cs
  37. 33
      src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs
  38. 31
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
  39. 288
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  40. 19
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither3x3.cs
  41. 6
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherFactory.cs
  42. 40
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor.cs
  43. 83
      src/ImageSharp/Processing/Processors/Dithering/OrderedDitherPaletteProcessor{TPixel}.cs
  44. 58
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor.cs
  45. 109
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  46. 48
      src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs
  47. 32
      src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs
  48. 33
      src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs
  49. 32
      src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs
  50. 33
      src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs
  51. 33
      src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs
  52. BIN
      src/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf
  53. 106
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  54. 136
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
  55. 285
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs
  56. 44
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  57. 30
      src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
  58. 38
      src/ImageSharp/Processing/Processors/Quantization/IQuantizedFrame{TPixel}.cs
  59. 21
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs
  60. 354
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  61. 76
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  62. 102
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  63. 60
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  64. 4
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs
  65. 23
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  66. 29
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrameExtensions.cs
  67. 27
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  68. 23
      src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs
  69. 42
      src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs
  70. 21
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  71. 23
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs
  72. 829
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  73. 69
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  74. 11
      tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs
  75. 7
      tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs
  76. 10
      tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs
  77. 35
      tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs
  78. 4
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  79. 3
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  80. 6
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  81. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  82. 20
      tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs
  83. 106
      tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs
  84. 112
      tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs
  85. 44
      tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs
  86. 62
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs
  87. 55
      tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs
  88. 51
      tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs
  89. 220
      tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs
  90. 55
      tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs
  91. 77
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  92. 121
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  93. 1
      tests/ImageSharp.Tests/TestImages.cs
  94. 10
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs
  95. 4
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  96. 7
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
  97. 2
      tests/Images/External
  98. BIN
      tests/Images/Input/Png/bike-small.png
  99. BIN
      tests/Images/Input/Png/david.png

34
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -1,12 +1,14 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -82,6 +84,7 @@ namespace SixLabors.ImageSharp.Advanced
// This is we actually call all the individual methods you need to seed.
AotCompileOctreeQuantizer<TPixel>();
AotCompileWuQuantizer<TPixel>();
AotCompilePaletteQuantizer<TPixel>();
AotCompileDithering<TPixel>();
AotCompilePixelOperations<TPixel>();
@ -109,9 +112,10 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileOctreeQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = new OctreeFrameQuantizer<TPixel>(Configuration.Default, new OctreeQuantizer(false)))
using (var test = new OctreeFrameQuantizer<TPixel>(Configuration.Default, new OctreeQuantizer().Options))
{
test.AotGetPalette();
var frame = new ImageFrame<TPixel>(Configuration.Default, 1, 1);
test.QuantizeFrame(frame, frame.Bounds());
}
}
@ -122,10 +126,24 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileWuQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = new WuFrameQuantizer<TPixel>(Configuration.Default, new WuQuantizer(false)))
using (var test = new WuFrameQuantizer<TPixel>(Configuration.Default, new WuQuantizer().Options))
{
test.QuantizeFrame(new ImageFrame<TPixel>(Configuration.Default, 1, 1));
test.AotGetPalette();
var frame = new ImageFrame<TPixel>(Configuration.Default, 1, 1);
test.QuantizeFrame(frame, frame.Bounds());
}
}
/// <summary>
/// This method pre-seeds the PaletteQuantizer in the AoT compiler for iOS.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
private static void AotCompilePaletteQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
using (var test = (PaletteFrameQuantizer<TPixel>)new PaletteQuantizer(Array.Empty<Color>()).CreateFrameQuantizer<TPixel>(Configuration.Default))
{
var frame = new ImageFrame<TPixel>(Configuration.Default, 1, 1);
test.QuantizeFrame(frame, frame.Bounds());
}
}
@ -136,11 +154,13 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileDithering<TPixel>()
where TPixel : struct, IPixel<TPixel>
{
var test = new FloydSteinbergDiffuser();
ErrorDither errorDither = ErrorDither.FloydSteinberg;
OrderedDither orderedDither = OrderedDither.Bayer2x2;
TPixel pixel = default;
using (var image = new ImageFrame<TPixel>(Configuration.Default, 1, 1))
{
test.Dither(image, pixel, pixel, 0, 0, 0, 0, 0);
errorDither.Dither(image, image.Bounds(), pixel, pixel, 0, 0, 0);
orderedDither.Dither(pixel, 0, 0, 0, 0);
}
}

5
src/ImageSharp/Color/Color.NamedColors.cs

@ -8,6 +8,7 @@ namespace SixLabors.ImageSharp
{
/// <content>
/// Contains static named color values.
/// <see href="https://www.w3.org/TR/css-color-3/"/>
/// </content>
public readonly partial struct Color
{
@ -719,9 +720,9 @@ namespace SixLabors.ImageSharp
public static readonly Color Tomato = FromRgba(255, 99, 71, 255);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #FFFFFF.
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #00000000.
/// </summary>
public static readonly Color Transparent = FromRgba(255, 255, 255, 0);
public static readonly Color Transparent = FromRgba(0, 0, 0, 0);
/// <summary>
/// Represents a <see paramref="Color"/> matching the W3C definition that has an hex value of #40E0D0.

53
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Bmp
@ -87,7 +88,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
this.quantizer = options.Quantizer ?? new OctreeQuantizer(dither: true, maxColors: 256);
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
}
/// <summary>
@ -335,36 +336,36 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : struct, IPixel<TPixel>
{
using (IQuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, 256).QuantizeFrame(image))
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
// TODO: Use bulk conversion here for better perf
int idx = 0;
foreach (TPixel quantizedColor in quantizedColors)
{
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
quantizedColor.ToRgba32(ref color);
colorPalette[idx] = color.B;
colorPalette[idx + 1] = color.G;
colorPalette[idx + 2] = color.R;
// TODO: Use bulk conversion here for better perf
int idx = 0;
foreach (TPixel quantizedColor in quantizedColors)
{
quantizedColor.ToRgba32(ref color);
colorPalette[idx] = color.B;
colorPalette[idx + 1] = color.G;
colorPalette[idx + 2] = color.R;
// Padding byte, always 0.
colorPalette[idx + 3] = 0;
idx += 4;
}
// Padding byte, always 0.
colorPalette[idx + 3] = 0;
idx += 4;
}
stream.Write(colorPalette);
stream.Write(colorPalette);
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<byte> pixelSpan = quantized.GetRowSpan(y);
stream.Write(pixelSpan);
for (int y = image.Height - 1; y >= 0; y--)
for (int i = 0; i < this.padding; i++)
{
ReadOnlySpan<byte> pixelSpan = quantized.GetRowSpan(y);
stream.Write(pixelSpan);
for (int i = 0; i < this.padding; i++)
{
stream.WriteByte(0);
}
stream.WriteByte(0);
}
}
}

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

@ -6,8 +6,6 @@ using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -28,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Configuration bound to the encoding operation.
/// </summary>
private Configuration configuration;
private readonly Configuration configuration;
/// <summary>
/// A reusable buffer used to reduce allocations.
@ -81,10 +79,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
IQuantizedFrame<TPixel> quantized;
QuantizedFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
// Get the number of bits.
@ -127,7 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeGlobal<TPixel>(Image<TPixel> image, IQuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
for (int i = 0; i < image.Frames.Count; i++)
@ -144,19 +142,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (IFrameQuantizer<TPixel> paletteFrameQuantizer =
new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Diffuser, quantized.Palette))
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
using (IQuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame))
{
this.WriteImageData(paletteQuantized, stream);
}
this.WriteImageData(paletteQuantized, stream);
}
}
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, IQuantizedFrame<TPixel> quantized, Stream stream)
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
@ -171,16 +166,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength
&& frameMetadata.ColorTableLength > 0)
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, frameMetadata.ColorTableLength))
var options = new QuantizerOptions
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
else
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(frame);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
}
@ -206,7 +208,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private int GetTransparentIndex<TPixel>(IQuantizedFrame<TPixel> quantized)
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette
@ -435,7 +437,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteColorTable<TPixel>(IQuantizedFrame<TPixel> image, Stream stream)
private void WriteColorTable<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
// The maximum number of colors for the bit depth
@ -457,9 +459,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="IQuantizedFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="image">The <see cref="QuantizedFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IQuantizedFrame<TPixel> image, Stream stream)
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))

14
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -146,7 +146,7 @@ namespace SixLabors.ImageSharp.Formats.Png
ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata();
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel);
IQuantizedFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
QuantizedFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IQuantizedFrame<TPixel> quantized, int row)
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
switch (this.options.ColorType)
@ -440,7 +440,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IQuantizedFrame<TPixel> quantized, int row)
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
this.CollectPixelBytes(rowSpan, quantized, row);
@ -546,7 +546,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, IQuantizedFrame<TPixel> quantized)
private void WritePaletteChunk<TPixel>(Stream stream, QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
if (quantized == null)
@ -783,7 +783,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IQuantizedFrame<TPixel> quantized, Stream stream)
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
byte[] buffer;
@ -881,7 +881,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IQuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : struct, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -960,7 +960,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</param>
private void EncodeAdam7IndexedPixels<TPixel>(IQuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodeAdam7IndexedPixels<TPixel>(QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : struct, IPixel<TPixel>
{
int width = quantized.Width;

10
src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs

@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param>
/// <param name="image">The image.</param>
public static IQuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>(
public static QuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options,
Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
@ -72,13 +72,15 @@ namespace SixLabors.ImageSharp.Formats.Png
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (options.Quantizer is null)
{
options.Quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits));
var maxColors = ImageMaths.GetColorCountForBitDepth(bits);
options.Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = maxColors });
}
// Create quantized frame returning the palette and set the bit depth.
using (IFrameQuantizer<TPixel> frameQuantizer = options.Quantizer.CreateFrameQuantizer<TPixel>(image.GetConfiguration()))
{
return frameQuantizer.QuantizeFrame(image.Frames.RootFrame);
ImageFrame<TPixel> frame = image.Frames.RootFrame;
return frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
@ -92,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
IQuantizedFrame<TPixel> quantizedFrame)
QuantizedFrame<TPixel> quantizedFrame)
where TPixel : struct, IPixel<TPixel>
{
byte bitDepth;

41
src/ImageSharp/Primitives/DenseMatrix{T}.cs

@ -136,7 +136,7 @@ namespace SixLabors.ImageSharp
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
#pragma warning disable SA1008 // Opening parenthesis should be spaced correctly
public static implicit operator T[,] (in DenseMatrix<T> data)
public static implicit operator T[,](in DenseMatrix<T> data)
#pragma warning restore SA1008 // Opening parenthesis should be spaced correctly
{
var result = new T[data.Rows, data.Columns];
@ -153,6 +153,24 @@ namespace SixLabors.ImageSharp
return result;
}
/// <summary>
/// Compares the two <see cref="DenseMatrix{T}"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(DenseMatrix<T> left, DenseMatrix<T> right)
=> left.Equals(right);
/// <summary>
/// Compares the two <see cref="DenseMatrix{T}"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(DenseMatrix<T> left, DenseMatrix<T> right)
=> !(left == right);
/// <summary>
/// Transposes the rows and columns of the dense matrix.
/// </summary>
@ -210,15 +228,32 @@ namespace SixLabors.ImageSharp
}
/// <inheritdoc/>
public override bool Equals(object obj) => obj is DenseMatrix<T> other && this.Equals(other);
public override bool Equals(object obj)
=> obj is DenseMatrix<T> other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool Equals(DenseMatrix<T> other) =>
this.Columns == other.Columns
&& this.Rows == other.Rows
&& this.Span.SequenceEqual(other.Span);
/// <inheritdoc/>
public override int GetHashCode() => this.Data.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)]
public override int GetHashCode()
{
HashCode code = default;
code.Add(this.Columns);
code.Add(this.Rows);
Span<T> span = this.Span;
for (int i = 0; i < span.Length; i++)
{
code.Add(span[i]);
}
return code.ToHashCode();
}
}
}

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

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

@ -1,8 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing
@ -14,12 +13,12 @@ namespace SixLabors.ImageSharp.Processing
public static class DitherExtensions
{
/// <summary>
/// Dithers the image reducing it to a web-safe palette using Bayer4x4 ordered dithering.
/// Dithers the image reducing it to a web-safe palette using <see cref="KnownDitherings.Bayer8x8"/>.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(this IImageProcessingContext source) =>
Dither(source, KnownDitherers.BayerDither4x4);
Dither(source, KnownDitherings.Bayer8x8);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
@ -27,21 +26,62 @@ 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 a web-safe palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</param>
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Dither(
this IImageProcessingContext source,
IDither dither,
float ditherScale) =>
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</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 Dither(
this IImageProcessingContext source,
IDither dither,
ReadOnlyMemory<Color> palette) =>
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</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 Dither(
this IImageProcessingContext source,
IOrderedDither dither,
IDither dither,
float ditherScale,
ReadOnlyMemory<Color> palette) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette));
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette));
/// <summary>
/// Dithers the image reducing it to a web-safe palette using <see cref="KnownDitherings.Bayer8x8"/>.
/// </summary>
/// <param name="source">The image this method extends.</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 Dither(this IImageProcessingContext source, Rectangle rectangle) =>
Dither(source, KnownDitherings.Bayer8x8, rectangle);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
@ -54,15 +94,50 @@ 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 PaletteDitherProcessor(dither), rectangle);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</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 Dither(
this IImageProcessingContext source,
IDither dither,
float ditherScale,
Rectangle rectangle) =>
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</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 Dither(
this IImageProcessingContext source,
IDither dither,
ReadOnlyMemory<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither), rectangle);
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</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.
@ -70,9 +145,10 @@ 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,
float ditherScale,
ReadOnlyMemory<Color> palette,
Rectangle rectangle) =>
source.ApplyProcessor(new OrderedDitherPaletteProcessor(dither, palette), rectangle);
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale, palette), rectangle);
}
}
}

27
src/ImageSharp/Processing/Extensions/Quantization/QuantizeExtensions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -27,5 +27,28 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
public static IImageProcessingContext Quantize(this IImageProcessingContext source, IQuantizer quantizer) =>
source.ApplyProcessor(new QuantizeProcessor(quantizer));
/// <summary>
/// Applies quantization to the image using the <see cref="OctreeQuantizer"/>.
/// </summary>
/// <param name="source">The image this method extends.</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 Quantize(this IImageProcessingContext source, Rectangle rectangle) =>
Quantize(source, KnownQuantizers.Octree, rectangle);
/// <summary>
/// Applies quantization to the image.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="quantizer">The quantizer to apply to perform the operation.</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 Quantize(this IImageProcessingContext source, IQuantizer quantizer, Rectangle rectangle) =>
source.ApplyProcessor(new QuantizeProcessor(quantizer), 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();
}
}

33
src/ImageSharp/Processing/KnownDitherers.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Contains reusable static instances of known ordered dither matrices
/// </summary>
public static class KnownDitherers
{
/// <summary>
/// Gets the order ditherer using the 2x2 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither2x2 { get; } = new BayerDither2x2();
/// <summary>
/// Gets the order ditherer using the 3x3 dithering matrix
/// </summary>
public static IOrderedDither OrderedDither3x3 { get; } = new OrderedDither3x3();
/// <summary>
/// Gets the order ditherer using the 4x4 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither4x4 { get; } = new BayerDither4x4();
/// <summary>
/// Gets the order ditherer using the 8x8 Bayer dithering matrix
/// </summary>
public static IOrderedDither BayerDither8x8 { get; } = new BayerDither8x8();
}
}

78
src/ImageSharp/Processing/KnownDitherings.cs

@ -0,0 +1,78 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Contains reusable static instances of known dithering algorithms.
/// </summary>
public static class KnownDitherings
{
/// <summary>
/// Gets the order ditherer using the 2x2 Bayer dithering matrix
/// </summary>
public static IDither Bayer2x2 { get; } = OrderedDither.Bayer2x2;
/// <summary>
/// Gets the order ditherer using the 3x3 dithering matrix
/// </summary>
public static IDither Ordered3x3 { get; } = OrderedDither.Ordered3x3;
/// <summary>
/// Gets the order ditherer using the 4x4 Bayer dithering matrix
/// </summary>
public static IDither Bayer4x4 { get; } = OrderedDither.Bayer4x4;
/// <summary>
/// Gets the order ditherer using the 8x8 Bayer dithering matrix
/// </summary>
public static IDither Bayer8x8 { get; } = OrderedDither.Bayer8x8;
/// <summary>
/// Gets the error Dither that implements the Atkinson algorithm.
/// </summary>
public static IDither Atkinson { get; } = ErrorDither.Atkinson;
/// <summary>
/// Gets the error Dither that implements the Burks algorithm.
/// </summary>
public static IDither Burks { get; } = ErrorDither.Burkes;
/// <summary>
/// Gets the error Dither that implements the Floyd-Steinberg algorithm.
/// </summary>
public static IDither FloydSteinberg { get; } = ErrorDither.FloydSteinberg;
/// <summary>
/// Gets the error Dither that implements the Jarvis-Judice-Ninke algorithm.
/// </summary>
public static IDither JarvisJudiceNinke { get; } = ErrorDither.JarvisJudiceNinke;
/// <summary>
/// Gets the error Dither that implements the Sierra-2 algorithm.
/// </summary>
public static IDither Sierra2 { get; } = ErrorDither.Sierra2;
/// <summary>
/// Gets the error Dither that implements the Sierra-3 algorithm.
/// </summary>
public static IDither Sierra3 { get; } = ErrorDither.Sierra3;
/// <summary>
/// Gets the error Dither that implements the Sierra-Lite algorithm.
/// </summary>
public static IDither SierraLite { get; } = ErrorDither.SierraLite;
/// <summary>
/// Gets the error Dither that implements the Stevenson-Arce algorithm.
/// </summary>
public static IDither StevensonArce { get; } = ErrorDither.StevensonArce;
/// <summary>
/// Gets the error Dither that implements the Stucki algorithm.
/// </summary>
public static IDither Stucki { get; } = ErrorDither.Stucki;
}
}

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

210
src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
@ -22,26 +21,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
internal class BokehBlurProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The kernel radius.
/// </summary>
private readonly int radius;
/// <summary>
/// The gamma highlight factor to use when applying the effect
/// </summary>
private readonly float gamma;
/// <summary>
/// The maximum size of the kernel in either direction
/// </summary>
private readonly int kernelSize;
/// <summary>
/// The number of components to use when applying the bokeh blur
/// </summary>
private readonly int componentsCount;
/// <summary>
/// The kernel parameters to use for the current instance (a: X, b: Y, A: Z, B: W)
/// </summary>
@ -52,16 +36,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// </summary>
private readonly Complex64[][] kernels;
/// <summary>
/// The scaling factor for kernel values
/// </summary>
private readonly float kernelsScale;
/// <summary>
/// The mapping of initialized complex kernels and parameters, to speed up the initialization of new <see cref="BokehBlurProcessor{TPixel}"/> instances
/// </summary>
private static readonly ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData> Cache = new ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData>();
/// <summary>
/// Initializes a new instance of the <see cref="BokehBlurProcessor{TPixel}"/> class.
/// </summary>
@ -72,29 +46,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
public BokehBlurProcessor(Configuration configuration, BokehBlurProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.radius = definition.Radius;
this.kernelSize = (this.radius * 2) + 1;
this.componentsCount = definition.Components;
this.gamma = definition.Gamma;
// Reuse the initialized values from the cache, if possible
var parameters = new BokehBlurParameters(this.radius, this.componentsCount);
if (Cache.TryGetValue(parameters, out BokehBlurKernelData info))
{
this.kernelParameters = info.Parameters;
this.kernelsScale = info.Scale;
this.kernels = info.Kernels;
}
else
{
// Initialize the complex kernels and parameters with the current arguments
(this.kernelParameters, this.kernelsScale) = this.GetParameters();
this.kernels = this.CreateComplexKernels();
this.NormalizeKernels();
// Get the bokeh blur data
BokehBlurKernelData data = BokehBlurKernelDataProvider.GetBokehBlurKernelData(
definition.Radius,
(definition.Radius * 2) + 1,
definition.Components);
// Store them in the cache for future use
Cache.TryAdd(parameters, new BokehBlurKernelData(this.kernelParameters, this.kernelsScale, this.kernels));
}
this.kernelParameters = data.Parameters;
this.kernels = data.Kernels;
}
/// <summary>
@ -107,163 +68,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution
/// </summary>
public IReadOnlyList<Vector4> KernelParameters => this.kernelParameters;
/// <summary>
/// Gets the kernel scales to adjust the component values in each kernel
/// </summary>
private static IReadOnlyList<float> KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f };
/// <summary>
/// Gets the available bokeh blur kernel parameters
/// </summary>
private static IReadOnlyList<Vector4[]> KernelComponents { get; } = new[]
{
// 1 component
new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) },
// 2 components
new[]
{
new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f),
new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f)
},
// 3 components
new[]
{
new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f),
new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f),
new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f)
},
// 4 components
new[]
{
new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f),
new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f),
new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f),
new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f)
},
// 5 components
new[]
{
new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f),
new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f),
new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f),
new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f),
new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f)
},
// 6 components
new[]
{
new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f),
new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f),
new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f),
new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f),
new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f),
new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f)
}
};
/// <summary>
/// Gets the kernel parameters and scaling factor for the current count value in the current instance
/// </summary>
private (Vector4[] Parameters, float Scale) GetParameters()
{
// Prepare the kernel components
int index = Math.Max(0, Math.Min(this.componentsCount - 1, KernelComponents.Count));
return (KernelComponents[index], KernelScales[index]);
}
/// <summary>
/// Creates the collection of complex 1D kernels with the specified parameters
/// </summary>
private Complex64[][] CreateComplexKernels()
{
var kernels = new Complex64[this.kernelParameters.Length][];
ref Vector4 baseRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan());
for (int i = 0; i < this.kernelParameters.Length; i++)
{
ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i);
kernels[i] = this.CreateComplex1DKernel(paramsRef.X, paramsRef.Y);
}
return kernels;
}
/// <summary>
/// Creates a complex 1D kernel with the specified parameters
/// </summary>
/// <param name="a">The exponential parameter for each complex component</param>
/// <param name="b">The angle component for each complex component</param>
private Complex64[] CreateComplex1DKernel(float a, float b)
{
var kernel = new Complex64[this.kernelSize];
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan());
int r = this.radius, n = -r;
for (int i = 0; i < this.kernelSize; i++, n++)
{
// Incrementally compute the range values
float value = n * this.kernelsScale * (1f / r);
value *= value;
// Fill in the complex kernel values
Unsafe.Add(ref baseRef, i) = new Complex64(
MathF.Exp(-a * value) * MathF.Cos(b * value),
MathF.Exp(-a * value) * MathF.Sin(b * value));
}
return kernel;
}
/// <summary>
/// Normalizes the kernels with respect to A * real + B * imaginary
/// </summary>
private void NormalizeKernels()
{
// Calculate the complex weighted sum
float total = 0;
Span<Complex64[]> kernelsSpan = this.kernels.AsSpan();
ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan);
ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(this.kernelParameters.AsSpan());
for (int i = 0; i < this.kernelParameters.Length; i++)
{
ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, i);
int length = kernelRef.Length;
ref Complex64 valueRef = ref kernelRef[0];
ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, i);
for (int j = 0; j < length; j++)
{
for (int k = 0; k < length; k++)
{
ref Complex64 jRef = ref Unsafe.Add(ref valueRef, j);
ref Complex64 kRef = ref Unsafe.Add(ref valueRef, k);
total +=
(paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary)))
+ (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real)));
}
}
}
// Normalize the kernels
float scalar = 1f / MathF.Sqrt(total);
for (int i = 0; i < kernelsSpan.Length; i++)
{
ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, i);
int length = kernelsRef.Length;
ref Complex64 valueRef = ref kernelsRef[0];
for (int j = 0; j < length; j++)
{
Unsafe.Add(ref valueRef, j) *= scalar;
}
}
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{

11
src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelData.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
@ -15,11 +15,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters
/// </summary>
public readonly Vector4[] Parameters;
/// <summary>
/// The scaling factor for the kernel values
/// </summary>
public readonly float Scale;
/// <summary>
/// The kernel components to apply the bokeh blur effect
/// </summary>
@ -29,12 +24,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters
/// Initializes a new instance of the <see cref="BokehBlurKernelData"/> struct.
/// </summary>
/// <param name="parameters">The kernel parameters</param>
/// <param name="scale">The kernel scale factor</param>
/// <param name="kernels">The complex kernel components</param>
public BokehBlurKernelData(Vector4[] parameters, float scale, Complex64[][] kernels)
public BokehBlurKernelData(Vector4[] parameters, Complex64[][] kernels)
{
this.Parameters = parameters;
this.Scale = scale;
this.Kernels = kernels;
}
}

228
src/ImageSharp/Processing/Processors/Convolution/Parameters/BokehBlurKernelDataProvider.cs

@ -0,0 +1,228 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Processing.Processors.Convolution.Parameters
{
/// <summary>
/// Provides parameters to be used in the <see cref="BokehBlurProcessor{TPixel}"/>.
/// </summary>
internal static class BokehBlurKernelDataProvider
{
/// <summary>
/// The mapping of initialized complex kernels and parameters, to speed up the initialization of new <see cref="BokehBlurProcessor{TPixel}"/> instances
/// </summary>
private static readonly ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData> Cache = new ConcurrentDictionary<BokehBlurParameters, BokehBlurKernelData>();
/// <summary>
/// Gets the kernel scales to adjust the component values in each kernel
/// </summary>
private static IReadOnlyList<float> KernelScales { get; } = new[] { 1.4f, 1.2f, 1.2f, 1.2f, 1.2f, 1.2f };
/// <summary>
/// Gets the available bokeh blur kernel parameters
/// </summary>
private static IReadOnlyList<Vector4[]> KernelComponents { get; } = new[]
{
// 1 component
new[] { new Vector4(0.862325f, 1.624835f, 0.767583f, 1.862321f) },
// 2 components
new[]
{
new Vector4(0.886528f, 5.268909f, 0.411259f, -0.548794f),
new Vector4(1.960518f, 1.558213f, 0.513282f, 4.56111f)
},
// 3 components
new[]
{
new Vector4(2.17649f, 5.043495f, 1.621035f, -2.105439f),
new Vector4(1.019306f, 9.027613f, -0.28086f, -0.162882f),
new Vector4(2.81511f, 1.597273f, -0.366471f, 10.300301f)
},
// 4 components
new[]
{
new Vector4(4.338459f, 1.553635f, -5.767909f, 46.164397f),
new Vector4(3.839993f, 4.693183f, 9.795391f, -15.227561f),
new Vector4(2.791880f, 8.178137f, -3.048324f, 0.302959f),
new Vector4(1.342190f, 12.328289f, 0.010001f, 0.244650f)
},
// 5 components
new[]
{
new Vector4(4.892608f, 1.685979f, -22.356787f, 85.91246f),
new Vector4(4.71187f, 4.998496f, 35.918936f, -28.875618f),
new Vector4(4.052795f, 8.244168f, -13.212253f, -1.578428f),
new Vector4(2.929212f, 11.900859f, 0.507991f, 1.816328f),
new Vector4(1.512961f, 16.116382f, 0.138051f, -0.01f)
},
// 6 components
new[]
{
new Vector4(5.143778f, 2.079813f, -82.326596f, 111.231024f),
new Vector4(5.612426f, 6.153387f, 113.878661f, 58.004879f),
new Vector4(5.982921f, 9.802895f, 39.479083f, -162.028887f),
new Vector4(6.505167f, 11.059237f, -71.286026f, 95.027069f),
new Vector4(3.869579f, 14.81052f, 1.405746f, -3.704914f),
new Vector4(2.201904f, 19.032909f, -0.152784f, -0.107988f)
}
};
/// <summary>
/// Gets the bokeh blur kernel data for the specified parameters.
/// </summary>
/// <param name="radius">The value representing the size of the area to sample.</param>
/// <param name="kernelSize">The size of each kernel to compute.</param>
/// <param name="componentsCount">The number of components to use to approximate the original 2D bokeh blur convolution kernel.</param>
/// <returns>A <see cref="BokehBlurKernelData"/> instance with the kernel data for the current parameters.</returns>
public static BokehBlurKernelData GetBokehBlurKernelData(
int radius,
int kernelSize,
int componentsCount)
{
// Reuse the initialized values from the cache, if possible
var parameters = new BokehBlurParameters(radius, componentsCount);
if (!Cache.TryGetValue(parameters, out BokehBlurKernelData info))
{
// Initialize the complex kernels and parameters with the current arguments
(Vector4[] kernelParameters, float kernelsScale) = GetParameters(componentsCount);
Complex64[][] kernels = CreateComplexKernels(kernelParameters, radius, kernelSize, kernelsScale);
NormalizeKernels(kernels, kernelParameters);
// Store them in the cache for future use
info = new BokehBlurKernelData(kernelParameters, kernels);
Cache.TryAdd(parameters, info);
}
return info;
}
/// <summary>
/// Gets the kernel parameters and scaling factor for the current count value in the current instance
/// </summary>
private static (Vector4[] Parameters, float Scale) GetParameters(int componentsCount)
{
// Prepare the kernel components
int index = Math.Max(0, Math.Min(componentsCount - 1, KernelComponents.Count));
return (KernelComponents[index], KernelScales[index]);
}
/// <summary>
/// Creates the collection of complex 1D kernels with the specified parameters
/// </summary>
/// <param name="kernelParameters">The parameters to use to normalize the kernels</param>
/// <param name="radius">The value representing the size of the area to sample.</param>
/// <param name="kernelSize">The size of each kernel to compute.</param>
/// <param name="kernelsScale">The scale factor for each kernel.</param>
private static Complex64[][] CreateComplexKernels(
Vector4[] kernelParameters,
int radius,
int kernelSize,
float kernelsScale)
{
var kernels = new Complex64[kernelParameters.Length][];
ref Vector4 baseRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan());
for (int i = 0; i < kernelParameters.Length; i++)
{
ref Vector4 paramsRef = ref Unsafe.Add(ref baseRef, i);
kernels[i] = CreateComplex1DKernel(radius, kernelSize, kernelsScale, paramsRef.X, paramsRef.Y);
}
return kernels;
}
/// <summary>
/// Creates a complex 1D kernel with the specified parameters
/// </summary>
/// <param name="radius">The value representing the size of the area to sample.</param>
/// <param name="kernelSize">The size of each kernel to compute.</param>
/// <param name="kernelsScale">The scale factor for each kernel.</param>
/// <param name="a">The exponential parameter for each complex component</param>
/// <param name="b">The angle component for each complex component</param>
private static Complex64[] CreateComplex1DKernel(
int radius,
int kernelSize,
float kernelsScale,
float a,
float b)
{
var kernel = new Complex64[kernelSize];
ref Complex64 baseRef = ref MemoryMarshal.GetReference(kernel.AsSpan());
int r = radius, n = -r;
for (int i = 0; i < kernelSize; i++, n++)
{
// Incrementally compute the range values
float value = n * kernelsScale * (1f / r);
value *= value;
// Fill in the complex kernel values
Unsafe.Add(ref baseRef, i) = new Complex64(
MathF.Exp(-a * value) * MathF.Cos(b * value),
MathF.Exp(-a * value) * MathF.Sin(b * value));
}
return kernel;
}
/// <summary>
/// Normalizes the kernels with respect to A * real + B * imaginary
/// </summary>
/// <param name="kernels">The current convolution kernels to normalize</param>
/// <param name="kernelParameters">The parameters to use to normalize the kernels</param>
private static void NormalizeKernels(Complex64[][] kernels, Vector4[] kernelParameters)
{
// Calculate the complex weighted sum
float total = 0;
Span<Complex64[]> kernelsSpan = kernels.AsSpan();
ref Complex64[] baseKernelsRef = ref MemoryMarshal.GetReference(kernelsSpan);
ref Vector4 baseParamsRef = ref MemoryMarshal.GetReference(kernelParameters.AsSpan());
for (int i = 0; i < kernelParameters.Length; i++)
{
ref Complex64[] kernelRef = ref Unsafe.Add(ref baseKernelsRef, i);
int length = kernelRef.Length;
ref Complex64 valueRef = ref kernelRef[0];
ref Vector4 paramsRef = ref Unsafe.Add(ref baseParamsRef, i);
for (int j = 0; j < length; j++)
{
for (int k = 0; k < length; k++)
{
ref Complex64 jRef = ref Unsafe.Add(ref valueRef, j);
ref Complex64 kRef = ref Unsafe.Add(ref valueRef, k);
total +=
(paramsRef.Z * ((jRef.Real * kRef.Real) - (jRef.Imaginary * kRef.Imaginary)))
+ (paramsRef.W * ((jRef.Real * kRef.Imaginary) + (jRef.Imaginary * kRef.Real)));
}
}
}
// Normalize the kernels
float scalar = 1f / MathF.Sqrt(total);
for (int i = 0; i < kernelsSpan.Length; i++)
{
ref Complex64[] kernelsRef = ref Unsafe.Add(ref baseKernelsRef, i);
int length = kernelsRef.Length;
ref Complex64 valueRef = ref kernelsRef[0];
for (int j = 0; j < length; j++)
{
Unsafe.Add(ref valueRef, j) *= scalar;
}
}
}
}
}

33
src/ImageSharp/Processing/Processors/Dithering/AtkinsonDiffuser.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 8F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> AtkinsonMatrix =
new float[,]
{
{ 0, 0, 1 / Divisor, 1 / Divisor },
{ 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 },
{ 0, 1 / Divisor, 0, 0 }
};
/// <summary>
/// Initializes a new instance of the <see cref="AtkinsonDiffuser"/> class.
/// </summary>
public AtkinsonDiffuser()
: base(AtkinsonMatrix)
{
}
}
}

19
src/ImageSharp/Processing/Processors/Dithering/BayerDither2x2.cs

@ -1,19 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Applies order dithering using the 2x2 Bayer dithering matrix.
/// </summary>
public sealed class BayerDither2x2 : OrderedDither
{
/// <summary>
/// Initializes a new instance of the <see cref="BayerDither2x2"/> class.
/// </summary>
public BayerDither2x2()
: base(2)
{
}
}
}

19
src/ImageSharp/Processing/Processors/Dithering/BayerDither4x4.cs

@ -1,19 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Applies order dithering using the 4x4 Bayer dithering matrix.
/// </summary>
public sealed class BayerDither4x4 : OrderedDither
{
/// <summary>
/// Initializes a new instance of the <see cref="BayerDither4x4"/> class.
/// </summary>
public BayerDither4x4()
: base(4)
{
}
}
}

19
src/ImageSharp/Processing/Processors/Dithering/BayerDither8x8.cs

@ -1,19 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Applies order dithering using the 8x8 Bayer dithering matrix.
/// </summary>
public sealed class BayerDither8x8 : OrderedDither
{
/// <summary>
/// Initializes a new instance of the <see cref="BayerDither8x8"/> class.
/// </summary>
public BayerDither8x8()
: base(8)
{
}
}
}

32
src/ImageSharp/Processing/Processors/Dithering/BurksDiffuser.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 32F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> BurksMatrix =
new float[,]
{
{ 0, 0, 0, 8 / Divisor, 4 / Divisor },
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="BurksDiffuser"/> class.
/// </summary>
public BurksDiffuser()
: base(BurksMatrix)
{
}
}
}

188
src/ImageSharp/Processing/Processors/Dithering/ErroDither.KnownTypes.cs

@ -0,0 +1,188 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// An error diffusion dithering implementation.
/// </summary>
public readonly partial struct ErrorDither
{
/// <summary>
/// Applies error diffusion based dithering using the Atkinson image dithering algorithm.
/// </summary>
public static ErrorDither Atkinson = CreateAtkinson();
/// <summary>
/// Applies error diffusion based dithering using the Burks image dithering algorithm.
/// </summary>
public static ErrorDither Burkes = CreateBurks();
/// <summary>
/// Applies error diffusion based dithering using the Floyd–Steinberg image dithering algorithm.
/// </summary>
public static ErrorDither FloydSteinberg = CreateFloydSteinberg();
/// <summary>
/// Applies error diffusion based dithering using the Jarvis, Judice, Ninke image dithering algorithm.
/// </summary>
public static ErrorDither JarvisJudiceNinke = CreateJarvisJudiceNinke();
/// <summary>
/// Applies error diffusion based dithering using the Sierra2 image dithering algorithm.
/// </summary>
public static ErrorDither Sierra2 = CreateSierra2();
/// <summary>
/// Applies error diffusion based dithering using the Sierra3 image dithering algorithm.
/// </summary>
public static ErrorDither Sierra3 = CreateSierra3();
/// <summary>
/// Applies error diffusion based dithering using the Sierra Lite image dithering algorithm.
/// </summary>
public static ErrorDither SierraLite = CreateSierraLite();
/// <summary>
/// Applies error diffusion based dithering using the Stevenson-Arce image dithering algorithm.
/// </summary>
public static ErrorDither StevensonArce = CreateStevensonArce();
/// <summary>
/// Applies error diffusion based dithering using the Stucki image dithering algorithm.
/// </summary>
public static ErrorDither Stucki = CreateStucki();
private static ErrorDither CreateAtkinson()
{
const float Divisor = 8F;
const int Offset = 1;
var matrix = new float[,]
{
{ 0, 0, 1 / Divisor, 1 / Divisor },
{ 1 / Divisor, 1 / Divisor, 1 / Divisor, 0 },
{ 0, 1 / Divisor, 0, 0 }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateBurks()
{
const float Divisor = 32F;
const int Offset = 2;
var matrix = new float[,]
{
{ 0, 0, 0, 8 / Divisor, 4 / Divisor },
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateFloydSteinberg()
{
const float Divisor = 16F;
const int Offset = 1;
var matrix = new float[,]
{
{ 0, 0, 7 / Divisor },
{ 3 / Divisor, 5 / Divisor, 1 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateJarvisJudiceNinke()
{
const float Divisor = 48F;
const int Offset = 2;
var matrix = new float[,]
{
{ 0, 0, 0, 7 / Divisor, 5 / Divisor },
{ 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor },
{ 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateSierra2()
{
const float Divisor = 16F;
const int Offset = 2;
var matrix = new float[,]
{
{ 0, 0, 0, 4 / Divisor, 3 / Divisor },
{ 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateSierra3()
{
const float Divisor = 32F;
const int Offset = 2;
var matrix = new float[,]
{
{ 0, 0, 0, 5 / Divisor, 3 / Divisor },
{ 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor },
{ 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateSierraLite()
{
const float Divisor = 4F;
const int Offset = 1;
var matrix = new float[,]
{
{ 0, 0, 2 / Divisor },
{ 1 / Divisor, 1 / Divisor, 0 }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateStevensonArce()
{
const float Divisor = 200F;
const int Offset = 3;
var matrix = new float[,]
{
{ 0, 0, 0, 0, 0, 32 / Divisor, 0 },
{ 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor },
{ 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 },
{ 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
private static ErrorDither CreateStucki()
{
const float Divisor = 42F;
const int Offset = 2;
var matrix = new float[,]
{
{ 0, 0, 0, 8 / Divisor, 4 / Divisor },
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor },
{ 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor }
};
return new ErrorDither(matrix, Offset);
}
}
}

93
src/ImageSharp/Processing/Processors/Dithering/ErrorDiffuser.cs

@ -1,93 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// The base class for performing error diffusion based dithering.
/// </summary>
public abstract class ErrorDiffuser : IErrorDiffuser
{
private readonly int offset;
private readonly DenseMatrix<float> matrix;
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDiffuser"/> class.
/// </summary>
/// <param name="matrix">The dithering matrix.</param>
internal ErrorDiffuser(in DenseMatrix<float> matrix)
{
// Calculate the offset position of the pixel relative to
// the diffusion matrix.
this.offset = 0;
for (int col = 0; col < matrix.Columns; col++)
{
if (matrix[0, col] != 0)
{
this.offset = col - 1;
break;
}
}
this.matrix = matrix;
}
/// <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)
where TPixel : struct, IPixel<TPixel>
{
image[x, y] = transformed;
// Equal? Break out as there's no error to pass.
if (source.Equals(transformed))
{
return;
}
// Calculate the error
Vector4 error = source.ToVector4() - transformed.ToVector4();
this.DoDither(image, x, y, minX, maxX, maxY, error);
}
[MethodImpl(InliningOptions.ShortMethod)]
private void DoDither<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 && targetY < maxY; row++, targetY++)
{
Span<TPixel> rowSpan = image.GetPixelRowSpan(targetY);
for (int col = 0; col < matrix.Columns; col++)
{
int targetX = x + (col - offset);
if (targetX >= minX && targetX < maxX)
{
float coefficient = matrix[row, col];
if (coefficient == 0)
{
continue;
}
ref TPixel pixel = ref rowSpan[targetX];
var result = pixel.ToVector4();
result += error * coefficient;
pixel.FromVector4(result);
}
}
}
}
}
}

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

220
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -0,0 +1,220 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// An error diffusion dithering implementation.
/// <see href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT"/>
/// </summary>
public readonly partial struct ErrorDither : IDither, IEquatable<ErrorDither>, IEquatable<IDither>
{
private readonly int offset;
private readonly DenseMatrix<float> matrix;
/// <summary>
/// Initializes a new instance of the <see cref="ErrorDither"/> struct.
/// </summary>
/// <param name="matrix">The diffusion matrix.</param>
/// <param name="offset">The starting offset within the matrix.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public ErrorDither(in DenseMatrix<float> matrix, int offset)
{
this.matrix = matrix;
this.offset = offset;
}
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(IDither left, ErrorDither right)
=> right == left;
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(IDither left, ErrorDither right)
=> !(right == left);
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(ErrorDither left, IDither right)
=> left.Equals(right);
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(ErrorDither left, IDither right)
=> !(left == right);
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(ErrorDither left, ErrorDither right)
=> left.Equals(right);
/// <summary>
/// Compares the two <see cref="ErrorDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(ErrorDither left, ErrorDither right)
=> !(left == right);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
Span<byte> outputSpan = output.Span;
ReadOnlySpan<TPixel> paletteSpan = palette.Span;
int width = bounds.Width;
int offsetY = bounds.Top;
int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width;
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = row[x];
outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
}
}
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
where TPixel : struct, IPixel<TPixel>
{
var pixelMap = new EuclideanPixelMap<TPixel>(palette);
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = row[x];
pixelMap.GetClosestColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
row[x] = transformed;
}
}
}
// Internal for AOT
[MethodImpl(InliningOptions.ShortMethod)]
internal TPixel Dither<TPixel>(
ImageFrame<TPixel> image,
Rectangle bounds,
TPixel source,
TPixel transformed,
int x,
int y,
float scale)
where TPixel : struct, IPixel<TPixel>
{
// Equal? Break out as there's no error to pass.
if (source.Equals(transformed))
{
return transformed;
}
// Calculate the error
Vector4 error = (source.ToVector4() - transformed.ToVector4()) * scale;
int offset = this.offset;
DenseMatrix<float> matrix = this.matrix;
// Loop through and distribute the error amongst neighboring pixels.
for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++)
{
if (targetY >= bounds.Bottom)
{
continue;
}
Span<TPixel> rowSpan = image.GetPixelRowSpan(targetY);
for (int col = 0; col < matrix.Columns; col++)
{
int targetX = x + (col - offset);
if (targetX < bounds.Left || targetX >= bounds.Right)
{
continue;
}
float coefficient = matrix[row, col];
if (coefficient == 0)
{
continue;
}
ref TPixel pixel = ref rowSpan[targetX];
var result = pixel.ToVector4();
result += error * coefficient;
pixel.FromVector4(result);
}
}
return transformed;
}
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is ErrorDither dither && this.Equals(dither);
/// <inheritdoc/>
public bool Equals(ErrorDither other)
=> this.offset == other.offset && this.matrix.Equals(other.matrix);
/// <inheritdoc/>
public bool Equals(IDither other)
=> this.Equals((object)other);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.offset, this.matrix);
}
}

32
src/ImageSharp/Processing/Processors/Dithering/FloydSteinbergDiffuser.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 16F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> FloydSteinbergMatrix =
new float[,]
{
{ 0, 0, 7 / Divisor },
{ 3 / Divisor, 5 / Divisor, 1 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="FloydSteinbergDiffuser"/> class.
/// </summary>
public FloydSteinbergDiffuser()
: base(FloydSteinbergMatrix)
{
}
}
}

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

@ -0,0 +1,53 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Defines the contract for types that apply dithering to images.
/// </summary>
public interface IDither
{
/// <summary>
/// Transforms the quantized image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels.
/// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame quantizer.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</param>
/// <param name="output">The output target</param>
/// <param name="bounds">The region of interest bounds.</param>
void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</param>
/// <param name="bounds">The region of interest bounds.</param>
/// <param name="scale">The dithering scale used to adjust the amount of dither. Range 0..1.</param>
void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
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>;
}
}

33
src/ImageSharp/Processing/Processors/Dithering/JarvisJudiceNinkeDiffuser.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 48F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> JarvisJudiceNinkeMatrix =
new float[,]
{
{ 0, 0, 0, 7 / Divisor, 5 / Divisor },
{ 3 / Divisor, 5 / Divisor, 7 / Divisor, 5 / Divisor, 3 / Divisor },
{ 1 / Divisor, 3 / Divisor, 5 / Divisor, 3 / Divisor, 1 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="JarvisJudiceNinkeDiffuser"/> class.
/// </summary>
public JarvisJudiceNinkeDiffuser()
: base(JarvisJudiceNinkeMatrix)
{
}
}
}

31
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <content>
/// An ordered dithering matrix with equal sides of arbitrary length
/// </content>
public readonly partial struct OrderedDither : IDither
{
/// <summary>
/// Applies order dithering using the 2x2 Bayer dithering matrix.
/// </summary>
public static OrderedDither Bayer2x2 = new OrderedDither(2);
/// <summary>
/// Applies order dithering using the 4x4 Bayer dithering matrix.
/// </summary>
public static OrderedDither Bayer4x4 = new OrderedDither(4);
/// <summary>
/// Applies order dithering using the 8x8 Bayer dithering matrix.
/// </summary>
public static OrderedDither Bayer8x8 = new OrderedDither(8);
/// <summary>
/// Applies order dithering using the 3x3 ordered dithering matrix.
/// </summary>
public static OrderedDither Ordered3x3 = new OrderedDither(3);
}
}

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

@ -1,49 +1,303 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// An ordered dithering matrix with equal sides of arbitrary length
/// </summary>
public class OrderedDither : IOrderedDither
public readonly partial struct OrderedDither : IDither, IEquatable<OrderedDither>, IEquatable<IDither>
{
private readonly DenseMatrix<uint> thresholdMatrix;
private readonly DenseMatrix<float> thresholdMatrix;
private readonly int modulusX;
private readonly int modulusY;
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDither"/> class.
/// Initializes a new instance of the <see cref="OrderedDither"/> struct.
/// </summary>
/// <param name="length">The length of the matrix sides</param>
[MethodImpl(InliningOptions.ShortMethod)]
public OrderedDither(uint length)
{
DenseMatrix<uint> ditherMatrix = OrderedDitherFactory.CreateDitherMatrix(length);
// Create a new matrix to run against, that pre-thresholds the values.
// We don't want to adjust the original matrix generation code as that
// creates known, easy to test values.
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
var thresholdMatrix = new DenseMatrix<float>((int)length);
float m2 = length * length;
for (int y = 0; y < length; y++)
{
for (int x = 0; x < length; x++)
{
thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F;
}
}
this.modulusX = ditherMatrix.Columns;
this.modulusY = ditherMatrix.Rows;
this.thresholdMatrix = thresholdMatrix;
}
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(IDither left, OrderedDither right)
=> right == left;
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(IDither left, OrderedDither right)
=> !(right == left);
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(OrderedDither left, IDither right)
=> left.Equals(right);
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(OrderedDither left, IDither right)
=> !(left == right);
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are equal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator ==(OrderedDither left, OrderedDither right)
=> left.Equals(right);
/// <summary>
/// Compares the two <see cref="OrderedDither"/> instances to determine whether they are unequal.
/// </summary>
/// <param name="left">The first source instance.</param>
/// <param name="right">The second source instance.</param>
/// <returns>The <see cref="bool"/>.</returns>
public static bool operator !=(OrderedDither left, OrderedDither right)
=> !(left == right);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>(
ref quantizer,
in Unsafe.AsRef(this),
source,
output,
bounds,
palette,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in ditherOperation);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
where TPixel : struct, IPixel<TPixel>
{
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>(
in Unsafe.AsRef(this),
source,
bounds,
palette,
scale,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
// Adjust the matrix range for 0-255
// TODO: It looks like it's actually possible to dither an image using it's own colors. We should investigate for V2
// https://stackoverflow.com/questions/12422407/monochrome-dithering-in-javascript-bayer-atkinson-floyd-steinberg
int multiplier = 256 / ditherMatrix.Count;
for (int y = 0; y < ditherMatrix.Rows; y++)
ParallelRowIterator.IterateRows(
configuration,
bounds,
in ditherOperation);
}
[MethodImpl(InliningOptions.ShortMethod)]
internal TPixel Dither<TPixel>(
TPixel source,
int x,
int y,
int bitDepth,
float scale)
where TPixel : struct, IPixel<TPixel>
{
Rgba32 rgba = default;
source.ToRgba32(ref rgba);
Rgba32 attempt;
// Spread assumes an even colorspace distribution and precision.
// Calculated as 0-255/component count. 256 / bitDepth
// https://bisqwit.iki.fi/story/howto/dither/jy/
// https://en.wikipedia.org/wiki/Ordered_dithering#Algorithm
int spread = 256 / bitDepth;
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale;
attempt.R = (byte)(rgba.R + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.G = (byte)(rgba.G + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.B = (byte)(rgba.B + factor).Clamp(byte.MinValue, byte.MaxValue);
attempt.A = (byte)(rgba.A + factor).Clamp(byte.MinValue, byte.MaxValue);
TPixel result = default;
result.FromRgba32(attempt);
return result;
}
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is OrderedDither dither && this.Equals(dither);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public bool Equals(OrderedDither other)
=> this.thresholdMatrix.Equals(other.thresholdMatrix) && this.modulusX == other.modulusX && this.modulusY == other.modulusY;
/// <inheritdoc/>
public bool Equals(IDither other)
=> this.Equals((object)other);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
private readonly struct QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowIntervalOperation(
ref TFrameQuantizer quantizer,
in OrderedDither dither,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
int bitDepth)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
this.output = output;
this.bounds = bounds;
this.palette = palette;
this.bitDepth = bitDepth;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int x = 0; x < ditherMatrix.Columns; x++)
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale;
for (int y = rows.Min; y < rows.Max; y++)
{
ditherMatrix[y, x] = (uint)((ditherMatrix[y, x] + 1) * multiplier) - 1;
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width;
// TODO: This can be a bulk operation.
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, scale);
outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _);
}
}
}
this.thresholdMatrix = ditherMatrix;
}
/// <inheritdoc />
public void Dither<TPixel>(ImageFrame<TPixel> image, TPixel source, TPixel upper, TPixel lower, float threshold, int x, int y)
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation
where TPixel : struct, IPixel<TPixel>
{
image[x, y] = this.thresholdMatrix[y % this.modulusY, x % this.modulusX] >= threshold ? lower : upper;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly float scale;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowIntervalOperation(
in OrderedDither dither,
ImageFrame<TPixel> source,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
float scale,
int bitDepth)
{
this.dither = dither;
this.source = source;
this.bounds = bounds;
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
this.scale = scale;
this.bitDepth = bitDepth;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale);
this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
row[x] = transformed;
}
}
}
}
}
}
}

19
src/ImageSharp/Processing/Processors/Dithering/OrderedDither3x3.cs

@ -1,19 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Applies order dithering using the 3x3 dithering matrix.
/// </summary>
public sealed class OrderedDither3x3 : OrderedDither
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderedDither3x3"/> class.
/// </summary>
public OrderedDither3x3()
: base(3)
{
}
}
}

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

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

@ -2,32 +2,78 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <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, QuantizerConstants.MaxDitherScale)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The ordered ditherer.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</param>
public PaletteDitherProcessor(IDither dither, float ditherScale)
: this(dither, ditherScale, 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, QuantizerConstants.MaxDitherScale, palette)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteDitherProcessor"/> class.
/// </summary>
/// <param name="dither">The dithering algorithm.</param>
/// <param name="ditherScale">The dithering scale used to adjust the amount of dither.</param>
/// <param name="palette">The palette to select substitute colors from.</param>
public PaletteDitherProcessor(IDither dither, float ditherScale, ReadOnlyMemory<Color> palette)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(dither, nameof(dither));
this.Dither = dither;
this.DitherScale = ditherScale.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale);
this.Palette = palette;
}
/// <summary>
/// Gets the dithering algorithm to apply to the output image.
/// </summary>
public IDither Dither { get; }
/// <summary>
/// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
/// </summary>
public float DitherScale { 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);
}
}

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

@ -3,25 +3,22 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <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 IDither dither;
private readonly float ditherScale;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private IMemoryOwner<Vector4> paletteVector;
private bool palleteVectorMapped;
private bool isDisposed;
/// <summary>
@ -31,34 +28,39 @@ 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.dither = definition.Dither;
this.ditherScale = definition.DitherScale;
this.sourcePalette = definition.Palette;
}
protected PaletteDitherProcessor Definition { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.dither.ApplyPaletteDither(
this.Configuration,
this.palette.Memory,
source,
interest,
this.ditherScale);
}
/// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source)
{
// Lazy init palettes:
if (!this.palleteVectorMapped)
if (this.palette 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.palleteVectorMapped = true;
base.BeforeFrameApply(source);
}
@ -73,71 +75,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
if (disposing)
{
this.palette?.Dispose();
this.paletteVector?.Dispose();
}
this.palette = null;
this.paletteVector = null;
this.isDisposed = true;
base.Dispose(disposing);
}
/// <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;
}
}
}

48
src/ImageSharp/Processing/Processors/Dithering/PixelPair.cs

@ -1,48 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// Represents a composite pair of pixels. Used for caching color distance lookups.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct PixelPair<TPixel> : IEquatable<PixelPair<TPixel>>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="PixelPair{TPixel}"/> struct.
/// </summary>
/// <param name="first">The first pixel color</param>
/// <param name="second">The second pixel color</param>
public PixelPair(TPixel first, TPixel second)
{
this.First = first;
this.Second = second;
}
/// <summary>
/// Gets the first pixel color
/// </summary>
public TPixel First { get; }
/// <summary>
/// Gets the second pixel color
/// </summary>
public TPixel Second { get; }
/// <inheritdoc/>
public bool Equals(PixelPair<TPixel> other)
=> this.First.Equals(other.First) && this.Second.Equals(other.Second);
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is PixelPair<TPixel> other && this.First.Equals(other.First) && this.Second.Equals(other.Second);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.First, this.Second);
}
}

32
src/ImageSharp/Processing/Processors/Dithering/Sierra2Diffuser.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 16F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> Sierra2Matrix =
new float[,]
{
{ 0, 0, 0, 4 / Divisor, 3 / Divisor },
{ 1 / Divisor, 2 / Divisor, 3 / Divisor, 2 / Divisor, 1 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="Sierra2Diffuser"/> class.
/// </summary>
public Sierra2Diffuser()
: base(Sierra2Matrix)
{
}
}
}

33
src/ImageSharp/Processing/Processors/Dithering/Sierra3Diffuser.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 32F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> Sierra3Matrix =
new float[,]
{
{ 0, 0, 0, 5 / Divisor, 3 / Divisor },
{ 2 / Divisor, 4 / Divisor, 5 / Divisor, 4 / Divisor, 2 / Divisor },
{ 0, 2 / Divisor, 3 / Divisor, 2 / Divisor, 0 }
};
/// <summary>
/// Initializes a new instance of the <see cref="Sierra3Diffuser"/> class.
/// </summary>
public Sierra3Diffuser()
: base(Sierra3Matrix)
{
}
}
}

32
src/ImageSharp/Processing/Processors/Dithering/SierraLiteDiffuser.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 4F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> SierraLiteMatrix =
new float[,]
{
{ 0, 0, 2 / Divisor },
{ 1 / Divisor, 1 / Divisor, 0 }
};
/// <summary>
/// Initializes a new instance of the <see cref="SierraLiteDiffuser"/> class.
/// </summary>
public SierraLiteDiffuser()
: base(SierraLiteMatrix)
{
}
}
}

33
src/ImageSharp/Processing/Processors/Dithering/StevensonArceDiffuser.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
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
{
private const float Divisor = 200F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> StevensonArceMatrix =
new float[,]
{
{ 0, 0, 0, 0, 0, 32 / Divisor, 0 },
{ 12 / Divisor, 0, 26 / Divisor, 0, 30 / Divisor, 0, 16 / Divisor },
{ 0, 12 / Divisor, 0, 26 / Divisor, 0, 12 / Divisor, 0 },
{ 5 / Divisor, 0, 12 / Divisor, 0, 12 / Divisor, 0, 5 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="StevensonArceDiffuser"/> class.
/// </summary>
public StevensonArceDiffuser()
: base(StevensonArceMatrix)
{
}
}
}

33
src/ImageSharp/Processing/Processors/Dithering/StuckiDiffuser.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
/// <summary>
/// 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
{
private const float Divisor = 42F;
/// <summary>
/// The diffusion matrix
/// </summary>
private static readonly DenseMatrix<float> StuckiMatrix =
new float[,]
{
{ 0, 0, 0, 8 / Divisor, 4 / Divisor },
{ 2 / Divisor, 4 / Divisor, 8 / Divisor, 4 / Divisor, 2 / Divisor },
{ 1 / Divisor, 2 / Divisor, 4 / Divisor, 2 / Divisor, 1 / Divisor }
};
/// <summary>
/// Initializes a new instance of the <see cref="StuckiDiffuser"/> class.
/// </summary>
public StuckiDiffuser()
: base(StuckiMatrix)
{
}
}
}

BIN
src/ImageSharp/Processing/Processors/Dithering/optimal-parallel-error-diffusion-dithering.pdf

Binary file not shown.

106
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -0,0 +1,106 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Gets the closest color to the supplied color based upon the Eucladean distance.
/// TODO: Expose this somehow.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>>
where TPixel : struct, IPixel<TPixel>
{
private readonly ConcurrentDictionary<int, Vector4> vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache;
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary>
/// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(ReadOnlyMemory<TPixel> palette)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
this.Palette = palette;
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
this.vectorCache = new ConcurrentDictionary<int, Vector4>();
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
for (int i = 0; i < paletteSpan.Length; i++)
{
this.vectorCache[i] = paletteSpan[i].ToScaledVector4();
}
}
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette { get; }
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map);
/// <inheritdoc/>
public bool Equals(EuclideanPixelMap<TPixel> other)
=> this.Palette.Equals(other.Palette);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public int 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 int index))
{
match = paletteSpan[index];
return index;
}
return this.GetClosestColorSlow(color, paletteSpan, out match);
}
/// <inheritdoc/>
public override int GetHashCode()
=> this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)]
private int 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);
// Less than... assign.
if (distance < leastDistance)
{
index = i;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance == 0)
{
break;
}
}
}
// Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index;
match = palette[index];
return index;
}
}
}

136
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs

@ -0,0 +1,136 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Contains extension methods for frame quantizers.
/// </summary>
public static class FrameQuantizerExtensions
{
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
/// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame </param>
/// <param name="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns>
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(source, nameof(source));
var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
Memory<byte> output = quantizedFrame.GetWritablePixelMemory();
if (quantizer.Options.Dither is null)
{
SecondPass(ref quantizer, source, interest, output, palette);
}
else
{
// We clone the image as we don't want to alter the original via error diffusion based dithering.
using (ImageFrame<TPixel> clone = source.Clone())
{
SecondPass(ref quantizer, clone, interest, output, palette);
}
}
return quantizedFrame;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds,
Memory<byte> output,
ReadOnlyMemory<TPixel> palette)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
IDither dither = quantizer.Options.Dither;
if (dither is null)
{
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, output, bounds, palette);
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in operation);
return;
}
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds);
}
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
in TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette)
{
this.quantizer = quantizer;
this.source = source;
this.output = output;
this.bounds = bounds;
this.palette = palette;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
Span<byte> outputSpan = this.output.Span;
int width = this.bounds.Width;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width;
// TODO: This can be a bulk operation.
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
outputSpan[rowStart + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _);
}
}
}
}
}
}

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

@ -1,285 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// The base class for all <see cref="IFrameQuantizer{TPixel}"/> implementations
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
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 bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The quantizer</param>
/// <param name="singlePass">
/// If true, the quantization process only needs to loop through the source pixels once
/// </param>
/// <remarks>
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
/// 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, IQuantizer quantizer, bool singlePass)
{
Guard.NotNull(quantizer, nameof(quantizer));
this.Configuration = configuration;
this.Diffuser = quantizer.Diffuser;
this.Dither = this.Diffuser != null;
this.singlePass = singlePass;
}
/// <summary>
/// Initializes a new instance of the <see cref="FrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="diffuser">The diffuser</param>
/// <param name="singlePass">
/// If true, the quantization process only needs to loop through the source pixels once
/// </param>
/// <remarks>
/// If you construct this class with a <value>true</value> for <paramref name="singlePass"/>, then the code will
/// 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)
{
this.Configuration = configuration;
this.Diffuser = diffuser;
this.Dither = this.Diffuser != null;
this.singlePass = singlePass;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
/// <inheritdoc />
public bool Dither { get; }
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.
/// </summary>
protected Configuration Configuration { get; }
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public IQuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image)
{
Guard.NotNull(image, nameof(image));
// Get the size of the source image
int height = image.Height;
int width = image.Width;
// Call the FirstPass function if not a single pass algorithm.
// For something like an Octree quantizer, this will run through
// all image pixels, build a data structure, and create a palette.
if (!this.singlePass)
{
this.FirstPass(image, width, height);
}
// Collect the palette. Required before the second pass runs.
ReadOnlyMemory<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);
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, width, height, palette);
Span<byte> pixelSpan = quantizedFrame.GetWritablePixelSpan();
if (this.Dither)
{
// We clone the image as we don't want to alter the original via dithering.
using (ImageFrame<TPixel> clone = image.Clone())
{
this.SecondPass(clone, pixelSpan, palette.Span, width, height);
}
}
else
{
this.SecondPass(image, pixelSpan, palette.Span, width, height);
}
return quantizedFrame;
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">Whether to dispose managed and unmanaged objects.</param>
protected virtual void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
this.paletteVector?.Dispose();
}
this.paletteVector = null;
this.isDisposed = true;
}
/// <summary>
/// Execute the first pass through the pixels in the image to create the palette.
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="width">The width in pixels of the image.</param>
/// <param name="height">The height in pixels of the image.</param>
protected virtual void FirstPass(ImageFrame<TPixel> source, int width, int height)
{
}
/// <summary>
/// Returns the closest color from the palette to the given color by calculating the
/// Euclidean distance in the Rgba colorspace.
/// </summary>
/// <param name="pixel">The 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);
}
/// <summary>
/// Retrieve the palette for the quantized image.
/// </summary>
/// <returns>
/// <see cref="ReadOnlyMemory{TPixel}"/>
/// </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>
/// <param name="source">The source image.</param>
/// <param name="output">The output pixel array.</param>
/// <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(
ImageFrame<TPixel> source,
Span<byte> output,
ReadOnlySpan<TPixel> palette,
int width,
int height);
[MethodImpl(MethodImplOptions.NoInlining)]
private byte GetClosestPixelSlow(ref TPixel pixel)
{
// Loop through the palette and find the nearest match.
int colorIndex = 0;
float leastDistance = float.MaxValue;
Vector4 vector = pixel.ToScaledVector4();
float epsilon = Constants.EpsilonSquared;
Span<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);
// Greater... Move on.
if (!(distance < leastDistance))
{
continue;
}
colorIndex = index;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance < epsilon)
{
break;
}
}
// Now I have the index, pop it into the cache for next time
byte result = (byte)colorIndex;
this.distanceCache.Add(pixel, result);
return result;
}
}
}

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

@ -1,9 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
@ -15,22 +14,45 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets a value indicating whether to apply dithering to the output image.
/// Gets the configuration.
/// </summary>
bool Dither { get; }
Configuration Configuration { get; }
/// <summary>
/// Gets the error diffusion algorithm to apply to the output image.
/// Gets the quantizer options defining quantization rules.
/// </summary>
IErrorDiffuser Diffuser { get; }
QuantizerOptions Options { get; }
/// <summary>
/// Quantize an image frame and return the resulting output pixels.
/// Quantizes an image frame and return the resulting output pixels.
/// </summary>
/// <param name="image">The image to quantize.</param>
/// <param name="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns>
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the image pixels.
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
IQuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image);
QuantizedFrame<TPixel> QuantizeFrame(
ImageFrame<TPixel> source,
Rectangle bounds);
/// <summary>
/// Builds the quantized palette from the given image frame and bounds.
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="bounds">The region of interest bounds.</param>
/// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns>
ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Returns the index and color from the quantized palette corresponding to the give to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match);
// TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);
}
}
}

30
src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs

@ -0,0 +1,30 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Allows the mapping of input colors to colors within a given palette.
/// TODO: Expose this somehow.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal interface IPixelMap<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets the color palette containing colors to match.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
int GetClosestColor(TPixel color, out TPixel match);
}
}

38
src/ImageSharp/Processing/Processors/Quantization/IQuantizedFrame{TPixel}.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Defines an abstraction to represent a quantized image frame where the pixels indexed by a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public interface IQuantizedFrame<TPixel> : IDisposable
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Gets the width of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="Span{T}"/>The pixel span.</returns>
ReadOnlySpan<byte> GetPixelSpan();
}
}

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

@ -1,8 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
@ -12,27 +11,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public interface IQuantizer
{
/// <summary>
/// Gets the error diffusion algorithm to apply to the output image.
/// Gets the quantizer options defining quantization rules.
/// </summary>
IErrorDiffuser Diffuser { get; }
QuantizerOptions Options { get; }
/// <summary>
/// Creates the generic frame quantizer
/// Creates the generic frame quantizer.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/>.</returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// Creates the generic frame quantizer
/// Creates the generic frame quantizer.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <param name="options">The options to create the quantizer with.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/>.</returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>;
}
}
}

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

@ -2,11 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
@ -16,168 +17,114 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class OctreeFrameQuantizer<TPixel> : FrameQuantizer<TPixel>
public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Maximum allowed color depth
/// </summary>
private readonly int colors;
/// <summary>
/// Stores the tree
/// </summary>
private readonly Octree octree;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
/// <summary>
/// The transparent index
/// </summary>
private byte transparentIndex;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The octree quantizer</param>
/// <remarks>
/// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree,
/// the second pass quantizes a color based on the nodes in the tree
/// </remarks>
public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer)
: this(configuration, quantizer, quantizer.MaxColors)
/// <param name="options">The quantizer options defining quantization rules.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public OctreeFrameQuantizer(Configuration configuration, QuantizerOptions options)
{
}
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="quantizer">The octree quantizer.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <remarks>
/// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree,
/// the second pass quantizes a color based on the nodes in the tree
/// </remarks>
public OctreeFrameQuantizer(Configuration configuration, OctreeQuantizer quantizer, int maxColors)
: base(configuration, quantizer, false)
{
this.colors = maxColors;
this.Configuration = configuration;
this.Options = options;
this.colors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
this.pixelMap = default;
this.isDithering = !(this.Options.Dither is null);
}
/// <inheritdoc/>
protected override void FirstPass(ImageFrame<TPixel> source, int width, int height)
{
// Loop through each row
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row);
public Configuration Configuration { get; }
// And loop through each column
for (int x = 0; x < width; x++)
{
ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x);
/// <inheritdoc/>
public QuantizerOptions Options { get; }
// Add the color to the Octree
this.octree.AddColor(ref pixel);
}
}
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
/// <inheritdoc/>
protected override void SecondPass(
ImageFrame<TPixel> source,
Span<byte> output,
ReadOnlySpan<TPixel> palette,
int width,
int height)
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
this.transparentIndex = this.GetTransparentIndex();
byte pixelValue = this.QuantizePixel(ref sourcePixel);
TPixel transformedPixel = palette[pixelValue];
for (int y = 0; y < height; y++)
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
// Loop through each row
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
Span<TPixel> row = source.GetPixelRowSpan(y).Slice(bounds.Left, bounds.Width);
PixelOperations<TPixel>.Instance.ToRgba32(this.Configuration, row, bufferSpan);
// And loop through each column
for (int x = 0; x < width; x++)
for (int x = 0; x < bufferSpan.Length; x++)
{
// Get the pixel.
sourcePixel = row[x];
Rgba32 rgba = bufferSpan[x];
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
{
// Quantize the pixel
pixelValue = this.QuantizePixel(ref sourcePixel);
// And setup the previous pointer
previousPixel = sourcePixel;
if (this.Dither)
{
transformedPixel = palette[pixelValue];
}
}
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height);
}
output[(y * source.Width) + x] = pixelValue;
// Add the color to the Octree
this.octree.AddColor(rgba);
}
}
}
internal ReadOnlyMemory<TPixel> AotGetPalette() => this.GetPalette();
TPixel[] palette = this.octree.Palletize(this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
/// <inheritdoc/>
protected override ReadOnlyMemory<TPixel> GetPalette() => this.octree.Palletize(this.colors);
return palette;
}
/// <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)
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
{
if (this.Dither)
// Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent
// pixel and a black one.
if (!this.isDithering && !color.Equals(default))
{
// The colors have changed so we need to use Euclidean distance calculation to
// find the closest value.
return this.GetClosestPixel(ref pixel);
var index = (byte)this.octree.GetPaletteIndex(color);
match = palette[index];
return index;
}
Rgba32 rgba = default;
pixel.ToRgba32(ref rgba);
if (rgba.Equals(default))
{
return this.transparentIndex;
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
return (byte)this.octree.GetPaletteIndex(ref pixel);
/// <inheritdoc/>
public void Dispose()
{
}
/// <summary>
/// Class which does the actual quantization
/// 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
/// Mask used when getting the appropriate pixels for a given node.
/// </summary>
// ReSharper disable once StaticMemberInGenericType
private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
private static readonly byte[] Mask = new byte[]
{
0b10000000,
0b1000000,
0b100000,
0b10000,
0b1000,
0b100,
0b10,
0b1
};
/// <summary>
/// The root of the Octree
@ -197,7 +144,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Cache the previous color quantized
/// </summary>
private TPixel previousColor;
private Rgba32 previousColor;
/// <summary>
/// Initializes a new instance of the <see cref="Octree"/> class.
@ -220,10 +167,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,36 +179,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private OctreeNode[] ReducibleNodes
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <summary>
/// Add a given color value to the Octree
/// </summary>
/// <param name="pixel">The pixel data.</param>
public void AddColor(ref TPixel pixel)
/// <param name="color">The color to add.</param>
public void AddColor(Rgba32 color)
{
// Check if this request is for the same color as the last
if (this.previousColor.Equals(pixel))
if (this.previousColor.Equals(color))
{
// If so, check if I have a previous node setup. This will only occur if the first color in the image
// If so, check if I have a previous node setup.
// This will only occur if the first color in the image
// happens to be black, with an alpha component of zero.
if (this.previousNode is null)
{
this.previousColor = pixel;
this.root.AddColor(ref pixel, this.maxColorBits, 0, this);
this.previousColor = color;
this.root.AddColor(ref color, this.maxColorBits, 0, this);
}
else
{
// Just update the previous node
this.previousNode.Increment(ref pixel);
this.previousNode.Increment(ref color);
}
}
else
{
this.previousColor = pixel;
this.root.AddColor(ref pixel, this.maxColorBits, 0, this);
this.previousColor = color;
this.root.AddColor(ref color, this.maxColorBits, 0, this);
}
}
@ -272,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <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)
@ -293,12 +241,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Get the palette index for the passed color
/// </summary>
/// <param name="pixel">The pixel data.</param>
/// <param name="color">The color to match.</param>
/// <returns>
/// The <see cref="int"/>.
/// The <see cref="int"/> index.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPaletteIndex(ref TPixel pixel) => this.root.GetPaletteIndex(ref pixel, 0);
[MethodImpl(InliningOptions.ShortMethod)]
public int GetPaletteIndex(TPixel color)
{
Rgba32 rgba = default;
color.ToRgba32(ref rgba);
return this.root.GetPaletteIndex(ref rgba, 0);
}
/// <summary>
/// Keep track of the previous node that was quantized
@ -306,8 +259,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 +289,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
@ -376,15 +329,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
/// </summary>
/// <param name="level">
/// The level in the tree = 0 - 7
/// </param>
/// <param name="colorBits">
/// The number of significant color bits in the image
/// </param>
/// <param name="octree">
/// The tree to which this node belongs
/// </param>
/// <param name="level">The level in the tree = 0 - 7.</param>
/// <param name="colorBits">The number of significant color bits in the image.</param>
/// <param name="octree">The tree to which this node belongs.</param>
public OctreeNode(int level, int colorBits, Octree octree)
{
// Construct the new node
@ -414,23 +361,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public OctreeNode NextReducible
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <summary>
/// Add a color into the tree
/// </summary>
/// <param name="pixel">The pixel color</param>
/// <param name="colorBits">The number of significant color bits</param>
/// <param name="level">The level in the tree</param>
/// <param name="octree">The tree to which this node belongs</param>
public void AddColor(ref TPixel pixel, int colorBits, int level, Octree octree)
/// <param name="color">The color to add.</param>
/// <param name="colorBits">The number of significant color bits.</param>
/// <param name="level">The level in the tree.</param>
/// <param name="octree">The tree to which this node belongs.</param>
public void AddColor(ref Rgba32 color, int colorBits, int level, Octree octree)
{
// Update the color information if this is a leaf
if (this.leaf)
{
this.Increment(ref pixel);
this.Increment(ref color);
// Setup the previous node
octree.TrackPrevious(this);
@ -438,13 +385,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
else
{
// Go to the next level down in the tree
int shift = 7 - level;
Rgba32 rgba = default;
pixel.ToRgba32(ref rgba);
int index = ((rgba.B & Mask[level]) >> (shift - 2))
| ((rgba.G & Mask[level]) >> (shift - 1))
| ((rgba.R & Mask[level]) >> shift);
int index = GetColorIndex(ref color, level);
OctreeNode child = this.children[index];
if (child is null)
@ -455,7 +396,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
// Add the color to the child node
child.AddColor(ref pixel, colorBits, level + 1, octree);
child.AddColor(ref color, colorBits, level + 1, octree);
}
}
@ -495,7 +436,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param>
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index)
{
if (this.leaf)
@ -527,29 +468,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>
/// The <see cref="int"/> representing the index of the pixel in the palette.
/// </returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public int GetPaletteIndex(ref TPixel pixel, int level)
[MethodImpl(InliningOptions.ColdPath)]
public int GetPaletteIndex(ref Rgba32 pixel, int level)
{
int index = this.paletteIndex;
if (!this.leaf)
if (this.leaf)
{
int shift = 7 - level;
Rgba32 rgba = default;
pixel.ToRgba32(ref rgba);
return this.paletteIndex;
}
int pixelIndex = ((rgba.B & Mask[level]) >> (shift - 2))
| ((rgba.G & Mask[level]) >> (shift - 1))
| ((rgba.R & Mask[level]) >> shift);
int colorIndex = GetColorIndex(ref pixel, level);
OctreeNode child = this.children[colorIndex];
OctreeNode child = this.children[pixelIndex];
if (child != null)
{
index = child.GetPaletteIndex(ref pixel, level + 1);
}
else
int index = 0;
if (child != null)
{
index = child.GetPaletteIndex(ref pixel, level + 1);
}
else
{
// Check other children.
for (int i = 0; i < this.children.Length; i++)
{
throw new Exception($"Cannot retrieve a pixel at the given index {pixelIndex}.");
child = this.children[i];
if (child != null)
{
var childIndex = child.GetPaletteIndex(ref pixel, level + 1);
if (childIndex != 0)
{
return childIndex;
}
}
}
}
@ -557,18 +505,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <summary>
/// Increment the pixel count and add to the color information
/// Gets the color index at the given level.
/// </summary>
/// <param name="color">The color.</param>
/// <param name="level">The node level.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetColorIndex(ref Rgba32 color, int level)
{
int shift = 7 - level;
byte mask = Mask[level];
return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
}
/// <summary>
/// Increment the color count and add to the color information
/// </summary>
/// <param name="pixel">The pixel to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Increment(ref TPixel pixel)
/// <param name="color">The pixel to add.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void Increment(ref Rgba32 color)
{
Rgba32 rgba = default;
pixel.ToRgba32(ref rgba);
this.pixelCount++;
this.red += rgba.R;
this.green += rgba.G;
this.blue += rgba.B;
this.red += color.R;
this.green += color.G;
this.blue += color.B;
}
}
}

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

@ -2,97 +2,45 @@
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// 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>
/// </para>
/// </summary>
public class OctreeQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public OctreeQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image.</param>
public OctreeQuantizer(bool dither)
: this(GetDiffuser(dither), QuantizerConstants.MaxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <param name="dither">Whether to apply dithering to the output image.</param>
public OctreeQuantizer(bool dither, int maxColors)
: this(GetDiffuser(dither), maxColors)
{
}
/// <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)
: this(diffuser, QuantizerConstants.MaxColors)
: this(new QuantizerOptions())
{
}
/// <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="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors)
/// <param name="options">The quantizer options defining quantization rules.</param>
public OctreeQuantizer(QuantizerOptions options)
{
this.Diffuser = diffuser;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
Guard.NotNull(options, nameof(options));
this.Options = options;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
/// </summary>
public int MaxColors { get; }
public QuantizerOptions Options { get; }
/// <param name="configuration"></param>
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
=> new OctreeFrameQuantizer<TPixel>(configuration, this);
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
return new OctreeFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
=> new OctreeFrameQuantizer<TPixel>(configuration, options);
}
}

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

@ -3,10 +3,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
@ -15,88 +12,55 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal sealed class PaletteFrameQuantizer<TPixel> : FrameQuantizer<TPixel>
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// The reduced image palette.
/// </summary>
private readonly ReadOnlyMemory<TPixel> palette;
private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> class.
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct.
/// </summary>
/// <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)
: base(configuration, diffuser, true) => this.palette = colors;
/// <inheritdoc/>
protected override void SecondPass(
ImageFrame<TPixel> source,
Span<byte> output,
ReadOnlySpan<TPixel> palette,
int width,
int height)
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> colors)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(ref sourcePixel);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette);
TPixel transformedPixel = Unsafe.Add(ref paletteRef, pixelValue);
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
for (int y = 0; y < height; y++)
{
ref TPixel rowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
this.Configuration = configuration;
this.Options = options;
// And loop through each column
for (int x = 0; x < width; x++)
{
// Get the pixel.
sourcePixel = Unsafe.Add(ref rowRef, x);
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
{
// Quantize the pixel
pixelValue = this.QuantizePixel(ref sourcePixel);
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
}
// And setup the previous pointer
previousPixel = sourcePixel;
/// <inheritdoc/>
public Configuration Configuration { get; }
if (this.Dither)
{
transformedPixel = Unsafe.Add(ref paletteRef, pixelValue);
}
}
/// <inheritdoc/>
public QuantizerOptions Options { get; }
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.Diffuser.Dither(source, sourcePixel, transformedPixel, x, y, 0, width, height);
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
output[(y * source.Width) + x] = pixelValue;
}
}
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override ReadOnlyMemory<TPixel> GetPalette() => this.palette;
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match);
/// <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);
/// <inheritdoc/>
public void Dispose()
{
}
}
}

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

@ -2,80 +2,62 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// 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.
/// </para>
/// </summary>
public class PaletteQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="palette">The color palette.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette)
: this(palette, true)
: this(palette, new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="dither">Whether to apply dithering to the output image</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, bool dither)
: this(palette, GetDiffuser(dither))
/// <param name="palette">The color palette.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette, QuantizerOptions options)
{
}
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
/// <summary>
/// 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)
{
this.Palette = palette;
this.Diffuser = diffuser;
this.Options = options;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
/// <summary>
/// Gets the palette.
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public QuantizerOptions Options { get; }
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
{
var palette = new TPixel[this.Palette.Length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, this.Diffuser, palette);
}
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
int max = Math.Min(maxColors, this.Palette.Length);
Guard.NotNull(options, nameof(options));
var palette = new TPixel[max];
Color.ToPixel(configuration, this.Palette.Span.Slice(0, max), palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, this.Diffuser, palette);
}
int length = Math.Min(this.Palette.Span.Length, options.MaxColors);
var palette = new TPixel[length];
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
}
}
}

4
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs

@ -15,9 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="quantizer">The quantizer used to reduce the color palette.</param>
public QuantizeProcessor(IQuantizer quantizer)
{
this.Quantizer = quantizer;
}
=> this.Quantizer = quantizer;
/// <summary>
/// Gets the quantizer.

23
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -35,14 +35,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc />
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle);
Configuration configuration = this.Configuration;
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration);
using IQuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source);
using QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source, interest);
var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
ParallelRowIterator.IterateRows(
configuration,
this.SourceRectangle,
interest,
in operation);
}
@ -50,19 +52,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly IQuantizedFrame<TPixel> quantized;
private readonly int maxPaletteIndex;
private readonly QuantizedFrame<TPixel> quantized;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
ImageFrame<TPixel> source,
IQuantizedFrame<TPixel> quantized)
QuantizedFrame<TPixel> quantized)
{
this.bounds = bounds;
this.source = source;
this.quantized = quantized;
this.maxPaletteIndex = quantized.Palette.Length - 1;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -70,16 +70,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
int width = this.bounds.Width;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
int yy = y * this.bounds.Width;
int rowStart = (y - offsetY) * width;
for (int x = this.bounds.X; x < this.bounds.Right; x++)
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
int i = x + yy;
row[x] = paletteSpan[Math.Min(this.maxPaletteIndex, quantizedPixelSpan[i])];
int i = rowStart + x - offsetX;
row[x] = paletteSpan[quantizedPixelSpan[i]];
}
}
}

29
src/ImageSharp/Processing/Processors/Quantization/QuantizedFrameExtensions.cs

@ -1,29 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Contains extension methods for <see cref="IQuantizedFrame{TPixel}"/>.
/// </summary>
public static class QuantizedFrameExtensions
{
/// <summary>
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="frame">The <see cref="IQuantizedFrame{TPixel}"/>.</param>
/// <param name="rowIndex">The row.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static ReadOnlySpan<byte> GetRowSpan<TPixel>(this IQuantizedFrame<TPixel> frame, int rowIndex)
where TPixel : struct, IPixel<TPixel>
=> frame.GetPixelSpan().Slice(rowIndex * frame.Width, frame.Width);
}
}

27
src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -13,10 +13,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Represents a quantized image frame where the pixels indexed by a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public class QuantizedFrame<TPixel> : IQuantizedFrame<TPixel>
public sealed class QuantizedFrame<TPixel> : IDisposable
where TPixel : struct, IPixel<TPixel>
{
private IMemoryOwner<byte> pixels;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="QuantizedFrame{TPixel}"/> class.
@ -58,17 +59,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelSpan() => this.pixels.GetSpan();
/// <summary>
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="rowIndex">The row.</param>
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetRowSpan(int rowIndex)
=> this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.pixels?.Dispose();
this.pixels = null;
this.Palette = null;
}
/// <summary>
/// Get the non-readonly span of pixel data so <see cref="FrameQuantizer{TPixel}"/> can fill it.
/// Get the non-readonly memory of pixel data so <see cref="IFrameQuantizer{TPixel}"/> can fill it.
/// </summary>
internal Span<byte> GetWritablePixelSpan() => this.pixels.GetSpan();
internal Memory<byte> GetWritablePixelMemory() => this.pixels.Memory;
}
}
}

23
src/ImageSharp/Processing/Processors/Quantization/QuantizerConstants.cs

@ -1,12 +1,14 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Contains color quantization specific constants.
/// </summary>
internal static class QuantizerConstants
public static class QuantizerConstants
{
/// <summary>
/// The minimum number of colors to use when quantizing an image.
@ -17,5 +19,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// The maximum number of colors to use when quantizing an image.
/// </summary>
public const int MaxColors = 256;
/// <summary>
/// The minumim dithering scale used to adjust the amount of dither.
/// </summary>
public const float MinDitherScale = 0;
/// <summary>
/// The max dithering scale used to adjust the amount of dither.
/// </summary>
public const float MaxDitherScale = 1F;
/// <summary>
/// Gets the default dithering algorithm to use.
/// </summary>
public static IDither DefaultDither { get; } = KnownDitherings.FloydSteinberg;
}
}
}

42
src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs

@ -0,0 +1,42 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Defines options for quantization.
/// </summary>
public class QuantizerOptions
{
private float ditherScale = QuantizerConstants.MaxDitherScale;
private int maxColors = QuantizerConstants.MaxColors;
/// <summary>
/// Gets or sets the algorithm to apply to the output image.
/// Defaults to <see cref="QuantizerConstants.DefaultDither"/>; set to <see langword="null"/> for no dithering.
/// </summary>
public IDither Dither { get; set; } = QuantizerConstants.DefaultDither;
/// <summary>
/// Gets or sets the dithering scale used to adjust the amount of dither. Range 0..1.
/// Defaults to <see cref="QuantizerConstants.MaxDitherScale"/>.
/// </summary>
public float DitherScale
{
get { return this.ditherScale; }
set { this.ditherScale = value.Clamp(QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); }
}
/// <summary>
/// Gets or sets the maximum number of colors to hold in the color palette. Range 0..256.
/// Defaults to <see cref="QuantizerConstants.MaxColors"/>.
/// </summary>
public int MaxColors
{
get { return this.maxColors; }
set { this.maxColors = value.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors); }
}
}
}

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -14,26 +14,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
public WebSafePaletteQuantizer()
: this(true)
: this(new QuantizerOptions())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WebSafePaletteQuantizer(bool dither)
: base(Color.WebSafePalette, dither)
{
}
/// <summary>
/// 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)
: base(Color.WebSafePalette, diffuser)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WebSafePaletteQuantizer(QuantizerOptions options)
: base(Color.WebSafePalette, options)
{
}
}
}
}

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

@ -1,8 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
@ -15,26 +13,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
public WernerPaletteQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WernerPaletteQuantizer(bool dither)
: base(Color.WernerPalette, dither)
: this(new QuantizerOptions())
{
}
/// <summary>
/// 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)
: base(Color.WernerPalette, diffuser)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WernerPaletteQuantizer(QuantizerOptions options)
: base(Color.WernerPalette, options)
{
}
}
}
}

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

File diff suppressed because it is too large

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

@ -2,89 +2,44 @@
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <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>
/// </para>
/// </summary>
public class WuQuantizer : IQuantizer
{
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public WuQuantizer()
: this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WuQuantizer(bool dither)
: this(GetDiffuser(dither), QuantizerConstants.MaxColors)
{
}
/// <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)
: this(diffuser, QuantizerConstants.MaxColors)
: this(new QuantizerOptions())
{
}
/// <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="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(IErrorDiffuser diffuser, int maxColors)
/// <param name="options">The quantizer options defining quantization rules.</param>
public WuQuantizer(QuantizerOptions options)
{
this.Diffuser = diffuser;
this.MaxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
Guard.NotNull(options, nameof(options));
this.Options = options;
}
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
/// <summary>
/// Gets the maximum number of colors to hold in the color palette.
/// </summary>
public int MaxColors { get; }
public QuantizerOptions Options { get; }
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
return new WuFrameQuantizer<TPixel>(configuration, this);
}
=> this.CreateFrameQuantizer<TPixel>(configuration, this.Options);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, int maxColors)
/// <inheritdoc />
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(Configuration configuration, QuantizerOptions options)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
maxColors = maxColors.Clamp(QuantizerConstants.MinColors, QuantizerConstants.MaxColors);
return new WuFrameQuantizer<TPixel>(configuration, this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
=> new WuFrameQuantizer<TPixel>(configuration, options);
}
}

11
tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Drawing.Imaging;
@ -6,6 +6,7 @@ using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests;
using SDImage = System.Drawing.Image;
@ -53,11 +54,15 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
public void GifCore()
{
// Try to get as close to System.Drawing's output as possible
var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) };
var options = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (var memoryStream = new MemoryStream())
{
this.bmpCore.SaveAsGif(memoryStream, options);
}
}
}
}
}

7
tests/ImageSharp.Benchmarks/Codecs/EncodeGifMultiple.cs

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Drawing.Imaging;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Benchmarks.Codecs
@ -23,7 +24,11 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
this.ForEachImageSharpImage((img, ms) =>
{
// Try to get as close to System.Drawing's output as possible
var options = new GifEncoder { Quantizer = new WebSafePaletteQuantizer(false) };
var options = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
img.Save(ms, options);
return null;
});

10
tests/ImageSharp.Benchmarks/Codecs/EncodeIndexedPng.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
using (var memoryStream = new MemoryStream())
{
var options = new PngEncoder { Quantizer = new OctreeQuantizer(false) };
var options = new PngEncoder { Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null }) };
this.bmpCore.SaveAsPng(memoryStream, options);
}
}
@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
using (var memoryStream = new MemoryStream())
{
var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(false) };
var options = new PngEncoder { Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null }) };
this.bmpCore.SaveAsPng(memoryStream, options);
}
}
@ -95,9 +95,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
{
using (var memoryStream = new MemoryStream())
{
var options = new PngEncoder { Quantizer = new WuQuantizer(false) };
var options = new PngEncoder { Quantizer = new WuQuantizer(new QuantizerOptions { Dither = null }) };
this.bmpCore.SaveAsPng(memoryStream, options);
}
}
}
}
}

35
tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs

@ -15,7 +15,18 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
{
using (var image = new Image<Rgba32>(Configuration.Default, 800, 800, Color.BlanchedAlmond))
{
image.Mutate(x => x.Diffuse());
image.Mutate(x => x.Dither(KnownDitherings.FloydSteinberg));
return image.Size();
}
}
[Benchmark]
public Size DoDither()
{
using (var image = new Image<Rgba32>(Configuration.Default, 800, 800, Color.BlanchedAlmond))
{
image.Mutate(x => x.Dither());
return image.Size();
}
@ -48,3 +59,25 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
// |---------- |----- |-------- |----------:|----------:|----------:|------:|------:|------:|----------:|
// | DoDiffuse | Clr | Clr | 124.93 ms | 33.297 ms | 1.8251 ms | - | - | - | 2 KB |
// | DoDiffuse | Core | Core | 89.63 ms | 9.895 ms | 0.5424 ms | - | - | - | 1.91 KB |
// #### 20th February 2020 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.1.101
//
// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
// Job-OJKYBT : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
// Job-RZWLFP : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
// Job-NUYUQV : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |---------- |-------------- |----------:|----------:|----------:|------:|------:|------:|----------:|
// | DoDiffuse | .NET 4.7.2 | 30.535 ms | 19.217 ms | 1.0534 ms | - | - | - | 26.25 KB |
// | DoDither | .NET 4.7.2 | 14.174 ms | 1.625 ms | 0.0891 ms | - | - | - | 31.38 KB |
// | DoDiffuse | .NET Core 2.1 | 15.984 ms | 3.686 ms | 0.2020 ms | - | - | - | 25.98 KB |
// | DoDither | .NET Core 2.1 | 8.646 ms | 1.635 ms | 0.0896 ms | - | - | - | 28.99 KB |
// | DoDiffuse | .NET Core 3.1 | 16.235 ms | 9.612 ms | 0.5269 ms | - | - | - | 25.96 KB |
// | DoDither | .NET Core 3.1 | 8.429 ms | 1.270 ms | 0.0696 ms | - | - | - | 31.61 KB |

4
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -197,7 +197,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
var encoder = new BmpEncoder
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new WuQuantizer(256)
Quantizer = new WuQuantizer()
};
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false);
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
@ -223,7 +223,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
var encoder = new BmpEncoder
{
BitsPerPixel = BmpBitsPerPixel.Pixel8,
Quantizer = new OctreeQuantizer(256)
Quantizer = new OctreeQuantizer()
};
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "bmp", encoder, appendPixelTypeToFileName: false);
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);

3
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
@ -90,7 +91,7 @@ namespace SixLabors.ImageSharp.Tests
private static IQuantizer GetQuantizer(string name)
{
PropertyInfo property = typeof(KnownQuantizers).GetTypeInfo().GetProperty(name);
return (IQuantizer)property.GetMethod.Invoke(null, new object[0]);
return (IQuantizer)property.GetMethod.Invoke(null, Array.Empty<object>());
}
[Fact]

6
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
// Use the palette quantizer without dithering to ensure results
// are consistent
Quantizer = new WebSafePaletteQuantizer(false)
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = null })
};
// Always save as we need to compare the encoded output.
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
var encoder = new GifEncoder
{
ColorTableMode = GifColorTableMode.Global,
Quantizer = new OctreeQuantizer(false)
Quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null })
};
// Always save as we need to compare the encoded output.
@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
var encoder = new GifEncoder
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(frameMetadata.ColorTableLength)
Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength })
};
image.Save(outStream, encoder);

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -448,7 +448,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(paletteSize),
Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = paletteSize }),
InterlaceMethod = interlaceMode
};

20
tests/ImageSharp.Tests/Primitives/DenseMatrixTests.cs

@ -116,5 +116,25 @@ namespace SixLabors.ImageSharp.Tests.Primitives
Assert.Equal(2, transposed[1, 0]);
Assert.Equal(3, transposed[2, 0]);
}
[Fact]
public void DenseMatrixEquality()
{
var dense = new DenseMatrix<int>(3, 1);
var dense2 = new DenseMatrix<int>(3, 1);
var dense3 = new DenseMatrix<int>(1, 3);
Assert.True(dense == dense2);
Assert.False(dense != dense2);
Assert.Equal(dense, dense2);
Assert.Equal(dense, (object)dense2);
Assert.Equal(dense.GetHashCode(), dense2.GetHashCode());
Assert.False(dense == dense3);
Assert.True(dense != dense3);
Assert.NotEqual(dense, dense3);
Assert.NotEqual(dense, (object)dense3);
Assert.NotEqual(dense.GetHashCode(), dense3.GetHashCode());
}
}
}

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

@ -1,106 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Binarization;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Binarization
{
public class BinaryDitherTest : BaseImageOperationsExtensionTest
{
private readonly IOrderedDither orderedDither;
private readonly IErrorDiffuser errorDiffuser;
public BinaryDitherTest()
{
this.orderedDither = KnownDitherers.BayerDither4x4;
this.errorDiffuser = KnownDiffusers.FloydSteinberg;
}
[Fact]
public void BinaryDither_CorrectProcessor()
{
this.operations.BinaryDither(this.orderedDither);
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.White, p.UpperColor);
Assert.Equal(Color.Black, p.LowerColor);
}
[Fact]
public void BinaryDither_rect_CorrectProcessor()
{
this.operations.BinaryDither(this.orderedDither, this.rect);
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.White, p.UpperColor);
Assert.Equal(Color.Black, p.LowerColor);
}
[Fact]
public void BinaryDither_index_CorrectProcessor()
{
this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink);
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.Yellow, p.UpperColor);
Assert.Equal(Color.HotPink, p.LowerColor);
}
[Fact]
public void BinaryDither_index_rect_CorrectProcessor()
{
this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink, this.rect);
BinaryOrderedDitherProcessor p = this.Verify<BinaryOrderedDitherProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.HotPink, p.LowerColor);
}
[Fact]
public void BinaryDither_ErrorDiffuser_CorrectProcessor()
{
this.operations.BinaryDiffuse(this.errorDiffuser, .4F);
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.4F, p.Threshold);
Assert.Equal(Color.White, p.UpperColor);
Assert.Equal(Color.Black, p.LowerColor);
}
[Fact]
public void BinaryDither_ErrorDiffuser_rect_CorrectProcessor()
{
this.operations.BinaryDiffuse(this.errorDiffuser, .3F, this.rect);
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.3F, p.Threshold);
Assert.Equal(Color.White, p.UpperColor);
Assert.Equal(Color.Black, p.LowerColor);
}
[Fact]
public void BinaryDither_ErrorDiffuser_CorrectProcessorWithColors()
{
this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow);
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
Assert.Equal(Color.HotPink, p.UpperColor);
Assert.Equal(Color.Yellow, p.LowerColor);
}
[Fact]
public void BinaryDither_ErrorDiffuser_rect_CorrectProcessorWithColors()
{
this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow, this.rect);
BinaryErrorDiffusionProcessor p = this.Verify<BinaryErrorDiffusionProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
Assert.Equal(Color.HotPink, p.UpperColor);
Assert.Equal(Color.Yellow, p.LowerColor);
}
}
}

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

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Binarization
@ -20,8 +18,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
}
}
private readonly IOrderedDither orderedDither;
private readonly IErrorDiffuser errorDiffuser;
private readonly IDither orderedDither;
private readonly IDither errorDiffuser;
private readonly Color[] testPalette =
{
Color.Red,
@ -31,15 +29,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public DitherTest()
{
this.orderedDither = KnownDitherers.BayerDither4x4;
this.errorDiffuser = KnownDiffusers.FloydSteinberg;
this.orderedDither = KnownDitherings.Bayer4x4;
this.errorDiffuser = KnownDitherings.FloydSteinberg;
}
[Fact]
public void Dither_CorrectProcessor()
{
this.operations.Dither(this.orderedDither);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>();
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
@ -48,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_rect_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.rect);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>(this.rect);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
@ -57,7 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_index_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.testPalette);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>();
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>();
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(this.testPalette, p.Palette);
}
@ -66,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
public void Dither_index_rect_CorrectProcessor()
{
this.operations.Dither(this.orderedDither, this.testPalette, this.rect);
OrderedDitherPaletteProcessor p = this.Verify<OrderedDitherPaletteProcessor>(this.rect);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>(this.rect);
Assert.Equal(this.orderedDither, p.Dither);
Assert.Equal(this.testPalette, p.Palette);
}
@ -74,41 +72,103 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization
[Fact]
public void Dither_ErrorDiffuser_CorrectProcessor()
{
this.operations.Diffuse(this.errorDiffuser, .4F);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.4F, p.Threshold);
this.operations.Dither(this.errorDiffuser);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>();
Assert.Equal(this.errorDiffuser, p.Dither);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
[Fact]
public void Dither_ErrorDiffuser_rect_CorrectProcessor()
{
this.operations.Diffuse(this.errorDiffuser, .3F, this.rect);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.3F, p.Threshold);
this.operations.Dither(this.errorDiffuser, this.rect);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Dither);
Assert.Equal(Color.WebSafePalette, p.Palette);
}
[Fact]
public void Dither_ErrorDiffuser_CorrectProcessorWithColors()
{
this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>();
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
this.operations.Dither(this.errorDiffuser, this.testPalette);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>();
Assert.Equal(this.errorDiffuser, p.Dither);
Assert.Equal(this.testPalette, p.Palette);
}
[Fact]
public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors()
{
this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette, this.rect);
ErrorDiffusionPaletteProcessor p = this.Verify<ErrorDiffusionPaletteProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Diffuser);
Assert.Equal(.5F, p.Threshold);
this.operations.Dither(this.errorDiffuser, this.testPalette, this.rect);
PaletteDitherProcessor p = this.Verify<PaletteDitherProcessor>(this.rect);
Assert.Equal(this.errorDiffuser, p.Dither);
Assert.Equal(this.testPalette, p.Palette);
}
[Fact]
public void ErrorDitherEquality()
{
IDither dither = KnownDitherings.FloydSteinberg;
ErrorDither dither2 = ErrorDither.FloydSteinberg;
ErrorDither dither3 = ErrorDither.FloydSteinberg;
Assert.True(dither == dither2);
Assert.True(dither2 == dither);
Assert.False(dither != dither2);
Assert.False(dither2 != dither);
Assert.Equal(dither, dither2);
Assert.Equal(dither, (object)dither2);
Assert.Equal(dither.GetHashCode(), dither2.GetHashCode());
dither = null;
Assert.False(dither == dither2);
Assert.False(dither2 == dither);
Assert.True(dither != dither2);
Assert.True(dither2 != dither);
Assert.NotEqual(dither, dither2);
Assert.NotEqual(dither, (object)dither2);
Assert.NotEqual(dither?.GetHashCode(), dither2.GetHashCode());
Assert.True(dither2 == dither3);
Assert.True(dither3 == dither2);
Assert.False(dither2 != dither3);
Assert.False(dither3 != dither2);
Assert.Equal(dither2, dither3);
Assert.Equal(dither2, (object)dither3);
Assert.Equal(dither2.GetHashCode(), dither3.GetHashCode());
}
[Fact]
public void OrderedDitherEquality()
{
IDither dither = KnownDitherings.Bayer2x2;
OrderedDither dither2 = OrderedDither.Bayer2x2;
OrderedDither dither3 = OrderedDither.Bayer2x2;
Assert.True(dither == dither2);
Assert.True(dither2 == dither);
Assert.False(dither != dither2);
Assert.False(dither2 != dither);
Assert.Equal(dither, dither2);
Assert.Equal(dither, (object)dither2);
Assert.Equal(dither.GetHashCode(), dither2.GetHashCode());
dither = null;
Assert.False(dither == dither2);
Assert.False(dither2 == dither);
Assert.True(dither != dither2);
Assert.True(dither2 != dither);
Assert.NotEqual(dither, dither2);
Assert.NotEqual(dither, (object)dither2);
Assert.NotEqual(dither?.GetHashCode(), dither2.GetHashCode());
Assert.True(dither2 == dither3);
Assert.True(dither3 == dither2);
Assert.False(dither2 != dither3);
Assert.False(dither3 != dither2);
Assert.Equal(dither2, dither3);
Assert.Equal(dither2, (object)dither3);
Assert.Equal(dither2.GetHashCode(), dither3.GetHashCode());
}
}
}

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

@ -18,37 +18,37 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
TestImages.Png.CalliphoraPartial, TestImages.Png.Bike
};
public static readonly TheoryData<string, IOrderedDither> OrderedDitherers = new TheoryData<string, IOrderedDither>
public static readonly TheoryData<string, IDither> OrderedDitherers = new TheoryData<string, IDither>
{
{ "Bayer8x8", KnownDitherers.BayerDither8x8 },
{ "Bayer4x4", KnownDitherers.BayerDither4x4 },
{ "Ordered3x3", KnownDitherers.OrderedDither3x3 },
{ "Bayer2x2", KnownDitherers.BayerDither2x2 }
{ "Bayer8x8", KnownDitherings.Bayer8x8 },
{ "Bayer4x4", KnownDitherings.Bayer4x4 },
{ "Ordered3x3", KnownDitherings.Ordered3x3 },
{ "Bayer2x2", KnownDitherings.Bayer2x2 }
};
public static readonly TheoryData<string, IErrorDiffuser> ErrorDiffusers = new TheoryData<string, IErrorDiffuser>
public static readonly TheoryData<string, IDither> ErrorDiffusers = new TheoryData<string, IDither>
{
{ "Atkinson", KnownDiffusers.Atkinson },
{ "Burks", KnownDiffusers.Burks },
{ "FloydSteinberg", KnownDiffusers.FloydSteinberg },
{ "JarvisJudiceNinke", KnownDiffusers.JarvisJudiceNinke },
{ "Sierra2", KnownDiffusers.Sierra2 },
{ "Sierra3", KnownDiffusers.Sierra3 },
{ "SierraLite", KnownDiffusers.SierraLite },
{ "StevensonArce", KnownDiffusers.StevensonArce },
{ "Stucki", KnownDiffusers.Stucki },
{ "Atkinson", KnownDitherings.Atkinson },
{ "Burks", KnownDitherings.Burks },
{ "FloydSteinberg", KnownDitherings.FloydSteinberg },
{ "JarvisJudiceNinke", KnownDitherings.JarvisJudiceNinke },
{ "Sierra2", KnownDitherings.Sierra2 },
{ "Sierra3", KnownDitherings.Sierra3 },
{ "SierraLite", KnownDitherings.SierraLite },
{ "StevensonArce", KnownDitherings.StevensonArce },
{ "Stucki", KnownDitherings.Stucki },
};
public const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24;
private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IDither DefaultDitherer => KnownDitherings.Bayer4x4;
private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson;
private static IDither DefaultErrorDiffuser => KnownDitherings.Atkinson;
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(OrderedDitherers), 100, 100, PixelTypes.Rgba32)]
public void BinaryDitherFilter_WorksWithAllDitherers<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,12 +61,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(ErrorDiffusers), 100, 100, PixelTypes.Rgba32)]
public void DiffusionFilter_WorksWithAllErrorDiffusers<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())
{
image.Mutate(x => x.BinaryDiffuse(diffuser, .5F));
image.Mutate(x => x.BinaryDither(diffuser));
image.DebugSave(provider, name);
}
}
@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
{
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, 0.5f));
image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser));
image.DebugSave(provider);
}
}
@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
{
var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2);
image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, .5F, bounds));
image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser, bounds));
image.DebugSave(provider);
ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds);

62
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, string> ErrorDiffusers
= new TheoryData<IDither, string>
{
{ KnownDitherings.Atkinson, nameof(KnownDitherings.Atkinson) },
{ KnownDitherings.Burks, nameof(KnownDitherings.Burks) },
{ KnownDitherings.FloydSteinberg, nameof(KnownDitherings.FloydSteinberg) },
{ KnownDitherings.JarvisJudiceNinke, nameof(KnownDitherings.JarvisJudiceNinke) },
{ KnownDitherings.Sierra2, nameof(KnownDitherings.Sierra2) },
{ KnownDitherings.Sierra3, nameof(KnownDitherings.Sierra3) },
{ KnownDitherings.SierraLite, nameof(KnownDitherings.SierraLite) },
{ KnownDitherings.StevensonArce, nameof(KnownDitherings.StevensonArce) },
{ KnownDitherings.Stucki, nameof(KnownDitherings.Stucki) },
};
public static readonly TheoryData<IDither, string> OrderedDitherers
= new TheoryData<IDither,string>
{
{ KnownDitherings.Bayer2x2, nameof(KnownDitherings.Bayer2x2) },
{ KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) },
{ KnownDitherings.Bayer8x8, nameof(KnownDitherings.Bayer8x8) },
{ KnownDitherings.Ordered3x3, nameof(KnownDitherings.Ordered3x3) }
};
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05f);
private static IOrderedDither DefaultDitherer => KnownDitherers.BayerDither4x4;
private static IDither DefaultDitherer => KnownDitherings.Bayer4x4;
private static IErrorDiffuser DefaultErrorDiffuser => KnownDiffusers.Atkinson;
private static IDither DefaultErrorDiffuser => KnownDitherings.Atkinson;
/// <summary>
/// The output is visually correct old 32bit runtime,
@ -62,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
}
provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.Diffuse(DefaultErrorDiffuser, .5F, rect),
(x, rect) => x.Dither(DefaultErrorDiffuser, rect),
comparer: ValidatorComparer);
}
@ -93,14 +95,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
// Increased tolerance because of compatibility issues on .NET 4.6.2:
var comparer = ImageComparer.TolerantPercentage(1f);
provider.RunValidatingProcessorTest(x => x.Diffuse(DefaultErrorDiffuser, 0.5f), comparer: comparer);
provider.RunValidatingProcessorTest(x => x.Dither(DefaultErrorDiffuser), comparer: comparer);
}
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(ErrorDiffusers), PixelTypes.Rgba32)]
public void DiffusionFilter_WorksWithAllErrorDiffusers<TPixel>(
TestImageProvider<TPixel> provider,
IErrorDiffuser diffuser)
IDither diffuser,
string name)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllDitherTests)
@ -109,8 +112,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
}
provider.RunValidatingProcessorTest(
x => x.Diffuse(diffuser, 0.5f),
testOutputDetails: diffuser.GetType().Name,
x => x.Dither(diffuser),
testOutputDetails: name,
comparer: ValidatorComparer,
appendPixelTypeToFileName: false);
}
@ -134,7 +137,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
[WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)]
public void DitherFilter_WorksWithAllDitherers<TPixel>(
TestImageProvider<TPixel> provider,
IOrderedDither ditherer)
IDither ditherer,
string name)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllDitherTests)
@ -144,7 +148,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization
provider.RunValidatingProcessorTest(
x => x.Dither(ditherer),
testOutputDetails: ditherer.GetType().Name,
testOutputDetails: name,
comparer: ValidatorComparer,
appendPixelTypeToFileName: false);
}

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
[Fact]
public void OctreeQuantizerConstructor()
{
var quantizer = new OctreeQuantizer(128);
Assert.Equal(128, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser);
quantizer = new OctreeQuantizer(false);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors);
Assert.Null(quantizer.Diffuser);
quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser);
quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson, 128);
Assert.Equal(128, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser);
var expected = new QuantizerOptions { MaxColors = 128 };
var quantizer = new OctreeQuantizer(expected);
Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = null };
quantizer = new OctreeQuantizer(expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Null(quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson };
quantizer = new OctreeQuantizer(expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 };
quantizer = new OctreeQuantizer(expected);
Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
}
[Fact]
@ -38,21 +42,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.NotNull(frameQuantizer.Options);
Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new OctreeQuantizer(false);
quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = null });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson);
quantizer = new OctreeQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
}
}
}

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

@ -10,61 +10,70 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
{
public class PaletteQuantizerTests
{
private static readonly Color[] Rgb = new Color[] { Color.Red, Color.Green, Color.Blue };
private static readonly Color[] Palette = new Color[] { Color.Red, Color.Green, Color.Blue };
[Fact]
public void PaletteQuantizerConstructor()
{
var quantizer = new PaletteQuantizer(Rgb);
var expected = new QuantizerOptions { MaxColors = 128 };
var quantizer = new PaletteQuantizer(Palette, expected);
Assert.Equal(Rgb, quantizer.Palette);
Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser);
Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
quantizer = new PaletteQuantizer(Rgb, false);
Assert.Equal(Rgb, quantizer.Palette);
Assert.Null(quantizer.Diffuser);
expected = new QuantizerOptions { Dither = null };
quantizer = new PaletteQuantizer(Palette, expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Null(quantizer.Options.Dither);
quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson);
Assert.Equal(Rgb, quantizer.Palette);
Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson };
quantizer = new PaletteQuantizer(Palette, expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 };
quantizer = new PaletteQuantizer(Palette, expected);
Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
}
[Fact]
public void PaletteQuantizerCanCreateFrameQuantizer()
{
var quantizer = new PaletteQuantizer(Rgb);
var quantizer = new PaletteQuantizer(Palette);
IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.NotNull(frameQuantizer.Options);
Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new PaletteQuantizer(Rgb, false);
quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = null });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson);
quantizer = new PaletteQuantizer(Palette, new QuantizerOptions { Dither = KnownDitherings.Atkinson });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
}
[Fact]
public void KnownQuantizersWebSafeTests()
{
IQuantizer quantizer = KnownQuantizers.WebSafe;
Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
}
[Fact]
public void KnownQuantizersWernerTests()
{
IQuantizer quantizer = KnownQuantizers.Werner;
Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
}
}
}

220
tests/ImageSharp.Tests/Processing/Processors/Quantization/QuantizerTests.cs

@ -0,0 +1,220 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
{
public class QuantizerTests
{
/// <summary>
/// Something is causing tests to fail on NETFX in CI.
/// Could be a JIT error as everything runs well and is identical to .NET Core output.
/// Not worth investigating for now.
/// <see href="https://github.com/SixLabors/ImageSharp/pull/1114/checks?check_run_id=448891164#step:11:631"/>
/// </summary>
private static readonly bool SkipAllQuantizerTests = TestEnvironment.RunsOnCI && TestEnvironment.IsFramework;
public static readonly string[] CommonTestImages =
{
TestImages.Png.CalliphoraPartial,
TestImages.Png.Bike
};
private static readonly QuantizerOptions NoDitherOptions = new QuantizerOptions { Dither = null };
private static readonly QuantizerOptions DiffuserDitherOptions = new QuantizerOptions { Dither = KnownDitherings.FloydSteinberg };
private static readonly QuantizerOptions OrderedDitherOptions = new QuantizerOptions { Dither = KnownDitherings.Bayer8x8 };
private static readonly QuantizerOptions Diffuser0_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.FloydSteinberg,
DitherScale = 0F
};
private static readonly QuantizerOptions Diffuser0_25_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.FloydSteinberg,
DitherScale = .25F
};
private static readonly QuantizerOptions Diffuser0_5_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.FloydSteinberg,
DitherScale = .5F
};
private static readonly QuantizerOptions Diffuser0_75_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.FloydSteinberg,
DitherScale = .75F
};
private static readonly QuantizerOptions Ordered0_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.Bayer8x8,
DitherScale = 0F
};
private static readonly QuantizerOptions Ordered0_25_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.Bayer8x8,
DitherScale = .25F
};
private static readonly QuantizerOptions Ordered0_5_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.Bayer8x8,
DitherScale = .5F
};
private static readonly QuantizerOptions Ordered0_75_ScaleDitherOptions = new QuantizerOptions
{
Dither = KnownDitherings.Bayer8x8,
DitherScale = .75F
};
public static readonly TheoryData<IQuantizer> Quantizers
= new TheoryData<IQuantizer>
{
// Known uses error diffusion by default.
KnownQuantizers.Octree,
KnownQuantizers.WebSafe,
KnownQuantizers.Werner,
KnownQuantizers.Wu,
new OctreeQuantizer(NoDitherOptions),
new WebSafePaletteQuantizer(NoDitherOptions),
new WernerPaletteQuantizer(NoDitherOptions),
new WuQuantizer(NoDitherOptions),
new OctreeQuantizer(OrderedDitherOptions),
new WebSafePaletteQuantizer(OrderedDitherOptions),
new WernerPaletteQuantizer(OrderedDitherOptions),
new WuQuantizer(OrderedDitherOptions)
};
public static readonly TheoryData<IQuantizer> DitherScaleQuantizers
= new TheoryData<IQuantizer>
{
new OctreeQuantizer(Diffuser0_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_ScaleDitherOptions),
new WuQuantizer(Diffuser0_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_25_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_25_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_25_ScaleDitherOptions),
new WuQuantizer(Diffuser0_25_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_5_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_5_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_5_ScaleDitherOptions),
new WuQuantizer(Diffuser0_5_ScaleDitherOptions),
new OctreeQuantizer(Diffuser0_75_ScaleDitherOptions),
new WebSafePaletteQuantizer(Diffuser0_75_ScaleDitherOptions),
new WernerPaletteQuantizer(Diffuser0_75_ScaleDitherOptions),
new WuQuantizer(Diffuser0_75_ScaleDitherOptions),
new OctreeQuantizer(DiffuserDitherOptions),
new WebSafePaletteQuantizer(DiffuserDitherOptions),
new WernerPaletteQuantizer(DiffuserDitherOptions),
new WuQuantizer(DiffuserDitherOptions),
new OctreeQuantizer(Ordered0_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_ScaleDitherOptions),
new WuQuantizer(Ordered0_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_25_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_25_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_25_ScaleDitherOptions),
new WuQuantizer(Ordered0_25_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_5_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_5_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_5_ScaleDitherOptions),
new WuQuantizer(Ordered0_5_ScaleDitherOptions),
new OctreeQuantizer(Ordered0_75_ScaleDitherOptions),
new WebSafePaletteQuantizer(Ordered0_75_ScaleDitherOptions),
new WernerPaletteQuantizer(Ordered0_75_ScaleDitherOptions),
new WuQuantizer(Ordered0_75_ScaleDitherOptions),
new OctreeQuantizer(OrderedDitherOptions),
new WebSafePaletteQuantizer(OrderedDitherOptions),
new WernerPaletteQuantizer(OrderedDitherOptions),
new WuQuantizer(OrderedDitherOptions),
};
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.05F);
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)]
public void ApplyQuantizationInBox<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllQuantizerTests)
{
return;
}
string quantizerName = quantizer.GetType().Name;
string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither";
string testOutputDetails = $"{quantizerName}_{ditherName}";
provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.Quantize(quantizer, rect),
comparer: ValidatorComparer,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: false);
}
[Theory]
[WithFileCollection(nameof(CommonTestImages), nameof(Quantizers), PixelTypes.Rgba32)]
public void ApplyQuantization<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllQuantizerTests)
{
return;
}
string quantizerName = quantizer.GetType().Name;
string ditherName = quantizer.Options.Dither?.GetType()?.Name ?? "NoDither";
string testOutputDetails = $"{quantizerName}_{ditherName}";
provider.RunValidatingProcessorTest(
x => x.Quantize(quantizer),
comparer: ValidatorComparer,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: false);
}
[Theory]
[WithFile(TestImages.Png.David, nameof(DitherScaleQuantizers), PixelTypes.Rgba32)]
public void ApplyQuantizationWithDitheringScale<TPixel>(TestImageProvider<TPixel> provider, IQuantizer quantizer)
where TPixel : struct, IPixel<TPixel>
{
if (SkipAllQuantizerTests)
{
return;
}
string quantizerName = quantizer.GetType().Name;
string ditherName = quantizer.Options.Dither.GetType().Name;
float ditherScale = quantizer.Options.DitherScale;
string testOutputDetails = FormattableString.Invariant($"{quantizerName}_{ditherName}_{ditherScale}");
provider.RunValidatingProcessorTest(
x => x.Quantize(quantizer),
comparer: ValidatorComparer,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: false);
}
}
}

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
@ -13,22 +13,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
[Fact]
public void WuQuantizerConstructor()
{
var quantizer = new WuQuantizer(128);
Assert.Equal(128, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser);
quantizer = new WuQuantizer(false);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors);
Assert.Null(quantizer.Diffuser);
quantizer = new WuQuantizer(KnownDiffusers.Atkinson);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser);
quantizer = new WuQuantizer(KnownDiffusers.Atkinson, 128);
Assert.Equal(128, quantizer.MaxColors);
Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser);
var expected = new QuantizerOptions { MaxColors = 128 };
var quantizer = new WuQuantizer(expected);
Assert.Equal(expected.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(QuantizerConstants.DefaultDither, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = null };
quantizer = new WuQuantizer(expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Null(quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson };
quantizer = new WuQuantizer(expected);
Assert.Equal(QuantizerConstants.MaxColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
expected = new QuantizerOptions { Dither = KnownDitherings.Atkinson, MaxColors = 0 };
quantizer = new WuQuantizer(expected);
Assert.Equal(QuantizerConstants.MinColors, quantizer.Options.MaxColors);
Assert.Equal(KnownDitherings.Atkinson, quantizer.Options.Dither);
}
[Fact]
@ -38,21 +42,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization
IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Diffuser);
Assert.NotNull(frameQuantizer.Options);
Assert.Equal(QuantizerConstants.DefaultDither, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new WuQuantizer(false);
quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.False(frameQuantizer.Dither);
Assert.Null(frameQuantizer.Diffuser);
Assert.Null(frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
quantizer = new WuQuantizer(KnownDiffusers.Atkinson);
quantizer = new WuQuantizer(new QuantizerOptions { Dither = KnownDitherings.Atkinson });
frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(Configuration.Default);
Assert.NotNull(frameQuantizer);
Assert.True(frameQuantizer.Dither);
Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Diffuser);
Assert.Equal(KnownDitherings.Atkinson, frameQuantizer.Options.Dither);
frameQuantizer.Dispose();
}
}
}

77
tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs

@ -22,15 +22,30 @@ namespace SixLabors.ImageSharp.Tests
var octree = new OctreeQuantizer();
var wu = new WuQuantizer();
Assert.NotNull(werner.Diffuser);
Assert.NotNull(webSafe.Diffuser);
Assert.NotNull(octree.Diffuser);
Assert.NotNull(wu.Diffuser);
Assert.True(werner.CreateFrameQuantizer<Rgba32>(this.Configuration).Dither);
Assert.True(webSafe.CreateFrameQuantizer<Rgba32>(this.Configuration).Dither);
Assert.True(octree.CreateFrameQuantizer<Rgba32>(this.Configuration).Dither);
Assert.True(wu.CreateFrameQuantizer<Rgba32>(this.Configuration).Dither);
Assert.NotNull(werner.Options.Dither);
Assert.NotNull(webSafe.Options.Dither);
Assert.NotNull(octree.Options.Dither);
Assert.NotNull(wu.Options.Dither);
using (IFrameQuantizer<Rgba32> quantizer = werner.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = webSafe.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = octree.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.NotNull(quantizer.Options.Dither);
}
using (IFrameQuantizer<Rgba32> quantizer = wu.CreateFrameQuantizer<Rgba32>(this.Configuration))
{
Assert.NotNull(quantizer.Options.Dither);
}
}
[Theory]
@ -43,17 +58,24 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<TPixel> image = provider.GetImage())
{
Assert.True(image[0, 0].Equals(default(TPixel)));
Assert.True(image[0, 0].Equals(default));
var options = new QuantizerOptions();
if (!dither)
{
options.Dither = null;
}
var quantizer = new OctreeQuantizer(dither);
var quantizer = new OctreeQuantizer(options);
foreach (ImageFrame<TPixel> frame in image.Frames)
{
IQuantizedFrame<TPixel> quantized =
quantizer.CreateFrameQuantizer<TPixel>(this.Configuration).QuantizeFrame(frame);
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
}
}
}
}
@ -66,22 +88,29 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<TPixel> image = provider.GetImage())
{
Assert.True(image[0, 0].Equals(default(TPixel)));
Assert.True(image[0, 0].Equals(default));
var quantizer = new WuQuantizer(dither);
var options = new QuantizerOptions();
if (!dither)
{
options.Dither = null;
}
var quantizer = new WuQuantizer(options);
foreach (ImageFrame<TPixel> frame in image.Frames)
{
IQuantizedFrame<TPixel> quantized =
quantizer.CreateFrameQuantizer<TPixel>(this.Configuration).QuantizeFrame(frame);
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
}
}
}
}
private int GetTransparentIndex<TPixel>(IQuantizedFrame<TPixel> quantized)
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette

121
tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

@ -15,34 +15,38 @@ namespace SixLabors.ImageSharp.Tests.Quantization
public void SinglePixelOpaque()
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
using (var image = new Image<Rgba32>(config, 1, 1, Color.Black))
using (IQuantizedFrame<Rgba32> result = quantizer.CreateFrameQuantizer<Rgba32>(config).QuantizeFrame(image.Frames[0]))
{
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
using var image = new Image<Rgba32>(config, 1, 1, Color.Black);
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
[Fact]
public void SinglePixelTransparent()
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
using (var image = new Image<Rgba32>(config, 1, 1, default(Rgba32)))
using (IQuantizedFrame<Rgba32> result = quantizer.CreateFrameQuantizer<Rgba32>(config).QuantizeFrame(image.Frames[0]))
{
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
using var image = new Image<Rgba32>(config, 1, 1, default(Rgba32));
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
[Fact]
@ -63,46 +67,47 @@ namespace SixLabors.ImageSharp.Tests.Quantization
[Fact]
public void Palette256()
{
using (var image = new Image<Rgba32>(1, 256))
using var image = new Image<Rgba32>(1, 256);
for (int i = 0; i < 256; i++)
{
for (int i = 0; i < 256; i++)
{
byte r = (byte)((i % 4) * 85);
byte g = (byte)(((i / 4) % 4) * 85);
byte b = (byte)(((i / 16) % 4) * 85);
byte a = (byte)((i / 64) * 85);
byte r = (byte)((i % 4) * 85);
byte g = (byte)(((i / 4) % 4) * 85);
byte b = (byte)(((i / 16) % 4) * 85);
byte a = (byte)((i / 64) * 85);
image[0, i] = new Rgba32(r, g, b, a);
}
image[0, i] = new Rgba32(r, g, b, a);
}
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config))
using (IQuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(image.Frames[0]))
{
Assert.Equal(256, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
var actualImage = new Image<Rgba32>(1, 256);
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
int yy = y * actualImage.Width;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
for (int x = 0; x < actualImage.Width; x++)
{
int i = x + yy;
row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])];
}
}
Assert.Equal(256, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);
var actualImage = new Image<Rgba32>(1, 256);
Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan()));
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++)
{
int i = x + yy;
row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])];
}
}
Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan()));
}
[Theory]
@ -114,12 +119,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
using (Image<TPixel> image = provider.GetImage())
{
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(config))
using (IQuantizedFrame<TPixel> result = frameQuantizer.QuantizeFrame(image.Frames[0]))
{
Assert.Equal(48, result.Palette.Length);
}
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
ImageFrame<TPixel> frame = image.Frames.RootFrame;
using IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(config);
using QuantizedFrame<TPixel> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(48, result.Palette.Length);
}
}
@ -142,10 +148,11 @@ namespace SixLabors.ImageSharp.Tests.Quantization
}
Configuration config = Configuration.Default;
var quantizer = new WuQuantizer(false);
var quantizer = new WuQuantizer(new QuantizerOptions { Dither = null });
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config))
using (IQuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(image.Frames[0]))
using (QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);

1
tests/ImageSharp.Tests/TestImages.cs

@ -56,6 +56,7 @@ namespace SixLabors.ImageSharp.Tests
public const string LowColorVariance = "Png/low-variance.png";
public const string PngWithMetadata = "Png/PngWithMetaData.png";
public const string InvalidTextData = "Png/InvalidTextData.png";
public const string David = "Png/david.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";

10
tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs

@ -24,6 +24,16 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison
sb.Append(Environment.NewLine);
// TODO: We should add OSX.
sb.AppendFormat("Test Environment OS : {0}", TestEnvironment.IsWindows ? "Windows" : "Linux");
sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment is CI : {0}", TestEnvironment.RunsOnCI);
sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment is .NET Core : {0}", !TestEnvironment.IsFramework);
sb.Append(Environment.NewLine);
int i = 0;
foreach (ImageSimilarityReport r in reports)
{

4
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -342,10 +342,10 @@ namespace SixLabors.ImageSharp.Tests
if (!File.Exists(referenceOutputFile))
{
throw new System.IO.FileNotFoundException("Reference output file missing: " + referenceOutputFile, referenceOutputFile);
throw new FileNotFoundException("Reference output file missing: " + referenceOutputFile, referenceOutputFile);
}
decoder = decoder ?? TestEnvironment.GetReferenceDecoder(referenceOutputFile);
decoder ??= TestEnvironment.GetReferenceDecoder(referenceOutputFile);
return Image.Load<TPixel>(referenceOutputFile, decoder);
}

7
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -294,7 +294,8 @@ namespace SixLabors.ImageSharp.Tests
this TestImageProvider<TPixel> provider,
Action<IImageProcessingContext, Rectangle> process,
object testOutputDetails = null,
ImageComparer comparer = null)
ImageComparer comparer = null,
bool appendPixelTypeToFileName = true)
where TPixel : struct, IPixel<TPixel>
{
if (comparer == null)
@ -306,8 +307,8 @@ namespace SixLabors.ImageSharp.Tests
{
var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2);
image.Mutate(x => process(x, bounds));
image.DebugSave(provider, testOutputDetails);
image.CompareToReferenceOutput(comparer, provider, testOutputDetails);
image.DebugSave(provider, testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName);
image.CompareToReferenceOutput(comparer, provider, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName);
}
}

2
tests/Images/External

@ -1 +1 @@
Subproject commit fbba5e2a78aa479c0752dc0fd91ec25b4948704a
Subproject commit 2d1505d7087d91cd83d0cda409aee213de7841ab

BIN
tests/Images/Input/Png/bike-small.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
tests/Images/Input/Png/david.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Loading…
Cancel
Save