Histogram Analysis

Histogram Analysis

Overview

A histogram is a graph representing the brightness distribution of an image. It is used for image analysis, contrast enhancement, color comparison, and more. In this lesson, we will learn about histogram calculation, equalization, CLAHE, comparison, backprojection, and other techniques.


Table of Contents

  1. Histogram Basics
  2. Histogram Calculation
  3. Histogram Equalization
  4. CLAHE
  5. Histogram Comparison
  6. Backprojection
  7. Practice Problems

1. Histogram Basics

What is a Histogram?

Histogram:
A graph representing the distribution of pixel brightness values in an image

X-axis: Brightness value (0-255)
Y-axis: Number of pixels with that brightness value

Dark Image                Bright Image            High Contrast Image
                                                  
Freq│█                                                  
uenc│██                            ██                ███ ███
y   │███                          ███               █████████
    └────────────           └────────────          └────────────
    0          255          0          255         0          255
     Brightness               Brightness              Brightness

Applications of Histograms

1. Image Analysis
   - Check exposure status (overexposed, underexposed)
   - Assess contrast level

2. Image Enhancement
   - Histogram equalization
   - Contrast adjustment

3. Image Comparison
   - Similar image search
   - Color-based matching

4. Object Tracking
   - Color histogram backprojection
   - CamShift/MeanShift algorithms

2. Histogram Calculation

cv2.calcHist() Function

hist = cv2.calcHist(images, channels, mask, histSize, ranges)
Parameter Description
images List of input images [img]
channels Channel indices [0], [1], [2] or [0, 1], etc.
mask Mask (None = entire image)
histSize Number of bins [256]
ranges Value range [0, 256]

Grayscale Histogram

import cv2
import numpy as np
import matplotlib.pyplot as plt

