Browse Source

Simplify, fix color mapping, and refactor for performance.

pull/1138/head
James Jackson-South 6 years ago
parent
commit
63f277b404
  1. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  2. 24
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  4. 19
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  5. 3
      src/ImageSharp/Processing/Processors/Dithering/IPaletteDitherImageProcessor{TPixel}.cs
  6. 17
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  7. 9
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  8. 68
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  9. 12
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
  10. 3
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  11. 30
      src/ImageSharp/Processing/Processors/Quantization/IPixelMap{TPixel}.cs
  12. 26
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  13. 53
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  14. 14
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  15. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  16. 4
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  17. 10
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  18. 18
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  19. 2
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  20. 8
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

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

@ -336,10 +336,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
private void Write8BitColor<TPixel>(Stream stream, ImageFrame<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel>
{
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette;
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
var color = default(Rgba32);
// TODO: Use bulk conversion here for better perf

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

@ -128,6 +128,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// The palette quantizer can reuse the same pixel map across multiple frames
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
EuclideanPixelMap<TPixel> pixelMap = default;
bool pixelMapSet = false;
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
@ -142,7 +147,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette);
if (!pixelMapSet)
{
pixelMapSet = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette, quantized.Palette.Span.Length);
}
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
@ -214,7 +225,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{
@ -321,8 +332,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
return;
}
foreach (string comment in metadata.Comments)
for (var i = 0; i < metadata.Comments.Count; i++)
{
string comment = metadata.Comments[i];
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
@ -330,7 +342,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
for (;
idx <= comment.Length - GifConstants.MaxCommentSubBlockLength;
idx += GifConstants.MaxCommentSubBlockLength)
{
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
}
@ -443,7 +457,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette,
image.Palette.Span,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);

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

@ -555,7 +555,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
// Grab the palette and write it to the stream.
ReadOnlySpan<TPixel> palette = quantized.Palette;
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3;
bool anyAlpha = false;

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

@ -4,6 +4,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -95,20 +96,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
ReadOnlySpan<TPixel> paletteSpan = destination.Palette;
int offsetY = bounds.Top;
int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<byte> destinationRow = destination.GetPixelRowSpan(y - offsetY);
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y - offsetY));
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = sourceRow[x];
destinationRow[x - offsetX] = quantizer.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed);
TPixel sourcePixel = Unsafe.Add(ref sourceRowRef, x);
Unsafe.Add(ref destinationRowRef, x - offsetX) = quantizer.GetQuantizedColor(sourcePixel, out TPixel transformed);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
}
}
@ -124,16 +124,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TPixel : unmanaged, IPixel<TPixel>
{
float scale = processor.DitherScale;
ReadOnlySpan<TPixel> palette = processor.Palette.Span;
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
for (int x = bounds.Left; x < bounds.Right; x++)
{
TPixel sourcePixel = row[x];
TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel, palette);
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel transformed = Unsafe.AsRef(processor).GetPaletteColor(sourcePixel);
this.Dither(source, bounds, sourcePixel, transformed, x, y, scale);
row[x] = transformed;
sourcePixel = transformed;
}
}
}

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

@ -32,8 +32,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// Returns the color from the dithering palette corresponding to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <returns>The <typeparamref name="TPixel"/> match.</returns>
TPixel GetPaletteColor(TPixel color, ReadOnlySpan<TPixel> palette);
TPixel GetPaletteColor(TPixel color);
}
}

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

