Browse Source

Merge pull request #1138 from SixLabors/js/dither-quantize-updates

Dither and Quantize API and Performance Improvements.
pull/1142/head
James Jackson-South 6 years ago
committed by GitHub
parent
commit
fab07aba5c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  2. 3
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  3. 110
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 2
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  5. 88
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 4
      src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs
  7. 2
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
  8. 4
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  9. 35
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  10. 21
      src/ImageSharp/Processing/Processors/Dithering/IDither.cs
  11. 38
      src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
  12. 2
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.KnownTypes.cs
  13. 111
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  14. 81
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  15. 93
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  16. 85
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs
  17. 32
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  18. 30
      src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
  19. 116
      src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs
  20. 87
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  21. 4
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  22. 26
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  23. 23
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  24. 8
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  25. 91
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  26. 4
      src/ImageSharp/Processing/Processors/Quantization/WebSafePaletteQuantizer.cs
  27. 4
      src/ImageSharp/Processing/Processors/Quantization/WernerPaletteQuantizer.cs
  28. 144
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  29. 4
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  30. 19
      tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs
  31. 20
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  32. 30
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs
  33. 2
      tests/Images/External

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

@ -336,8 +336,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
@ -360,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
for (int y = image.Height - 1; y >= 0; y--)
{
ReadOnlySpan<byte> pixelSpan = quantized.GetRowSpan(y);
ReadOnlySpan<byte> pixelSpan = quantized.GetPixelRowSpan(y);
stream.Write(pixelSpan);
for (int i = 0; i < this.padding; i++)

3
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -4,6 +4,7 @@
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif
@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/>
/// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer();
public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
/// <summary>
/// Gets or sets the color table mode: Global or local.

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

@ -79,14 +79,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized;
IndexedImageFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
// Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
// Write the header.
this.WriteHeader(stream);
@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
// Clean up.
quantized?.Dispose();
quantized.Dispose();
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
private void EncodeGlobal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
EuclideanPixelMap<TPixel> pixelMap = default;
bool pixelMapSet = false;
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
if (!pixelMapSet)
{
this.WriteImageData(paletteQuantized, stream);
pixelMapSet = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette);
}
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using IndexedImageFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
private void EncodeLocal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames)
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
else
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette
// Transparent pixels are much more likely to be found at the end of a palette.
int index = -1;
int length = quantized.Palette.Length;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(length))
{
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
for (int i = rgbaSpan.Length - 1; i >= 0; i--)
{
if (Unsafe.Add(ref rgbaSpanRef, i).Equals(default))
{
if (Unsafe.Add(ref paletteRef, i).Equals(default))
{
index = i;
}
index = i;
}
}
@ -326,8 +330,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
return;
}
foreach (string comment in metadata.Comments)
for (var i = 0; i < metadata.Comments.Count; i++)
{
string comment = metadata.Comments[i];
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
@ -335,7 +340,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
for (;
idx <= comment.Length - GifConstants.MaxCommentSubBlockLength;
idx += GifConstants.MaxCommentSubBlockLength)
{
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
}
@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="extension">The extension to write to the stream.</param>
/// <param name="stream">The stream to write to.</param>
public void WriteExtension(IGifExtension extension, Stream stream)
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
@ -437,37 +445,33 @@ 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>(QuantizedFrame<TPixel> image, Stream stream)
private void WriteColorTable<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// The maximum number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
int pixelCount = image.Palette.Length;
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>();
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
{
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);
}
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
image.Palette.Length);
stream.Write(colorTable.Array, 0, colorTableLength);
}
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="QuantizedFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="image">The <see cref="IndexedImageFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
private void WriteImageData<TPixel>(IndexedImageFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{
encoder.Encode(image.GetPixelSpan(), stream);
}
using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(image.GetPixelBufferSpan(), stream);
}
}
}

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

