Browse Source

Dump progress so far

pull/1138/head
James Jackson-South 6 years ago
parent
commit
d113c787fd
  1. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  2. 3
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  3. 54
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 2
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  5. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 2
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  7. 6
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  8. 33
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  9. 18
      src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerExtensions.cs
  10. 6
      src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs
  11. 61
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  12. 56
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  13. 16
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  14. 2
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  15. 18
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  16. 56
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  17. 14
      tests/ImageSharp.Benchmarks/Codecs/EncodeGif.cs
  18. 2
      tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs
  19. 8
      tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs

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

@ -339,7 +339,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
using IFrameQuantizer<TPixel> quantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
using QuantizedFrame<TPixel> quantized = quantizer.QuantizeFrame(image, image.Bounds());
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette.Span;
ReadOnlySpan<TPixel> quantizedColors = quantized.Palette;
var color = default(Rgba32);
// TODO: Use bulk conversion here for better perf

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

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

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

@ -119,7 +119,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
// Clean up.
quantized?.Dispose();
quantized.Dispose();
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
@ -142,11 +142,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
this.WriteImageData(paletteQuantized, stream);
}
using var paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette);
using QuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
}
}
@ -156,8 +154,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames)
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
@ -173,17 +172,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
else
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
using IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration);
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
@ -193,7 +188,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
@ -219,7 +214,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.Span, rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette, rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{
@ -391,7 +386,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="extension">The extension to write to the stream.</param>
/// <param name="stream">The stream to write to.</param>
public void WriteExtension(IGifExtension extension, Stream stream)
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
@ -444,15 +440,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
int pixelCount = image.Palette.Length;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
{
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);
}
using IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength);
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);
}
/// <summary>
@ -464,10 +458,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{
encoder.Encode(image.GetPixelSpan(), stream);
}
using var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth);
encoder.Encode(image.GetPixelSpan(), stream);
}
}
}

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

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

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.Span;
ReadOnlySpan<TPixel> palette = quantized.Palette;
int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3;
bool anyAlpha = false;

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

@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TFrameQuantizer : struct, IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
ReadOnlySpan<TPixel> paletteSpan = destination.Palette.Span;
ReadOnlySpan<TPixel> paletteSpan = destination.Palette;
int offsetY = bounds.Top;
int offsetX = bounds.Left;
float scale = quantizer.Options.DitherScale;

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

@ -203,7 +203,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
private readonly int bitDepth;
[MethodImpl(InliningOptions.ShortMethod)]
@ -219,14 +218,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.source = source;
this.destination = destination;
this.bounds = bounds;
this.palette = destination.Palette;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Span.Length);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(destination.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
float scale = this.quantizer.Options.DitherScale;

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

@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
/// <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>
/// <typeparam name="TPixel">The pixel format.</typeparam>
@ -62,18 +62,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<TPixel> paletteSpan = this.Palette.Span;
// Check if the color is in the lookup table
if (this.distanceCache.TryGetValue(color, out int index))
if (!this.distanceCache.TryGetValue(color, out int index))
{
match = paletteSpan[index];
return index;
return this.GetClosestColorSlow(color, paletteSpan, out match);
}
return this.GetClosestColorSlow(color, paletteSpan, out match);
match = paletteSpan[index];
return index;
}
/// <inheritdoc/>
public override int GetHashCode()
=> this.vectorCache.GetHashCode();
public override int GetHashCode() => this.vectorCache.GetHashCode();
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
@ -88,17 +87,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Vector4 candidate = this.vectorCache[i];
float distance = Vector4.DistanceSquared(vector, candidate);
if (!(distance < leastDistance))
{
continue;
}
// Less than... assign.
if (distance < leastDistance)
index = i;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance == 0)
{
index = i;
leastDistance = distance;
// And if it's an exact match, exit the loop
if (distance == 0)
{
break;
}
break;
}
}

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

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <typeparam name="TFrameQuantizer">The type of frame quantizer.</typeparam>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="quantizer">The frame </param>
/// <param name="quantizer">The frame quantizer.</param>
/// <param name="source">The source image frame to quantize.</param>
/// <param name="bounds">The bounds within the frame to quantize.</param>
/// <returns>
@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
var interest = Rectangle.Intersect(source.Bounds(), bounds);
// Collect the palette. Required before the second pass runs.
ReadOnlyMemory<TPixel> palette = quantizer.BuildPalette(source, interest);
ReadOnlySpan<TPixel> palette = quantizer.BuildPalette(source, interest);
MemoryAllocator memoryAllocator = quantizer.Configuration.MemoryAllocator;
var quantizedFrame = new QuantizedFrame<TPixel>(memoryAllocator, interest.Width, interest.Height, palette);
@ -49,10 +49,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
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);
}
using ImageFrame<TPixel> clone = source.Clone();
SecondPass(ref quantizer, clone, quantizedFrame, interest);
}
return quantizedFrame;
@ -71,7 +69,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null)
{
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(quantizer, source, destination, bounds);
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(ref quantizer, source, destination, bounds);
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
@ -91,11 +89,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private readonly ImageFrame<TPixel> source;
private readonly QuantizedFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly ReadOnlyMemory<TPixel> palette;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
in TFrameQuantizer quantizer,
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
QuantizedFrame<TPixel> destination,
Rectangle bounds)
@ -104,13 +101,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.source = source;
this.destination = destination;
this.bounds = bounds;
this.palette = destination.Palette;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.palette.Span;
ReadOnlySpan<TPixel> paletteSpan = this.destination.Palette;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;

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

