Keypoints, descriptors and matching

OpenCV · Advanced · 15 min

Feature matching underlies panorama stitching, object localisation and image alignment without any training.

Keypoints are distinctive locations (corners, blobs) that can be found again in another view; a descriptor is a vector describing the patch around each one.

  • cv2.ORB_create(nfeatures): fast, free, binary descriptors compared with Hamming distance
  • cv2.SIFT_create(): float descriptors, more robust to scale and lighting, compared with L2 distance; in the main package since 4.4 after its patent expired
  • cv2.AKAZE_create(): a good middle ground

kp, des = detector.detectAndCompute(gray, None) returns both.

Match with cv2.BFMatcher(norm) (brute force) or FLANN for large sets. Use NORM_HAMMING for ORB and NORM_L2 for SIFT. Raw matches contain many mistakes; filter them with Lowe's ratio test: take the two nearest neighbours via knnMatch(des1, des2, k=2) and keep a match only if the best distance is clearly smaller (for example under 0.75x) than the second best.

Even filtered matches include outliers, so estimate geometry robustly: cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) returns the matrix and an inlier mask. Many inliers mean the object is really there; the matrix lets you draw its outline or warp one image onto the other. cv2.USAC_MAGSAC is a newer, often more accurate robust method.

Example

import cv2
import numpy as np

rng = np.random.default_rng(3)
img1 = np.zeros((240, 320), np.uint8)
for x, y, w, h, v in zip(*rng.integers([0, 0, 5, 5, 60], [300, 220, 30, 30, 255], (60, 5)).T):
    cv2.rectangle(img1, (int(x), int(y)), (int(x + w), int(y + h)), int(v), -1)
M = np.float32([[1, 0, 25], [0, 1, 15]])             # shift by (25, 15)
img2 = cv2.warpAffine(img1, M, (320, 240))

orb = cv2.ORB_create(500)
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
pairs = cv2.BFMatcher(cv2.NORM_HAMMING).knnMatch(des1, des2, k=2)
good = [m for m, n in (p for p in pairs if len(p) == 2) if m.distance < 0.75 * n.distance]

src = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
H, inliers = cv2.findHomography(src, dst, cv2.RANSAC, 3.0)
print(len(good), int(inliers.sum()))                 # e.g. 200+ matches, most inliers
print(H[:2, 2].round())                              # about [25. 15.]: the shift

Remember

  • detectAndCompute gives keypoints plus descriptors
  • ORB uses Hamming distance, SIFT uses L2
  • Lowe's ratio test removes ambiguous matches
  • findHomography with RANSAC rejects outliers and returns an inlier mask

Watch out for

  • Matching ORB's binary descriptors with NORM_L2, which gives meaningless distances.

Check yourself

  1. Why use knnMatch with k=2 and a ratio test instead of just the single best match?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.opencv.org/4.x/dc/dc3/tutorial_py_matcher.html

Builds on

Unlocks

Nothing builds on this yet.