AI/ML11 min read · 5 December 2025
Achieving 94% Accuracy with YOLOv8 for Real-Time Traffic Management
How I trained a custom YOLOv8 model on traffic footage, implemented adaptive signal control logic, and optimised inference speed for real-time deployment.
YOLOv8Computer VisionPyTorchPythonOpenCV
Why Traffic Management Needs Computer Vision
Traditional traffic signals run on fixed timers — completely blind to actual traffic density. A computer vision system that counts vehicles in real time can adapt signal timing dynamically, reducing average wait times by 30-40%.
Dataset Preparation
I used a combination of public datasets and custom-annotated footage:
- UA-DETRAC — 140k+ annotated frames from Chinese highways
- Custom footage — 2,000 frames from UK roundabouts (annotated with Roboflow)
- Classes — Car, Truck, Bus, Motorcycle, Cyclist, Pedestrian
python
# Dataset config (data.yaml)
train: datasets/traffic/train
val: datasets/traffic/val
test: datasets/traffic/test
nc: 6
names: ['car', 'truck', 'bus', 'motorcycle', 'cyclist', 'pedestrian']
Training YOLOv8
python
from ultralytics import YOLO
model = YOLO('yolov8m.pt') # Medium model — good speed/accuracy tradeoff
results = model.train(
data='data.yaml',
epochs=100,
imgsz=640,
batch=16,
lr0=0.01,
lrf=0.001,
augment=True, # Random flips, crops, colour jitter
hsv_h=0.015, # Hue augmentation
hsv_s=0.7, # Saturation
mosaic=1.0, # Mosaic augmentation
device='cuda', # GPU required
patience=20, # Early stopping
save_period=10,
)
Real-Time Inference Pipeline
python
import cv2
from ultralytics import YOLO
from collections import defaultdict
model = YOLO('best.pt')
cap = cv2.VideoCapture('traffic_feed.mp4')
# Define counting lines per lane
COUNTING_LINES = {
'north': [(0, 400), (640, 400)],
'south': [(0, 200), (640, 200)],
}
vehicle_counts = defaultdict(int)
track_history = defaultdict(list)
while cap.isOpened():
success, frame = cap.read()
if not success:
break
# Track vehicles across frames
results = model.track(frame, persist=True, classes=[0,1,2,3])
if results[0].boxes.id is not None:
boxes = results[0].boxes.xywh.cpu()
track_ids = results[0].boxes.id.int().cpu().tolist()
for box, track_id in zip(boxes, track_ids):
x, y, w, h = box
track_history[track_id].append((float(x), float(y)))
# Check if vehicle crossed counting line
if crossed_line(track_history[track_id], COUNTING_LINES['north']):
vehicle_counts['north'] += 1
# Adaptive signal logic
update_signal_timing(vehicle_counts)
Adaptive Signal Control
python
def update_signal_timing(counts: dict) -> dict:
total = sum(counts.values())
if total == 0:
return {lane: 30 for lane in counts} # Default 30s
# Proportional allocation with min/max constraints
return {
lane: max(15, min(90, int((count / total) * 120)))
for lane, count in counts.items()
}
Results
| Metric | Score |
|---|
| mAP@0.5 | 94.2% |
| mAP@0.5:0.95 | 71.8% |
| Inference speed | 28ms/frame (RTX 3060) |
| FPS | ~35 real-time |
The 94% mAP@0.5 beat the project target of 85% and was achieved largely through the mosaic augmentation and custom UK road footage that improved generalisation.