Master classical computer vision techniques with OpenCV. Learn image processing, feature detection, object recognition, and build a foundation for deep learning-based computer vision applications.
Master classical computer vision techniques with OpenCV. Learn image processing, feature detection, object recognition, and build a foundation for deep learning-based computer vision applications.
Computer vision is the field that enables computers to "see" and understand visual information from the world. Before the deep learning revolution, classical computer vision techniques formed the foundation of image analysis and object recognition.
OpenCV (Open Source Computer Vision Library) is the most widely used library for computer vision, offering:
This lesson covers classical computer vision techniques that remain essential for:
---
A digital image is a 2D array (or 3D for color) of discrete pixels:
1import cv2
2import numpy as np
3import matplotlib.pyplot as plt
4
5# Load an image
6image = cv2.imread('image.jpg')
7
8# OpenCV uses BGR color order (not RGB!)
9# Convert to RGB for matplotlib
10image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
11
12# Image properties
13print(f"Image shape: {image.shape}") # (height, width, channels)
14print(f"Data type: {image.dtype}") # uint8 (0-255)
15print(f"Image size: {image.size} pixels")
16
17# Access pixel value
18pixel = image[100, 150] # [row, col] = [y, x]
19print(f"Pixel at (150, 100): {pixel}") # [B, G, R]Key concepts:
Different representations of color information:
1# RGB: Red, Green, Blue (most common)
2rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
3
4# Grayscale: Single channel (intensity)
5gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
6
7# HSV: Hue, Saturation, Value (useful for color-based segmentation)
8hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
9
10# LAB: Lightness, A (green-red), B (blue-yellow)
11lab_image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
12
13# Visualize channels
14fig, axes = plt.subplots(2, 3, figsize=(15, 10))
15
16axes[0, 0].imshow(rgb_image)
17axes[0, 0].set_title('Original (RGB)')
18
19axes[0, 1].imshow(gray_image, cmap='gray')
20axes[0, 1].set_title('Grayscale')
21
22axes[0, 2].imshow(hsv_image)
23axes[0, 2].set_title('HSV')
24
25# Show individual RGB channels
26axes[1, 0].imshow(rgb_image[:, :, 0], cmap='Reds')
27axes[1, 0].set_title('Red Channel')
28
29axes[1, 1].imshow(rgb_image[:, :, 1], cmap='Greens')
30axes[1, 1].set_title('Green Channel')
31
32axes[1, 2].imshow(rgb_image[:, :, 2], cmap='Blues')
33axes[1, 2].set_title('Blue Channel')
34
35plt.tight_layout()
36plt.show()When to use each color space:
1# Resize image
2resized = cv2.resize(image, (640, 480)) # (width, height)
3
4# Maintain aspect ratio
5height, width = image.shape[:2]
6new_width = 640
7new_height = int(height * (new_width / width))
8resized_aspect = cv2.resize(image, (new_width, new_height))
9
10# Rotate image
11center = (width // 2, height // 2)
12rotation_matrix = cv2.getRotationMatrix2D(center, 45, 1.0) # 45 degrees
13rotated = cv2.warpAffine(image, rotation_matrix, (width, height))
14
15# Flip image
16flipped_horizontal = cv2.flip(image, 1) # 1: horizontal, 0: vertical, -1: both
17flipped_vertical = cv2.flip(image, 0)
18
19# Crop image
20x, y, w, h = 100, 100, 400, 300 # x, y, width, height
21cropped = image[y:y+h, x:x+w]
22
23# Brightness adjustment
24bright_image = cv2.convertScaleAbs(image, alpha=1.5, beta=30) # alpha: contrast, beta: brightness
25
26# Contrast adjustment
27contrast_image = cv2.convertScaleAbs(image, alpha=2.0, beta=0)---
Blurring reduces noise and detail:
1# 1. Averaging (Box Filter)
2avg_blur = cv2.blur(image, (5, 5)) # kernel size
3
4# 2. Gaussian Blur (most common)
5# Better than averaging: weights by distance from center
6gaussian_blur = cv2.GaussianBlur(image, (5, 5), 0) # kernel size, sigma
7
8# 3. Median Blur (excellent for salt-and-pepper noise)
9median_blur = cv2.medianBlur(image, 5)
10
11# 4. Bilateral Filter (edge-preserving)
12# Smooths while keeping edges sharp
13bilateral = cv2.bilateralFilter(image, 9, 75, 75)
14
15# Visualize different blurs
16fig, axes = plt.subplots(2, 2, figsize=(12, 10))
17
18axes[0, 0].imshow(cv2.cvtColor(avg_blur, cv2.COLOR_BGR2RGB))
19axes[0, 0].set_title('Average Blur')
20
21axes[0, 1].imshow(cv2.cvtColor(gaussian_blur, cv2.COLOR_BGR2RGB))
22axes[0, 1].set_title('Gaussian Blur')
23
24axes[1, 0].imshow(cv2.cvtColor(median_blur, cv2.COLOR_BGR2RGB))
25axes[1, 0].set_title('Median Blur')
26
27axes[1, 1].imshow(cv2.cvtColor(bilateral, cv2.COLOR_BGR2RGB))
28axes[1, 1].set_title('Bilateral Filter (Edge-Preserving)')
29
30plt.tight_layout()
31plt.show()Applications:
Enhance edges and details:
1# Method 1: Using kernel
2sharpening_kernel = np.array([
3 [0, -1, 0],
4 [-1, 5, -1],
5 [0, -1, 0]
6])
7sharpened = cv2.filter2D(image, -1, sharpening_kernel)
8
9# Method 2: Unsharp masking (more control)
10blurred = cv2.GaussianBlur(image, (0, 0), 3)
11sharpened_unsharp = cv2.addWeighted(image, 1.5, blurred, -0.5, 0)Understanding convolution - the foundation of CNNs:
1# Define custom kernel
2# Edge detection kernel
3edge_kernel = np.array([
4 [-1, -1, -1],
5 [-1, 8, -1],
6 [-1, -1, -1]
7])
8
9# Apply kernel
10edges = cv2.filter2D(gray_image, -1, edge_kernel)
11
12# Common kernels
13kernels = {
14 'identity': np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]),
15 'sharpen': np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]),
16 'box_blur': np.ones((3, 3)) / 9,
17 'gaussian': np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) / 16,
18 'edge_detect': np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]])
19}
20
21# Visualize kernel effects
22fig, axes = plt.subplots(2, 3, figsize=(15, 10))
23for ax, (name, kernel) in zip(axes.flat, kernels.items()):
24 filtered = cv2.filter2D(gray_image, -1, kernel)
25 ax.imshow(filtered, cmap='gray')
26 ax.set_title(name)
27plt.tight_layout()
28plt.show()---
Edges represent significant changes in intensity - crucial for object boundaries.
1# 1. Sobel Operator (gradient in x and y directions)
2sobelx = cv2.Sobel(gray_image, cv2.CV_64F, 1, 0, ksize=3) # dx=1, dy=0
3sobely = cv2.Sobel(gray_image, cv2.CV_64F, 0, 1, ksize=3) # dx=0, dy=1
4
5# Combine x and y gradients
6sobel_combined = cv2.magnitude(sobelx, sobely)
7
8# 2. Scharr Operator (improved Sobel)
9scharrx = cv2.Scharr(gray_image, cv2.CV_64F, 1, 0)
10scharry = cv2.Scharr(gray_image, cv2.CV_64F, 0, 1)
11scharr_combined = cv2.magnitude(scharrx, scharry)
12
13# 3. Laplacian (second derivative)
14laplacian = cv2.Laplacian(gray_image, cv2.CV_64F)
15
16# 4. Canny Edge Detection (most popular)
17# Multi-stage algorithm: noise reduction, gradient, non-max suppression, hysteresis
18edges_canny = cv2.Canny(gray_image, threshold1=50, threshold2=150)
19
20# Visualize edge detectors
21fig, axes = plt.subplots(2, 3, figsize=(15, 10))
22
23axes[0, 0].imshow(gray_image, cmap='gray')
24axes[0, 0].set_title('Original')
25
26axes[0, 1].imshow(np.abs(sobelx), cmap='gray')
27axes[0, 1].set_title('Sobel X')
28
29axes[0, 2].imshow(np.abs(sobely), cmap='gray')
30axes[0, 2].set_title('Sobel Y')
31
32axes[1, 0].imshow(sobel_combined, cmap='gray')
33axes[1, 0].set_title('Sobel Combined')
34
35axes[1, 1].imshow(np.abs(laplacian), cmap='gray')
36axes[1, 1].set_title('Laplacian')
37
38axes[1, 2].imshow(edges_canny, cmap='gray')
39axes[1, 2].set_title('Canny Edges')
40
41plt.tight_layout()
42plt.show()Canny is the most sophisticated edge detector:
Algorithm steps:
1# Canny with different thresholds
2low_threshold = 50
3high_threshold = 150
4
5# Try different threshold combinations
6fig, axes = plt.subplots(2, 3, figsize=(15, 10))
7
8thresholds = [(30, 90), (50, 150), (100, 200), (150, 250), (200, 300)]
9
10for ax, (low, high) in zip(axes.flat, thresholds):
11 edges = cv2.Canny(gray_image, low, high)
12 ax.imshow(edges, cmap='gray')
13 ax.set_title(f'Canny: low={low}, high={high}')
14
15plt.tight_layout()
16plt.show()---
Operations based on image shape, useful for processing binary images:
1# Create binary image (threshold)
2_, binary = cv2.threshold(gray_image, 127, 255, cv2.THRESH_BINARY)
3
4# Define structuring element (kernel)
5kernel = np.ones((5, 5), np.uint8)
6
7# 1. Erosion: Shrink foreground, remove small noise
8eroded = cv2.erode(binary, kernel, iterations=1)
9
10# 2. Dilation: Expand foreground, fill small holes
11dilated = cv2.dilate(binary, kernel, iterations=1)
12
13# 3. Opening: Erosion followed by dilation (remove noise)
14opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
15
16# 4. Closing: Dilation followed by erosion (fill holes)
17closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
18
19# 5. Gradient: Difference between dilation and erosion (outline)
20gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)
21
22# 6. Top Hat: Difference between input and opening (bright spots)
23tophat = cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel)
24
25# 7. Black Hat: Difference between closing and input (dark spots)
26blackhat = cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel)
27
28# Visualize morphological operations
29operations = {
30 'Original': binary,
31 'Erosion': eroded,
32 'Dilation': dilated,
33 'Opening': opened,
34 'Closing': closed,
35 'Gradient': gradient,
36 'Top Hat': tophat,
37 'Black Hat': blackhat
38}
39
40fig, axes = plt.subplots(2, 4, figsize=(16, 8))
41for ax, (name, img) in zip(axes.flat, operations.items()):
42 ax.imshow(img, cmap='gray')
43 ax.set_title(name)
44plt.tight_layout()
45plt.show()Applications:
---
Features are distinctive points or regions in images that can be reliably detected.
Corners are important features where intensity changes in multiple directions:
1# Harris Corner Detection
2gray_float = np.float32(gray_image)
3harris_corners = cv2.cornerHarris(gray_float, blockSize=2, ksize=3, k=0.04)
4
5# Dilate to mark corners
6harris_corners = cv2.dilate(harris_corners, None)
7
8# Threshold and mark corners
9image_corners = image.copy()
10image_corners[harris_corners > 0.01 * harris_corners.max()] = [0, 0, 255]
11
12plt.figure(figsize=(12, 6))
13plt.subplot(121)
14plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
15plt.title('Original')
16plt.subplot(122)
17plt.imshow(cv2.cvtColor(image_corners, cv2.COLOR_BGR2RGB))
18plt.title('Harris Corners')
19plt.show()Improved corner detection method:
1# Detect corners
2corners = cv2.goodFeaturesToTrack(
3 gray_image,
4 maxCorners=100,
5 qualityLevel=0.01,
6 minDistance=10
7)
8
9# Draw corners
10image_corners = image.copy()
11for corner in corners:
12 x, y = corner.ravel()
13 cv2.circle(image_corners, (int(x), int(y)), 5, (0, 255, 0), -1)
14
15plt.imshow(cv2.cvtColor(image_corners, cv2.COLOR_BGR2RGB))
16plt.title('Shi-Tomasi Corners')
17plt.show()SIFT detects features invariant to scale, rotation, and illumination:
1# Create SIFT detector
2sift = cv2.SIFT_create()
3
4# Detect keypoints and compute descriptors
5keypoints, descriptors = sift.detectAndCompute(gray_image, None)
6
7# Draw keypoints
8image_sift = cv2.drawKeypoints(
9 image, keypoints, None,
10 flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
11)
12
13print(f"Number of SIFT keypoints: {len(keypoints)}")
14print(f"Descriptor shape: {descriptors.shape}") # (num_keypoints, 128)
15
16plt.figure(figsize=(12, 8))
17plt.imshow(cv2.cvtColor(image_sift, cv2.COLOR_BGR2RGB))
18plt.title(f'SIFT Features ({len(keypoints)} keypoints)')
19plt.show()SIFT descriptor: 128-dimensional feature vector describing local image patch
Faster alternative to SIFT, free to use:
1# Create ORB detector
2orb = cv2.ORB_create(nfeatures=500)
3
4# Detect and compute
5keypoints, descriptors = orb.detectAndCompute(gray_image, None)
6
7# Draw keypoints
8image_orb = cv2.drawKeypoints(
9 image, keypoints, None,
10 color=(0, 255, 0),
11 flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
12)
13
14print(f"Number of ORB keypoints: {len(keypoints)}")
15print(f"Descriptor shape: {descriptors.shape}") # (num_keypoints, 32)
16
17plt.figure(figsize=(12, 8))
18plt.imshow(cv2.cvtColor(image_orb, cv2.COLOR_BGR2RGB))
19plt.title(f'ORB Features ({len(keypoints)} keypoints)')
20plt.show()Comparison:
---
Match features between two images:
1# Load two images
2img1 = cv2.imread('image1.jpg', cv2.IMREAD_GRAYSCALE)
3img2 = cv2.imread('image2.jpg', cv2.IMREAD_GRAYSCALE)
4
5# Detect ORB features
6orb = cv2.ORB_create()
7kp1, des1 = orb.detectAndCompute(img1, None)
8kp2, des2 = orb.detectAndCompute(img2, None)
9
10# Create BFMatcher (Brute Force Matcher)
11bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
12
13# Match descriptors
14matches = bf.match(des1, des2)
15
16# Sort by distance (best matches first)
17matches = sorted(matches, key=lambda x: x.distance)
18
19# Draw matches
20img_matches = cv2.drawMatches(
21 img1, kp1, img2, kp2, matches[:50],
22 None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
23)
24
25plt.figure(figsize=(16, 8))
26plt.imshow(img_matches)
27plt.title(f'Feature Matches (top 50 of {len(matches)})')
28plt.show()1# FLANN parameters
2FLANN_INDEX_LSH = 6
3index_params = dict(
4 algorithm=FLANN_INDEX_LSH,
5 table_number=6,
6 key_size=12,
7 multi_probe_level=1
8)
9search_params = dict(checks=50)
10
11# Create FLANN matcher
12flann = cv2.FlannBasedMatcher(index_params, search_params)
13
14# Match with k=2 neighbors for ratio test
15matches = flann.knnMatch(des1, des2, k=2)
16
17# Apply ratio test (Lowe's ratio test)
18good_matches = []
19for match_pair in matches:
20 if len(match_pair) == 2:
21 m, n = match_pair
22 if m.distance < 0.7 * n.distance:
23 good_matches.append(m)
24
25print(f"Good matches: {len(good_matches)} out of {len(matches)}")---
Haar cascades are pre-trained classifiers for object detection:
1# Load pre-trained face cascade
2face_cascade = cv2.CascadeClassifier(
3 cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
4)
5
6# Load eye cascade
7eye_cascade = cv2.CascadeClassifier(
8 cv2.data.haarcascades + 'haarcascade_eye.xml'
9)
10
11# Detect faces
12faces = face_cascade.detectMultiScale(
13 gray_image,
14 scaleFactor=1.1,
15 minNeighbors=5,
16 minSize=(30, 30)
17)
18
19print(f"Found {len(faces)} faces")
20
21# Draw rectangles around faces and detect eyes
22image_detected = image.copy()
23for (x, y, w, h) in faces:
24 cv2.rectangle(image_detected, (x, y), (x+w, y+h), (255, 0, 0), 2)
25
26 # Region of interest for eyes (within face)
27 roi_gray = gray_image[y:y+h, x:x+w]
28 roi_color = image_detected[y:y+h, x:x+w]
29
30 eyes = eye_cascade.detectMultiScale(roi_gray)
31 for (ex, ey, ew, eh) in eyes:
32 cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)
33
34plt.figure(figsize=(12, 8))
35plt.imshow(cv2.cvtColor(image_detected, cv2.COLOR_BGR2RGB))
36plt.title('Face and Eye Detection')
37plt.show()Available cascades:
---
HOG is a feature descriptor used for object detection:
1from skimage.feature import hog
2from skimage import exposure
3
4# Compute HOG features
5hog_features, hog_image = hog(
6 gray_image,
7 orientations=9,
8 pixels_per_cell=(8, 8),
9 cells_per_block=(2, 2),
10 visualize=True,
11 block_norm='L2-Hys'
12)
13
14# Rescale for visualization
15hog_image_rescaled = exposure.rescale_intensity(hog_image, in_range=(0, 10))
16
17plt.figure(figsize=(12, 6))
18plt.subplot(121)
19plt.imshow(gray_image, cmap='gray')
20plt.title('Original')
21plt.subplot(122)
22plt.imshow(hog_image_rescaled, cmap='gray')
23plt.title('HOG Features')
24plt.show()
25
26print(f"HOG feature vector length: {len(hog_features)}")HOG + SVM was the state-of-the-art for pedestrian detection before deep learning.
---
Complete face detection and analysis system:
1def detect_and_analyze_faces(image_path):
2 """Complete face detection and analysis pipeline"""
3
4 # Load image
5 image = cv2.imread(image_path)
6 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7
8 # Load cascades
9 face_cascade = cv2.CascadeClassifier(
10 cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
11 )
12 eye_cascade = cv2.CascadeClassifier(
13 cv2.data.haarcascades + 'haarcascade_eye.xml'
14 )
15 smile_cascade = cv2.CascadeClassifier(
16 cv2.data.haarcascades + 'haarcascade_smile.xml'
17 )
18
19 # Detect faces
20 faces = face_cascade.detectMultiScale(
21 gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
22 )
23
24 print(f"\nDetected {len(faces)} face(s)")
25
26 result_image = image.copy()
27
28 for i, (x, y, w, h) in enumerate(faces):
29 # Draw face rectangle
30 cv2.rectangle(result_image, (x, y), (x+w, y+h), (255, 0, 0), 2)
31 cv2.putText(
32 result_image, f'Face {i+1}', (x, y-10),
33 cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 0, 0), 2
34 )
35
36 # ROI for eyes and smile
37 roi_gray = gray[y:y+h, x:x+w]
38 roi_color = result_image[y:y+h, x:x+w]
39
40 # Detect eyes
41 eyes = eye_cascade.detectMultiScale(roi_gray)
42 for (ex, ey, ew, eh) in eyes:
43 cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)
44
45 # Detect smile
46 smiles = smile_cascade.detectMultiScale(
47 roi_gray, scaleFactor=1.8, minNeighbors=20
48 )
49 for (sx, sy, sw, sh) in smiles:
50 cv2.rectangle(roi_color, (sx, sy), (sx+sw, sy+sh), (0, 0, 255), 2)
51 cv2.putText(
52 roi_color, 'Smile', (sx, sy-10),
53 cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2
54 )
55
56 print(f" Face {i+1}: {len(eyes)} eyes, {len(smiles)} smile(s)")
57
58 return result_image
59
60# Use the pipeline
61result = detect_and_analyze_faces('group_photo.jpg')
62plt.figure(figsize=(12, 8))
63plt.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
64plt.title('Complete Face Analysis')
65plt.axis('off')
66plt.show()---
Classical CV techniques complement deep learning:
1def preprocess_for_deep_learning(image):
2 """Prepare image for deep learning model"""
3
4 # Resize to model input size
5 resized = cv2.resize(image, (224, 224))
6
7 # Convert BGR to RGB
8 rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
9
10 # Normalize pixel values
11 normalized = rgb.astype(np.float32) / 255.0
12
13 # Subtract mean (ImageNet mean)
14 mean = np.array([0.485, 0.456, 0.406])
15 std = np.array([0.229, 0.224, 0.225])
16 preprocessed = (normalized - mean) / std
17
18 return preprocessed1def non_max_suppression(boxes, scores, threshold=0.5):
2 """Remove overlapping bounding boxes"""
3 if len(boxes) == 0:
4 return []
5
6 # Convert to float
7 boxes = boxes.astype(float)
8
9 # Coordinates of bounding boxes
10 x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
11
12 # Compute area
13 areas = (x2 - x1 + 1) * (y2 - y1 + 1)
14
15 # Sort by score
16 order = scores.argsort()[::-1]
17
18 keep = []
19 while order.size > 0:
20 i = order[0]
21 keep.append(i)
22
23 # Compute IoU with remaining boxes
24 xx1 = np.maximum(x1[i], x1[order[1:]])
25 yy1 = np.maximum(y1[i], y1[order[1:]])
26 xx2 = np.minimum(x2[i], x2[order[1:]])
27 yy2 = np.minimum(y2[i], y2[order[1:]])
28
29 w = np.maximum(0.0, xx2 - xx1 + 1)
30 h = np.maximum(0.0, yy2 - yy1 + 1)
31 intersection = w * h
32
33 iou = intersection / (areas[i] + areas[order[1:]] - intersection)
34
35 # Keep boxes with IoU less than threshold
36 inds = np.where(iou <= threshold)[0]
37 order = order[inds + 1]
38
39 return keep---
Classical computer vision with OpenCV provides:
Why classical CV still matters:
Next Steps:
Classical CV techniques remain essential tools in the modern AI practitioner's toolkit!
Your progress is saved in this browser, on this device only.