def calc_gray_histogram(image_path):
    """Calculate and visualize grayscale histogram"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # Calculate histogram
    hist = cv2.calcHist(
        [img],           # Image (passed as list)
        [0],             # Channel (0 for grayscale)
        None,            # Mask (entire image)
        [256],           # Number of bins (0-255: 256 bins)
        [0, 256]         # Value range
    )

    # Visualize with Matplotlib
    plt.figure(figsize=(12, 4))

    plt.subplot(1, 2, 1)
    plt.imshow(img, cmap='gray')
    plt.title('Image')
    plt.axis('off')

    plt.subplot(1, 2, 2)
    plt.plot(hist, color='black')
    plt.title('Histogram')
    plt.xlabel('Pixel Value')
    plt.ylabel('Frequency')
    plt.xlim([0, 256])

    plt.tight_layout()
    plt.show()

    return hist

hist = calc_gray_histogram('image.jpg')

Color Histogram

import cv2
import numpy as np
import matplotlib.pyplot as plt

def calc_color_histogram(image_path):
    """RGB channel-wise histogram"""
    img = cv2.imread(image_path)
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    colors = ('r', 'g', 'b')
    channel_names = ('Red', 'Green', 'Blue')

    plt.figure(figsize=(12, 4))

    plt.subplot(1, 2, 1)
    plt.imshow(img_rgb)
    plt.title('Image')
    plt.axis('off')

    plt.subplot(1, 2, 2)
    for i, (color, name) in enumerate(zip(colors, channel_names)):
        # BGR order, so adjust index: R=2, G=1, B=0
        channel_idx = 2 - i
        hist = cv2.calcHist([img], [channel_idx], None, [256], [0, 256])
        plt.plot(hist, color=color, label=name)

    plt.title('Color Histogram')
    plt.xlabel('Pixel Value')
    plt.ylabel('Frequency')
    plt.xlim([0, 256])
    plt.legend()

    plt.tight_layout()
    plt.show()

calc_color_histogram('colorful.jpg')

2D Histogram (Hue-Saturation)

import cv2
import numpy as np
import matplotlib.pyplot as plt

def calc_2d_histogram(image_path):
    """Hue-Saturation 2D histogram"""
    img = cv2.imread(image_path)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # H: 0-180, S: 0-256
    hist = cv2.calcHist(
        [hsv],
        [0, 1],          # H and S channels
        None,
        [30, 32],        # Number of bins (H: 30, S: 32)
        [0, 180, 0, 256] # Ranges (H: 0-180, S: 0-256)
    )

    plt.figure(figsize=(10, 4))

    plt.subplot(1, 2, 1)
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title('Image')
    plt.axis('off')

    plt.subplot(1, 2, 2)
    plt.imshow(hist, interpolation='nearest')
    plt.title('2D Histogram (H-S)')
    plt.xlabel('Saturation')
    plt.ylabel('Hue')
    plt.colorbar()

    plt.tight_layout()
    plt.show()

    return hist

hist_2d = calc_2d_histogram('colorful.jpg')

Histogram with Mask

import cv2
import numpy as np
import matplotlib.pyplot as plt

def histogram_with_mask(image_path):
    """Calculate histogram for specific region only"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    h, w = img.shape

    # Create circular mask
    mask = np.zeros((h, w), dtype=np.uint8)
    cv2.circle(mask, (w//2, h//2), min(h, w)//3, 255, -1)

    # Full histogram
    hist_full = cv2.calcHist([img], [0], None, [256], [0, 256])

    # Masked region histogram
    hist_masked = cv2.calcHist([img], [0], mask, [256], [0, 256])

    plt.figure(figsize=(12, 4))

    plt.subplot(1, 3, 1)
    plt.imshow(img, cmap='gray')
    plt.title('Original')
    plt.axis('off')

    plt.subplot(1, 3, 2)
    plt.imshow(mask, cmap='gray')
    plt.title('Mask')
    plt.axis('off')

    plt.subplot(1, 3, 3)
    plt.plot(hist_full, label='Full', alpha=0.7)
    plt.plot(hist_masked, label='Masked', alpha=0.7)
    plt.legend()
    plt.title('Histograms')

    plt.tight_layout()
    plt.show()

histogram_with_mask('image.jpg')

3. Histogram Equalization

Concept

Histogram Equalization:
Makes the brightness distribution of an image uniform to enhance contrast

Original Histogram            Equalized Histogram
    │                              │
    │█                             │   █ █ █
    │███                           │ █ █ █ █ █
    │█████                         │█ █ █ █ █ █ █
    └────────────                  └────────────────
    0          255                 0              255

Transformation Process:
1. Calculate histogram
2. Calculate cumulative distribution function (CDF)
3. Normalize CDF
4. Map pixel values

cv2.equalizeHist()

import cv2
import numpy as np
import matplotlib.pyplot as plt

def equalize_histogram_demo(image_path):
    """Histogram equalization demo"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # Histogram equalization
    equalized = cv2.equalizeHist(img)

    # Calculate histograms
    hist_before = cv2.calcHist([img], [0], None, [256], [0, 256])
    hist_after = cv2.calcHist([equalized], [0], None, [256], [0, 256])

    # Visualize
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))

    axes[0, 0].imshow(img, cmap='gray')
    axes[0, 0].set_title('Original')
    axes[0, 0].axis('off')

    axes[0, 1].imshow(equalized, cmap='gray')
    axes[0, 1].set_title('Equalized')
    axes[0, 1].axis('off')

    axes[1, 0].plot(hist_before)
    axes[1, 0].set_title('Original Histogram')
    axes[1, 0].set_xlim([0, 256])

    axes[1, 1].plot(hist_after)
    axes[1, 1].set_title('Equalized Histogram')
    axes[1, 1].set_xlim([0, 256])

    plt.tight_layout()
    plt.show()

    return equalized

equalized = equalize_histogram_demo('dark_image.jpg')

Color Image Equalization

import cv2
import numpy as np

def equalize_color_image(image_path):
    """Histogram equalization for color images"""
    img = cv2.imread(image_path)

    # Method 1: Use YCrCb color space (recommended)
    ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
    ycrcb[:, :, 0] = cv2.equalizeHist(ycrcb[:, :, 0])  # Equalize Y channel only
    result_ycrcb = cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR)

    # Method 2: Use HSV color space
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    hsv[:, :, 2] = cv2.equalizeHist(hsv[:, :, 2])  # Equalize V channel only
    result_hsv = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

    # Method 3: Equalize each channel individually (may cause color distortion)
    b, g, r = cv2.split(img)
    b_eq = cv2.equalizeHist(b)
    g_eq = cv2.equalizeHist(g)
    r_eq = cv2.equalizeHist(r)
    result_rgb = cv2.merge([b_eq, g_eq, r_eq])

    cv2.imshow('Original', img)
    cv2.imshow('YCrCb Equalization', result_ycrcb)
    cv2.imshow('HSV Equalization', result_hsv)
    cv2.imshow('RGB Equalization', result_rgb)
    cv2.waitKey(0)

    return result_ycrcb

equalize_color_image('dark_color.jpg')

4. CLAHE

Concept

CLAHE (Contrast Limited Adaptive Histogram Equalization):
Adaptive histogram equalization

Problem: Global equalization can amplify noise
Solution: Divide image into tiles and equalize locally

┌────┬────┬────┬────┐
│    │    │    │    │
│ T1 │ T2 │ T3 │ T4 │   Apply equalization
├────┼────┼────┼────┤   to each tile
│    │    │    │    │
│ T5 │ T6 │ T7 │ T8 │   Smooth boundaries
├────┼────┼────┼────┤   with interpolation
│ T9 │T10 │T11 │T12 │
└────┴────┴────┴────┘

Features:
- clipLimit: Contrast limit (higher = stronger contrast)
- tileGridSize: Tile size (smaller = more detailed)

cv2.createCLAHE()

import cv2
import numpy as np
import matplotlib.pyplot as plt

def clahe_demo(image_path):
    """CLAHE application demo"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    # Standard equalization
    equalized = cv2.equalizeHist(img)

    # Create and apply CLAHE
    clahe = cv2.createCLAHE(
        clipLimit=2.0,      # Contrast limit (1.0 ~ 4.0 recommended)
        tileGridSize=(8, 8) # Tile size
    )
    clahe_result = clahe.apply(img)

    # Comparison
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    axes[0].imshow(img, cmap='gray')
    axes[0].set_title('Original')
    axes[0].axis('off')

    axes[1].imshow(equalized, cmap='gray')
    axes[1].set_title('Standard Equalization')
    axes[1].axis('off')

    axes[2].imshow(clahe_result, cmap='gray')
    axes[2].set_title('CLAHE')
    axes[2].axis('off')

    plt.tight_layout()
    plt.show()

    return clahe_result

clahe_demo('low_contrast.jpg')

CLAHE Parameter Comparison

import cv2
import numpy as np
import matplotlib.pyplot as plt

def compare_clahe_params(image_path):
    """Compare CLAHE with different parameters"""
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    clip_limits = [1.0, 2.0, 4.0, 8.0]
    tile_sizes = [(4, 4), (8, 8), (16, 16)]

    fig, axes = plt.subplots(len(tile_sizes), len(clip_limits) + 1,
                              figsize=(15, 10))

    for i, tile_size in enumerate(tile_sizes):
        axes[i, 0].imshow(img, cmap='gray')
        axes[i, 0].set_title(f'Original\nTile: {tile_size}')
        axes[i, 0].axis('off')

        for j, clip_limit in enumerate(clip_limits):
            clahe = cv2.createCLAHE(clipLimit=clip_limit,
                                     tileGridSize=tile_size)
            result = clahe.apply(img)

            axes[i, j + 1].imshow(result, cmap='gray')
            axes[i, j + 1].set_title(f'clip={clip_limit}')
            axes[i, j + 1].axis('off')

    plt.tight_layout()
    plt.show()

compare_clahe_params('low_contrast.jpg')

Applying CLAHE to Color Images

import cv2
import numpy as np

def clahe_color(image_path, clip_limit=2.0, tile_size=(8, 8)):
    """Apply CLAHE to color image"""
    img = cv2.imread(image_path)

    # Convert to LAB color space
    lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)

    # Apply CLAHE to L channel
    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_size)
    lab[:, :, 0] = clahe.apply(lab[:, :, 0])

    # Convert back to BGR
    result = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)

    cv2.imshow('Original', img)
    cv2.imshow('CLAHE', result)
    cv2.waitKey(0)

    return result

