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
- Python 3.8+
- OpenCV (
pip install opencv-python) - An object detector (we’ll use YOLOv8 via
ultralytics) - A tracker (ByteTrack, included in ultralytics)
- Line coordinates exported from RegionKit
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:
- Press
L - Click the start point of the line
- Click the end point
- Double-click or press
Enterto 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