Repository navigation
Quality loss when quantizing an image that already has (fewer than) 256 colors #2862
Replies: 1 comment 7 replies
|
Hi @pwitvoet I've converted this into a discussion because the library is working as designed. Thanks for providing such an excellent summary. It's actually something I've been thinking about recently. Just a few notes on your comments:
You shouldn't need to use this constructor. If you look at other quantizer implementations they defer the creation of the type to
Yes, that's not great at the moment but I don't think I can make that configurable. I have plans to improve the bit preservation in the pixel map by replacing the current cache with a hybrid cache that stores the first 65536 unique colors in the image using full fidelity and falls back to the current implementation if not found. That would fix your issue, but I need to benchmark this to make sure it's usable. Proposed implementationusing System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
public unsafe struct HybridColorDistanceCacheRGBA : IDisposable
{
private const int IndexRBits = 5;
private const int IndexGBits = 5;
private const int IndexBBits = 5;
private const int IndexABits = 6;
private const int IndexRCount = 1 << IndexRBits; // 32 bins for red
private const int IndexGCount = 1 << IndexGBits; // 32 bins for green
private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue
private const int IndexACount = 1 << IndexABits; // 64 bins for alpha
private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 1,048,576 bins
private readonly IMemoryOwner<short> fallbackTable;
private readonly short* fallbackPointer;
private MemoryHandle fallbackHandle;
private readonly Dictionary<int, short> hashTable;
private readonly int maxHashSize; // Maximum size for the hash table
public HybridColorDistanceCacheRGBA(MemoryAllocator allocator, int maxHashSize = 65536)
{
this.fallbackTable = allocator.Allocate<short>(TotalBins);
this.fallbackTable.GetSpan().Fill(-1);
this.fallbackHandle = this.fallbackTable.Memory.Pin();
this.fallbackPointer = (short*)this.fallbackHandle.Pointer;
this.hashTable = new Dictionary<int, short>(maxHashSize);
this.maxHashSize = maxHashSize;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(Rgba32 color, short index)
{
int hash = GetHash(color);
if (this.hashTable.Count < this.maxHashSize)
{
this.hashTable.TryAdd(hash, index);
}
int coarseIndex = GetCoarseIndex(color);
this.fallbackPointer[coarseIndex] = index;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(Rgba32 color, out short match)
{
if (this.hashTable.TryGetValue(GetHash(color), out match))
{
return true; // Exact match found
}
match = this.fallbackPointer[GetCoarseIndex(color)];
return match > -1; // Coarse match found
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetHash(Rgba32 color)
{
/*
* Improved hash function for better distribution:
* The constants used here (73856093, 19349663, 83492791, 113429371) are prime numbers.
* Multiplying the color channels by these primes helps in achieving a uniform distribution
* of hash values by reducing collisions when colors differ slightly.
* This approach ensures that similar colors produce different hash values, improving hash table performance.
*/
return (color.R * 73856093) ^ (color.G * 19349663) ^ (color.B * 83492791) ^ (color.A * 113429371);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetCoarseIndex(Rgba32 color)
{
int rIndex = color.R >> (8 - IndexRBits);
int gIndex = color.G >> (8 - IndexGBits);
int bIndex = color.B >> (8 - IndexBBits);
int aIndex = color.A >> (8 - IndexABits);
return (aIndex * IndexRCount * IndexGCount * IndexBCount) +
(rIndex * IndexGCount * IndexBCount) +
(gIndex * IndexBCount) +
bIndex;
}
public void Clear()
{
this.hashTable.Clear();
this.fallbackTable.GetSpan().Fill(-1);
}
public void Dispose()
{
if (this.fallbackTable != null)
{
this.fallbackHandle.Dispose();
this.fallbackTable.Dispose();
}
}
} |



Man... Oh man... Have I spent some time on this. It turns out implementing such a thing but keeping it fast is hard!
Here's what I've come up with. It 1.4x - 2x slower than the current approach depending on the number of collisions but will give you your accuracy for the first 256 colors.
Proposed implementation