← Back to curriculum

Module 2 — Geometry & correspondence

Project: panorama stitching with OpenCV

Detect ORB features, match with ratio test, estimate homography with RANSAC, warp and blend two photos into a panorama.

~180 min read + exercises

Project: panorama stitching with OpenCV

Before we begin

Stitch two overlapping photos into one panorama using ORB features, ratio-test matching, RANSAC homography, and blending.

Time: ~3 hours.


Requirements

  • Two photos of the same scene with 30–50% overlap (phone photos work).
  • pip install opencv-python numpy matplotlib

Pipeline overview

  1. Detect ORB keypoints and descriptors.
  2. Match with BFMatcher + Lowe ratio test.
  3. Estimate homography with cv2.findHomography(..., cv2.RANSAC).
  4. Warp image 2 into image 1's plane.
  5. Blend overlap (simple average or feather).

Starter code

python
import cv2
import numpy as np
 
img1 = cv2.imread("left.jpg")
img2 = cv2.imread("right.jpg")
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
 
orb = cv2.ORB_create(5000)
kp1, des1 = orb.detectAndCompute(gray1, None)
kp2, des2 = orb.detectAndCompute(gray2, None)
 
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
raw = bf.knnMatch(des1, des2, k=2)
good = [m for m, n in raw if m.distance < 0.75 * n.distance]
 
pts1 = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
pts2 = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
 
H, mask = cv2.findHomography(pts2, pts1, cv2.RANSAC, 5.0)
print("inliers:", mask.sum(), "/", len(good))
 
h1, w1 = img1.shape[:2]
h2, w2 = img2.shape[:2]
corners = np.float32([[0,0],[w2,0],[w2,h2],[0,h2]]).reshape(-1,1,2)
warped_corners = cv2.perspectiveTransform(corners, H)
all_pts = np.concatenate((np.float32([[0,0],[w1,0],[w1,h1],[0,h1]]).reshape(-1,1,2), warped_corners), axis=0)
[xmin, ymin] = np.int32(all_pts.min(axis=0).ravel() - 0.5)
[xmax, ymax] = np.int32(all_pts.max(axis=0).ravel() + 0.5)
tx, ty = -xmin, -ymin
T = np.array([[1,0,tx],[0,1,ty],[0,0,1]], dtype=np.float64)
 
panorama = cv2.warpPerspective(img2, T @ H, (xmax - xmin, ymax - ymin))
panorama[ty:ty+h1, tx:tx+w1] = img1
cv2.imwrite("panorama.jpg", panorama)

Deliverables

  1. panorama.jpg output.
  2. Visualization of matches (cv2.drawMatches).
  3. Short write-up: inlier count, what failed if alignment is poor.

Extensions

  • Multi-band blending (cv2.detail.MultiBandBlender).
  • Use SIFT if ORB struggles on low-texture scenes.
  • Auto-crop black borders.

What's next

Welcome to Module 3