@ -3,6 +3,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -224,20 +225,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<byte> destinationRow = this.destination.GetPixelRowSpan(y - offsetY);
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
ref byte destinationRowRef = ref MemoryMarshal.GetReference(this.destination.GetPixelRowSpan(y - offsetY));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
TPixel dithered = this.dither.Dither(sourceRow[x], x, y, this.bitDepth, scale);
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, paletteSpan, out TPixel _);
TPixel dithered = this.dither.Dither(Unsafe.Add(ref sourceRowRef, x), x, y, this.bitDepth, scale);
Unsafe.Add(ref destinationRowRef, x - offsetX) = Unsafe.AsRef(this.quantizer).GetQuantizedColor(dithered, out TPixel _);
}
}
}
@ -272,16 +272,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.processor.Palette.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(this.source.GetPixelRowSpan(y));
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
ref TPixel sourcePixel = ref row[x];
ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x);
TPixel dithered = this.dither.Dither(sourcePixel, x, y, this.bitDepth, this.scale);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered, paletteSpan);
sourcePixel = Unsafe.AsRef(this.processor).GetPaletteColor(dithered);
}
}
}

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

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -39,6 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.ditherProcessor = new DitherProcessor(
this.Configuration,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
this.paletteMemory.Memory,
definition.DitherScale);
}
@ -71,7 +73,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <summary>
/// Used to allow inlining of calls to
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel, ReadOnlySpan{TPixel})"/>.
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary>
private readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
{
@ -80,11 +82,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public DitherProcessor(
Configuration configuration,
Rectangle bounds,
ReadOnlyMemory<TPixel> palette,
float ditherScale)
{
this.Configuration = configuration;
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette, palette.Span.Length);
this.Palette = palette;
this.DitherScale = ditherScale;
}
@ -96,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
public float DitherScale { get; }
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel GetPaletteColor(TPixel color, ReadOnlySpan<TPixel> palette)
public TPixel GetPaletteColor(TPixel color)
{
this.pixelMap.GetClosestColor(color, out TPixel match);
return match;

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

@ -2,88 +2,86 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <summary>
/// Gets the closest color to the supplied color based upon the Euclidean distance.
/// TODO: Expose this somehow.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel> : IPixelMap<TPixel>, IEquatable<EuclideanPixelMap<TPixel>>
internal readonly struct EuclideanPixelMap<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int length;
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="palette">The color palette to map from.</param>
/// <param name="length">The length of the color palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int length)
{
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
this.Palette = palette;
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
this.vectorCache = new Vector4[paletteSpan.Length];
this.distanceCache = new ConcurrentDictionary<TPixel, int>();
this.palette = palette;
this.length = length;
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span.Slice(0, this.length);
this.vectorCache = new Vector4[length];
// Use the same rules across all target frameworks.
this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
PixelOperations<TPixel>.Instance.ToVector4(configuration, paletteSpan, this.vectorCache);
}
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is EuclideanPixelMap<TPixel> map && this.Equals(map);
/// <inheritdoc/>
public bool Equals(EuclideanPixelMap<TPixel> other)
=> this.Palette.Equals(other.Palette);
/// <summary>
/// Returns the palette span.
/// </summary>
/// <returns>The <seealso cref="ReadOnlySpan{TPixel}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlySpan<TPixel> GetPaletteSpan() => this.palette.Span.Slice(0, this.length);
/// <inheritdoc/>
/// <summary>
/// Returns the closest color in the palette and the index of that pixel.
/// The palette contents must match the one used in the constructor.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="int"/> index.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetClosestColor(TPixel color, out TPixel match)
{
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.GetPaletteSpan());
// Check if the color is in the lookup table
if (!this.distanceCache.TryGetValue(color, out int index))
{
return this.GetClosestColorSlow(color, paletteSpan, out match);
return this.GetClosestColorSlow(color, ref paletteRef, out match);
}
match = paletteSpan[index];
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
public override int GetHashCode() => this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
Vector4 vector = color.ToScaledVector4();
ref TPixel paletteRef = ref MemoryMarshal.GetReference(palette);
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.length; i++)
{
Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate);
@ -108,5 +106,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
match = Unsafe.Add(ref paletteRef, index);
return index;
}
}
}
}

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

@ -3,6 +3,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -40,20 +41,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<TPixel> palette = quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
var destination = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
if (quantizer.Options.Dither is null)
{
SecondPass(ref quantizer, source, quantizedFrame, interest);
SecondPass(ref quantizer, source, destination, interest);
}
else
{
// We clone the image as we don't want to alter the original via error diffusion based dithering.
using ImageFrame<TPixel> clone = source.Clone();
SecondPass(ref quantizer, clone, quantizedFrame, interest);
SecondPass(ref quantizer, clone, destination, interest);
}
return quantizedFrame;
return destination;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -106,7 +107,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], paletteSpan, out TPixel _);
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
}
}
}

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

