Histogram Equalization
Histogram equalization spreads clustered gray levels with the cumulative distribution so the output histogram sits flat and hidden detail shows.
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 count pixels at level , total pixels, levels. The normalized cumulative distribution is , and the mapping is . 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 levels and 16 pixels with counts : everything bunches in levels 3 to 7. Cumulative sums: . Multiply by and round: level 3 maps ; level 4 maps ; level 5 maps ; level 6 maps ; level 7 maps . The occupied band spreads to : 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 from the CDF.
- Worked example: counts spread from to .
equalizeHistworks 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.