// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageProcessorCore.Quantizers { using System; using System.Collections.Generic; /// /// Encapsulates methods to calculate the colour palette if an image using an Octree pattern. /// /// public sealed class OctreeQuantizer : Quantizer { /// /// Stores the tree /// private Octree octree; /// /// Maximum allowed color depth /// private int colors; /// /// Initializes a new instance of the class. /// /// /// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree, /// the second pass quantizes a color based on the nodes in the tree /// public OctreeQuantizer() : base(false) { } /// public override QuantizedImage Quantize(ImageBase image, int maxColors) { this.colors = maxColors.Clamp(1, 255); if (this.octree == null) { // Construct the Octree this.octree = new Octree(this.GetBitsNeededForColorDepth(maxColors)); } return base.Quantize(image, maxColors); } /// /// Process the pixel in the first pass of the algorithm /// /// /// The pixel to quantize /// /// /// This function need only be overridden if your quantize algorithm needs two passes, /// such as an Octree quantizer. /// protected override void InitialQuantizePixel(Bgra32 pixel) { // Add the color to the Octree this.octree.AddColor(pixel); } /// /// Override this to process the pixel in the second pass of the algorithm /// /// /// The pixel to quantize /// /// /// The quantized value /// protected override byte QuantizePixel(Bgra32 pixel) { // The color at [maxColors] is set to transparent byte paletteIndex = (byte)this.colors; // Get the palette index if it's transparency meets criterea. if (pixel.A > this.Threshold) { paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); } return paletteIndex; } /// /// Retrieve the palette for the quantized image. /// /// /// The new color palette /// protected override List GetPalette() { // First off convert the Octree to maxColors colors List palette = this.octree.Palletize(Math.Max(this.colors, 1)); palette.Add(Bgra32.Empty); this.TransparentIndex = this.colors; return palette; } /// /// Returns how many bits are required to store the specified number of colors. /// Performs a Log2() on the value. /// /// The number of colors. /// /// The /// private int GetBitsNeededForColorDepth(int colorCount) { return (int)Math.Ceiling(Math.Log(colorCount, 2)); } /// /// Class which does the actual quantization /// private class Octree { /// /// Mask used when getting the appropriate pixels for a given node /// private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; /// /// The root of the Octree /// private readonly OctreeNode root; /// /// Array of reducible nodes /// private readonly OctreeNode[] reducibleNodes; /// /// Maximum number of significant bits in the image /// private readonly int maxColorBits; /// /// Store the last node quantized /// private OctreeNode previousNode; /// /// Cache the previous color quantized /// private int previousColor; /// /// Initializes a new instance of the class. /// /// /// The maximum number of significant bits in the image /// public Octree(int maxColorBits) { this.maxColorBits = maxColorBits; this.Leaves = 0; this.reducibleNodes = new OctreeNode[9]; this.root = new OctreeNode(0, this.maxColorBits, this); this.previousColor = 0; this.previousNode = null; } /// /// Gets or sets the number of leaves in the tree /// private int Leaves { get; set; } /// /// Gets the array of reducible nodes /// private OctreeNode[] ReducibleNodes => this.reducibleNodes; /// /// Add a given color value to the Octree /// /// /// The containing color information to add. /// public void AddColor(Bgra32 pixel) { // Check if this request is for the same color as the last if (this.previousColor == pixel.Bgra) { // If so, check if I have a previous node setup. This will only occur if the first color in the image // happens to be black, with an alpha component of zero. if (this.previousNode == null) { this.previousColor = pixel.Bgra; this.root.AddColor(pixel, this.maxColorBits, 0, this); } else { // Just update the previous node this.previousNode.Increment(pixel); } } else { this.previousColor = pixel.Bgra; this.root.AddColor(pixel, this.maxColorBits, 0, this); } } /// /// Convert the nodes in the Octree to a palette with a maximum of colorCount colors /// /// /// The maximum number of colors /// /// /// An with the palletized colors /// public List Palletize(int colorCount) { while (this.Leaves > colorCount) { this.Reduce(); } // Now palletize the nodes List palette = new List(this.Leaves); int paletteIndex = 0; this.root.ConstructPalette(palette, ref paletteIndex); // And return the palette return palette; } /// /// Get the palette index for the passed color /// /// /// The containing the pixel data. /// /// /// The index of the given structure. /// public int GetPaletteIndex(Bgra32 pixel) { return this.root.GetPaletteIndex(pixel, 0); } /// /// Keep track of the previous node that was quantized /// /// /// The node last quantized /// protected 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] == 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(); // 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 /// protected 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; /// /// 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, Octree octree) { // Construct the new node this.leaf = level == colorBits; this.red = this.green = this.blue = 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[8]; } } /// /// Gets the next reducible node /// public OctreeNode NextReducible { get; } /// /// Add a color into the tree /// /// /// The color /// /// /// The number of significant color bits /// /// /// The level in the tree /// /// /// The tree to which this node belongs /// public void AddColor(Bgra32 pixel, int colorBits, int level, Octree octree) { // Update the color information if this is a leaf if (this.leaf) { this.Increment(pixel); // Setup the previous node octree.TrackPrevious(this); } else { // Go to the next level down in the tree int shift = 7 - level; int index = ((pixel.R & Mask[level]) >> (shift - 2)) | ((pixel.G & Mask[level]) >> (shift - 1)) | ((pixel.B & Mask[level]) >> shift); OctreeNode child = this.children[index]; if (child == 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(pixel, colorBits, level + 1, octree); } } /// /// Reduce this node by removing all of its children /// /// The number of leaves removed public int Reduce() { this.red = this.green = this.blue = 0; int childNodes = 0; // Loop through all children and add their information to this node for (int index = 0; index < 8; index++) { if (this.children[index] != null) { this.red += this.children[index].red; this.green += this.children[index].green; this.blue += this.children[index].blue; this.pixelCount += this.children[index].pixelCount; ++childNodes; this.children[index] = null; } } // Now change this to a leaf node this.leaf = true; // Return the number of nodes to decrement the leaf count by return childNodes - 1; } /// /// Traverse the tree, building up the color palette /// /// /// The palette /// /// /// The current palette index /// public void ConstructPalette(List palette, ref int index) { if (this.leaf) { // Consume the next palette index this.paletteIndex = index++; byte r = (this.red / this.pixelCount).ToByte(); byte g = (this.green / this.pixelCount).ToByte(); byte b = (this.blue / this.pixelCount).ToByte(); // And set the color of the palette entry palette.Add(new Bgra32(b, g, r)); } else { // Loop through children looking for leaves for (int i = 0; i < 8; i++) { if (this.children[i] != null) { this.children[i].ConstructPalette(palette, ref index); } } } } /// /// Return the palette index for the passed color /// /// /// The representing the pixel. /// /// /// The level. /// /// /// The representing the index of the pixel in the palette. /// public int GetPaletteIndex(Bgra32 pixel, int level) { int index = this.paletteIndex; if (!this.leaf) { int shift = 7 - level; int pixelIndex = ((pixel.R & Mask[level]) >> (shift - 2)) | ((pixel.G & Mask[level]) >> (shift - 1)) | ((pixel.B & Mask[level]) >> shift); if (this.children[pixelIndex] != null) { index = this.children[pixelIndex].GetPaletteIndex(pixel, level + 1); } else { throw new Exception("Didn't expect this!"); } } return index; } /// /// Increment the pixel count and add to the color information /// /// /// The pixel to add. /// public void Increment(Bgra32 pixel) { this.pixelCount++; this.red += pixel.R; this.green += pixel.G; this.blue += pixel.B; } } } } }