Learn how you can become a Python programmer in just 12 weeks.

    We respect your privacy. Unsubscribe at anytime.

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

    Python Circle

    Do you know what the 5 key success factors every programmer must have?

    How is it possible that some people become programmer so fast?

    While others struggle for years and still fail.

    Not only do they learn python 10 times faster they solve complex problems with ease.

    What separates them from the rest?

    I identified these 5 success factors that every programmer must have to succeed:

    1. Collaboration: sharing your work with others and receiving help with any questions or challenges you may have.
    2. Networking: the ability to connect with the right people and leverage their knowledge, experience, and resources.
    3. Support: receive feedback on your work and ask questions without feeling intimidated or judged.
    4. Accountability: stay motivated and accountable to your learning goals by surrounding yourself with others who are also committed to learning Python.
    5. Feedback from the instructor: receiving feedback and support from an instructor with years of experience in the field.

    I know how important these success factors are for growth and progress in mastering Python.

    That is why I want to make them available to anyone struggling to learn or who just wants to improve faster.

    With the Python Circle community, you can take advantage of 5 key success factors every programmer must have.

    Python Circle
    Python Circle

    Be part of something bigger and join the Python Circle community.

    6 thoughts on “OpenCV: A Simple Approach to Counting Cars”

    Leave a Comment