From 354af45815e3c3d6d2142cdf0e514616d1b8204d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 20 Sep 2016 22:33:28 +1000 Subject: [PATCH] Fix cross-format quantization Png can now store more transparent pixels when indexed and work with all quantizers. Former-commit-id: 6a4724535829d2c73024b6b1f0235e94e25ccad1 Former-commit-id: 24e4cfff4583c97caef30369302340a3a75ea57a Former-commit-id: 56ff119c5a2a2506f06b59a23e5e13ec0c3a4688 --- .../Formats/Gif/GifEncoderCore.cs | 42 +++++++++-- .../Formats/Png/PngEncoder.cs | 8 +-- .../Formats/Png/PngEncoderCore.cs | 16 +++-- .../Quantizers/IQuantizer.cs | 4 -- .../Quantizers/Octree/OctreeQuantizer.cs | 62 +++++++++-------- .../Quantizers/Octree/Quantizer.cs | 10 +-- .../Quantizers/Palette/PaletteQuantizer.cs | 69 ++++++++----------- .../Quantizers/QuantizedImage.cs | 11 +-- .../Quantizers/Wu/WuQuantizer.cs | 29 ++------ 9 files changed, 121 insertions(+), 130 deletions(-) diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs index b73a5bab59..a9b39f4b2b 100644 --- a/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs @@ -55,7 +55,7 @@ namespace ImageProcessorCore.Formats if (this.Quantizer == null) { - this.Quantizer = new OctreeQuantizer { Threshold = this.Threshold }; + this.Quantizer = new OctreeQuantizer(); } // Do not use IDisposable pattern here as we want to preserve the stream. @@ -71,14 +71,16 @@ namespace ImageProcessorCore.Formats // Quantize the image returning a palette. QuantizedImage quantized = ((IQuantizer)this.Quantizer).Quantize(image, this.Quality); + int index = GetTransparentIndex(quantized); + // Write the header. this.WriteHeader(writer); // Write the LSD. We'll use local color tables for now. - this.WriteLogicalScreenDescriptor(image, writer, quantized.TransparentIndex); + this.WriteLogicalScreenDescriptor(image, writer, index); // Write the first frame. - this.WriteGraphicalControlExtension(image, writer, quantized.TransparentIndex); + this.WriteGraphicalControlExtension(image, writer, index); this.WriteImageDescriptor(image, writer); this.WriteColorTable(quantized, writer); this.WriteImageData(quantized, writer); @@ -90,7 +92,8 @@ namespace ImageProcessorCore.Formats foreach (ImageFrame frame in image.Frames) { QuantizedImage quantizedFrame = ((IQuantizer)this.Quantizer).Quantize(frame, this.Quality); - this.WriteGraphicalControlExtension(frame, writer, quantizedFrame.TransparentIndex); + + this.WriteGraphicalControlExtension(frame, writer, GetTransparentIndex(quantizedFrame)); this.WriteImageDescriptor(frame, writer); this.WriteColorTable(quantizedFrame, writer); this.WriteImageData(quantizedFrame, writer); @@ -101,6 +104,37 @@ namespace ImageProcessorCore.Formats writer.Write(GifConstants.EndIntroducer); } + /// + /// Returns the index of the most transparent color in the palette. + /// + /// + /// The quantized. + /// + /// The pixel format. + /// The packed format. uint, long, float. + /// + /// The . + /// + private static int GetTransparentIndex(QuantizedImage quantized) + where TColor : IPackedVector + where TPacked : struct + { + // Find the lowest alpha value and make it the transparent index. + int index = 255; + float alpha = 1; + for (int i = 0; i < quantized.Palette.Length; i++) + { + float a = quantized.Palette[i].ToVector4().W; + if (a < alpha) + { + alpha = a; + index = i; + } + } + + return index; + } + /// /// Writes the file header signature and version to the stream. /// diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs index 75f6868578..de0dbac289 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs @@ -32,7 +32,7 @@ namespace ImageProcessorCore.Formats public string Extension => "png"; /// - /// The compression level 1-9. + /// Gets or sets the compression level 1-9. /// Defaults to 6. /// public int CompressionLevel { get; set; } = 6; @@ -46,14 +46,14 @@ namespace ImageProcessorCore.Formats public float Gamma { get; set; } = 2.2F; /// - /// The quantizer for reducing the color count. + /// Gets or sets quantizer for reducing the color count. /// public IQuantizer Quantizer { get; set; } /// /// Gets or sets the transparency threshold. /// - public byte Threshold { get; set; } = 128; + public byte Threshold { get; set; } = 0; /// /// Gets or sets a value indicating whether this instance should write @@ -81,7 +81,7 @@ namespace ImageProcessorCore.Formats CompressionLevel = this.CompressionLevel, Gamma = this.Gamma, Quality = this.Quality, - PngColorType = PngColorType, + PngColorType = this.PngColorType, Quantizer = this.Quantizer, WriteGamma = this.WriteGamma, Threshold = this.Threshold diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index 0613c26ad9..fb3e2df6fc 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -8,6 +8,7 @@ namespace ImageProcessorCore.Formats using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Threading.Tasks; using Quantizers; @@ -86,7 +87,7 @@ namespace ImageProcessorCore.Formats /// /// Gets or sets the transparency threshold. /// - public byte Threshold { get; set; } = 128; + public byte Threshold { get; set; } /// /// Encodes the image to the specified stream from the . @@ -492,7 +493,7 @@ namespace ImageProcessorCore.Formats if (this.Quantizer == null) { - this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; + this.Quantizer = new WuQuantizer(); } // Quantize the image returning a palette. This boxing is icky. @@ -501,7 +502,7 @@ namespace ImageProcessorCore.Formats // Grab the palette and write it to the stream. TColor[] palette = quantized.Palette; int pixelCount = palette.Length; - + List transparentPixels = new List(); // Get max colors for bit depth. int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3; byte[] colorTable = new byte[colorTableLength]; @@ -517,14 +518,19 @@ namespace ImageProcessorCore.Formats colorTable[offset] = color.R; colorTable[offset + 1] = color.G; colorTable[offset + 2] = color.B; + + if (color.A <= this.Threshold) + { + transparentPixels.Add((byte)offset); + } }); this.WriteChunk(stream, PngChunkTypes.Palette, colorTable); // Write the transparency data - if (quantized.TransparentIndex > -1) + if (transparentPixels.Any()) { - this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, new[] { (byte)quantized.TransparentIndex }); + this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, transparentPixels.ToArray()); } return quantized; diff --git a/src/ImageProcessorCore/Quantizers/IQuantizer.cs b/src/ImageProcessorCore/Quantizers/IQuantizer.cs index e0087a52e9..1ad7ad9c38 100644 --- a/src/ImageProcessorCore/Quantizers/IQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/IQuantizer.cs @@ -30,9 +30,5 @@ namespace ImageProcessorCore.Quantizers /// 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 index 89d18575a6..0e53cbbab9 100644 --- a/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs @@ -7,7 +7,6 @@ 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. @@ -44,7 +43,7 @@ namespace ImageProcessorCore.Quantizers /// public override QuantizedImage Quantize(ImageBase image, int maxColors) { - this.colors = maxColors.Clamp(1, 255); + this.colors = maxColors.Clamp(1, 256); if (this.octree == null) { @@ -80,16 +79,7 @@ namespace ImageProcessorCore.Quantizers /// protected override byte QuantizePixel(TColor 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 (new Color(pixel.ToVector4()).A > this.Threshold) - { - paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); - } - - return paletteIndex; + return (byte)this.octree.GetPaletteIndex(pixel); } /// @@ -100,17 +90,18 @@ namespace ImageProcessorCore.Quantizers /// protected override List GetPalette() { + return this.octree.Palletize(Math.Max(this.colors, 1)); + // First off convert the Octree to maxColors colors - List palette = this.octree.Palletize(Math.Max(this.colors, 1)); + //List palette = this.octree.Palletize(Math.Max(this.colors, 1)); - int diff = this.colors - palette.Count; - if (diff > 0) - { - palette.AddRange(Enumerable.Repeat(default(TColor), diff)); - } - this.TransparentIndex = this.colors; + //int diff = this.colors - palette.Count; + //if (diff > 0) + //{ + // palette.AddRange(Enumerable.Repeat(default(TColor), diff)); + //} - return palette; + //return palette; } /// @@ -134,7 +125,7 @@ namespace ImageProcessorCore.Quantizers /// /// Mask used when getting the appropriate pixels for a given node /// - private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + private static readonly int[] Mask = { 0x100, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; /// /// The root of the Octree @@ -196,6 +187,7 @@ namespace ImageProcessorCore.Quantizers public void AddColor(TColor pixel) { TPacked packed = pixel.PackedValue; + // Check if this request is for the same color as the last if (this.previousColor.Equals(packed)) { @@ -324,6 +316,11 @@ namespace ImageProcessorCore.Quantizers /// private int blue; + /// + /// Alpha component + /// + private int alpha; + /// /// The index of this node in the palette /// @@ -346,7 +343,7 @@ namespace ImageProcessorCore.Quantizers // Construct the new node this.leaf = level == colorBits; - this.red = this.green = this.blue = 0; + this.red = this.green = this.blue = this.alpha = 0; this.pixelCount = 0; // If a leaf, increment the leaf count @@ -392,9 +389,10 @@ namespace ImageProcessorCore.Quantizers // Go to the next level down in the tree int shift = 7 - level; Color color = new Color(pixel.ToVector4()); - int index = ((color.B & Mask[level]) >> (shift - 2)) | - ((color.G & Mask[level]) >> (shift - 1)) | - ((color.R & Mask[level]) >> shift); + int index = ((color.A & Mask[0]) >> (shift - 3)) | + ((color.B & Mask[level + 1]) >> (shift - 2)) | + ((color.G & Mask[level + 1]) >> (shift - 1)) | + ((color.R & Mask[level + 1]) >> shift); OctreeNode child = this.children[index]; @@ -416,7 +414,7 @@ namespace ImageProcessorCore.Quantizers /// The number of leaves removed public int Reduce() { - this.red = this.green = this.blue = 0; + this.red = this.green = this.blue = this.alpha = 0; int childNodes = 0; // Loop through all children and add their information to this node @@ -427,6 +425,7 @@ namespace ImageProcessorCore.Quantizers this.red += this.children[index].red; this.green += this.children[index].green; this.blue += this.children[index].blue; + this.alpha += this.children[index].alpha; this.pixelCount += this.children[index].pixelCount; ++childNodes; this.children[index] = null; @@ -455,10 +454,11 @@ namespace ImageProcessorCore.Quantizers byte r = (this.red / this.pixelCount).ToByte(); byte g = (this.green / this.pixelCount).ToByte(); byte b = (this.blue / this.pixelCount).ToByte(); + byte a = (this.alpha / this.pixelCount).ToByte(); // And set the color of the palette entry TColor pixel = default(TColor); - pixel.PackFromVector4(new Color(r, g, b).ToVector4()); + pixel.PackFromVector4(new Color(r, g, b, a).ToVector4()); palette.Add(pixel); } else @@ -490,9 +490,10 @@ namespace ImageProcessorCore.Quantizers { int shift = 7 - level; Color color = new Color(pixel.ToVector4()); - int pixelIndex = ((color.B & Mask[level]) >> (shift - 2)) | - ((color.G & Mask[level]) >> (shift - 1)) | - ((color.R & Mask[level]) >> shift); + int pixelIndex = ((color.A & Mask[0]) >> (shift - 3)) | + ((color.B & Mask[level + 1]) >> (shift - 2)) | + ((color.G & Mask[level + 1]) >> (shift - 1)) | + ((color.R & Mask[level + 1]) >> shift); if (this.children[pixelIndex] != null) { @@ -520,6 +521,7 @@ namespace ImageProcessorCore.Quantizers this.red += color.R; this.green += color.G; this.blue += color.B; + this.alpha += color.A; } } } diff --git a/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs index a5d933d4e6..cbaa8435ed 100644 --- a/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs @@ -38,14 +38,6 @@ namespace ImageProcessorCore.Quantizers 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) { @@ -73,7 +65,7 @@ namespace ImageProcessorCore.Quantizers this.SecondPass(pixels, quantizedPixels, width, height); } - return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex); + return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels); } /// diff --git a/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs b/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs index 0a3f12ba8d..07a4098463 100644 --- a/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs @@ -80,51 +80,36 @@ namespace ImageProcessorCore.Quantizers else { // Not found - loop through the palette and find the nearest match. - // Firstly check the alpha value - if less than the threshold, lookup the transparent color - Color color =new Color(pixel.ToVector4()); - if (!(color.A > this.Threshold)) - { - // Transparent. Lookup the first color with an alpha value of 0 - for (int index = 0; index < this.colors.Length; index++) - { - if (new Color(this.colors[index].ToVector4()).A == 0) - { - colorIndex = (byte)index; - this.TransparentIndex = colorIndex; - break; - } - } - } - else + Color color = new Color(pixel.ToVector4()); + + int leastDistance = int.MaxValue; + int red = color.R; + int green = color.G; + int blue = color.B; + int alpha = color.A; + + for (int index = 0; index < this.colors.Length; index++) { - // Not transparent... - int leastDistance = int.MaxValue; - int red = color.R; - int green = color.G; - int blue = color.B; - - // Loop through the entire palette, looking for the closest color match - for (int index = 0; index < this.colors.Length; index++) - { - Color paletteColor = new Color(this.colors[index].ToVector4()); - int redDistance = paletteColor.R - red; - int greenDistance = paletteColor.G - green; - int blueDistance = paletteColor.B - blue; + Color paletteColor = new Color(this.colors[index].ToVector4()); + int redDistance = paletteColor.R - red; + int greenDistance = paletteColor.G - green; + int blueDistance = paletteColor.B - blue; + int alphaDistance = paletteColor.A - alpha; - int distance = (redDistance * redDistance) + - (greenDistance * greenDistance) + - (blueDistance * blueDistance); + int distance = (redDistance * redDistance) + + (greenDistance * greenDistance) + + (blueDistance * blueDistance) + + (alphaDistance * alphaDistance); - if (distance < leastDistance) + if (distance < leastDistance) + { + colorIndex = (byte)index; + leastDistance = distance; + + // And if it's an exact match, exit the loop + if (distance == 0) { - colorIndex = (byte)index; - leastDistance = distance; - - // And if it's an exact match, exit the loop - if (distance == 0) - { - break; - } + break; } } } @@ -142,4 +127,4 @@ namespace ImageProcessorCore.Quantizers return this.colors.ToList(); } } -} +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Quantizers/QuantizedImage.cs b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs index 84ebcadfee..79b618a964 100644 --- a/src/ImageProcessorCore/Quantizers/QuantizedImage.cs +++ b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs @@ -24,8 +24,7 @@ namespace ImageProcessorCore.Quantizers /// The image height. /// The color palette. /// The quantized pixels. - /// The transparency index. - public QuantizedImage(int width, int height, TColor[] palette, byte[] pixels, int transparentIndex = -1) + public QuantizedImage(int width, int height, TColor[] palette, byte[] pixels) { Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); @@ -42,7 +41,6 @@ namespace ImageProcessorCore.Quantizers this.Height = height; this.Palette = palette; this.Pixels = pixels; - this.TransparentIndex = transparentIndex; } /// @@ -65,11 +63,6 @@ namespace ImageProcessorCore.Quantizers /// public byte[] Pixels { get; } - /// - /// Gets the transparent index - /// - public int TransparentIndex { get; } - /// /// Converts this quantized image to a normal image. /// @@ -98,4 +91,4 @@ namespace ImageProcessorCore.Quantizers return image; } } -} +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs index ed48203063..9521d4dc49 100644 --- a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs @@ -117,9 +117,6 @@ namespace ImageProcessorCore.Quantizers this.tag = new byte[TableLength]; } - /// - public byte Threshold { get; set; } - /// public QuantizedImage Quantize(ImageBase image, int maxColors) { @@ -334,7 +331,7 @@ namespace ImageProcessorCore.Quantizers for (int x = 0; x < pixels.Width; x++) { // Colors are expected in r->g->b->a format - Color color = new Color(pixels[x, y].ToVector4()); + Color color = new Color(pixels[x, y].ToVector4()); byte r = color.R; byte g = color.G; @@ -729,7 +726,6 @@ namespace ImageProcessorCore.Quantizers { List pallette = new List(); byte[] pixels = new byte[imagePixels.Width * imagePixels.Height]; - int transparentIndex = -1; int width = imagePixels.Width; int height = imagePixels.Height; @@ -741,25 +737,18 @@ namespace ImageProcessorCore.Quantizers if (Math.Abs(weight) > Epsilon) { - byte r = (byte)(Volume(cube[k], this.vmr) / weight); - byte g = (byte)(Volume(cube[k], this.vmg) / weight); - byte b = (byte)(Volume(cube[k], this.vmb) / weight); - byte a = (byte)(Volume(cube[k], this.vma) / weight); + float r = (float)(Volume(cube[k], this.vmr) / weight); + float g = (float)(Volume(cube[k], this.vmg) / weight); + float b = (float)(Volume(cube[k], this.vmb) / weight); + float a = (float)(Volume(cube[k], this.vma) / weight); TColor color = default(TColor); color.PackFromVector4(new Vector4(r, g, b, a) / 255F); - - if (color.Equals(default(TColor))) - { - transparentIndex = k; - } - pallette.Add(color); } else { pallette.Add(default(TColor)); - transparentIndex = k; } } @@ -778,18 +767,12 @@ namespace ImageProcessorCore.Quantizers int b = color.B >> (8 - IndexBits); int a = color.A >> (8 - IndexAlphaBits); - if (transparentIndex > -1 && color.A <= this.Threshold) - { - pixels[(y * width) + x] = (byte)transparentIndex; - continue; - } - int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); pixels[(y * width) + x] = this.tag[ind]; } }); - return new QuantizedImage(width, height, pallette.ToArray(), pixels, transparentIndex); + return new QuantizedImage(width, height, pallette.ToArray(), pixels); } } } \ No newline at end of file