@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(indexedPixels);
// TODO: PERF: It looks likt hshift could be calculated once statically.
// TODO: PERF: It looks like hshift could be calculated once statically.
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{

88
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);
QuantizedFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
IndexedImageFrame<TPixel> quantized = PngEncoderOptionsHelpers.CreateQuantizedFrame(this.options, image);
this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes);
@ -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, QuantizedFrame<TPixel> quantized, int row)
private void CollectPixelBytes<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel>
{
switch (this.options.ColorType)
@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.bitDepth < 8)
{
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth);
}
else
{
int stride = this.currentScanline.Length();
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan());
}
break;
@ -440,7 +439,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, QuantizedFrame<TPixel> quantized, int row)
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, IndexedImageFrame<TPixel> quantized, int row)
where TPixel : unmanaged, IPixel<TPixel>
{
this.CollectPixelBytes(rowSpan, quantized, row);
@ -546,59 +545,54 @@ 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, QuantizedFrame<TPixel> quantized)
private void WritePaletteChunk<TPixel>(Stream stream, IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel>
{
if (quantized == null)
if (quantized is null)
{
return;
}
// Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3;
bool anyAlpha = false;
int paletteLength = palette.Length;
int colorTableLength = paletteLength * Unsafe.SizeOf<Rgb24>();
bool hasAlpha = false;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
{
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
ReadOnlySpan<byte> quantizedSpan = quantized.GetPixelSpan();
Rgba32 rgba = default;
for (int i = 0; i < paletteLength; i++)
{
if (quantizedSpan.IndexOf((byte)i) > -1)
{
int offset = i * 3;
palette[i].ToRgba32(ref rgba);
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
using IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength);
byte alpha = rgba.A;
ref Rgb24 colorTableRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<byte, Rgb24>(colorTable.GetSpan()));
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
Unsafe.Add(ref colorTableRef, offset) = rgba.R;
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
// Bulk convert our palette to RGBA to allow assignment to tables.
using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteLength);
Span<Rgba32> rgbaPaletteSpan = rgbaOwner.GetSpan();
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, quantized.Palette.Span, rgbaPaletteSpan);
ref Rgba32 rgbaPaletteRef = ref MemoryMarshal.GetReference(rgbaPaletteSpan);
if (alpha > this.options.Threshold)
{
alpha = byte.MaxValue;
}
// Loop, assign, and extract alpha values from the palette.
for (int i = 0; i < paletteLength; i++)
{
Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i);
byte alpha = rgba.A;
anyAlpha = anyAlpha || alpha < byte.MaxValue;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
if (alpha > this.options.Threshold)
{
alpha = byte.MaxValue;
}
this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
hasAlpha = hasAlpha || alpha < byte.MaxValue;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
// Write the transparency data
if (anyAlpha)
{
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
// Write the transparency data
if (hasAlpha)
{
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
}
@ -783,7 +777,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, QuantizedFrame<TPixel> quantized, Stream stream)
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
byte[] buffer;
@ -881,7 +875,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, QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodePixels<TPixel>(ImageFrame<TPixel> pixels, IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -960,7 +954,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>(QuantizedFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
private void EncodeAdam7IndexedPixels<TPixel>(IndexedImageFrame<TPixel> quantized, ZlibDeflateStream deflateStream)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = quantized.Width;
@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png
row += Adam7.RowIncrement[pass])
{
// collect data
ReadOnlySpan<byte> srcRow = quantized.GetRowSpan(row);
ReadOnlySpan<byte> srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0;
col < width;
col += Adam7.ColumnIncrement[pass])

4
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 QuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>(
public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options,
Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options,
Image<TPixel> image,
QuantizedFrame<TPixel> quantizedFrame)
IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel>
{
byte bitDepth;

2
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs

@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Memory
/// <inheritdoc />
public override Span<T> GetSpan()
{
if (this.Data == null)
if (this.Data is null)
{
throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer<T>");
}

4
src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs

@ -18,12 +18,12 @@ namespace SixLabors.ImageSharp.Memory
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
/// </summary>
public int BufferLength { get; }
int BufferLength { get; }
/// <summary>
/// Gets the aggregate number of elements in the group.
/// </summary>
public long TotalLength { get; }
long TotalLength { get; }
/// <summary>
/// Gets a value indicating whether the group has been invalidated.

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

@ -4,6 +4,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -89,29 +90,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, 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;
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = row[x];
outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed);
TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
}
}
@ -119,25 +116,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var pixelMap = new EuclideanPixelMap<TPixel>(palette);
float scale = processor.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = row[x];
pixelMap.GetClosestColor(sourcePixel, out TPixel transformed);
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
row[x] = transformed;
sourcePixel = transformed;
}
}
}

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

