diff --git a/ImageProcessorCore.sln b/ImageProcessorCore.sln
index a4df907b45..5c42e1cc1a 100644
--- a/ImageProcessorCore.sln
+++ b/ImageProcessorCore.sln
@@ -7,6 +7,11 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ImageProcessorCore", "src\I
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ImageProcessorCore.Tests", "tests\ImageProcessorCore.Tests\ImageProcessorCore.Tests.xproj", "{F836E8E6-B4D9-4208-8346-140C74678B91}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/README.md b/README.md
index 75b30ff222..9c904656b6 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,9 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor
- [x] bmp (More bmp format saving support required, 24bit just now)
- [x] png (Need updating for saving indexed support)
- [x] gif
+- Quantizers (IQuantizer with alpha channel support)
+ - [x] Octree
+ - [x] Wu
- Basic color structs with implicit operators. Vector backed. [#260](https://github.com/JimBobSquarePants/ImageProcessor/issues/260)
- [x] Color - Float based, premultiplied alpha, No limit to r, g, b, a values allowing for a fuller color range.
- [x] BGRA32
diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs
index 981dfb241d..995cab2073 100644
--- a/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs
+++ b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs
@@ -10,6 +10,8 @@ namespace ImageProcessorCore.Formats
using System.Linq;
using System.Threading.Tasks;
+ using ImageProcessorCore.Quantizers;
+
///
/// Image encoder for writing image data to a stream in gif format.
///
@@ -21,6 +23,11 @@ namespace ImageProcessorCore.Formats
/// For gifs the value ranges from 1 to 256.
public int Quality { get; set; }
+ ///
+ /// The quantizer for reducing the color count.
+ ///
+ public IQuantizer Quantizer { get; set; } = new WuQuantizer();
+
///
public string Extension => "gif";
@@ -61,7 +68,7 @@ namespace ImageProcessorCore.Formats
this.WriteGlobalLogicalScreenDescriptor(image, stream, bitDepth);
QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality, bitDepth);
- this.WriteGraphicalControlExtension(imageBase, stream);
+ this.WriteGraphicalControlExtension(imageBase, stream, quantized.TransparentIndex);
this.WriteImageDescriptor(quantized, quality, stream);
if (image.Frames.Any())
@@ -69,7 +76,7 @@ namespace ImageProcessorCore.Formats
this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count);
foreach (ImageFrame frame in image.Frames)
{
- this.WriteGraphicalControlExtension(frame, stream);
+ this.WriteGraphicalControlExtension(frame, stream, quantized.TransparentIndex);
this.WriteFrameImageDescriptor(frame, stream);
}
}
@@ -124,8 +131,7 @@ namespace ImageProcessorCore.Formats
private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth)
{
// Quantize the image returning a pallete.
- IQuantizer quantizer = new OctreeQuantizer(quality.Clamp(1, 255), bitDepth);
- QuantizedImage quantizedImage = quantizer.Quantize(image);
+ QuantizedImage quantizedImage = this.Quantizer.Quantize(image, quality.Clamp(1, 255));
// Grab the pallete and write it to the stream.
Bgra32[] pallete = quantizedImage.Palette;
@@ -156,14 +162,10 @@ namespace ImageProcessorCore.Formats
///
/// The to encode.
/// The stream to write to.
- private void WriteGraphicalControlExtension(ImageBase image, Stream stream)
+ private void WriteGraphicalControlExtension(ImageBase image, Stream stream, int transparencyIndex)
{
- // Calculate the quality.
- int quality = this.Quality > 0 ? this.Quality : image.Quality;
- quality = quality > 0 ? quality.Clamp(1, 256) : 256;
-
// TODO: Check transparency logic.
- bool hasTransparent = quality > 1;
+ bool hasTransparent = transparencyIndex > -1;
DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified;
@@ -172,7 +174,7 @@ namespace ImageProcessorCore.Formats
{
DisposalMethod = disposalMethod,
TransparencyFlag = hasTransparent,
- TransparencyIndex = quality - 1, // Quantizer sets last index as transparent.
+ TransparencyIndex = transparencyIndex,
DelayTime = image.FrameDelay
};
diff --git a/src/ImageProcessorCore/Formats/Gif/Quantizer/IQuantizer.cs b/src/ImageProcessorCore/Quantizers/IQuantizer.cs
similarity index 70%
rename from src/ImageProcessorCore/Formats/Gif/Quantizer/IQuantizer.cs
rename to src/ImageProcessorCore/Quantizers/IQuantizer.cs
index b54334a105..68f7554dab 100644
--- a/src/ImageProcessorCore/Formats/Gif/Quantizer/IQuantizer.cs
+++ b/src/ImageProcessorCore/Quantizers/IQuantizer.cs
@@ -3,7 +3,7 @@
// Licensed under the Apache License, Version 2.0.
//
-namespace ImageProcessorCore.Formats
+namespace ImageProcessorCore.Quantizers
{
///
/// Provides methods for allowing quantization of images pixels.
@@ -13,10 +13,11 @@ namespace ImageProcessorCore.Formats
///
/// Quantize an image and return the resulting output pixels.
///
- /// The image to quantize.
+ /// The image to quantize.
+ /// The maximum number of colors to return.
///
/// A representing a quantized version of the image pixels.
///
- QuantizedImage Quantize(ImageBase imageBase);
+ QuantizedImage Quantize(ImageBase image, int maxColors);
}
}
diff --git a/src/ImageProcessorCore/Formats/Gif/Quantizer/OctreeQuantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs
similarity index 93%
rename from src/ImageProcessorCore/Formats/Gif/Quantizer/OctreeQuantizer.cs
rename to src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs
index 25c2171f91..c956651740 100644
--- a/src/ImageProcessorCore/Formats/Gif/Quantizer/OctreeQuantizer.cs
+++ b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs
@@ -3,7 +3,7 @@
// Licensed under the Apache License, Version 2.0.
//
-namespace ImageProcessorCore.Formats
+namespace ImageProcessorCore.Quantizers
{
using System;
using System.Collections.Generic;
@@ -12,59 +12,49 @@ namespace ImageProcessorCore.Formats
/// Encapsulates methods to calculate the colour palette if an image using an Octree pattern.
///
///
- public class OctreeQuantizer : Quantizer
+ public sealed class OctreeQuantizer : Quantizer
{
///
/// Stores the tree
///
- private readonly Octree octree;
+ private Octree octree;
///
/// Maximum allowed color depth
///
- private readonly int maxColors;
+ 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.
- ///
- /// Defaults to return a maximum of 255 colors plus transparency with 8 significant bits.
- ///
+ /// the second pass quantizes a color based on the nodes in the tree
///
public OctreeQuantizer()
- : this(255, 8)
+ : base(false)
{
}
///
- /// Initializes a new instance of the class.
+ /// Gets or sets the transparency threshold.
///
- ///
- /// 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
- ///
- /// The maximum number of colors to return
- /// The number of significant bits
- public OctreeQuantizer(int maxColors, int maxColorBits)
- : base(false)
+ public byte Threshold { get; set; } = 128;
+
+ ///
+ public override QuantizedImage Quantize(ImageBase image, int maxColors)
{
- Guard.MustBeLessThanOrEqualTo(maxColors, 255, "maxColors");
- Guard.MustBeBetweenOrEqualTo(maxColorBits, 1, 8, "maxColorBits");
+ this.colors = maxColors.Clamp(1, 255);
- // Construct the Octree
- this.octree = new Octree(maxColorBits);
+ if (this.octree == null)
+ {
+ // Construct the Octree
+ this.octree = new Octree(this.GetBitsNeededForColorDepth(maxColors));
+ }
- this.maxColors = maxColors;
+ return base.Quantize(image, maxColors);
}
- ///
- /// Gets or sets the transparency threshold.
- ///
- public byte Threshold { get; set; } = 128;
-
///
/// Process the pixel in the first pass of the algorithm
///
@@ -93,7 +83,7 @@ namespace ImageProcessorCore.Formats
protected override byte QuantizePixel(Bgra32 pixel)
{
// The color at [maxColors] is set to transparent
- byte paletteIndex = (byte)this.maxColors;
+ byte paletteIndex = (byte)this.colors;
// Get the palette index if it's transparency meets criterea.
if (pixel.A > this.Threshold)
@@ -113,13 +103,27 @@ namespace ImageProcessorCore.Formats
protected override List GetPalette()
{
// First off convert the Octree to maxColors colors
- List palette = this.octree.Palletize(Math.Max(this.maxColors - 1, 1));
+ List palette = this.octree.Palletize(Math.Max(this.colors, 1));
palette.Add(Bgra32.Empty);
+ 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
///
diff --git a/src/ImageProcessorCore/Formats/Gif/Quantizer/Quantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs
similarity index 87%
rename from src/ImageProcessorCore/Formats/Gif/Quantizer/Quantizer.cs
rename to src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs
index 9b8371e043..2a330f782e 100644
--- a/src/ImageProcessorCore/Formats/Gif/Quantizer/Quantizer.cs
+++ b/src/ImageProcessorCore/Quantizers/Octree/Quantizer.cs
@@ -3,8 +3,9 @@
// Licensed under the Apache License, Version 2.0.
//
-namespace ImageProcessorCore.Formats
+namespace ImageProcessorCore.Quantizers
{
+ using System;
using System.Collections.Generic;
///
@@ -33,19 +34,26 @@ namespace ImageProcessorCore.Formats
this.singlePass = singlePass;
}
+ ///
+ /// Gets or sets the transparency index.
+ ///
+ public int TransparentIndex { get; protected set; }
+
///
- public QuantizedImage Quantize(ImageBase imageBase)
+ public virtual QuantizedImage Quantize(ImageBase image, int maxColors)
{
+ Guard.NotNull(image, nameof(image));
+
// Get the size of the source image
- int height = imageBase.Height;
- int width = imageBase.Width;
+ int height = image.Height;
+ int width = image.Width;
// 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(imageBase, width, height);
+ this.FirstPass(image, width, height);
}
byte[] quantizedPixels = new byte[width * height];
@@ -53,9 +61,9 @@ namespace ImageProcessorCore.Formats
// Get the pallete
List palette = this.GetPalette();
- this.SecondPass(imageBase, quantizedPixels, width, height);
+ this.SecondPass(image, quantizedPixels, width, height);
- return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels);
+ return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels, this.TransparentIndex);
}
///
@@ -121,9 +129,7 @@ namespace ImageProcessorCore.Formats
///
/// Override this to process the pixel in the first pass of the algorithm
///
- ///
- /// The pixel to quantize
- ///
+ /// The pixel to quantize
///
/// This function need only be overridden if your quantize algorithm needs two passes,
/// such as an Octree quantizer.
@@ -135,9 +141,7 @@ namespace ImageProcessorCore.Formats
///
/// Override this to process the pixel in the second pass of the algorithm
///
- ///
- /// The pixel to quantize
- ///
+ /// The pixel to quantize
///
/// The quantized value
///
diff --git a/src/ImageProcessorCore/Formats/Gif/Quantizer/QuantizedImage.cs b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs
similarity index 89%
rename from src/ImageProcessorCore/Formats/Gif/Quantizer/QuantizedImage.cs
rename to src/ImageProcessorCore/Quantizers/QuantizedImage.cs
index d46f5748f7..fdf93abd33 100644
--- a/src/ImageProcessorCore/Formats/Gif/Quantizer/QuantizedImage.cs
+++ b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs
@@ -3,7 +3,7 @@
// Licensed under the Apache License, Version 2.0.
//
-namespace ImageProcessorCore.Formats
+namespace ImageProcessorCore.Quantizers
{
using System;
using System.Threading.Tasks;
@@ -20,7 +20,8 @@ namespace ImageProcessorCore.Formats
/// The image height.
/// The color palette.
/// The quantized pixels.
- public QuantizedImage(int width, int height, Bgra32[] palette, byte[] pixels)
+ /// The transparency index.
+ public QuantizedImage(int width, int height, Bgra32[] palette, byte[] pixels, int transparentIndex = -1)
{
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
@@ -37,6 +38,7 @@ namespace ImageProcessorCore.Formats
this.Height = height;
this.Palette = palette;
this.Pixels = pixels;
+ this.TransparentIndex = transparentIndex;
}
///
@@ -59,6 +61,11 @@ namespace ImageProcessorCore.Formats
///
public byte[] Pixels { get; }
+ ///
+ /// Gets the transparent index
+ ///
+ public int TransparentIndex { get; }
+
///
/// Converts this quantized image to a normal image.
///
diff --git a/src/ImageProcessorCore/Quantizers/Wu/Box.cs b/src/ImageProcessorCore/Quantizers/Wu/Box.cs
new file mode 100644
index 0000000000..b9300b0870
--- /dev/null
+++ b/src/ImageProcessorCore/Quantizers/Wu/Box.cs
@@ -0,0 +1,58 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Quantizers
+{
+ ///
+ /// Represents a box color cube.
+ ///
+ internal sealed class Box
+ {
+ ///
+ /// Gets or sets the min red value, exclusive.
+ ///
+ public int R0 { get; set; }
+
+ ///
+ /// Gets or sets the max red value, inclusive.
+ ///
+ public int R1 { get; set; }
+
+ ///
+ /// Gets or sets the min green value, exclusive.
+ ///
+ public int G0 { get; set; }
+
+ ///
+ /// Gets or sets the max green value, inclusive.
+ ///
+ public int G1 { get; set; }
+
+ ///
+ /// Gets or sets the min blue value, exclusive.
+ ///
+ public int B0 { get; set; }
+
+ ///
+ /// Gets or sets the max blue value, inclusive.
+ ///
+ public int B1 { get; set; }
+
+ ///
+ /// Gets or sets the min alpha value, exclusive.
+ ///
+ public int A0 { get; set; }
+
+ ///
+ /// Gets or sets the max alpha value, inclusive.
+ ///
+ public int A1 { get; set; }
+
+ ///
+ /// Gets or sets the volume.
+ ///
+ public int Volume { get; set; }
+ }
+}
diff --git a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs
new file mode 100644
index 0000000000..aa2a88048d
--- /dev/null
+++ b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs
@@ -0,0 +1,769 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessorCore.Quantizers
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// An implementation of Wu's color quantizer with alpha channel.
+ ///
+ ///
+ ///
+ /// Based on C Implementation of Xiaolin Wu's Color Quantizer (v. 2)
+ /// (see Graphics Gems volume II, pages 126-133)
+ /// ().
+ ///
+ ///
+ /// This adaptation is based on the excellent JeremyAnsel.ColorQuant by Jérémy Ansel
+ ///
+ ///
+ ///
+ /// Algorithm: Greedy orthogonal bipartition of RGB space for variance
+ /// minimization aided by inclusion-exclusion tricks.
+ /// For speed no nearest neighbor search is done. Slightly
+ /// better performance can be expected by more sophisticated
+ /// but more expensive versions.
+ ///
+ ///
+ public sealed class WuQuantizer : IQuantizer
+ {
+ ///
+ /// The epsilon for comparing floating point numbers.
+ ///
+ private const float Epsilon = 0.001f;
+
+ ///
+ /// The index bits.
+ ///
+ private const int IndexBits = 6;
+
+ ///
+ /// The index alpha bits.
+ ///
+ private const int IndexAlphaBits = 3;
+
+ ///
+ /// The index count.
+ ///
+ private const int IndexCount = (1 << IndexBits) + 1;
+
+ ///
+ /// The index alpha count.
+ ///
+ private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
+
+ ///
+ /// The table length.
+ ///
+ private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
+
+ ///
+ /// Moment of P(c).
+ ///
+ private readonly long[] vwt;
+
+ ///
+ /// Moment of r*P(c).
+ ///
+ private readonly long[] vmr;
+
+ ///
+ /// Moment of g*P(c).
+ ///
+ private readonly long[] vmg;
+
+ ///
+ /// Moment of b*P(c).
+ ///
+ private readonly long[] vmb;
+
+ ///
+ /// Moment of a*P(c).
+ ///
+ private readonly long[] vma;
+
+ ///
+ /// Moment of c^2*P(c).
+ ///
+ private readonly double[] m2;
+
+ ///
+ /// Color space tag.
+ ///
+ private readonly byte[] tag;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WuQuantizer()
+ {
+ this.vwt = new long[TableLength];
+ this.vmr = new long[TableLength];
+ this.vmg = new long[TableLength];
+ this.vmb = new long[TableLength];
+ this.vma = new long[TableLength];
+ this.m2 = new double[TableLength];
+ this.tag = new byte[TableLength];
+ }
+
+ ///
+ public QuantizedImage Quantize(ImageBase image, int maxColors)
+ {
+ Guard.NotNull(image, nameof(image));
+
+ int colorCount = maxColors.Clamp(1, 256);
+
+ this.Clear();
+
+ this.Build3DHistogram(image);
+ this.Get3DMoments();
+
+ Box[] cube;
+ this.BuildCube(out cube, ref colorCount);
+
+ return this.GenerateResult(image, colorCount, cube);
+ }
+
+ ///
+ /// Gets an index.
+ ///
+ /// The red value.
+ /// The green value.
+ /// The blue value.
+ /// The alpha value.
+ /// The index.
+ private static int GetPalleteIndex(int r, int g, int b, int a)
+ {
+ return (r << ((IndexBits * 2) + IndexAlphaBits))
+ + (r << (IndexBits + IndexAlphaBits + 1))
+ + (g << (IndexBits + IndexAlphaBits))
+ + (r << (IndexBits * 2))
+ + (r << (IndexBits + 1))
+ + (g << IndexBits)
+ + ((r + g + b) << IndexAlphaBits)
+ + r + g + b + a;
+ }
+
+ ///
+ /// Computes sum over a box of any given statistic.
+ ///
+ /// The cube.
+ /// The moment.
+ /// The result.
+ private static double Volume(Box cube, long[] moment)
+ {
+ return moment[GetPalleteIndex(cube.R1, cube.G1, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, cube.G1, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+ }
+
+ ///
+ /// Computes part of Volume(cube, moment) that doesn't depend on r1, g1, or b1 (depending on direction).
+ ///
+ /// The cube.
+ /// The direction.
+ /// The moment.
+ /// The result.
+ private static long Bottom(Box cube, int direction, long[] moment)
+ {
+ switch (direction)
+ {
+ // Red
+ case 0:
+ return -moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+
+ // Green
+ case 1:
+ return -moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+
+ // Blue
+ case 2:
+ return -moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+
+ // Alpha
+ case 3:
+ return -moment[GetPalleteIndex(cube.R1, cube.G1, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction));
+ }
+ }
+
+ ///
+ /// Computes remainder of Volume(cube, moment), substituting position for r1, g1, or b1 (depending on direction).
+ ///
+ /// The cube.
+ /// The direction.
+ /// The position.
+ /// The moment.
+ /// The result.
+ private static long Top(Box cube, int direction, int position, long[] moment)
+ {
+ switch (direction)
+ {
+ // Red
+ case 0:
+ return moment[GetPalleteIndex(position, cube.G1, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(position, cube.G1, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(position, cube.G1, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(position, cube.G1, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(position, cube.G0, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(position, cube.G0, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(position, cube.G0, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(position, cube.G0, cube.B0, cube.A0)];
+
+ // Green
+ case 1:
+ return moment[GetPalleteIndex(cube.R1, position, cube.B1, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, position, cube.B1, cube.A0)]
+ - moment[GetPalleteIndex(cube.R1, position, cube.B0, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, position, cube.B0, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, position, cube.B1, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, position, cube.B1, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, position, cube.B0, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, position, cube.B0, cube.A0)];
+
+ // Blue
+ case 2:
+ return moment[GetPalleteIndex(cube.R1, cube.G1, position, cube.A1)]
+ - moment[GetPalleteIndex(cube.R1, cube.G1, position, cube.A0)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, position, cube.A1)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, position, cube.A0)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, position, cube.A1)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, position, cube.A0)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, position, cube.A1)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, position, cube.A0)];
+
+ // Alpha
+ case 3:
+ return moment[GetPalleteIndex(cube.R1, cube.G1, cube.B1, position)]
+ - moment[GetPalleteIndex(cube.R1, cube.G1, cube.B0, position)]
+ - moment[GetPalleteIndex(cube.R1, cube.G0, cube.B1, position)]
+ + moment[GetPalleteIndex(cube.R1, cube.G0, cube.B0, position)]
+ - moment[GetPalleteIndex(cube.R0, cube.G1, cube.B1, position)]
+ + moment[GetPalleteIndex(cube.R0, cube.G1, cube.B0, position)]
+ + moment[GetPalleteIndex(cube.R0, cube.G0, cube.B1, position)]
+ - moment[GetPalleteIndex(cube.R0, cube.G0, cube.B0, position)];
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(direction));
+ }
+ }
+
+ ///
+ /// 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 image.
+ private void Build3DHistogram(ImageBase image)
+ {
+ for (int y = 0; y < image.Height; y++)
+ {
+ for (int x = 0; x < image.Width; x++)
+ {
+ Bgra32 color = image[x, y];
+
+ byte r = color.R;
+ byte g = color.G;
+ byte b = color.B;
+ byte a = color.A;
+
+ int inr = r >> (8 - IndexBits);
+ int ing = g >> (8 - IndexBits);
+ int inb = b >> (8 - IndexBits);
+ int ina = a >> (8 - IndexAlphaBits);
+
+ int ind = GetPalleteIndex(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.
+ ///
+ private void Get3DMoments()
+ {
+ long[] volume = new long[IndexCount * IndexAlphaCount];
+ long[] volumeR = new long[IndexCount * IndexAlphaCount];
+ long[] volumeG = new long[IndexCount * IndexAlphaCount];
+ long[] volumeB = new long[IndexCount * IndexAlphaCount];
+ long[] volumeA = new long[IndexCount * IndexAlphaCount];
+ double[] volume2 = new double[IndexCount * IndexAlphaCount];
+
+ long[] area = new long[IndexAlphaCount];
+ long[] areaR = new long[IndexAlphaCount];
+ long[] areaG = new long[IndexAlphaCount];
+ long[] areaB = new long[IndexAlphaCount];
+ long[] areaA = new long[IndexAlphaCount];
+ double[] area2 = new double[IndexAlphaCount];
+
+ for (int r = 1; r < IndexCount; r++)
+ {
+ Array.Clear(volume, 0, IndexCount * IndexAlphaCount);
+ Array.Clear(volumeR, 0, IndexCount * IndexAlphaCount);
+ Array.Clear(volumeG, 0, IndexCount * IndexAlphaCount);
+ Array.Clear(volumeB, 0, IndexCount * IndexAlphaCount);
+ Array.Clear(volumeA, 0, IndexCount * IndexAlphaCount);
+ Array.Clear(volume2, 0, IndexCount * IndexAlphaCount);
+
+ for (int g = 1; g < IndexCount; g++)
+ {
+ Array.Clear(area, 0, IndexAlphaCount);
+ Array.Clear(areaR, 0, IndexAlphaCount);
+ Array.Clear(areaG, 0, IndexAlphaCount);
+ Array.Clear(areaB, 0, IndexAlphaCount);
+ Array.Clear(areaA, 0, IndexAlphaCount);
+ Array.Clear(area2, 0, IndexAlphaCount);
+
+ for (int b = 1; b < IndexCount; b++)
+ {
+ long line = 0;
+ long lineR = 0;
+ long lineG = 0;
+ long lineB = 0;
+ long lineA = 0;
+ double line2 = 0;
+
+ for (int a = 1; a < IndexAlphaCount; a++)
+ {
+ int ind1 = GetPalleteIndex(r, g, b, a);
+
+ line += this.vwt[ind1];
+ lineR += this.vmr[ind1];
+ lineG += this.vmg[ind1];
+ lineB += this.vmb[ind1];
+ lineA += this.vma[ind1];
+ line2 += this.m2[ind1];
+
+ area[a] += line;
+ areaR[a] += lineR;
+ areaG[a] += lineG;
+ areaB[a] += lineB;
+ areaA[a] += lineA;
+ area2[a] += line2;
+
+ int inv = (b * IndexAlphaCount) + a;
+
+ volume[inv] += area[a];
+ volumeR[inv] += areaR[a];
+ volumeG[inv] += areaG[a];
+ volumeB[inv] += areaB[a];
+ volumeA[inv] += areaA[a];
+ volume2[inv] += area2[a];
+
+ int ind2 = ind1 - GetPalleteIndex(1, 0, 0, 0);
+
+ this.vwt[ind1] = this.vwt[ind2] + volume[inv];
+ this.vmr[ind1] = this.vmr[ind2] + volumeR[inv];
+ this.vmg[ind1] = this.vmg[ind2] + volumeG[inv];
+ this.vmb[ind1] = this.vmb[ind2] + volumeB[inv];
+ this.vma[ind1] = this.vma[ind2] + volumeA[inv];
+ this.m2[ind1] = this.m2[ind2] + volume2[inv];
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Computes the weighted variance of a box cube.
+ ///
+ /// The cube.
+ /// The .
+ private double Variance(Box cube)
+ {
+ double dr = Volume(cube, this.vmr);
+ double dg = Volume(cube, this.vmg);
+ double db = Volume(cube, this.vmb);
+ double da = Volume(cube, this.vma);
+
+ double xx =
+ this.m2[GetPalleteIndex(cube.R1, cube.G1, cube.B1, cube.A1)]
+ - this.m2[GetPalleteIndex(cube.R1, cube.G1, cube.B1, cube.A0)]
+ - this.m2[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A1)]
+ + this.m2[GetPalleteIndex(cube.R1, cube.G1, cube.B0, cube.A0)]
+ - this.m2[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A1)]
+ + this.m2[GetPalleteIndex(cube.R1, cube.G0, cube.B1, cube.A0)]
+ + this.m2[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A1)]
+ - this.m2[GetPalleteIndex(cube.R1, cube.G0, cube.B0, cube.A0)]
+ - this.m2[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A1)]
+ + this.m2[GetPalleteIndex(cube.R0, cube.G1, cube.B1, cube.A0)]
+ + this.m2[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A1)]
+ - this.m2[GetPalleteIndex(cube.R0, cube.G1, cube.B0, cube.A0)]
+ + this.m2[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A1)]
+ - this.m2[GetPalleteIndex(cube.R0, cube.G0, cube.B1, cube.A0)]
+ - this.m2[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A1)]
+ + this.m2[GetPalleteIndex(cube.R0, cube.G0, cube.B0, cube.A0)];
+
+ return xx - (((dr * dr) + (dg * dg) + (db * db) + (da * da)) / Volume(cube, this.vwt));
+ }
+
+ ///
+ /// We want to minimize the sum of the variances of two sub-boxes.
+ /// The sum(c^2) terms can be ignored since their sum over both sub-boxes
+ /// is the same (the sum for the whole box) no matter where we split.
+ /// The remaining terms have a minus sign in the variance formula,
+ /// so we drop the minus sign and maximize the sum of the two terms.
+ ///
+ /// The cube.
+ /// The direction.
+ /// The first position.
+ /// The last position.
+ /// The cutting point.
+ /// The whole red.
+ /// The whole green.
+ /// The whole blue.
+ /// The whole alpha.
+ /// The whole weight.
+ /// The .
+ private double Maximize(Box cube, int direction, int first, int last, out int cut, double wholeR, double wholeG, double wholeB, double wholeA, double wholeW)
+ {
+ long baseR = Bottom(cube, direction, this.vmr);
+ long baseG = Bottom(cube, direction, this.vmg);
+ long baseB = Bottom(cube, direction, this.vmb);
+ long baseA = Bottom(cube, direction, this.vma);
+ long baseW = Bottom(cube, direction, this.vwt);
+
+ double max = 0.0;
+ cut = -1;
+
+ for (int i = first; i < last; i++)
+ {
+ double halfR = baseR + Top(cube, direction, i, this.vmr);
+ double halfG = baseG + Top(cube, direction, i, this.vmg);
+ double halfB = baseB + Top(cube, direction, i, this.vmb);
+ double halfA = baseA + Top(cube, direction, i, this.vma);
+ double halfW = baseW + Top(cube, direction, i, this.vwt);
+
+ double temp;
+
+ if (Math.Abs(halfW) < Epsilon)
+ {
+ continue;
+ }
+
+ temp = ((halfR * halfR) + (halfG * halfG) + (halfB * halfB) + (halfA * halfA)) / halfW;
+
+ halfR = wholeR - halfR;
+ halfG = wholeG - halfG;
+ halfB = wholeB - halfB;
+ halfA = wholeA - halfA;
+ halfW = wholeW - halfW;
+
+ if (Math.Abs(halfW) < Epsilon)
+ {
+ continue;
+ }
+
+ temp += ((halfR * halfR) + (halfG * halfG) + (halfB * halfB) + (halfA * halfA)) / halfW;
+
+ if (temp > max)
+ {
+ max = temp;
+ cut = i;
+ }
+ }
+
+ return max;
+ }
+
+ ///
+ /// Cuts a box.
+ ///
+ /// The first set.
+ /// The second set.
+ /// Returns a value indicating whether the box has been split.
+ private bool Cut(Box set1, Box set2)
+ {
+ double wholeR = Volume(set1, this.vmr);
+ double wholeG = Volume(set1, this.vmg);
+ double wholeB = Volume(set1, this.vmb);
+ double wholeA = Volume(set1, this.vma);
+ double wholeW = Volume(set1, this.vwt);
+
+ int cutr;
+ int cutg;
+ int cutb;
+ int cuta;
+
+ double maxr = this.Maximize(set1, 0, set1.R0 + 1, set1.R1, out cutr, wholeR, wholeG, wholeB, wholeA, wholeW);
+ double maxg = this.Maximize(set1, 1, set1.G0 + 1, set1.G1, out cutg, wholeR, wholeG, wholeB, wholeA, wholeW);
+ double maxb = this.Maximize(set1, 2, set1.B0 + 1, set1.B1, out cutb, wholeR, wholeG, wholeB, wholeA, wholeW);
+ double maxa = this.Maximize(set1, 3, set1.A0 + 1, set1.A1, out cuta, wholeR, wholeG, wholeB, wholeA, wholeW);
+
+ int dir;
+
+ if ((maxr >= maxg) && (maxr >= maxb) && (maxr >= maxa))
+ {
+ dir = 0;
+
+ if (cutr < 0)
+ {
+ return false;
+ }
+ }
+ else if ((maxg >= maxr) && (maxg >= maxb) && (maxg >= maxa))
+ {
+ dir = 1;
+ }
+ else if ((maxb >= maxr) && (maxb >= maxg) && (maxb >= maxa))
+ {
+ dir = 2;
+ }
+ else
+ {
+ dir = 3;
+ }
+
+ set2.R1 = set1.R1;
+ set2.G1 = set1.G1;
+ set2.B1 = set1.B1;
+ set2.A1 = set1.A1;
+
+ switch (dir)
+ {
+ // Red
+ case 0:
+ set2.R0 = set1.R1 = cutr;
+ set2.G0 = set1.G0;
+ set2.B0 = set1.B0;
+ set2.A0 = set1.A0;
+ break;
+
+ // Green
+ case 1:
+ set2.G0 = set1.G1 = cutg;
+ set2.R0 = set1.R0;
+ set2.B0 = set1.B0;
+ set2.A0 = set1.A0;
+ break;
+
+ // Blue
+ case 2:
+ set2.B0 = set1.B1 = cutb;
+ set2.R0 = set1.R0;
+ set2.G0 = set1.G0;
+ set2.A0 = set1.A0;
+ break;
+
+ // Alpha
+ case 3:
+ set2.A0 = set1.A1 = cuta;
+ set2.R0 = set1.R0;
+ set2.G0 = set1.G0;
+ set2.B0 = set1.B0;
+ break;
+ }
+
+ set1.Volume = (set1.R1 - set1.R0) * (set1.G1 - set1.G0) * (set1.B1 - set1.B0) * (set1.A1 - set1.A0);
+ set2.Volume = (set2.R1 - set2.R0) * (set2.G1 - set2.G0) * (set2.B1 - set2.B0) * (set2.A1 - set2.A0);
+
+ return true;
+ }
+
+ ///
+ /// Marks a color space tag.
+ ///
+ /// The cube.
+ /// A label.
+ private void Mark(Box cube, byte label)
+ {
+ for (int r = cube.R0 + 1; r <= cube.R1; r++)
+ {
+ for (int g = cube.G0 + 1; g <= cube.G1; g++)
+ {
+ for (int b = cube.B0 + 1; b <= cube.B1; b++)
+ {
+ for (int a = cube.A0 + 1; a <= cube.A1; a++)
+ {
+ this.tag[GetPalleteIndex(r, g, b, a)] = label;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Builds the cube.
+ ///
+ /// The cube.
+ /// The color count.
+ private void BuildCube(out Box[] cube, ref int colorCount)
+ {
+ cube = new Box[colorCount];
+ double[] vv = new double[colorCount];
+
+ for (int i = 0; i < colorCount; i++)
+ {
+ cube[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;
+
+ int next = 0;
+
+ for (int i = 1; i < colorCount; i++)
+ {
+ if (this.Cut(cube[next], cube[i]))
+ {
+ vv[next] = cube[next].Volume > 1 ? this.Variance(cube[next]) : 0.0;
+ vv[i] = cube[i].Volume > 1 ? this.Variance(cube[i]) : 0.0;
+ }
+ else
+ {
+ vv[next] = 0.0;
+ i--;
+ }
+
+ next = 0;
+
+ double temp = vv[0];
+ for (int k = 1; k <= i; k++)
+ {
+ if (vv[k] > temp)
+ {
+ temp = vv[k];
+ next = k;
+ }
+ }
+
+ if (temp <= 0.0)
+ {
+ colorCount = i + 1;
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Generates the quantized result.
+ ///
+ /// The image.
+ /// The color count.
+ /// The cube.
+ /// The result.
+ private QuantizedImage GenerateResult(ImageBase image, int colorCount, Box[] cube)
+ {
+ List pallette = new List();
+ byte[] pixels = new byte[image.Width * image.Height];
+ int transparentIndex = 0;
+
+ for (int k = 0; k < colorCount; k++)
+ {
+ this.Mark(cube[k], (byte)k);
+
+ double weight = Volume(cube[k], this.vwt);
+
+ 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);
+
+ var color = new Bgra32(b, g, r, a);
+
+ if (color == Bgra32.Empty)
+ {
+ transparentIndex = k;
+ }
+
+ pallette.Add(color);
+ }
+ else
+ {
+ pallette.Add(Bgra32.Empty);
+ transparentIndex = k;
+ }
+ }
+
+ // TODO: Optimize here.
+ int i = 0;
+ for (int y = 0; y < image.Height; y++)
+ {
+ for (int x = 0; x < image.Width; x++)
+ {
+ Bgra32 color = image[x, y];
+ int a = color.A >> (8 - IndexAlphaBits);
+ int r = color.R >> (8 - IndexBits);
+ int g = color.G >> (8 - IndexBits);
+ int b = color.B >> (8 - IndexBits);
+
+ int ind = GetPalleteIndex(r + 1, g + 1, b + 1, a + 1);
+ pixels[i++] = this.tag[ind];
+ }
+ }
+
+ return new QuantizedImage(image.Width, image.Height, pallette.ToArray(), pixels, transparentIndex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs b/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs
index 310fa4a3bc..c5eea6ba05 100644
--- a/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs
+++ b/tests/ImageProcessorCore.Tests/Processors/Formats/EncoderDecoderTests.cs
@@ -7,6 +7,9 @@
using Xunit;
using System.Linq;
+
+ using ImageProcessorCore.Quantizers;
+
public class EncoderDecoderTests : ProcessorTestBase
{
[Fact]
@@ -55,7 +58,7 @@
{
Image image = new Image(stream);
IQuantizer quantizer = new OctreeQuantizer();
- QuantizedImage quantizedImage = quantizer.Quantize(image);
+ QuantizedImage quantizedImage = quantizer.Quantize(image, 256);
using (FileStream output = File.OpenWrite($"TestOutput/Quantize/{Path.GetFileName(file)}"))
{