Browse Source

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
af/merge-core
James Jackson-South 10 years ago
parent
commit
15b989561c
  1. 42
      src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs
  2. 8
      src/ImageProcessorCore/Formats/Png/PngEncoder.cs
  3. 16
      src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs
  4. 4
      src/ImageProcessorCore/Quantizers/IQuantizer.cs
  5. 62
      src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs
  6. 10
      src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs
  7. 69
      src/ImageProcessorCore/Quantizers/Palette/PaletteQuantizer.cs
  8. 11
      src/ImageProcessorCore/Quantizers/QuantizedImage.cs
  9. 29
      src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs

42
src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs

@ -55,7 +55,7 @@ namespace ImageProcessorCore.Formats
if (this.Quantizer == null)
{
this.Quantizer = new OctreeQuantizer<TColor, TPacked> { Threshold = this.Threshold };
this.Quantizer = new OctreeQuantizer<TColor, TPacked>();
}
// 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<TColor, TPacked> quantized = ((IQuantizer<TColor, TPacked>)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<TColor, TPacked> frame in image.Frames)
{
QuantizedImage<TColor, TPacked> quantizedFrame = ((IQuantizer<TColor, TPacked>)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);
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">
/// The quantized.
/// </param>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <typeparam name="TPacked">The packed format. <example>uint, long, float.</example></typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private static int GetTransparentIndex<TColor, TPacked>(QuantizedImage<TColor, TPacked> quantized)
where TColor : IPackedVector<TPacked>
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;
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>

8
src/ImageProcessorCore/Formats/Png/PngEncoder.cs

@ -32,7 +32,7 @@ namespace ImageProcessorCore.Formats
public string Extension => "png";
/// <summary>
/// The compression level 1-9.
/// Gets or sets the compression level 1-9.
/// <remarks>Defaults to 6.</remarks>
/// </summary>
public int CompressionLevel { get; set; } = 6;
@ -46,14 +46,14 @@ namespace ImageProcessorCore.Formats
public float Gamma { get; set; } = 2.2F;
/// <summary>
/// The quantizer for reducing the color count.
/// Gets or sets quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
public byte Threshold { get; set; } = 0;
/// <summary>
/// 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

16
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
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
public byte Threshold { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageBase{TColor, TPacked}"/>.
@ -492,7 +493,7 @@ namespace ImageProcessorCore.Formats
if (this.Quantizer == null)
{
this.Quantizer = new WuQuantizer<TColor, TPacked> { Threshold = this.Threshold };
this.Quantizer = new WuQuantizer<TColor, TPacked>();
}
// 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<byte> transparentPixels = new List<byte>();
// 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;

4
src/ImageProcessorCore/Quantizers/IQuantizer.cs

@ -30,9 +30,5 @@ namespace ImageProcessorCore.Quantizers
/// </summary>
public interface IQuantizer
{
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
byte Threshold { get; set; }
}
}

62
src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs

@ -7,7 +7,6 @@ namespace ImageProcessorCore.Quantizers
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Encapsulates methods to calculate the colour palette if an image using an Octree pattern.
@ -44,7 +43,7 @@ namespace ImageProcessorCore.Quantizers
/// <inheritdoc/>
public override QuantizedImage<TColor, TPacked> Quantize(ImageBase<TColor, TPacked> 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
/// </returns>
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);
}
/// <summary>
@ -100,17 +90,18 @@ namespace ImageProcessorCore.Quantizers
/// </returns>
protected override List<TColor> GetPalette()
{
return this.octree.Palletize(Math.Max(this.colors, 1));
// First off convert the Octree to maxColors colors
List<TColor> palette = this.octree.Palletize(Math.Max(this.colors, 1));
//List<TColor> 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;
}
/// <summary>
@ -134,7 +125,7 @@ namespace ImageProcessorCore.Quantizers
/// <summary>
/// Mask used when getting the appropriate pixels for a given node
/// </summary>
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 };
/// <summary>
/// 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
/// </summary>
private int blue;
/// <summary>
/// Alpha component
/// </summary>
private int alpha;
/// <summary>
/// The index of this node in the palette
/// </summary>
@ -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
/// <returns>The number of leaves removed</returns>
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;
}
}
}

10
src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs

@ -38,14 +38,6 @@ namespace ImageProcessorCore.Quantizers
this.singlePass = singlePass;
}
/// <summary>
/// Gets or sets the transparency index.
/// </summary>
public int TransparentIndex { get; protected set; } = -1;
/// <inheritdoc/>
public byte Threshold { get; set; }
/// <inheritdoc/>
public virtual QuantizedImage<TColor, TPacked> Quantize(ImageBase<TColor, TPacked> image, int maxColors)
{
@ -73,7 +65,7 @@ namespace ImageProcessorCore.Quantizers
this.SecondPass(pixels, quantizedPixels, width, height);
}
return new QuantizedImage<TColor, TPacked>(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex);
return new QuantizedImage<TColor, TPacked>(width, height, palette.ToArray(), quantizedPixels);
}
/// <summary>

69
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();
}
}
}
}

11
src/ImageProcessorCore/Quantizers/QuantizedImage.cs

@ -24,8 +24,7 @@ namespace ImageProcessorCore.Quantizers
/// <param name="height">The image height.</param>
/// <param name="palette">The color palette.</param>
/// <param name="pixels">The quantized pixels.</param>
/// <param name="transparentIndex">The transparency index.</param>
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;
}
/// <summary>
@ -65,11 +63,6 @@ namespace ImageProcessorCore.Quantizers
/// </summary>
public byte[] Pixels { get; }
/// <summary>
/// Gets the transparent index
/// </summary>
public int TransparentIndex { get; }
/// <summary>
/// Converts this quantized image to a normal image.
/// </summary>
@ -98,4 +91,4 @@ namespace ImageProcessorCore.Quantizers
return image;
}
}
}
}

29
src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs

@ -117,9 +117,6 @@ namespace ImageProcessorCore.Quantizers
this.tag = new byte[TableLength];
}
/// <inheritdoc/>
public byte Threshold { get; set; }
/// <inheritdoc/>
public QuantizedImage<TColor, TPacked> Quantize(ImageBase<TColor, TPacked> 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<TColor> pallette = new List<TColor>();
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<TColor, TPacked>(width, height, pallette.ToArray(), pixels, transparentIndex);
return new QuantizedImage<TColor, TPacked>(width, height, pallette.ToArray(), pixels);
}
}
}
Loading…
Cancel
Save