RegionKit Open Editor →
OpenCV line crossing object counting Python computer vision tripwire

How to Count Objects Crossing a Line with OpenCV

Implement object counting across a virtual tripwire in OpenCV Python — using a tracker, a crossing detection function, and zone coordinates exported from RegionKit. Includes direction-aware counting.

· RegionKit

Counting objects that cross a line — people entering a building, vehicles passing a checkpoint, packages moving along a conveyor — is one of the most common tasks in deployed computer vision. This guide implements it end-to-end: drawing the line in a visual tool, loading the coordinates, running a detector and tracker, and firing a crossing event.

What you need

Step 1: Draw the line in RegionKit

Open RegionKit and load a still frame from your camera. Use the Polyline tool (L) to draw the counting line:

  1. Press L
  2. Click the start point of the line
  3. Click the end point
  4. Double-click or press Enter to commit

Label the line (e.g. count_line) in the Properties panel. For direction-aware counting, draw two parallel lines labelled line_a and line_b.

Export as Native JSON and save as scene.json.

Step 2: Load the line coordinates

import json
import numpy as np

with open('scene.json') as f:
    scene = json.load(f)

def get_line(scene, label):
    for ann in scene['annotations']:
        if ann['type'] == 'polyline' and ann.get('label') == label:
            pts = ann['data']['points']
            return np.array(list(zip(pts[::2], pts[1::2])), dtype=np.float32)
    return None

line = get_line(scene, 'count_line')
line_start, line_end = line[0], line[-1]
print(f"Line: {line_start}{line_end}")

Step 3: The crossing detection function

A crossing occurs when an object’s position moves from one side of the line to the other between frames. We check this with a sign change in the cross product.

def side_of_line(point, start, end):
    """Returns positive if point is to the left of start→end, negative if right."""
    return (end[0]-start[0]) * (point[1]-start[1]) - (end[1]-start[1]) * (point[0]-start[0])

def crossed(prev_pos, curr_pos, line_start, line_end):
    """Returns True if the path prev→curr crosses the line start→end."""
    s1 = side_of_line(prev_pos, line_start, line_end)
    s2 = side_of_line(curr_pos, line_start, line_end)
    if s1 == 0 or s2 == 0:
        return False  # on the line — skip to avoid double-counting
    return (s1 > 0) != (s2 > 0)

def direction(prev_pos, curr_pos, line_start, line_end):
    """Returns 'forward' or 'backward' based on crossing direction."""
    s1 = side_of_line(prev_pos, line_start, line_end)
    return 'forward' if s1 > 0 else 'backward'

Step 4: The counting loop

from ultralytics import YOLO
import cv2

model = YOLO('yolov8n.pt')

cap = cv2.VideoCapture('video.mp4')  # or 0 for webcam

prev_positions = {}   # track_id → previous centroid
counts = {'forward': 0, 'backward': 0}

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

    results = model.track(frame, persist=True, tracker='bytetrack.yaml')

    if results[0].boxes.id is not None:
        boxes  = results[0].boxes.xyxy.cpu().numpy()
        ids    = results[0].boxes.id.cpu().numpy().astype(int)

        for box, track_id in zip(boxes, ids):
            cx = float((box[0] + box[2]) / 2)
            cy = float((box[1] + box[3]) / 2)
            curr_pos = np.array([cx, cy])

            if track_id in prev_positions:
                prev_pos = prev_positions[track_id]
                if crossed(prev_pos, curr_pos, line_start, line_end):
                    d = direction(prev_pos, curr_pos, line_start, line_end)
                    counts[d] += 1
                    print(f"Crossing! ID {track_id}{d}. Total: {counts}")

            prev_positions[track_id] = curr_pos

    # Draw the counting line
    cv2.line(frame,
             tuple(line_start.astype(int)),
             tuple(line_end.astype(int)),
             (0, 255, 128), 2)

    label = f"IN: {counts['forward']}  OUT: {counts['backward']}"
    cv2.putText(frame, label, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255), 2)

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

cap.release()
cv2.destroyAllWindows()

Handling edge cases

Fast-moving objects — If an object moves so fast that its centroid jumps past the line in a single frame, the sign-change check still works because you’re comparing position-before to position-after, not checking proximity to the line.

Objects that stop on the line — The s1 == 0 check skips crossing detection when the object is exactly on the line. Without this, an object oscillating slightly around the line (due to detection noise) would generate many false crossings.

Tracker ID reassignment — When an object leaves the frame and re-enters, the tracker may assign a new ID. This is an inherent limitation of single-camera counting. For doorway counting, the assumption is usually that each unique tracker ID corresponds to one physical crossing.

Scaling to camera resolution — The line coordinates from RegionKit are in image pixels relative to the still frame you used to draw them. If your inference frame is a different resolution, scale the line coordinates accordingly:

scale_x = inference_w / scene_w
scale_y = inference_h / scene_h
line_start = line_start * [scale_x, scale_y]
line_end   = line_end   * [scale_x, scale_y]

Using multiple counting lines

For a scene with multiple entry points, use multiple polyline annotations in RegionKit — each with a distinct label. Load all of them into a dict and check each one in the loop.


Related: What Is a Tripwire in Computer Vision? · Detection Zone Editor

Try RegionKit — it's free

Browser-based ROI editor. No install, no account, no data upload.

Open the Editor