Skip to content
AI360Xpert
Beta

Histogram Equalization

Histogram equalization spreads clustered gray levels with the cumulative distribution so the output histogram sits flat and hidden detail shows.

The cumulative curve maps bunched input levels to evenly spread outputs, flattening a clustered histogram.
The cumulative curve maps bunched input levels to evenly spread outputs, flattening a clustered histogram.

Why Does This Exist?

A photo can use 256 gray levels yet crowd 90 percent of its pixels into 30 of them; the remaining levels sit empty and contrast dies. Linear scaling stretches blindly, but the histogram itself knows exactly where pixels bunch. Equalization builds its remapping from the image's own cumulative distribution, spreading crowded levels apart and squeezing empty ones together. It is the standard rescue for washed-out scans, hazy frames and flat medical slices, sitting at the distribution-driven branch of enhancement.

This page covers the CDF mapping, a fully worked 3-bit example, and why global equalization overcooks mixed lighting (the case CLAHE exists for).

Think of It Like This

Reassigning seats in a crowded theater row

A theater row has 8 seats but everyone crowds into seats 3 through 7 while 0 through 2 sit empty. The usher reassigns: the two people in seat 3 move to seat 1, the four in seat 4 spread across seat 3, and so on, until all 8 seats hold people evenly. The crowd (pixels) is unchanged in number, but now fills the row (the range).

The analogy stops at taste. The usher fills seats mechanically, even if the result looks harsh: faces in a bright patch get pushed to posterized extremes. Equalization optimizes flatness, not beauty.

How It Actually Works

The CDF mapping

Let nkn_k count pixels at level kk, NN total pixels, LL levels. The normalized cumulative distribution is ck=1N∑j≤knjc_k = \frac{1}{N}\sum_{j \le k} n_j, and the mapping is sk=round((L−1)⋅ck)s_k = \mathrm{round}((L - 1) \cdot c_k). Crowded levels get large CDF jumps, hence wide output spacing; empty levels get none and merge. cv2.equalizeHist does this for 8-bit single-channel images in one call.

Worked 3-bit example

Take L=8L = 8 levels and 16 pixels with counts n=[0,0,0,2,4,4,4,2]n = [0, 0, 0, 2, 4, 4, 4, 2]: everything bunches in levels 3 to 7. Cumulative sums: [0,0,0,2,6,10,14,16][0, 0, 0, 2, 6, 10, 14, 16]. Multiply by 7/167/16 and round: level 3 maps 7⋅2/16=0.875→17 \cdot 2/16 = 0.875 \to 1; level 4 maps 7⋅6/16=2.625→37 \cdot 6/16 = 2.625 \to 3; level 5 maps 7⋅10/16=4.375→47 \cdot 10/16 = 4.375 \to 4; level 6 maps 7⋅14/16=6.125→67 \cdot 14/16 = 6.125 \to 6; level 7 maps 7⋅16/16=77 \cdot 16/16 = 7. The occupied band {3..7}\{3..7\} spreads to {1,3,4,6,7}\{1, 3, 4, 6, 7\}: same pixels, nearly double the contrast range.

What it assumes and breaks

Equalization assumes one global distribution describes the whole image. Mixed lighting breaks that: a dark foreground plus bright sky forces one compromise curve that grays the sky and overcooks faces. It also amplifies background noise in near-flat regions into visible grain, and it shifts mean brightness, which disturbs photometric pipelines. Apply to luminance channels for color work, and switch to CLAHE when lighting varies across the frame.

Code

import numpy as np
n = np.array([0, 0, 0, 2, 4, 4, 4, 2])  # 16 pixels on 8 levelscdf = np.cumsum(n)mapping = np.round(7 * cdf / 16).astype(int)print(cdf.tolist())# -> [0, 0, 0, 2, 6, 10, 14, 16]print(mapping.tolist())# -> [0, 0, 0, 1, 3, 4, 6, 7]

Watch Out For

Washed-out brights and blocked shadows

Global equalization happily pushes skin tones to chalk and shadows to ink to achieve flatness. The symptom is harsh, posterized portraits after "enhancing" unevenly lit photos. Reserve it for genuinely flat, single-illuminant images and use CLAHE otherwise.

equalizeHist on color images

The function expects one 8-bit channel; handing it BGR applies the mapping per channel independently and wrecks hues. The symptom is neon color shifts. Convert to YCrCb or HSV, equalize the Y or V channel only, and merge back.

The Quick Version

  • Equalization remaps levels through sk=round((L−1)⋅ck)s_k = \mathrm{round}((L-1) \cdot c_k) from the CDF.
  • Worked example: counts [0,0,0,2,4,4,4,2][0,0,0,2,4,4,4,2] spread from {3..7}\{3..7\} to {1,3,4,6,7}\{1,3,4,6,7\}.
  • equalizeHist works on 8-bit single-channel images only.
  • Best for flat, evenly lit images; overcooks mixed lighting and amplifies flat-region noise.
  • For color, equalize luminance channels, never raw BGR.