Introduction: Tuning "Magic Numbers"

Welcome to the fourth and final unit of our course! Throughout the previous units, we have built a solid foundation. You learned how to extract features, use RANSAC to calculate a homography matrix while filtering out bad matches, and warp images into a shared coordinate frame to stitch them together.

Up until now, whenever we matched features or ran RANSAC, we used fixed numbers for our settings — like a match ratio of 0.75 or a RANSAC threshold of 5.0. In programming, we often call these hardcoded values "magic numbers."

The problem with magic numbers is that the same image pair might align perfectly or fail completely depending on these settings. In the real world, you need to adjust them. In this lesson, we will build a parameter sweeper — a tool that automatically tests different combinations of these settings to help us find the perfect balance for our alignment.

Recall: The Feature Pipeline

Before we start tuning our settings, we need to load our images and extract their features. Since we already covered these steps in previous lessons, we will use our custom helper functions to handle this quickly.

from cvkit import preprocess_for_features, read_color
from features import detect_and_compute, match_descriptors
from geometry import estimate_homography

# Read the left and right images
left = read_color("left_image.jpg")
right = read_color("right_image.jpg")

# Detect keypoints and compute descriptors for both images
kp1, des1 = detect_and_compute(preprocess_for_features(left), method="sift")
kp2, des2 = detect_and_compute(preprocess_for_features(right), method="sift")

As a quick reminder, detect_and_compute finds the interesting points (keypoints or kp) and describes what they look like (descriptors or des). We do this for both images so we can compare them. With our features ready, we can focus entirely on the tuning process.

The Two Key Parameters: Ratio and RANSAC Threshold

When we try to align two images, there are two main parameters that control our success:

  1. Ratio: This controls how strict we are when matching descriptors between the two images. A lower ratio (like 0.65) means we are very strict and only keep matches we are highly confident about. A higher ratio (like 0.85) is more relaxed and will give us more matches, but many of them might be incorrect.
  2. RANSAC Threshold: This controls how much geometric error we tolerate. It is measured in pixels. If we set it to 3.0, a matched point must land within 3 pixels of its expected location to be considered a good match (an "inlier"). If we set it to 8.0, we allow up to 8 pixels of error.

Our goal is to find settings that produce a high percentage of inliers, not just the largest total number of matches. To do this, we need to test multiple combinations of both parameters.

Building the Parameter Sweep

To test multiple combinations, we can use nested for loops in Python. This means we will place one loop inside another.

First, let's set up a table header to make our results easy to read, and write our outer loop to test different ratio values.

# Print a table header to organize our output
print(f"{'ratio':>6} {'ransac':>8} {'matches':>8} {'inliers':>8} {'inlier_ratio':>13}")

# The outer loop tests different strictness levels for matching
for ratio in [0.65, 0.70, 0.75, 0.80, 0.85]:
    # We find our matches here because matches only depend on the ratio
    matches = match_descriptors(des1, des2, ratio=ratio)

In this code, we loop through five different ratio values. Notice that we call match_descriptors inside this loop. We do this here because the matching process only relies on the ratio, not the RANSAC threshold.

Next, we add our inner loop to test different RANSAC thresholds for each ratio.

print(f"{'ratio':>6} {'ransac':>8} {'matches':>8} {'inliers':>8} {'inlier_ratio':>13}")

for ratio in [0.65, 0.70, 0.75, 0.80, 0.85]:
    matches = match_descriptors(des1, des2, ratio=ratio)

    # The inner loop tests different error tolerances for RANSAC
    for ransac_threshold in [3.0, 5.0, 8.0]:
        # We will calculate the homography and check the inliers here
        pass 

With this nested structure, for every single ratio (like 0.65), Python will test all three ransac_threshold values (3.0, 5.0, and 8.0) before moving on to the next ratio.

Handling Failures and Formatting Output

When we estimate our homography matrix, RANSAC requires a minimum number of matches to work (usually at least 4). If our ratio is too strict, we might not get enough matches, and estimate_homography will crash by throwing a ValueError.

To prevent our entire script from crashing, we use a try...except block. This allows us to catch the error, print a "fail" message, and safely move on to the next combination.

Let's complete our code by adding the homography estimation, the try...except block, and the final print statements.

print(f"{'ratio':>6} {'ransac':>8} {'matches':>8} {'inliers':>8} {'inlier_ratio':>13}")

for ratio in [0.65, 0.70, 0.75, 0.80, 0.85]:
    matches = match_descriptors(des1, des2, ratio=ratio)

    for ransac_threshold in [3.0, 5.0, 8.0]:
        try:
            # Try to calculate the homography and find the inliers
            _, inliers = estimate_homography(
                kp1,
                kp2,
                matches,
                ransac_threshold=ransac_threshold,
            )
            
            # If successful, print the results
            print(
                f"{ratio:6.2f} "
                f"{ransac_threshold:8.1f} "
                f"{len(matches):8d} "
                f"{int(inliers.sum()):8d} "
                f"{float(inliers.mean()):13.3f}"
            )
        except ValueError as exc:
            # If it fails, catch the error and print a failure message
            print(
                f"{ratio:6.2f} "
                f"{ransac_threshold:8.1f} "
                f"{len(matches):8d} "
                f"{'fail':>8} "
                f"{str(exc):>13}"
            )

In the print statement for a success, we use a few formatting tricks. The :6.2f tells Python to use 6 spaces and 2 decimal places, keeping our columns perfectly aligned.

We also calculate two important numbers from our inliers list:

  • inliers.sum(): Since inliers are represented as 1s and outliers as 0s, adding them together gives us the total number of good matches.
  • inliers.mean(): This calculates the average. In this case, it represents our inlier ratio — the percentage of total matches that actually turned out to be good.

If you run this code, the output will look something like this:

 ratio   ransac  matches  inliers  inlier_ratio
  0.65      3.0        3     fail  At least four matches are required
  0.70      5.0       45       30         0.667
  0.75      5.0       80       40         0.500
  ...
Lesson Summary and Practice Prep

Excellent work! You have successfully built a diagnostic tool that sweeps through different parameters to find the best settings for image alignment.

When you look at the results of your sweep, remember that the highest total number of matches is not always the best choice. A better alignment usually comes from a setting that produces a reasonable number of total inliers alongside a high inlier ratio (meaning very few bad matches slipped through).

You have officially made it to the final lesson of this course! I am incredibly proud of how far you have come — from struggling to understand how a computer sees images, to building a robust pipeline that can analyze, tune, and stitch images together dynamically.

Up next are the final practice exercises on the CodeSignal IDE. You will write these nested loops, handle the exceptions, and run this sweep on real image pairs to analyze the results yourself. Let's finish strong!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal