Skip to main content

Feature Detection and Matching

Learn how to detect and match distinctive features in images, essential for tasks like image stitching, object recognition, and camera calibration.

What are Features?

Features are distinctive points or regions in an image that can be reliably detected across different views. Good features are:
  • Repeatable (can be found in different images of the same scene)
  • Distinctive (can be distinguished from nearby features)
  • Local (not affected by clutter or occlusion)
  • Efficient (fast to compute)

Corner Detection

Harris Corner Detector

import cv2 as cv
import numpy as np

# Load image
img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
gray = np.float32(gray)

# Apply Harris corner detection
dst = cv.cornerHarris(gray, blockSize=2, ksize=3, k=0.04)

# Dilate to mark the corners
dst = cv.dilate(dst, None)

# Threshold for optimal value (adjust based on image)
img[dst > 0.01 * dst.max()] = [0, 0, 255]

cv.imshow('Harris Corners', img)
cv.waitKey(0)

Shi-Tomasi Corner Detector (Good Features to Track)

import cv2 as cv
import numpy as np

img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Parameters
maxCorners = 100
qualityLevel = 0.01
minDistance = 10

# Detect corners
corners = cv.goodFeaturesToTrack(gray, maxCorners, qualityLevel, minDistance)
corners = np.int0(corners)

# Draw corners
for corner in corners:
    x, y = corner.ravel()
    cv.circle(img, (x, y), 5, (0, 255, 0), -1)

cv.imshow('Shi-Tomasi Corners', img)
cv.waitKey(0)

Edge Detection

Canny Edge Detector

import cv2 as cv

img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Apply Gaussian blur to reduce noise
blurred = cv.GaussianBlur(gray, (5, 5), 0)

# Canny edge detection
# threshold1: lower threshold
# threshold2: upper threshold
edges = cv.Canny(blurred, threshold1=50, threshold2=150)

cv.imshow('Original', gray)
cv.imshow('Edges', edges)
cv.waitKey(0)
For Canny edge detection:
  • Use a 2:1 or 3:1 ratio between upper and lower thresholds
  • Lower threshold: detects weak edges
  • Upper threshold: detects strong edges
  • Edges are connected if they’re above the lower threshold and connected to an edge above the upper threshold

Feature Descriptors

SIFT (Scale-Invariant Feature Transform)

import cv2 as cv

img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Create SIFT detector
sift = cv.SIFT_create()

# Detect keypoints and compute descriptors
keypoints, descriptors = sift.detectAndCompute(gray, None)