@ -1,7 +1,6 @@
// 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;
@ -19,15 +18,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <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="destination">The destination quantized frame.</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,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>;
@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels.
/// </summary>
/// <typeparam name="TPaletteDitherImageProcessor">The type of palette dithering processor.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="processor">The palette dithering processor.</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,
void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>;
}
}

38
src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs

@ -0,0 +1,38 @@
// 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>
/// Implements an algorithm to alter the pixels of an image via palette dithering.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public interface IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets the configuration instance to use when performing operations.
/// </summary>
Configuration Configuration { get; }
/// <summary>
/// Gets the dithering palette.
/// </summary>
ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the dithering scale used to adjust the amount of dither. Range 0..1.
/// </summary>
float DitherScale { get; }
/// <summary>
/// Returns the color from the dithering palette corresponding to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <returns>The <typeparamref name="TPixel"/> match.</returns>
TPixel GetPaletteColor(TPixel color);
}
}

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

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <content>
/// An ordered dithering matrix with equal sides of arbitrary length
/// </content>
public readonly partial struct OrderedDither : IDither
public readonly partial struct OrderedDither
{
/// <summary>
/// Applies order dithering using the 2x2 Bayer dithering matrix.

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

@ -3,8 +3,8 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -105,23 +105,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source,
Memory<byte> output,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>(
var ditherOperation = new QuantizeDitherRowOperation<TFrameQuantizer, TPixel>(
ref quantizer,
in Unsafe.AsRef(this),
source,
output,
bounds,
palette,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
destination,
bounds);
ParallelRowIterator.IterateRowIntervals(
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in ditherOperation);
@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
in TPaletteDitherImageProcessor processor,
ImageFrame<TPixel> source,
Rectangle bounds,
float scale)
Rectangle bounds)
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>(
var ditherOperation = new PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this),
source,
bounds,
palette,
scale,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
bounds);
ParallelRowIterator.IterateRowIntervals(
configuration,
ParallelRowIterator.IterateRows(
processor.Configuration,
bounds,
in ditherOperation);
}
@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
private readonly struct QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
private readonly struct QuantizeDitherRowOperation<TFrameQuantizer, TPixel> : IRowOperation
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowIntervalOperation(
public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer,
in OrderedDither dither,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
int bitDepth)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
this.output = output;
this.destination = destination;
this.bounds = bounds;
this.palette = palette;
this.bitDepth = bitDepth;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
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++)
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
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 _);
}
TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
}
}
}
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation
private readonly struct PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel> : IRowOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TPaletteDitherImageProcessor processor;
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(
public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither,
ImageFrame<TPixel> source,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
float scale,
int bitDepth)
Rectangle bounds)
{
this.processor = processor;
this.dither = dither;
this.source = source;
this.bounds = bounds;
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
this.scale = scale;
this.bitDepth = bitDepth;
this.scale = processor.DitherScale;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
public void Invoke(int y)
{
for (int y = rows.Min; y < rows.Max; y++)
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
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;
}
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
}
}
}

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

@ -3,7 +3,9 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{
@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int paletteLength;
private readonly DitherProcessor ditherProcessor;
private readonly IDither dither;
private readonly float ditherScale;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private IMemoryOwner<TPixel> paletteOwner;
private bool isDisposed;
/// <summary>
@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.paletteLength = definition.Palette.Span.Length;
this.dither = definition.Dither;
this.ditherScale = definition.DitherScale;
this.sourcePalette = definition.Palette;
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
ReadOnlySpan<Color> sourcePalette = definition.Palette.Span;
this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length);
Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span);
this.dither.ApplyPaletteDither(
this.ditherProcessor = new DitherProcessor(
this.Configuration,
this.palette.Memory,
source,
interest,
this.ditherScale);
this.paletteOwner.Memory,
definition.DitherScale);
}
/// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source)
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
// Lazy init palettes:
if (this.palette is null)
{
this.palette = this.Configuration.MemoryAllocator.Allocate<TPixel>(this.paletteLength);
ReadOnlySpan<Color> sourcePalette = this.sourcePalette.Span;
Color.ToPixel(this.Configuration, sourcePalette, this.palette.Memory.Span);
}
base.BeforeFrameApply(source);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
}
/// <inheritdoc/>
@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
return;
}
this.isDisposed = true;
if (disposing)
{
this.palette?.Dispose();
this.paletteOwner.Dispose();
}
this.palette = null;
this.isDisposed = true;
this.paletteOwner = null;
base.Dispose(disposing);
}
/// <summary>
/// Used to allow inlining of calls to
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary>
private readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
[MethodImpl(InliningOptions.ShortMethod)]
public DitherProcessor(
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
float ditherScale)
{
this.Configuration = configuration;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
this.Palette = palette;
this.DitherScale = ditherScale;
}
public Configuration Configuration { get; }
public ReadOnlyMemory<TPixel> Palette { get; }
public float DitherScale { get; }
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel GetPaletteColor(TPixel color)
{
this.pixelMap.GetClosestColor(color, out TPixel match);
return match;
}
}
}
}

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

