Browse Source

Merge branch 'master' into patch-1

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

4
src/ImageSharp/ColorSpaces/Conversion/ColorSpaceConverter.LinearRgb.cs

@ -248,7 +248,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
} }
/// <summary> /// <summary>
/// Performs the bulk conversion from <see cref="Lms"/> into <see cref="LinearRgb"/>. /// Performs the bulk conversion from <see cref="Rgb"/> into <see cref="LinearRgb"/>.
/// </summary> /// </summary>
/// <param name="source">The span to the source colors</param> /// <param name="source">The span to the source colors</param>
/// <param name="destination">The span to the destination colors</param> /// <param name="destination">The span to the destination colors</param>
@ -435,4 +435,4 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion
return this.ToLinearRgb(rgb); return this.ToLinearRgb(rgb);
} }
} }
} }

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

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

@ -4,6 +4,7 @@
using System.IO; using System.IO;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
@ -17,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets or sets the quantizer for reducing the color count. /// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/> /// Defaults to the <see cref="OctreeQuantizer"/>
/// </summary> /// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer(); public IQuantizer Quantizer { get; set; } = KnownQuantizers.Octree;
/// <summary> /// <summary>
/// Gets or sets the color table mode: Global or local. /// 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; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette. // Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized; IndexedImageFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration)) using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{ {
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
} }
// Get the number of bits. // 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. // Write the header.
this.WriteHeader(stream); this.WriteHeader(stream);
@ -119,15 +119,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
// Clean up. // Clean up.
quantized?.Dispose(); quantized.Dispose();
// TODO: Write extension etc // TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer); 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> 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++) for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
@ -142,22 +147,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else else
{ {
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette)) if (!pixelMapSet)
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{ {
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> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrame<TPixel> previousFrame = null; ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = 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; ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null) if (quantized is null)
@ -173,27 +183,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength MaxColors = frameMetadata.ColorTableLength
}; };
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options)) using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options);
{ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
} }
else else
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration)) using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
{ quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
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.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream); this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream); this.WriteImageData(quantized, stream);
quantized?.Dispose(); quantized.Dispose();
quantized = null; // So next frame can regenerate it quantized = null; // So next frame can regenerate it
previousFrame = frame; previousFrame = frame;
previousMeta = frameMetadata; previousMeta = frameMetadata;
@ -208,25 +214,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns> /// <returns>
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized) private int GetTransparentIndex<TPixel>(IndexedImageFrame<TPixel> quantized)
where TPixel : unmanaged, IPixel<TPixel> 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 index = -1;
int length = quantized.Palette.Length; ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(length)) using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteSpan.Length);
{ Span<Rgba32> rgbaSpan = rgbaOwner.GetSpan();
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan(); PixelOperations<TPixel>.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan);
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan); ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, 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; 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[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel; this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2); 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. // Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan(); ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0; 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); WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
} }
@ -391,7 +398,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="extension">The extension to write to the stream.</param> /// <param name="extension">The extension to write to the stream.</param>
/// <param name="stream">The stream to write to.</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[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label; this.buffer[1] = extension.Label;
@ -437,37 +445,33 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="stream">The stream to write to.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
// The maximum number of colors for the bit depth // The maximum number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3; int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf<Rgb24>();
int pixelCount = image.Palette.Length;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength, AllocationOptions.Clean);
{ PixelOperations<TPixel>.Instance.ToRgb24Bytes(
PixelOperations<TPixel>.Instance.ToRgb24Bytes( this.configuration,
this.configuration, image.Palette.Span,
image.Palette.Span, colorTable.GetSpan(),
colorTable.GetSpan(), image.Palette.Length);
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength); stream.Write(colorTable.Array, 0, colorTableLength);
}
} }
/// <summary> /// <summary>
/// Writes the image pixel data to the stream. /// Writes the image pixel data to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <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> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth)) using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
{ encoder.Encode(image.GetPixelBufferSpan(), stream);
encoder.Encode(image.GetPixelSpan(), stream);
}
} }
} }
} }

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

