|
|
|
@ -654,479 +654,4 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel> |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Class which does the actual quantization.
|
|
|
|
/// </summary>
|
|
|
|
private sealed class Octree2 |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// The root of the Octree2
|
|
|
|
/// </summary>
|
|
|
|
private readonly OctreeNode root; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Maximum number of significant bits in the image
|
|
|
|
/// </summary>
|
|
|
|
private readonly int maxColorBits; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The threshold for transparent colors.
|
|
|
|
/// </summary>
|
|
|
|
private readonly int transparencyThreshold; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Store the last node quantized
|
|
|
|
/// </summary>
|
|
|
|
private OctreeNode? previousNode; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Cache the previous color quantized
|
|
|
|
/// </summary>
|
|
|
|
private Rgba32 previousColor; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="Octree2"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="maxColorBits">
|
|
|
|
/// The maximum number of significant bits in the image
|
|
|
|
/// </param>
|
|
|
|
/// <param name="transparencyThreshold">The threshold for transparent colors.</param>
|
|
|
|
public Octree2(int maxColorBits, int transparencyThreshold) |
|
|
|
{ |
|
|
|
this.maxColorBits = maxColorBits; |
|
|
|
this.transparencyThreshold = transparencyThreshold; |
|
|
|
this.Leaves = 0; |
|
|
|
this.ReducibleNodes = new OctreeNode[9]; |
|
|
|
this.root = new OctreeNode(0, this.maxColorBits, this); |
|
|
|
this.previousColor = default; |
|
|
|
this.previousNode = null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the number of leaves in the tree
|
|
|
|
/// </summary>
|
|
|
|
public int Leaves |
|
|
|
{ |
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
get; |
|
|
|
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
set; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the array of reducible nodes
|
|
|
|
/// </summary>
|
|
|
|
private OctreeNode?[] ReducibleNodes |
|
|
|
{ |
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
get; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Add a given color value to the Octree2
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to add.</param>
|
|
|
|
public void AddColor(Rgba32 color) |
|
|
|
{ |
|
|
|
if (color.A < this.transparencyThreshold) |
|
|
|
{ |
|
|
|
color.A = 0; |
|
|
|
} |
|
|
|
|
|
|
|
// Check if this request is for the same color as the last
|
|
|
|
if (this.previousColor.Equals(color)) |
|
|
|
{ |
|
|
|
// If so, check if I have a previous node setup.
|
|
|
|
if (this.previousNode is null) |
|
|
|
{ |
|
|
|
this.previousColor = color; |
|
|
|
this.root.AddColor(color, this.maxColorBits, 0, this); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Just update the previous node
|
|
|
|
this.previousNode.Increment(color, this); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.previousColor = color; |
|
|
|
this.root.AddColor(color, this.maxColorBits, 0, this); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Convert the nodes in the Octree2 to a palette with a maximum of colorCount colors
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="palette">The palette to fill.</param>
|
|
|
|
/// <param name="colorCount">The maximum number of colors</param>
|
|
|
|
/// <param name="paletteIndex">The palette index, used to calculate the final size of the palette.</param>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public void Palettize(Span<TPixel> palette, int colorCount, ref int paletteIndex) |
|
|
|
{ |
|
|
|
while (this.Leaves > colorCount) |
|
|
|
{ |
|
|
|
this.Reduce(); |
|
|
|
} |
|
|
|
|
|
|
|
this.root.ConstructPalette(palette, ref paletteIndex); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Get the palette index for the passed color
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to match.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The <see cref="int"/> index.
|
|
|
|
/// </returns>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public int GetPaletteIndex(TPixel color) |
|
|
|
=> this.root.GetPaletteIndex(color.ToRgba32(), 0); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Keep track of the previous node that was quantized
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="node">
|
|
|
|
/// The node last quantized
|
|
|
|
/// </param>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public void TrackPrevious(OctreeNode node) => this.previousNode = node; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reduce the depth of the tree
|
|
|
|
/// </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] is null)) |
|
|
|
{ |
|
|
|
index--; |
|
|
|
} |
|
|
|
|
|
|
|
// Reduce the node most recently added to the list at level 'index'
|
|
|
|
OctreeNode node = this.ReducibleNodes[index]!; |
|
|
|
this.ReducibleNodes[index] = node.NextReducible; |
|
|
|
|
|
|
|
// Decrement the leaf count after reducing the node
|
|
|
|
this.Leaves -= 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...
|
|
|
|
this.previousNode = null; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Class which encapsulates each node in the tree
|
|
|
|
/// </summary>
|
|
|
|
public sealed class OctreeNode |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Pointers to any child nodes
|
|
|
|
/// </summary>
|
|
|
|
private readonly OctreeNode?[]? children; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Flag indicating that this is a leaf node
|
|
|
|
/// </summary>
|
|
|
|
private bool leaf; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Number of pixels in this node
|
|
|
|
/// </summary>
|
|
|
|
private int pixelCount; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Red component
|
|
|
|
/// </summary>
|
|
|
|
private int red; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Green Component
|
|
|
|
/// </summary>
|
|
|
|
private int green; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Blue component
|
|
|
|
/// </summary>
|
|
|
|
private int blue; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Alpha component
|
|
|
|
/// </summary>
|
|
|
|
private int alpha; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The index of this node in the palette
|
|
|
|
/// </summary>
|
|
|
|
private int paletteIndex; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="level">The level in the tree = 0 - 7.</param>
|
|
|
|
/// <param name="colorBits">The number of significant color bits in the image.</param>
|
|
|
|
/// <param name="octree">The tree to which this node belongs.</param>
|
|
|
|
public OctreeNode(int level, int colorBits, Octree2 octree) |
|
|
|
{ |
|
|
|
// Construct the new node
|
|
|
|
this.leaf = level == colorBits; |
|
|
|
|
|
|
|
this.red = this.green = this.blue = this.alpha = 0; |
|
|
|
this.pixelCount = 0; |
|
|
|
|
|
|
|
// If a leaf, increment the leaf count
|
|
|
|
if (this.leaf) |
|
|
|
{ |
|
|
|
octree.Leaves++; |
|
|
|
this.NextReducible = null; |
|
|
|
this.children = null; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Otherwise add this to the reducible nodes
|
|
|
|
this.NextReducible = octree.ReducibleNodes[level]; |
|
|
|
octree.ReducibleNodes[level] = this; |
|
|
|
this.children = new OctreeNode[16]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the next reducible node
|
|
|
|
/// </summary>
|
|
|
|
public OctreeNode? NextReducible |
|
|
|
{ |
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
get; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Add a color into the tree
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color to add.</param>
|
|
|
|
/// <param name="colorBits">The number of significant color bits.</param>
|
|
|
|
/// <param name="level">The level in the tree.</param>
|
|
|
|
/// <param name="octree">The tree to which this node belongs.</param>
|
|
|
|
public void AddColor(Rgba32 color, int colorBits, int level, Octree2 octree) |
|
|
|
{ |
|
|
|
// Update the color information if this is a leaf
|
|
|
|
if (this.leaf) |
|
|
|
{ |
|
|
|
this.Increment(color, octree); |
|
|
|
|
|
|
|
// Setup the previous node
|
|
|
|
octree.TrackPrevious(this); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Go to the next level down in the tree
|
|
|
|
int index = GetColorIndex(color, level); |
|
|
|
|
|
|
|
OctreeNode? child = this.children![index]; |
|
|
|
if (child is null) |
|
|
|
{ |
|
|
|
// Create a new child node and store it in the array
|
|
|
|
child = new OctreeNode(level + 1, colorBits, octree); |
|
|
|
this.children[index] = child; |
|
|
|
} |
|
|
|
|
|
|
|
// Add the color to the child node
|
|
|
|
child.AddColor(color, colorBits, level + 1, octree); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reduce this node by removing all of its children.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>The number of leaves removed</returns>
|
|
|
|
/// <param name="octree">The tree to which this node belongs.</param>
|
|
|
|
public int Reduce(Octree2 octree) |
|
|
|
{ |
|
|
|
if (this.leaf) |
|
|
|
{ |
|
|
|
return 1; |
|
|
|
} |
|
|
|
|
|
|
|
int childNodes = 0; |
|
|
|
int sumRed = 0, sumGreen = 0, sumBlue = 0, sumAlpha = 0, pixelCount = 0; |
|
|
|
|
|
|
|
// Loop through all children.
|
|
|
|
for (int index = 0; index < this.children!.Length; index++) |
|
|
|
{ |
|
|
|
OctreeNode? child = this.children[index]; |
|
|
|
if (child != null) |
|
|
|
{ |
|
|
|
childNodes++; |
|
|
|
|
|
|
|
sumRed += child.red; |
|
|
|
sumGreen += child.green; |
|
|
|
sumBlue += child.blue; |
|
|
|
sumAlpha += child.alpha; |
|
|
|
pixelCount += child.pixelCount; |
|
|
|
|
|
|
|
// Remove the child reference.
|
|
|
|
this.children[index] = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (pixelCount > 0) |
|
|
|
{ |
|
|
|
int offset = pixelCount >> 1; |
|
|
|
this.red = sumRed; |
|
|
|
this.green = sumGreen; |
|
|
|
this.blue = sumBlue; |
|
|
|
this.alpha = ((sumAlpha + offset) / pixelCount < octree.transparencyThreshold) ? 0 : sumAlpha; |
|
|
|
this.pixelCount = pixelCount; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.red = this.green = this.blue = this.alpha = 0; |
|
|
|
this.pixelCount = 0; |
|
|
|
} |
|
|
|
|
|
|
|
// Convert this node into a leaf.
|
|
|
|
this.leaf = true; |
|
|
|
|
|
|
|
// Return the number of nodes merged (for decrementing the leaf count).
|
|
|
|
return childNodes - 1; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Traverse the tree, building up the color palette
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="palette">The palette</param>
|
|
|
|
/// <param name="index">The current palette index</param>
|
|
|
|
[MethodImpl(InliningOptions.ColdPath)] |
|
|
|
public void ConstructPalette(Span<TPixel> palette, ref int index) |
|
|
|
{ |
|
|
|
if (this.leaf) |
|
|
|
{ |
|
|
|
// Set the color of the palette entry
|
|
|
|
Vector4 sum = new(this.red, this.green, this.blue, this.alpha); |
|
|
|
Vector4 offset = new(this.pixelCount >> 1); |
|
|
|
|
|
|
|
Vector4 vector = Vector4.Clamp( |
|
|
|
(sum + offset) / this.pixelCount, |
|
|
|
Vector4.Zero, |
|
|
|
new Vector4(255)); |
|
|
|
|
|
|
|
palette[index] = TPixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, (byte)vector.W)); |
|
|
|
|
|
|
|
// Consume the next palette index
|
|
|
|
this.paletteIndex = index++; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Loop through children looking for leaves
|
|
|
|
for (int i = 0; i < this.children!.Length; i++) |
|
|
|
{ |
|
|
|
this.children[i]?.ConstructPalette(palette, ref index); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Return the palette index for the passed color
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="pixel">The pixel data.</param>
|
|
|
|
/// <param name="level">The level.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The <see cref="int"/> representing the index of the pixel in the palette.
|
|
|
|
/// </returns>
|
|
|
|
[MethodImpl(InliningOptions.ColdPath)] |
|
|
|
public int GetPaletteIndex(Rgba32 pixel, int level) |
|
|
|
{ |
|
|
|
if (this.leaf) |
|
|
|
{ |
|
|
|
return this.paletteIndex; |
|
|
|
} |
|
|
|
|
|
|
|
int colorIndex = GetColorIndex(pixel, level); |
|
|
|
OctreeNode? child = this.children![colorIndex]; |
|
|
|
|
|
|
|
int index = -1; |
|
|
|
if (child != null) |
|
|
|
{ |
|
|
|
index = child.GetPaletteIndex(pixel, level + 1); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
// Check other children.
|
|
|
|
for (int i = 0; i < this.children.Length; i++) |
|
|
|
{ |
|
|
|
child = this.children[i]; |
|
|
|
if (child != null) |
|
|
|
{ |
|
|
|
int childIndex = child.GetPaletteIndex(pixel, level + 1); |
|
|
|
if (childIndex != -1) |
|
|
|
{ |
|
|
|
return childIndex; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return index; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the color index at the given level.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The color.</param>
|
|
|
|
/// <param name="level">The node level.</param>
|
|
|
|
/// <returns>The <see cref="int"/> index.</returns>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
private static int GetColorIndex(Rgba32 color, int level) |
|
|
|
{ |
|
|
|
int shift = 7 - level; |
|
|
|
byte mask = (byte)(1 << shift); |
|
|
|
|
|
|
|
// Compute luminance of the RGB channels.
|
|
|
|
int luminance = ColorNumerics.Get8BitBT709Luminance(color.R, color.G, color.B); |
|
|
|
|
|
|
|
// Shift the threshold (arbitrary) right by the current level.
|
|
|
|
// This allows us to partition the RGB space into smaller and smaller cubes
|
|
|
|
// with increasing accuracy for the alpha component.
|
|
|
|
int darkThreshold = 24 >> level; |
|
|
|
|
|
|
|
// For fully opaque and bright pixels, ignore the alpha bit to achieve finer RGB partitioning.
|
|
|
|
// For dark pixels, include the alpha bit so that dark drop shadows remain distinct.
|
|
|
|
if (color.A == 255 && luminance > darkThreshold) |
|
|
|
{ |
|
|
|
int rBits = ((color.R & mask) >> shift) << 2; |
|
|
|
int gBits = ((color.G & mask) >> shift) << 1; |
|
|
|
int bBits = (color.B & mask) >> shift; |
|
|
|
return rBits | gBits | bBits; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
int aBits = ((color.A & mask) >> shift) << 3; |
|
|
|
int rBits = ((color.R & mask) >> shift) << 2; |
|
|
|
int gBits = ((color.G & mask) >> shift) << 1; |
|
|
|
int bBits = (color.B & mask) >> shift; |
|
|
|
return aBits | rBits | gBits | bBits; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Increment the color count and add to the color information
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="color">The pixel to add.</param>
|
|
|
|
/// <param name="octree">The parent octree.</param>
|
|
|
|
[MethodImpl(InliningOptions.ShortMethod)] |
|
|
|
public void Increment(Rgba32 color, Octree2 octree) |
|
|
|
{ |
|
|
|
this.pixelCount++; |
|
|
|
this.red += color.R; |
|
|
|
this.green += color.G; |
|
|
|
this.blue += color.B; |
|
|
|
|
|
|
|
int sumAlpha = this.alpha + color.A; |
|
|
|
int offset = this.pixelCount >> 1; |
|
|
|
this.alpha = ((sumAlpha + offset) / this.pixelCount < octree.transparencyThreshold) ? 0 : sumAlpha; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|