@ -5,101 +5,100 @@ using System;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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.
/// Gets the closest color to the supplied color based upon the Euclidean distance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>>
internal readonly struct EuclideanPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ConcurrentDictionary<int, Vector4> vectorCache;
private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache;
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
public EuclideanPixelMap(ReadOnlyMemory<TPixel> palette)
[MethodImpl(InliningOptions.ShortMethod)]
public EuclideanPixelMap(Configuration configuration, 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>();
this.vectorCache = new Vector4[palette.Length];
for (int i = 0; i < paletteSpan.Length; i++)
{
this.vectorCache[i] = paletteSpan[i].ToScaledVector4();
}
// Use the same rules across all target frameworks.
this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
PixelOperations<TPixel>.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
}
/// <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);
/// <summary>
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it.
/// </summary>
public ReadOnlyMemory<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <inheritdoc/>
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// The palette contents must match the one used in the constructor.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match)
{
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
// Check if the color is in the lookup table
if (this.distanceCache.TryGetValue(color, out int index))
if (!this.distanceCache.TryGetValue(color, out int index))
{
match = paletteSpan[index];
return index;
return this.GetClosestColorSlow(color, ref paletteRef, out match);
}
return this.GetClosestColorSlow(color, paletteSpan, out match);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public override int GetHashCode()
=> this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, 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++)
var vector = color.ToVector4();
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < this.Palette.Length; i++)
{
Vector4 candidate = this.vectorCache[i];
Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate);
// Less than... assign.
// If it's an exact match, exit the loop
if (distance == 0)
{
index = i;
break;
}
if (distance < leastDistance)
{
// Less than... assign.
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];
match = Unsafe.Add(ref paletteRef, index);
return index;
}
}

85
src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs → src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerUtilities.cs

@ -11,22 +11,40 @@ using SixLabors.ImageSharp.Processing.Processors.Dithering;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Contains extension methods for frame quantizers.
/// Contains utility methods for <see cref="IFrameQuantizer{TPixel}"/> instances.
/// </summary>
public static class FrameQuantizerExtensions
public static class FrameQuantizerUtilities
{
/// <summary>
/// Helper method for throwing an exception when a frame quantizer palette has
/// been requested but not built yet.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="palette">The frame quantizer palette.</param>
/// <exception cref="InvalidOperationException">
/// The palette has not been built via <see cref="IFrameQuantizer{TPixel}.BuildPalette(ImageFrame{TPixel}, Rectangle)"/>
/// </exception>
public static void CheckPaletteState<TPixel>(in ReadOnlyMemory<TPixel> palette)
where TPixel : unmanaged, IPixel<TPixel>
{
if (palette.Equals(default))
{
throw new InvalidOperationException("Frame Quantizer palette has not been built.");
}
}
/// <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="quantizer">The frame quantizer.</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.
/// A <see cref="IndexedImageFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
public static IndexedImageFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds)
@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
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;
quantizer.BuildPalette(source, interest);
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
Memory<byte> output = quantizedFrame.GetWritablePixelMemory();
var destination = new IndexedImageFrame<TPixel>(
quantizer.Configuration,
interest.Width,
interest.Height,
quantizer.Palette);
if (quantizer.Options.Dither is null)
{
SecondPass(ref quantizer, source, interest, output, palette);
SecondPass(ref quantizer, source, destination, interest);
}
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);
}
using ImageFrame<TPixel> clone = source.Clone();
SecondPass(ref quantizer, clone, destination, interest);
}
return quantizedFrame;
return destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Rectangle bounds,
Memory<byte> output,
ReadOnlyMemory<TPixel> palette)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
@ -73,7 +90,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null)
{
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, output, bounds, palette);
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(
ref quantizer,
source,
destination,
bounds);
ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration,
bounds,
@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return;
}
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds);
dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
}
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
in TFrameQuantizer quantizer,
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
Memory<byte> output,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette)
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.source = source;
this.output = output;
this.destination = destination;
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;
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<byte> destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY);
// 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 _);
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
}
}
}

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

