RgbQuant.js
an image quantization lib (MIT Licensed)
Intro
Color quantization is the process of reducing an image with thousands or millions of colors to one with fewer (usually 256). The trick is to balance speed, cpu and memory requirements while minimizing the perceptual loss in output quality. More info can be found on wikipedia. Various algorithms can be found on rosettacode.org.
RgbQuant.js is not a port or implementation of any specific quantization algorithm, though some overlap is inevitable.
Usage
Use Chrome, Firefox or IE10+ since many HTML5/JS features are used. Canvas, Typed Arrays, Array.forEach.
var opts = {
colors: 256,
method: 2,
boxSize: [64,64],
boxPxls: 2,
initColors: 4096,
minHueCols: 0,
dithKern: null,
dithDelta: 0,
dithSerp: false,
palette: [],
reIndex: false,
useCache: true,
cacheFreq: 10,
colorDist: "euclidean",
};
var q = new RgbQuant(opts);
q.sample(imgA);
q.sample(imgB);
q.sample(imgC);
var pal = q.palette();
var outA = q.reduce(imgA),
outB = q.reduce(imgB),
outC = q.reduce(imgC);
Docs
.sample(image, width) - Performs histogram analysis.
image
may be any of <img>, <canvas>, Context2D, ImageData, Typed Array, Array.
width
is required if image
is an array.
.palette(tuples, noSort) - Retrieves the palette, building it on first call.
tuples
if true
will return an array of [r,g,b]
triplets, otherwise a Uint8Array is returned by default.
noSort
if true
will disable palette sorting by hue/luminance and leaves it ordered from highest to lowest color occurrence counts.
.reduce(image, retType, dithKern, dithSerp) - Quantizes an image.
image
can be any of the types specified for .sample()
above.
retType
determines returned type. 1
- Uint8Array (default), 2
- Indexed array.
dithKern
is a dithering kernel that can override what was specified in global opts (off by default), available options are:
- FloydSteinberg
- FalseFloydSteinberg
- Stucki
- Atkinson
- Jarvis
- Burkes
- Sierra
- TwoSierra
- SierraLite
dithSerp
can be true
or false
and determines if dithering is done in a serpentine pattern.
* Transparent pixels will result in a sparse indexed array.
Caveats & Tips
RgbQuant.js, as any quantizer, makes trade-offs which affect its performance in certain cases. Some parameters may be tweaked to improve the quality of the output at the expense of palette computation and reduction speed. Since the methods used to determine the palette are based on occurrence counts of each pixel's color, three problematic situations can arise.
- No two pixels are the same color. eg: unscaled bidirectional gradients.
- Visually distinctive but low-density hues are overwhelmed by dissimilar, dominating hues. (see Quantum Frog)
- Hues are numerous and densities relatively equal such that choosing the 'top' ones is unpredictable and eliminates a large number of important ones. (see Fish)
The symptom of these issues is a lack of important color groups in the final palette which results in poorly reduced images.
Frequently, the solution is to set minHueCols: 256
during instantiation. What this will do is inject the first 256 encountered distinct colors for each hue group (by default there are 10) into the initial palette for analysis. In effect, it forces each encountered hue group to be represented, regardless of specific color counts. If using method: 1
, you may additionally increase initColors
to advance the slicing point of the frequency-sorted initial histogram.
These adjustments come with a (often significant) speed penalty for palette generation. Reduction passes may also be affected because of the internal memoization/caching used during palette building.
Why?
Let me acknowledge the elephant in the room: why not just use or port an existing quantization algorithm? As far as JS ports go, there are really only 3.5 options (which implement 2.5 algos).
My original goal was to upscale frames from <canvas>
graphics animations and pixelated SNES-style games for GIFter.js. It became apparent after trying the first three options that I would need something different.