Computer Vision Chapter 4

Histograms & Feature Intro

Histogram equalization, CLAHE, and an introduction to keypoints, descriptors, and matching.

Histograms & contrast

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.

Feature detection (intro)

Keypoint + descriptor + matcher

  1. Detect interesting points (corners, blobs).
  2. Compute a descriptor per keypoint (sometimes using a canonical orientation and scale).
  3. Match descriptors with a distance (L2 for float vectors, Hamming for binary).
  4. Filter false matches (ratio test, geometric checks with homography or fundamental matrix).
MethodDescriptorSpeedNotes
Shi-Tomasi / goodFeaturesToTrackNone (points only)FastGreat for optical flow and tracking
FASTOptional (e.g. ORB)Very fastCorner score on circle of pixels
ORBBinary (BRIEF-like)FastFree; rotation-aware
SIFT128-D floatSlowerStrong invariance; check build/license

Shi-Tomasi corners — goodFeaturesToTrack

Returns up to maxCorners 2D points with high corner response—no descriptor. Often used with cv2.calcOpticalFlowPyrLK for video tracking.

import cv2
import numpy as np

gray = cv2.imread("room.jpg", cv2.IMREAD_GRAYSCALE)
pts = cv2.goodFeaturesToTrack(
    gray, maxCorners=200, qualityLevel=0.01, minDistance=10, blockSize=7)

if pts is not None:
    for p in pts:
        x, y = p.ravel()
        cv2.circle(gray, (int(x), int(y)), 3, 255, -1)

Tighter quality, larger separation

pts2 = cv2.goodFeaturesToTrack(
    gray, maxCorners=80, qualityLevel=0.05, minDistance=20, blockSize=9)

FAST keypoints

cv2.FastFeatureDetector_create (or constructor) flags corners by comparing intensity on a Bresenham circle. Very fast; combine with ORB or BRIEF for descriptors.

import cv2

gray = cv2.imread("scene.jpg", cv2.IMREAD_GRAYSCALE)
fast = cv2.FastFeatureDetector_create(threshold=40, nonmaxSuppression=True)
kps = fast.detect(gray, None)

vis = cv2.drawKeypoints(gray, kps, None, color=(255, 0, 0),
                        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

ORB — detect, compute, draw

ORB builds oriented FAST keypoints and binary descriptors. Default is 256 bits; Hamming distance applies. Free to use in typical OpenCV builds.

import cv2

img = cv2.imread("book.jpg", cv2.IMREAD_GRAYSCALE)
orb = cv2.ORB_create(nfeatures=500, scaleFactor=1.2, nlevels=8)
kp, des = orb.detectAndCompute(img, None)

img_bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
img_kp = cv2.drawKeypoints(img_bgr, kp, None, color=(0, 255, 0),
                           flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

des is None if no keypoints; shape (N, 32) for 256-bit ORB (8 bits per byte × 32).

Brute-force matching

BFMatcher with NORM_HAMMING for ORB. Use knnMatch and Lowe’s ratio test to reject ambiguous pairs.

import cv2

im1 = cv2.imread("obj1.jpg", cv2.IMREAD_GRAYSCALE)
im2 = cv2.imread("obj2.jpg", cv2.IMREAD_GRAYSCALE)
orb = cv2.ORB_create(800)
k1, d1 = orb.detectAndCompute(im1, None)
k2, d2 = orb.detectAndCompute(im2, None)

bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
pairs = bf.knnMatch(d1, d2, k=2)

good = []
for m, n in pairs:
    if m.distance < 0.75 * n.distance:
        good.append(m)

vis = cv2.drawMatches(im1, k1, im2, k2, good[:50], None, flags=2)

flags=2 is NOT_DRAW_SINGLE_POINTS (hide unmatched keypoints). On newer OpenCV you can write cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS.

Cross-check (mutual nearest neighbor)

bf_strict = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf_strict.match(d1, d2)
matches = sorted(matches, key=lambda m: m.distance)

Practical tips

  • Work in grayscale for classical detectors unless you use a color-aware variant.
  • Resize very large images before matching—scale affects keypoint count and runtime.
  • For panorama stitching, follow matching with RANSAC homography to kill outliers (covered in geometry topics).
  • SIFT may live in opencv-contrib-python and has patent/history considerations; ORB is the default free baseline.

Takeaways

  • goodFeaturesToTrack → points only; pair with optical flow.
  • ORB gives keypoints + binary descriptors + fast Hamming match.
  • Use ratio test or crossCheck before trusting correspondences.

Quick FAQ

Repeated texture, symmetry, and clutter produce similar descriptors. Tighten ratio threshold, increase minDistance at detection, or enforce geometric consistency (homography/fundamental matrix inliers).

Start with ORB: no extra modules, fast, enough for many alignment demos. Move to SIFT when you need stronger scale/view invariance and can install contrib builds.

Chapter FAQ

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.

Quick FAQ

Repeated texture, symmetry, and clutter produce similar descriptors. Tighten ratio threshold, increase minDistance at detection, or enforce geometric consistency (homography/fundamental matrix inliers).

Start with ORB: no extra modules, fast, enough for many alignment demos. Move to SIFT when you need stronger scale/view invariance and can install contrib builds.