Introduction: Building the Master Function

Welcome to the second unit of our course! In our previous unit, we learned how to composite and crop aligned images to create a clean, merged panorama. Now, we are going to build the engine that powers our entire panorama maker.

Our goal is to take all the separate tools we have built or learned about so far — such as finding keypoints, matching features, and estimating geometry — and combine them into a single, reliable function called stitch_pair. This master function will take two raw images, attempt to stitch them, handle any errors gracefully, and return both the final image and a detailed diagnostic report.

Quick Recall: The Image Stitching Pipeline

Before we start coding, let us briefly recall the sequence of operations required to stitch two images together:

  1. Preprocessing: Convert images to grayscale so algorithms can process them easily.
  2. Detection and Computation: Find unique points (keypoints) in both images and describe their surrounding patterns.
  3. Matching: Pair up matching descriptions between the left and right images.
  4. Homography Estimation: Use the matched points to figure out the math (a geometric grid) needed to warp one image so that it aligns with the other.
  5. Warping and Compositing: Stretch the image using our math, overlap the two images, blend them, and crop out any unwanted black borders (as we practiced in the last unit).

We will rely on pre-built helper functions for the heavy math and geometry. This allows us to focus entirely on the integration logic: making sure the pipeline runs smoothly from start to finish.

Step 1: Setting Up the Tracking Report

Let us begin writing our stitch_pair function. Whenever you build a complex pipeline, it is very helpful to keep track of what is happening under the hood. We will do this by creating a report dictionary.

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

def stitch_pair(left, right, method="sift", ratio=0.75, ransac_threshold=5.0):
    report = {
        "status": "failed",
        "homography_direction": "left/source -> right/target",
        "method": method,
        "ratio": ratio,
        "ransac_threshold": ransac_threshold,
        "keypoints_left": 0,
        "keypoints_right": 0,
        "matches": 0,
        "inliers": 0,
        "inlier_ratio": 0.0,
        "diagnosis": "",
    }
    
    return None, report

In this initial code, we define our function to accept two images (left and right) and a few settings. Inside, we create the report dictionary.

Notice that we set the initial "status" to "failed". This is a defensive programming practice: we assume the process will fail until it successfully completes every step. The report also holds slots to track how many keypoints we find, how many matches we make, and a space for a "diagnosis" message to help us understand what went wrong if it fails.

Step 2: Executing the Pipeline Safely

Now, let us start processing the images. We will run our preprocessing and feature detection functions and immediately update our report with the results.

    # Preprocess images
    left_gray = preprocess_for_features(left)
    right_gray = preprocess_for_features(right)

    # Detect features and compute descriptors
    kp1, des1 = detect_and_compute(left_gray, method=method)
    kp2, des2 = detect_and_compute(right_gray, method=method)

    # Match descriptors between the two images
    matches = match_descriptors(des1, des2, ratio=ratio)

    # Update our tracking report
    report["keypoints_left"] = len(kp1)
    report["keypoints_right"] = len(kp2)
    report["matches"] = len(matches)

At this point, we have matched features. However, calculating a homography (the mathematical alignment grid) requires at least four matched points. If we do not have four points, the math will crash. Let us add a safety check to stop the process early if necessary:

    if len(matches) < 4:
        report["diagnosis"] = "fewer than four matches; homography cannot be estimated"
        return None, report

If we have enough matches, we move on to estimating the homography. Because complex geometry can sometimes fail even with enough points, we wrap this in a try-except block to catch any mathematical errors.

    try:
        homography, inliers = estimate_homography(
            kp1,
            kp2,
            matches,
            ransac_threshold=ransac_threshold,
        )
    except ValueError as exc:
        report["diagnosis"] = f"homography failed: {exc}"
        return None, report

    # Update inlier statistics
    report["inliers"] = int(inliers.sum())
    report["inlier_ratio"] = float(inliers.mean()) if len(inliers) else 0.0

    if report["inliers"] < 4:
        report["diagnosis"] = "fewer than four inliers; alignment is not reliable"
        return None, report

