|
|
|
@ -12,19 +12,28 @@ using SixLabors.ImageSharp.PixelFormats; |
|
|
|
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Encapsulates methods to calculate the color palette if an image using an Octree pattern.
|
|
|
|
/// <see href="http://msdn.microsoft.com/en-us/library/aa479306.aspx"/>
|
|
|
|
/// Quantizes an image by building an adaptive 16-way color tree and reducing it to the requested palette size.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// <para>
|
|
|
|
/// Each level routes colors using one bit of RGB and, when useful, one bit of alpha, giving the tree up to 16 children
|
|
|
|
/// per node and letting transparency participate directly in palette construction.
|
|
|
|
/// </para>
|
|
|
|
/// <para>
|
|
|
|
/// Fully opaque mid-tone colors use RGB-only routing so more branch resolution is spent on visible color detail.
|
|
|
|
/// Transparent, dark, and light colors use alpha-aware routing so opacity changes can form distinct palette buckets.
|
|
|
|
/// </para>
|
|
|
|
/// </remarks>
|
|
|
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
|
|
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
|
|
|
|
// See https://github.com/dotnet/roslyn-analyzers/issues/6151
|
|
|
|
public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
public struct HexadecatreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
|
|
|
|
where TPixel : unmanaged, IPixel<TPixel> |
|
|
|
{ |
|
|
|
private readonly int maxColors; |
|
|
|
private readonly int bitDepth; |
|
|
|
private readonly Octree octree; |
|
|
|
private readonly Hexadecatree tree; |
|
|
|
private readonly IMemoryOwner<TPixel> paletteOwner; |
|
|
|
private ReadOnlyMemory<TPixel> palette; |
|
|
|
private PixelMap<TPixel>? pixelMap; |
|
|
|
@ -32,19 +41,19 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
private bool isDisposed; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="OctreeQuantizer{TPixel}"/> struct.
|
|
|
|
/// Initializes a new instance of the <see cref="HexadecatreeQuantizer{TPixel}"/> struct.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
|
|
|
|
/// <param name="options">The quantizer options defining quantization rules.</param>
|
|
|
|
/// <param name="configuration">The configuration that provides memory allocation and pixel conversion services.</param>
|
|
|
|
/// <param name="options">The quantizer options that control palette size, dithering, and transparency behavior.</param>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public OctreeQuantizer(Configuration configuration, QuantizerOptions options) |
|
|
|
public HexadecatreeQuantizer(Configuration configuration, QuantizerOptions options) |
|
|
|
{ |
|
|
|
this.Configuration = configuration; |
|
|
|
this.Options = options; |
|
|
|
|
|
|
|
this.maxColors = this.Options.MaxColors; |
|
|
|
this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8); |
|
|
|
this.octree = new Octree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); |
|
|
|
this.tree = new Hexadecatree(configuration, this.bitDepth, this.maxColors, this.Options.TransparencyThreshold); |
|
|
|
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean); |
|
|
|
this.pixelMap = default; |
|
|
|
this.palette = default; |
|
|
|
@ -76,23 +85,28 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
/// <inheritdoc/>
|
|
|
|
public readonly void AddPaletteColors(in Buffer2DRegion<TPixel> pixelRegion) |
|
|
|
{ |
|
|
|
PixelRowDelegate pixelRowDelegate = new(this.octree); |
|
|
|
QuantizerUtilities.AddPaletteColors<OctreeQuantizer<TPixel>, TPixel, Rgba32, PixelRowDelegate>( |
|
|
|
PixelRowDelegate pixelRowDelegate = new(this.tree); |
|
|
|
QuantizerUtilities.AddPaletteColors<HexadecatreeQuantizer<TPixel>, TPixel, Rgba32, PixelRowDelegate>( |
|
|
|
ref Unsafe.AsRef(in this), |
|
|
|
in pixelRegion, |
|
|
|
in pixelRowDelegate); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Materializes the final palette from the accumulated tree and prepares the dither lookup map when needed.
|
|
|
|
/// </summary>
|
|
|
|
private void ResolvePalette() |
|
|
|
{ |
|
|
|
short paletteIndex = 0; |
|
|
|
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan(); |
|
|
|
|
|
|
|
this.octree.Palettize(paletteSpan, ref paletteIndex); |
|
|
|
this.tree.Palettize(paletteSpan, ref paletteIndex); |
|
|
|
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory[..paletteSpan.Length]; |
|
|
|
|
|
|
|
if (this.isDithering) |
|
|
|
{ |
|
|
|
// Dithered colors often no longer land on a color that was seen during palette construction,
|
|
|
|
// so the quantization pass switches to nearest-palette matching once the palette is finalized.
|
|
|
|
this.pixelMap = PixelMapFactory.Create(this.Configuration, result, this.Options.ColorMatchingMode); |
|
|
|
} |
|
|
|
|
|
|
|
@ -108,17 +122,15 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public readonly byte GetQuantizedColor(TPixel color, out TPixel match) |
|
|
|
{ |
|
|
|
// Due to the addition of new colors by dithering that are not part of the original histogram,
|
|
|
|
// the octree nodes might not match the correct color.
|
|
|
|
// In this case, we must use the pixel map to get the closest color.
|
|
|
|
if (this.isDithering) |
|
|
|
{ |
|
|
|
// Dithering introduces adjusted colors that were never inserted into the tree, so tree lookup
|
|
|
|
// is only reliable for the non-dithered path.
|
|
|
|
return (byte)this.pixelMap!.GetClosestColor(color, out match); |
|
|
|
} |
|
|
|
|
|
|
|
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span); |
|
|
|
|
|
|
|
int index = this.octree.GetPaletteIndex(color); |
|
|
|
int index = this.tree.GetPaletteIndex(color); |
|
|
|
match = Unsafe.Add(ref paletteRef, (nuint)index); |
|
|
|
return (byte)index; |
|
|
|
} |
|
|
|
@ -132,34 +144,43 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
this.paletteOwner.Dispose(); |
|
|
|
this.pixelMap?.Dispose(); |
|
|
|
this.pixelMap = null; |
|
|
|
this.octree.Dispose(); |
|
|
|
this.tree.Dispose(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Forwards source rows into the tree without creating an intermediate buffer.
|
|
|
|
/// </summary>
|
|
|
|
private readonly struct PixelRowDelegate : IQuantizingPixelRowDelegate<Rgba32> |
|
|
|
{ |
|
|
|
private readonly Octree octree; |
|
|
|
private readonly Hexadecatree tree; |
|
|
|
|
|
|
|
public PixelRowDelegate(Octree octree) => this.octree = octree; |
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="PixelRowDelegate"/> struct.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="tree">The destination tree that should accumulate each visited row.</param>
|
|
|
|
public PixelRowDelegate(Hexadecatree tree) => this.tree = tree; |
|
|
|
|
|
|
|
public void Invoke(ReadOnlySpan<Rgba32> row, int rowIndex) => this.octree.AddColors(row); |
|
|
|
/// <inheritdoc/>
|
|
|
|
public void Invoke(ReadOnlySpan<Rgba32> row, int rowIndex) => this.tree.AddColors(row); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A hexadecatree-based color quantization structure used for fast color distance lookups and palette generation.
|
|
|
|
/// This tree maintains a fixed pool of nodes (capacity 4096) where each node can have up to 16 children, stores
|
|
|
|
/// color accumulation data, and supports dynamic node allocation and reduction. It offers near-constant-time insertions
|
|
|
|
/// and lookups while consuming roughly 240 KB for the node pool.
|
|
|
|
/// Stores the adaptive 16-way partition tree used to accumulate colors and emit palette entries.
|
|
|
|
/// </summary>
|
|
|
|
internal sealed class Octree : IDisposable |
|
|
|
/// <remarks>
|
|
|
|
/// The tree uses a fixed node arena for predictable allocation behavior, keeps per-level reducible node lists so
|
|
|
|
/// deeper buckets can be merged until the palette fits, and caches the previously inserted leaf so repeated colors
|
|
|
|
/// can be accumulated cheaply.
|
|
|
|
/// </remarks>
|
|
|
|
internal sealed class Hexadecatree : IDisposable |
|
|
|
{ |
|
|
|
// The memory allocator.
|
|
|
|
private readonly MemoryAllocator allocator; |
|
|
|
|
|
|
|
// Pooled buffer for OctreeNodes.
|
|
|
|
private readonly IMemoryOwner<OctreeNode> nodesOwner; |
|
|
|
private readonly IMemoryOwner<Node> nodesOwner; |
|
|
|
|
|
|
|
// Reducible nodes: one per level; we use an integer index; -1 means “no node.”
|
|
|
|
// One reducible-node head per level.
|
|
|
|
// Each entry stores a node index, or -1 when that level currently
|
|
|
|
// has no reducible nodes.
|
|
|
|
private readonly short[] reducibleNodes; |
|
|
|
|
|
|
|
// Maximum number of allowable colors.
|
|
|
|
@ -186,13 +207,13 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
private readonly Stack<short> freeIndices = new(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="Octree"/> class.
|
|
|
|
/// Initializes a new instance of the <see cref="Hexadecatree"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="configuration">The configuration which allows altering default behavior or extending the library.</param>
|
|
|
|
/// <param name="maxColorBits">The maximum number of significant bits in the image.</param>
|
|
|
|
/// <param name="maxColors">The maximum number of colors to allow in the palette.</param>
|
|
|
|
/// <param name="transparencyThreshold">The threshold for transparent colors.</param>
|
|
|
|
public Octree( |
|
|
|
/// <param name="configuration">The configuration that provides the backing memory allocator.</param>
|
|
|
|
/// <param name="maxColorBits">The number of levels to descend before forcing leaves.</param>
|
|
|
|
/// <param name="maxColors">The maximum number of palette entries the reduced tree may retain.</param>
|
|
|
|
/// <param name="transparencyThreshold">The alpha threshold below which generated palette entries become fully transparent.</param>
|
|
|
|
public Hexadecatree( |
|
|
|
Configuration configuration, |
|
|
|
int maxColorBits, |
|
|
|
int maxColors, |
|
|
|
@ -207,8 +228,7 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
|
|
|
|
// Allocate a conservative buffer for nodes.
|
|
|
|
const int capacity = 4096; |
|
|
|
this.allocator = configuration.MemoryAllocator; |
|
|
|
this.nodesOwner = this.allocator.Allocate<OctreeNode>(capacity, AllocationOptions.Clean); |
|
|
|
this.nodesOwner = configuration.MemoryAllocator.Allocate<Node>(capacity, AllocationOptions.Clean); |
|
|
|
|
|
|
|
// Create the reducible nodes array (one per level 0 .. maxColorBits-1).
|
|
|
|
this.reducibleNodes = new short[this.maxColorBits]; |
|
|
|
@ -216,24 +236,24 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
|
|
|
|
// Reserve index 0 for the root.
|
|
|
|
this.rootIndex = 0; |
|
|
|
ref OctreeNode root = ref this.Nodes[this.rootIndex]; |
|
|
|
ref Node root = ref this.Nodes[this.rootIndex]; |
|
|
|
root.Initialize(0, this.maxColorBits, this, this.rootIndex); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the number of leaves in the tree.
|
|
|
|
/// Gets or sets the number of leaf nodes currently representing palette buckets.
|
|
|
|
/// </summary>
|
|
|
|
public int Leaves { get; set; } |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the full collection of nodes as a span.
|
|
|
|
/// Gets the underlying node arena.
|
|
|
|
/// </summary>
|
|
|
|
internal Span<OctreeNode> Nodes => this.nodesOwner.Memory.Span; |
|
|
|
internal Span<Node> Nodes => this.nodesOwner.Memory.Span; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Adds a span of colors to the octree.
|
|
|
|
/// Adds a row of colors to the tree.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="row">A span of color values to be added.</param>
|
|
|
|
/// <param name="row">The colors to accumulate.</param>
|
|
|
|
public void AddColors(ReadOnlySpan<Rgba32> row) |
|
|
|
{ |
|
|
|
for (int x = 0; x < row.Length; x++) |
|
|
|
@ -243,12 +263,13 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Add a color to the Octree.
|
|
|
|
/// Adds a single color sample to the tree.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to add.</param>
|
|
|
|
/// <param name="color">The color to accumulate.</param>
|
|
|
|
private void AddColor(Rgba32 color) |
|
|
|
{ |
|
|
|
// Ensure that the tree is not already full.
|
|
|
|
// Once the node arena is full and there are no recycled slots available, keep collapsing
|
|
|
|
// reducible leaves until the tree is small enough to make forward progress again.
|
|
|
|
if (this.nextNode >= this.Nodes.Length && this.freeIndices.Count == 0) |
|
|
|
{ |
|
|
|
while (this.Leaves > this.maxColors) |
|
|
|
@ -257,32 +278,32 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// If the color is the same as the previous color, increment the node.
|
|
|
|
// Otherwise, add a new node.
|
|
|
|
// Scanlines often contain long runs of the same color. Caching the previous leaf lets those
|
|
|
|
// repeats skip the tree walk and just bump the accumulated sums in place.
|
|
|
|
if (this.previousColor.Equals(color)) |
|
|
|
{ |
|
|
|
if (this.previousNode == -1) |
|
|
|
{ |
|
|
|
this.previousColor = color; |
|
|
|
OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); |
|
|
|
Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
OctreeNode.Increment(this.previousNode, color, this); |
|
|
|
Node.Increment(this.previousNode, color, this); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.previousColor = color; |
|
|
|
OctreeNode.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); |
|
|
|
Node.AddColor(this.rootIndex, color, this.maxColorBits, 0, this); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Construct the palette from the octree.
|
|
|
|
/// Reduces the tree to the requested palette size and emits the final palette entries.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="palette">The palette to construct.</param>
|
|
|
|
/// <param name="paletteIndex">The current palette index.</param>
|
|
|
|
/// <param name="palette">The destination palette span.</param>
|
|
|
|
/// <param name="paletteIndex">The running palette index.</param>
|
|
|
|
public void Palettize(Span<TPixel> palette, ref short paletteIndex) |
|
|
|
{ |
|
|
|
while (this.Leaves > this.maxColors) |
|
|
|
@ -294,48 +315,45 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Get the palette index for the passed color.
|
|
|
|
/// Gets the palette index selected by the tree for the supplied color.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to get the palette index for.</param>
|
|
|
|
/// <returns>The <see cref="int"/>.</returns>
|
|
|
|
/// <param name="color">The color to resolve.</param>
|
|
|
|
/// <returns>The palette index represented by the best matching leaf in the reduced tree.</returns>
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|
|
|
public int GetPaletteIndex(TPixel color) |
|
|
|
=> this.Nodes[this.rootIndex].GetPaletteIndex(color.ToRgba32(), 0, this); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Track the previous node and color.
|
|
|
|
/// Records the most recently touched leaf so repeated colors can bypass another descent.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="nodeIndex">The node index.</param>
|
|
|
|
/// <param name="nodeIndex">The leaf node index.</param>
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|
|
|
public void TrackPrevious(int nodeIndex) |
|
|
|
=> this.previousNode = nodeIndex; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reduce the depth of the tree.
|
|
|
|
/// Collapses the deepest currently reducible node into a single leaf.
|
|
|
|
/// </summary>
|
|
|
|
private void Reduce() |
|
|
|
{ |
|
|
|
// Find the deepest level containing at least one reducible node
|
|
|
|
int index = this.maxColorBits - 1; |
|
|
|
while ((index > 0) && (this.reducibleNodes[index] == -1)) |
|
|
|
{ |
|
|
|
index--; |
|
|
|
} |
|
|
|
|
|
|
|
// Reduce the node most recently added to the list at level 'index'
|
|
|
|
ref OctreeNode node = ref this.Nodes[this.reducibleNodes[index]]; |
|
|
|
ref Node node = ref this.Nodes[this.reducibleNodes[index]]; |
|
|
|
this.reducibleNodes[index] = node.NextReducibleIndex; |
|
|
|
|
|
|
|
// Decrement the leaf count after reducing the node
|
|
|
|
node.Reduce(this); |
|
|
|
|
|
|
|
// And just in case I've reduced the last color to be added, and the next color to
|
|
|
|
// be added is the same, invalidate the previousNode...
|
|
|
|
// If the last inserted leaf was merged away, the next repeated color must walk the tree again.
|
|
|
|
this.previousNode = -1; |
|
|
|
} |
|
|
|
|
|
|
|
// Allocate a new OctreeNode from the pooled buffer.
|
|
|
|
// First check the freeIndices stack.
|
|
|
|
/// <summary>
|
|
|
|
/// Allocates a node index from the free list or from the unused tail of the arena.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>The allocated node index, or <c>-1</c> if no node can be allocated.</returns>
|
|
|
|
internal short AllocateNode() |
|
|
|
{ |
|
|
|
if (this.freeIndices.Count > 0) |
|
|
|
@ -354,9 +372,9 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Free a node index, making it available for re-allocation.
|
|
|
|
/// Returns a node index to the free list.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="index">The index to free.</param>
|
|
|
|
/// <param name="index">The node index to recycle.</param>
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|
|
|
internal void FreeNode(short index) |
|
|
|
{ |
|
|
|
@ -367,8 +385,11 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
/// <inheritdoc/>
|
|
|
|
public void Dispose() => this.nodesOwner.Dispose(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Represents one node in the hexadecatree node arena.
|
|
|
|
/// </summary>
|
|
|
|
[StructLayout(LayoutKind.Sequential)] |
|
|
|
internal unsafe struct OctreeNode |
|
|
|
internal struct Node |
|
|
|
{ |
|
|
|
public bool Leaf; |
|
|
|
public int PixelCount; |
|
|
|
@ -380,19 +401,21 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
public short NextReducibleIndex; |
|
|
|
private InlineArray16<short> children; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the 16 child slots for this node.
|
|
|
|
/// </summary>
|
|
|
|
[UnscopedRef] |
|
|
|
public Span<short> Children => this.children; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initialize the <see cref="OctreeNode"/>.
|
|
|
|
/// Initializes a node either as a leaf or as a reducible interior node.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="level">The level of the node.</param>
|
|
|
|
/// <param name="colorBits">The number of significant color bits in the image.</param>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
/// <param name="index">The index of the node.</param>
|
|
|
|
public void Initialize(int level, int colorBits, Octree octree, short index) |
|
|
|
/// <param name="level">The depth of the node being initialized.</param>
|
|
|
|
/// <param name="colorBits">The maximum tree depth.</param>
|
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
/// <param name="index">The node index in the arena.</param>
|
|
|
|
public void Initialize(int level, int colorBits, Hexadecatree tree, short index) |
|
|
|
{ |
|
|
|
// Construct the new node.
|
|
|
|
this.Leaf = level == colorBits; |
|
|
|
this.Red = 0; |
|
|
|
this.Green = 0; |
|
|
|
@ -401,76 +424,73 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
this.PixelCount = 0; |
|
|
|
this.PaletteIndex = 0; |
|
|
|
this.NextReducibleIndex = -1; |
|
|
|
|
|
|
|
// Always clear the Children array.
|
|
|
|
this.Children.Fill(-1); |
|
|
|
|
|
|
|
if (this.Leaf) |
|
|
|
{ |
|
|
|
octree.Leaves++; |
|
|
|
tree.Leaves++; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Add this node to the reducible nodes list for its level.
|
|
|
|
this.NextReducibleIndex = octree.reducibleNodes[level]; |
|
|
|
octree.reducibleNodes[level] = index; |
|
|
|
// Track reducible nodes per level so palette reduction can always collapse the deepest
|
|
|
|
// buckets first without scanning the entire arena.
|
|
|
|
this.NextReducibleIndex = tree.reducibleNodes[level]; |
|
|
|
tree.reducibleNodes[level] = index; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Add a color to the Octree.
|
|
|
|
/// Descends the tree for the supplied color, allocating nodes as needed until a leaf is reached.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="nodeIndex">The node index.</param>
|
|
|
|
/// <param name="color">The color to add.</param>
|
|
|
|
/// <param name="colorBits">The number of significant color bits in the image.</param>
|
|
|
|
/// <param name="level">The level of the node.</param>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Octree octree) |
|
|
|
/// <param name="nodeIndex">The current node index.</param>
|
|
|
|
/// <param name="color">The color being accumulated.</param>
|
|
|
|
/// <param name="colorBits">The maximum tree depth.</param>
|
|
|
|
/// <param name="level">The current depth.</param>
|
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
public static void AddColor(int nodeIndex, Rgba32 color, int colorBits, int level, Hexadecatree tree) |
|
|
|
{ |
|
|
|
ref OctreeNode node = ref octree.Nodes[nodeIndex]; |
|
|
|
ref Node node = ref tree.Nodes[nodeIndex]; |
|
|
|
if (node.Leaf) |
|
|
|
{ |
|
|
|
Increment(nodeIndex, color, octree); |
|
|
|
octree.TrackPrevious(nodeIndex); |
|
|
|
Increment(nodeIndex, color, tree); |
|
|
|
tree.TrackPrevious(nodeIndex); |
|
|
|
return; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
int index = GetColorIndex(color, level); |
|
|
|
short childIndex; |
|
|
|
|
|
|
|
Span<short> children = node.Children; |
|
|
|
childIndex = children[index]; |
|
|
|
int index = GetColorIndex(color, level); |
|
|
|
Span<short> children = node.Children; |
|
|
|
short childIndex = children[index]; |
|
|
|
|
|
|
|
if (childIndex == -1) |
|
|
|
{ |
|
|
|
childIndex = tree.AllocateNode(); |
|
|
|
if (childIndex == -1) |
|
|
|
{ |
|
|
|
childIndex = octree.AllocateNode(); |
|
|
|
|
|
|
|
if (childIndex == -1) |
|
|
|
{ |
|
|
|
// No room in the tree, so increment the count and return.
|
|
|
|
Increment(nodeIndex, color, octree); |
|
|
|
octree.TrackPrevious(nodeIndex); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
ref OctreeNode child = ref octree.Nodes[childIndex]; |
|
|
|
child.Initialize(level + 1, colorBits, octree, childIndex); |
|
|
|
children[index] = childIndex; |
|
|
|
// If the arena is exhausted and no node can be reclaimed yet, fall back to
|
|
|
|
// accumulating into the current node instead of failing the insert outright.
|
|
|
|
Increment(nodeIndex, color, tree); |
|
|
|
tree.TrackPrevious(nodeIndex); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
AddColor(childIndex, color, colorBits, level + 1, octree); |
|
|
|
ref Node child = ref tree.Nodes[childIndex]; |
|
|
|
child.Initialize(level + 1, colorBits, tree, childIndex); |
|
|
|
children[index] = childIndex; |
|
|
|
} |
|
|
|
|
|
|
|
// Keep descending until we reach the leaf bucket that should accumulate this sample.
|
|
|
|
AddColor(childIndex, color, colorBits, level + 1, tree); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Increment the color components of this node.
|
|
|
|
/// Adds the supplied color sample to an existing node's running sums.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="nodeIndex">The node index.</param>
|
|
|
|
/// <param name="color">The color to increment by.</param>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
public static void Increment(int nodeIndex, Rgba32 color, Octree octree) |
|
|
|
/// <param name="nodeIndex">The node index to update.</param>
|
|
|
|
/// <param name="color">The color sample being accumulated.</param>
|
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
public static void Increment(int nodeIndex, Rgba32 color, Hexadecatree tree) |
|
|
|
{ |
|
|
|
ref OctreeNode node = ref octree.Nodes[nodeIndex]; |
|
|
|
ref Node node = ref tree.Nodes[nodeIndex]; |
|
|
|
node.PixelCount++; |
|
|
|
node.Red += color.R; |
|
|
|
node.Green += color.G; |
|
|
|
@ -479,10 +499,10 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reduce this node by ensuring its children are all reduced (i.e. leaves) and then merging their data.
|
|
|
|
/// Merges all child nodes into this node and turns it into a leaf.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
public void Reduce(Octree octree) |
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
public void Reduce(Hexadecatree tree) |
|
|
|
{ |
|
|
|
// If already a leaf, do nothing.
|
|
|
|
if (this.Leaf) |
|
|
|
@ -492,25 +512,27 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
|
|
|
|
// Now merge the (presumably reduced) children.
|
|
|
|
int pixelCount = 0; |
|
|
|
int sumRed = 0, sumGreen = 0, sumBlue = 0, sumAlpha = 0; |
|
|
|
int sumRed = 0; |
|
|
|
int sumGreen = 0; |
|
|
|
int sumBlue = 0; |
|
|
|
int sumAlpha = 0; |
|
|
|
Span<short> children = this.Children; |
|
|
|
|
|
|
|
for (int i = 0; i < children.Length; i++) |
|
|
|
{ |
|
|
|
short childIndex = children[i]; |
|
|
|
if (childIndex != -1) |
|
|
|
{ |
|
|
|
ref OctreeNode child = ref octree.Nodes[childIndex]; |
|
|
|
ref Node child = ref tree.Nodes[childIndex]; |
|
|
|
int pixels = child.PixelCount; |
|
|
|
|
|
|
|
sumRed += child.Red; |
|
|
|
sumGreen += child.Green; |
|
|
|
sumBlue += child.Blue; |
|
|
|
sumAlpha += child.Alpha; |
|
|
|
pixelCount += pixels; |
|
|
|
|
|
|
|
// Free the child immediately.
|
|
|
|
children[i] = -1; |
|
|
|
octree.FreeNode(childIndex); |
|
|
|
tree.FreeNode(childIndex); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -529,16 +551,16 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
this.Leaf = true; |
|
|
|
octree.Leaves++; |
|
|
|
tree.Leaves++; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Traverse the tree to construct the palette.
|
|
|
|
/// Traverses the reduced tree and emits one palette color per leaf.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
/// <param name="palette">The palette to construct.</param>
|
|
|
|
/// <param name="paletteIndex">The current palette index.</param>
|
|
|
|
public void ConstructPalette(Octree octree, Span<TPixel> palette, ref short paletteIndex) |
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
/// <param name="palette">The destination palette span.</param>
|
|
|
|
/// <param name="paletteIndex">The running palette index.</param>
|
|
|
|
public void ConstructPalette(Hexadecatree tree, Span<TPixel> palette, ref short paletteIndex) |
|
|
|
{ |
|
|
|
if (this.Leaf) |
|
|
|
{ |
|
|
|
@ -549,13 +571,12 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
Vector4.Zero, |
|
|
|
new Vector4(255)); |
|
|
|
|
|
|
|
if (vector.W < octree.transparencyThreshold255) |
|
|
|
if (vector.W < tree.transparencyThreshold255) |
|
|
|
{ |
|
|
|
vector = Vector4.Zero; |
|
|
|
} |
|
|
|
|
|
|
|
palette[paletteIndex] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W)); |
|
|
|
|
|
|
|
this.PaletteIndex = paletteIndex++; |
|
|
|
} |
|
|
|
else |
|
|
|
@ -566,19 +587,20 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
int childIndex = children[i]; |
|
|
|
if (childIndex != -1) |
|
|
|
{ |
|
|
|
octree.Nodes[childIndex].ConstructPalette(octree, palette, ref paletteIndex); |
|
|
|
tree.Nodes[childIndex].ConstructPalette(tree, palette, ref paletteIndex); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Get the palette index for the passed color.
|
|
|
|
/// Resolves the palette index represented by this node for the supplied color.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to get the palette index for.</param>
|
|
|
|
/// <param name="level">The level of the node.</param>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
public int GetPaletteIndex(Rgba32 color, int level, Octree octree) |
|
|
|
/// <param name="color">The color to resolve.</param>
|
|
|
|
/// <param name="level">The current tree depth.</param>
|
|
|
|
/// <param name="tree">The owning tree.</param>
|
|
|
|
/// <returns>The palette index for the best reachable leaf, or <c>-1</c> if no leaf can be reached.</returns>
|
|
|
|
public int GetPaletteIndex(Rgba32 color, int level, Hexadecatree tree) |
|
|
|
{ |
|
|
|
if (this.Leaf) |
|
|
|
{ |
|
|
|
@ -590,15 +612,16 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
int childIndex = children[colorIndex]; |
|
|
|
if (childIndex != -1) |
|
|
|
{ |
|
|
|
return octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); |
|
|
|
return tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); |
|
|
|
} |
|
|
|
|
|
|
|
// After reductions the exact branch can disappear, so fall back to the first reachable descendant leaf.
|
|
|
|
for (int i = 0; i < children.Length; i++) |
|
|
|
{ |
|
|
|
childIndex = children[i]; |
|
|
|
if (childIndex != -1) |
|
|
|
{ |
|
|
|
int childPaletteIndex = octree.Nodes[childIndex].GetPaletteIndex(color, level + 1, octree); |
|
|
|
int childPaletteIndex = tree.Nodes[childIndex].GetPaletteIndex(color, level + 1, tree); |
|
|
|
if (childPaletteIndex != -1) |
|
|
|
{ |
|
|
|
return childPaletteIndex; |
|
|
|
@ -610,37 +633,35 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the color index at the given level.
|
|
|
|
/// Computes the child slot for a color at the supplied tree level.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to get the index for.</param>
|
|
|
|
/// <param name="level">The level to get the index at.</param>
|
|
|
|
/// <param name="color">The color being routed.</param>
|
|
|
|
/// <param name="level">The tree depth whose bit plane should be sampled.</param>
|
|
|
|
/// <returns>The child slot index for the color at the supplied level.</returns>
|
|
|
|
/// <remarks>
|
|
|
|
/// For fully opaque mid-tone colors the tree ignores alpha and routes on RGB only, preserving more branch
|
|
|
|
/// resolution for visible color detail. For transparent, dark, and light colors it includes alpha as the
|
|
|
|
/// most significant routing bit so opacity changes can form their own branches.
|
|
|
|
/// </remarks>
|
|
|
|
public static int GetColorIndex(Rgba32 color, int level) |
|
|
|
{ |
|
|
|
// Determine how many bits to shift based on the current tree level.
|
|
|
|
// At level 0, shift = 7; as level increases, the shift decreases.
|
|
|
|
// Sample one bit plane per level, starting at the most significant bit and moving downward.
|
|
|
|
int shift = 7 - level; |
|
|
|
byte mask = (byte)(1 << shift); |
|
|
|
|
|
|
|
// Compute the luminance of the RGB components using the BT.709 standard.
|
|
|
|
// This gives a measure of brightness for the color.
|
|
|
|
// Use BT.709 luminance as a cheap brightness estimate for deciding whether alpha carries
|
|
|
|
// useful information at this level for fully opaque colors.
|
|
|
|
int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B); |
|
|
|
|
|
|
|
// Define thresholds for determining when to include the alpha bit in the index.
|
|
|
|
// The thresholds are scaled according to the current level.
|
|
|
|
// 128 is the midpoint of the 8-bit range (0–255), so shifting it right by 'level'
|
|
|
|
// produces a threshold that scales with the color cube subdivision.
|
|
|
|
// Scale the brightness thresholds with depth so deeper levels become stricter about when
|
|
|
|
// to spend a branch bit on alpha instead of RGB detail.
|
|
|
|
int darkThreshold = 128 >> level; |
|
|
|
|
|
|
|
// The light threshold is set symmetrically: 255 minus the scaled midpoint.
|
|
|
|
int lightThreshold = 255 - (128 >> level); |
|
|
|
|
|
|
|
// If the pixel is fully opaque and its brightness falls between the dark and light thresholds,
|
|
|
|
// ignore the alpha channel to maximize RGB resolution.
|
|
|
|
// Otherwise (if the pixel is dark, light, or semi-transparent), include the alpha bit
|
|
|
|
// to preserve any gradient that may be present.
|
|
|
|
if (color.A == 255 && luminance > darkThreshold && luminance < lightThreshold) |
|
|
|
{ |
|
|
|
// Extract one bit each from R, G, and B channels and combine them into a 3-bit index.
|
|
|
|
// Fully opaque mid-tone colors route on RGB only, which preserves more visible color
|
|
|
|
// resolution because alpha would contribute no extra separation here.
|
|
|
|
int rBits = ((color.R & mask) >> shift) << 2; |
|
|
|
int gBits = ((color.G & mask) >> shift) << 1; |
|
|
|
int bBits = (color.B & mask) >> shift; |
|
|
|
@ -648,7 +669,8 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Extract one bit from each channel including alpha (alpha becomes the most significant bit).
|
|
|
|
// Transparent, dark, and light colors include alpha as the high routing bit so opacity
|
|
|
|
// changes can form distinct buckets alongside RGB differences.
|
|
|
|
int aBits = ((color.A & mask) >> shift) << 3; |
|
|
|
int rBits = ((color.R & mask) >> shift) << 2; |
|
|
|
int gBits = ((color.G & mask) >> shift) << 1; |