@ -38,8 +38,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="bounds">The region of interest bounds.</param>
/// <returns>The <see cref="ReadOnlyMemory{TPixel}"/> palette.</returns>
ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
/// <returns>The <see cref="ReadOnlySpan{TPixel}"/> palette.</returns>
ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds);
/// <summary>
/// Returns the index and color from the quantized palette corresponding to the given color.
@ -48,7 +48,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The output color palette.</param>
/// <param name="match">The matched color.</param>
/// <returns>The <see cref="byte"/> index.</returns>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match);
byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match);
// TODO: Enable bulk operations.
// void GetQuantizedColors(ReadOnlySpan<TPixel> colors, ReadOnlySpan<TPixel> palette, Span<byte> indices, Span<TPixel> matches);

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

@ -3,7 +3,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
@ -22,8 +21,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
private readonly int colors;
private readonly Octree octree;
private IMemoryOwner<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> struct.
@ -41,8 +42,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.colors = this.Options.MaxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
this.palette = configuration.MemoryAllocator.Allocate<TPixel>(this.colors, AllocationOptions.Clean);
this.pixelMap = default;
this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
}
/// <inheritdoc/>
@ -53,12 +56,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public readonly QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
public ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
@ -78,15 +81,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
TPixel[] palette = this.octree.Palletize(this.colors);
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
Span<TPixel> paletteSpan = this.palette.GetSpan();
this.octree.Palletize(paletteSpan, this.colors);
return palette;
// TODO: Cannot make method readonly due to this line.
this.pixelMap = new EuclideanPixelMap<TPixel>(this.palette.Memory);
return paletteSpan;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
{
// Octree only maps the RGB component of a color
// so cannot tell the difference between a fully transparent
@ -104,6 +110,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.palette.Dispose();
this.palette = null;
}
}
/// <summary>
@ -116,14 +128,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private static readonly byte[] Mask = new byte[]
{
0b10000000,
0b1000000,
0b100000,
0b10000,
0b1000,
0b100,
0b10,
0b1
0b10000000, 0b1000000, 0b100000, 0b10000, 0b1000, 0b100, 0b10, 0b1
};
/// <summary>
@ -216,26 +221,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Convert the nodes in the Octree to a palette with a maximum of colorCount colors
/// </summary>
/// <param name="palette">The palette to fill.</param>
/// <param name="colorCount">The maximum number of colors</param>
/// <returns>
/// An <see cref="List{TPixel}"/> with the palletized colors
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
public TPixel[] Palletize(int colorCount)
public void Palletize(Span<TPixel> palette, int colorCount)
{
while (this.Leaves > colorCount - 1)
{
this.Reduce();
}
// Now palletize the nodes
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex);
// And return the palette
return palette;
}
/// <summary>
@ -437,12 +434,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="palette">The palette</param>
/// <param name="index">The current palette index</param>
[MethodImpl(InliningOptions.ColdPath)]
public void ConstructPalette(TPixel[] palette, ref int index)
public void ConstructPalette(Span<TPixel> palette, ref int index)
{
if (this.leaf)
{
// Set the color of the palette entry
var vector = Vector3.Clamp(new Vector3(this.red, this.green, this.blue) / this.pixelCount, Vector3.Zero, new Vector3(255));
var vector = Vector3.Clamp(
new Vector3(this.red, this.green, this.blue) / this.pixelCount,
Vector3.Zero,
new Vector3(255));
TPixel pixel = default;
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel;
@ -516,8 +517,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
int shift = 7 - level;
byte mask = Mask[level];
return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
}
/// <summary>

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

@ -2,7 +2,9 @@
// 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
@ -15,8 +17,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
internal struct PaletteFrameQuantizer<TPixel> : IFrameQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly ReadOnlyMemory<TPixel> palette;
private IMemoryOwner<TPixel> paletteOwner;
private readonly EuclideanPixelMap<TPixel> pixelMap;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteFrameQuantizer{TPixel}"/> struct.
@ -25,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="colors">A <see cref="ReadOnlyMemory{TPixel}"/> containing all colors in the palette.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory<TPixel> colors)
public PaletteFrameQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlySpan<Color> colors)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
@ -33,8 +36,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.palette = colors;
this.pixelMap = new EuclideanPixelMap<TPixel>(colors);
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>(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>(this.paletteOwner.Memory);
this.isDisposed = false;
}
/// <inheritdoc/>
@ -45,22 +75,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public readonly QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.palette;
public readonly ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
=> this.paletteOwner.GetSpan();
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, 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;
}
}
}

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

