Image Color Number Reduction with alpha support using RGBQuant/NeuQuant/Xiaolin Wu's algorithms and Euclidean/Manhattan/CIEDE2000 color distance formulas in TypeScript
import { PNG } from 'pngjs';
import { buildPaletteSync, utils } from 'image-q';
// read file
const { data, width, height } = PNG.sync.read(fs.readFileSync('file.png'));
const inPointContainer = utils.PointContainer.fromUint8Array(data, width, height);
// convert
const palette = buildPaletteSync([inPointContainer]);
const outPointContainer = applyPaletteSync(inPointContainer, palette);
// use outPointContainer.toUint8Array() somehowThis API allows to Build (quantize) palette using Sample Images, returns [[Palette]] instance.
import { buildPalette } from 'image-q'; // or const buildPalette = require('image-q').buildPalette
const palette = await buildPalette([pointContainer], {
colorDistanceFormula: 'euclidean', // optional
paletteQuantization: 'neuquant', // optional
colors: 128, // optional
onProgress: progress => console.log('applyPalette', progress), // optional
});import { buildPaletteSync } from 'image-q'; // or const buildPaletteSync = require('image-q').buildPaletteSync
const palette = buildPaletteSync([pointContainer], {
colorDistanceFormula: 'euclidean', // optional
paletteQuantization: 'neuquant', // optional
colors: 128, // optional
});implementation detail: generator is wrapped with setImmediate (polyfilled)
This API applies given [[Palette]] to the [[PointContainer]], returns [[PointContainer]] containing new image.
import { applyPalette } from 'image-q'; // or const applyPalette = require('image-q').applyPalette
const outPointContainer = await applyPalette(pointContainer, palette, {
colorDistanceFormula: 'euclidean', // optional
imageQuantization: 'floyd-steinberg', // optional
onProgress: progress => console.log('applyPalette', progress), // optional
});import { applyPaletteSync } from 'image-q'; // or const applyPaletteSync = require('image-q').applyPaletteSync
const outPointContainer = applyPaletteSync(pointContainer, palette, {
colorDistanceFormula: 'euclidean', // optional
imageQuantization: 'floyd-steinberg', // optional
});See description of string constants in Advanced API Section
export type ColorDistanceFormula =
| 'cie94-textiles'
| 'cie94-graphic-arts'
| 'ciede2000'
| 'color-metric'
| 'euclidean'
| 'euclidean-bt709-noalpha'
| 'euclidean-bt709'
| 'manhattan'
| 'manhattan-bt709'
| 'manhattan-nommyde'
| 'pngquant';export type PaletteQuantization =
| 'neuquant'
| 'neuquant-float'
| 'rgbquant'
| 'wuquant';export type ImageQuantization =
| 'nearest'
| 'riemersma'
| 'floyd-steinberg'
| 'false-floyd-steinberg'
| 'stucki'
| 'atkinson'
| 'jarvis'
| 'burkes'
| 'sierra'
| 'two-sierra'
| 'sierra-lite';| API | Source | |
|---|---|---|
| Canvas related | ||
| [[PointContainer.fromHTMLCanvasElement]] | HTMLCanvasElement | |
| [[PointContainer.fromImageData]] | ImageData | ctx.getImageData() |
| [[PointContainer.fromUint8Array]] | Uint8ClampedArray | ctx.getImageData().data |
| [[PointContainer.fromUint8Array]] | deprecated CanvasPixelArray | ctx.getImageData().data |
| Other | ||
| [[PointContainer.fromHTMLImageElement]] | HTMLImageElement | |
| [[PointContainer.fromImageData]] | Array | |
| [[PointContainer.fromUint8Array]] | Uint8Array | |
| [[PointContainer.fromUint32Array]] | Uint32Array | |
| [[PointContainer.fromBuffer]] | Buffer (Node.js) |
Usage:
const canvas = document.querySelector('#canvas');
const pointContainer = PointContainer.fromHTMLCanvasElement(canvas);| API | Description | Originally used by |
|---|---|---|
| [[Euclidean]] | 1/1/1/1 coefficients | WuQuant |
| [[EuclideanBT709]] | BT.709 sRGB coefficients | |
| [[Manhattan]] | 1/1/1/1 coefficients | NeuQuant |
| [[ManhattanBT709]] | BT.709 sRGB coefficients | |
| [[CIEDE2000]] | CIEDE2000 (very slow) | |
| [[CIE94Textiles]] | CIE94 implementation for textiles | |
| [[CIE94GraphicArts]] | CIE94 implementation for graphic arts | |
| [[CMetric]] | see | |
| [[PNGQuant]] | used in PNGQuant tools | |
| [[EuclideanBT709NoAlpha]] | BT.709 sRGB coefficients | RGBQuant |
| [[ManhattanNommyde]] | discussion |
Usage:
const distanceCalculator = new EuclideanBT709();| API | Description |
|---|---|
| [[NeuQuant]] | original code ported, integer calculations |
| [[RGBQuant]] | |
| [[WuQuant]] | |
| [[NeuQuantFloat]] | floating-point calculations |
Usage (sync):
const paletteQuantizer = new WuQuant(distanceCalculator, 256);
paletteQuantizer.sample(pointContainer1);
paletteQuantizer.sample(pointContainer2);
const palette = paletteQuantizer.quantizeSync();Usage (generator):
// example 1
const paletteQuantizer = new WuQuant(distanceCalculator, 256);
paletteQuantizer.sample(pointContainer1);
paletteQuantizer.sample(pointContainer2);
const generator = paletteQuantizer.quantize();
let palette;
while(true) {
// calling to generator.next() may be easily wrapped with setTimeout to make it async
const result = generator.next();
if (result.done) break;
if (result.value.palette) palette = result.palette;
console.log(`${result.value.progress}% done`);
}
// example 2
const paletteQuantizer = new WuQuant(distanceCalculator, 256);
paletteQuantizer.sample(pointContainer1);
paletteQuantizer.sample(pointContainer2);
const palette = Array.from(paletteQuantizer.quantize()).pop().palette;| API | Description |
|---|---|
| [[NearestColor]] | |
| [[ErrorDiffusionArray]] | 2 modes of error propagation are supported: xnview and gimp |
| - 1. [[FloydSteinberg]] | |
| - 2. [[FalseFloydSteinberg]] | |
| - 3. [[Stucki]] | |
| - 4. [[Atkinson]] | |
| - 5. [[Jarvis]] | |
| - 6. [[Burkes]] | |
| - 7. [[Sierra]] | |
| - 8. [[TwoSierra]] | |
| - 9. [[SierraLite]] | |
| [[ErrorDiffusionRiemersma]] | Hilbert space-filling curve is used |
Usage (sync):
const imageQuantizer = new ErrorDiffusionArray(distanceCalculator, ErrorDiffusionArrayKernel.Jarvis);
const outPointContainer = imageQuantizer.quantizeSync(inPointContainer, palette);Usage (generator):
// example 1
const imageQuantizer = new ErrorDiffusionArray(distanceCalculator, ErrorDiffusionArrayKernel.Jarvis);
const generator = imageQuantizer.quantize(inPointContainer, palette);
let outPointContainer;
while(true) {
// calling to generator.next() may be easily wrapped with setTimeout to make it async
const result = generator.next();
if (result.done) break;
if (result.value.pointContainer) outPointContainer = result.pointContainer;
console.log(`${result.value.progress}% done`);
}
// example 2
const imageQuantizer = new ErrorDiffusionArray(distanceCalculator, ErrorDiffusionArrayKernel.Jarvis);
const outPointContainer = Array.from(imageQuantizer.quantize(inPointContainer, palette)).pop().pointContainer;| API | Description |
|---|---|
| [[PointContainer.toUint8Array]] | Uint8Array |
| [[PointContainer.toUint32Array]] | Uint32Array |
Usage:
// write PNG using pngjs
png.data = outPointContainer.toUint8Array();
fs.writeFileSync('filename.png', PNG.sync.write(png))var img = document.createElement("img");
img.onload = function() {
// image is loaded, here should be all code utilizing image
...
}
img.src = "http://pixabay.com/static/uploads/photo/2012/04/11/11/32/letter-a-27580_640.png"// desired colors number
var targetColors = 256;
// create pointContainer and fill it with image
var pointContainer = iq.utils.PointContainer.fromHTMLImageElement(img);
// create chosen distance calculator (see classes inherited from `iq.distance.AbstractDistanceCalculator`)
var distanceCalculator = new iq.distance.Euclidean();
// create chosen palette quantizer (see classes implementing `iq.palette.AbstractPaletteQuantizer`)
var paletteQuantizer = new iq.palette.RGBQuant(distanceCalculator, targetColors);
// feed out pointContainer filled with image to paletteQuantizer
paletteQuantizer.sample(pointContainer);
... (you may sample more than one image to create mutual palette)
// take generated palette
var palette = paletteQuantizer.quantizeSync();// create image quantizer (see classes implementing `iq.image.AbstractImageQuantizer`)
var imageQuantizer = new iq.image.NearestColor(distanceCalculator);
// apply palette to image
var resultPointContainer = imageQuantizer.quantizeSync(pointContainer, palette);You may work with resultPointContainer directly or you may convert it to Uint8Array/Uint32Array
var uint8array = resultPointContainer.toUint8Array();please also refer to tests
| API | Description |
|---|---|
| [[lab2rgb]] | CIE L*a*b* => CIE RGB |
| [[lab2xyz]] | CIE L*a*b* => CIE XYZ |
| [[rgb2hsl]] | CIE RGB => HSL |
| [[rgb2lab]] | CIE RGB => CIE L*a*b* |
| [[rgb2xyz]] | CIE RGB => CIE XYZ |
| [[xyz2lab]] | CIE XYZ => CIE L*a*b* |
| [[xyz2rgb]] | CIE XYZ => CIE RGB |
https://wolfcrow.com/blog/what-is-the-difference-between-cie-lab-cie-rgb-cie-xyy-and-cie-xyz/
-
[[ssim]] - https://en.wikipedia.org/wiki/Structural_similarity
Usage:
const similarity = ssim(pointContainer1, pointContainer2);
Have fun! Any problems or queries let me know!
-- Igor