@ -274,7 +274,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(indexedPixels); 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; hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2) 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; ImageMetadata metadata = image.Metadata;
PngMetadata pngMetadata = metadata.GetPngMetadata(); PngMetadata pngMetadata = metadata.GetPngMetadata();
PngEncoderOptionsHelpers.AdjustOptions<TPixel>(this.options, pngMetadata, out this.use16Bit, out this.bytesPerPixel); 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); this.bitDepth = PngEncoderOptionsHelpers.CalculateBitDepth(this.options, image, quantized);
stream.Write(PngConstants.HeaderBytes); stream.Write(PngConstants.HeaderBytes);
@ -371,7 +371,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="rowSpan">The row span.</param> /// <param name="rowSpan">The row span.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
switch (this.options.ColorType) switch (this.options.ColorType)
@ -380,12 +380,11 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.bitDepth < 8) 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 else
{ {
int stride = this.currentScanline.Length(); quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan());
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.currentScanline.GetSpan());
} }
break; break;
@ -440,7 +439,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="quantized">The quantized pixels. Can be null.</param> /// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param> /// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.CollectPixelBytes(rowSpan, quantized, row); this.CollectPixelBytes(rowSpan, quantized, row);
@ -546,59 +545,54 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="quantized">The quantized frame.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (quantized == null) if (quantized is null)
{ {
return; return;
} }
// Grab the palette and write it to the stream. // Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette.Span; ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256); int paletteLength = palette.Length;
int colorTableLength = paletteLength * 3; int colorTableLength = paletteLength * Unsafe.SizeOf<Rgb24>();
bool anyAlpha = false; bool hasAlpha = false;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength)) 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);
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; // Bulk convert our palette to RGBA to allow assignment to tables.
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G; using IMemoryOwner<Rgba32> rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate<Rgba32>(paletteLength);
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B; 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) // Loop, assign, and extract alpha values from the palette.
{ for (int i = 0; i < paletteLength; i++)
alpha = byte.MaxValue; {
} Rgba32 rgba = Unsafe.Add(ref rgbaPaletteRef, i);
byte alpha = rgba.A;
anyAlpha = anyAlpha || alpha < byte.MaxValue; Unsafe.Add(ref colorTableRef, i) = rgba.Rgb;
Unsafe.Add(ref alphaTableRef, i) = alpha; 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 this.WriteChunk(stream, PngChunkType.Palette, colorTable.Array, 0, colorTableLength);
if (anyAlpha)
{ // Write the transparency data
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength); 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="pixels">The image.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param> /// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte[] buffer; byte[] buffer;
@ -881,7 +875,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="pixels">The pixels.</param> /// <param name="pixels">The pixels.</param>
/// <param name="quantized">The quantized pixels span.</param> /// <param name="quantized">The quantized pixels span.</param>
/// <param name="deflateStream">The deflate stream.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesPerScanline = this.CalculateScanlineLength(this.width); int bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -960,7 +954,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="quantized">The quantized.</param> /// <param name="quantized">The quantized.</param>
/// <param name="deflateStream">The deflate stream.</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> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = quantized.Width; int width = quantized.Width;
@ -987,7 +981,7 @@ namespace SixLabors.ImageSharp.Formats.Png
row += Adam7.RowIncrement[pass]) row += Adam7.RowIncrement[pass])
{ {
// collect data // collect data
ReadOnlySpan<byte> srcRow = quantized.GetRowSpan(row); ReadOnlySpan<byte> srcRow = quantized.GetPixelRowSpan(row);
for (int col = startCol, i = 0; for (int col = startCol, i = 0;
col < width; col < width;
col += Adam7.ColumnIncrement[pass]) 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> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="image">The image.</param> /// <param name="image">The image.</param>
public static QuantizedFrame<TPixel> CreateQuantizedFrame<TPixel>( public static IndexedImageFrame<TPixel> CreateQuantizedFrame<TPixel>(
PngEncoderOptions options, PngEncoderOptions options,
Image<TPixel> image) Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Png
public static byte CalculateBitDepth<TPixel>( public static byte CalculateBitDepth<TPixel>(
PngEncoderOptions options, PngEncoderOptions options,
Image<TPixel> image, Image<TPixel> image,
QuantizedFrame<TPixel> quantizedFrame) IndexedImageFrame<TPixel> quantizedFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte bitDepth; byte bitDepth;

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

@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Memory
/// <inheritdoc /> /// <inheritdoc />
public override Span<T> GetSpan() public override Span<T> GetSpan()
{ {
if (this.Data == null) if (this.Data is null)
{ {
throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer<T>"); 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. /// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller. /// The last buffer is allowed to be smaller.
/// </summary> /// </summary>
public int BufferLength { get; } int BufferLength { get; }
/// <summary> /// <summary>
/// Gets the aggregate number of elements in the group. /// Gets the aggregate number of elements in the group.
/// </summary> /// </summary>
public long TotalLength { get; } long TotalLength { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the group has been invalidated. /// 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;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -89,29 +90,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<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 offsetY = bounds.Top;
int offsetX = bounds.Left; int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale; float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++) for (int y = bounds.Top; y < bounds.Bottom; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
int rowStart = (y - offsetY) * width; ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = row[x]; TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
outputSpan[rowStart + x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
} }
} }
@ -119,25 +116,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
Configuration configuration, in TPaletteDitherImageProcessor processor,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<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++) 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++) for (int x = bounds.Left; x < bounds.Right; x++)
{ {
TPixel sourcePixel = row[x]; ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
pixelMap.GetClosestColor(sourcePixel, out TPixel transformed); TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale); 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. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; 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="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame quantizer.</param> /// <param name="quantizer">The frame quantizer.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</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> /// <param name="bounds">The region of interest bounds.</param>
void ApplyQuantizationDither<TFrameQuantizer, TPixel>( void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
@ -36,18 +33,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Transforms the image frame applying a dither matrix. /// Transforms the image frame applying a dither matrix.
/// This method should be treated as destructive, altering the input pixels. /// This method should be treated as destructive, altering the input pixels.
/// </summary> /// </summary>
/// <typeparam name="TPaletteDitherImageProcessor">The type of palette dithering processor.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The configuration.</param> /// <param name="processor">The palette dithering processor.</param>
/// <param name="palette">The quantized palette.</param>
/// <param name="source">The source image.</param> /// <param name="source">The source image.</param>
/// <param name="bounds">The region of interest bounds.</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<TPaletteDitherImageProcessor, TPixel>(
void ApplyPaletteDither<TPixel>( in TPaletteDitherImageProcessor processor,
Configuration configuration,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<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> /// <content>
/// An ordered dithering matrix with equal sides of arbitrary length /// An ordered dithering matrix with equal sides of arbitrary length
/// </content> /// </content>
public readonly partial struct OrderedDither : IDither public readonly partial struct OrderedDither
{ {
/// <summary> /// <summary>
/// Applies order dithering using the 2x2 Bayer dithering matrix. /// 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;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -105,23 +105,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyQuantizationDither<TFrameQuantizer, TPixel>( public void ApplyQuantizationDither<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds) Rectangle bounds)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var ditherOperation = new QuantizeDitherRowIntervalOperation<TFrameQuantizer, TPixel>( var ditherOperation = new QuantizeDitherRowOperation<TFrameQuantizer, TPixel>(
ref quantizer, ref quantizer,
in Unsafe.AsRef(this), in Unsafe.AsRef(this),
source, source,
output, destination,
bounds, bounds);
palette,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
ParallelRowIterator.IterateRowIntervals( ParallelRowIterator.IterateRows(
quantizer.Configuration, quantizer.Configuration,
bounds, bounds,
in ditherOperation); in ditherOperation);
@ -129,24 +126,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void ApplyPaletteDither<TPixel>( public void ApplyPaletteDither<TPaletteDitherImageProcessor, TPixel>(
Configuration configuration, in TPaletteDitherImageProcessor processor,
ReadOnlyMemory<TPixel> palette,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
float scale) where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var ditherOperation = new PaletteDitherRowIntervalOperation<TPixel>( var ditherOperation = new PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this), in Unsafe.AsRef(this),
source, source,
bounds, bounds);
palette,
scale,
ImageMaths.GetBitsNeededForColorDepth(palette.Span.Length));
ParallelRowIterator.IterateRowIntervals( ParallelRowIterator.IterateRows(
configuration, processor.Configuration,
bounds, bounds,
in ditherOperation); in ditherOperation);
} }
@ -200,102 +194,87 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public override int GetHashCode() public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY); => 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 TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly TFrameQuantizer quantizer; private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither; private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output; private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth; private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowIntervalOperation( public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
in OrderedDither dither, in OrderedDither dither,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette,
int bitDepth)
{ {
this.quantizer = quantizer; this.quantizer = quantizer;
this.dither = dither; this.dither = dither;
this.source = source; this.source = source;
this.output = output; this.destination = destination;
this.bounds = bounds; this.bounds = bounds;
this.palette = palette; this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
this.bitDepth = bitDepth;
} }
[MethodImpl(InliningOptions.ShortMethod)] [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 offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale; 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); TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
int rowStart = (y - offsetY) * width; Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
// 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 _);
}
} }
} }
} }
private readonly struct PaletteDitherRowIntervalOperation<TPixel> : IRowIntervalOperation private readonly struct PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel> : IRowOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither; private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly float scale; private readonly float scale;
private readonly int bitDepth; private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowIntervalOperation( public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither, in OrderedDither dither,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette,
float scale,
int bitDepth)
{ {
this.processor = processor;
this.dither = dither; this.dither = dither;
this.source = source; this.source = source;
this.bounds = bounds; this.bounds = bounds;
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); this.scale = processor.DitherScale;
this.scale = scale; this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
this.bitDepth = bitDepth;
} }
[MethodImpl(InliningOptions.ShortMethod)] [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); ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
for (int x = this.bounds.Left; x < this.bounds.Right; x++) sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
{
TPixel dithered = this.dither.Dither(row[x], x, y, this.bitDepth, this.scale);
this.pixelMap.GetClosestColor(dithered, out TPixel transformed);
row[x] = transformed;
}
} }
} }
} }

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

