diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index be00bc433a..65e814e620 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -654,479 +654,4 @@ public struct OctreeQuantizer : IQuantizer } } } - - /// - /// Class which does the actual quantization. - /// - private sealed class Octree2 - { - /// - /// The root of the Octree2 - /// - private readonly OctreeNode root; - - /// - /// Maximum number of significant bits in the image - /// - private readonly int maxColorBits; - - /// - /// The threshold for transparent colors. - /// - private readonly int transparencyThreshold; - - /// - /// Store the last node quantized - /// - private OctreeNode? previousNode; - - /// - /// Cache the previous color quantized - /// - private Rgba32 previousColor; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The maximum number of significant bits in the image - /// - /// The threshold for transparent colors. - 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; - } - - /// - /// Gets or sets the number of leaves in the tree - /// - public int Leaves - { - [MethodImpl(InliningOptions.ShortMethod)] - get; - - [MethodImpl(InliningOptions.ShortMethod)] - set; - } - - /// - /// Gets the array of reducible nodes - /// - private OctreeNode?[] ReducibleNodes - { - [MethodImpl(InliningOptions.ShortMethod)] - get; - } - - /// - /// Add a given color value to the Octree2 - /// - /// The color to add. - 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); - } - } - - /// - /// Convert the nodes in the Octree2 to a palette with a maximum of colorCount colors - /// - /// The palette to fill. - /// The maximum number of colors - /// The palette index, used to calculate the final size of the palette. - [MethodImpl(InliningOptions.ShortMethod)] - public void Palettize(Span palette, int colorCount, ref int paletteIndex) - { - while (this.Leaves > colorCount) - { - this.Reduce(); - } - - this.root.ConstructPalette(palette, ref paletteIndex); - } - - /// - /// Get the palette index for the passed color - /// - /// The color to match. - /// - /// The index. - /// - [MethodImpl(InliningOptions.ShortMethod)] - public int GetPaletteIndex(TPixel color) - => this.root.GetPaletteIndex(color.ToRgba32(), 0); - - /// - /// Keep track of the previous node that was quantized - /// - /// - /// The node last quantized - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void TrackPrevious(OctreeNode node) => this.previousNode = node; - - /// - /// Reduce the depth of the tree - /// - 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; - } - - /// - /// Class which encapsulates each node in the tree - /// - public sealed class OctreeNode - { - /// - /// Pointers to any child nodes - /// - private readonly OctreeNode?[]? children; - - /// - /// Flag indicating that this is a leaf node - /// - private bool leaf; - - /// - /// Number of pixels in this node - /// - private int pixelCount; - - /// - /// Red component - /// - private int red; - - /// - /// Green Component - /// - private int green; - - /// - /// Blue component - /// - private int blue; - - /// - /// Alpha component - /// - private int alpha; - - /// - /// The index of this node in the palette - /// - private int paletteIndex; - - /// - /// Initializes a new instance of the class. - /// - /// The level in the tree = 0 - 7. - /// The number of significant color bits in the image. - /// The tree to which this node belongs. - 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]; - } - } - - /// - /// Gets the next reducible node - /// - public OctreeNode? NextReducible - { - [MethodImpl(InliningOptions.ShortMethod)] - get; - } - - /// - /// Add a color into the tree - /// - /// The color to add. - /// The number of significant color bits. - /// The level in the tree. - /// The tree to which this node belongs. - 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); - } - } - - /// - /// Reduce this node by removing all of its children. - /// - /// The number of leaves removed - /// The tree to which this node belongs. - 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; - } - - /// - /// Traverse the tree, building up the color palette - /// - /// The palette - /// The current palette index - [MethodImpl(InliningOptions.ColdPath)] - public void ConstructPalette(Span 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); - } - } - } - - /// - /// Return the palette index for the passed color - /// - /// The pixel data. - /// The level. - /// - /// The representing the index of the pixel in the palette. - /// - [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; - } - - /// - /// Gets the color index at the given level. - /// - /// The color. - /// The node level. - /// The index. - [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; - } - } - - /// - /// Increment the color count and add to the color information - /// - /// The pixel to add. - /// The parent octree. - [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; - } - } - } }