Showing posts with label OpenCV. Show all posts
Showing posts with label OpenCV. Show all posts

Tuesday, October 27, 2026

Count Cars with YOLO: Why Track IDs Are Not Vehicle Counts

A box follows a car, a number appears above it, and a counter increases. It looks simple. But what exactly did the application count: detections, tracking identities, or vehicles passing a particular point? Those are different measurements.

In this Python example, YOLOv8n detects vehicles, ByteTrack associates observations across frames, and a separate rule accepts downward crossings of a horizontal line. Keeping those responsibilities separate makes the result easier to explain and test.

Traffic counter showing tracked vehicle boxes, temporary IDs and a horizontal counting segment
The demonstration uses a synthetic traffic scene. Boxes, IDs and crossing events come from the same recorded tracking run.

Three measurements that should stay separate

A detection describes an object in one frame. A track ID associates observations over time. A crossing event records an observation sequence that satisfies our counting rule.

One car can appear in many frames. A tracker may also lose a vehicle and assign a new ID when it is detected again. Counting every detection would repeatedly count visible objects; counting distinct IDs would measure the tracker's associations. Neither automatically tells us how many physical vehicles passed our chosen location.

In this particular 241-frame, 1280 × 720 clip, the recorded run contains 23 temporary IDs. Our selected rule accepts 7 crossing events. This is a result for one clip and configuration, not an accuracy claim for general traffic footage.

Define a crossing before calculating a total

Image coordinates start at the top left: y increases downward. We place the line at line_y = 413 and restrict its horizontal span to x_range = (80, 1000). A qualifying center must move from above the line to the line or below it. Its current x must fall within that span.

The complete rule below remembers the previous y for each ID. It also remembers IDs already counted during this function call.

# Turn tracked positions into one downward pass per ID.
def count_passes(frame_tracks, line_y, x_range):
    previous_y, counted_ids, events = {}, set(), []
    for frame in frame_tracks:
        for obj in frame["objects"]:
            track_id = obj["track_id"]
            x, y = obj["center"]
            crossed = previous_y.get(track_id, y) < line_y <= y
            if crossed and x_range[0] <= x <= x_range[1] and track_id not in counted_ids:
                counted_ids.add(track_id)
                events.append({"frame": frame["frame_idx"], "id": track_id, "class": obj["class_name"]})
            previous_y[track_id] = y
    return events

Code: Copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials. Licensed under the GNU AGPL version 3, without warranty. This code notice does not automatically apply to the independent images, video or course presentation.

The decisive comparison is previous_y.get(track_id, y) < line_y <= y. For a first observation, the default previous value is the current y, so the comparison is false. An object first detected below the line therefore does not produce an invented crossing.

counted_ids prevents a second accepted event for the same ID in one call. The function updates the previous y after examining every observation, even when the object is outside the horizontal span. It does not interpolate where the center crossed the line: the span check uses the current x.

Explain one event with pictures and numbers

For ID 3, the recorded center moves from y = 411.236 in frame 36 to y = 414.239 in frame 37. The rule accepts frame 37 because 411.236 < 413 <= 414.239. Its current x is approximately 474.9, inside the permitted span. ID 3 has not yet been counted.

Before, crossing and after observations for track ID 3 in frames 36, 37 and 38
The same ID before, at and after its accepted event. Coordinates are rounded for display; the rule uses the original values.

This evidence is more informative than a final total alone. You can inspect the visible vehicle, the relevant center positions and the exact decision together.

Equal totals can hide different events

Move only the line upward by 30 pixels, from y = 413 to y = 383. Reuse the same recorded tracks and horizontal span. The result is again 7 events, but the accepted IDs differ:

LineEventsAccepted IDs
y = 41374, 3, 2, 5, 9, 7, 55
y = 38373, 2, 5, 9, 7, 55, 48

ID 4 is first observed at y ≈ 398.5, already below the shifted line. Its observations support a crossing of y = 413, but not y = 383. ID 48 supplies an event only at the shifted line. Comparing event membership exposes what the matching totals hide.

Check logic separately from tracking quality

Run these checks after defining count_passes above. Their deliberately chosen center positions isolate Python logic from model quality. The three assertions test repeated downward crossings by one ID, an ID first seen below the line, and upward movement. All x positions are inside the counting segment.

def sample(y_values):
    return [{"frame_idx": i, "objects": [{"track_id": 1,
             "class_name": "car", "center": (200, y)}]}
            for i, y in enumerate(y_values)]

assert len(count_passes(sample([411, 413, 410, 416]), 413, (80, 1000))) == 1
assert len(count_passes(sample([414, 420]), 413, (80, 1000))) == 0
assert len(count_passes(sample([420, 410]), 413, (80, 1000))) == 0
print("All three counting-rule checks passed.")

The expected counts are 1, 0 and 0. After these checks pass, inspect a short video interval manually and match individual passages to event rows.

This compact rule has limits. A changed ID can allow the same vehicle to count again. Missing observations may leave a long gap between the two positions used by the comparison. There is no gap limit or hysteresis band here, and motion upward is intentionally excluded. Real deployment needs representative footage and separate checks for missed passages and false counts.

For the tracking API and tracker configuration, see the official Ultralytics tracking documentation. This example explicitly selects bytetrack.yaml; it does not rely on whichever tracker a library version uses by default.

If you want to build the full project, including the notebook, local application and practical exercises, see my English course: Computer Vision: OpenCV, Machine Learning and YOLO.

New Udemy Course: Computer Vision with OpenCV, Machine Learning and YOLO

I have published my new English-language course on Udemy: Computer Vision: OpenCV, Machine Learning and YOLO.

The course takes you from working with image arrays to building Python applications that detect, track and count objects in images and video. It combines theory with separate practical lessons, so you can see the inputs, follow the code and inspect the results.

What you will learn

  • Python, NumPy and OpenCV: image representation, color channels, transformations, filtering and edge detection.
  • Machine learning: feature extraction, classification, model comparison and evaluation with scikit-learn.
  • Deep learning and CNNs: neural network foundations, convolution, pooling, image classification and transfer learning with TensorFlow and PyTorch.
  • YOLO: object detection, custom training, object tracking, segmentation and image classification.
  • Pose estimation and deployment: body keypoints, movement features, model export, CPU performance measurements and comparisons with alternative detectors.

Four projects that bring the ideas together

  • Parking Lot Monitor: work with parking-space regions and occupancy information.
  • Traffic Counter: detect and track vehicles, then count crossings of a defined line.
  • Fitness Tracker: use pose keypoints and movement states to count squat repetitions.
  • Document Scanner: find document boundaries and correct perspective.

These are learning projects that make the connection between model outputs and application logic visible.

Video lessons, notebooks and knowledge checks

The course includes more than 10 hours of English video, downloadable notebooks and lesson resources, and 12 section quizzes with 36 questions. Practical lessons include setup and dependency instructions to help you run the examples locally.

It is aimed at developers, students and engineers who want to move from computer vision concepts to working Python examples. Some programming experience is helpful; the opening sections review Python and NumPy foundations.

Launch offer: US$12.99

Coupon code: CBEE0F25D4870B5F9123

Open the course with the US$12.99 coupon

The coupon is valid until 27 November 2026 at 06:20 UTC (26 November, 22:20 PST). Check that the coupon is applied and review the final price shown by Udemy before purchasing.

You can also use my standard Udemy course link to view the course and its curriculum. This separate link does not include the coupon code.

I would be glad to hear which computer vision applications you would like to build.