Introduction and Quick Recall

Welcome back! In the previous unit, we explored Harris corners and used them as a visual diagnostic for image texture. While that was a great starting point, real-world panoramas require algorithms that are stronger and more reliable when handling scale, rotation, and complex textured scenes.

In this lesson, we will upgrade our toolkit. We are going to build a clean, reusable feature detection module named features.py. By storing our detection logic in a single reusable file, we will not have to rewrite it every time we want to inspect or match image features. Think of it as organizing tools into a toolbox: whenever you need a specific detector, it is ready and waiting.

As a quick reminder, in our CodeSignal environment, powerful libraries like OpenCV (cv2) are already pre-installed for you. You will not need to worry about setting up your environment or installing libraries — we can dive straight into writing code!

Comparing SIFT, ORB, and AKAZE

Each feature method has unique strengths and produces a different type of descriptor, which is the numeric "fingerprint" around a keypoint.

AlgorithmFull NamePrimary StrengthDescriptor Type
SIFTScale-Invariant Feature TransformHighly accurate and robust to scale/rotation.128-dimensional floating point vectors.
ORBOriented FAST and Rotated BRIEFExtremely fast and efficient; ideal for real-time apps.32-byte binary strings (integers).
AKAZEAccelerated-KAZEA balanced choice that preserves image edges.Binary strings (integers).

Here is how to interpret the table:

  • SIFT is the heavy lifter. It is usually the strongest default for panorama-style matching because it handles scale and rotation well.
  • ORB is the speedster. It uses compact binary descriptors, so it is fast and memory efficient.
  • AKAZE is a modern middle ground. It often performs well on textured scenes with strong edges.

Separating this comparison from the code keeps the lesson focused: first we choose the tool conceptually, then we implement a factory that creates it.

Building the Detector Factory

Our first reusable function is a detector factory. It returns the appropriate OpenCV detector based on the method argument.

import cv2

def create_detector(method="sift", nfeatures=2000):
    if method == "sift":
        if not hasattr(cv2, "SIFT_create"):
            raise ValueError("SIFT is not available in this OpenCV build")
        return cv2.SIFT_create(nfeatures=nfeatures)

    if method == "orb":
        return cv2.ORB_create(nfeatures=nfeatures)

    if method == "akaze":
        return cv2.AKAZE_create()

    raise ValueError(f"Unknown feature method: {method}")

The SIFT branch includes an availability check because older OpenCV builds sometimes placed SIFT in an extra module or did not include it at all. Modern OpenCV builds usually include it, but the explicit check gives learners a clear error message if it is missing.

We do not add the same availability check for ORB and AKAZE here because they are standard OpenCV feature detectors in the environment used for this course. If a very unusual OpenCV build were missing one of them, the detector creation call would fail immediately. For our course pipeline, the SIFT check is the most helpful one because it is the detector with the most common version-related history.

Notice that AKAZE_create() does not receive nfeatures. OpenCV's AKAZE constructor does not use the same simple maximum-feature argument that SIFT and ORB do, so we call it without that option.

Detecting Keypoints and Computing Descriptors

Now that we can create a detector, we need one clean entry point for feature extraction. This helper accepts a grayscale image, creates the requested detector, and returns keypoints plus descriptors.

def detect_and_compute(gray, method="sift", nfeatures=2000):
    detector = create_detector(method=method, nfeatures=nfeatures)
    keypoints, descriptors = detector.detectAndCompute(gray, None)
    return keypoints, descriptors

The returned values are:

  • Keypoints: locations, sizes, and orientations of interesting image points.
  • Descriptors: numeric fingerprints describing the local appearance around each keypoint.

We pass None as the second argument because we are not masking the image. The detector is allowed to search everywhere.

Integrating and Visualizing in the Main Script

With our features.py module ready, let's switch to our main program, solution.py, to test it.

First, we will set up the script to accept command-line inputs using argparse. We will also import our new detect_and_compute function alongside the helpful image reading tools we built in previous lessons (located in our cvkit module).

import argparse
import cv2

from cvkit import make_panel, preprocess_for_features, read_color
from features import detect_and_compute

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("path")
    parser.add_argument("--method", choices=["sift", "orb", "akaze"], default="sift")
    parser.add_argument("--nfeatures", type=int, default=2000)
    parser.add_argument("--preview-size", type=int, default=900)
    args = parser.parse_args()

Next, let's load our image, convert it to grayscale using our pre-built preprocessing tool, and pass it to our new feature detector.

    image = read_color(args.path)
    gray = preprocess_for_features(image)

    keypoints, descriptors = detect_and_compute(
        gray,
        method=args.method,
        nfeatures=args.nfeatures,
    )

To visualize what the computer found, we can use OpenCV's drawKeypoints function. We will use a special flag called DRAW_RICH_KEYPOINTS. This draws circles around the features to indicate their size and orientation.

    output = cv2.drawKeypoints(
        image,
        keypoints,
        None,
        flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS,
    )

Printing the shape and data type of our descriptors is beneficial for grounding these concepts in real data. This helps us understand what will be passed to our matching algorithms later. Let's print the details and then display the image on the screen.

    print("method:", args.method)
    print("keypoints:", len(keypoints))
    print("descriptor shape:", None if descriptors is None else descriptors.shape)
    print("descriptor dtype:", None if descriptors is None else descriptors.dtype)

    cv2.imshow(
        "keypoints",
        make_panel(
            [
                ("input", gray),
                ("keypoints", output),
            ],
            max_size=args.preview_size,
        ),
    )
    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

When you run this code on an image, the terminal output will look similar to this:

method: sift
keypoints: 2000
descriptor shape: (2000, 128)
descriptor dtype: float32

This tells you that SIFT found the maximum number of requested keypoints, produced one descriptor row per keypoint, and used 128 floating-point values per descriptor.

Try comparing methods from the terminal:

python solution.py sample_image.jpg --method sift
python solution.py sample_image.jpg --method orb
python solution.py sample_image.jpg --method akaze

Look for differences in keypoint count, descriptor shape, descriptor type, and the visual density of keypoints in the preview.

Summary and Next Steps

Great job! You have successfully built a standardized, reusable module for extracting features from images using powerful algorithms like SIFT, ORB, and AKAZE. By cleanly separating this logic into features.py, we have made our main script much easier to read and maintain.

This step perfectly prepares us for the next phase of our image stitching journey. Now that we have reliable feature fingerprints, we can learn how to match these fingerprints between two separate images to determine how they overlap.

You are now ready to proceed to the interactive practices! These exercises will provide hands-on experience in writing and testing these detection functions yourself. Good luck, and have fun!

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