Here, we try to calculate the homography. The estimate_homography function also returns inliers (the matches that are geometrically consistent within the RANSAC reprojection threshold, discarding bad matches). This ransac_threshold controls how much alignment error is tolerated: a smaller value requires more precise consistency, while a larger one is more lenient. We calculate the total number of inliers and check if we have fewer than four. If we do, we update the diagnosis and safely return None instead of a broken image.

Step 3: Finishing the Stitch

If our code survives all the safety checks, we have a valid homography! Now we can warp the images and merge them using the composite and crop_black logic we learned in the previous unit.

Before we finish the function, let us define a quick helper function to give us a friendly diagnosis message for successful (or mostly successful) stitches.

def diagnose_stitch(report):
    if report["matches"] < 20:
        return "too few descriptor matches; choose images with more overlap or texture"
    if report["inliers"] < 10:
        return "too few geometric inliers; inspect the inlier view"
    if report["inlier_ratio"] < 0.25:
        return "low inlier ratio; try a stricter ratio threshold or a better image pair"
    return "alignment probably usable; inspect the seam, exposure differences, and parallax"

This helper looks at the numbers in our report and provides advice. Now, we add the final lines to our stitch_pair function:

    # Warp and composite
    warped, target_canvas = warp_to_canvas(left, right, homography)
    
    # We assume 'composite' and 'crop_black' are defined based on our previous unit
    result = crop_black(composite(warped, target_canvas))

    # Mark as success!
    report["status"] = "ok"
    report["diagnosis"] = diagnose_stitch(report)

    return result, report

If we reach the bottom of the function, everything has worked. We change the "status" to "ok", get our final diagnostic advice, and return the result image alongside our fully populated report.

Step 4: Running the Code via the Command Line

To make our new stitch_pair function useful, we need a way to run it from our computer's terminal. We can do this by setting up a main execution script that takes inputs from the user.

First, we set up argparse to read the image filenames and settings the user types into the command line.

import argparse
import cv2
from cvkit import read_color

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("left")
    parser.add_argument("right")
    parser.add_argument("--method", choices=["sift", "orb", "akaze"], default="sift")
    parser.add_argument("--ratio", type=float, default=0.75)
    parser.add_argument("--ransac-threshold", type=float, default=5.0)
    parser.add_argument("--out", default="stitched_pair.jpg")
    args = parser.parse_args()

    left = read_color(args.left)
    right = read_color(args.right)

Next, we pass those read images directly into our stitch_pair function.

    result, report = stitch_pair(
        left,
        right,
        method=args.method,
        ratio=args.ratio,
        ransac_threshold=args.ransac_threshold,
    )

    # Print the report to the console
    for key, value in report.items():
        print(f"{key}: {value}")

If you ran this in your terminal, the printed report output would look something like this:

status: ok
homography_direction: left/source -> right/target
method: sift
ratio: 0.75
ransac_threshold: 5.0
keypoints_left: 1250
keypoints_right: 1100
matches: 150
inliers: 112
inlier_ratio: 0.746
diagnosis: alignment probably usable; inspect the seam, exposure differences, and parallax

Finally, we handle the result. If the function returned None, we stop the program. If it succeeded, we save the resulting image to a file and display it on the screen.

    if result is None:
        raise SystemExit("stitching failed; see diagnosis above")

    # Save and show the successful stitch
    cv2.imwrite(args.out, result)
    cv2.imshow("stitched pair", result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
Summary and Next Steps

Great job! In this lesson, we successfully wrapped our image stitching pipeline into a single, clean function. By adding step-by-step checks, a tracking report, and safe error handling, we transformed a loose collection of scripts into a robust tool. If an image pair lacks enough matching points to stitch safely, our program no longer crashes — it simply explains exactly why it could not complete the job.

Now, it is your turn to put this into action. In the upcoming practice exercises, you will head into the CodeSignal IDE to write this master function yourself, apply the error-handling logic, and test it against real image pairs.

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