Introduction: Merging the Pieces Together

Welcome to the first unit of our course on building a complete panorama stitcher! In our previous course, we learned how to detect features, match them, and calculate a transformation to align two overlapping photos. That process left us with two output images: a warped_source and a target_canvas.

Right now, these two images are perfectly aligned, but they sit in two separate arrays on the exact same giant coordinate grid. Imagine two transparent plastic sheets stacked on top of each other, each with a piece of the puzzle. In this lesson, we are going to learn how to blend these two sheets together into a single, combined image array. Once they are combined, we will also slice away the large, empty black borders that the warping process left behind.

Finding the Pixels: Creating Image Masks

Before we can merge our two images, we need a way to tell our computer which parts of our giant canvas actually contain a photograph, and which parts are just empty, black background. We do this by creating image "masks." A mask is simply a map of True and False values.

In digital images, a completely black pixel has a value of [0, 0, 0] for its Blue, Green, and Red channels. If any of those channels is greater than zero, we know there is image data there. Let's create our masks.

import numpy as np

def composite(warped_source, target_canvas):
    source_mask = np.any(warped_source > 0, axis=2)
    target_mask = np.any(target_canvas > 0, axis=2)

By checking > 0 along axis=2 (which represents our three color channels), np.any creates a True/False map for each image. True means "there is a picture here," and False means "this is empty black space."

Now, we need to locate specific regions. We want to know where only the source image exists, and where both images overlap.

import numpy as np

def composite(warped_source, target_canvas):
    source_mask = np.any(warped_source > 0, axis=2)
    target_mask = np.any(target_canvas > 0, axis=2)
    
    source_only = source_mask & ~target_mask
    overlap = source_mask & target_mask

Here, we use the bitwise AND operator (&) and the bitwise NOT operator (~).

  • source_only becomes True only where the source_mask is True AND the target_mask is False.
  • overlap becomes True where both masks are True.
Compositing: Blending the Canvases

Now that we have our masks, we can start pasting pixels onto a final result canvas. We will start by copying everything from our target_canvas.

import numpy as np

def composite(warped_source, target_canvas):
    source_mask = np.any(warped_source > 0, axis=2)
    target_mask = np.any(target_canvas > 0, axis=2)

    result = target_canvas.copy()

    source_only = source_mask & ~target_mask
    overlap = source_mask & target_mask

Since our result array already has the target image data, we only need to add the source image data. First, let's copy over the pixels that exist only in the source image. We can use our source_only mask to select those specific pixels.

    result[source_only] = warped_source[source_only]

Next, we must handle the overlapping region where both images exist. For our simple stitcher, we will blend them by taking a 50/50 mathematical average.

    result[overlap] = (
        0.5 * warped_source[overlap] + 0.5 * target_canvas[overlap]
    ).astype(np.uint8)

    return result

Notice that we multiplied both arrays by 0.5 and added them together. Because multiplying turns our whole numbers into decimals (floats), we use .astype(np.uint8) to convert the values back into the standard 8-bit integers that OpenCV expects for images.

If you were to test this right now, you would likely notice a visible seam or line where the two images meet. This is completely normal! It means our math is working, but real-world cameras shift their lighting and exposure slightly between shots, making one side of the overlap slightly brighter or darker than the other.

Cleaning Up: Cropping the Empty Borders
Putting It All Together in the Pipeline

Now, let's zoom out and look at how these two new functions fit into our main stitching pipeline. After we calculate the homography and warp our images, we get our two canvases.

Instead of dealing with them separately, we can now pass them directly into our new functions in our main script.

    # Previous steps left us with warped and target_canvas
    warped, target_canvas = warp_to_canvas(left, right, homography)
    
    # New blending and cropping steps
    result = crop_black(composite(warped, target_canvas))

The output of our composite function acts as the direct input for our crop_black function. The final result variable now holds our clean, tightly cropped, two-image panorama. When you run this complete pipeline, the console output will summarize our hard work:

homography direction: left/source -> right/target
matches: 142
inliers: 120
inlier ratio: 0.8450704225352113
note: a visible seam is expected with this simple average compositor
Summary and Next Steps

Great job! You now understand the final piece of the basic image stitching puzzle. In this lesson, we learned how to:

  • Use NumPy arrays to map out masks that differentiate our real photo from the empty black background.
  • Combine arrays using logical operators (&, ~) to isolate specific image regions.
  • Apply basic mathematical operations to average out overlapping areas.
  • Locate extreme coordinates and slice arrays to cleanly crop an image.

You are now ready to tackle the upcoming interactive exercises. In those practices, you will get hands-on experience building these masking and compositing functions yourself. Good luck!

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