Browse Source

Use pooling for pixelmap cache.

pull/1654/head
James Jackson-South 5 years ago
parent
commit
8c202d8fc2
  1. 15
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 8
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs
  3. 5
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  4. 34
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  5. 39
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs
  6. 2
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  7. 12
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  8. 1
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  9. 13
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  10. 3
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

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

@ -150,8 +150,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// 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;
Unsafe.SkipInit(out EuclideanPixelMap<TPixel> pixelMap);
bool pixelMapHasValue = false;
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
@ -166,17 +166,22 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
if (!pixelMapSet)
if (!pixelMapHasValue)
{
pixelMapSet = true;
pixelMapHasValue = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette);
}
using var paletteFrameQuantizer = new PaletteQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using var paletteFrameQuantizer = new PaletteQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap, true);
using IndexedImageFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
}
if (pixelMapHasValue)
{
pixelMap.Dispose();
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, Stream stream)

8
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Memory
int bufferSizeInBytes = length * itemSizeBytes;
if (bufferSizeInBytes < 0 || bufferSizeInBytes > this.BufferCapacityInBytes)
{
ThrowInvalidAllocationException<T>(length);
ThrowInvalidAllocationException<T>(length, this.BufferCapacityInBytes);
}
ArrayPool<byte> pool = this.GetArrayPool(bufferSizeInBytes);
@ -171,9 +171,9 @@ namespace SixLabors.ImageSharp.Memory
}
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowInvalidAllocationException<T>(int length) =>
private static void ThrowInvalidAllocationException<T>(int length, int max) =>
throw new InvalidMemoryOperationException(
$"Requested allocation: {length} elements of {typeof(T).Name} is over the capacity of the MemoryAllocator.");
$"Requested allocation: '{length}' elements of '{typeof(T).Name}' is over the capacity in bytes '{max}' of the MemoryAllocator.");
private ArrayPool<byte> GetArrayPool(int bufferSizeInBytes)
{

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

@ -62,6 +62,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
if (disposing)
{
this.paletteOwner.Dispose();
this.ditherProcessor.Dispose();
}
this.paletteOwner = null;
@ -73,7 +74,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary>
/// <remarks>Internal for AOT</remarks>
internal readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
internal readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>, IDisposable
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
@ -101,6 +102,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.pixelMap.GetClosestColor(color, out TPixel match);
return match;
}
public void Dispose() => this.pixelMap.Dispose();
}
}
}

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

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
@ -16,7 +18,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// This class is not threadsafe and should not be accessed in parallel.
/// Doing so will result in non-idempotent results.
/// </para>
internal readonly struct EuclideanPixelMap<TPixel>
internal readonly struct EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Rgba32[] rgbaPalette;
@ -32,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
{
this.Palette = palette;
this.rgbaPalette = new Rgba32[palette.Length];
this.cache = ColorDistanceCache.Create();
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
}
@ -118,6 +120,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}
public void Dispose() => this.cache.Dispose();
/// <summary>
/// A cache for storing color distance matching results.
/// </summary>
@ -129,7 +133,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Entry count is currently limited to 610929 entries (1221858 bytes ~1.17MB).
/// </para>
/// </remarks>
private struct ColorDistanceCache
private unsafe struct ColorDistanceCache : IDisposable
{
private const int IndexBits = 5;
private const int IndexAlphaBits = 4;
@ -138,16 +142,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private const int RgbShift = 8 - IndexBits;
private const int AlphaShift = 8 - IndexAlphaBits;
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
private short[] table;
private readonly IMemoryOwner<short> tableOwner;
private MemoryHandle tableHandle;
private readonly short* table;
public static ColorDistanceCache Create()
public ColorDistanceCache(MemoryAllocator memoryAllocator)
{
ColorDistanceCache result = default;
short[] entries = new short[TableLength];
entries.AsSpan().Fill(-1);
result.table = entries;
return result;
this.tableOwner = memoryAllocator.Allocate<short>(TableLength);
this.tableOwner.GetSpan().Fill(-1);
this.tableHandle = this.tableOwner.Memory.Pin();
this.table = (short*)this.tableHandle.Pointer;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -173,6 +177,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return match > -1;
}
public void Clear() => this.tableOwner.GetSpan().Fill(-1);
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetPaletteIndex(int r, int g, int b, int a)
=> (r << ((IndexBits * 2) + IndexAlphaBits))
@ -183,6 +189,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
+ (g << IndexBits)
+ ((r + g + b) << IndexAlphaBits)
+ r + g + b + a;
public void Dispose()
{
this.tableHandle.Dispose();
this.tableOwner?.Dispose();
}
}
}
}