@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class PaletteQuantizer : IQuantizer
{
private readonly ReadOnlyMemory<Color> palette;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
@ -30,15 +32,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette));
Guard.NotNull(options, nameof(options));
this.Palette = palette;
this.palette = palette;
this.Options = options;
}
/// <summary>
/// Gets the color palette.
/// </summary>
public ReadOnlyMemory<Color> Palette { get; }
/// <inheritdoc />
public QuantizerOptions Options { get; }
@ -52,12 +49,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(options, nameof(options));
int length = Math.Min(this.Palette.Span.Length, options.MaxColors);
var palette = new TPixel[length];
Color.ToPixel(configuration, this.Palette.Span, palette.AsSpan());
return new PaletteFrameQuantizer<TPixel>(configuration, options, palette);
return new PaletteFrameQuantizer<TPixel>(configuration, options, this.palette.Span);
}
}
}

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.Span;
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
int width = this.bounds.Width;

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

@ -16,6 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public sealed class QuantizedFrame<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private IMemoryOwner<TPixel> palette;
private IMemoryOwner<byte> pixels;
private bool isDisposed;
@ -26,15 +27,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <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)
internal QuantizedFrame(MemoryAllocator memoryAllocator, int width, int height, ReadOnlySpan<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);
this.palette = memoryAllocator.Allocate<TPixel>(palette.Length);
palette.CopyTo(this.palette.GetSpan());
}
/// <summary>
@ -50,7 +53,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Gets the color palette of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
public ReadOnlyMemory<TPixel> Palette { get; private set; }
public ReadOnlySpan<TPixel> Palette
{
[MethodImpl(InliningOptions.ShortMethod)]
get { return this.palette.GetSpan(); }
}
/// <summary>
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
@ -72,15 +79,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
if (!this.isDisposed)
{
return;
}
this.isDisposed = true;
this.pixels?.Dispose();
this.palette?.Dispose();
this.pixels = null;
this.Palette = null;
this.palette = null;
}
}
}

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

