Welcome to Unit 2 of our four-unit course on building a panorama stitcher! In the previous unit, we learned the basics of loading and inspecting image files. Now, as our project grows, we want to keep our main stitching logic clean and easy to read.
To achieve this, we are going to extract our common helper functions into a dedicated, reusable file called **cvkit.py**. Building a dedicated computer vision toolkit allows us to easily reuse code across our entire project. In this lesson, we will build a suite of tools to safely load, label, and perfectly stack multiple images side-by-side into a single visual "workbench" panel.
As a quick reminder from our previous unit, **OpenCV** treats images as multi-dimensional **NumPy** arrays. An image has dimensions for its height (rows), its width (columns), and its color channels (like Blue, Green, and Red). We already have a function called resize_long_edge provided in our toolkit to handle resizing, so let's focus on safely reading and standardizing our image files.
First, we want a reliable way to load our images. Let's create a function called read_color in our cvkit.py file.
Here, we use cv2.imread to load the image from the given path and force it into a color format using the cv2.IMREAD_COLOR flag.
However, if we provide a bad file path, OpenCV does not automatically throw an error; instead, it simply returns None. This can cause confusing errors later in our program. Let's add a safety check to explicitly raise an error if the image fails to load.
By explicitly raising a ValueError, we ensure our program fails gracefully and tells us exactly which file is missing.
Next, as we process images in future lessons, some of our operations will output 2-dimensional grayscale images (just height and width) instead of 3-dimensional color arrays. Because we want to stack our images side-by-side in our preview panel, they must all have the same number of dimensions. Let's build an as_bgr function to standardize this.
This function checks the number of dimensions in the image array using .ndim. If it is 2 (meaning it is a grayscale image), we use OpenCV's cvtColor to convert it to a 3-channel BGR image. If it already has 3 channels, we simply return a fresh copy of the image.
When we are inspecting multiple steps of our panorama pipeline, it is incredibly helpful to have text labels directly on the images. Let's build a label_image function to accomplish this.
First, we will ensure the image is in color so that our labels can be drawn consistently.
Next, to make sure our text is readable regardless of what is happening in the picture, we will draw a black rectangle in the top-left corner as a background for our text.
In this code, we dynamically calculate the label_width so that it is wide enough to fit our text, but not wider than the image itself (output.shape[1]). Then, we use cv2.rectangle to draw a solid black box. The coordinates (0, 0) represent the top-left corner, and (label_width, 34) represent the bottom-right corner of our rectangle. The (0, 0, 0) sets the color to black, and -1 tells OpenCV to fill the rectangle completely solid.
Finally, we will stamp our text in white over the black background.
The cv2.putText function places our text starting at the coordinates (10, 23) inside our black box. We use a simple font (cv2.FONT_HERSHEY_SIMPLEX), scale the text to 0.65, set the color to pure white (255, 255, 255), and set the line thickness to 2.
To view our images side-by-side, we will use a NumPy function called np.hstack (horizontal stack). However, np.hstack has a strict rule: all image arrays provided to it must have the exact same height.
If we have one tall image and one short image, we cannot simply stretch or crop the short image, as that would destroy the data we are trying to inspect! Instead, we will build a pad_to_height function that adds a blank black space to the bottom of shorter images.
First, we check if the image even needs padding.
Here, image.shape[0] gives us the height in pixels. If it already matches the target height, we do nothing and return the image.
If the image is shorter, we need to create a new, blank black canvas that is the correct size.
We use np.zeros to create an array filled with black pixels. It takes a tuple of our desired dimensions: the target height, the original width (image.shape[1]), and 3 color channels. We also ensure it shares the exact same data type (dtype) as the original image.
Finally, we paste our original image directly onto the top of this black canvas.
Using Python array slicing, output[: image.shape[0], : image.shape[1]] selects a region in the top-left of the canvas that perfectly matches the dimensions of our original image. By assigning image to this region, the original picture sits at the top, and the extra height at the bottom remains black.
Now we can combine everything into our make_panel function. This function takes a list of items (which are pairs of text names and images) and a max_size for resizing.
We loop through our items, apply our label_image function to give them a title, and then use our provided resize_long_edge function to scale them down so they fit nicely on our screen. We store these prepared images in a list called tiles.
Lastly, we find the tallest image in our list, pad all the images to match that height, and stack them.
We use max() to find the largest height among all our tiles. Then, we apply our pad_to_height function to every tile. Now that they are all the exact same height, np.hstack(tiles) safely stitches them together into one large preview image.
Because we extracted all of this logic into cvkit.py, our main script (**solution.py**) becomes beautifully simple.
Let's set up our main script to accept a file path from the command line and import our new custom tools.
Notice that we import make_panel and read_color directly from our cvkit module. Next, we will use them to read our image file. Let's prove our toolkit works by creating a panel that displays two identical images side-by-side.
By passing a list of two items, our make_panel function will label the first one original and the second one duplicate, resize them both, and safely stack them horizontally.
Finally, we display the generated panel on the screen.
When this script runs, it opens a window called workbench. Inside, you will see your image displayed twice, side-by-side, each with a neat black-and-white label in the top-left corner. If you resize the window or pass images of different sizes in the future, the toolkit handles all the messy dimensional math for you.
In this lesson, we significantly leveled up our project structure by moving our reusable code into a dedicated module (cvkit.py). We standardized our image dimensions to safely handle BGR vs grayscale inputs, and we used NumPy array slicing to pad smaller images with black space, ensuring we never lose data or crash when stacking images side-by-side.
Now it is your turn to build out these tools! In the upcoming practice exercises, you will write cvkit.py piece by piece — from handling array standardization to writing the panel builder — until you have a fully functioning visual workbench. Let's head over to the exercises and put this toolkit together!