39
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs

@ -25,6 +25,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap;
private bool pixelMapHasValue;
private readonly bool isDithering;
private bool isDisposed;
@ -46,8 +47,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8);
this.octree = new Octree(this.bitDepth);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.pixelMap = default;
this.pixelMapHasValue = false;
this.palette = default;
this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
}
@ -69,26 +71,27 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion)
{
Rectangle bounds = pixelRegion.Rectangle;
Buffer2D<TPixel> source = pixelRegion.Buffer;
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
// Loop through each row
for (int y = bounds.Top; y < bounds.Bottom; y++)
using (IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width))
{
Span<TPixel> row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width);
PixelOperations<TPixel>.Instance.ToRgba32(this.Configuration, row, bufferSpan);
Span<Rgba32> bufferSpan = buffer.GetSpan();
for (int x = 0; x < bufferSpan.Length; x++)
// Loop through each row
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Rgba32 rgba = bufferSpan[x];
Span<TPixel> row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width);
PixelOperations<TPixel>.Instance.ToRgba32(this.Configuration, row, bufferSpan);
for (int x = 0; x < bufferSpan.Length; x++)
{
Rgba32 rgba = bufferSpan[x];
// Add the color to the Octree
this.octree.AddColor(rgba);
// Add the color to the Octree
this.octree.AddColor(rgba);
}
}
}
@ -108,7 +111,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.octree.Palletize(paletteSpan, max, ref paletteIndex);
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length);
// When called by QuantizerUtilities.BuildPalette this prevents
// mutiple instances of the map being created but not disposed.
if (this.pixelMapHasValue)
{
this.pixelMap.Dispose();
}
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.pixelMapHasValue = true;
this.palette = result;
}
@ -143,6 +155,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
this.pixelMap.Dispose();
}
}

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

@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
var pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
return new PaletteQuantizer<TPixel>(configuration, options, pixelMap);
return new PaletteQuantizer<TPixel>(configuration, options, pixelMap, false);
}
}
}

12
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -17,6 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly bool leaveMap;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
@ -24,11 +25,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <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="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
/// <param name="leaveMap">
/// <see langword="true"/> to leave the pixel map undisposed after disposing the <see cref="PaletteQuantizer{TPixel}"/> object; otherwise, <see langword="false"/>.
/// </param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
EuclideanPixelMap<TPixel> pixelMap,
bool leaveMap)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
@ -36,6 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.pixelMap = pixelMap;
this.leaveMap = leaveMap;
}
/// <inheritdoc/>
@ -66,6 +72,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public void Dispose()
{
if (!this.leaveMap)
{
this.pixelMap.Dispose();
}
}
}
}

1
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -3,7 +3,6 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;

13
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs

@ -72,6 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private int maxColors;
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap;
private bool pixelMapHasValue;
private readonly bool isDithering;
private bool isDisposed;
@ -93,10 +94,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false;
this.pixelMap = default;
this.pixelMapHasValue = false;
this.palette = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null);
}
@ -145,7 +147,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length);
if (this.isDithering)
{
// When called by QuantizerUtilities.BuildPalette this prevents
// mutiple instances of the map being created but not disposed.
if (this.pixelMapHasValue)
{
this.pixelMap.Dispose();
}
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.pixelMapHasValue = true;
}
this.palette = result;
@ -191,6 +201,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.momentsOwner = null;
this.tagsOwner = null;
this.paletteOwner = null;
this.pixelMap.Dispose();
}
}

3
tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -155,7 +156,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering
appendPixelTypeToFileName: false);
}
[Theory]
[Theory(Skip = "Unable to assign capacity smaller than the image.")]
[WithFile(TestImages.Png.Bike, PixelTypes.Rgba32, nameof(OrderedDither.Ordered3x3))]
[WithFile(TestImages.Png.Bike, PixelTypes.Rgba32, nameof(ErrorDither.FloydSteinberg))]
public void CommonDitherers_WorkWithDiscoBuffers<TPixel>(

Loading…
Cancel
Save