Master traditional computer vision feature detection techniques including SIFT, SURF, ORB, and HOG descriptors for image matching, recognition, and analysis.
Feature detection and extraction are fundamental techniques in computer vision that identify distinctive points and patterns in images. These features enable tasks like image matching, object recognition, panorama stitching, and 3D reconstruction.
Before deep learning dominated computer vision, handcrafted features like SIFT, SURF, ORB, and HOG were the gold standard for many applications. Understanding these techniques remains crucial because they offer interpretability, work without training data, and are computationally efficient for many real-time applications.
In this lesson, we'll explore the theory and implementation of major feature detection algorithms, learn how to match features between images, and build practical applications using these powerful techniques.
Feature detection involves two main components: keypoints (where interesting patterns exist) and descriptors (what those patterns look like).
Keypoint Detection Fundamentals:
1import cv2
2import numpy as np
3
4def understand_keypoints():
5 """Demonstrate keypoint concepts."""
6
7 # Create test image with corners and edges
8 image = np.zeros((300, 400), dtype=np.uint8)
9 cv2.rectangle(image, (50, 50), (150, 150), 255, -1)
10 cv2.circle(image, (280, 100), 50, 200, -1)
11
12 # Harris corner detection - classic keypoint detector
13 harris_corners = cv2.cornerHarris(image.astype(np.float32), 2, 3, 0.04)
14 harris_corners = cv2.dilate(harris_corners, None)
15
16 # Threshold for corner response
17 corner_threshold = 0.01 * harris_corners.max()
18 corner_points = np.where(harris_corners > corner_threshold)
19
20 print(f"Harris corners found: {len(corner_points[0])}")
21
22 # Shi-Tomasi corner detection (Good Features to Track)
23 corners = cv2.goodFeaturesToTrack(
24 image,
25 maxCorners=100,
26 qualityLevel=0.01,
27 minDistance=10
28 )
29
30 print(f"Shi-Tomasi corners: {len(corners) if corners is not None else 0}")
31
32 return harris_corners, corners
33
34# Keypoint properties
35class KeypointAnalyzer:
36 """Analyze keypoint properties."""
37
38 def __init__(self):
39 self.orb = cv2.ORB_create(nfeatures=500)
40
41 def extract_and_analyze(self, image):
42 """Extract keypoints and analyze their properties."""
43 keypoints, descriptors = self.orb.detectAndCompute(image, None)
44
45 if not keypoints:
46 return None
47
48 # Analyze keypoint properties
49 properties = {
50 'count': len(keypoints),
51 'sizes': [kp.size for kp in keypoints],
52 'responses': [kp.response for kp in keypoints],
53 'angles': [kp.angle for kp in keypoints],
54 'octaves': [kp.octave for kp in keypoints]
55 }
56
57 print(f"Keypoints found: {properties['count']}")
58 print(f"Average size: {np.mean(properties['sizes']):.2f}")
59 print(f"Response range: [{min(properties['responses']):.4f}, "
60 f"{max(properties['responses']):.4f}]")
61
62 return keypoints, descriptors, properties
63
64 def visualize_keypoints(self, image, keypoints):
65 """Draw keypoints on image."""
66 output = cv2.drawKeypoints(
67 image, keypoints, None,
68 flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
69 )
70 return outputDescriptor Basics:
1def understand_descriptors(image, keypoints):
2 """Explain how descriptors work."""
3
4 # ORB creates 32-byte (256-bit) binary descriptors
5 orb = cv2.ORB_create()
6 keypoints, descriptors = orb.detectAndCompute(image, None)
7
8 print(f"Descriptor shape: {descriptors.shape}")
9 print(f"Each descriptor: {descriptors.shape[1]} bytes = {descriptors.shape[1] * 8} bits")
10 print(f"First descriptor:\n{descriptors[0]}")
11
12 # Descriptors encode local image patterns
13 # They should be:
14 # - Distinctive: Different for different image regions
15 # - Repeatable: Same for the same region under transformations
16 # - Compact: Efficient for storage and matching
17
18 return descriptorsThree major feature detectors dominate traditional computer vision: SIFT (Scale-Invariant Feature Transform), SURF (Speeded-Up Robust Features), and ORB (Oriented FAST and Rotated BRIEF).
SIFT (Scale-Invariant Feature Transform):
1import cv2
2import numpy as np
3
4def sift_detection(image):
5 """Detect features using SIFT."""
6 # SIFT is now free to use (patent expired)
7 sift = cv2.SIFT_create(
8 nfeatures=500, # Maximum features to retain
9 nOctaveLayers=3, # Layers per octave
10 contrastThreshold=0.04, # Filter low-contrast features
11 edgeThreshold=10, # Filter edge-like features
12 sigma=1.6 # Gaussian blur sigma
13 )
14
15 # Detect and compute
16 keypoints, descriptors = sift.detectAndCompute(image, None)
17
18 print(f"SIFT keypoints: {len(keypoints)}")
19 print(f"Descriptor shape: {descriptors.shape}") # (N, 128) float32
20 print(f"Descriptor type: {descriptors.dtype}")
21
22 # SIFT properties:
23 # - Scale invariant (works at different image scales)
24 # - Rotation invariant (assigns orientation to keypoints)
25 # - 128-dimensional float descriptor
26 # - Most distinctive but computationally expensive
27
28 return keypoints, descriptors
29
30def analyze_sift_keypoints(keypoints):
31 """Analyze SIFT keypoint distribution."""
32 sizes = [kp.size for kp in keypoints]
33 angles = [kp.angle for kp in keypoints]
34
35 print(f"Scale range: [{min(sizes):.2f}, {max(sizes):.2f}]")
36 print(f"Angle distribution: mean={np.mean(angles):.1f}, std={np.std(angles):.1f}")ORB (Oriented FAST and Rotated BRIEF):
1def orb_detection(image):
2 """Detect features using ORB - fast and free alternative."""
3 orb = cv2.ORB_create(
4 nfeatures=500, # Maximum features
5 scaleFactor=1.2, # Pyramid scale factor
6 nlevels=8, # Pyramid levels
7 edgeThreshold=31, # Border size
8 firstLevel=0, # First pyramid level
9 WTA_K=2, # Points for BRIEF comparison
10 scoreType=cv2.ORB_HARRIS_SCORE,
11 patchSize=31, # BRIEF descriptor patch size
12 fastThreshold=20 # FAST threshold
13 )
14
15 keypoints, descriptors = orb.detectAndCompute(image, None)
16
17 print(f"ORB keypoints: {len(keypoints)}")
18 print(f"Descriptor shape: {descriptors.shape}") # (N, 32) uint8
19 print(f"Descriptor type: {descriptors.dtype}")
20
21 # ORB properties:
22 # - Very fast (10x faster than SURF)
23 # - Binary descriptor (32 bytes)
24 # - Rotation invariant
25 # - Good for real-time applications
26
27 return keypoints, descriptors
28
29def compare_detectors(image):
30 """Compare different feature detectors."""
31 import time
32
33 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
34
35 results = {}
36
37 # SIFT
38 sift = cv2.SIFT_create(nfeatures=500)
39 start = time.time()
40 kp_sift, desc_sift = sift.detectAndCompute(gray, None)
41 results['SIFT'] = {
42 'keypoints': len(kp_sift),
43 'descriptor_size': desc_sift.shape[1] if desc_sift is not None else 0,
44 'time': time.time() - start
45 }
46
47 # ORB
48 orb = cv2.ORB_create(nfeatures=500)
49 start = time.time()
50 kp_orb, desc_orb = orb.detectAndCompute(gray, None)
51 results['ORB'] = {
52 'keypoints': len(kp_orb),
53 'descriptor_size': desc_orb.shape[1] if desc_orb is not None else 0,
54 'time': time.time() - start
55 }
56
57 # BRISK
58 brisk = cv2.BRISK_create()
59 start = time.time()
60 kp_brisk, desc_brisk = brisk.detectAndCompute(gray, None)
61 results['BRISK'] = {
62 'keypoints': len(kp_brisk),
63 'descriptor_size': desc_brisk.shape[1] if desc_brisk is not None else 0,
64 'time': time.time() - start
65 }
66
67 # AKAZE
68 akaze = cv2.AKAZE_create()
69 start = time.time()
70 kp_akaze, desc_akaze = akaze.detectAndCompute(gray, None)
71 results['AKAZE'] = {
72 'keypoints': len(kp_akaze),
73 'descriptor_size': desc_akaze.shape[1] if desc_akaze is not None else 0,
74 'time': time.time() - start
75 }
76
77 # Print comparison
78 print("Feature Detector Comparison:")
79 print("-" * 60)
80 print(f"{'Detector':<10} {'Keypoints':<12} {'Desc Size':<12} {'Time (ms)':<12}")
81 print("-" * 60)
82 for name, data in results.items():
83 print(f"{name:<10} {data['keypoints']:<12} {data['descriptor_size']:<12} "
84 f"{data['time']*1000:.2f}")
85
86 return resultsHOG descriptors capture edge direction distributions and are particularly effective for object detection, especially pedestrians and faces.
HOG Feature Extraction:
1import cv2
2import numpy as np
3from skimage.feature import hog
4from skimage import exposure
5
6def compute_hog_features(image, visualize=False):
7 """Compute HOG features for an image."""
8
9 # Resize to standard size
10 resized = cv2.resize(image, (64, 128)) # Standard for pedestrians
11 gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) if len(resized.shape) == 3 else resized
12
13 # HOG parameters
14 pixels_per_cell = (8, 8) # Cell size
15 cells_per_block = (2, 2) # Block size (for normalization)
16 orientations = 9 # Number of gradient bins
17
18 if visualize:
19 features, hog_image = hog(
20 gray,
21 orientations=orientations,
22 pixels_per_cell=pixels_per_cell,
23 cells_per_block=cells_per_block,
24 visualize=True,
25 channel_axis=None
26 )
27 # Enhance visualization
28 hog_image = exposure.rescale_intensity(hog_image, in_range=(0, 10))
29 return features, hog_image
30 else:
31 features = hog(
32 gray,
33 orientations=orientations,
34 pixels_per_cell=pixels_per_cell,
35 cells_per_block=cells_per_block,
36 channel_axis=None
37 )
38 return features
39
40def explain_hog_computation():
41 """Explain HOG computation step by step."""
42
43 print("HOG Computation Steps:")
44 print("=" * 50)
45 print("1. Compute gradients (Gx, Gy) using Sobel filters")
46 print("2. Compute magnitude: sqrt(Gx^2 + Gy^2)")
47 print("3. Compute orientation: arctan(Gy/Gx)")
48 print("4. Divide image into cells (e.g., 8x8 pixels)")
49 print("5. For each cell, create histogram of orientations")
50 print("6. Group cells into blocks for normalization")
51 print("7. Concatenate normalized histograms")
52 print()
53
54 # Example calculation
55 image_size = (128, 64) # Standard pedestrian window
56 cell_size = (8, 8)
57 block_size = (2, 2) # cells per block
58 orientations = 9
59
60 cells = (image_size[0] // cell_size[0], image_size[1] // cell_size[1])
61 blocks = (cells[0] - block_size[0] + 1, cells[1] - block_size[1] + 1)
62 features_per_block = block_size[0] * block_size[1] * orientations
63 total_features = blocks[0] * blocks[1] * features_per_block
64
65 print(f"Image size: {image_size}")
66 print(f"Cells: {cells} = {cells[0] * cells[1]} cells")
67 print(f"Blocks: {blocks} = {blocks[0] * blocks[1]} blocks")
68 print(f"Features per block: {features_per_block}")
69 print(f"Total HOG features: {total_features}")
70
71# OpenCV HOG Descriptor
72def opencv_hog_detector():
73 """Use OpenCV's HOG detector for pedestrians."""
74
75 hog = cv2.HOGDescriptor()
76 hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
77
78 # For custom training:
79 # hog = cv2.HOGDescriptor(
80 # _winSize=(64, 128),
81 # _blockSize=(16, 16),
82 # _blockStride=(8, 8),
83 # _cellSize=(8, 8),
84 # _nbins=9
85 # )
86
87 return hog
88
89def detect_pedestrians(image, hog_detector):
90 """Detect pedestrians using HOG + SVM."""
91
92 # Detect with multiple scales
93 boxes, weights = hog_detector.detectMultiScale(
94 image,
95 winStride=(8, 8),
96 padding=(4, 4),
97 scale=1.05
98 )
99
100 # Apply non-max suppression
101 boxes = np.array([[x, y, x + w, y + h] for (x, y, w, h) in boxes])
102
103 if len(boxes) > 0:
104 from scipy import ndimage
105 # Simple NMS
106 pick = []
107 x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
108 area = (x2 - x1 + 1) * (y2 - y1 + 1)
109 idxs = np.argsort(weights.flatten())
110
111 while len(idxs) > 0:
112 last = len(idxs) - 1
113 i = idxs[last]
114 pick.append(i)
115
116 xx1 = np.maximum(x1[i], x1[idxs[:last]])
117 yy1 = np.maximum(y1[i], y1[idxs[:last]])
118 xx2 = np.minimum(x2[i], x2[idxs[:last]])
119 yy2 = np.minimum(y2[i], y2[idxs[:last]])
120
121 w = np.maximum(0, xx2 - xx1 + 1)
122 h = np.maximum(0, yy2 - yy1 + 1)
123
124 overlap = (w * h) / area[idxs[:last]]
125 idxs = np.delete(idxs, np.concatenate(([last],
126 np.where(overlap > 0.3)[0])))
127
128 boxes = boxes[pick]
129 weights = weights[pick]
130
131 return boxes, weightsFeature matching finds corresponding features between images, essential for object recognition, image stitching, and 3D reconstruction.
Brute-Force Matching:
1import cv2
2import numpy as np
3
4def brute_force_matching(desc1, desc2, detector_type='ORB'):
5 """Match features using brute-force matcher."""
6
7 if detector_type == 'ORB':
8 # Binary descriptors use Hamming distance
9 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
10 matches = bf.match(desc1, desc2)
11 else:
12 # Float descriptors (SIFT) use L2 distance
13 bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
14 matches = bf.match(desc1, desc2)
15
16 # Sort by distance
17 matches = sorted(matches, key=lambda x: x.distance)
18
19 print(f"Total matches: {len(matches)}")
20 print(f"Best match distance: {matches[0].distance:.2f}")
21 print(f"Worst match distance: {matches[-1].distance:.2f}")
22
23 return matches
24
25def knn_matching(desc1, desc2, ratio_threshold=0.75, detector_type='SIFT'):
26 """Match using k-NN with Lowe's ratio test."""
27
28 if detector_type in ['ORB', 'BRISK', 'AKAZE']:
29 bf = cv2.BFMatcher(cv2.NORM_HAMMING)
30 else:
31 bf = cv2.BFMatcher(cv2.NORM_L2)
32
33 # Find 2 nearest neighbors
34 matches = bf.knnMatch(desc1, desc2, k=2)
35
36 # Apply ratio test
37 good_matches = []
38 for m, n in matches:
39 if m.distance < ratio_threshold * n.distance:
40 good_matches.append(m)
41
42 print(f"Total matches: {len(matches)}")
43 print(f"Good matches (ratio test): {len(good_matches)}")
44 print(f"Ratio: {len(good_matches)/len(matches)*100:.1f}%")
45
46 return good_matchesFLANN Matching (Faster):
1def flann_matching(desc1, desc2, detector_type='SIFT'):
2 """Fast approximate matching using FLANN."""
3
4 if detector_type in ['ORB', 'BRISK', 'AKAZE']:
5 # For binary descriptors
6 FLANN_INDEX_LSH = 6
7 index_params = dict(
8 algorithm=FLANN_INDEX_LSH,
9 table_number=6,
10 key_size=12,
11 multi_probe_level=1
12 )
13 else:
14 # For float descriptors (SIFT, SURF)
15 FLANN_INDEX_KDTREE = 1
16 index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
17
18 search_params = dict(checks=50)
19
20 flann = cv2.FlannBasedMatcher(index_params, search_params)
21 matches = flann.knnMatch(desc1, desc2, k=2)
22
23 # Ratio test
24 good_matches = []
25 for match in matches:
26 if len(match) == 2:
27 m, n = match
28 if m.distance < 0.7 * n.distance:
29 good_matches.append(m)
30
31 return good_matches
32
33def visualize_matches(img1, kp1, img2, kp2, matches, num_matches=50):
34 """Visualize feature matches between two images."""
35
36 # Draw top matches
37 matched_img = cv2.drawMatches(
38 img1, kp1, img2, kp2,
39 matches[:num_matches], None,
40 matchColor=(0, 255, 0),
41 singlePointColor=(255, 0, 0),
42 flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
43 )
44
45 return matched_imgHomography Estimation:
1def find_homography_from_matches(kp1, kp2, matches, min_matches=4):
2 """Estimate homography from matched keypoints."""
3
4 if len(matches) < min_matches:
5 print(f"Not enough matches: {len(matches)} < {min_matches}")
6 return None, None
7
8 # Extract matched keypoint coordinates
9 src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
10 dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
11
12 # Find homography using RANSAC
13 H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
14
15 inliers = mask.ravel().sum()
16 print(f"Homography inliers: {inliers}/{len(matches)}")
17
18 return H, mask
19
20def warp_perspective_with_homography(image, H, output_size):
21 """Warp image using homography."""
22 warped = cv2.warpPerspective(image, H, output_size)
23 return warped
24
25def stitch_images(img1, img2):
26 """Simple image stitching using feature matching."""
27
28 # Detect features
29 sift = cv2.SIFT_create()
30 kp1, desc1 = sift.detectAndCompute(img1, None)
31 kp2, desc2 = sift.detectAndCompute(img2, None)
32
33 # Match features
34 bf = cv2.BFMatcher()
35 matches = bf.knnMatch(desc1, desc2, k=2)
36
37 # Ratio test
38 good = [m for m, n in matches if m.distance < 0.75 * n.distance]
39
40 # Find homography
41 H, mask = find_homography_from_matches(kp1, kp2, good)
42
43 if H is None:
44 return None
45
46 # Warp and blend
47 h1, w1 = img1.shape[:2]
48 h2, w2 = img2.shape[:2]
49
50 # Determine output size
51 corners1 = np.float32([[0, 0], [w1, 0], [w1, h1], [0, h1]]).reshape(-1, 1, 2)
52 corners1_warped = cv2.perspectiveTransform(corners1, H)
53
54 all_corners = np.concatenate([
55 corners1_warped,
56 np.float32([[0, 0], [w2, 0], [w2, h2], [0, h2]]).reshape(-1, 1, 2)
57 ])
58
59 [x_min, y_min] = np.int32(all_corners.min(axis=0).ravel())
60 [x_max, y_max] = np.int32(all_corners.max(axis=0).ravel())
61
62 # Translation matrix
63 translation = np.array([[1, 0, -x_min], [0, 1, -y_min], [0, 0, 1]])
64 H_translated = translation @ H
65
66 # Warp first image
67 output_size = (x_max - x_min, y_max - y_min)
68 result = cv2.warpPerspective(img1, H_translated, output_size)
69
70 # Add second image
71 result[-y_min:-y_min+h2, -x_min:-x_min+w2] = img2
72
73 return resultBuild a robust feature matching system that can match objects across different images with scale and rotation invariance.
1"""
2Complete Feature Matching Pipeline
3Demonstrates robust feature detection and matching for image analysis.
4"""
5
6import numpy as np
7import cv2
8
9class FeatureMatchingPipeline:
10 """Robust feature matching between images."""
11
12 def __init__(self, detector='SIFT', matcher='FLANN'):
13 self.detector_type = detector
14 self.matcher_type = matcher
15 self.detector = self._create_detector(detector)
16 self.matcher = self._create_matcher(detector, matcher)
17
18 def _create_detector(self, detector_type):
19 """Create feature detector."""
20 detectors = {
21 'SIFT': cv2.SIFT_create(nfeatures=1000),
22 'ORB': cv2.ORB_create(nfeatures=1000),
23 'BRISK': cv2.BRISK_create(),
24 'AKAZE': cv2.AKAZE_create()
25 }
26 return detectors.get(detector_type, cv2.SIFT_create())
27
28 def _create_matcher(self, detector_type, matcher_type):
29 """Create feature matcher."""
30 if matcher_type == 'FLANN':
31 if detector_type in ['ORB', 'BRISK', 'AKAZE']:
32 index_params = dict(algorithm=6, table_number=6,
33 key_size=12, multi_probe_level=1)
34 else:
35 index_params = dict(algorithm=1, trees=5)
36 search_params = dict(checks=50)
37 return cv2.FlannBasedMatcher(index_params, search_params)
38 else:
39 norm = cv2.NORM_HAMMING if detector_type in ['ORB', 'BRISK', 'AKAZE'] else cv2.NORM_L2
40 return cv2.BFMatcher(norm)
41
42 def detect_and_compute(self, image):
43 """Detect keypoints and compute descriptors."""
44 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
45 keypoints, descriptors = self.detector.detectAndCompute(gray, None)
46 return keypoints, descriptors
47
48 def match_features(self, desc1, desc2, ratio_threshold=0.75):
49 """Match features with ratio test."""
50 if desc1 is None or desc2 is None:
51 return []
52
53 matches = self.matcher.knnMatch(desc1, desc2, k=2)
54
55 good_matches = []
56 for match in matches:
57 if len(match) == 2:
58 m, n = match
59 if m.distance < ratio_threshold * n.distance:
60 good_matches.append(m)
61
62 return good_matches
63
64 def find_homography(self, kp1, kp2, matches, ransac_threshold=5.0):
65 """Find homography matrix using RANSAC."""
66 if len(matches) < 4:
67 return None, None
68
69 src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
70 dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
71
72 H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, ransac_threshold)
73 return H, mask
74
75 def match_images(self, img1, img2, visualize=True):
76 """Complete matching pipeline."""
77 # Detect features
78 kp1, desc1 = self.detect_and_compute(img1)
79 kp2, desc2 = self.detect_and_compute(img2)
80
81 print(f"Image 1: {len(kp1)} keypoints")
82 print(f"Image 2: {len(kp2)} keypoints")
83
84 # Match features
85 matches = self.match_features(desc1, desc2)
86 print(f"Good matches: {len(matches)}")
87
88 # Find homography
89 H, mask = self.find_homography(kp1, kp2, matches)
90
91 if H is not None and mask is not None:
92 inliers = mask.ravel().sum()
93 print(f"Homography inliers: {inliers}/{len(matches)}")
94
95 result = {
96 'keypoints1': kp1,
97 'keypoints2': kp2,
98 'descriptors1': desc1,
99 'descriptors2': desc2,
100 'matches': matches,
101 'homography': H,
102 'inlier_mask': mask
103 }
104
105 if visualize and len(matches) > 0:
106 result['visualization'] = self._visualize(img1, kp1, img2, kp2, matches, mask)
107
108 return result
109
110 def _visualize(self, img1, kp1, img2, kp2, matches, mask=None):
111 """Create visualization of matches."""
112 if mask is not None:
113 # Filter to inliers only
114 inlier_matches = [m for m, is_inlier in zip(matches, mask.ravel()) if is_inlier]
115 else:
116 inlier_matches = matches
117
118 vis = cv2.drawMatches(
119 img1, kp1, img2, kp2,
120 inlier_matches[:50], None,
121 matchColor=(0, 255, 0),
122 flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
123 )
124 return vis
125
126
127# Example usage
128pipeline = FeatureMatchingPipeline(detector='SIFT', matcher='FLANN')
129
130# Create test images (rotated version)
131img1 = np.random.randint(0, 255, (300, 400, 3), dtype=np.uint8)
132cv2.rectangle(img1, (100, 100), (200, 200), (255, 0, 0), -1)
133cv2.circle(img1, (300, 150), 50, (0, 255, 0), -1)
134
135# Rotate img1 to create img2
136center = (img1.shape[1] // 2, img1.shape[0] // 2)
137M = cv2.getRotationMatrix2D(center, 15, 0.9)
138img2 = cv2.warpAffine(img1, M, (img1.shape[1], img1.shape[0]))
139
140# Match images
141result = pipeline.match_images(img1, img2)
142
143print(f"\nMatching complete!")
144print(f"Total good matches: {len(result['matches'])}")
145if result['homography'] is not None:
146 print("Homography found successfully")This pipeline demonstrates a complete feature matching workflow including detection with SIFT/ORB, matching with ratio test, homography estimation with RANSAC, and visualization. The modular design allows easy switching between different detectors and matchers for different use cases.
In the next lesson, we'll dive into Convolutional Neural Networks (CNNs), understanding how deep learning revolutionized feature extraction with learned representations that outperform handcrafted features.
Your progress is saved in this browser, on this device only.