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
- Python
- C++
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)
#include <opencv2/opencv.hpp>
using namespace cv;
int main() {
Mat img = imread("image.jpg");
Mat gray, dst, dst_norm;
cvtColor(img, gray, COLOR_BGR2GRAY);
// Harris corner detection
cornerHarris(gray, dst, 2, 3, 0.04);
// Normalize
normalize(dst, dst_norm, 0, 255, NORM_MINMAX);
// Draw corners
for(int i = 0; i < dst_norm.rows; i++) {
for(int j = 0; j < dst_norm.cols; j++) {
if((int)dst_norm.at<float>(i,j) > 200) {
circle(img, Point(j,i), 5, Scalar(0,0,255), 2);
}
}
}
imshow("Harris Corners", img);
waitKey(0);
return 0;
}
Shi-Tomasi Corner Detector (Good Features to Track)
- Python
- C++
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)
#include <opencv2/opencv.hpp>
using namespace cv;
int main() {
Mat img = imread("image.jpg");
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
vector<Point2f> corners;
int maxCorners = 100;
double qualityLevel = 0.01;
double minDistance = 10;
goodFeaturesToTrack(gray, corners, maxCorners,
qualityLevel, minDistance);
for(size_t i = 0; i < corners.size(); i++) {
circle(img, corners[i], 5, Scalar(0, 255, 0), -1);
}
imshow("Shi-Tomasi Corners", img);
waitKey(0);
return 0;
}
Edge Detection
Canny Edge Detector
- Python
- C++
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)
#include <opencv2/opencv.hpp>
using namespace cv;
int main() {
Mat img = imread("image.jpg");
Mat gray, blurred, edges;
cvtColor(img, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, blurred, Size(5, 5), 0);
Canny(blurred, edges, 50, 150);
imshow("Original", gray);
imshow("Edges", edges);
waitKey(0);
return 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)
- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
Mat img = imread("image.jpg");
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
// Create SIFT detector
Ptr<SIFT> sift = SIFT::create();
vector<KeyPoint> keypoints;
Mat descriptors;
sift->detectAndCompute(gray, Mat(), keypoints, descriptors);
Mat img_keypoints;
drawKeypoints(img, keypoints, img_keypoints, Scalar::all(-1),
DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
cout << "Keypoints: " << keypoints.size() << endl;
imshow("SIFT Keypoints", img_keypoints);
waitKey(0);
return 0;
}
ORB (Oriented FAST and Rotated BRIEF)
ORB is a fast alternative to SIFT and SURF:- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
using namespace cv;
int main() {
Mat img = imread("image.jpg");
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
Ptr<ORB> orb = ORB::create(400);
vector<KeyPoint> keypoints;
Mat descriptors;
orb->detectAndCompute(gray, Mat(), keypoints, descriptors);
Mat img_keypoints;
drawKeypoints(img, keypoints, img_keypoints,
Scalar(0, 255, 0));
imshow("ORB Keypoints", img_keypoints);
waitKey(0);
return 0;
}
AKAZE
- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
using namespace cv;
int main() {
Mat img = imread("image.jpg");
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
Ptr<AKAZE> akaze = AKAZE::create();
vector<KeyPoint> keypoints;
Mat descriptors;
akaze->detectAndCompute(gray, Mat(), keypoints, descriptors);
Mat img_keypoints;
drawKeypoints(img, keypoints, img_keypoints);
imshow("AKAZE Keypoints", img_keypoints);
waitKey(0);
return 0;
}
Feature Matching
Based on OpenCV’s find_obj.py sample:Brute-Force Matcher
- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
using namespace cv;
using namespace std;
int main() {
Mat img1 = imread("box.png", IMREAD_GRAYSCALE);
Mat img2 = imread("box_in_scene.png", IMREAD_GRAYSCALE);
Ptr<ORB> orb = ORB::create(400);
vector<KeyPoint> kp1, kp2;
Mat desc1, desc2;
orb->detectAndCompute(img1, Mat(), kp1, desc1);
orb->detectAndCompute(img2, Mat(), kp2, desc2);
BFMatcher bf(NORM_HAMMING, true);
vector<DMatch> matches;
bf.match(desc1, desc2, matches);
Mat img_matches;
drawMatches(img1, kp1, img2, kp2, matches, img_matches);
imshow("Matches", img_matches);
waitKey(0);
return 0;
}
FLANN-Based Matcher
Faster for large datasets:- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
using namespace cv;
using namespace std;
int main() {
Mat img1 = imread("box.png", IMREAD_GRAYSCALE);
Mat img2 = imread("box_in_scene.png", IMREAD_GRAYSCALE);
Ptr<SIFT> sift = SIFT::create();
vector<KeyPoint> kp1, kp2;
Mat desc1, desc2;
sift->detectAndCompute(img1, Mat(), kp1, desc1);
sift->detectAndCompute(img2, Mat(), kp2, desc2);
FlannBasedMatcher flann;
vector<vector<DMatch>> knn_matches;
flann.knnMatch(desc1, desc2, knn_matches, 2);
// Ratio test
vector<DMatch> good_matches;
for(size_t i = 0; i < knn_matches.size(); i++) {
if(knn_matches[i][0].distance < 0.75 * knn_matches[i][1].distance) {
good_matches.push_back(knn_matches[i][0]);
}
}
Mat img_matches;
drawMatches(img1, kp1, img2, kp2, good_matches, img_matches);
imshow("FLANN Matches", img_matches);
waitKey(0);
return 0;
}
Finding Homography
Find the transformation between matched images:- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/calib3d.hpp>
using namespace cv;
using namespace std;
// After getting good_matches
vector<Point2f> src_pts, dst_pts;
for(size_t i = 0; i < good_matches.size(); i++) {
src_pts.push_back(kp1[good_matches[i].queryIdx].pt);
dst_pts.push_back(kp2[good_matches[i].trainIdx].pt);
}
Mat H = findHomography(src_pts, dst_pts, RANSAC, 5.0);
// Transform corners
vector<Point2f> corners(4);
corners[0] = Point2f(0, 0);
corners[1] = Point2f(img1.cols, 0);
corners[2] = Point2f(img1.cols, img1.rows);
corners[3] = Point2f(0, img1.rows);
vector<Point2f> scene_corners(4);
perspectiveTransform(corners, scene_corners, H);
// Draw box
Mat img2_color;
cvtColor(img2, img2_color, COLOR_GRAY2BGR);
line(img2_color, scene_corners[0], scene_corners[1], Scalar(0, 255, 0), 3);
line(img2_color, scene_corners[1], scene_corners[2], Scalar(0, 255, 0), 3);
line(img2_color, scene_corners[2], scene_corners[3], Scalar(0, 255, 0), 3);
line(img2_color, scene_corners[3], scene_corners[0], Scalar(0, 255, 0), 3);
Blob Detection
- Python
- C++
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)
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
using namespace cv;
int main() {
Mat img = imread("blobs.jpg", IMREAD_GRAYSCALE);
SimpleBlobDetector::Params params;
params.filterByArea = true;
params.minArea = 100;
params.filterByCircularity = true;
params.minCircularity = 0.1;
params.filterByConvexity = true;
params.minConvexity = 0.5;
params.filterByInertia = true;
params.minInertiaRatio = 0.01;
Ptr<SimpleBlobDetector> detector =
SimpleBlobDetector::create(params);
vector<KeyPoint> keypoints;
detector->detect(img, keypoints);
Mat img_with_keypoints;
drawKeypoints(img, keypoints, img_with_keypoints,
Scalar(0, 0, 255),
DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
imshow("Blobs", img_with_keypoints);
waitKey(0);
return 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
- Use features for Object Detection
- Apply to Camera Calibration
- Explore Image Stitching and Panoramas
