// ----------------------------------------------------------------------- // // Copyright (c) James South. // Dual licensed under the MIT or GPL Version 2 licenses. // // ----------------------------------------------------------------------- namespace ImageProcessor.Imaging { #region Using using System; using System.Collections; using System.Drawing; using System.Drawing.Imaging; #endregion /// /// Encapsulates methods to calculate the colour palette if an image using an octree pattern. /// internal class OctreeQuantizer : Quantizer { #region Fields /// /// Stores the tree. /// private readonly Octree octree; /// /// The maximum allowed color depth. /// private readonly int maxColors; #endregion /// /// Initializes a new instance of the OctreeQuantizer class. /// /// /// The Octree quantizer is a two pass algorithm. The initial pass sets up the octree, /// the second pass quantizes a colour based on the nodes in the tree /// /// The maximum number of colours to return, maximum 255. /// The number of significant bits minimum 1, maximum 8. public OctreeQuantizer(int maxColors, int maxColorBits) : base(false) { if (maxColors > 255) { throw new ArgumentOutOfRangeException("maxColors", maxColors, "The number of colours 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 colour 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 colour at [this.maxColors] is set to transparent byte paletteIndex; // Get the palette index if this non-transparent if (pixel.Alpha > 0) { paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); } else { paletteIndex = (byte)this.maxColors; } return paletteIndex; } /// /// Retrieve the palette for the quantized image /// /// Any old palette, this is overwritten /// The new colour palette protected override ColorPalette GetPalette(ColorPalette original) { // First off convert the octree to this.maxColors colours ArrayList palette = this.octree.Palletize(this.maxColors - 1); // Then convert the palette based on those colours for (int index = 0; index < palette.Count; index++) { original.Entries[index] = (Color)palette[index]; } // Add the transparent colour original.Entries[this.maxColors] = Color.FromArgb(0, 0, 0, 0); return original; } /// /// Describes a tree data structure in which each internal node has exactly eight children. /// private class Octree { #region Fields /// /// Mask used when getting the appropriate pixels for a given node /// private static int[] mask = new int[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; /// /// The root of the octree /// private OctreeNode root; /// /// Number of leaves in the tree /// private int leafCount; /// /// Array of reducible nodes /// private OctreeNode[] reducibleNodes; /// /// Maximum number of significant bits in the image /// private int maxColorBits; /// /// Store the last node quantized /// private OctreeNode previousNode; /// /// Cache the previous color quantized /// private int previousColor; #endregion #region Constructors /// /// Initializes a new instance of the Octree class. /// /// The maximum number of significant bits in the image public Octree(int maxBits) { this.maxColorBits = maxBits; this.leafCount = 0; this.reducibleNodes = new OctreeNode[9]; this.root = new OctreeNode(0, this.maxColorBits, this); this.previousColor = 0; this.previousNode = null; } #endregion #region Properties /// /// Gets or sets the number of leaves in the tree /// public int Leaves { get { return this.leafCount; } set { this.leafCount = value; } } /// /// Gets the array of reducible nodes /// protected OctreeNode[] ReducibleNodes { get { return this.reducibleNodes; } } #endregion /// /// Add a given colour value to the octree /// /// /// The color value to add. /// public void AddColor(Color32 pixel) { // Check if this request is for the same colour 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 colour 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); } } /// /// Reduce the depth of the tree /// public 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.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; } /// /// Convert the nodes in the octree to a palette with a maximum of colorCount colours /// /// The maximum number of colours /// An array list with the palletized colours 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 colour. /// /// /// The color to return the palette index for. /// /// /// The palette index for the passed colour. /// 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; } /// /// Class which encapsulates each node in the tree /// protected class OctreeNode { #region Fields /// /// 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; /// /// Pointers to any child nodes /// private OctreeNode[] children; /// /// The index of this node in the palette /// private int paletteIndex; #endregion #region Constructors /// /// 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]; } } #endregion #region Properties /// /// Gets or the next reducible node /// public OctreeNode NextReducible { get; private set; } /// /// Gets the child nodes /// private OctreeNode[] Children { get { return this.children; } } #endregion #region Methods /// /// 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.Red & mask[level]) >> (shift - 2)) | ((pixel.Green & mask[level]) >> (shift - 1)) | ((pixel.Blue & 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 childPosition = 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; ++childPosition; 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 childPosition - 1; } /// /// Traverse the tree, building up the color palette /// /// The palette /// The current palette index public void ConstructPalette(ArrayList palette, ref int currentPaletteIndex) { if (this.leaf) { // Consume the next palette index this.paletteIndex = currentPaletteIndex++; // 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 index = 0; index < 8; index++) { if (null != this.children[index]) { this.children[index].ConstructPalette(palette, ref currentPaletteIndex); } } } } /// /// Return the palette index for the passed color. /// /// /// The pixel. /// /// /// The level. /// /// /// The palette index for the passed color. /// public int GetPaletteIndex(Color32 pixel, int level) { int currentPaletteIndex = this.paletteIndex; if (!this.leaf) { int shift = 7 - level; int index = ((pixel.Red & mask[level]) >> (shift - 2)) | ((pixel.Green & mask[level]) >> (shift - 1)) | ((pixel.Blue & mask[level]) >> shift); if (null != this.children[index]) { currentPaletteIndex = this.children[index].GetPaletteIndex(pixel, level + 1); } else { throw new Exception("Didn't expect this!"); } } return currentPaletteIndex; } /// /// Increment the pixel count and add to the color information /// /// /// The pixel. /// public void Increment(Color32 pixel) { this.pixelCount++; this.red += pixel.Red; this.green += pixel.Green; this.blue += pixel.Blue; } #endregion } } } }