clahe_color('dark_scene.jpg')

5. Histogram Comparison

cv2.compareHist()

similarity = cv2.compareHist(hist1, hist2, method)
Method Description Range Interpretation
cv2.HISTCMP_CORREL Correlation -1 ~ 1 1: Perfect match
cv2.HISTCMP_CHISQR Chi-Square 0 ~ ∞ 0: Perfect match
cv2.HISTCMP_INTERSECT Intersection 0 ~ min(sum) Higher = more similar
cv2.HISTCMP_BHATTACHARYYA Bhattacharyya distance 0 ~ 1 0: Perfect match

Histogram Comparison Example

import cv2
import numpy as np

def compare_histograms(image_paths):
    """Compare histograms of multiple images"""
    # Base image
    base_img = cv2.imread(image_paths[0])
    base_hsv = cv2.cvtColor(base_img, cv2.COLOR_BGR2HSV)

    # Calculate histogram (H-S 2D)
    base_hist = cv2.calcHist(
        [base_hsv], [0, 1], None,
        [50, 60], [0, 180, 0, 256]
    )
    cv2.normalize(base_hist, base_hist, 0, 1, cv2.NORM_MINMAX)

    print(f"Base image: {image_paths[0]}")
    print("-" * 50)

    methods = [
        (cv2.HISTCMP_CORREL, 'Correlation'),
        (cv2.HISTCMP_CHISQR, 'Chi-Square'),
        (cv2.HISTCMP_INTERSECT, 'Intersection'),
        (cv2.HISTCMP_BHATTACHARYYA, 'Bhattacharyya')
    ]

    for path in image_paths[1:]:
        img = cv2.imread(path)
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

        hist = cv2.calcHist(
            [hsv], [0, 1], None,
            [50, 60], [0, 180, 0, 256]
        )
        cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)

        print(f"\nComparing: {path}")
        for method, name in methods:
            result = cv2.compareHist(base_hist, hist, method)
            print(f"  {name}: {result:.4f}")