@ -24,33 +24,37 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
QuantizerOptions Options { get; }
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
/// Gets the quantized color palette.
/// </summary>
/// <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>
QuantizedFrame<TPixel> QuantizeFrame(
ImageFrame<TPixel> source,
Rectangle bounds);
/// <exception cref="InvalidOperationException">
/// The palette has not been built via <see cref="BuildPalette(ImageFrame{TPixel}, Rectangle)"/>.
/// </exception>
ReadOnlyMemory<TPixel> Palette { get; }
/// <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);
void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Quantizes an image frame and return the resulting output pixels.
/// </summary>
/// <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="IndexedImageFrame{TPixel}"/> representing a quantized version of the source frame pixels.
/// </returns>
IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Returns the index and color from the quantized palette corresponding to the give to the given color.
/// Returns the index and color from the quantized palette corresponding 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);
byte GetQuantizedColor(TPixel color, 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

@ -1,30 +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>
/// 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 : unmanaged, 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);
}
}

116
src/ImageSharp/Processing/Processors/Quantization/IndexedImageFrame{TPixel}.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// A pixel-specific image frame where each pixel buffer value represents an index in a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public sealed class IndexedImageFrame<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<byte> pixelsOwner;
private IMemoryOwner<TPixel> paletteOwner;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="IndexedImageFrame{TPixel}"/> class.
/// </summary>
/// <param name="configuration">
/// The configuration which allows altering default behaviour or extending the library.
/// </param>
/// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param>
/// <param name="palette">The color palette.</param>
internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory<TPixel> palette)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette));
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.Configuration = configuration;
this.Width = width;
this.Height = height;
this.pixelsOwner = configuration.MemoryAllocator.AllocateManagedByteBuffer(width * height);
// Copy the palette over. We want the lifetime of this frame to be independant of any palette source.
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(palette.Length);
palette.Span.CopyTo(this.paletteOwner.GetSpan());
this.Palette = this.paletteOwner.Memory.Slice(0, palette.Length);
}
/// <summary>
/// Gets the configuration which allows altering default behaviour or extending the library.
/// </summary>
public Configuration Configuration { get; }
/// <summary>
/// Gets the width of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
public ReadOnlyMemory<TPixel> Palette { get; }
/// <summary>
/// Gets the pixels of this <see cref="IndexedImageFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="ReadOnlySpan{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelBufferSpan() => this.pixelsOwner.GetSpan(); // TODO: Buffer2D<byte>
/// <summary>
/// Gets the representation of the pixels as a <see cref="ReadOnlySpan{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="rowIndex">The row index in the pixel buffer.</param>
/// <returns>The pixel row as a <see cref="ReadOnlySpan{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<byte> GetPixelRowSpan(int rowIndex)
=> this.GetWritablePixelRowSpanUnsafe(rowIndex);
/// <summary>
/// <para>
/// 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.
/// </para>
/// <para>
/// Note: Values written to this span are not sanitized against the palette length.
/// Care should be taken during assignment to prevent out-of-bounds errors.
/// </para>
/// </summary>
/// <param name="rowIndex">The row index in the pixel buffer.</param>
/// <returns>The pixel row as a <see cref="Span{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<byte> GetWritablePixelRowSpanUnsafe(int rowIndex)
=> this.pixelsOwner.GetSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.pixelsOwner.Dispose();
this.paletteOwner.Dispose();
this.pixelsOwner = null;
this.paletteOwner = null;
}
}
}
}

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

