In the article, I will go over some basic shape and contour detection using Python3 and OpenCV.

The source code for this article can be found here.

Originally post on Lachlan Miller’s blog.

Goal

The goal is to detect the largest body of water and calculate the radius and approximate area, given screen capture from Google Maps. The article introduces some code that, given a screen capture, detects the largest body of water and draws a line around the edge:

Setup

I will use Python3 and OpenCV3. There are many ways to install Python3, I used conda. I installed OpenCV using homebrew by running brew install opencv .

To check if opencv was installed, create a detector.py script and add the following:

And run with python3 detector.py . My output is:

Prior to the above output, I got an error regarding numpy. The fix was to reinstalling numpy using homebrew.

Basic Thresholding using inRange

The next step is to apply a threshold, and get rid of the data we are not interested in. Since we are using Google Maps, lakes are always the same shade of blue, which makes things simple.

Lakes have an RGB color of [170, 218, 255] . OpenCV uses a different ordering, BGR.

We will use cv.inRange function, which takes three arguments: an image, a lower color range, and an upper color range. The documentation is here. Based on trial and error, I found +-10 for the ranges work well.

Update the script:

Now add two functions: read_image , to get the image we will be operating on, and find_mask , which applies the thresholding with inRange .

Before displaying the thresholded image, it’s good to understand what cv.imread returns. Add the following code:

I saved my screen capture as “pond.png”. Running the above code with python3 detector.py prints the following:

596 is the height of the image, or the number of rows. Each row in an array containing 697 values, where each value is a 1x3 matrix contains [B, G, R] values. So an image is just a collection of BGR pixels.

inRange is similar, however instead of each pixel being mapped to a BGR value, is it simply assigned a value of 0 or 1 - whether or not it is between the threshold.

Try rendering the mask with this code:

Running the script shows the mask:

The output in the terminal confirms inRange returns an array of 0 or 1 for each pixel:

Finding Contours with findContours

OpenCV has a findContours function which can find edges in a binary image. We have a binary image - that's why we created the mask. Read about findContours in the documentation here. The arguments are:

image : the binary image to use. findContours modifies the image, so we should pass in a copy

: the binary image to use. modifies the image, so we should pass in a copy mode : the contour retrieval mode. The modes are described in the documentation. We are focusing on the largest area, so the best fit for this problem is CV_RETR_EXTERNAL

: the contour retrieval mode. The modes are described in the documentation. We are focusing on the largest area, so the best fit for this problem is method : the contour approximation method. Again, described in the documentation. I don't really understand which is the best fit for this problem, so I just used CHAIN_APPROX_SIMPLE since this is a simple problem and that method has simple in the name. ¯\_(ツ)_/¯

Now we know about findContours , we can write the following function:

findContours returns three values. The first appears to be the image modified by findContours , which we don't really need. The second is the contours that were found. The last is the hierarchy, which contains information about the image topology. I don't fully understand what this can be used for yet. We only want the second value, cnts .

Running the find_contours function and passing in the mask from earlier prints Found 93 black shapes . This is counting all the small bodies of water, or other blue pixels, in the image. Not ideal for now. We will fix this soon.

Drawing Contours using drawContours

Let’s go ahead and create a show_contours function to visualize the 93 contours, using OpenCV's drawContours function, described here. The arguments are:

image: the image to draw on

contours: an array of contour to draw. A contour is an array of points

contour_index: the index of the contour to draw. For now we will pass -1, which draws all the contours

color: the color to draw the contours. I will use red: 0, 0, 255

thickness: the thickness of the contours drawn. I found 2 was a good number

Now we know the parameters, we can implement show_contours :

Using this with find_contours gives us the following:

Extracting the Largest Body of Water

We have 93 contours, as shown above in the image. We only want the largest one, which is the one with the most points. Add a main_contour function:

We simply sort the contours by length and return the longest one. Bringing it all together:

And, it works:

Great!

Conclusion

This article described:

how to use inRange to threshold and make a mask

to threshold and make a mask finding contours using findContours and the arguments it takes

and the arguments it takes showing the contours with drawContours

This was my first time doing image recognition in a long time. My previous experience was using OpenCV with C++, and I am impressed at how much easier and more approachable it has become with the Python bindings. I learned a lot reading Py Image Search, and it is a great resources web developers looking to try out Python and image recognition.

The source code for this article can be found here.