Image Histograms in JavaScript

Most cameras and image editing applications will generate histograms of image data, showing the distributions of colours for the three channels, red, green and blue. As part of an Electron application I am working on I developed a JavaScript implementation of image histograms using the NodeJS Jimp package. This is it...

Image Histograms

Let's first take a look at exactly what an image histogram is. A colour digital photo such as the one below, which incidentally is Google's London office, is made up of pixels containing a red, a green and a blue value. Each of these can be any integer between 0 (none of that colour) and 255 (the maximum amount of that colour).

For each colour or channel you can count the total number of 0s, the total number of 1s etc. up to 255. You will then have 256 x 256 x 256 = 768 different values. These can be plotted on histograms like the one below which was generated by the Gimp.

The values 0-255 are on the x-axis and the amounts of each value are on the y-axis. Here we can see that there is a large number of instances of very dark red, with the rest of the red values being far less frequent. These are the other two Gimp histograms for the above photo. The aim of this project is to replicate them in JavaScript.

Getting Raw Data Using Jimp

Details of the Jimp package, including installation instructions, are here https://www.npmjs.com/package/jimp.

I will only be using a small proportion of Jimp's functionality which I will describe as we go along, but at the core of this project is Jimp's ability to provide us with an array of raw pixel data, four bytes for each pixel for red, green, blue and alpha (ie opacity). This project is for use with JPEG which does not support opacity so the last byte of each pixel can be ignored. The following table illustrates the data in the first 12 bytes of an array. (The white cells are the alpha channel.)

pixel 0 pixel 1 pixel 2
0 1 2 3 4 5 6 7 8 9 10 11

So to generate histograms we need to iterate this data, keeping running totals of each value for each colour channel, after which we can plot the data as SVG.

The Project

This project consists of the following files which can be downloaded from the Downloads page, or you can clone/download the Github repository if you prefer.

  • imagehistograms.js
  • imagehistogramsdemo.js

Source Code Links

ZIP File
GitHub

This is imagehistograms.js.

imagehistograms.js

exports.colorChannels =
{
    Red: 0,
    Green: 1,
    Blue: 2
};


exports.histogramRGB = function(channel, jimpImage)
{
    const colourFrequencies = getColourFrequencies(channel, jimpImage);

    const histogram = createHistogram(channel, colourFrequencies.colourFrequencies, colourFrequencies.maxFrequency);

    return histogram;
}


function getColourFrequencies(channel, jimpImage)
{
    const startIndex = channel; // StartIndex same as RGB enum: R=0, G=1, B=2

    let maxFrequency = 0;
    const colourFrequencies = Array(256).fill(0);

    // Iterate bitmap and count frequencies of specified component values
    for(let i = startIndex, len = jimpImage.bitmap.data.length; i < len; i+= 4)
    {
        colourFrequencies[jimpImage.bitmap.data[i]]++;

        if(colourFrequencies[jimpImage.bitmap.data[i]] > maxFrequency)
        {
            maxFrequency++;
        }
    }

    const result =
    {
        colourFrequencies: colourFrequencies,
        maxFrequency: maxFrequency
    }

    return result;
}


function createHistogram(channel, colourFrequencies, maxFrequency)
{
    const histWidth = 512;
    const histHeight = 316;
    const columnWidth = 2;
    const pixelsPerUnit = histHeight / maxFrequency;

    let hexColour;
    let x = 0;
    let columnHeight;

    let svgstring = `<svg width='${histWidth}px' height='${histHeight}px' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink'>\n`;

    for(let i = 0; i <= 255; i++)
    {
        hexColour = i.toString(16).padStart(2, "0");

        switch(channel)
        {
            case exports.colorChannels.Red:
                hexColour = "#" + hexColour + "0000";
                break;
            case exports.colorChannels.Green:
                hexColour = "#00" + hexColour + "00";
                break;
            case exports.colorChannels.Blue:
                hexColour = "#0000" + hexColour;
                break;
        }

        columnHeight = colourFrequencies[i] * pixelsPerUnit;

        svgstring += `    <rect fill='${hexColour}' stroke='${hexColour}' stroke-width='0.25px' width='${columnWidth}' height='${columnHeight}' y='${histHeight - columnHeight}' x='${x}' />\n`;

        x += columnWidth;
    }

    svgstring += "</svg>";

    return svgstring;
}

exports.colorChannels

Firstly note that as this is a Node module this is added to the export object. The object exported here is simply used as an enumeration for the three colour channels.

exports.histogramRGB

This function is also exported and simply calls the following two functions to get frequency data and use it to generate histograms which are then returned.

getColourFrequencies

This and the next function are for internal use only and therefore are not exported. getColourFrequencies takes a channel (R, G or B) and a Jimp image and returns an object with an array of 256 frequencies for the specified channel, and a single variable containing the highest frequency.

As per the table above, the red data starts at 0, the green at 1 and the blue at 2. These correspnd to the colorChannels values so we just store the channel argument in the startIndex const. We also need a single maxFrequency and an array of 256 0s to hold the frequencies for each possible colour value.

We the iterate the Jimp image's bitmap.data, incrementing the correspnding frequency in the colourFrequencies array. If the frequency for the current colour is higher than maxFrequency then maxFrequency is also incremented.

After the loop colourFrequencies and maxFrequency are stuck together in an object which is then returned.

createHistogram

After a pile of consts and variables we create a string to hold the entire SVG string, starting off with the opening tag. we then loop from 0 to 255, and for each value create a string with the hexadecimal equivalent, left-padded to two characters. This then has to be joined to a leading # character and two pairs of 0s to make up a complete hexadecimal RGB colour.

Next we calculate the height of the column based on the current frequency and, in an excessively long and unpleasant line of code, we add a rectangle to the SVG string.

Finally a closing SVG tag is added and the string returned.

Try it Out

The module is now finished so we need a bit of code to try it out.

imagehistogramsdemo.js

function createHistograms()
{
    const Jimp = require("jimp");
    const imghist = require('./imagehistograms.js');

    Jimp.read("central_st_giles.jpg", function (err, photo)
    {
        if (err)
        {
            console.error(err);
        }
        else
        {
            const histred = imghist.histogramRGB(imghist.colorChannels.Red, photo);
            saveHistogram(histred, "histred.svg");

            const histgreen = imghist.histogramRGB(imghist.colorChannels.Green, photo);
            saveHistogram(histgreen, "histgreen.svg");

            let histblue = imghist.histogramRGB(imghist.colorChannels.Blue, photo);
            saveHistogram(histblue, "histblue.svg");
        }
    });
}


function saveHistogram(histogramstring, filename)
{
    const fs = require("fs");

    fs.writeFile(filename, histogramstring, function (err)
    {
        if (err)
        {
            console.error(err);
        }
        else
        {
            console.log(filename + ' saved');
        }
    });
}


createHistograms();

createHistograms

I'll write a full post on Jimp in the future but the few snippets I have used here should be self-explanatory. If the image is successfully opened it is just passed to histogramRGB three times for red, green and blue and the resulting strings saved in a call to saveHistogram.

saveHistogram

A very straightforward bit of code which saves a string to a file.

Now let's run the program. If you are using your own photo you will need to edit the filename in createHistograms.

Running the Program

node imagehistogramsdemo.js

Open the folder where your code is and you will find three new svg files.

I am pleased with the result and I think using actual colours is very effective. The histograms are deliberately minimalist as their primary use is within a GUI which would provide its own border, headings and other information, similar to the Gimp screenshots above.