# All files / fast-colors/src histogram.ts

93.88% Statements 46/49
84.21% Branches 16/19
100% Functions 4/4
93.62% Lines 44/47

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

    6x

    20x

    20x

    20x
    20x
    20x
    20x
    20x
    20x
    20x
    20x

    20x
    20x
    20x

    20x
    20x
    20x
    2644x
    528800x
    373165x

    155635x
    155635x
    47240x
    6437x

    149198x
    149198x
    149198x

    149198x
    149198x
    149198x
    149198x
    149198x
    149198x

    149198x

    149198x
    149198x

    20x

    1054478x
    1054478x

    1054478x

    20x
    905218x

    20x
    1x

     import { PixelBlob } from "./pixel-blob";

    /**
     * For each possible color, this counts how many pixels in the source image match that color.
     * If signifigantBits is less tahn 8, each channel (eg: red, green, blue) in each color is reduced to fit in significantBits. So for the default value of 5 significantBits colors are reduced from 8 bits per channel (0-255) to 5 (0-31). Colors that were previously distinct get combined together.
     * If the image source has more than 2^32 pixels (eg: a square image 65536x65536 in size) of the same color this code will break.
     */
    export class Histogram {
        /**
         * @param source
         * @param significantBits The memory needed for the histogram increases dramaticially if significantBits is increased. It needs a buffer which is 4*2^(3*significantBits) in size. EG: for 5 significant bits the histogram is 128K while for 8 it is 64 megs.
         * @param pixelSkipping CPU time increases linearly as pixelSkipping is reduced.
         * @param isHistogramPixelValid isHistogramPixelValid is an optional predicate which can screen out unwanted pixels from the source data. EG: ignoring transparent pixels.
         */
        constructor(
            source: PixelBlob,
            significantBits: number = 5,
            pixelSkipping: number = 5,
            isHistogramPixelValid: ((pixel: number[]) => boolean) | null = null
        ) {
            Iif (significantBits < 1 || significantBits > 8) {
                throw new Error("significantBits must be in the range [1,8]");
            }
            Iif (pixelSkipping < 0) {
                throw new Error("pixelSkipping must be >= 0");
            }

            this.significantBits = significantBits;
            const sigShift: number = 8 - this.significantBits;
            this.minRed = 255 >>> sigShift;
            this.maxRed = 0;
            this.minGreen = 255 >>> sigShift;
            this.maxGreen = 0;
            this.minBlue = 255 >>> sigShift;
            this.maxBlue = 0;

            const histoSize: number = 1 << (significantBits * 3);
            this.data = new Uint32Array(histoSize);
            this.data.fill(0);

            this.total = 0;
            let pixelIndex: number = 0;
            for (let y: number = 0; y < source.height; y++) {
                for (let x: number = 0; x < source.width; x++) {
                    if (pixelSkipping > 0 && pixelIndex++ % pixelSkipping !== 0) {
                        continue;
                    }
                    const rgba: number[] = source.getPixelRGBA(x, y);
                    if (isHistogramPixelValid !== null) {
                        if (!isHistogramPixelValid(rgba)) {
                            continue;
                        }
                    }

                    // Shift the pixel data into the range determined by significantBits
                    // after checking minAlpha the alpha data is no longer needed
                    rgba[0] = rgba[0] >>> sigShift;
                    rgba[1] = rgba[1] >>> sigShift;
                    rgba[2] = rgba[2] >>> sigShift;

                    this.minRed = Math.min(rgba[0], this.minRed);
                    this.maxRed = Math.max(rgba[0], this.maxRed);
                    this.minGreen = Math.min(rgba[1], this.minGreen);
                    this.maxGreen = Math.max(rgba[1], this.maxGreen);
                    this.minBlue = Math.min(rgba[2], this.minBlue);
                    this.maxBlue = Math.max(rgba[2], this.maxBlue);

                    const histoIndex: number = this.getHistogramIndex(
                        rgba[0],
                        rgba[1],
                        rgba[2]
                    );
                    this.data[histoIndex] += 1;
                    this.total++;
                }
            }
        }

        public readonly data: Uint32Array;

        public readonly significantBits: number;
        public readonly total: number;
        public readonly minRed: number;
        public readonly maxRed: number;
        public readonly minGreen: number;
        public readonly maxGreen: number;
        public readonly minBlue: number;
        public readonly maxBlue: number;

        public getHistogramIndex = (r: number, g: number, b: number): number => {
            const index: number =
                (r << (2 * this.significantBits)) + (g << this.significantBits) + b;
            Iif (index >= this.data.length) {
                throw new Error("RGB value is outside the bounds of the histogram");
            }
            return index;
        };

        public getHistogramValue = (r: number, g: number, b: number): number => {
            return this.data[this.getHistogramIndex(r, g, b)];
        };

        public setHistogramValue = (value: number, r: number, g: number, b: number): void => {
            this.data[this.getHistogramIndex(r, g, b)] = value;
        };
    }