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