@ -3,7 +3,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -21,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int colors;
private readonly int maxColors;
private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct.
@ -40,10 +42,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.colors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
this.maxColors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.maxColors).Clamp(1, 8));
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.pixelMap = default;
this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
}
/// <inheritdoc/>
@ -53,13 +58,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public ReadOnlyMemory<TPixel> Palette
{
get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
@ -79,32 +89,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
TPixel[] palette = this.octree.Palletize(this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
int paletteIndex = 0;
this.octree.Palletize(paletteSpan, this.maxColors, ref paletteIndex);
// Length of reduced palette + transparency.
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors));
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
return palette;
this.palette = result;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{
// 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))
if (this.isDithering || color.Equals(default))
{
var index = (byte)this.octree.GetPaletteIndex(color);
match = palette[index];
return index;
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span);
var index = (byte)this.octree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
}
}
/// <summary>
@ -217,26 +244,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Convert the nodes in the Octree to a palette with a maximum of colorCount colors
/// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param>
/// <returns>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
/// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount)
public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{
while (this.Leaves > colorCount - 1)
{
this.Reduce();
}
// Now palletize the nodes
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex);
// And return the palette
return palette;
}
/// <summary>
@ -438,12 +457,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param>
[MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index)
public void ConstructPalette(Span<TPixel> palette, ref int index)
{
if (this.leaf)
{
// Set the color of the palette entry
var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255));
var vector = Vector3.Clamp(
new Vector3(this.red, this.green, this.blue) / this.pixelCount,
Vector3.Zero,
new Vector3(255));
TPixel pixel = default;
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel;
@ -521,8 +544,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
byte mask = Unsafe.Add(ref maskRef, level);
return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
}
/// <summary>

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

@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class OctreeQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public OctreeQuantizer()
: this(new QuantizerOptions())
: this(DefaultOptions)
{
}

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

@ -15,7 +15,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ReadOnlyMemory<TPixel> palette;
private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary>
@ -23,18 +22,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <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>
/// <param name="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> colors)
public PaletteFrameQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
this.pixelMap = pixelMap;
}
/// <inheritdoc/>
@ -43,19 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette;
public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/>

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

@ -11,12 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class PaletteQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
private readonly ReadOnlyMemory<Color> colorPalette;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="palette">The color palette.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette)
: this(palette, new QuantizerOptions())
: this(palette, DefaultOptions)
{
}
@ -30,15 +33,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.Palette = palette;
this.colorPalette = palette;
this.Options = options;
}
/// <summary>
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public QuantizerOptions Options { get; }
@ -53,11 +51,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
Guard.NotNull(options, nameof(options));
int length = Math.Min(this.Palette.Span.Length, options.MaxColors);
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
int length = Math.Min(this.colorPalette.Length, options.MaxColors);
var palette = new TPixel[length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
var pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
return new PaletteFrameQuantizer<TPixel>(configuration, options, pixelMap);
}
}
}

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

@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Configuration configuration = this.Configuration;
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(configuration);
using QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source, interest);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(source, interest);
var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
ParallelRowIterator.IterateRowIntervals(
@ -52,13 +52,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> quantized;
private readonly IndexedImageFrame<TPixel> quantized;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
ImageFrame<TPixel> source,
QuantizedFrame<TPixel> quantized)
IndexedImageFrame<TPixel> quantized)
{
this.bounds = bounds;
this.source = source;
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelBufferSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;

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

@ -1,91 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Represents a quantized image frame where the pixels indexed by a color palette.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public sealed class QuantizedFrame<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<byte> pixels;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="QuantizedFrame{TPixel}"/> class.
/// </summary>
/// <param name="memoryAllocator">Used to allocated memory for image processing operations.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="palette">The color palette.</param>
internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlyMemory<TPixel> palette)
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.Width = width;
this.Height = height;
this.Palette = palette;
this.pixels = memoryAllocator.AllocateManagedByteBuffer(width * height, AllocationOptions.Clean);
}
/// <summary>
/// Gets the width of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public ReadOnlyMemory<TPixel> Palette { get; private set; }
/// <summary>
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[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 memory of pixel data so <see cref="IFrameQuantizer{TPixel}"/> can fill it.
/// </summary>
internal Memory<byte> GetWritablePixelMemory() => this.pixels.Memory;
}
}

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

@ -10,11 +10,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class WebSafePaletteQuantizer : PaletteQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary>
public WebSafePaletteQuantizer()
: this(new QuantizerOptions())
: this(DefaultOptions)
{
}

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

