mirror of https://github.com/SixLabors/ImageSharp
27 changed files with 1363 additions and 805 deletions
@ -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,545 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Gets the closest color to the supplied color based upon the Euclidean distance.
|
|||
/// </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 readonly HybridColorDistanceCache 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 = configuration; |
|||
this.Palette = palette; |
|||
this.rgbaPalette = new Rgba32[palette.Length]; |
|||
this.cache = new HybridColorDistanceCache(configuration.MemoryAllocator); |
|||
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); |
|||
} |
|||
|
|||
/// <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>
|
|||
/// <param name="transparencyThreshold">The transparency threshold.</param>
|
|||
/// <returns>The <see cref="int"/> index.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public int GetClosestColor(TPixel color, out TPixel match, short transparencyThreshold = -1) |
|||
{ |
|||
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); |
|||
Rgba32 rgba = color.ToRgba32(); |
|||
|
|||
if (transparencyThreshold > -1 && rgba.A < transparencyThreshold) |
|||
{ |
|||
rgba = default; |
|||
} |
|||
|
|||
// Check if the color is in the lookup table
|
|||
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); |
|||
} |
|||
|
|||
/// <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.cache.Clear(); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
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.Add(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(MethodImplOptions.AggressiveInlining)] |
|||
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 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>
|
|||
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
|
|||
// https://github.com/dotnet/roslyn-analyzers/issues/6151
|
|||
private readonly unsafe struct HybridColorDistanceCache : IDisposable |
|||
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
|
|||
{ |
|||
private readonly CoarseCache coarseCache; |
|||
private readonly ExactCache exactCache; |
|||
|
|||
public HybridColorDistanceCache(MemoryAllocator allocator) |
|||
{ |
|||
this.exactCache = new ExactCache(allocator); |
|||
this.coarseCache = new CoarseCache(allocator); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public readonly void Add(Rgba32 color, short index) |
|||
{ |
|||
if (this.exactCache.TryAdd(color.PackedValue, index)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
this.coarseCache.Add(color, index); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public readonly bool TryGetValue(Rgba32 color, out short match) |
|||
{ |
|||
if (this.exactCache.TryGetValue(color.PackedValue, out match)) |
|||
{ |
|||
return true; // Exact match found
|
|||
} |
|||
|
|||
if (this.coarseCache.TryGetValue(color, out match)) |
|||
{ |
|||
return true; // Coarse match found
|
|||
} |
|||
|
|||
match = -1; |
|||
return false; |
|||
} |
|||
|
|||
public readonly void Clear() |
|||
{ |
|||
this.exactCache.Clear(); |
|||
this.coarseCache.Clear(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.exactCache.Dispose(); |
|||
this.coarseCache.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <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 sealed unsafe class ExactCache : IDisposable |
|||
{ |
|||
// 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; |
|||
|
|||
public ExactCache(MemoryAllocator allocator) |
|||
{ |
|||
this.Count = 0; |
|||
|
|||
// Allocate exactly 512 ints 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; } |
|||
|
|||
/// <summary>
|
|||
/// Adds a key/value pair to the dictionary.
|
|||
/// If the key already exists, the dictionary is left unchanged.
|
|||
/// </summary>
|
|||
/// <param name="key">The key to add.</param>
|
|||
/// <param name="value">The value to add.</param>
|
|||
/// <returns><see langword="true"/> if the key was added; otherwise, <see langword="false"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public bool TryAdd(uint key, short value) |
|||
{ |
|||
if (this.Count == Capacity) |
|||
{ |
|||
return false; // Dictionary is full.
|
|||
} |
|||
|
|||
// 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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to retrieve the value associated with the specified key.
|
|||
/// Returns true if the key is found; otherwise, returns false.
|
|||
/// </summary>
|
|||
/// <param name="key">The key to search for.</param>
|
|||
/// <param name="value">The value associated with the key, if found.</param>
|
|||
/// <returns><see langword="true"/> if the key is found; otherwise, <see langword="false"/>.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public bool TryGetValue(uint key, out short value) |
|||
{ |
|||
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>
|
|||
/// <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 4 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 4 entries.
|
|||
/// - Each bucket occupies approximately 18 bytes.
|
|||
/// - Overall, the buckets occupy roughly 32,768 × 18 = 589,824 bytes (576 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 sealed unsafe class CoarseCache : IDisposable |
|||
{ |
|||
// 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 BucketCount = 1 << (RgbBits * 3); // 32768
|
|||
private readonly IMemoryOwner<AlphaBucket> bucketsOwner; |
|||
private readonly AlphaBucket* buckets; |
|||
private MemoryHandle bucketHandle; |
|||
|
|||
public CoarseCache(MemoryAllocator allocator) |
|||
{ |
|||
this.bucketsOwner = allocator.Allocate<AlphaBucket>(BucketCount, AllocationOptions.Clean); |
|||
this.bucketHandle = this.bucketsOwner.Memory.Pin(); |
|||
this.buckets = (AlphaBucket*)this.bucketHandle.Pointer; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int GetBucketIndex(byte r, byte g, byte b) |
|||
{ |
|||
int qr = r >> (8 - RgbBits); |
|||
int qg = g >> (8 - RgbBits); |
|||
int qb = b >> (8 - RgbBits); |
|||
|
|||
// Combine the quantized channels into a single index.
|
|||
return (qr << (RgbBits * 2)) | (qg << RgbBits) | qb; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static byte QuantizeAlpha(byte a) |
|||
|
|||
// Quantize to 6 bits: shift right by (8 - 6) = 2 bits.
|
|||
=> (byte)(a >> 2); |
|||
|
|||
public void Add(Rgba32 color, short paletteIndex) |
|||
{ |
|||
int bucketIndex = GetBucketIndex(color.R, color.G, color.B); |
|||
byte quantAlpha = QuantizeAlpha(color.A); |
|||
this.buckets[bucketIndex].Add(quantAlpha, paletteIndex); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.bucketHandle.Dispose(); |
|||
this.bucketsOwner.Dispose(); |
|||
} |
|||
|
|||
public 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); |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
Span<AlphaBucket> bucketsSpan = this.bucketsOwner.GetSpan(); |
|||
bucketsSpan.Clear(); |
|||
} |
|||
|
|||
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 4 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 4 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 4 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 4 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 = 4; |
|||
public byte Count; |
|||
private InlineArray4<AlphaEntry> entries; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
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(MethodImplOptions.AggressiveInlining)] |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,491 @@ |
|||
// 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 ExactCache exactCache; |
|||
|
|||
public HybridCache(MemoryAllocator allocator) |
|||
{ |
|||
this.exactCache = ExactCache.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.exactCache.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.exactCache.TryGetValue(color, out value)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return this.coarseCache.TryGetValue(color, out value); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly void Clear() |
|||
{ |
|||
this.exactCache.Clear(); |
|||
this.coarseCache.Clear(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.exactCache.Dispose(); |
|||
this.coarseCache.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 4 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 4 entries.
|
|||
/// - Each bucket occupies approximately 18 bytes.
|
|||
/// - Overall, the buckets occupy roughly 32,768 × 18 = 589,824 bytes (576 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 CoarseCache : IColorIndexCache<CoarseCache> |
|||
{ |
|||
// 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 CoarseCache(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 CoarseCache 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 4 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 4 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 4 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 4 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 = 4; |
|||
public byte Count; |
|||
private InlineArray4<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 ExactCache : IColorIndexCache<ExactCache> |
|||
{ |
|||
// 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 ExactCache(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 ExactCache 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,27 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats; |
|||
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>
|
|||
/// Gets the transparent color mode to use when adding colors to the palette.
|
|||
/// </summary>
|
|||
public TransparentColorMode TransparentColorMode { get; } |
|||
|
|||
/// <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); |
|||
} |
|||
Loading…
Reference in new issue