@ -3,7 +3,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Processing.Processors.Dithering namespace SixLabors.ImageSharp.Processing.Processors.Dithering
{ {
@ -14,11 +16,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel> internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly int paletteLength; private readonly DitherProcessor ditherProcessor;
private readonly IDither dither; private readonly IDither dither;
private readonly float ditherScale; private IMemoryOwner<TPixel> paletteOwner;
private readonly ReadOnlyMemory<Color> sourcePalette;
private IMemoryOwner<TPixel> palette;
private bool isDisposed; private bool isDisposed;
/// <summary> /// <summary>
@ -31,37 +31,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle) public PaletteDitherProcessor(Configuration configuration, PaletteDitherProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle) : base(configuration, source, sourceRectangle)
{ {
this.paletteLength = definition.Palette.Span.Length;
this.dither = definition.Dither; this.dither = definition.Dither;
this.ditherScale = definition.DitherScale;
this.sourcePalette = definition.Palette;
}
/// <inheritdoc/> ReadOnlySpan<Color> sourcePalette = definition.Palette.Span;
protected override void OnFrameApply(ImageFrame<TPixel> source) this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length);
{ Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span);
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
this.dither.ApplyPaletteDither( this.ditherProcessor = new DitherProcessor(
this.Configuration, this.Configuration,
this.palette.Memory, this.paletteOwner.Memory,
source, definition.DitherScale);
interest,
this.ditherScale);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void BeforeFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
{ {
// Lazy init palettes: var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
if (this.palette is null) this.dither.ApplyPaletteDither(in this.ditherProcessor, source, interest);
{
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);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -72,15 +58,48 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
return; return;
} }
this.isDisposed = true;
if (disposing) if (disposing)
{ {
this.palette?.Dispose(); this.paletteOwner.Dispose();
} }
this.palette = null; this.paletteOwner = null;
this.isDisposed = true;
base.Dispose(disposing); 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.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
/// <summary> /// <summary>
/// Gets the closest color to the supplied color based upon the Eucladean distance. /// Gets the closest color to the supplied color based upon the Euclidean distance.
/// TODO: Expose this somehow.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <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> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly ConcurrentDictionary<int, Vector4> vectorCache; private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache; private readonly ConcurrentDictionary<TPixel, int> distanceCache;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct. /// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary> /// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</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; this.Palette = palette;
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span; this.vectorCache = new Vector4[palette.Length];
this.vectorCache = new ConcurrentDictionary<int, Vector4>();
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
for (int i = 0; i < paletteSpan.Length; i++) // Use the same rules across all target frameworks.
{ this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
this.vectorCache[i] = paletteSpan[i].ToScaledVector4(); PixelOperations<TPixel>.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
}
} }
/// <inheritdoc/> /// <summary>
public ReadOnlyMemory<TPixel> Palette { get; } /// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
/// The palette memory is owned by the palette source that created it.
/// <inheritdoc/> /// </summary>
public override bool Equals(object obj) public ReadOnlyMemory<TPixel> Palette
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map); {
[MethodImpl(InliningOptions.ShortMethod)]
/// <inheritdoc/> get;
public bool Equals(EuclideanPixelMap<TPixel> other) }
=> this.Palette.Equals(other.Palette);
/// <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)] [MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match) 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 // 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 this.GetClosestColorSlow(color, ref paletteRef, out match);
return index;
} }
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)] [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. // Loop through the palette and find the nearest match.
int index = 0; int index = 0;
float leastDistance = float.MaxValue; float leastDistance = float.MaxValue;
Vector4 vector = color.ToScaledVector4(); var vector = color.ToVector4();
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < palette.Length; i++) 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); 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) if (distance < leastDistance)
{ {
// Less than... assign.
index = i; index = i;
leastDistance = distance; 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 // Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index; this.distanceCache[color] = index;
match = palette[index]; match = Unsafe.Add(ref paletteRef, index);
return 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 namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
/// <summary> /// <summary>
/// Contains extension methods for frame quantizers. /// Contains utility methods for <see cref="IFrameQuantizer{TPixel}"/> instances.
/// </summary> /// </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> /// <summary>
/// Quantizes an image frame and return the resulting output pixels. /// Quantizes an image frame and return the resulting output pixels.
/// </summary> /// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam> /// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</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="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param> /// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns> /// <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> /// </returns>
public static QuantizedFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>( public static IndexedImageFrame<TPixel> QuantizeFrame<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds) Rectangle bounds)
@ -37,35 +55,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var interest = Rectangle.Intersect(source.Bounds(), bounds); var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs. // Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest); quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette); var destination = new IndexedImageFrame<TPixel>(
Memory<byte> output = quantizedFrame.GetWritablePixelMemory(); quantizer.Configuration,
interest.Width,
interest.Height,
quantizer.Palette);
if (quantizer.Options.Dither is null) if (quantizer.Options.Dither is null)
{ {
SecondPass(ref quantizer, source, interest, output, palette); SecondPass(ref quantizer, source, destination, interest);
} }
else else
{ {
// We clone the image as we don't want to alter the original via error diffusion based dithering. // We clone the image as we don't want to alter the original via error diffusion based dithering.
using (ImageFrame<TPixel> clone = source.Clone()) using ImageFrame<TPixel> clone = source.Clone();
{ SecondPass(ref quantizer, clone, destination, interest);
SecondPass(ref quantizer, clone, interest, output, palette);
}
} }
return quantizedFrame; return destination;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
private static void SecondPass<TFrameQuantizer, TPixel>( private static void SecondPass<TFrameQuantizer, TPixel>(
ref TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Rectangle bounds, IndexedImageFrame<TPixel> destination,
Memory<byte> output, Rectangle bounds)
ReadOnlyMemory<TPixel> palette)
where TFrameQuantizer : struct, IFrameQuantizer<TPixel> where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -73,7 +90,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null) 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( ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration, quantizer.Configuration,
bounds, bounds,
@ -82,7 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return; return;
} }
dither.ApplyQuantizationDither(ref quantizer, palette, source, output, bounds); dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
} }
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
@ -91,43 +113,36 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
private readonly TFrameQuantizer quantizer; private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly Memory<byte> output; private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowIntervalOperation(
in TFrameQuantizer quantizer, ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
Memory<byte> output, IndexedImageFrame<TPixel> destination,
Rectangle bounds, Rectangle bounds)
ReadOnlyMemory<TPixel> palette)
{ {
this.quantizer = quantizer; this.quantizer = quantizer;
this.source = source; this.source = source;
this.output = output; this.destination = destination;
this.bounds = bounds; this.bounds = bounds;
this.palette = palette;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) 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 offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++) for (int y = rows.Min; y < rows.Max; y++)
{ {
Span<TPixel> row = this.source.GetPixelRowSpan(y); Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
int rowStart = (y - offsetY) * width; 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++) 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; } QuantizerOptions Options { get; }
/// <summary> /// <summary>
/// Quantizes an image frame and return the resulting output pixels. /// Gets the quantized color palette.
/// </summary> /// </summary>
/// <param name="source">The source image frame to quantize.</param> /// <exception cref="InvalidOperationException">
/// <param name="bounds">The bounds within the frame to quantize.</param> /// The palette has not been built via <see cref="BuildPalette(ImageFrame{TPixel}, Rectangle)"/>.
/// <returns> /// </exception>
/// A <see cref="QuantizedFrame{TPixel}"/> representing a quantized version of the source frame pixels. ReadOnlyMemory<TPixel> Palette { get; }
/// </returns>
QuantizedFrame<TPixel> QuantizeFrame(
ImageFrame<TPixel> source,
Rectangle bounds);
/// <summary> /// <summary>
/// Builds the quantized palette from the given image frame and bounds. /// Builds the quantized palette from the given image frame and bounds.
/// </summary> /// </summary>
/// <param name="source">The source image frame.</param> /// <param name="source">The source image frame.</param>
/// <param name="bounds">The region of interest bounds.</param> /// <param name="bounds">The region of interest bounds.</param>
/// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns> void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
ReadOnlyMemory<TPixel> 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> /// <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> /// </summary>
/// <param name="color">The color to match.</param> /// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param> /// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns> /// <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. // TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches); // 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;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -21,10 +20,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel> public struct OctreeFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly int colors; private readonly int maxColors;
private readonly Octree octree; private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap; private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering; private readonly bool isDithering;
private bool isDisposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct. /// 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.Configuration = configuration;
this.Options = options; this.Options = options;
this.colors = this.Options.MaxColors; this.maxColors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); 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.pixelMap = default;
this.isDithering = !(this.Options.Dither is null); this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -53,13 +58,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] public ReadOnlyMemory<TPixel> Palette
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) {
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [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); using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan(); Span<Rgba32> bufferSpan = buffer.GetSpan();
@ -79,32 +89,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
} }
} }
TPixel[] palette = this.octree.Palletize(this.colors); Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); 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/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [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 // Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent // so cannot tell the difference between a fully transparent
// pixel and a black one. // pixel and a black one.
if (!this.isDithering && !color.Equals(default)) if (this.isDithering || color.Equals(default))
{ {
var index = (byte)this.octree.GetPaletteIndex(color); return (byte)this.pixelMap.GetClosestColor(color, out match);
match = palette[index];
return index;
} }
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/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
if (!this.isDisposed)
{
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
}
} }
/// <summary> /// <summary>
@ -217,26 +244,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors
/// </summary> /// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param> /// <param name="colorCount">The maximum number of colors</param>
/// <returns> /// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount) public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{ {
while (this.Leaves > colorCount - 1) while (this.Leaves > colorCount - 1)
{ {
this.Reduce(); this.Reduce();
} }
// Now palletize the nodes
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex); this.root.ConstructPalette(palette, ref paletteIndex);
// And return the palette
return palette;
} }
/// <summary> /// <summary>
@ -438,12 +457,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The palette</param> /// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param> /// <param name="index">The current palette index</param>
[MethodImpl(InliningOptions.ColdPath)] [MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index) public void ConstructPalette(Span<TPixel> palette, ref int index)
{ {
if (this.leaf) if (this.leaf)
{ {
// Set the color of the palette entry // 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; TPixel pixel = default;
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue)); pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel; palette[index] = pixel;
@ -521,8 +544,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
byte mask = Unsafe.Add(ref maskRef, level); byte mask = Unsafe.Add(ref maskRef, level);
return ((color.R & mask) >> shift) return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1)) | ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2)); | ((color.B & mask) >> (shift - 2));
} }
/// <summary> /// <summary>

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

@ -11,12 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class OctreeQuantizer : IQuantizer public class OctreeQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class /// Initializes a new instance of the <see cref="OctreeQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>. /// using the default <see cref="QuantizerOptions"/>.
/// </summary> /// </summary>
public OctreeQuantizer() 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> internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private readonly ReadOnlyMemory<TPixel> palette;
private readonly EuclideanPixelMap<TPixel> pixelMap; private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary> /// <summary>
@ -23,18 +22,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param> /// <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="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)] [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(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.pixelMap = pixelMap;
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -43,19 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/> /// <inheritdoc/>
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette => this.pixelMap.Palette;
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) public readonly IndexedImageFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); => FrameQuantizerUtilities.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette; {
}
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [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); => (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/> /// <inheritdoc/>

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

@ -11,12 +11,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class PaletteQuantizer : IQuantizer public class PaletteQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
private readonly ReadOnlyMemory<Color> colorPalette;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class. /// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary> /// </summary>
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
public PaletteQuantizer(ReadOnlyMemory<Color> palette) 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.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.Palette = palette; this.colorPalette = palette;
this.Options = options; this.Options = options;
} }
/// <summary>
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc /> /// <inheritdoc />
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
@ -53,11 +51,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
Guard.NotNull(options, nameof(options)); 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]; var palette = new TPixel[length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan()); Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
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; Configuration configuration = this.Configuration;
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(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); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
ParallelRowIterator.IterateRowIntervals( ParallelRowIterator.IterateRowIntervals(
@ -52,13 +52,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{ {
private readonly Rectangle bounds; private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source; private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> quantized; private readonly IndexedImageFrame<TPixel> quantized;
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation( public RowIntervalOperation(
Rectangle bounds, Rectangle bounds,
ImageFrame<TPixel> source, ImageFrame<TPixel> source,
QuantizedFrame<TPixel> quantized) IndexedImageFrame<TPixel> quantized)
{ {
this.bounds = bounds; this.bounds = bounds;
this.source = source; this.source = source;
@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows) 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; ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top; int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left; 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> /// </summary>
public class WebSafePaletteQuantizer : PaletteQuantizer public class WebSafePaletteQuantizer : PaletteQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WebSafePaletteQuantizer" /> class.
/// </summary> /// </summary>
public WebSafePaletteQuantizer() 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> /// </summary>
public class WernerPaletteQuantizer : PaletteQuantizer public class WernerPaletteQuantizer : PaletteQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class. /// Initializes a new instance of the <see cref="WernerPaletteQuantizer" /> class.
/// </summary> /// </summary>
public WernerPaletteQuantizer() 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.Buffers;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -65,30 +66,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary> private IMemoryOwner<Moment> momentsOwner;
/// Color moments. private IMemoryOwner<byte> tagsOwner;
/// </summary> private IMemoryOwner<TPixel> paletteOwner;
private IMemoryOwner<Moment> moments; private ReadOnlyMemory<TPixel> palette;
private int maxColors;
/// <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 readonly Box[] colorCube; private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap; private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering; private readonly bool isDithering;
private bool isDisposed; private bool isDisposed;
/// <summary> /// <summary>
@ -104,11 +89,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;
this.maxColors = this.Options.MaxColors;
this.memoryAllocator = this.Configuration.MemoryAllocator; this.memoryAllocator = this.Configuration.MemoryAllocator;
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean); this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean); this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = this.Options.MaxColors; this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.colorCube = new Box[this.colors]; this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false; this.isDisposed = false;
this.pixelMap = default; this.pixelMap = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null); this.isDithering = this.isDithering = !(this.Options.Dither is null);
@ -121,21 +108,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public QuantizerOptions Options { get; } public QuantizerOptions Options { get; }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] public ReadOnlyMemory<TPixel> Palette
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds) {
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds); get
{
FrameQuantizerUtilities.CheckPaletteState(in this.palette);
return this.palette;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds) public void BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{ {
this.Build3DHistogram(source, bounds); this.Build3DHistogram(source, bounds);
this.Get3DMoments(this.memoryAllocator); this.Get3DMoments(this.memoryAllocator);
this.BuildCube(); this.BuildCube();
var palette = new TPixel[this.colors]; ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan(); Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
for (int k = 0; k < this.maxColors; k++)
for (int k = 0; k < this.colors; k++)
{ {
this.Mark(ref this.colorCube[k], (byte)k); this.Mark(ref this.colorCube[k], (byte)k);
@ -143,50 +134,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (moment.Weight > 0) if (moment.Weight > 0)
{ {
ref TPixel color = ref palette[k]; ref TPixel color = ref paletteSpan[k];
color.FromScaledVector4(moment.Normalize()); color.FromScaledVector4(moment.Normalize());
} }
} }
this.pixelMap = new EuclideanPixelMap<TPixel>(palette); ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, this.maxColors);
return palette; this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.palette = result;
} }
/// <inheritdoc/> /// <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; return (byte)this.pixelMap.GetClosestColor(color, out match);
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); 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/> /// <inheritdoc/>
public void Dispose() 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> /// <summary>
@ -364,7 +362,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="bounds">The bounds within the source image to quantize.</param> /// <param name="bounds">The bounds within the source image to quantize.</param>
private void Build3DHistogram(ImageFrame<TPixel> source, Rectangle bounds) 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 // Build up the 3-D color histogram
using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width); using IMemoryOwner<Rgba32> buffer = this.memoryAllocator.Allocate<Rgba32>(bounds.Width);
@ -392,13 +390,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary> /// </summary>
/// <param name="memoryAllocator">The memory allocator used for allocating buffers.</param> /// <param name="allocator">The memory allocator used for allocating buffers.</param>
private void Get3DMoments(MemoryAllocator memoryAllocator) private void Get3DMoments(MemoryAllocator allocator)
{ {
using IMemoryOwner<Moment> volume = memoryAllocator.Allocate<Moment>(IndexCount * IndexAlphaCount); using IMemoryOwner<Moment> volume = allocator.Allocate<Moment>(IndexCount * IndexAlphaCount);
using IMemoryOwner<Moment> area = memoryAllocator.Allocate<Moment>(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> volumeSpan = volume.GetSpan();
Span<Moment> areaSpan = area.GetSpan(); Span<Moment> areaSpan = area.GetSpan();
int baseIndex = GetPaletteIndex(1, 0, 0, 0); int baseIndex = GetPaletteIndex(1, 0, 0, 0);
@ -440,7 +438,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private double Variance(ref Box cube) 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 volume = Volume(ref cube, momentSpan);
Moment variance = Moment variance =
@ -481,7 +479,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) 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); Moment bottom = Bottom(ref cube, direction, momentSpan);
float max = 0F; 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> /// <returns>Returns a value indicating whether the box has been split.</returns>
private bool Cut(ref Box set1, ref Box set2) 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); Moment whole = Volume(ref set1, momentSpan);
float maxR = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutR, whole); 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> /// <param name="label">A label.</param>
private void Mark(ref Box cube, byte label) 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++) for (int r = cube.RMin + 1; r <= cube.RMax; r++)
{ {
@ -634,7 +632,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
private void BuildCube() 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]; ref Box cube = ref this.colorCube[0];
cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0;
@ -643,7 +643,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int next = 0; 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 nextCube = ref this.colorCube[next];
ref Box currentCube = ref this.colorCube[i]; ref Box currentCube = ref this.colorCube[i];
@ -672,7 +672,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (temp <= 0D) if (temp <= 0D)
{ {
this.colors = i + 1; this.maxColors = i + 1;
break; break;
} }
} }

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

