The world’s leading publication for data science, AI, and ML professionals.

Image Contouring with OpenCV

First steps for beginners

Quick Success Data Science

To "contour an image" means to generate boundaries that enclose continuous regions with similar color or intensity. In computer science, contours are typically represented by lists of points that define these boundaries.

Contours are widely used in computer vision applications including object detection; image segmentation; and the calculation of shape descriptors such as area and centroid.

In this Quick Success Data Science project, we’ll use the world’s most popular computer vision library, OpenCV, to generate contours on an image of the moon. Our goal will be to create some fun art, but the same general process is used for more sophisticated applications.


Installing Libraries

For this project, we’ll use the NumPy, Matplotlib, and OpenCV libraries. OpenCV relies heavily on NumPy arrays, and we can use Matplotlib as a quick and simple way to visualize the contoured images (though OpenCV does include methods for directly visualizing images).

You can find installation instructions for NumPy and Matplotlib in the previous links and for OpenCV here.

As an Anaconda user, I’ve always had a difficult time installing OpenCV using conda. It seems to install correctly, but I invariably encounter an error when I try to import the library into a script. To avoid this, I create a new conda environment, use conda to install everything I need except for OpenCV, and then, as the final step, use pip to install OpenCV:

pip install opencv-python

NOTE: If you’re curious about mixing conda with pip, please see the Addendum at the end of my article, Introducing Conda Environments.


The Moon Image

Here’s the moon image we’ll use. It’s a public image from Unsplash that I’ve rotated slightly. I’ve also replaced the black background with white using Microsoft’s Paint 3D photo editor.

Moon image by Mike Petrucci (Unsplash).
Moon image by Mike Petrucci (Unsplash).

To use the image, right-click on it and choose Save image as. Name it "moon.jpg" and save it in the folder containing your notebook or Python script, so we don’t have to deal with path names.


The Code

The following code was written in a single cell in JupyterLab. It loads an image, converts it to grayscale, smooths it, generates contours, and then draws the contours over a transparent version of the original image.

Importing Libraries and Processing the Image

Before detecting contours, it’s generally a good idea to do a bit of preprocessing to enhance features and reduce noise. The most common steps are to convert color images to grayscale and to smooth the resulting image with filters like _Gaussian blur_. Fortunately, OpenCV comes with methods for performing these tasks.

Here’s the preprocessing code. A detailed explanation follows.

import numpy as np
import matplotlib.pyplot as plt
import cv2 as cv

# Use these constants to tune your results:
SMOOTHING_KERNEL = (11, 11)  # Must be odd numbers.
NUM_CONTOURS = 8
ALPHA = 0.6  

# Load the moon image:
IMG_GRAY = cv.imread('moon.jpg', cv.IMREAD_GRAYSCALE)

# Smooth the image using Gaussian blur:
smoothed_img = cv.GaussianBlur(IMG_GRAY, 
                               SMOOTHING_KERNEL, 
                               sigmaX=0)

After importing the libraries, we assign three constants for "tuning" the contours and the final image. The first, SMOOTHING_KERNEL, represents an input to OpenCV’s GaussianBlur() class.

Blurring reduces the sharpness of an image. This process uses a filter comprised of a square matrix called a kernel. This kernel progressively slides across the image and, for all the pixels within it, calculates a weighted average of their intensity values. It then replaces the value of the center pixel in the matrix with this averaged value. This removes extreme values and creates a "smoother" image.

The kernel argument is a tuple of width and height, in pixels. Since the average value is placed in the center pixel in the kernel, the kernel dimensions should always be positive and odd. So, (5, 5) works, but (2, 2) doesn’t. (To read more about image smoothing visit this OpenCV site).

The next constant, NUM_CONTOURS, represents the number of levels argument for the OpenCV contour() method, which we’ll discuss in a moment.

The levels parameter specifies the intensity levels at which to define contours. If you input a single value, such as 8, OpenCV will automatically divide the image’s intensity range into 8 intervals.

You can also specify the intervals manually. Instead of using a constant value, as we do here, you could pass the values as an argument. For example, levels=[50, 100, 150] will cause contours to be drawn at intensity levels 50, 100, and 150.

The final constant, ALPHA, determines the transparency of the initial image over which the contours will be drawn. The lower the value, the more transparent the image.

As mentioned earlier, you can use these three constants to "fine-tune" your results if they appear too noisy or too sparse.

Next, we load the image in grayscale using OpenCV’s imread() method and the IMREAD_GRAYSCALE argument. We then smooth it using the GuassianBlur() class, passing it the image, the kernel size, and a standard deviation value (sigmaX). Using 0 for this last argument tells OpenCV to automatically estimate the value.

