# Building a Centroid Tracker Counter System: A Step\-by\-Step Tutorial

- Canonical: https://33rdsquare.com/a-tutorial-on-centroid-tracker-counter-system/
- Published: 2024-09-03
- Author: Jordan Brown
- Categories: [Artificial Intelligence & Machine Learning & ChatGPT](https://33rdsquare.com/category/tech/ai/)

---

Object tracking is a fundamental problem in computer vision with numerous real-world applications, from traffic monitoring to sports analytics to retail analytics. The goal is to detect and track objects of interest, such as vehicles or people, as they move through a video stream. By keeping track of each object‘s location and movement over time, we can derive useful insights like counting the number of objects, measuring traffic flow, or analyzing behavior.

In this tutorial, we‘ll walk through how to build an object tracking system using the centroid tracking algorithm. We‘ll implement it in Python using the OpenCV library and extend it to create a vehicle counter. Finally, we‘ll discuss potential improvements using more advanced techniques.

## Overview of Object Tracking Algorithms

There are many approaches to object tracking, each with their own strengths and weaknesses. Here are a few of the most popular:

**SORT (Simple Online and Realtime Tracking)**: This algorithm uses a Kalman filter to estimate object trajectories based on bounding box detections from an object detector like YOLO. It‘s relatively simple to implement and runs efficiently in real-time.

**Deep SORT**: An extension of SORT that incorporates appearance information using a deep neural network. This allows it to re-identify objects even if detection is lost momentarily. Deep SORT is more accurate than SORT but also more computationally expensive.

**Centroid Tracking**: A simple yet effective algorithm that tracks objects based on the Euclidean distance between their centroids (centers) across consecutive frames. Its main advantage is that it‘s fast and easy to implement.

In this tutorial, we‘ll focus on centroid tracking since it‘s a great starting point to learn the fundamentals of object tracking. We‘ll then discuss how you can swap in more advanced algorithms later on.

## How Centroid Tracking Works

The key idea behind centroid tracking is to assign each object a unique ID and track its center point over time. Here‘s a breakdown of the algorithm:

1. Detect objects in the current frame using an object detector (like a pre-trained deep learning model). This gives us a set of bounding box coordinates for each detected object.
2. Compute the centroid (center point) of each bounding box.
3. For each detected object in the current frame, compute the Euclidean distance between its centroid and the centroids of existing tracked objects from the previous frame.
4. Assign each detected object to its nearest neighbor from the previous frame, as long as the distance is below a specified threshold. If there‘s no match, register it as a new object with a new ID. If an object from the previous frame has no match in the current frame, assume it has disappeared or left the scene.
5. Return the list of tracked objects with their bounding box coordinates and assigned IDs.
6. Repeat steps 1-5 for each new frame in the video stream.

By matching objects based on the distance between their centroids, we can track them through the video even as their apparent size and position changes. The tracking algorithm itself is quite simple, but accurately detecting the objects to track is a challenging problem. That‘s why object trackers are often paired with state-of-the-art object detectors.

## Implementing a Centroid Tracker in Python

Now let‘s see how to implement centroid tracking in Python using OpenCV. We‘ll assume you already have OpenCV and NumPy installed. Here‘s the code for the centroid tracker class:

```
import math
import numpy as np

class CentroidTracker:
    def __init__(self, max_disappeared=30):
        self.next_id = 0
        self.objects = dict()
        self.disappeared = dict()
        self.max_disappeared = max_disappeared

    def register(self, centroid):
        self.objects[self.next_id] = centroid
        self.disappeared[self.next_id] = 0
        self.next_id += 1

    def deregister(self, object_id):
        del self.objects[object_id]
        del self.disappeared[object_id]

    def update(self, bboxes):
        if len(bboxes) == 0:
            for object_id in list(self.disappeared.keys()):
                self.disappeared[object_id] += 1
                if self.disappeared[object_id] > self.max_disappeared:
                    self.deregister(object_id)
            return self.objects

        input_centroids = np.zeros((len(bboxes), 2), dtype="int")
        for (i, (x, y, w, h)) in enumerate(bboxes):
            centroid_x = int((2 * x + w) / 2)
            centroid_y = int((2 * y + h) / 2)
            input_centroids[i] = (centroid_x, centroid_y)

        if len(self.objects) == 0:
            for i in range(len(input_centroids)):
                self.register(input_centroids[i])
        else:
            object_ids = list(self.objects.keys())
            object_centroids = list(self.objects.values())

            distances = dist.cdist(np.array(object_centroids), input_centroids)

            rows = distances.min(axis=1).argsort()
            cols = distances.argmin(axis=1)[rows]

            used_rows = set()
            used_cols = set()

            for (row, col) in zip(rows, cols):
                if row in used_rows or col in used_cols:
                    continue

                object_id = object_ids[row]
                self.objects[object_id] = input_centroids[col]
                self.disappeared[object_id] = 0

                used_rows.add(row)
                used_cols.add(col)

            unused_rows = set(range(distances.shape[0])).difference(used_rows)
            unused_cols = set(range(distances.shape[1])).difference(used_cols)

            if distances.shape[0] >= distances.shape[1]:
                for row in unused_rows:
                    object_id = object_ids[row]
                    self.disappeared[object_id] += 1

                    if self.disappeared[object_id] > self.max_disappeared:
                        self.deregister(object_id)
            else:
                for col in unused_cols:
                    self.register(input_centroids[col])

        return self.objects
```

This implements the core centroid tracking logic as described in the previous section. The key methods are:

- `register`: Adds a new object to the tracker with a unique ID
- `deregister`: Removes an object from the tracker
- `update`: Takes a list of bounding boxes for the current frame, matches them with existing objects, and updates their centroids. Returns a dictionary mapping object IDs to centroid coordinates.

The `update` method does the heavy lifting of matching input detections to existing objects based on the Euclidean distance between their centroids. It uses the `cdist` function from SciPy to efficiently compute pairwise distances. Objects that don‘t match any detections for more than `max_disappeared` frames are assumed to have left the scene and are removed.

To use this tracker, you‘ll need to plug in your own object detector to provide the bounding boxes for each frame. Here‘s a toy example using a simple background subtractor:

```
import cv2
from centroid_tracker import CentroidTracker

cap = cv2.VideoCapture(‘path/to/video‘)

background_subtractor = cv2.createBackgroundSubtractorMOG2(history=100, varThreshold=50)

tracker = CentroidTracker(max_disappeared=40)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    mask = background_subtractor.apply(frame)
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    detections = []
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        if cv2.contourArea(contour) > 500:
            detections.append([x, y, w, h])

    objects = tracker.update(detections)

    for (object_id, centroid) in objects.items():
        text = f"ID {object_id}"
        cv2.putText(frame, text, (centroid[0] - 10, centroid[1] - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        cv2.circle(frame, (centroid[0], centroid[1]), 4, (0, 255, 0), -1)

    cv2.imshow("Frame", frame)

    if cv2.waitKey(30) == 27:
        break

cv2.destroyAllWindows()
```

This code loads a video file, applies a background subtractor to each frame to detect moving objects, and passes the bounding boxes of those objects to the centroid tracker. The tracker assigns each object an ID and tracks its centroid across frames. We display the object ID and centroid on the output frame.

In a real application, you would swap out the background subtractor for a more robust object detector like YOLOv8 or Faster R-CNN. The centroid tracker doesn‘t care what type of detector you use, as long as it provides bounding boxes.

## Extending to a Vehicle Counter

Now that we have a working object tracker, we can easily extend it to count vehicles in traffic footage. The idea is to define a "counting line" across the road, and increment a counter each time an object crosses that line.

Here‘s a simple implementation:

```
line = [(0, int(frame_height/2)), (frame_width, int(frame_height/2))]

counter = 0
counted_ids = set()

while True:
    ret, frame = cap.read()
    if not ret:
        break

    # ... (detect objects and update tracker as before) ...

    for (object_id, centroid) in objects.items():
        if object_id not in counted_ids:
            x, y = centroid
            if intersects(line[0], line[1], (x, y-10), (x, y+10)):
                counted_ids.add(object_id)
                counter += 1

    cv2.line(frame, line[0], line[1], (0, 255, 255), 2)
    cv2.putText(frame, f"Count: {counter}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)

    cv2.imshow("Frame", frame)

    if cv2.waitKey(30) == 27:
        break

cv2.destroyAllWindows()
```

We define a line across the middle of the frame using two points. For each tracked object, we check if its centroid intersects with this line segment (using a helper function `intersects`). If so, and it hasn‘t been counted yet, increment the counter and add its ID to the `counted_ids` set. This ensures we only count each object once. We display the counting line and total count on the output frame.

And there you have it – a simple vehicle counting system using centroid tracking! You can imagine extending this further by classifying the type of vehicle using a CNN, estimating speed by measuring distance traveled over time, or tracking lane changes and turns.

## Advanced Tracking Techniques

While centroid tracking is a good introduction to object tracking, it has some limitations. It can struggle with occlusion (objects overlapping or passing behind each other), doesn‘t handle objects entering and leaving the frame very gracefully, and can get confused if the object detector produces noisy or inconsistent results.

More advanced tracking algorithms like SORT and Deep SORT address these issues by incorporating motion prediction using Kalman filters and appearance descriptors from deep networks. This allows them to more robustly track objects through occlusion and brief detection dropouts.

Another key aspect of object tracking is properly handling the coordinate transformation from the camera perspective to an overhead "bird‘s eye view". This is important for getting accurate distance and speed measurements. It requires calibrating the camera and applying a perspective transform to the detections before tracking.

## Conclusion

In this tutorial, we covered the basics of object tracking and counting using the centroid tracking algorithm. We walked through a Python implementation using OpenCV, and showed how to extend it to create a simple vehicle counter.

The key steps are:

1. Detect objects in each frame using an object detector
2. Compute centroids of detected objects
3. Match centroids across frames based on Euclidean distance
4. Assign object IDs and update tracker state
5. Analyze object trajectories to derive counts, speeds, etc.

Centroid tracking is a good starting point, but for more challenging scenarios you‘ll want to explore more advanced algorithms like SORT and Deep SORT. You‘ll also need to think carefully about camera calibration and coordinate transforms to get meaningful real-world measurements.

I hope this tutorial has given you a solid foundation to start experimenting with object tracking and counting. OpenCV and Python make it easy to quickly prototype and iterate on computer vision applications. Try applying these techniques to your own videos and see what insights you can uncover!

Here are some resources to learn more:

- "Multiple Object Tracking: A Literature Review" by Wenhan Luo, et al. provides a comprehensive survey of academic research on object tracking algorithms and datasets.
- "Simple Online and Realtime Tracking with a Deep Association Metric" by Wojke et al. describes the Deep SORT algorithm in detail.
- "YOLO: Real-Time Object Detection" introduces the breakthrough YOLO family of object detectors, which are often used as the backbone for object trackers.
- "Traffic Counting and Tracking with OpenCV" is a three part blog series that goes deeper into building a traffic monitoring system.

Have fun tracking!

---

Source: [Building a Centroid Tracker Counter System: A Step\-by\-Step Tutorial](https://33rdsquare.com/a-tutorial-on-centroid-tracker-counter-system/)
