// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Encapsulates methods to calculate the colour palette if an image using an Octree pattern. // // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Imaging.Quantizers { using System; using System.Collections; using System.Drawing; using System.Drawing.Imaging; using ImageProcessor.Imaging.Colors; /// /// Encapsulates methods to calculate the colour palette if an image using an Octree pattern. /// /// public unsafe class OctreeQuantizer : Quantizer { /// /// Stores the tree /// private readonly Octree octree; /// /// Maximum allowed color depth /// private readonly int maxColors; /// /// 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. /// /// Defaults to return a maximum of 255 colors plus transparency with 8 significant bits. /// /// public OctreeQuantizer() : this(255, 8) { } /// /// 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 /// /// /// The maximum number of colors to return /// /// /// The number of significant bits /// public OctreeQuantizer(int maxColors, int maxColorBits) : base(false) { if (maxColors > 255) { throw new ArgumentOutOfRangeException("maxColors", maxColors, "The number of colors should be less than 256"); } if ((maxColorBits < 1) | (maxColorBits > 8)) { throw new ArgumentOutOfRangeException("maxColorBits", maxColorBits, "This should be between 1 and 8"); } // Construct the Octree this.octree = new Octree(maxColorBits); this.maxColors = 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(Color32* 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(Color32* pixel) { // The color at [_maxColors] is set to transparent byte paletteIndex = (byte)this.maxColors; // Get the palette index if this non-transparent if (pixel->A > 0) { paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); } return paletteIndex; } /// /// Retrieve the palette for the quantized image /// /// /// Any old palette, this is overwritten /// /// /// The new color palette /// protected override ColorPalette GetPalette(ColorPalette original) { // First off convert the Octree to maxColors colors ArrayList palette = this.octree.Palletize(this.maxColors - 1); // Then convert the palette based on those colors for (int index = 0; index < palette.Count; index++) { original.Entries[index] = (Color)palette[index]; } // Add the transparent color original.Entries[this.maxColors] = Color.FromArgb(0, 0, 0, 0); return original; } /// /// 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; /// /// Number of leaves in the tree /// private int leafCount; /// /// 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.leafCount = 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 { return this.leafCount; } set { this.leafCount = value; } } /// /// Gets the array of reducible nodes /// private OctreeNode[] ReducibleNodes { get { return this.reducibleNodes; } } /// /// Add a given color value to the Octree /// /// /// The containing color information to add. /// public void AddColor(Color32* pixel) { // Check if this request is for the same color as the last if (this.previousColor == pixel->Argb) { // 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 (null == this.previousNode) { this.previousColor = pixel->Argb; this.root.AddColor(pixel, this.maxColorBits, 0, this); } else { // Just update the previous node this.previousNode.Increment(pixel); } } else { this.previousColor = pixel->Argb; 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 ArrayList Palletize(int colorCount) { while (this.Leaves > colorCount) { this.Reduce(); } // Now palletize the nodes ArrayList palette = new ArrayList(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(Color32* 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) && (null == this.reducibleNodes[index])) { 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.leafCount -= 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; /// /// Pointer to next reducible node /// private readonly OctreeNode nextReducible; /// /// 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 { return this.nextReducible; } } /// /// 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(Color32* 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 (null == child) { // Create a new child node & store 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 (null != this.children[index]) { 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(ArrayList palette, ref int index) { if (this.leaf) { // Consume the next palette index this.paletteIndex = index++; // And set the color of the palette entry palette.Add(Color.FromArgb(this.red / this.pixelCount, this.green / this.pixelCount, this.blue / this.pixelCount)); } else { // Loop through children looking for leaves for (int i = 0; i < 8; i++) { if (null != this.children[i]) { 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(Color32* 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 (null != this.children[pixelIndex]) { 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(Color32* pixel) { this.pixelCount++; this.red += pixel->R; this.green += pixel->G; this.blue += pixel->B; } } } } }