Histograms reveal exposure problems and drive thresholds; CLAHE is a standard fix for low-contrast images.
A histogram counts how many pixels have each intensity. cv2.calcHist([img], [channel], mask, [bins], [range]) takes lists because it can combine several images and channels; for a gray image use cv2.calcHist([gray], [0], None, [256], [0, 256]), which returns float32 counts of shape (256, 1) in OpenCV 4 (flat (256,) in OpenCV 5). The upper range bound is exclusive, so 256 covers 0 to 255. np.bincount(gray.ravel(), minlength=256) gives the same counts.
Histograms tell you a lot at a glance: everything bunched at the left means underexposed, two clear peaks mean a threshold (or Otsu) will work well. 2-D hue-saturation histograms are used for colour-based object matching (cv2.compareHist) and back-projection.
Histogram equalisation, cv2.equalizeHist(gray), remaps intensities so the cumulative histogram becomes roughly linear, spreading a narrow range across 0 to 255. It works on the whole image, so it can over-amplify noise and wash out regions.
CLAHE (Contrast Limited Adaptive Histogram Equalisation) equalises small tiles separately and clips each histogram to limit noise amplification:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
out = clahe.apply(gray)For colour images, never equalise B, G and R separately (that shifts colours). Convert to LAB, apply CLAHE to the L channel, and convert back.
Example
import cv2
import numpy as np
rng = np.random.default_rng(0)
dull = rng.integers(100, 140, (64, 64), dtype=np.uint8) # low contrast
hist = cv2.calcHist([dull], [0], None, [256], [0, 256])
print(hist.shape, int(hist[:100].sum())) # (256, 1) 0 -- (256,) in OpenCV 5
print(dull.min(), dull.max()) # 100 139
eq = cv2.equalizeHist(dull)
print(eq.min(), eq.max()) # spread to roughly 0..255
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4))
print(clahe.apply(dull).std() > dull.std()) # True: more contrast
bgr = cv2.merge([dull, dull, rng.integers(0, 255, (64, 64), dtype=np.uint8)])
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
lab = cv2.merge([clahe.apply(l), a, b]) # only lightness changed
out = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
print(out.shape) # (64, 64, 3)Remember
calcHisttakes lists: images, channels, bins, ranges- equalizeHist spreads intensities globally
- CLAHE equalises tiles with a clip limit and amplifies less noise
- For colour, equalise the L channel of LAB, not B, G, R
Watch out for
- Equalising each BGR channel independently, which distorts the colours.