calcHist — grayscale
For one channel, pass channel index list [0], mask (optional), hist size (bins), and range. Output shape is typically (bins, 1) unless you squeeze.
import cv2
import numpy as np
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
hist = hist.flatten() # length 256
# With a rectangular ROI mask
h, w = gray.shape
mask = np.zeros((h, w), np.uint8)
mask[50:200, 100:300] = 255
hist_roi = cv2.calcHist([gray], [0], mask, [256], [0, 256]).flatten()
2D histogram (Hue vs Saturation)
bgr = cv2.imread("color.jpg")
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
hist2d = cv2.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# hist2d shape (180, 256) — useful for skin or object color models
Normalize and cumulative view
Display or compare histograms by scaling to a fixed height. The cumulative distribution (CDF) of the normalized histogram drives global equalization.
import cv2
import numpy as np
hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).flatten()
hist_n = hist / (hist.sum() + 1e-9)
cdf = np.cumsum(hist_n)
# Map intensity i -> round((L-1) * cdf[i]) — idea behind equalizeHist
Global equalization
cv2.equalizeHist applies the classic transform on a single 8-bit channel—strong lift for underexposed images but can over-amplify noise and clip highlights globally.
import cv2
gray = cv2.imread("dark.jpg", cv2.IMREAD_GRAYSCALE)
eq = cv2.equalizeHist(gray)
# Per-channel on BGR (use sparingly — shifts color balance)
bgr = cv2.imread("dark_color.jpg")
ycrcb = cv2.cvtColor(bgr, cv2.COLOR_BGR2YCrCb)
y, cr, cb = cv2.split(ycrcb)
y_eq = cv2.equalizeHist(y)
bgr_eq = cv2.cvtColor(cv2.merge([y_eq, cr, cb]), cv2.COLOR_YCrCb2BGR)
Equalizing only the Y (luma) channel preserves chroma more naturally than equalizing B, G, R separately.
CLAHE (contrast limited adaptive)
CLAHE tiles the image, equalizes each tile, then interpolates—clipLimit caps contrast amplification to reduce noise blow-up. Applying CLAHE to L* in LAB is a popular photo and medical-imaging tweak.
import cv2
bgr = cv2.imread("backlit.jpg")
lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
l2 = clahe.apply(l)
lab2 = cv2.merge([l2, a, b])
bgr2 = cv2.cvtColor(lab2, cv2.COLOR_LAB2BGR)
Another CLAHE setting
# Stronger local contrast (watch for halos and noise)
clahe_aggressive = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(4, 4))
l3 = clahe_aggressive.apply(l)
compareHist
Compare two normalized histograms with correlation, Chi-square, intersection, or Bhattacharyya distance—handy for template color similarity or rough image retrieval.
import cv2
gray1 = cv2.imread("patch_a.jpg", cv2.IMREAD_GRAYSCALE)
gray2 = cv2.imread("patch_b.jpg", cv2.IMREAD_GRAYSCALE)
h1 = cv2.calcHist([gray1], [0], None, [256], [0, 256])
h2 = cv2.calcHist([gray2], [0], None, [256], [0, 256])
cv2.normalize(h1, h1, 0, 1, cv2.NORM_MINMAX)
cv2.normalize(h2, h2, 0, 1, cv2.NORM_MINMAX)
corr = cv2.compareHist(h1, h2, cv2.HISTCMP_CORREL)
bhatta = cv2.compareHist(h1, h2, cv2.HISTCMP_BHATTACHARYYA)
Plot with Matplotlib
import cv2
import matplotlib.pyplot as plt
gray = cv2.imread("photo.jpg", cv2.IMREAD_GRAYSCALE)
hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
plt.figure(figsize=(8, 3))
plt.plot(hist, color="orange")
plt.xlim([0, 256])
plt.title("Grayscale histogram")
plt.xlabel("Intensity")
plt.ylabel("Count")
plt.tight_layout()
plt.savefig("hist.png", dpi=120)
Takeaways
- equalizeHist is global and fast; prefer CLAHE on L (LAB) or Y (YCrCb) for color photos.
- Raise
clipLimitand shrinktileGridSizefor stronger local effect—watch artifacts. - compareHist after
normalizegives simple similarity scores between regions or images.
Quick FAQ
clipLimit create visible seams. Larger tiles or lower clip limit reduce halos; bilateral smoothing is sometimes applied afterward (trade detail for smoothness).