@ -45,10 +45,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Returns the index and color from the quantized palette corresponding to the given color.
/// </summary>
/// <param name="color">The color to match.</param>
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns>
byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match);
byte GetQuantizedColor(TPixel color, out TPixel match);
// TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);

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

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

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

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -82,29 +83,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
Span<TPixel> paletteSpan = this.palette.GetSpan();
this.octree.Palletize(paletteSpan, this.colors);
int paletteIndex = 0;
this.octree.Palletize(paletteSpan, this.colors, ref paletteIndex);
// TODO: Cannot make method readonly due to this line.
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory);
// Length of reduced palette + transparency.
paletteSpan = paletteSpan.Slice(0, Math.Min(paletteIndex + 2, QuantizerConstants.MaxColors));
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory, paletteSpan.Length);
return paletteSpan;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{
// Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent
// pixel and a black one.
if (!this.isDithering && !color.Equals(default))
if (this.isDithering || color.Equals(default))
{
var index = (byte)this.octree.GetPaletteIndex(color);
match = palette[index];
return index;
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
return (byte)this.pixelMap.GetClosestColor(color, out match);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan());
var index = (byte)this.octree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <inheritdoc/>
@ -223,15 +227,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param>
/// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void Palletize(Span<TPixel> palette, int colorCount)
public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{
while (this.Leaves > colorCount - 1)
{
this.Reduce();
}
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex);
}

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

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -17,54 +18,26 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<TPixel> paletteOwner;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
/// <param name="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan<Color> colors)
public PaletteFrameQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
int maxLength = Math.Min(colors.Length, options.MaxColors);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(maxLength);
Color.ToPixel(configuration, colors, this.paletteOwner.GetSpan());
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, this.paletteOwner.Memory);
this.isDisposed = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="palette">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan<TPixel> palette)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration;
this.Options = options;
int maxLength = Math.Min(palette.Length, options.MaxColors);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(maxLength);
palette.CopyTo(this.paletteOwner.GetSpan());
this.pixelMap = new EuclideanPixelMap<TPixel>(configuration, this.paletteOwner.Memory);
this.isDisposed = false;
this.pixelMap = pixelMap;
}
/// <inheritdoc/>
@ -81,24 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.paletteOwner.GetSpan();
=> this.pixelMap.GetPaletteSpan();
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
=> (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
}
}
}

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

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

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

@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<byte> quantizedPixelSpan = this.quantized.GetPixelSpan();
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette;
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
int width = this.bounds.Width;

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

@ -53,10 +53,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public ReadOnlySpan<TPixel> Palette
public ReadOnlyMemory<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get { return this.palette.GetSpan(); }
get { return this.palette.Memory; }
}
/// <summary>

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

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -131,13 +132,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
// TODO: Cannot make methods readonly due to this line.
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory);
paletteSpan = paletteSpan.Slice(0, this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, this.palette.Memory, paletteSpan.Length);
return paletteSpan;
}
/// <inheritdoc/>
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, out TPixel match)
{
if (this.isDithering)
{
@ -154,7 +155,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<byte> tagSpan = this.tag.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
match = palette[index];
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.GetPaletteSpan());
match = Unsafe.Add(ref paletteRef, index);
return index;
}

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

@ -5,6 +5,7 @@ using System.IO;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@ -25,6 +26,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
{ TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
[Theory]
[WithFile(TestImages.Bmp.Car, PixelTypes.Rgba32)]
public void EncodeAllocationCheck<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
GifEncoder encoder = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (Image<TPixel> image = provider.GetImage())
{
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder);
}
}
[Theory]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]
[WithTestPatternImages(100, 100, TestPixelTypes, false)]

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

@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Tests
// Transparent pixels are much more likely to be found at the end of a palette
int index = -1;
Rgba32 trans = default;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
for (int i = paletteSpan.Length - 1; i >= 0; i--)
{
paletteSpan[i].ToRgba32(ref trans);

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

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(Color.Black, (Color)result.Palette[0]);
Assert.Equal(Color.Black, (Color)result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(1, result.Palette.Length);
Assert.Equal(1, result.GetPixelSpan().Length);
Assert.Equal(default, result.Palette[0]);
Assert.Equal(default, result.Palette.Span[0]);
Assert.Equal(0, result.GetPixelSpan()[0]);
}
@ -92,7 +92,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
var actualImage = new Image<Rgba32>(1, 256);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette;
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{
@ -157,7 +157,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization
Assert.Equal(4 * 8, result.Palette.Length);
Assert.Equal(256, result.GetPixelSpan().Length);
ReadOnlySpan<Rgba32> paletteSpan = result.Palette;
ReadOnlySpan<Rgba32> paletteSpan = result.Palette.Span;
int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{

Loading…
Cancel
Save