A Counting Cars

Oscar Rojo
8 min readSep 3, 2020

--

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.

Photo by Annie Spratt on Unsplash

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.

To close window, clic ‘q’

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).

from IPython.display import Video

Video("video.mp4")

https://youtu.be/g80jayBXviI

Your browser does not support the video element.

from IPython.display import HTML

HTML("""
<div align="middle">
<video width="80%" controls>
<source src="video.mp4" type="video/mp4">
</video></div>""")

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((200, 250), (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) & 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.

from IPython.display import Video
Video("counting1.mp4")

https://www.youtube.com/watch?v=qHUPUaivJxA

Your browser does not support the video element.

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.

Conclusion

This is a small sample of the openCV library. Imagine what we can do…

I hope it will help you to develop your training.

No matter what books or blogs or courses or videos one learns from, when it comes to implementation everything might look like “Out of Syllabus”

Best way to learn is by doing! Best way to learn is by teaching what you have learned!

--

--