mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
215 changed files with 3331 additions and 1449 deletions
@ -0,0 +1,38 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
// <auto-generated />
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a safe, fixed sized buffer of 4 elements.
|
||||
|
/// </summary>
|
||||
|
[InlineArray(4)] |
||||
|
internal struct InlineArray4<T> |
||||
|
{ |
||||
|
private T t; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a safe, fixed sized buffer of 8 elements.
|
||||
|
/// </summary>
|
||||
|
[InlineArray(8)] |
||||
|
internal struct InlineArray8<T> |
||||
|
{ |
||||
|
private T t; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a safe, fixed sized buffer of 16 elements.
|
||||
|
/// </summary>
|
||||
|
[InlineArray(16)] |
||||
|
internal struct InlineArray16<T> |
||||
|
{ |
||||
|
private T t; |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,38 @@ |
|||||
|
<#@ template debug="false" hostspecific="false" language="C#" #> |
||||
|
<#@ assembly name="System.Core" #> |
||||
|
<#@ import namespace="System.Linq" #> |
||||
|
<#@ import namespace="System.Text" #> |
||||
|
<#@ import namespace="System.Collections.Generic" #> |
||||
|
// Copyright (c) Six Labors. |
||||
|
// Licensed under the Six Labors Split License. |
||||
|
|
||||
|
// <auto-generated /> |
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp; |
||||
|
|
||||
|
<#GenerateInlineArrays();#> |
||||
|
|
||||
|
<#+ |
||||
|
private static int[] Lengths = new int[] {4, 8, 16 }; |
||||
|
|
||||
|
void GenerateInlineArrays() |
||||
|
{ |
||||
|
foreach (int length in Lengths) |
||||
|
{ |
||||
|
#> |
||||
|
/// <summary> |
||||
|
/// Represents a safe, fixed sized buffer of <#=length#> elements. |
||||
|
/// </summary> |
||||
|
[InlineArray(<#=length#>)] |
||||
|
internal struct InlineArray<#=length#><T> |
||||
|
{ |
||||
|
private T t; |
||||
|
} |
||||
|
|
||||
|
<#+ |
||||
|
} |
||||
|
} |
||||
|
#> |
||||
@ -0,0 +1,28 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the precision level used when matching colors during quantization.
|
||||
|
/// </summary>
|
||||
|
public enum ColorMatchingMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Uses a coarse caching strategy optimized for performance at the expense of exact matches.
|
||||
|
/// This provides the fastest matching but may yield approximate results.
|
||||
|
/// </summary>
|
||||
|
Coarse, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables an exact color match cache for the first 512 unique colors encountered,
|
||||
|
/// falling back to coarse matching thereafter.
|
||||
|
/// </summary>
|
||||
|
Hybrid, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs exact color matching without any caching optimizations.
|
||||
|
/// This is the slowest but most accurate matching strategy.
|
||||
|
/// </summary>
|
||||
|
Exact |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the closest color to the supplied color based upon the Euclidean distance.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <typeparam name="TCache">The cache type.</typeparam>
|
||||
|
/// <para>
|
||||
|
/// This class is not thread safe and should not be accessed in parallel.
|
||||
|
/// Doing so will result in non-idempotent results.
|
||||
|
/// </para>
|
||||
|
internal sealed class EuclideanPixelMap<TPixel, TCache> : PixelMap<TPixel> |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
where TCache : struct, IColorIndexCache<TCache> |
||||
|
{ |
||||
|
private Rgba32[] rgbaPalette; |
||||
|
|
||||
|
// Do not make readonly. It's a mutable struct.
|
||||
|
#pragma warning disable IDE0044 // Add readonly modifier
|
||||
|
private TCache cache; |
||||
|
#pragma warning restore IDE0044 // Add readonly modifier
|
||||
|
|
||||
|
private readonly Configuration configuration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel, TCache}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="configuration">Specifies the settings and resources for the pixel map's operations.</param>
|
||||
|
/// <param name="palette">Defines the color palette used for pixel mapping.</param>
|
||||
|
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) |
||||
|
{ |
||||
|
this.configuration = configuration; |
||||
|
this.Palette = palette; |
||||
|
this.rgbaPalette = new Rgba32[palette.Length]; |
||||
|
this.cache = TCache.Create(configuration.MemoryAllocator); |
||||
|
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public override int GetClosestColor(TPixel color, out TPixel match) |
||||
|
{ |
||||
|
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); |
||||
|
Rgba32 rgba = color.ToRgba32(); |
||||
|
|
||||
|
if (this.cache.TryGetValue(rgba, out short index)) |
||||
|
{ |
||||
|
match = Unsafe.Add(ref paletteRef, (ushort)index); |
||||
|
return index; |
||||
|
} |
||||
|
|
||||
|
return this.GetClosestColorSlow(rgba, ref paletteRef, out match); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Clear(ReadOnlyMemory<TPixel> palette) |
||||
|
{ |
||||
|
this.Palette = palette; |
||||
|
this.rgbaPalette = new Rgba32[palette.Length]; |
||||
|
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); |
||||
|
this.cache.Clear(); |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ColdPath)] |
||||
|
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) |
||||
|
{ |
||||
|
// Loop through the palette and find the nearest match.
|
||||
|
int index = 0; |
||||
|
float leastDistance = float.MaxValue; |
||||
|
for (int i = 0; i < this.rgbaPalette.Length; i++) |
||||
|
{ |
||||
|
Rgba32 candidate = this.rgbaPalette[i]; |
||||
|
if (candidate.PackedValue == rgba.PackedValue) |
||||
|
{ |
||||
|
index = i; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
float distance = DistanceSquared(rgba, candidate); |
||||
|
if (distance == 0) |
||||
|
{ |
||||
|
index = i; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (distance < leastDistance) |
||||
|
{ |
||||
|
index = i; |
||||
|
leastDistance = distance; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Now I have the index, pop it into the cache for next time
|
||||
|
_ = this.cache.TryAdd(rgba, (short)index); |
||||
|
match = Unsafe.Add(ref paletteRef, (uint)index); |
||||
|
|
||||
|
return index; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns the Euclidean distance squared between two specified points.
|
||||
|
/// </summary>
|
||||
|
/// <param name="a">The first point.</param>
|
||||
|
/// <param name="b">The second point.</param>
|
||||
|
/// <returns>The distance squared.</returns>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
private static float DistanceSquared(Rgba32 a, Rgba32 b) |
||||
|
{ |
||||
|
float deltaR = a.R - b.R; |
||||
|
float deltaG = a.G - b.G; |
||||
|
float deltaB = a.B - b.B; |
||||
|
float deltaA = a.A - b.A; |
||||
|
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Dispose() => this.cache.Dispose(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a map of colors to indices.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal abstract class PixelMap<TPixel> : IDisposable |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the color palette of this <see cref="PixelMap{TPixel}"/>.
|
||||
|
/// </summary>
|
||||
|
public ReadOnlyMemory<TPixel> Palette { get; private protected set; } |
||||
|
|
||||
|
/// <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>
|
||||
|
public abstract int GetClosestColor(TPixel color, out TPixel match); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the map, resetting it to use the given palette.
|
||||
|
/// </summary>
|
||||
|
/// <param name="palette">The color palette to map from.</param>
|
||||
|
public abstract void Clear(ReadOnlyMemory<TPixel> palette); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public abstract void Dispose(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A factory for creating <see cref="PixelMap{TPixel}"/> instances.
|
||||
|
/// </summary>
|
||||
|
internal static class PixelMapFactory |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new <see cref="PixelMap{TPixel}"/> instance.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="configuration">The configuration.</param>
|
||||
|
/// <param name="palette">The color palette to map from.</param>
|
||||
|
/// <param name="colorMatchingMode">The color matching mode.</param>
|
||||
|
/// <returns>
|
||||
|
/// The <see cref="PixelMap{TPixel}"/>.
|
||||
|
/// </returns>
|
||||
|
public static PixelMap<TPixel> Create<TPixel>( |
||||
|
Configuration configuration, |
||||
|
ReadOnlyMemory<TPixel> palette, |
||||
|
ColorMatchingMode colorMatchingMode) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> => colorMatchingMode switch |
||||
|
{ |
||||
|
ColorMatchingMode.Hybrid => new EuclideanPixelMap<TPixel, HybridCache>(configuration, palette), |
||||
|
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, NullCache>(configuration, palette), |
||||
|
_ => new EuclideanPixelMap<TPixel, CoarseCache>(configuration, palette), |
||||
|
}; |
||||
|
} |
||||
@ -1,258 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
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; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the closest color to the supplied color based upon the Euclidean distance.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
||||
/// <para>
|
|
||||
/// This class is not thread safe and should not be accessed in parallel.
|
|
||||
/// Doing so will result in non-idempotent results.
|
|
||||
/// </para>
|
|
||||
internal sealed class EuclideanPixelMap<TPixel> : IDisposable |
|
||||
where TPixel : unmanaged, IPixel<TPixel> |
|
||||
{ |
|
||||
private Rgba32[] rgbaPalette; |
|
||||
private int transparentIndex; |
|
||||
private readonly TPixel transparentMatch; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
|
|
||||
/// </summary>
|
|
||||
private ColorDistanceCache cache; |
|
||||
private readonly Configuration configuration; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="configuration">The configuration.</param>
|
|
||||
/// <param name="palette">The color palette to map from.</param>
|
|
||||
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) |
|
||||
: this(configuration, palette, -1) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="configuration">The configuration.</param>
|
|
||||
/// <param name="palette">The color palette to map from.</param>
|
|
||||
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
|
|
||||
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int transparentIndex = -1) |
|
||||
{ |
|
||||
this.configuration = configuration; |
|
||||
this.Palette = palette; |
|
||||
this.rgbaPalette = new Rgba32[palette.Length]; |
|
||||
this.cache = new ColorDistanceCache(configuration.MemoryAllocator); |
|
||||
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); |
|
||||
|
|
||||
this.transparentIndex = transparentIndex; |
|
||||
this.transparentMatch = TPixel.FromRgba32(default); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
|
|
||||
/// The palette memory is owned by the palette source that created it.
|
|
||||
/// </summary>
|
|
||||
public ReadOnlyMemory<TPixel> Palette { get; private set; } |
|
||||
|
|
||||
/// <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) |
|
||||
{ |
|
||||
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); |
|
||||
Rgba32 rgba = color.ToRgba32(); |
|
||||
|
|
||||
// Check if the color is in the lookup table
|
|
||||
if (!this.cache.TryGetValue(rgba, out short index)) |
|
||||
{ |
|
||||
return this.GetClosestColorSlow(rgba, ref paletteRef, out match); |
|
||||
} |
|
||||
|
|
||||
match = Unsafe.Add(ref paletteRef, (ushort)index); |
|
||||
return index; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clears the map, resetting it to use the given palette.
|
|
||||
/// </summary>
|
|
||||
/// <param name="palette">The color palette to map from.</param>
|
|
||||
public void Clear(ReadOnlyMemory<TPixel> palette) |
|
||||
{ |
|
||||
this.Palette = palette; |
|
||||
this.rgbaPalette = new Rgba32[palette.Length]; |
|
||||
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); |
|
||||
this.transparentIndex = -1; |
|
||||
this.cache.Clear(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Allows setting the transparent index after construction.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">An explicit index at which to match transparent pixels.</param>
|
|
||||
public void SetTransparentIndex(int index) |
|
||||
{ |
|
||||
if (index != this.transparentIndex) |
|
||||
{ |
|
||||
this.cache.Clear(); |
|
||||
} |
|
||||
|
|
||||
this.transparentIndex = index; |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) |
|
||||
{ |
|
||||
// Loop through the palette and find the nearest match.
|
|
||||
int index = 0; |
|
||||
|
|
||||
if (this.transparentIndex >= 0 && rgba == default) |
|
||||
{ |
|
||||
// We have explicit instructions. No need to search.
|
|
||||
index = this.transparentIndex; |
|
||||
this.cache.Add(rgba, (byte)index); |
|
||||
match = this.transparentMatch; |
|
||||
return index; |
|
||||
} |
|
||||
|
|
||||
float leastDistance = float.MaxValue; |
|
||||
for (int i = 0; i < this.rgbaPalette.Length; i++) |
|
||||
{ |
|
||||
Rgba32 candidate = this.rgbaPalette[i]; |
|
||||
float distance = DistanceSquared(rgba, candidate); |
|
||||
|
|
||||
// If it's an exact match, exit the loop
|
|
||||
if (distance == 0) |
|
||||
{ |
|
||||
index = i; |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if (distance < leastDistance) |
|
||||
{ |
|
||||
// Less than... assign.
|
|
||||
index = i; |
|
||||
leastDistance = distance; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Now I have the index, pop it into the cache for next time
|
|
||||
this.cache.Add(rgba, (byte)index); |
|
||||
match = Unsafe.Add(ref paletteRef, (uint)index); |
|
||||
return index; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns the Euclidean distance squared between two specified points.
|
|
||||
/// </summary>
|
|
||||
/// <param name="a">The first point.</param>
|
|
||||
/// <param name="b">The second point.</param>
|
|
||||
/// <returns>The distance squared.</returns>
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
private static float DistanceSquared(Rgba32 a, Rgba32 b) |
|
||||
{ |
|
||||
float deltaR = a.R - b.R; |
|
||||
float deltaG = a.G - b.G; |
|
||||
float deltaB = a.B - b.B; |
|
||||
float deltaA = a.A - b.A; |
|
||||
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>
|
|
||||
/// <remarks>
|
|
||||
/// <para>
|
|
||||
/// The granularity of the cache has been determined based upon the current
|
|
||||
/// suite of test images and provides the lowest possible memory usage while
|
|
||||
/// providing enough match accuracy.
|
|
||||
/// Entry count is currently limited to 2335905 entries (4MB).
|
|
||||
/// </para>
|
|
||||
/// </remarks>
|
|
||||
private unsafe struct ColorDistanceCache : IDisposable |
|
||||
{ |
|
||||
private const int IndexRBits = 5; |
|
||||
private const int IndexGBits = 5; |
|
||||
private const int IndexBBits = 5; |
|
||||
private const int IndexABits = 6; |
|
||||
private const int IndexRCount = (1 << IndexRBits) + 1; |
|
||||
private const int IndexGCount = (1 << IndexGBits) + 1; |
|
||||
private const int IndexBCount = (1 << IndexBBits) + 1; |
|
||||
private const int IndexACount = (1 << IndexABits) + 1; |
|
||||
private const int RShift = 8 - IndexRBits; |
|
||||
private const int GShift = 8 - IndexGBits; |
|
||||
private const int BShift = 8 - IndexBBits; |
|
||||
private const int AShift = 8 - IndexABits; |
|
||||
private const int Entries = IndexRCount * IndexGCount * IndexBCount * IndexACount; |
|
||||
private MemoryHandle tableHandle; |
|
||||
private readonly IMemoryOwner<short> table; |
|
||||
private readonly short* tablePointer; |
|
||||
|
|
||||
public ColorDistanceCache(MemoryAllocator allocator) |
|
||||
{ |
|
||||
this.table = allocator.Allocate<short>(Entries); |
|
||||
this.table.GetSpan().Fill(-1); |
|
||||
this.tableHandle = this.table.Memory.Pin(); |
|
||||
this.tablePointer = (short*)this.tableHandle.Pointer; |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
public readonly void Add(Rgba32 rgba, byte index) |
|
||||
{ |
|
||||
int idx = GetPaletteIndex(rgba); |
|
||||
this.tablePointer[idx] = index; |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
public readonly bool TryGetValue(Rgba32 rgba, out short match) |
|
||||
{ |
|
||||
int idx = GetPaletteIndex(rgba); |
|
||||
match = this.tablePointer[idx]; |
|
||||
return match > -1; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clears the cache resetting each entry to empty.
|
|
||||
/// </summary>
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
public readonly void Clear() => this.table.GetSpan().Fill(-1); |
|
||||
|
|
||||
[MethodImpl(InliningOptions.ShortMethod)] |
|
||||
private static int GetPaletteIndex(Rgba32 rgba) |
|
||||
{ |
|
||||
int rIndex = rgba.R >> RShift; |
|
||||
int gIndex = rgba.G >> GShift; |
|
||||
int bIndex = rgba.B >> BShift; |
|
||||
int aIndex = rgba.A >> AShift; |
|
||||
|
|
||||
return (aIndex * (IndexRCount * IndexGCount * IndexBCount)) + |
|
||||
(rIndex * (IndexGCount * IndexBCount)) + |
|
||||
(gIndex * IndexBCount) + bIndex; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
if (this.table != null) |
|
||||
{ |
|
||||
this.tableHandle.Dispose(); |
|
||||
this.table.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,569 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a cache used for efficiently retrieving palette indices for colors.
|
||||
|
/// </summary>
|
||||
|
internal interface IColorIndexCache : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Adds a color to the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="color">The color to add.</param>
|
||||
|
/// <param name="value">The index of the color in the palette.</param>
|
||||
|
/// <returns>
|
||||
|
/// <see langword="true"/> if the color was added; otherwise, <see langword="false"/>.
|
||||
|
/// </returns>
|
||||
|
public bool TryAdd(Rgba32 color, short value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the index of the color in the palette.
|
||||
|
/// </summary>
|
||||
|
/// <param name="color">The color to get the index for.</param>
|
||||
|
/// <param name="value">The index of the color in the palette.</param>
|
||||
|
/// <returns>
|
||||
|
/// <see langword="true"/> if the color is in the palette; otherwise, <see langword="false"/>.
|
||||
|
/// </returns>
|
||||
|
public bool TryGetValue(Rgba32 color, out short value); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the cache.
|
||||
|
/// </summary>
|
||||
|
public void Clear(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a cache used for efficiently retrieving palette indices for colors.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of the cache.</typeparam>
|
||||
|
internal interface IColorIndexCache<T> : IColorIndexCache |
||||
|
where T : struct, IColorIndexCache |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the cache.
|
||||
|
/// </summary>
|
||||
|
/// <param name="allocator">The memory allocator to use.</param>
|
||||
|
/// <returns>
|
||||
|
/// The new instance of the cache.
|
||||
|
/// </returns>
|
||||
|
public static abstract T Create(MemoryAllocator allocator); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A hybrid color distance cache that combines a small, fixed-capacity exact-match dictionary
|
||||
|
/// (ExactCache, ~4–5 KB for up to 512 entries) with a coarse lookup table (CoarseCache) for 5,5,5,6 precision.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// ExactCache provides O(1) lookup for common cases using a simple 256-entry hash-based dictionary, while CoarseCache
|
||||
|
/// quantizes RGB channels to 5 bits (yielding 32^3 buckets) and alpha to 6 bits, storing up to 4 alpha entries per bucket
|
||||
|
/// (a design chosen based on probability theory to capture most real-world variations) for a total memory footprint of
|
||||
|
/// roughly 576 KB. Lookups and insertions are performed in constant time, making the overall design both fast and memory-predictable.
|
||||
|
/// </remarks>
|
||||
|
internal unsafe struct HybridCache : IColorIndexCache<HybridCache> |
||||
|
{ |
||||
|
private CoarseCache coarseCache; |
||||
|
private AccurateCache accurateCache; |
||||
|
|
||||
|
public HybridCache(MemoryAllocator allocator) |
||||
|
{ |
||||
|
this.accurateCache = AccurateCache.Create(allocator); |
||||
|
this.coarseCache = CoarseCache.Create(allocator); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static HybridCache Create(MemoryAllocator allocator) => new(allocator); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool TryAdd(Rgba32 color, short index) |
||||
|
{ |
||||
|
if (this.accurateCache.TryAdd(color, index)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return this.coarseCache.TryAdd(color, index); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public readonly bool TryGetValue(Rgba32 color, out short value) |
||||
|
{ |
||||
|
if (this.accurateCache.TryGetValue(color, out value)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return this.coarseCache.TryGetValue(color, out value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public readonly void Clear() |
||||
|
{ |
||||
|
this.accurateCache.Clear(); |
||||
|
this.coarseCache.Clear(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.accurateCache.Dispose(); |
||||
|
this.coarseCache.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A coarse cache for color distance lookups that uses a fixed-size lookup table.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This cache uses a fixed lookup table with 2,097,152 bins, each storing a 2-byte value,
|
||||
|
/// resulting in a memory usage of approximately 4 MB. Lookups and insertions are
|
||||
|
/// performed in constant time (O(1)) via direct table indexing. This design is optimized for
|
||||
|
/// speed while maintaining a predictable, fixed memory footprint.
|
||||
|
/// </remarks>
|
||||
|
internal unsafe struct CoarseCache : IColorIndexCache<CoarseCache> |
||||
|
{ |
||||
|
private const int IndexRBits = 5; |
||||
|
private const int IndexGBits = 5; |
||||
|
private const int IndexBBits = 5; |
||||
|
private const int IndexABits = 6; |
||||
|
private const int IndexRCount = 1 << IndexRBits; // 32 bins for red
|
||||
|
private const int IndexGCount = 1 << IndexGBits; // 32 bins for green
|
||||
|
private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue
|
||||
|
private const int IndexACount = 1 << IndexABits; // 64 bins for alpha
|
||||
|
private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 2,097,152 bins
|
||||
|
|
||||
|
private readonly IMemoryOwner<short> binsOwner; |
||||
|
private readonly short* binsPointer; |
||||
|
private MemoryHandle binsHandle; |
||||
|
|
||||
|
private CoarseCache(MemoryAllocator allocator) |
||||
|
{ |
||||
|
this.binsOwner = allocator.Allocate<short>(TotalBins); |
||||
|
this.binsOwner.GetSpan().Fill(-1); |
||||
|
this.binsHandle = this.binsOwner.Memory.Pin(); |
||||
|
this.binsPointer = (short*)this.binsHandle.Pointer; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public readonly bool TryAdd(Rgba32 color, short value) |
||||
|
{ |
||||
|
this.binsPointer[GetCoarseIndex(color)] = value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public readonly bool TryGetValue(Rgba32 color, out short value) |
||||
|
{ |
||||
|
value = this.binsPointer[GetCoarseIndex(color)]; |
||||
|
return value > -1; // Coarse match found
|
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
private static int GetCoarseIndex(Rgba32 color) |
||||
|
{ |
||||
|
int rIndex = color.R >> (8 - IndexRBits); |
||||
|
int gIndex = color.G >> (8 - IndexGBits); |
||||
|
int bIndex = color.B >> (8 - IndexBBits); |
||||
|
int aIndex = color.A >> (8 - IndexABits); |
||||
|
|
||||
|
return (aIndex * IndexRCount * IndexGCount * IndexBCount) + |
||||
|
(rIndex * IndexGCount * IndexBCount) + |
||||
|
(gIndex * IndexBCount) + |
||||
|
bIndex; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public readonly void Clear() |
||||
|
=> this.binsOwner.GetSpan().Fill(-1); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.binsHandle.Dispose(); |
||||
|
this.binsOwner.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <para>
|
||||
|
/// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values,
|
||||
|
/// using a quantized representation of 5,5,5,6 (RGB: 5 bits each, Alpha: 6 bits).
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// The cache quantizes the RGB channels to 5 bits each, resulting in 32 levels per channel and a total of 32³ = 32,768 buckets.
|
||||
|
/// Each bucket is represented by an <see cref="AlphaBucket"/>, which holds a small, inline array of alpha entries.
|
||||
|
/// Each alpha entry stores the alpha value quantized to 6 bits (0–63) along with a palette index (a 16-bit value).
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// Performance Characteristics:
|
||||
|
/// - Lookup: O(1) for computing the bucket index from the RGB channels, plus a small constant time (up to 8 iterations)
|
||||
|
/// to search through the alpha entries in the bucket.
|
||||
|
/// - Insertion: O(1) for bucket index computation and a quick linear search over a very small (fixed) number of entries.
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// Memory Characteristics:
|
||||
|
/// - The cache consists of 32,768 buckets.
|
||||
|
/// - Each <see cref="AlphaBucket"/> is implemented using an inline array with a capacity of 8 entries.
|
||||
|
/// - Each bucket occupies approximately 1 byte (Count) + (8 entries × 3 bytes each) ≈ 25 bytes.
|
||||
|
/// - Overall, the buckets occupy roughly 32,768 × 25 bytes = 819,200 bytes (≈ 800 KB).
|
||||
|
/// </para>
|
||||
|
/// <para>
|
||||
|
/// This design provides nearly constant-time lookup and insertion with minimal memory usage,
|
||||
|
/// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries).
|
||||
|
/// </para>
|
||||
|
/// </summary>
|
||||
|
internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite> |
||||
|
{ |
||||
|
// Use 5 bits per channel for R, G, and B: 32 levels each.
|
||||
|
// Total buckets = 32^3 = 32768.
|
||||
|
private const int RgbBits = 5; |
||||
|
private const int RgbShift = 8 - RgbBits; // 3
|
||||
|
private const int BucketCount = 1 << (RgbBits * 3); // 32768
|
||||
|
private readonly IMemoryOwner<AlphaBucket> bucketsOwner; |
||||
|
private readonly AlphaBucket* buckets; |
||||
|
private MemoryHandle bucketHandle; |
||||
|
|
||||
|
private CoarseCacheLite(MemoryAllocator allocator) |
||||
|
{ |
||||
|
this.bucketsOwner = allocator.Allocate<AlphaBucket>(BucketCount, AllocationOptions.Clean); |
||||
|
this.bucketHandle = this.bucketsOwner.Memory.Pin(); |
||||
|
this.buckets = (AlphaBucket*)this.bucketHandle.Pointer; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static CoarseCacheLite Create(MemoryAllocator allocator) => new(allocator); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public readonly bool TryAdd(Rgba32 color, short paletteIndex) |
||||
|
{ |
||||
|
int bucketIndex = GetBucketIndex(color.R, color.G, color.B); |
||||
|
byte quantAlpha = QuantizeAlpha(color.A); |
||||
|
this.buckets[bucketIndex].Add(quantAlpha, paletteIndex); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public readonly bool TryGetValue(Rgba32 color, out short paletteIndex) |
||||
|
{ |
||||
|
int bucketIndex = GetBucketIndex(color.R, color.G, color.B); |
||||
|
byte quantAlpha = QuantizeAlpha(color.A); |
||||
|
return this.buckets[bucketIndex].TryGetValue(quantAlpha, out paletteIndex); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public readonly void Clear() |
||||
|
{ |
||||
|
Span<AlphaBucket> bucketsSpan = this.bucketsOwner.GetSpan(); |
||||
|
bucketsSpan.Clear(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.bucketHandle.Dispose(); |
||||
|
this.bucketsOwner.Dispose(); |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
private static int GetBucketIndex(byte r, byte g, byte b) |
||||
|
{ |
||||
|
int qr = r >> RgbShift; |
||||
|
int qg = g >> RgbShift; |
||||
|
int qb = b >> RgbShift; |
||||
|
|
||||
|
// Combine the quantized channels into a single index.
|
||||
|
return (qr << (RgbBits << 1)) | (qg << RgbBits) | qb; |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
private static byte QuantizeAlpha(byte a) |
||||
|
|
||||
|
// Quantize to 6 bits: shift right by (8 - 6) = 2 bits.
|
||||
|
=> (byte)(a >> 2); |
||||
|
|
||||
|
public struct AlphaEntry |
||||
|
{ |
||||
|
// Store the alpha value quantized to 6 bits (0..63)
|
||||
|
public byte QuantizedAlpha; |
||||
|
public short PaletteIndex; |
||||
|
} |
||||
|
|
||||
|
public struct AlphaBucket |
||||
|
{ |
||||
|
// Fixed capacity for alpha entries in this bucket.
|
||||
|
// We choose a capacity of 8 for several reasons:
|
||||
|
//
|
||||
|
// 1. The alpha channel is quantized to 6 bits, so there are 64 possible distinct values.
|
||||
|
// In the worst-case, a given RGB bucket might encounter up to 64 different alpha values.
|
||||
|
//
|
||||
|
// 2. However, in practice (based on probability theory and typical image data),
|
||||
|
// the number of unique alpha values that actually occur for a given quantized RGB
|
||||
|
// bucket is usually very small. If you randomly sample 8 values out of 64,
|
||||
|
// the probability that these 4 samples are all unique is high if the distribution
|
||||
|
// of alpha values is skewed or if only a few alpha values are used.
|
||||
|
//
|
||||
|
// 3. Statistically, for many real-world images, most RGB buckets will have only a couple
|
||||
|
// of unique alpha values. Allocating 8 slots per bucket provides a good trade-off:
|
||||
|
// it captures the common-case scenario while keeping overall memory usage low.
|
||||
|
//
|
||||
|
// 4. Even if more than 8 unique alpha values occur in a bucket,
|
||||
|
// our design overwrites the first entry. This behavior gives us some "wriggle room"
|
||||
|
// while preserving the most frequently encountered or most recent values.
|
||||
|
public const int Capacity = 8; |
||||
|
public byte Count; |
||||
|
private InlineArray8<AlphaEntry> entries; |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool TryGetValue(byte quantizedAlpha, out short paletteIndex) |
||||
|
{ |
||||
|
for (int i = 0; i < this.Count; i++) |
||||
|
{ |
||||
|
ref AlphaEntry entry = ref this.entries[i]; |
||||
|
if (entry.QuantizedAlpha == quantizedAlpha) |
||||
|
{ |
||||
|
paletteIndex = entry.PaletteIndex; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
paletteIndex = -1; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public void Add(byte quantizedAlpha, short paletteIndex) |
||||
|
{ |
||||
|
// Check for an existing entry with the same quantized alpha.
|
||||
|
for (int i = 0; i < this.Count; i++) |
||||
|
{ |
||||
|
ref AlphaEntry entry = ref this.entries[i]; |
||||
|
if (entry.QuantizedAlpha == quantizedAlpha) |
||||
|
{ |
||||
|
// Update palette index if found.
|
||||
|
entry.PaletteIndex = paletteIndex; |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If there's room, add a new entry.
|
||||
|
if (this.Count < Capacity) |
||||
|
{ |
||||
|
ref AlphaEntry newEntry = ref this.entries[this.Count]; |
||||
|
newEntry.QuantizedAlpha = quantizedAlpha; |
||||
|
newEntry.PaletteIndex = paletteIndex; |
||||
|
this.Count++; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Bucket is full. Overwrite the first entry to give us some wriggle room.
|
||||
|
this.entries[0].QuantizedAlpha = quantizedAlpha; |
||||
|
this.entries[0].PaletteIndex = paletteIndex; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A fixed-capacity dictionary with exactly 512 entries mapping a <see cref="uint"/> key
|
||||
|
/// to a <see cref="short"/> value.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The dictionary is implemented using a fixed array of 512 buckets and an entries array
|
||||
|
/// of the same size. The bucket for a key is computed as (key & 0x1FF), and collisions are
|
||||
|
/// resolved through a linked chain stored in the <see cref="Entry.Next"/> field.
|
||||
|
/// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are,
|
||||
|
/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are
|
||||
|
/// typically very short; in the worst-case, the number of iterations is bounded by 256.
|
||||
|
/// This guarantees highly efficient and predictable performance for small, fixed-size color palettes.
|
||||
|
/// </remarks>
|
||||
|
internal unsafe struct AccurateCache : IColorIndexCache<AccurateCache> |
||||
|
{ |
||||
|
// Buckets array: each bucket holds the index (0-based) into the entries array
|
||||
|
// of the first entry in the chain, or -1 if empty.
|
||||
|
private readonly IMemoryOwner<short> bucketsOwner; |
||||
|
private MemoryHandle bucketsHandle; |
||||
|
private short* buckets; |
||||
|
|
||||
|
// Entries array: stores up to 256 entries.
|
||||
|
private readonly IMemoryOwner<Entry> entriesOwner; |
||||
|
private MemoryHandle entriesHandle; |
||||
|
private Entry* entries; |
||||
|
|
||||
|
public const int Capacity = 512; |
||||
|
|
||||
|
private AccurateCache(MemoryAllocator allocator) |
||||
|
{ |
||||
|
this.Count = 0; |
||||
|
|
||||
|
// Allocate exactly 512 indexes for buckets.
|
||||
|
this.bucketsOwner = allocator.Allocate<short>(Capacity, AllocationOptions.Clean); |
||||
|
Span<short> bucketSpan = this.bucketsOwner.GetSpan(); |
||||
|
bucketSpan.Fill(-1); |
||||
|
this.bucketsHandle = this.bucketsOwner.Memory.Pin(); |
||||
|
this.buckets = (short*)this.bucketsHandle.Pointer; |
||||
|
|
||||
|
// Allocate exactly 512 entries.
|
||||
|
this.entriesOwner = allocator.Allocate<Entry>(Capacity, AllocationOptions.Clean); |
||||
|
this.entriesHandle = this.entriesOwner.Memory.Pin(); |
||||
|
this.entries = (Entry*)this.entriesHandle.Pointer; |
||||
|
} |
||||
|
|
||||
|
public int Count { get; private set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool TryAdd(Rgba32 color, short value) |
||||
|
{ |
||||
|
if (this.Count == Capacity) |
||||
|
{ |
||||
|
return false; // Dictionary is full.
|
||||
|
} |
||||
|
|
||||
|
uint key = color.PackedValue; |
||||
|
|
||||
|
// The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A
|
||||
|
// (with R in the most significant byte and A in the least significant).
|
||||
|
// To compute the bucket index:
|
||||
|
// 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels.
|
||||
|
// 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A).
|
||||
|
// 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A),
|
||||
|
// which helps to counteract situations where one or more channels have a limited range.
|
||||
|
// 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511,
|
||||
|
// which corresponds to our fixed bucket count of 512.
|
||||
|
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); |
||||
|
int i = this.buckets[bucket]; |
||||
|
|
||||
|
// Traverse the collision chain.
|
||||
|
Entry* entries = this.entries; |
||||
|
while (i != -1) |
||||
|
{ |
||||
|
Entry e = entries[i]; |
||||
|
if (e.Key == key) |
||||
|
{ |
||||
|
// Key already exists; do not overwrite.
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
i = e.Next; |
||||
|
} |
||||
|
|
||||
|
short index = (short)this.Count; |
||||
|
this.Count++; |
||||
|
|
||||
|
// Insert the new entry:
|
||||
|
entries[index].Key = key; |
||||
|
entries[index].Value = value; |
||||
|
|
||||
|
// Link this new entry into the bucket chain.
|
||||
|
entries[index].Next = this.buckets[bucket]; |
||||
|
this.buckets[bucket] = index; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool TryGetValue(Rgba32 color, out short value) |
||||
|
{ |
||||
|
uint key = color.PackedValue; |
||||
|
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); |
||||
|
int i = this.buckets[bucket]; |
||||
|
|
||||
|
// If the bucket is empty, return immediately.
|
||||
|
if (i == -1) |
||||
|
{ |
||||
|
value = -1; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Traverse the chain.
|
||||
|
Entry* entries = this.entries; |
||||
|
do |
||||
|
{ |
||||
|
Entry e = entries[i]; |
||||
|
if (e.Key == key) |
||||
|
{ |
||||
|
value = e.Value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
i = e.Next; |
||||
|
} |
||||
|
while (i != -1); |
||||
|
|
||||
|
value = -1; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Clears the dictionary.
|
||||
|
/// </summary>
|
||||
|
public void Clear() |
||||
|
{ |
||||
|
Span<short> bucketSpan = this.bucketsOwner.GetSpan(); |
||||
|
bucketSpan.Fill(-1); |
||||
|
this.Count = 0; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.bucketsHandle.Dispose(); |
||||
|
this.bucketsOwner.Dispose(); |
||||
|
this.entriesHandle.Dispose(); |
||||
|
this.entriesOwner.Dispose(); |
||||
|
this.buckets = null; |
||||
|
this.entries = null; |
||||
|
} |
||||
|
|
||||
|
private struct Entry |
||||
|
{ |
||||
|
public uint Key; // The key (packed RGBA)
|
||||
|
public short Value; // The value; -1 means unused.
|
||||
|
public short Next; // Index of the next entry in the chain, or -1 if none.
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a cache that does not store any values.
|
||||
|
/// It allows adding colors, but always returns false when trying to retrieve them.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct NullCache : IColorIndexCache<NullCache> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public static NullCache Create(MemoryAllocator allocator) => default; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool TryAdd(Rgba32 color, short value) => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool TryGetValue(Rgba32 color, out short value) |
||||
|
{ |
||||
|
value = -1; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Clear() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines a delegate for processing a row of pixels in an image for quantization.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">Represents a pixel type that can be processed in a quantizing operation.</typeparam>
|
||||
|
internal interface IQuantizingPixelRowDelegate<TPixel> |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Processes a row of pixels for quantization.
|
||||
|
/// </summary>
|
||||
|
/// <param name="row">The row of pixels to process.</param>
|
||||
|
/// <param name="rowIndex">The index of the row being processed.</param>
|
||||
|
public void Invoke(ReadOnlySpan<TPixel> row, int rowIndex); |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Drawing.Imaging; |
||||
|
using BenchmarkDotNet.Attributes; |
||||
|
using SixLabors.ImageSharp.Formats.Gif; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
||||
|
using SixLabors.ImageSharp.Tests; |
||||
|
using SDImage = System.Drawing.Image; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Benchmarks.Codecs; |
||||
|
|
||||
|
public abstract class DecodeEncodeGif |
||||
|
{ |
||||
|
private MemoryStream outputStream; |
||||
|
|
||||
|
protected abstract GifEncoder Encoder { get; } |
||||
|
|
||||
|
[Params(TestImages.Gif.Leo, TestImages.Gif.Cheers)] |
||||
|
public string TestImage { get; set; } |
||||
|
|
||||
|
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
||||
|
|
||||
|
[GlobalSetup] |
||||
|
public void Setup() => this.outputStream = new MemoryStream(); |
||||
|
|
||||
|
[GlobalCleanup] |
||||
|
public void Cleanup() => this.outputStream.Close(); |
||||
|
|
||||
|
[Benchmark(Baseline = true)] |
||||
|
public void SystemDrawing() |
||||
|
{ |
||||
|
this.outputStream.Position = 0; |
||||
|
using SDImage image = SDImage.FromFile(this.TestImageFullPath); |
||||
|
image.Save(this.outputStream, ImageFormat.Gif); |
||||
|
} |
||||
|
|
||||
|
[Benchmark] |
||||
|
public void ImageSharp() |
||||
|
{ |
||||
|
this.outputStream.Position = 0; |
||||
|
using Image image = Image.Load(this.TestImageFullPath); |
||||
|
image.SaveAsGif(this.outputStream, this.Encoder); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class DecodeEncodeGif_DefaultEncoder : DecodeEncodeGif |
||||
|
{ |
||||
|
protected override GifEncoder Encoder => new(); |
||||
|
} |
||||
|
|
||||
|
public class DecodeEncodeGif_CoarsePaletteEncoder : DecodeEncodeGif |
||||
|
{ |
||||
|
protected override GifEncoder Encoder => new() |
||||
|
{ |
||||
|
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4, ColorMatchingMode = ColorMatchingMode.Coarse }) |
||||
|
}; |
||||
|
} |
||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d |
oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4 |
||||
size 9270 |
size 9270 |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:a899a84c6af24bfad89f9fde75957c7a979d65bcf096ab667cb976efd71cb560 |
||||
|
size 271171 |
||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:7e22401dddf6552cd91517c1cdd142d3b9a66a7ad5c80d2e52ae07a7f583708e |
oid sha256:e44c49a8f2ab1280c38e6ba71da29a93803b2aa4cf117e1e919909521b0373e6 |
||||
size 57657 |
size 57636 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:819a0ce38e27e2adfa454d8c5ad5b24e818bf8954c9f2406f608dcecf506c2c4 |
oid sha256:359a44bb957481c85d5acd65559b43ffc0acf806d4f4e57d6a791ca65b28295b |
||||
size 59838 |
size 59839 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:007ac609ec61b39c7bdd04bc87a698f5cdc76eadd834c1457f41eb9c135c3f7b |
oid sha256:7fb3743098a8147fd24294d933d93a61ec0155d754f52544650f6589719905be |
||||
size 60688 |
size 60688 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:46892c07e9a93f1df71f0e38b331a437fb9b7c52d8f40cf62780cb6bd35d3b13 |
oid sha256:41fa7d92a10db450f3b3729ab9e36074224baaefeda21cffd0466e37a111e138 |
||||
size 58963 |
size 59113 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:1b83345ca3de8d1fc0fbb5d8e68329b94ad79fc29b9f10a1392a97ffe9a0733e |
oid sha256:bebf3b3762b339874891e3d434511e5f2557be90d66d6d7fe827b50334ede6c2 |
||||
size 58985 |
size 58976 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:c775a5b19ba09e1b335389e0dc12cb0c3feaff6072e904da750a676fcd6b07dc |
oid sha256:fd4358826739db2c22064e8aa90597f8b6403b9d7e2866ec280e743c51d2f41f |
||||
size 59202 |
size 59203 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:8cc216ed952216d203836dc559234216614f1ed059651677cc0ea714010bd932 |
oid sha256:174ee39c08eb9a174b48b19dc618d043bf6b71eee68ab7127407eb713e164e61 |
||||
size 58855 |
size 58934 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
||||
size 871 |
size 870 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:bb3e3b9b3001e76505fb0e2db7ad200cad2a016c06f1993c60c3cab42c134863 |
oid sha256:e51abcab66201997deda99637de604330ef977fd2d1dbebaa0416c621d03b8f9 |
||||
size 867 |
size 869 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
||||
size 871 |
size 870 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
||||
size 871 |
size 870 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:9316cbbcb137ae6ff31646f6a5ba1d0aec100db4512509f7684187e74d16a111 |
oid sha256:eb86f2037a0aff48a84c0161f22eb2e2495daadbfa9c33185ddfd7b8429a4ea9 |
||||
size 51074 |
size 51266 |
||||
|
|||||
@ -1,3 +1,3 @@ |
|||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:6d2289ed4fa0c679f0f120d260fec8ab40b1599043cc0a1fbebc6b67e238ff87 |
oid sha256:ef033a419e2e1b06b57a66175bad9068f71ae4c862a66c5734f65cdaae8a27f0 |
||||
size 51428 |
size 51461 |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:0b980fb1927a70f88bd26b039c54e4ea20a6a1ad68aacd6f1a68a46eb1997a29 |
||||
|
size 1180 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:4006374b88ff4c4ed665333608a19e693fc083ae72beb71850d0e39ad45c9943 |
||||
|
size 1144 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:1f5054e1e464c9e9fc999eec00b9949a6dc256ee062e9910b5718b6d4658661a |
||||
|
size 1303 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:49e8bcbcc5dc63fbd555f90a52b4e111cfc058f3adba2ca9c52dec966dbbae8f |
||||
|
size 1371 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:0b980fb1927a70f88bd26b039c54e4ea20a6a1ad68aacd6f1a68a46eb1997a29 |
||||
|
size 1180 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:4006374b88ff4c4ed665333608a19e693fc083ae72beb71850d0e39ad45c9943 |
||||
|
size 1144 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:1f5054e1e464c9e9fc999eec00b9949a6dc256ee062e9910b5718b6d4658661a |
||||
|
size 1303 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:49e8bcbcc5dc63fbd555f90a52b4e111cfc058f3adba2ca9c52dec966dbbae8f |
||||
|
size 1371 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:18b60d2066cb53d41988da37b8c521ddcb5355b995320a8413b95522a0492140 |
||||
|
size 687 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:30ff7708250c5f02dc02d74238d398b319d8fc6c071178f32f82a17e3b637afd |
||||
|
size 542 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:d21f4576486692122b6ee719d75883849f65ddb07f632ea1c62b42651c289688 |
||||
|
size 591 |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue