diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index a3bf2cafb..4c76a5c00 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -9,7 +9,7 @@ namespace ImageProcessorCore.Formats using System.IO; using System.Threading.Tasks; - using ImageProcessorCore.Quantizers; + using Quantizers; /// /// Performs the png encoding operation. @@ -218,11 +218,12 @@ namespace ImageProcessorCore.Formats if (this.Quantizer == null) { - this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; + //this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; + this.Quantizer = new OctreeQuantizer { Threshold = this.Threshold }; } - // Quantize the image returning a palette. - QuantizedImage quantized = Quantizer.Quantize(image, this.Quality); + // Quantize the image returning a palette. This boxing is icky. + QuantizedImage quantized = ((IQuantizer)this.Quantizer).Quantize(image, this.Quality); // Grab the palette and write it to the stream. T[] palette = quantized.Palette; @@ -233,7 +234,7 @@ namespace ImageProcessorCore.Formats byte[] colorTable = new byte[colorTableLength]; Parallel.For( - 0, + 0, pixelCount, Bootstrapper.Instance.ParallelOptions, i => diff --git a/src/ImageProcessorCore/Quantizers/IQuantizer.cs b/src/ImageProcessorCore/Quantizers/IQuantizer.cs index 336582d5a..f6169b429 100644 --- a/src/ImageProcessorCore/Quantizers/IQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/IQuantizer.cs @@ -8,25 +8,31 @@ namespace ImageProcessorCore.Quantizers /// /// Provides methods for allowing quantization of images pixels. /// - public interface IQuantizer + /// The pixel format. + /// The packed format. long, float. + public interface IQuantizer : IQuantizer + where T : IPackedVector + where TP : struct { - /// - /// Gets or sets the transparency threshold. - /// - byte Threshold { get; set; } - /// /// Quantize an image and return the resulting output pixels. /// - /// The pixel format. - /// The packed format. long, float. /// The image to quantize. /// The maximum number of colors to return. /// /// A representing a quantized version of the image pixels. /// - QuantizedImage Quantize(ImageBase image, int maxColors) - where T : IPackedVector - where TP : struct; + QuantizedImage Quantize(ImageBase image, int maxColors); + } + + /// + /// Provides methods for allowing quantization of images pixels. + /// + public interface IQuantizer + { + /// + /// Gets or sets the transparency threshold. + /// + byte Threshold { get; set; } } } diff --git a/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs new file mode 100644 index 000000000..9d94869ce --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs @@ -0,0 +1,538 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Encapsulates methods to calculate the colour palette if an image using an Octree pattern. + /// + /// + /// The pixel format. + /// The packed format. long, float. + public sealed class OctreeQuantizer : Quantizer + where T : IPackedVector + where TP : struct + { + /// + /// 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(T 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(T 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.ToBytes()[3] > 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)); + + int diff = this.colors - palette.Count; + palette.AddRange(Enumerable.Repeat(default(T), diff)); + 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 TP 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 = default(TP); + 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(T pixel) + { + TP packed = pixel.PackedValue(); + // Check if this request is for the same color as the last + if (this.previousColor.Equals(packed)) + { + // 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 = packed; + this.root.AddColor(pixel, this.maxColorBits, 0, this); + } + else + { + // Just update the previous node + this.previousNode.Increment(pixel); + } + } + else + { + this.previousColor = packed; + 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(T 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(T 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; + byte[] components = pixel.ToBytes(); + int index = ((components[2] & Mask[level]) >> (shift - 2)) | + ((components[1] & Mask[level]) >> (shift - 1)) | + ((components[0] & 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 + T pixel = default(T); + pixel.PackBytes(r, g, b, 255); + palette.Add(pixel); + } + 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(T pixel, int level) + { + int index = this.paletteIndex; + + if (!this.leaf) + { + int shift = 7 - level; + byte[] components = pixel.ToBytes(); + int pixelIndex = ((components[2] & Mask[level]) >> (shift - 2)) | + ((components[1] & Mask[level]) >> (shift - 1)) | + ((components[0] & Mask[level]) >> shift); + + if (this.children[pixelIndex] != null) + { + index = this.children[pixelIndex].GetPaletteIndex(pixel, level + 1); + } + else + { + throw new Exception($"Cannot retrive a pixel at the given index {pixelIndex}."); + } + } + + return index; + } + + /// + /// Increment the pixel count and add to the color information + /// + /// + /// The pixel to add. + /// + public void Increment(T pixel) + { + this.pixelCount++; + byte[] components = pixel.ToBytes(); + this.red += components[0]; + this.green += components[1]; + this.blue += components[2]; + } + } + } + } +} diff --git a/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs new file mode 100644 index 000000000..57ea9203a --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + using System.Collections.Generic; + using System.Threading.Tasks; + + /// + /// Encapsulates methods to calculate the color palette of an image. + /// + public abstract class Quantizer : IQuantizer + where T : IPackedVector + where TP : struct + { + /// + /// Flag used to indicate whether a single pass or two passes are needed for quantization. + /// + private readonly bool singlePass; + + /// + /// Initializes a new instance of the class. + /// + /// + /// If true, the quantization only needs to loop through the source pixels once + /// + /// + /// If you construct this class with a true value for singlePass, then the code will, when quantizing your image, + /// only call the 'QuantizeImage' function. If two passes are required, the code will call 'InitialQuantizeImage' + /// and then 'QuantizeImage'. + /// + protected Quantizer(bool singlePass) + { + this.singlePass = singlePass; + } + + /// + /// Gets or sets the transparency index. + /// + public int TransparentIndex { get; protected set; } = -1; + + /// + public byte Threshold { get; set; } + + /// + public virtual QuantizedImage Quantize(ImageBase image, int maxColors) + { + Guard.NotNull(image, nameof(image)); + + // Get the size of the source image + int height = image.Height; + int width = image.Width; + byte[] quantizedPixels = new byte[width * height]; + List palette; + + using (IPixelAccessor pixels = image.Lock()) + { + // 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(pixels, width, height); + } + + // Get the palette + palette = this.GetPalette(); + + this.SecondPass(pixels, quantizedPixels, width, height); + } + + return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex); + } + + /// + /// 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(IPixelAccessor source, int width, int height) + { + // Loop through each row + for (int y = 0; y < height; y++) + { + // And loop through each column + 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(IPixelAccessor source, byte[] output, int width, int height) + { + Parallel.For( + 0, + source.Height, + Bootstrapper.Instance.ParallelOptions, + y => + { + for (int x = 0; x < source.Width; x++) + { + T sourcePixel = source[x, y]; + output[(y * source.Width) + x] = this.QuantizePixel(sourcePixel); + } + }); + } + + /// + /// 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(T 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(T pixel); + + /// + /// Retrieve the palette for the quantized image + /// + /// + /// The new color palette + /// + protected abstract List GetPalette(); + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs index 31225f630..8b6e72e22 100644 --- a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs @@ -30,7 +30,11 @@ namespace ImageProcessorCore.Quantizers /// but more expensive versions. /// /// - public sealed class WuQuantizer : IQuantizer + /// The pixel format. + /// The packed format. long, float. + public sealed class WuQuantizer : IQuantizer + where T : IPackedVector + where TP : struct { /// /// The epsilon for comparing floating point numbers. @@ -98,7 +102,7 @@ namespace ImageProcessorCore.Quantizers private readonly byte[] tag; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public WuQuantizer() { @@ -115,9 +119,7 @@ namespace ImageProcessorCore.Quantizers public byte Threshold { get; set; } /// - public QuantizedImage Quantize(ImageBase image, int maxColors) - where T : IPackedVector - where TP : struct + public QuantizedImage Quantize(ImageBase image, int maxColors) { Guard.NotNull(image, nameof(image)); @@ -322,12 +324,8 @@ namespace ImageProcessorCore.Quantizers /// /// Builds a 3-D color histogram of counts, r/g/b, c^2. /// - /// The pixel format. - /// The packed format. long, float. /// The pixel accessor. - private void Build3DHistogram(IPixelAccessor pixels) - where T : IPackedVector - where TP : struct + private void Build3DHistogram(IPixelAccessor pixels) { for (int y = 0; y < pixels.Height; y++) { @@ -721,15 +719,11 @@ namespace ImageProcessorCore.Quantizers /// /// Generates the quantized result. /// - /// The pixel format. - /// The packed format. long, float. /// The image pixels. /// The color count. /// The cube. /// The result. - private QuantizedImage GenerateResult(IPixelAccessor imagePixels, int colorCount, Box[] cube) - where T : IPackedVector - where TP : struct + private QuantizedImage GenerateResult(IPixelAccessor imagePixels, int colorCount, Box[] cube) { List pallette = new List(); byte[] pixels = new byte[imagePixels.Width * imagePixels.Height]; @@ -793,7 +787,6 @@ namespace ImageProcessorCore.Quantizers } }); - return new QuantizedImage(width, height, pallette.ToArray(), pixels, transparentIndex); } }