From d1a69bb3cc60e2a967efa231150372e7bffe7263 Mon Sep 17 00:00:00 2001 From: James South Date: Fri, 1 May 2015 22:39:29 +0100 Subject: [PATCH] *Almost* translated quantizer. Former-commit-id: c8c139fe7e6cecd77978de44d85287d1658a2828 Former-commit-id: b32ac3200f0943841750e7b4e7d8edb167478a63 Former-commit-id: e5ae0846f3316b14fa3909fc05c3c71d8b3adb67 --- src/ImageProcessor/Formats/Gif/GifEncoder.cs | 2 +- .../Formats/Gif/Quantizer/IQuantizer.cs | 4 +- .../Formats/Gif/Quantizer/OctreeQuantizer.cs | 493 +++++++++++++++++- .../Formats/Gif/Quantizer/Quantizer.cs | 95 +++- src/ImageProcessor/Formats/Jpg/JpegEncoder.cs | 8 +- .../Formats/Jpg/LibJpeg/BitStream.cs | 8 +- 6 files changed, 593 insertions(+), 17 deletions(-) diff --git a/src/ImageProcessor/Formats/Gif/GifEncoder.cs b/src/ImageProcessor/Formats/Gif/GifEncoder.cs index 64d210872..8d630f808 100644 --- a/src/ImageProcessor/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifEncoder.cs @@ -85,7 +85,7 @@ namespace ImageProcessor.Formats this.WriteShort(stream, descriptor.Width); int size = descriptor.GlobalColorTableSize; int bitdepth = this.GetBitsNeededForColorDepth(size) - 1; - int packed = 0x80 | // 1 : global color table flag = 1 (GCT used) + int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) 0x70 | // 2-4 : color resolution 0x00 | // 5 : GCT sort flag = 0 bitdepth; // 6-8 : GCT size assume 1:1 diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/IQuantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/IQuantizer.cs index f9867d674..bdc1ca27e 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/IQuantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/IQuantizer.cs @@ -18,10 +18,10 @@ namespace ImageProcessor.Formats /// /// Quantize an image and return the resulting output pixels. /// - /// The image to quantize. + /// The image to quantize. /// /// A representing a quantized version of the image pixels. /// - byte[] Quantize(ImageBase image); + byte[] Quantize(ImageBase imageBase); } } diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs index 838caf57c..368e5ace9 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs @@ -11,12 +11,20 @@ namespace ImageProcessor.Formats { + using System; + using System.Collections.Generic; + /// - /// Encapsulates methods to calculate the color palette of an image using an Octree pattern. + /// Encapsulates methods to calculate the colour palette if an image using an Octree pattern. /// /// public class OctreeQuantizer : Quantizer { + /// + /// Stores the tree + /// + private readonly Octree octree; + /// /// Maximum allowed color depth /// @@ -50,6 +58,489 @@ namespace ImageProcessor.Formats : base(false) { Guard.LessEquals(maxColors, 255, "maxColors"); + Guard.BetweenEquals(maxColorBits, 1, 8, "maxColorBits"); + + // 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(Bgra 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(Bgra 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. + /// + /// + /// The new color palette + /// + protected override List GetPalette() + { + // First off convert the Octree to maxColors colors + List palette = this.octree.Palletize(this.maxColors - 1); + + // Add empty color for transparency + palette.Add(Bgra.Empty); + + return palette; + } + + /// + /// 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 => this.reducibleNodes; + + /// + /// Add a given color value to the Octree + /// + /// + /// The containing color information to add. + /// + public void AddColor(Bgra 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 (null == this.previousNode) + { + 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(Bgra 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; + + /// + /// 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(Bgra 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(List palette, ref int index) + { + if (this.leaf) + { + // Consume the next palette index + this.paletteIndex = index++; + + byte r = (byte)(this.red / this.pixelCount).Clamp(0, 255); + byte g = (byte)(this.green / this.pixelCount).Clamp(0, 255); + byte b = (byte)(this.blue / this.pixelCount).Clamp(0, 255); + + // And set the color of the palette entry + palette.Add(new Bgra(b, g, r)); + } + 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(Bgra 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(Bgra pixel) + { + this.pixelCount++; + this.red += pixel.R; + this.green += pixel.G; + this.blue += pixel.B; + } + } } } } diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs index 002715e52..38f16bb88 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs @@ -10,6 +10,8 @@ namespace ImageProcessor.Formats { + using System.Collections.Generic; + /// /// Encapsulates methods to calculate the color palette of an image. /// @@ -39,13 +41,102 @@ namespace ImageProcessor.Formats /// /// Quantize an image and return the resulting output pixels. /// - /// The image to quantize. + /// The image to quantize. /// /// A representing a quantized version of the image pixels. /// - public byte[] Quantize(ImageBase image) + public byte[] Quantize(ImageBase imageBase) { + // Get the size of the source image + int height = imageBase.Height; + int width = imageBase.Width; + ImageBase copy = new ImageFrame((ImageFrame)imageBase); + + // Call the FirstPass function if not a single pass algorithm. + // For something like an Octree quantizer, this will run through + // all image pixels, build a data structure, and create a palette. + if (!this.singlePass) + { + this.FirstPass(copy, width, height); + } + throw new System.NotImplementedException(); } + + /// + /// Execute the first pass through the pixels in the image + /// + /// The source data + /// The width in pixels of the image. + /// The height in pixels of the image. + protected virtual void FirstPass(ImageBase source, int width, int height) + { + // Loop through each row + for (int y = 0; y < height; y++) + { + // And loop through each xumn + for (int x = 0; x < width; x++) + { + // Now I have the pixel, call the FirstPassQuantize function... + this.InitialQuantizePixel(source[x, y]); + } + } + } + + /// + /// Execute a second pass through the bitmap + /// + /// The source image. + /// The output pixel array + /// The width in pixels of the image + /// The height in pixels of the image + protected virtual void SecondPass(ImageBase source, byte[] output, int width, int height) + { + Bgra sourcePixel = source[0, 0]; + + // And convert the first pixel, so that I have values going into the loop + byte pixelValue = this.QuantizePixel(sourcePixel); + + output[0] = pixelValue; + + for (int y = 0; y < height; y++) + { + // TODO: Translate this from the old method. + } + + } + + /// + /// Override this to 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 virtual void InitialQuantizePixel(Bgra pixel) + { + } + + /// + /// Override this to process the pixel in the second pass of the algorithm + /// + /// + /// The pixel to quantize + /// + /// + /// The quantized value + /// + protected abstract byte QuantizePixel(Bgra pixel); + + /// + /// Retrieve the palette for the quantized image + /// + /// + /// The new color palette + /// + protected abstract List GetPalette(); } } diff --git a/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs b/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs index c9c0d056d..03a395aad 100644 --- a/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs +++ b/src/ImageProcessor/Formats/Jpg/JpegEncoder.cs @@ -87,8 +87,8 @@ namespace ImageProcessor.Formats Guard.NotNull(image, "image"); Guard.NotNull(stream, "stream"); - int pixelWidth = image.PixelWidth; - int pixelHeight = image.PixelHeight; + int pixelWidth = image.Width; + int pixelHeight = image.Height; byte[] sourcePixels = image.Pixels; @@ -101,7 +101,7 @@ namespace ImageProcessor.Formats for (int x = 0; x < pixelWidth; x++) { int start = x * 3; - int source = (y * pixelWidth + x) * 4; + int source = ((y * pixelWidth) + x) * 4; samples[start] = sourcePixels[source + 2]; samples[start + 1] = sourcePixels[source + 1]; @@ -112,7 +112,7 @@ namespace ImageProcessor.Formats } JpegImage jpg = new JpegImage(rows, Colorspace.RGB); - jpg.WriteJpeg(stream, new CompressionParameters { Quality = Quality }); + jpg.WriteJpeg(stream, new CompressionParameters { Quality = this.Quality }); } #endregion diff --git a/src/ImageProcessor/Formats/Jpg/LibJpeg/BitStream.cs b/src/ImageProcessor/Formats/Jpg/LibJpeg/BitStream.cs index 9885e4170..2bdaae68c 100644 --- a/src/ImageProcessor/Formats/Jpg/LibJpeg/BitStream.cs +++ b/src/ImageProcessor/Formats/Jpg/LibJpeg/BitStream.cs @@ -91,13 +91,7 @@ namespace ImageProcessor.Formats /// /// Gets the underlying stream. /// - public Stream UnderlyingStream - { - get - { - return this.stream; - } - } + public Stream UnderlyingStream => this.stream; /// /// Disposes the object and frees resources for the Garbage Collector.