# Usage example
image_files = ['ref.jpg', 'similar1.jpg', 'similar2.jpg', 'different.jpg']
compare_histograms(image_files)
import cv2
import numpy as np
import os

def find_similar_images(query_path, search_dir, top_k=5):
    """Histogram-based similar image search"""
    # Query image histogram
    query = cv2.imread(query_path)
    query_hsv = cv2.cvtColor(query, cv2.COLOR_BGR2HSV)
    query_hist = cv2.calcHist([query_hsv], [0, 1], None,
                               [50, 60], [0, 180, 0, 256])
    cv2.normalize(query_hist, query_hist, 0, 1, cv2.NORM_MINMAX)

    results = []

    # Compare with all images in search directory
    for filename in os.listdir(search_dir):
        if not filename.lower().endswith(('.jpg', '.jpeg', '.png')):
            continue

        filepath = os.path.join(search_dir, filename)
        img = cv2.imread(filepath)
        if img is None:
            continue

        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        hist = cv2.calcHist([hsv], [0, 1], None,
                             [50, 60], [0, 180, 0, 256])
        cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)

        # Calculate correlation (higher = more similar)
        similarity = cv2.compareHist(query_hist, hist, cv2.HISTCMP_CORREL)
        results.append((filename, similarity))

    # Sort by similarity
    results.sort(key=lambda x: x[1], reverse=True)

    print(f"Query: {query_path}")
    print(f"\nTop {top_k} similar images:")
    for filename, sim in results[:top_k]:
        print(f"  {filename}: {sim:.4f}")

    return results[:top_k]

# Usage example
find_similar_images('query.jpg', './image_database/', top_k=5)

6. Backprojection

Concept

Backprojection:
Detect specific color regions using histograms

Process:
1. Calculate color histogram of object of interest (ROI)
2. Replace each pixel in the entire image with its histogram value
3. High value = similar to color of interest

