Shape Detectors


In this example, we would make a shape detector using OpenCV and Python. Following as you will see, we will use the detect.py main python script that we’ll use to load an image, analyze it for shapes, and then perform shape detection and identification via the ShapeDetector class from the shapedetector.py script.

Given below is the shapedetector.py script. Store this script in a folder named modules inside your project directory.

modules/shapedetector.py

In [ ]:
# import the necessary packages
import cv2

class ShapeDetector:
    def __init__(self):
        pass

    def detect(self, c):
        # initialize the shape name and approximate the contour
        shape = "unidentified"
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.04 * peri, True)

        # if the shape is a triangle, it will have 3 vertices
        if len(approx) == 3:
            shape = "triangle"

        # if the shape has 4 vertices, it is either a square or a rectangle
        elif len(approx) == 4:
            # compute the bounding box of the contour and use the bounding box to compute the aspect ratio
            (x, y, w, h) = cv2.boundingRect(approx)
            ar = w / float(h)

            # a square will have an aspect ratio that is approximately equal to one, otherwise, the shape is a rectangle
            shape = "square" if ar >= 0.95 and ar <= 1.05 else "rectangle"

        # if the shape is a pentagon, it will have 5 vertices
        elif len(approx) == 5:
            shape = "pentagon"

        # otherwise, we assume the shape is a circle
        else:
            shape = "circle"

        # return the name of the shape
        return shape

To perform shape detection, we’ll be using contour approximation. The ShapeDetector class inside this script has a detect method that takes one argument 'c' , the contour (i.e., outline) of the shape we are trying to identify.

Contour approximation can be implemented by using the predefined OpenCV method cv2.approxPolyDP(). Common values for the second parameter to cv2.approxPolyDP are normally in the range of 1-5% of the original contour perimeter.

Here in the code, we first compute the perimeter of the contour, followed by constructing the actual contour approximation. And thereafter, we can move on to performing shape detection.

A contour consists of a list of vertices. We can check the number of entries in this list to determine the shape of an object. For example, if the approximated contour has three vertices, then it must be a triangle. If a contour has four vertices, then it must be either a square or a rectangle. To determine which, we compute the aspect ratio of the shape, which is simply the width of the contour bounding box divided by the height. If the aspect ratio is ~1.0, then we are examining a square (since all sides have approximately equal length). Otherwise, the shape is a rectangle. Labelling a shape as a pentagon is straightforward, we just have to detect it has 5 sides. Ultimately, by process of elimination, we can assume that the analyzed shape is a circle.

Finally, we return the detected shape to the calling method.


We would use the shapedetector script as a module and import it. Given below is the code of our main script detect() that would use the ShapeDetector class from our shapedetector script.

detect()

In [ ]:
# import the necessary packages
from modules.shapedetector import ShapeDetector
import argparse
import imutils
import numpy as np
import cv2

# load the image and resize it to a smaller factor so that the shapes can be approximated better
image = cv2.imread("images/shapes_and_colors.png")
resized = imutils.resize(image, width=300)
ratio = (image.shape[0] / float(resized.shape[0]))

# convert the resized image to grayscale, blur it slightly, and threshold it
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.threshold(blurred, 60, 255, cv2.THRESH_BINARY)[1]

# find contours in the thresholded image and initialize the shape detector
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
sd = ShapeDetector()

# loop over the contours
for c in cnts:
    # compute the center of the contour, then detect the name of the ashape using only the contour
    M = cv2.moments(c)
    cX = int((M["m10"] / M["m00"]) * ratio)
    cY = int((M["m01"] / M["m00"]) * ratio)
    shape = sd.detect(c)

    # multiply the contour (x, y)-coordinates by the resize ratio,
    # then draw the contours and the name of the shape on the image
    c *= np.int32(ratio)
    cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
    cv2.putText(image, shape, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,
        0.5, (255, 255, 255), 2)

    # show the output image
    cv2.imshow("Image", image)
    cv2.waitKey(0)

Here we first load our image and then resize it. We keep track of the ratio of the old height to the new resized height. From there we perform basic image processing: converting the resized image to grayscale, smoothing it to reduce high frequency noise, and finally thresholding it to reveal the shapes in the image.

Lastly, we find contours in our threshold image, handle grabbing the correct tuple value from cv2.findContours(), and finally initialize our ShapeDetector: We start looping over each of the individual contours. For each of them, we compute the center of the contour, followed by performing shape detection and labeling.

Since we are processing the contours extracted from the resized image (rather than the original image), we need to multiply the contours and center (x, y)-coordinates by our resize ratio. This will give us the correct (x, y)-coordinates for both the contours and centroid of the original image.

Lastly, we draw the contours and the labeled shape on our image, followed by displaying our results.

I used the following image for testing the code. Feel free to make your own images with shapes to try and test it.

The resulting output with the threshold image and the final image with all the shapes detected looked like: