Computer Vision Chapter 9

Histograms & contrast

A histogram counts how many pixels fall into each intensity bin. It summarizes exposure, contrast, and whether the scene is dominated by dark or bright tones. Histogram equalization spreads intensities to use the full range; CLAHE does that locally to avoid blowing out small bright regions. You will also see calcHist, compareHist, and a color-aware workflow in LAB.

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 clipLimit and shrink tileGridSize for stronger local effect—watch artifacts.
  • compareHist after normalize gives simple similarity scores between regions or images.

Quick FAQ

Tile boundaries and strong clipLimit create visible seams. Larger tiles or lower clip limit reduce halos; bilateral smoothing is sometimes applied afterward (trade detail for smoothness).

Often denoise first—equalization amplifies noise. For heavy noise, mild blur + CLAHE can be more stable than equalizeHist alone.