Image Processing 7. Histogram

3 min read June 19, 2025 #image processing #javascript

Today's article is about histograms. I'll cover what histograms can tell us about a photo, how to construct histograms, and how image adjustments affect their appearance.

logo7

In the RGB representation of an image, there are 256 brightness values per channel. What if we count the number of pixels for each of these values? We would obtain some statistics about the brightness distribution, allowing us to determine which brightness levels predominate in the image and which are not. This is precisely what an image histogram represents.

If we count the values of each color channel as if it were a single channel, or convert colors to grayscale and then count the statistics, we get a luminance histogram. It indicates whether a photo is underexposed or overexposed. Thus, we would need to compensate a brightness level during post-processing or adjust exposure during shooting.

If we plot a graph for each RGB component, we get an RGB histogram. It reveals which color channel dominates in the image, enabling corrective actions. For instance, if the histogram is generated by the camera during focusing, a red-dominated histogram might suggest adjusting the white balance toward cooler tones. If the histogram is generated during post-processing, color channel clipping can be compensated using appropriate color filter.

But enough theory, let's move on to practice.

Generating a Histogram#

We'll draw the histogram on a canvas. First, create an array of 256 elements and fill it with zeros. Then count occurrences of each component value:

let histBrightness = (new Array(256)).fill(0);
for (let i = 0; i < src.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  histBrightness[r]++;
  histBrightness[g]++;
  histBrightness[b]++;
}

Next, find the maximum value to fit the graph to the canvas height and, of course, draw the lines:

let maxBrightness = 0;
for (let i = 1; i < 256; i++) {
  if (maxBrightness < histBrightness[i]) {
    maxBrightness = histBrightness[i]
  }
}

const canvas = document.getElementById('canvasHistogram');
const ctx = canvas.getContext('2d');
let dx = canvas.width / 256;
let dy = canvas.height / maxBrightness;
ctx.lineWidth = dx;
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 256; i++) {
  let x = i * dx;
  ctx.strokeStyle = "#000000";
  ctx.beginPath();
  ctx.moveTo(x, canvas.height);
  ctx.lineTo(x, canvas.height - histBrightness[i] * dy);
  ctx.closePath();
  ctx.stroke();
}

Let's take an ordinary photo:

sample_0m.jpg hist_ev0.png

From this histogram, it's clear that the image is quite contrasty, with both dark and light tones present.

Here's the same scene at -2.0 exposure:

sample_-2m.jpg hist_ev-2.png

The histogram shows that there are almost no light areas here, and the large peak on the left indicates that the photo is underexposed.

hist_ev-2b.png

Finally, a photo with +2.0 exposure:

sample_+2m.jpg hist_ev2.png

There's so much overexposure here that the midtones on the histogram are almost invisible, everything has shifted to the right edge.

hist_ev2b.png

Fixing the image#

We'll use the brightness/contrast adjustments from the previous article and see how the histogram changes.

As you can see, brightness shifts the histogram left or right, while contrast narrows or widens the range.

If we adjust the parameters so that there's no empty space on the left and right and there's no significant underexposure or overexposure, we get a slightly more acceptable image:

hist_fixed_js.jpg

This's the principle behind the Auto Contrast correction in Photoshop:

hist_fixed_ps

The result is almost the same as with manual adjustment in JavaScript.