@ -10,12 +10,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
public class WuQuantizer : IQuantizer public class WuQuantizer : IQuantizer
{ {
private static readonly QuantizerOptions DefaultOptions = new QuantizerOptions();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class /// Initializes a new instance of the <see cref="WuQuantizer"/> class
/// using the default <see cref="QuantizerOptions"/>. /// using the default <see cref="QuantizerOptions"/>.
/// </summary> /// </summary>
public WuQuantizer() 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 SDImage bmpDrawing;
private Image<Rgba32> bmpCore; 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] [GlobalSetup]
public void ReadImages() public void ReadImages()
{ {
if (this.bmpStream == null) 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.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpStream.Position = 0; this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream); this.bmpDrawing = SDImage.FromStream(this.bmpStream);
@ -53,15 +62,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
[Benchmark(Description = "ImageSharp Gif")] [Benchmark(Description = "ImageSharp Gif")]
public void GifCore() 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()) 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) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration)) 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); 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) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
using (IFrameQuantizer<TPixel> frameQuantizer = quantizer.CreateFrameQuantizer<TPixel>(this.Configuration)) 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); 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> 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 index = -1;
Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span; ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--) Span<Rgba32> colorSpan = stackalloc Rgba32[QuantizerConstants.MaxColors].Slice(0, paletteSpan.Length);
{
paletteSpan[i].ToRgba32(ref trans);
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; index = i;
} }

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

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

2
tests/Images/External

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