Plotting the Image

Now we’re ready to generate the contoured image and plot it using Matplotlib.

# Create a 2D Matplotlib figure:
fig, ax = plt.subplots()

# Set the contour levels:
contour_levels = np.linspace(0, 255, NUM_CONTOURS)

# Create the contour plot:
contour_plot = ax.contour(smoothed_img, 
                          levels=contour_levels, 
                          colors='black')

# Add the smoothed image:
ax.imshow(smoothed_img, 
          cmap='gray', 
          alpha=ALPHA)

# Customize the plot settings:
ax.set_title('Smoothed Moon Image with Contours')
ax.set_axis_off()

# fig.savefig('moon_contour_medium.png', dpi=500)
plt.show()

First, we create Matplotlib figure and axes objects to hold the plot. Next, we determine the threshold values to use for each contour. To do this we call NumPy’s linspace() method, which returns a NumPy array with the maximum possible intensity range (0–255) subdivided into 8 values, as specified by the NUM_CONTOURS constant.

We then call the OpenCV contour() method on the ax object, passing it the smoothed image, the contour_levels array, and a line color. At this point, we just have a white display with black contours. To give it some life, we call the Matplotlib imshow() method on ax, pass it the smoothed image, a Matplotlib colormap (cmap='gray'), and an alpha argument to prevent it from overwhelming the contours.

Finally, we add a title and turn off the x and y axes. Here’s the result:

The contoured moon image in gray (by the author)
The contoured moon image in gray (by the author)

Increasing the kernel size will smooth the image more and produce a simpler contour pattern. Here’s the result of using a kernel of 23 x 23 pixels (SMOOTHING_KERNEL = (23, 23)) and ALPHA=0.7:

The contoured moon image smoothed with a 23x23 kernel (by the author)
The contoured moon image smoothed with a 23×23 kernel (by the author)

Fun with Color Maps

Our "moon sketch" reminds me of the illustrations in the book, Star Wars: The Essential Guide to Planets and Moons. Indeed, by changing the underlying color map, you can easily change the moon into an interesting sci-fi or fantasy world. Here’s an example where we change the cmap parameter in the imshow() method from gray to terrain.

The contoured moon with the terrain color map and a black background (by the author)
The contoured moon with the terrain color map and a black background (by the author)

The dark gray areas on the moon are called maria (Latin for "seas") because the ancients thought they resembled water-filled basins. By changing a single argument in the code, we can imagine what the moon would look like if the maria really were seas, and the moon was a living world similar to Earth.

By using the reversed Reds color map, we can create a desert world like Arrakis or Tatooine:

The contoured moon plotted with the Reds_r color map and a black background (by the author)
The contoured moon plotted with the Reds_r color map and a black background (by the author)

Matplotlib comes with dozens of interesting color maps. You can find them all at this site.

Speaking of desert planets, you can use Contouring on Mars. Here’s a view of the famous Mariner Valley based on a NASA image in the [public domain](http://JPL Image Use Policy (nasa.gov)).

Contoured Mars image plotted with the RdGy color map (by the author)
Contoured Mars image plotted with the RdGy color map (by the author)

Of course, any image can be contoured (though not always well). Do you recognize this fellow:

Contoured George Washington (by the author)
Contoured George Washington (by the author)

Limitations

Contouring algorithms work best with high-quality images where the edges of objects are clearly defined. They tend to fail or return unsatisfactory results when backgrounds are busy and when foreground and background objects – or overlapping objects – have similar values.

Preprocessing techniques, like grayscale conversion and blurring, can improve results, but they can only go so far. Fortunately, OpenCV and other Python libraries provide additional workflows for dealing with problems like low-contrast images. While these are beyond the scope of this article, you can view an example workflow here.

OpenCV also contains functionality for post-processing contoured images. This includes methods for extracting and smoothing contours as well as for removing contours that enclose small areas.


Summary

In this article, we looked at a simple and quick way to contour an image using Python’s most popular computer vision library, OpenCV. These contours represented boundaries between regions of varying intensity.

To place these contours, we chose thresholds, called "contour levels," by specifying the number of contours we wanted in the final image and letting an algorithm choose the intensity values. Alternatively, we could’ve supplied a list of specific intensity values where we wanted to place contours.

Image contouring is a hugely important process in computer vision, and we’ve barely scratched the surface. We used contours here to "sketch" the Moon and Mars and to create visions of imaginary worlds. But applications go much, much further, from identifying tumors to recognizing faces to enabling self-driving cars.


Thanks!

Thanks for reading and please follow me for more Quick Success Data Science projects in the future.


Related Articles