@ -9,11 +9,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class WernerPaletteQuantizer : PaletteQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary>
public WernerPaletteQuantizer()
: this(new QuantizerOptions())
: this(DefaultOptions)
{
}

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

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -65,30 +66,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// Color moments.
/// </summary>
private IMemoryOwner<Moment> moments;
/// <summary>
/// Color space tag.
/// </summary>
private IMemoryOwner<byte> tag;
/// <summary>
/// Maximum allowed color depth
/// </summary>
private int colors;
/// <summary>
/// The color cube representing the image palette
/// </summary>
private IMemoryOwner<Moment> momentsOwner;
private IMemoryOwner<byte> tagsOwner;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private int maxColors;
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
@ -104,11 +89,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.maxColors = this.Options.MaxColors;
this.memoryAllocator = this.Configuration.MemoryAllocator;
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = this.Options.MaxColors;
this.colorCube = new Box[this.colors];
this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false;
this.pixelMap = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null);
@ -121,21 +108,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public ReadOnlyMemory<TPixel> Palette
{
get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
this.Build3DHistogram(source, bounds);
this.Get3DMoments(this.memoryAllocator);
this.BuildCube();
var palette = new TPixel[this.colors];
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan();
for (int k = 0; k < this.colors; k++)
ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
for (int k = 0; k < this.maxColors; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
@ -143,50 +134,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (moment.Weight > 0)
{
ref TPixel color = ref palette[k];
ref TPixel color = ref paletteSpan[k];
color.FromScaledVector4(moment.Normalize());
}
}
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
return palette;
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, this.maxColors);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.palette = result;
}
/// <inheritdoc/>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
[MethodImpl(InliningOptions.ShortMethod)]
public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{
if (!this.isDithering)
if (this.isDithering)
{
Rgba32 rgba = default;
color.ToRgba32(ref rgba);
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
ReadOnlySpan<byte> tagSpan = this.tag.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
match = palette[index];
return index;
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
Rgba32 rgba = default;
color.ToRgba32(ref rgba);
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
ReadOnlySpan<byte> tagSpan = this.tagsOwner.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
if (!this.isDisposed)
{
return;
this.isDisposed = true;
this.momentsOwner?.Dispose();
this.tagsOwner?.Dispose();
this.paletteOwner?.Dispose();
this.momentsOwner = null;
this.tagsOwner = null;
this.paletteOwner = null;
}
this.isDisposed = true;
this.moments?.Dispose();
this.tag?.Dispose();
this.moments = null;
this.tag = null;
}
/// <summary>
@ -364,7 +362,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="bounds">The bounds within the source image to quantize.</param>
private void Build3DHistogram(ImageFrame<TPixel> source, Rectangle bounds)
{
Span<Moment> momentSpan = this.moments.GetSpan();
Span<Moment> momentSpan = this.momentsOwner.GetSpan();
// Build up the 3-D color histogram
using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width);
@ -392,13 +390,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary>
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator)
/// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator allocator)
{
using IMemoryOwner<Moment> volume = memoryAllocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = memoryAllocator.Allocate<Moment>(IndexAlphaCount);
using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = allocator.Allocate<Moment>(IndexAlphaCount);
Span<Moment> momentSpan = this.moments.GetSpan();
Span<Moment> momentSpan = this.momentsOwner.GetSpan();
Span<Moment> volumeSpan = volume.GetSpan();
Span<Moment> areaSpan = area.GetSpan();
int baseIndex = GetPaletteIndex(1, 0, 0, 0);
@ -440,7 +438,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns>
private double Variance(ref Box cube)
{
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan();
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment volume = Volume(ref cube, momentSpan);
Moment variance =
@ -481,7 +479,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole)
{
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan();
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment bottom = Bottom(ref cube, direction, momentSpan);
float max = 0F;
@ -527,7 +525,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>Returns a value indicating whether the box has been split.</returns>
private bool Cut(ref Box set1, ref Box set2)
{
ReadOnlySpan<Moment> momentSpan = this.moments.GetSpan();
ReadOnlySpan<Moment> momentSpan = this.momentsOwner.GetSpan();
Moment whole = Volume(ref set1, momentSpan);
float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole);
@ -612,7 +610,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="label">A label.</param>
private void Mark(ref Box cube, byte label)
{
Span<byte> tagSpan = this.tag.GetSpan();
Span<byte> tagSpan = this.tagsOwner.GetSpan();
for (int r = cube.RMin + 1; r <= cube.RMax; r++)
{
@ -634,7 +632,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private void BuildCube()
{
Span<double> vv = stackalloc double[this.colors];
// Store the volume variance.
using IMemoryOwner<double> vvOwner = this.Configuration.MemoryAllocator.Allocate<double>(this.maxColors);
Span<double> vv = vvOwner.GetSpan();
ref Box cube = ref this.colorCube[0];
cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0;
@ -643,7 +643,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int next = 0;
for (int i = 1; i < this.colors; i++)
for (int i = 1; i < this.maxColors; i++)
{
ref Box nextCube = ref this.colorCube[next];
ref Box currentCube = ref this.colorCube[i];
@ -672,7 +672,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (temp <= 0D)
{
this.colors = i + 1;
this.maxColors = i + 1;
break;
}
}

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

@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class WuQuantizer : IQuantizer
{
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>.
/// </summary>
public WuQuantizer()
: this(new QuantizerOptions())
: this(DefaultOptions)
{
}

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

@ -21,12 +21,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
private SDImage bmpDrawing;
private Image<Rgba32> bmpCore;
// Try to get as close to System.Drawing's output as possible
private readonly GifEncoder encoder = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
[Params(TestImages.Bmp.Car, TestImages.Png.Rgb48Bpp)]
public string TestImage { get; set; }
[GlobalSetup]
public void ReadImages()
{
if (this.bmpStream == null)
{
this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Bmp.Car));
this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage));
this.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream);
@ -53,15 +62,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
[Benchmark(Description = "ImageSharp Gif")]
public void GifCore()
{
// Try to get as close to System.Drawing's output as possible
var options = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (var memoryStream = new MemoryStream())
{
this.bmpCore.SaveAsGif(memoryStream, options);
this.bmpCore.SaveAsGif(memoryStream, this.encoder);
}
}
}

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

@ -71,10 +71,10 @@ namespace SixLabors.ImageSharp.Tests
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
using (IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
Assert.Equal(index, quantized.GetPixelBufferSpan()[0]);
}
}
}
@ -101,27 +101,27 @@ namespace SixLabors.ImageSharp.Tests
foreach (ImageFrame<TPixel> frame in image.Frames)
{
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration))
using (QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
using (IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
int index = this.GetTransparentIndex(quantized);
Assert.Equal(index, quantized.GetPixelSpan()[0]);
Assert.Equal(index, quantized.GetPixelBufferSpan()[0]);
}
}
}
}
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette
int index = -1;
Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--)
{
paletteSpan[i].ToRgba32(ref trans);
Span<Rgba32> colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length);
if (trans.Equals(default))
PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, colorSpan);
for (int i = colorSpan.Length - 1; i >= 0; i--)
{
if (colorSpan[i].Equals(default))
{
index = i;
}

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

@ -21,13 +21,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(1, result.GetPixelBufferSpan().Length);
Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
Assert.Equal(0, result.GetPixelBufferSpan()[0]);
}
[Fact]
@ -40,13 +40,13 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(1, result.GetPixelBufferSpan().Length);
Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
Assert.Equal(0, result.GetPixelBufferSpan()[0]);
}
[Fact]
@ -85,19 +85,19 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config);
using QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(256, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);
Assert.Equal(256, result.GetPixelBufferSpan().Length);
var actualImage = new Image<Rgba32>(1, 256);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++)
@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<TPixel> frame = image.Frames.RootFrame;
using IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(config);
using QuantizedFrame<TPixel> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
using IndexedImageFrame<TPixel> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
Assert.Equal(48, result.Palette.Length);
}
@ -152,17 +152,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization
ImageFrame<Rgba32> frame = image.Frames.RootFrame;
using (IFrameQuantizer<Rgba32> frameQuantizer = quantizer.CreateFrameQuantizer<Rgba32>(config))
using (QuantizedFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
using (IndexedImageFrame<Rgba32> result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);
Assert.Equal(256, result.GetPixelBufferSpan().Length);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
int paletteCount = paletteSpan.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{
Span<Rgba32> row = actualImage.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelSpan();
ReadOnlySpan<byte> quantizedPixelSpan = result.GetPixelBufferSpan();
int yy = y * actualImage.Width;
for (int x = 0; x < actualImage.Width; x++)

2
tests/Images/External

@ -1 +1 @@
Subproject commit f8a76fd3a900b90c98df67ac896574383a4d09f3
Subproject commit 1fea1ceab89e87cc5f11376fa46164d3d27566c0
Loading…
Cancel
Save