AI Hints
Home›Computer Vision Tutorial›Geometry in Practice
Computer Vision · Lesson 57 of 111

Stitching Images

One wide view from two cameras.

⏱ 2 min read▶ Video at 1:31:14🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

The belt is too wide for a single camera, so there are two, with some overlap. To count properly, you need one wide image. That's exactly how your phone makes a panorama, and now you know enough to build one.

Stitching Images
From the video: Stitching Images
  • Two overlapping cameras
  • You need one wide image
  • That's how a panorama works

Building a panorama

We cut one long scene into two overlapping views. SIFT finds distinctive points in each. We match them, and use the ratio test, which keeps a match only if it's clearly better than the second best. Many of those matches will still be wrong. So RANSAC tries random small sets of matches, and keeps the transform that the largest number agree on. Run it. Thirty matches, and RANSAC trusted twenty one of them. And the shift that it found is exactly three hundred pixels, which is the true answer. It worked it out from the images alone.

stitch.pypython
import cv2
import numpy as np
from belt import belt

scene, _, _ = belt(n=14, size=(420, 1000), seed=2)
left, right = scene[:, :650], scene[:, 300:]

sift = cv2.SIFT_create()
ka, da = sift.detectAndCompute(left, None)
kb, db = sift.detectAndCompute(right, None)
pairs = cv2.BFMatcher().knnMatch(da, db, k=2)
matches = [m for m, n in pairs if m.distance < 0.75 * n.distance]

src = np.float32([kb[m.trainIdx].pt for m in matches])
dst = np.float32([ka[m.queryIdx].pt for m in matches])
H, inliers = cv2.findHomography(src, dst, cv2.RANSAC, 3.0)

pano = cv2.warpPerspective(right, H, (1000, 420))
pano[:, :650] = left
print(f"Matches: {len(matches)}, kept by RANSAC: {int(inliers.sum())}")
print("Shift found:", round(float(H[0, 2])), "pixels, truth 300")
cv2.imwrite("pano.png", pano)
Output
Matches: 30, kept by RANSAC: 21
Shift found: 300 pixels, truth 300
Also uses belt.py
import numpy as np
import cv2

def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
    """Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
    rng = np.random.default_rng(seed)
    h, w = size
    img = np.full((h, w, 3), (70, 75, 80), np.uint8)
    img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
    boxes, spots, taken = [], [], []
    while len(boxes) < n:
        r = int(rng.integers(32, 48))
        x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
        if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
            continue
        taken.append((x, y, r))
        red = int(rng.integers(150, 225))
        cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
        cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
        boxes.append((x - r, y - r, x + r, y + r))
    for x, y, r in taken[:bruises]:
        cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
        spots.append((x, y, r))
    img = cv2.GaussianBlur(img, (3, 3), 0)
    return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spots

RANSAC is everywhere

RANSAC deserves a moment, because it's one of the most useful ideas in all of computer vision. The idea is simple. Assume that much of your data is wrong. Pick a tiny random sample, fit a model, and count how many other points agree. Repeat, and keep the winner. It's used for stitching, robot navigation, and 3D reconstruction.

Assume bad dataoutliers are normal
Sample smallrandom sets
Count agreementwho fits?
Keep the bestignore the rest

Summary

Match, filter with RANSAC, and warp. Next, how to measure exactly what your camera's lens is doing.

Key takeaways
  • The ratio test filters matches
  • RANSAC handles wrong matches
  • It recovers the shift exactly
Next up: Calibrating a Camera — turning a camera into a measuring tool