@ -65,30 +65,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// Color moments.
/// </summary>
private IMemoryOwner<Moment> moments;
/// <summary>
/// Color space tag.
/// </summary>
private IMemoryOwner<byte> tag;
/// <summary>
/// Maximum allowed color depth
/// </summary>
private IMemoryOwner<TPixel> palette;
private int colors;
/// <summary>
/// The color cube representing the image palette
/// </summary>
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap;
private readonly bool isDithering;
private bool isDisposed;
/// <summary>
@ -104,10 +87,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.colors = this.Options.MaxColors;
this.memoryAllocator = this.Configuration.MemoryAllocator;
this.moments = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tag = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.colors = this.Options.MaxColors;
this.palette = this.memoryAllocator.Allocate<TPixel>(this.colors, AllocationOptions.Clean);
this.colorCube = new Box[this.colors];
this.isDisposed = false;
this.pixelMap = default;
@ -122,19 +106,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref this, source, bounds);
public readonly QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> source, Rectangle bounds)
=> FrameQuantizerExtensions.QuantizeFrame(ref Unsafe.AsRef(this), source, bounds);
/// <inheritdoc/>
public ReadOnlyMemory<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
public ReadOnlySpan<TPixel> BuildPalette(ImageFrame<TPixel> source, Rectangle bounds)
{
this.Build3DHistogram(source, bounds);
this.Get3DMoments(this.memoryAllocator);
this.BuildCube();
var palette = new TPixel[this.colors];
ReadOnlySpan<Moment> momentsSpan = this.moments.GetSpan();
Span<TPixel> paletteSpan = this.palette.GetSpan();
for (int k = 0; k < this.colors; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
@ -143,17 +126,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (moment.Weight > 0)
{
ref TPixel color = ref palette[k];
ref TPixel color = ref paletteSpan[k];
color.FromScaledVector4(moment.Normalize());
}
}
this.pixelMap = new EuclideanPixelMap<TPixel>(palette);
return palette;
// TODO: Cannot make methods readonly due to this line.
this.pixelMap = new EuclideanPixelMap<TPixel>(this.palette.Memory);
return paletteSpan;
}
/// <inheritdoc/>
public byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
public readonly byte GetQuantizedColor(TPixel color, ReadOnlySpan<TPixel> palette, out TPixel match)
{
if (!this.isDithering)
{
@ -177,16 +161,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public void Dispose()
{
if (this.isDisposed)
if (!this.isDisposed)
{
return;
this.isDisposed = true;
this.moments?.Dispose();
this.tag?.Dispose();
this.palette?.Dispose();
this.moments = null;
this.tag = null;
this.palette = null;
}
this.isDisposed = true;
this.moments?.Dispose();
this.tag?.Dispose();
this.moments = null;
this.tag = null;
}
/// <summary>

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

@ -21,6 +21,12 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
private SDImage bmpDrawing;
private Image<Rgba32> bmpCore;
// Try to get as close to System.Drawing's output as possible
private readonly GifEncoder encoder = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
[GlobalSetup]
public void ReadImages()
{
@ -53,15 +59,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
[Benchmark(Description = "ImageSharp Gif")]
public void GifCore()
{
// Try to get as close to System.Drawing's output as possible
var options = new GifEncoder
{
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4 })
};
using (var memoryStream = new MemoryStream())
{
this.bmpCore.SaveAsGif(memoryStream, options);
this.bmpCore.SaveAsGif(memoryStream, this.encoder);
}
}
}

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.Span;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette;
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.Span[0]);
Assert.Equal(Color.Black, (Color)result.Palette[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.Span[0]);
Assert.Equal(default, result.Palette[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.Span;
ReadOnlySpan<Rgba32> paletteSpan = result.Palette;
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.Span;
ReadOnlySpan<Rgba32> paletteSpan = result.Palette;
int paletteCount = result.Palette.Length - 1;
for (int y = 0; y < actualImage.Height; y++)
{

Loading…
Cancel
Save