# Draw keypoints
img_keypoints = cv.drawKeypoints(img, keypoints, None,
                                 flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

print(f"Number of keypoints: {len(keypoints)}")
print(f"Descriptor shape: {descriptors.shape}")

cv.imshow('SIFT Keypoints', img_keypoints)
cv.waitKey(0)

ORB (Oriented FAST and Rotated BRIEF)

ORB is a fast alternative to SIFT and SURF:
import cv2 as cv

img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Create ORB detector
orb = cv.ORB_create(nfeatures=400)

# Detect and compute
keypoints, descriptors = orb.detectAndCompute(gray, None)

# Draw keypoints
img_keypoints = cv.drawKeypoints(img, keypoints, None, 
                                 color=(0, 255, 0))

print(f"Number of ORB keypoints: {len(keypoints)}")
cv.imshow('ORB Keypoints', img_keypoints)
cv.waitKey(0)

AKAZE

import cv2 as cv

img = cv.imread('image.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# Create AKAZE detector
akaze = cv.AKAZE_create()

# Detect and compute
keypoints, descriptors = akaze.detectAndCompute(gray, None)

# Draw keypoints
img_keypoints = cv.drawKeypoints(img, keypoints, None, 
                                 flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv.imshow('AKAZE Keypoints', img_keypoints)
cv.waitKey(0)

Feature Matching

Based on OpenCV’s find_obj.py sample:

Brute-Force Matcher

import cv2 as cv
import numpy as np

# Load two images
img1 = cv.imread('box.png', cv.IMREAD_GRAYSCALE)
img2 = cv.imread('box_in_scene.png', cv.IMREAD_GRAYSCALE)

# Create ORB detector
orb = cv.ORB_create(400)

# Detect and compute for both images
kp1, desc1 = orb.detectAndCompute(img1, None)
kp2, desc2 = orb.detectAndCompute(img2, None)

# Create BFMatcher
bf = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True)

# Match descriptors
matches = bf.match(desc1, desc2)

# Sort matches by distance (best matches first)
matches = sorted(matches, key=lambda x: x.distance)

# Draw top 20 matches
img_matches = cv.drawMatches(img1, kp1, img2, kp2, matches[:20], None,
                             flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)

print(f"Number of matches: {len(matches)}")
cv.imshow('Matches', img_matches)
cv.waitKey(0)

FLANN-Based Matcher

Faster for large datasets:
import cv2 as cv
import numpy as np

img1 = cv.imread('box.png', cv.IMREAD_GRAYSCALE)
img2 = cv.imread('box_in_scene.png', cv.IMREAD_GRAYSCALE)

# Use SIFT for FLANN
sift = cv.SIFT_create()
kp1, desc1 = sift.detectAndCompute(img1, None)
kp2, desc2 = sift.detectAndCompute(img2, None)

# FLANN parameters
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)

# Create FLANN matcher
flann = cv.FlannBasedMatcher(index_params, search_params)

# Find k=2 best matches for each descriptor
matches = flann.knnMatch(desc1, desc2, k=2)

# Apply ratio test (Lowe's ratio test)
good_matches = []
for m, n in matches:
    if m.distance < 0.75 * n.distance:
        good_matches.append(m)

print(f"Good matches: {len(good_matches)} / {len(matches)}")

# Draw matches
img_matches = cv.drawMatches(img1, kp1, img2, kp2, good_matches, None,
                             flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)

cv.imshow('FLANN Matches', img_matches)
cv.waitKey(0)

Finding Homography

Find the transformation between matched images:
import cv2 as cv
import numpy as np

# After getting good matches (from previous example)
# Extract location of good matches
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)

# Find homography
M, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0)

# Get dimensions of first image
h, w = img1.shape

# Define corners of first image
pts = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)

# Transform corners to second image
dst = cv.perspectiveTransform(pts, M)

# Draw bounding box in second image
img2_color = cv.cvtColor(img2, cv.COLOR_GRAY2BGR)
cv.polylines(img2_color, [np.int32(dst)], True, (0, 255, 0), 3)

cv.imshow('Object Detection', img2_color)
cv.waitKey(0)

Blob Detection

import cv2 as cv
import numpy as np

img = cv.imread('blobs.jpg', cv.IMREAD_GRAYSCALE)

# Setup SimpleBlobDetector parameters
params = cv.SimpleBlobDetector_Params()

# Filter by area
params.filterByArea = True
params.minArea = 100

# Filter by circularity
params.filterByCircularity = True
params.minCircularity = 0.1

# Filter by convexity
params.filterByConvexity = True
params.minConvexity = 0.5

# Filter by inertia
params.filterByInertia = True
params.minInertiaRatio = 0.01

# Create detector
detector = cv.SimpleBlobDetector_create(params)

# Detect blobs
keypoints = detector.detect(img)

# Draw detected blobs
img_with_keypoints = cv.drawKeypoints(img, keypoints, None, 
                                     (0, 0, 255),
                                     cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

print(f"Number of blobs: {len(keypoints)}")
cv.imshow('Blobs', img_with_keypoints)
cv.waitKey(0)
Feature detector comparison:
  • SIFT: Most robust, patented (free since 2020), slower
  • SURF: Fast, patented, good for real-time
  • ORB: Free, fast, good alternative to SIFT/SURF
  • AKAZE: Free, fast, works well with planar scenes
  • BRISK: Free, very fast, binary descriptor
When matching features, always use the ratio test (Lowe’s ratio test) to filter out ambiguous matches and reduce false positives.

Next Steps