Applications:
- Color-based object tracking
- Core of CamShift/MeanShift algorithms

Example:
┌─────────────┐       ┌─────────────┐
   🟡 ROI                 
  (Yellow)     ──▶          High value = Yellow
                          
└─────────────┘       └─────────────┘
  Color Histogram      Backprojection Result

cv2.calcBackProject()

import cv2
import numpy as np

def backprojection_demo(image_path, roi_coords):
    """Backprojection demo"""
    img = cv2.imread(image_path)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # Set ROI region
    x, y, w, h = roi_coords
    roi = hsv[y:y+h, x:x+w]

    # Calculate ROI histogram
    roi_hist = cv2.calcHist(
        [roi], [0, 1], None,
        [180, 256], [0, 180, 0, 256]
    )
    cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)

    # Backprojection
    backproj = cv2.calcBackProject(
        [hsv], [0, 1], roi_hist,
        [0, 180, 0, 256], 1
    )

    # Remove noise with filtering
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    cv2.filter2D(backproj, -1, kernel, backproj)
    _, backproj = cv2.threshold(backproj, 50, 255, cv2.THRESH_BINARY)

    # Visualization
    result = img.copy()
    cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 0), 2)

    # Mask detected region
    mask = cv2.merge([backproj, backproj, backproj])
    detected = cv2.bitwise_and(img, mask)

    cv2.imshow('Original with ROI', result)
    cv2.imshow('Back Projection', backproj)
    cv2.imshow('Detected', detected)
    cv2.waitKey(0)

    return backproj

# Usage example (x, y, width, height)
backprojection_demo('scene.jpg', (100, 100, 50, 50))

Skin Color Detection

import cv2
import numpy as np

def detect_skin(image_path):
    """Skin color detection (using backprojection)"""
    img = cv2.imread(image_path)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # Skin color range (HSV)
    # H: 0-20, S: 48-255, V: 80-255 (typical skin color)
    lower_skin = np.array([0, 48, 80], dtype=np.uint8)
    upper_skin = np.array([20, 255, 255], dtype=np.uint8)

    # Skin color mask
    skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)

    # Generate histogram of skin region
    skin_region = cv2.bitwise_and(hsv, hsv, mask=skin_mask)
    skin_hist = cv2.calcHist([skin_region], [0, 1], skin_mask,
                              [180, 256], [0, 180, 0, 256])
    cv2.normalize(skin_hist, skin_hist, 0, 255, cv2.NORM_MINMAX)

    # Backprojection
    backproj = cv2.calcBackProject([hsv], [0, 1], skin_hist,
                                    [0, 180, 0, 256], 1)

    # Morphological operations
    kernel = np.ones((5, 5), np.uint8)
    backproj = cv2.morphologyEx(backproj, cv2.MORPH_OPEN, kernel)
    backproj = cv2.morphologyEx(backproj, cv2.MORPH_CLOSE, kernel)

    # Result
    result = cv2.bitwise_and(img, img, mask=backproj)

    cv2.imshow('Original', img)
    cv2.imshow('Skin Mask', backproj)
    cv2.imshow('Detected Skin', result)
    cv2.waitKey(0)

    return backproj

detect_skin('person.jpg')

Object Tracking with CamShift

import cv2
import numpy as np

