Skip to content
Dimitri Montgomery

Computer vision

Panorama stitching

Two photos of the same view, joined into one by finding the points they share and solving for the turn between them.

Shipped — Finished, or finished enough that the write-up is about what it does. Coursework, CSC 340 · Spring 2025

0.03 px
Off the true homography
25,000
RANSAC rounds
4 s
Run time
01

What it does

A panorama pipeline from CSC 340: SIFT keypoints, matches filtered by a ratio test, and a homography found by RANSAC written by hand rather than called from OpenCV. The RANSAC was later revised to score by inliers and refit on them, which took it from 10 px off the true answer to 0.03 px, closer than OpenCV's own. Blending was specified and not finished, and this page says which is which.

Finds the same points in both photos
SIFT keypoints and descriptors on each image, matched with a brute-force matcher. A match is kept only when its best candidate is clearly better than the second best, at a ratio of 0.7, which throws out most of the matches that only look alike.
Solves for the camera's turn by hand
RANSAC written out in full: 25,000 rounds, each taking four random matches, normalising them, and taking the null vector of the 8 by 9 system by SVD as a candidate homography. The candidate that the most matches agree with wins and is refitted on all of them, then printed beside OpenCV's findHomography on the same matches.
Maps the second photo into the first
Every pixel of the panorama outside the first photo is looked up in the second through the inverse homography, so the warped image has no holes. Five stitched results are in the final report.
02

Demo

Stitch it

Two photos of one scene, the second taken with the camera turned. Step through corners, matches, 25,000 rounds of RANSAC and the stitch, with the first version of the RANSAC beside the revised one. Then mix in bad matches and see which holds.

  • Match the revised fit agrees with
  • Match it rejects
  • Where each fit puts photo A: solid revised, dashed first version

Drawn, not photographed. The pipeline runs live on these two images.

Both versions, on the same 25,000 samples
First version least total distance over every matchRevised most matches within 3 px, then refit on them
Matches it agrees with––
Off from the true overlap––
Found in round––
First version least total distance over every match

Revised most matches within 3 px, then refit on them

The seams are left hard: blending them away was specified and not finished. Two substitutions, because a browser has no OpenCV: corners come from a Harris detector and are described by normalised 8 × 8 patches instead of SIFT, and each system is solved with the last entry fixed at 1 instead of by SVD. The ratio test, the round count and both scoring rules are the repo's.

03

How it works

The revision

The first version scored each round by adding up the distance between where every match lands and where it should, outliers included, and kept the smallest total. Under a total, a single wild match can outweigh a dozen good ones, so the winning round was the one that compromised with the bad matches rather than ignoring them, and a ratio test always lets some through.

The revision, on 18 October 2026, changed three things:

  • Score by agreement. A round counts the matches that land within 3 px, and the round that explains the most wins.
  • Refit on every inlier. The winner is solved again from all of its inliers at once, rather than kept as the best four-point guess.
  • Normalise first. Points are centred and scaled before the system is built, which keeps the SVD well conditioned when coordinates run into the hundreds.

On a drawn pair with a known answer and 417 real SIFT matches, the first version landed 10.06 px from the truth and the revision lands 0.03 px from it; OpenCV’s findHomography lands 0.05 px from it. Scoring all matches at once with NumPy also took a run from 60 seconds to 4. The demo above runs both versions side by side on the same samples.

What is not finished

The repository is named for multi-band blending, and the blending is not there. blending_assignment.py is the course’s starter file with every step still marked ..., and the handout beside it describes the method: split each image into three frequency bands with Gaussian blurs, blend the fine detail over a narrow seam and the broad colour over a wide one, and add the bands back together. That is why the stitches in the demo keep a hard seam where the two exposures meet. The warping code behind the report’s five panoramas is also not in the repository.

04

Testing

3 tests, under 2 s

The tests draw their own image pair, cut the second view through a homography they choose, and check that the RANSAC finds it again, so the answer is known exactly and no image files are needed. The script also prints OpenCV's homography beside its own on every run.

What the tests defend

  1. A third of the matches replaced with random pairings must not move the result more than half a pixel. That is the case the first version failed: under a total, those bad matches dragged the fit off.
  2. Four exact points must give back the exact homography, which pins the 8 by 9 system itself and the normalisation around it.
05

Built with

Language
PythonNumPy
Vision
OpenCVSIFTRANSAChomography
Testing
pytestdrawn pairs with a known answer