# All files / fast-colors/src color-quantization.ts

97.14% Statements 68/70
82.05% Branches 32/39
100% Functions 7/7
97.06% Lines 66/68

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    5x
    5x

    4168x

    4168x

    4168x
    242064x
    85338x

    4168x

    5x

    242428x

    156914x

    85514x

    20640x

    20640x

    5x

    4x

    4x

    4x
    4x

    4x

    4x
    7418x

    4x
    4x
    2120x
    2120x

    2120x

    2120x
    2120x

    2120x
    156x

    2120x
    2120x
    2x

    2118x

    4x
    4x
    10974x

    4x
    654x
    654x
    654x
    10x
    644x
    280x

    364x

    4x
    4x
    2044x
    2044x

    2044x

    2044x
    2044x

    2044x
    204x

    2044x
    2044x
    2x

    2042x

    4x
    4x
    4x
    364x
    176x

    176x

    4x

     import { PixelBlob } from "./pixel-blob";
    import { Histogram } from "./histogram";
    import { insertIntoSortedList, PixelBox } from "./pixel-box";
    import { ColorRGBA64 } from "./color-rgba-64";

    function countValidBoxes(
        queue: PixelBox[],
        isBoxValid: ((box: PixelBox) => boolean) | null
    ): number {
        Iif (isBoxValid === null) {
            return queue.length;
        }

        let retVal: number = 0;

        for (let i: number = 0; i < queue.length; i++) {
            if (isBoxValid(queue[i])) {
                retVal++;
            }
        }

        return retVal;
    }

    export interface QuantizedColor {
        color: ColorRGBA64;
        pixelCount: number;
        colorVolume: number;
    }

    export interface QuantizeConfig {
        /**
         * Must be in the range [1,8]. Memory use increases as 4*2^(3*significantBits). Setting significantBits to 8 requires a 64 megabyte histogram.
         */
        significantBits: number;
        /**
         * Lowering this value increases the CPU load but includes more pixels in the calculation.
         */
        pixelSkipping: number;
        /**
         * Desired output palette size. Actual output may vary in edge cases such as images with very few colors.
         */
        targetPaletteSize: number;
        /**
         * For a final palette of size targetPaletteSize, we determine the first fractionByPopulation*targetPaletteSize using population as the only factor when determening sort order. For the rest of the colors the sort order is population*colorVolume. This helps highly contrasting colors in a small area to show up in some of the final output.
         */

        fractionByPopulation: number;
        /**
         * This predicate can be used to screen out undesirable colors from the final output. EG: excluding colors with a pixelCount below a min value.
         */
        isBoxValid: ((box: PixelBox) => boolean) | null;
        /**
         * This predicate can be used to exlude pixels from the histogram. It is passed numbers in the range [0,255] in rgba order. EG: Excluding colors too close to pure white or ones which are transparent.
         */
        isHistogramPixelValid: ((pixel: number[]) => boolean) | null;
        /**
         * If the quantization process goes on for more iterations than maxIterations it is aborted and the current results are returned. Only likely to happen in extreme edge cases with strange input.
         */
        maxIterations: number;
    }

    export const defaultQuantizeConfig: QuantizeConfig = {
        significantBits: 5,
        pixelSkipping: 5,
        targetPaletteSize: 64,
        fractionByPopulation: 0.85,
        isBoxValid: (box: PixelBox): boolean => {
            if (box.pixelCount < 5) {
                // Screen out outlier results that aren't present in the original image
                return false;
            }
            return true;
        },
        isHistogramPixelValid: (pixel: number[]): boolean => {
            Iif (pixel[3] < 128) {
                // Ignore pixels that are too transparent
                return false;
            }
            return true;
        },
        maxIterations: 1000,
    };

    /**
     * The image stored in the source PixelBlob is reduced down to a small set of colors.
     * Based on the Modified Median Cut Quantization implementation from https://github.com/DanBloomberg/leptonica/blob/master/src/colorquant2.c
     */
    export function quantize(
        source: PixelBlob,
        config: QuantizeConfig = defaultQuantizeConfig
    ): QuantizedColor[] {
        const histogram: Histogram = new Histogram(
            source,
            config.significantBits,
            config.pixelSkipping,
            config.isHistogramPixelValid
        );
        const initialBox: PixelBox = new PixelBox(
            histogram,
            histogram.minRed,
            histogram.maxRed,
            histogram.minGreen,
            histogram.maxGreen,
            histogram.minBlue,
            histogram.maxBlue
        );

        const queue: PixelBox[] = [initialBox];
        let count: number = countValidBoxes(queue, config.isBoxValid);

        // For a final palette of size targetPaletteSize, we determine the first fractionByPopulation*targetPaletteSize
        // using population as the only factor when determening sort order. For the rest of the colors the
        // sort order is population * colorVolume. This helps highly contrasting colors in a small area to show
        // up in some of the final output.
        const colorsByPopulation: number = Math.floor(
            config.targetPaletteSize * config.fractionByPopulation
        );
        const popSort: (box: PixelBox) => number = (box: PixelBox): number => {
            return box.pixelCount;
        };

        let iterationCount: number = 0;
        while (iterationCount <= config.maxIterations) {
            Eif (queue.length > 0) {
                const currentBox: PixelBox = queue.shift()!;

                const cutBoxes: [
                    PixelBox | null,
                    PixelBox | null
                ] = currentBox.modifiedMedianCut();

                Eif (cutBoxes[0] !== null) {
                    insertIntoSortedList(queue, cutBoxes[0], popSort);
                }
                if (cutBoxes[1] !== null) {
                    insertIntoSortedList(queue, cutBoxes[1], popSort);
                }
            }

            count = countValidBoxes(queue, config.isBoxValid);
            if (count >= colorsByPopulation || queue.length <= 1) {
                break;
            }

            iterationCount++;
        }

        Eif (count < config.targetPaletteSize) {
            const popAndVolumeSort: (box: PixelBox) => number = (box: PixelBox): number => {
                return box.pixelCount * box.colorVolume;
            };

            queue.sort((a: PixelBox, b: PixelBox) => {
                const aSort: number = popAndVolumeSort(a);
                const bSort: number = popAndVolumeSort(b);
                if (aSort === bSort) {
                    return 0;
                } else if (aSort > bSort) {
                    return -1;
                }
                return 1;
            });

            iterationCount = 0;
            while (iterationCount <= config.maxIterations) {
                Eif (queue.length > 0) {
                    const currentBox: PixelBox = queue.shift()!;

                    const cutBoxes: [
                        PixelBox | null,
                        PixelBox | null
                    ] = currentBox.modifiedMedianCut();

                    Eif (cutBoxes[0] !== null) {
                        insertIntoSortedList(queue, cutBoxes[0], popAndVolumeSort);
                    }
                    if (cutBoxes[1] !== null) {
                        insertIntoSortedList(queue, cutBoxes[1], popAndVolumeSort);
                    }
                }

                count = countValidBoxes(queue, config.isBoxValid);
                if (count >= config.targetPaletteSize || queue.length <= 1) {
                    break;
                }

                iterationCount++;
            }
        }

        const retVal: QuantizedColor[] = new Array(count);
        let index: number = 0;
        for (let i: number = 0; i < queue.length; i++) {
            if (!config.isBoxValid || config.isBoxValid(queue[i])) {
                retVal[index] = {
                    color: queue[i].averageColor,
                    pixelCount: queue[i].pixelCount,
                    colorVolume: queue[i].colorVolume,
                };
                index++;
            }
        }

        return retVal;
    }