def camshift_tracking(video_path):
    """Object tracking using CamShift"""
    cap = cv2.VideoCapture(video_path)

    # Select ROI from first frame
    ret, frame = cap.read()
    if not ret:
        return

    # Select ROI (select with mouse or specify directly)
    roi = cv2.selectROI('Select ROI', frame, False)
    cv2.destroyWindow('Select ROI')

    x, y, w, h = roi
    track_window = (x, y, w, h)

    # Calculate ROI histogram
    roi_frame = frame[y:y+h, x:x+w]
    hsv_roi = cv2.cvtColor(roi_frame, cv2.COLOR_BGR2HSV)

    mask = cv2.inRange(hsv_roi, np.array([0, 60, 32]),
                       np.array([180, 255, 255]))

    roi_hist = cv2.calcHist([hsv_roi], [0], mask, [180], [0, 180])
    cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)

    # CamShift termination criteria
    term_criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1)

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

        # Backprojection
        backproj = cv2.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)

        # Apply CamShift
        ret, track_window = cv2.CamShift(backproj, track_window, term_criteria)

        # Draw result (rotated rectangle)
        pts = cv2.boxPoints(ret)
        pts = np.int_(pts)
        cv2.polylines(frame, [pts], True, (0, 255, 0), 2)

        cv2.imshow('CamShift Tracking', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

# camshift_tracking('video.mp4')

7. Practice Problems

Problem 1: Automatic Contrast Adjustment

Analyze the histogram of an image and automatically perform optimal contrast adjustment.

Solution Code
import cv2
import numpy as np

def auto_contrast(image):
    """Automatic contrast adjustment (histogram stretching)"""
    if len(image.shape) == 3:
        # Color image: LAB conversion
        lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)

        # Histogram stretching on L channel
        l_min = np.min(l)
        l_max = np.max(l)
        l_stretched = ((l - l_min) * 255 / (l_max - l_min)).astype(np.uint8)

        lab_stretched = cv2.merge([l_stretched, a, b])
        result = cv2.cvtColor(lab_stretched, cv2.COLOR_LAB2BGR)
    else:
        # Grayscale
        img_min = np.min(image)
        img_max = np.max(image)
        result = ((image - img_min) * 255 / (img_max - img_min)).astype(np.uint8)

    return result

# Test
img = cv2.imread('low_contrast.jpg')
result = auto_contrast(img)
cv2.imshow('Original', img)
cv2.imshow('Auto Contrast', result)
cv2.waitKey(0)

Problem 2: Color Distribution Analysis

Extract the top 3 dominant colors from an image.

Solution Code
import cv2
import numpy as np
from collections import Counter

def find_dominant_colors(image, k=3):
    """Extract dominant colors using K-means"""
    # Convert image to 1D array
    pixels = image.reshape(-1, 3).astype(np.float32)

    # K-means clustering
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
    _, labels, centers = cv2.kmeans(
        pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS
    )

    # Count pixels in each cluster
    label_counts = Counter(labels.flatten())

    # Return colors and ratios
    colors = []
    total = len(labels)
    for idx, count in label_counts.most_common(k):
        color = centers[idx].astype(int)
        percentage = count / total * 100
        colors.append((color, percentage))

    # Visualize results
    result = np.zeros((100, 300, 3), dtype=np.uint8)
    x = 0
    for color, pct in colors:
        width = int(pct * 3)
        result[:, x:x+width] = color
        x += width
        print(f"BGR: {color}, Ratio: {pct:.1f}%")

    cv2.imshow('Dominant Colors', result)
    cv2.waitKey(0)

    return colors

# Test
img = cv2.imread('colorful.jpg')
colors = find_dominant_colors(img, k=5)

Problem 3: Illumination Normalization

Normalize a document image with uneven illumination.

Solution Code
import cv2
import numpy as np

def normalize_illumination(image):
    """Illumination normalization"""
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # Estimate background (large blur)
    background = cv2.GaussianBlur(gray, (101, 101), 0)

    # Remove background (original / background)
    normalized = cv2.divide(gray, background, scale=255)

    # Apply additional CLAHE
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    enhanced = clahe.apply(normalized)

    cv2.imshow('Original', gray)
    cv2.imshow('Background', background)
    cv2.imshow('Normalized', normalized)
    cv2.imshow('Enhanced', enhanced)
    cv2.waitKey(0)

    return enhanced

# Test
img = cv2.imread('uneven_document.jpg')
result = normalize_illumination(img)
Difficulty Topic Description
Histogram Plotting Visualize RGB channel histograms
⭐⭐ Contrast Enhancement Compare equalizeHist vs CLAHE
⭐⭐ Image Similarity Find similar images using histograms
⭐⭐⭐ Object Tracking Track colored objects with CamShift
⭐⭐⭐ HDR Tone Mapping Merge multi-exposure images

Next Steps


References

to navigate between lessons