rgbquant.ts 6.14 KB
/*
 * Copyright (c) 2015, Leon Sorokin
 * All rights reserved. (MIT Licensed)
 *
 * RgbQuant.js - an image quantization lib
 */

/**
 * @preserve TypeScript port:
 * Copyright 2015-2016 Igor Bezkrovnyi
 * All rights reserved. (MIT Licensed)
 *
 * rgbquant.ts - part of Image Quantization Library
 */

import { Palette } from "../../utils/palette"
import { Point } from "../../utils/point"
import { PointContainer } from "../../utils/pointContainer"
import { AbstractDistanceCalculator } from "../../distance/abstractDistanceCalculator"
import { ColorHistogram } from "./colorHistogram"
import { IPaletteQuantizer } from "../common"
import { stableSort } from "../../utils/arithmetic"

class RemovedColor {
    readonly index : number;
    readonly color : Point;
    readonly distance : number;

    constructor(index : number, color : Point, distance : number) {
        this.index    = index;
        this.color    = color;
        this.distance = distance;
    }
}

// TODO: make input/output image and input/output palettes with instances of class Point only!
export class RGBQuant implements IPaletteQuantizer {
    // desired final palette size
    private readonly _colors : number;

    // color-distance threshold for initial reduction pass
    private readonly _initialDistance : number;

    // subsequent passes threshold
    private readonly _distanceIncrement : number;

    // accumulated histogram
    private readonly _histogram : ColorHistogram;
    private readonly _distance : AbstractDistanceCalculator;

    constructor(colorDistanceCalculator : AbstractDistanceCalculator, colors : number = 256, method : number = 2) {
        this._distance = colorDistanceCalculator;
        // desired final palette size
        this._colors   = colors;

        // histogram to accumulate
        this._histogram = new ColorHistogram(method, colors);

        this._initialDistance   = 0.01;
        this._distanceIncrement = 0.005;
    }

    // gathers histogram info
    sample(image : PointContainer) : void {
        /*
         var pointArray = image.getPointArray(), max = [0, 0, 0, 0], min = [255, 255, 255, 255];

         for (var i = 0, l = pointArray.length; i < l; i++) {
         var color = pointArray[i];
         for (var componentIndex = 0; componentIndex < 4; componentIndex++) {
         if (max[componentIndex] < color.rgba[componentIndex]) max[componentIndex] = color.rgba[componentIndex];
         if (min[componentIndex] > color.rgba[componentIndex]) min[componentIndex] = color.rgba[componentIndex];
         }
         }
         var rd = max[0] - min[0], gd = max[1] - min[1], bd = max[2] - min[2], ad = max[3] - min[3];
         this._distance.setWhitePoint(rd, gd, bd, ad);

         this._initialDistance = (Math.sqrt(rd * rd + gd * gd + bd * bd + ad * ad) / Math.sqrt(255 * 255 + 255 * 255 + 255 * 255)) * 0.01;
         */

        this._histogram.sample(image);
    }

    // reduces histogram to palette, remaps & memoizes reduced colors
    quantize() : Palette {
        const idxi32 = this._histogram.getImportanceSortedColorsIDXI32()
        if (idxi32.length === 0) {
            throw new Error("No colors in image")
        }

        const palette = this._buildPalette(idxi32);

        palette.sort();
        return palette;
    }

    // reduces similar colors from an importance-sorted Uint32 rgba array
    private _buildPalette(idxi32 : number[]) : Palette {
        // reduce histogram to create initial palette
        // build full rgb palette
        const palette    = new Palette(),
              colorArray = palette.getPointContainer().getPointArray(),
              usageArray = new Array(idxi32.length);

        for (let i = 0; i < idxi32.length; i++) {
            colorArray.push(Point.createByUint32(idxi32[ i ]));
            usageArray[ i ] = 1;
        }

        const len                      = colorArray.length,
              memDist : RemovedColor[] = [];

        let palLen = len,
            thold  = this._initialDistance;

        // palette already at or below desired length
        while (palLen > this._colors) {
            memDist.length = 0;

            // iterate palette
            for (let i = 0; i < len; i++) {
                if (usageArray[ i ] === 0) continue;
                const pxi = colorArray[ i ];
                //if (!pxi) continue;

                for (let j = i + 1; j < len; j++) {
                    if (usageArray[ j ] === 0) continue;
                    const pxj = colorArray[ j ];
                    //if (!pxj) continue;

                    const dist = this._distance.calculateNormalized(pxi, pxj);
                    if (dist < thold) {
                        // store index,rgb,dist
                        memDist.push(new RemovedColor(j, pxj, dist));
                        usageArray[ j ] = 0;
                        palLen--;
                    }
                }
            }
            // palette reduction pass
            // console.log("palette length: " + palLen);

            // if palette is still much larger than target, increment by larger initDist
            thold += (palLen > this._colors * 3) ? this._initialDistance : this._distanceIncrement;
        }

        // if palette is over-reduced, re-add removed colors with largest distances from last round
        if (palLen < this._colors) {
            // sort descending
            stableSort(memDist, function (a : RemovedColor, b : RemovedColor) {
                return b.distance - a.distance;
            });

            let k = 0;
            while (palLen < this._colors && k < memDist.length) {
                const removedColor               = memDist[ k ];
                // re-inject rgb into final palette
                usageArray[ removedColor.index ] = 1;
                palLen++;
                k++;
            }
        }

        let colors = colorArray.length;
        for (let colorIndex = colors - 1; colorIndex >= 0; colorIndex--) {
            if (usageArray[ colorIndex ] === 0) {
                if (colorIndex !== colors - 1) {
                    colorArray[ colorIndex ] = colorArray[ colors - 1 ];
                }
                --colors;
            }
        }
        colorArray.length = colors;

        return palette;
    }

}