diff --git a/src/ImageSharp/Quantizers/Wu/Box.cs b/src/ImageSharp/Quantizers/Wu/Box.cs
index 42e4217b55..7f2a320873 100644
--- a/src/ImageSharp/Quantizers/Wu/Box.cs
+++ b/src/ImageSharp/Quantizers/Wu/Box.cs
@@ -7,8 +7,9 @@ namespace ImageSharp.Quantizers
{
///
/// Represents a box color cube.
+ /// TODO: This should be a struct for performance
///
- internal struct Box
+ internal sealed class Box
{
///
/// Gets or sets the min red value, exclusive.
diff --git a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs b/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs
index ba8047c066..998bd0c82c 100644
--- a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs
+++ b/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs
@@ -7,8 +7,9 @@ namespace ImageSharp.Quantizers
{
using System;
using System.Buffers;
+ using System.Collections.Generic;
using System.Numerics;
- using System.Threading.Tasks;
+ using System.Runtime.CompilerServices;
///
/// An implementation of Wu's color quantizer with alpha channel.
@@ -30,7 +31,7 @@ namespace ImageSharp.Quantizers
///
///
/// The pixel format.
- public sealed class WuQuantizer : IQuantizer
+ public class WuQuantizer : Quantizer
where TColor : struct, IPixel
{
///
@@ -58,82 +59,232 @@ namespace ImageSharp.Quantizers
///
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
+ ///
+ /// A buffer for storing pixels
+ ///
+ private readonly byte[] rgbaBuffer = new byte[4];
+
+ ///
+ /// A lookup table for colors
+ ///
+ private readonly Dictionary colorMap = new Dictionary();
+
///
/// Moment of P(c).
///
- private readonly long[] vwt;
+ private long[] vwt;
///
/// Moment of r*P(c).
///
- private readonly long[] vmr;
+ private long[] vmr;
///
/// Moment of g*P(c).
///
- private readonly long[] vmg;
+ private long[] vmg;
///
/// Moment of b*P(c).
///
- private readonly long[] vmb;
+ private long[] vmb;
///
/// Moment of a*P(c).
///
- private readonly long[] vma;
+ private long[] vma;
///
/// Moment of c^2*P(c).
///
- private readonly float[] m2;
+ private float[] m2;
///
/// Color space tag.
///
- private readonly byte[] tag;
+ private byte[] tag;
///
- /// A buffer for storing pixels
+ /// Maximum allowed color depth
///
- private readonly byte[] rgbaBuffer = new byte[4];
+ private int colors;
+
+ ///
+ /// The reduced image palette
+ ///
+ private TColor[] palette;
+
+ ///
+ /// The color cube representing the image palette
+ ///
+ private Box[] colorCube;
///
/// Initializes a new instance of the class.
///
+ ///
+ /// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram,
+ /// the second pass quantizes a color based on the position in the histogram.
+ ///
public WuQuantizer()
+ : base(false)
{
- this.vwt = WuArrayPool.LongPool.Rent(TableLength);
- this.vmr = WuArrayPool.LongPool.Rent(TableLength);
- this.vmg = WuArrayPool.LongPool.Rent(TableLength);
- this.vmb = WuArrayPool.LongPool.Rent(TableLength);
- this.vma = WuArrayPool.LongPool.Rent(TableLength);
- this.m2 = WuArrayPool.FloatPool.Rent(TableLength);
- this.tag = WuArrayPool.BytePool.Rent(TableLength);
}
///
- public QuantizedImage Quantize(ImageBase image, int maxColors)
+ public override QuantizedImage Quantize(ImageBase image, int maxColors)
{
Guard.NotNull(image, nameof(image));
- int colorCount = maxColors.Clamp(1, 256);
+ this.colors = maxColors.Clamp(1, 256);
+
+ try
+ {
+ this.vwt = WuArrayPool.LongPool.Rent(TableLength);
+ this.vmr = WuArrayPool.LongPool.Rent(TableLength);
+ this.vmg = WuArrayPool.LongPool.Rent(TableLength);
+ this.vmb = WuArrayPool.LongPool.Rent(TableLength);
+ this.vma = WuArrayPool.LongPool.Rent(TableLength);
+ this.m2 = WuArrayPool.FloatPool.Rent(TableLength);
+ this.tag = WuArrayPool.BytePool.Rent(TableLength);
+
+ return base.Quantize(image, this.colors);
+ }
+ finally
+ {
+ WuArrayPool.LongPool.Return(this.vwt, true);
+ WuArrayPool.LongPool.Return(this.vmr, true);
+ WuArrayPool.LongPool.Return(this.vmg, true);
+ WuArrayPool.LongPool.Return(this.vmb, true);
+ WuArrayPool.LongPool.Return(this.vma, true);
+ WuArrayPool.FloatPool.Return(this.m2, true);
+ WuArrayPool.BytePool.Return(this.tag, true);
+ }
+ }
+
+ ///
+ protected override TColor[] GetPalette()
+ {
+ if (this.palette == null)
+ {
+ this.palette = new TColor[this.colors];
+ for (int k = 0; k < this.colors; k++)
+ {
+ this.Mark(this.colorCube[k], (byte)k);
+
+ float weight = Volume(this.colorCube[k], this.vwt);
+
+ if (MathF.Abs(weight) > Constants.Epsilon)
+ {
+ float r = Volume(this.colorCube[k], this.vmr) / weight;
+ float g = Volume(this.colorCube[k], this.vmg) / weight;
+ float b = Volume(this.colorCube[k], this.vmb) / weight;
+ float a = Volume(this.colorCube[k], this.vma) / weight;
+
+ TColor color = default(TColor);
+ color.PackFromVector4(new Vector4(r, g, b, a) / 255F);
+ this.palette[k] = color;
+ }
+ }
+ }
+
+ return this.palette;
+ }
+
+ ///
+ protected override void InitialQuantizePixel(TColor pixel)
+ {
+ // Add the color to a 3-D color histogram.
+ // Colors are expected in r->g->b->a format
+ pixel.ToXyzwBytes(this.rgbaBuffer, 0);
+
+ byte r = this.rgbaBuffer[0];
+ byte g = this.rgbaBuffer[1];
+ byte b = this.rgbaBuffer[2];
+ byte a = this.rgbaBuffer[3];
+
+ int inr = r >> (8 - IndexBits);
+ int ing = g >> (8 - IndexBits);
+ int inb = b >> (8 - IndexBits);
+ int ina = a >> (8 - IndexAlphaBits);
+
+ int ind = GetPaletteIndex(inr + 1, ing + 1, inb + 1, ina + 1);
+
+ this.vwt[ind]++;
+ this.vmr[ind] += r;
+ this.vmg[ind] += g;
+ this.vmb[ind] += b;
+ this.vma[ind] += a;
+ this.m2[ind] += (r * r) + (g * g) + (b * b) + (a * a);
+ }
+
+ ///
+ protected override void FirstPass(PixelAccessor source, int width, int height)
+ {
+ // Build up the 3-D color histogram
+ // 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]);
+ }
+ }
- this.Clear();
+ this.Get3DMoments();
+ this.BuildCube();
+ }
- using (PixelAccessor imagePixels = image.Lock())
+ ///
+ protected override void SecondPass(PixelAccessor source, byte[] output, int width, int height)
+ {
+ // Load up the values for the first pixel. We can use these to speed up the second
+ // pass of the algorithm by avoiding transforming rows of identical color.
+ TColor sourcePixel = source[0, 0];
+ TColor previousPixel = sourcePixel;
+ byte pixelValue = this.QuantizePixel(sourcePixel);
+ TColor[] colorPalette = this.GetPalette();
+ TColor transformedPixel = colorPalette[pixelValue];
+
+ for (int y = 0; y < height; y++)
{
- this.Build3DHistogram(imagePixels);
- this.Get3DMoments();
+ // And loop through each column
+ for (int x = 0; x < width; x++)
+ {
+ // Get the pixel.
+ sourcePixel = source[x, y];
+
+ // Check if this is the same as the last pixel. If so use that value
+ // rather than calculating it again. This is an inexpensive optimization.
+ if (!previousPixel.Equals(sourcePixel))
+ {
+ // Quantize the pixel
+ pixelValue = this.QuantizePixel(sourcePixel);
- this.BuildCube(out Box[] cube, ref colorCount);
+ // And setup the previous pointer
+ previousPixel = sourcePixel;
- return this.GenerateResult(imagePixels, colorCount, cube);
+ if (this.Dither)
+ {
+ transformedPixel = colorPalette[pixelValue];
+ }
+ }
+
+ if (this.Dither)
+ {
+ // Apply the dithering matrix. We have to reapply the value now as the original has changed.
+ this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height);
+ }
+
+ output[(y * source.Width) + x] = pixelValue;
+ }
}
}
///
- /// Gets an index.
+ /// Gets the index index of the given color in the palette.
///
/// The red value.
/// The green value.
@@ -294,55 +445,6 @@ namespace ImageSharp.Quantizers
}
}
- ///
- /// Clears the tables.
- ///
- private void Clear()
- {
- Array.Clear(this.vwt, 0, TableLength);
- Array.Clear(this.vmr, 0, TableLength);
- Array.Clear(this.vmg, 0, TableLength);
- Array.Clear(this.vmb, 0, TableLength);
- Array.Clear(this.vma, 0, TableLength);
- Array.Clear(this.m2, 0, TableLength);
- Array.Clear(this.tag, 0, TableLength);
- }
-
- ///
- /// Builds a 3-D color histogram of counts, r/g/b, c^2.
- ///
- /// The pixel accessor.
- private void Build3DHistogram(PixelAccessor pixels)
- {
- for (int y = 0; y < pixels.Height; y++)
- {
- for (int x = 0; x < pixels.Width; x++)
- {
- // Colors are expected in r->g->b->a format
- pixels[x, y].ToXyzwBytes(this.rgbaBuffer, 0);
-
- byte r = this.rgbaBuffer[0];
- byte g = this.rgbaBuffer[1];
- byte b = this.rgbaBuffer[2];
- byte a = this.rgbaBuffer[3];
-
- int inr = r >> (8 - IndexBits);
- int ing = g >> (8 - IndexBits);
- int inb = b >> (8 - IndexBits);
- int ina = a >> (8 - IndexAlphaBits);
-
- int ind = GetPaletteIndex(inr + 1, ing + 1, inb + 1, ina + 1);
-
- this.vwt[ind]++;
- this.vmr[ind] += r;
- this.vmg[ind] += g;
- this.vmb[ind] += b;
- this.vma[ind] += a;
- this.m2[ind] += (r * r) + (g * g) + (b * b) + (a * a);
- }
- }
- }
-
///
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
///
@@ -665,30 +767,28 @@ namespace ImageSharp.Quantizers
///
/// Builds the cube.
///
- /// The cube.
- /// The color count.
- private void BuildCube(out Box[] cube, ref int colorCount)
+ private void BuildCube()
{
- cube = new Box[colorCount];
- float[] vv = new float[colorCount];
+ this.colorCube = new Box[this.colors];
+ float[] vv = new float[this.colors];
- for (int i = 0; i < colorCount; i++)
+ for (int i = 0; i < this.colors; i++)
{
- cube[i] = default(Box);
+ this.colorCube[i] = new Box();
}
- cube[0].R0 = cube[0].G0 = cube[0].B0 = cube[0].A0 = 0;
- cube[0].R1 = cube[0].G1 = cube[0].B1 = IndexCount - 1;
- cube[0].A1 = IndexAlphaCount - 1;
+ this.colorCube[0].R0 = this.colorCube[0].G0 = this.colorCube[0].B0 = this.colorCube[0].A0 = 0;
+ this.colorCube[0].R1 = this.colorCube[0].G1 = this.colorCube[0].B1 = IndexCount - 1;
+ this.colorCube[0].A1 = IndexAlphaCount - 1;
int next = 0;
- for (int i = 1; i < colorCount; i++)
+ for (int i = 1; i < this.colors; i++)
{
- if (this.Cut(cube[next], cube[i]))
+ if (this.Cut(this.colorCube[next], this.colorCube[i]))
{
- vv[next] = cube[next].Volume > 1 ? this.Variance(cube[next]) : 0F;
- vv[i] = cube[i].Volume > 1 ? this.Variance(cube[i]) : 0F;
+ vv[next] = this.colorCube[next].Volume > 1 ? this.Variance(this.colorCube[next]) : 0F;
+ vv[i] = this.colorCube[i].Volume > 1 ? this.Variance(this.colorCube[i]) : 0F;
}
else
{
@@ -710,79 +810,38 @@ namespace ImageSharp.Quantizers
if (temp <= 0.0)
{
- colorCount = i + 1;
+ this.colors = i + 1;
break;
}
}
}
///
- /// Generates the quantized result.
+ /// Process the pixel in the second pass of the algorithm
///
- /// The image pixels.
- /// The color count.
- /// The cube.
- /// The result.
- private QuantizedImage GenerateResult(PixelAccessor imagePixels, int colorCount, Box[] cube)
+ /// The pixel to quantize
+ ///
+ /// The quantized value
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private byte QuantizePixel(TColor pixel)
{
- TColor[] pallette = new TColor[colorCount];
- byte[] pixels = new byte[imagePixels.Width * imagePixels.Height];
- int width = imagePixels.Width;
- int height = imagePixels.Height;
-
- for (int k = 0; k < colorCount; k++)
+ if (this.Dither)
{
- this.Mark(cube[k], (byte)k);
-
- float weight = Volume(cube[k], this.vwt);
-
- if (MathF.Abs(weight) > Constants.Epsilon)
- {
- float r = Volume(cube[k], this.vmr) / weight;
- float g = Volume(cube[k], this.vmg) / weight;
- float b = Volume(cube[k], this.vmb) / weight;
- float a = Volume(cube[k], this.vma) / weight;
-
- TColor color = default(TColor);
- color.PackFromVector4(new Vector4(r, g, b, a) / 255F);
- pallette[k] = color;
- }
+ // The colors have changed so we need to use Euclidean distance caclulation to find the closest value.
+ // This palette can never be null here.
+ return this.GetClosestColor(pixel, this.palette, this.colorMap);
}
- Parallel.For(
- 0,
- height,
- imagePixels.ParallelOptions,
- y =>
- {
- byte[] rgba = ArrayPool.Shared.Rent(4);
- for (int x = 0; x < width; x++)
- {
- // Expected order r->g->b->a
- imagePixels[x, y].ToXyzwBytes(rgba, 0);
-
- int r = rgba[0] >> (8 - IndexBits);
- int g = rgba[1] >> (8 - IndexBits);
- int b = rgba[2] >> (8 - IndexBits);
- int a = rgba[3] >> (8 - IndexAlphaBits);
-
- int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
- pixels[(y * width) + x] = this.tag[ind];
- }
-
- ArrayPool.Shared.Return(rgba);
- });
+ // Expected order r->g->b->a
+ pixel.ToXyzwBytes(this.rgbaBuffer, 0);
- // Cleanup
- WuArrayPool.LongPool.Return(this.vwt);
- WuArrayPool.LongPool.Return(this.vmr);
- WuArrayPool.LongPool.Return(this.vmg);
- WuArrayPool.LongPool.Return(this.vmb);
- WuArrayPool.LongPool.Return(this.vma);
- WuArrayPool.FloatPool.Return(this.m2);
- WuArrayPool.BytePool.Return(this.tag);
+ int r = this.rgbaBuffer[0] >> (8 - IndexBits);
+ int g = this.rgbaBuffer[1] >> (8 - IndexBits);
+ int b = this.rgbaBuffer[2] >> (8 - IndexBits);
+ int a = this.rgbaBuffer[3] >> (8 - IndexAlphaBits);
- return new QuantizedImage(width, height, pallette, pixels);
+ return (byte)GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
}
}
-}
\ No newline at end of file
+}