OpenCV: A Simple Approach to Counting Cars

KISS – Keep it simple s…

In this tutorial we will make a simple car counter using OpenCV from Python. It will not be a perfect solution, but it will be easy to understand and in some cases better.

The counter will take advantage of the simple assumptions that objects that move through a defined box on the right side of road are cars driving in one direction. And objects moving through a defined box of the left side of the road are cars driving the other direction.

This is of course not a perfect assumption, but it makes things easier. There is no need to identify if it is car or not. This is actually an advantage, since by the default car cascade classifiers might not recognize cars from the angle your camera is set. At least, I had problems with that. I could train my own cascade classifier, but why not try to do something smart.

Step 1: Get a live feed from the webcam in OpenCV

First you need to ensure you have installed OpenCV. If you use PyCharm we can recommend you read this tutorial on how to set it up.

To get a live feed from your webcam can be achieved by the following lines of code.

import cv2


cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

while cap.isOpened():
    _, frame = cap.read()

    cv2.imshow("Car counter", frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

The cv2.VideoCapture(0) assumes that you only have one webcam. If you have more, you might need to change 0 to something else.

The cap.set(…) are setting the width and height of the camera frames. In order to get good performance it is good to scale down. This can also be achieved with scaling the picture you make processing on after down.

Then cap.read() reads the next frame. It also returns a return value, but we ignore that value with the underscore (_). The cv2.imshow(…) will create a window with showing the frame. Finally, the cv2.waitkey(1) waits 1 millisecond and check if q was pressed. If so, it will break out and release the camera and destroy the window.

Step 2: Identify moving objects with OpenCV

The simple idea is that to compare each frame with the previous one. If there is a difference, we have a moving object. Of course, a bit more complex, as we also want to identify where the objects are and avoid identifying differences due to noise in the picture.

As most processing on moving images we will start by converting them to gray tones (cv2.cvtColor(…)). Then we will use blurring to minimize details in the picture (cv2.GaussianBlur(…)). This helps us with falsely identifying moving things that are just because of noise and minor changes.

When that is done, we compare that converted frame with the one from previous frame (cv2.absdiff(…)). This gives you an idea of what has changed. We keep a threshold (cv2.threshold(…)) on it and then dilate (cv2.dilate(…)) change to make it easier to identify with cv2.findContours(…).

It boils down to the following code.

import cv2
import imutils


cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# We will keep the last frame in order to see if there has been any movement
last_frame = None

while cap.isOpened():
    _, frame = cap.read()

    # Processing of frames are done in gray
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # We blur it to minimize reaction to small details
    gray = cv2.GaussianBlur(gray, (21, 21), 0)

    # Need to check if we have a last_frame, if not get it
    if last_frame is None:
        last_frame = gray
        continue

    # Get the difference from last_frame
    delta_frame = cv2.absdiff(last_frame, gray)
    last_frame = gray
    # Have some threshold on what is enough movement
    thresh = cv2.threshold(delta_frame, 25, 255, cv2.THRESH_BINARY)[1]
    # This dilates with two iterations
    thresh = cv2.dilate(thresh, None, iterations=2)
    # Returns a list of objects
    contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # Converts it
    contours = imutils.grab_contours(contours)

    # Loops over all objects found
    for contour in contours:
        # Get's a bounding box and puts it on the frame
        (x, y, w, h) = cv2.boundingRect(contour)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

    # Let's show the frame in our window
    cv2.imshow("Car counter", frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

If you don’t look out for what is happening it could turn out to a picture like this one (I am sure you take more care than me).

Example frame of moving objects.

One thing to notice is, that we could make a lower limit on the sizes of the moving objects. This can be achieved by inserting a check before we make the green boxes.

Step 3: Creating a helper class to track counts

To make our life easier we introduce a helper class to represent a box on the screen that keeps track on how many objects have been moving though it.

class Box:
    def __init__(self, start_point, width_height):
        self.start_point = start_point
        self.end_point = (start_point[0] + width_height[0], start_point[1] + width_height[1])
        self.counter = 0
        self.frame_countdown = 0

    def overlap(self, start_point, end_point):
        if self.start_point[0] >= end_point[0] or self.end_point[0] <= start_point[0] or \
                self.start_point[1] >= end_point[1] or self.end_point[1] <= start_point[1]:
            return False
        else:
            return True

The class will take the staring point (start_point) and the width and height (width_height) to the constructor. As we will need start_point and end_point when drawing the box in the frame we calculate that immediately in the constructor (__init__(…)).

Further, we will have a counter to keep track on how many object have passed through the box. There is also a frame_countdown, which is used to minimize multiple counts of the same moving object. What can happen is that in one frame the moving object is identified, while in the next it is not, but then it is identified again. If that all happens within the box, it will count the object twice. Hence, we will have countdown that says we need at minimum number of frames between identified moving objects before we can assume it is a new one.

Step 4: Using the helper class and start the counting

We need to add all the code together here.

It requires a few things. Before we enter the main while loop, we need to setup the boxes we want to count moving objects in. Here we setup two, which will be one for each direction the cars can drive. Inside the contours loop, we set a lower limit of the contour sizes. Then we go through all the boxes and update the appropriate variables and build the string text. After that, it will print the text in the frame as well as add all the boxes to it.

import cv2
import imutils


class Box:
    def __init__(self, start_point, width_height):
        self.start_point = start_point
        self.end_point = (start_point[0] + width_height[0], start_point[1] + width_height[1])
        self.counter = 0
        self.frame_countdown = 0

    def overlap(self, start_point, end_point):
        if self.start_point[0] >= end_point[0] or self.end_point[0] <= start_point[0] or \
                self.start_point[1] >= end_point[1] or self.end_point[1] <= start_point[1]:
            return False
        else:
            return True


cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

# We will keep the last frame in order to see if there has been any movement
last_frame = None

# To build a text string with counting status
text = ""

# The boxes we want to count moving objects in
boxes = []
boxes.append(Box((100, 200), (10, 80)))
boxes.append(Box((300, 350), (10, 80)))

while cap.isOpened():
    _, frame = cap.read()

    # Processing of frames are done in gray
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # We blur it to minimize reaction to small details
    gray = cv2.GaussianBlur(gray, (5, 5), 0)

    # Need to check if we have a lasqt_frame, if not get it
    if last_frame is None or last_frame.shape != gray.shape:
        last_frame = gray
        continue

    # Get the difference from last_frame
    delta_frame = cv2.absdiff(last_frame, gray)
    last_frame = gray
    # Have some threshold on what is enough movement
    thresh = cv2.threshold(delta_frame, 25, 255, cv2.THRESH_BINARY)[1]
    # This dilates with two iterations
    thresh = cv2.dilate(thresh, None, iterations=2)
    # Returns a list of objects
    contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    # Converts it
    contours = imutils.grab_contours(contours)

    # Loops over all objects found
    for contour in contours:
        # Skip if contour is small (can be adjusted)
        if cv2.contourArea(contour) < 500:
            continue

        # Get's a bounding box and puts it on the frame
        (x, y, w, h) = cv2.boundingRect(contour)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

        # The text string we will build up
        text = "Cars:"
        # Go through all the boxes
        for box in boxes:
            box.frame_countdown -= 1
            if box.overlap((x, y), (x + w, y + h)):
                if box.frame_countdown <= 0:
                    box.counter += 1
                # The number might be adjusted, it is just set based on my settings
                box.frame_countdown = 20
            text += " (" + str(box.counter) + " ," + str(box.frame_countdown) + ")"

    # Set the text string we build up
    cv2.putText(frame, text, (10, 20), cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 0), 2)

    # Let's also insert the boxes
    for box in boxes:
        cv2.rectangle(frame, box.start_point, box.end_point, (255, 255, 255), 2)

    # Let's show the frame in our window
    cv2.imshow("Car counter", frame)

    if cv2.waitKey(1) &amp; 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

Step 5: Real life test on counting cars (not just moving objects)

The real question is, does it work or did we oversimplify the problem. If it works we have created a very small piece of code (comparing to other implementations), which can count cars all day long.

I adjusted the parameters a bit and got the following with my first real trial.

Counting correctly.

Please notice, that it does not start from zero in the video. But it counts the number of cars in each direction correctly. As expected, it counts when each car reaches the white bar.

The number of cars is the first number, while the second is just visible for me to see if my guess of skipping frames was useable.

Are we done?

Not at all. This was just to see if we could make some simple and fast to count cars. My first problem was, that given the trained sets of car recognition (car cascade classifiers) was not happy about the angle on the cars from my window. I first thought of training my own cascade classifier, but I thought it was fun to try something more simple.

There are a lot of parameters which can be tuned to make it more reliable, but the main fact is, that it was counting correctly in the given test. I can see one challenge, if a big truck drives by from the left to the right, it might get in the way of the other counter. This could be a potential challenge with this simple approach.

Leave a Reply