All files / src histogram.ts

85.37% Statements 35/41
100% Branches 7/7
50% Functions 2/4
85.37% Lines 35/41

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                        1x                           1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x   1x 1x 1x 133x   21280x   5320x                 5320x 5320x 5320x   5320x 5320x 5320x 5320x 5320x 5320x   5320x         5320x 5320x                               1x   5381x       5381x     1x       1x        
// tslint:disable:member-ordering
// tslint:disable:no-bitwise
import { PixelBlob } from "./pixel-blob";
 
// The memory needed for the histogram increases dramaticially if signifigantBits is increased
// It needs a buffer which is 4*2^(3*signifigantBits) in size
// EG: for 5 signifigant bits the histogram is 128K while for 8 it is 64 megs.
// CPU time increases linearly as pixelSkipping is reduced
// isHistoPixelValid is an optional predicate which can screen out unwanted pixels from the
// source data. EG: ignoring transparent pixels.
// 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 {
    constructor(
        source: PixelBlob,
        signifigantBits: number = 5,
        pixelSkipping: number = 5,
        isHistoPixelValid: ((pixel: number[]) => boolean) | null = null
    ) {
        if (signifigantBits < 1 || signifigantBits > 8) {
            throw new Error("signifigantBits must be in the range [1,8]");
        }
        if (pixelSkipping < 0) {
            throw new Error("pixelSkipping must be >= 0");
        }
 
        this.signifigantBits = signifigantBits;
        const sigShift: number = 8 - this.signifigantBits;
        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 << (signifigantBits * 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 (isHistoPixelValid !== null) {
                    if (!isHistoPixelValid(rgba)) {
                        continue;
                    }
                }
 
                // Shift the pixel data into the range determined by signifigantBits
                // 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 signifigantBits: 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.signifigantBits)) + (g << this.signifigantBits) + b;
        if (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;
    };
}