YOLOv8+ByteTrack实时车辆追踪与流量统计实战

发布时间:2026/10/11 9:20:07
YOLOv8+ByteTrack实时车辆追踪与流量统计实战 简介这份资源面向计算机视觉与智能交通方向的个人学习者提供一套基于YOLOv8与ByteTrack的实时车辆多目标检测、追踪与交通流量统计的完整项目代码。它解决的是视频流中车辆精准识别、连续跟踪与区域计数问题适合具备一定Python与深度学习基础、希望动手复现交通监控场景的读者。压缩包共12个文件约70.72MB包含2个py主程序与工具脚本、2个png效果截图、1个mp4演示视频、1个md说明文档以及txt依赖清单、gitignore与zbak备份文件等覆盖从环境配置到运行演示的完整链路。目前已有69人学习下载。通过阅读代码与说明读者可掌握YOLOv8检测与ByteTrack轨迹关联的整合方式理解车辆计数、类型区分与时段流量统计的实现思路并借助演示视频和截图快速验证效果为交通流量分析、违章行为识别与拥堵趋势预测等应用提供可复用的工程参考。1. 实时车辆追踪与流量统计从 YOLOv8 检测到 ByteTrack 匹配的完整链路路口摄像头画面里车辆密集、遮挡频繁、光照突变单纯跑一个 YOLOv8 检测模型只能得到每帧的孤立框无法回答“这辆车从哪来、到哪去、今天过了多少辆”。基于 YOLOv8 与 ByteTrack 的实时车辆多目标检测追踪与交通流量统计系统核心就是解决这个问题用 YOLOv8 做逐帧目标检测用 ByteTrack 做跨帧身份关联再在轨迹上画虚拟线圈或越线计数输出车流量、车型分类和平均速度。这套方案适合做智慧交通原型、边缘盒子部署、课程设计或算法工程练手的人。它不追求论文级 SOTA但胜在工程链路完整、依赖清晰、单卡甚至 RK3588 都能跑。下面按“检测怎么选、跟踪怎么配、计数怎么稳、坑怎么避”的顺序把可复现的路径拆开讲。2. 检测层选型与 YOLOv8 最小推理链路2.1 为什么车辆场景优先选 YOLOv8 而不是 YOLOv5车辆检测在交通场景里属于中等难度任务目标尺度变化大、类别少car、bus、truck、motorcycle 为主、实时性要求高。YOLOv8 相比 YOLOv5 在工程上最直接的优势是 anchor-free 解耦头加 Task-Aligned Assigner小目标召回和密集场景下的框回归更稳而且 Ultralytics 的 API 把训练、验证、导出统一成一套接口省掉大量胶水代码。对于交通流量统计检测框的稳定性比绝对精度更重要——框抖一下ByteTrack 的匹配就可能断轨迹一断计数就丢。选模型规模时我一般按部署硬件倒推GTX 1660 Ti 这类 6GB 显存卡跑 yolov8s 在 640 输入下能到 60 FPS 以上留足跟踪和计数开销RK3588 边缘盒子建议用 yolov8n 或 yolov8s 导出 RKNNINT8 量化后单核 NPU 能到 30 FPS 左右。不要一上来就上 yolov8x车辆类别少大模型边际收益低反而拖垮实时性。2.2 用 Python 跑通 YOLOv8 车辆检测的最小命令先装环境再写一个最小推理脚本确认模型能出框再谈跟踪。# 创建虚拟环境并安装依赖ultralytics 会自动拉取 torch 等 python -m venv venv source venv/bin/activate pip install ultralytics opencv-python numpy# detect_min.py读取视频逐帧检测车辆并画框 import cv2 from ultralytics import YOLO # 加载预训练模型首次运行会自动下载 yolov8s.pt model YOLO(yolov8s.pt) # 车辆相关类别在 COCO 中的 id2car, 5bus, 7truck, 3motorcycle VEHICLE_CLASSES [2, 3, 5, 7] cap cv2.VideoCapture(traffic.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # conf 设 0.35 是车辆场景的经验值太低会引入大量误检 results model.predict(frame, conf0.35, iou0.5, classesVEHICLE_CLASSES, verboseFalse) annotated results[0].plot() cv2.imshow(detect, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码的逻辑是model.predict每帧独立推理classes参数把非车辆类别过滤掉减少后续跟踪的干扰。conf0.35和iou0.5是两个关键参数——conf 控制置信度阈值交通场景建议 0.3~0.4太低会把阴影、反光当车iou 控制 NMS 重叠阈值车辆密集时 0.5 比较平衡调到 0.7 容易保留重复框。verboseFalse只是关掉每帧日志不影响结果。2.3 自训练数据集的类别映射与导出注意点如果要用自己的交通数据集标注时类别名建议直接对齐 COCO 的 car/bus/truck/motorcycle避免后面写映射表。训练命令常见做法是yolo detect train datatraffic.yaml modelyolov8s.pt epochs100 imgsz640 batch16traffic.yaml里names的顺序必须和标注文件里的 class id 一致否则检测框会张冠李戴。训练完导出 ONNX 或 RKNN 时注意imgsz要和推理时一致动态轴在边缘部署上容易出问题建议固定 640x640。画损失函数曲线图直接用训练输出目录里的results.csv用 pandas 读出来 plot 即可不要自己另存格式。3. ByteTrack 跟踪层匹配策略与参数调优3.1 ByteTrack 为什么适合交通流量统计多目标跟踪的经典难题是遮挡和漏检。ByteTrack 的核心思路是把检测框按置信度分成高分组和低分组先用高分组和现有轨迹匹配再用低分组去补救那些被遮挡导致置信度掉下来的轨迹。交通场景里车辆互相遮挡是常态低分框往往就是被挡住的真实车辆直接丢掉就会断轨。ByteTrack 不依赖外观特征纯靠运动预测和 IoU 匹配速度快、显存占用低和 YOLOv8 搭配在单卡上跑实时很合适。它的匹配分两步第一次用高分检测框和轨迹做 IoU 匹配第二次用低分检测框和第一次没匹配上的轨迹再匹配一次。轨迹用卡尔曼滤波预测下一帧位置匹配代价用 IoU 距离。这个设计让 ByteTrack 在 MOT17 上 IDF1 表现不错同时保持高帧率。3.2 把 ByteTrack 接进 YOLOv8 推理循环Ultralytics 已经内置了 ByteTrack直接用model.track即可不需要单独装 ByteTrack 库。# track_min.pyYOLOv8 ByteTrack 联合推理 import cv2 from ultralytics import YOLO model YOLO(yolov8s.pt) VEHICLE_CLASSES [2, 3, 5, 7] cap cv2.VideoCapture(traffic.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # persistTrue 让跟踪器在帧间保持状态这是跟踪的关键 results model.track( frame, conf0.35, iou0.5, classesVEHICLE_CLASSES, trackerbytetrack.yaml, persistTrue, verboseFalse, ) boxes results[0].boxes if boxes.id is not None: # boxes.id 就是每个目标的跟踪 ID ids boxes.id.int().cpu().tolist() clss boxes.cls.int().cpu().tolist() for tid, cls in zip(ids, clss): print(ftrack_id{tid}, class{cls}) cv2.imshow(track, results[0].plot()) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()关键在persistTrue它让跟踪器在连续帧之间保留轨迹状态如果每帧都重新初始化ID 会乱跳。trackerbytetrack.yaml指定配置文件Ultralytics 自带一份默认参数但交通场景往往需要改。boxes.id是跟踪 ID只有跟踪成功的目标才有检测但未匹配上的框 id 为 None计数时要过滤掉。3.3 ByteTrack 配置文件里必须关注的三个参数默认bytetrack.yaml不一定适合你的路口常见做法是复制一份改这几个值参数默认值交通场景建议作用track_high_thresh0.50.4~0.5高分检测框阈值决定哪些框参与第一次匹配track_low_thresh0.10.1~0.2低分框阈值太低会引入噪声太高救不回遮挡目标match_thresh0.80.7~0.9IoU 匹配阈值路口车辆密集可适当降低track_high_thresh调低会让更多框进入第一次匹配轨迹更连续但可能引入误匹配调高则轨迹容易断。match_thresh是 IoU 距离阈值车辆运动快、帧间位移大时IoU 本身会小阈值太高会导致匹配失败建议根据帧率和车速实测调整。改完配置后在model.track里传入自定义 yaml 路径即可。3.4 轨迹生命周期管理max_age 与 min_hitsByteTrack 内部用max_age控制轨迹丢失后保留多少帧用min_hits控制轨迹连续命中多少帧才确认。交通流量统计里max_age设太小车辆被遮挡几帧轨迹就没了ID 会换设太大车辆已经离开画面轨迹还挂着可能和下一辆车误匹配。常见做法是max_age30、min_hits3在 25~30 FPS 下大约对应 1 秒的容忍窗口。这两个参数不在默认 yaml 里直接暴露需要看 Ultralytics 的 tracker 实现或通过继承修改如果不想改源码可以在计数逻辑里对 ID 做去重和超时清理。4. 交通流量统计虚拟线圈、越线计数与去重逻辑4.1 虚拟线圈计数的实现方式流量统计最稳的做法是在画面里画一条虚拟线或一个矩形区域当跟踪目标的中心点从线的一侧移动到另一侧时计数加一。相比区域计数越线计数对方向敏感能区分进出适合路口。# counter.py基于轨迹中心点越线计数 import cv2 from ultralytics import YOLO from collections import defaultdict model YOLO(yolov8s.pt) VEHICLE_CLASSES [2, 3, 5, 7] # 虚拟线水平线 y400从左到右为正向 LINE_Y 400 count_up 0 count_down 0 # 记录每个 track_id 上一次的中心点 y 坐标 last_y {} cap cv2.VideoCapture(traffic.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results model.track(frame, conf0.35, iou0.5, classesVEHICLE_CLASSES, trackerbytetrack.yaml, persistTrue, verboseFalse) boxes results[0].boxes if boxes.id is not None: ids boxes.id.int().cpu().tolist() xywh boxes.xywh.cpu().numpy() for tid, box in zip(ids, xywh): cx, cy box[0], box[1] if tid in last_y: prev_y last_y[tid] # 从下往上穿越 if prev_y LINE_Y cy: count_up 1 # 从上往下穿越 elif prev_y LINE_Y cy: count_down 1 last_y[tid] cy # 画线和计数 cv2.line(frame, (0, LINE_Y), (frame.shape[1], LINE_Y), (0, 255, 255), 2) cv2.putText(frame, fUP: {count_up}, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.putText(frame, fDOWN: {count_down}, (20, 80), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(count, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明last_y字典按 track_id 记录上一帧中心点 y 坐标当前帧和上一帧分别位于线两侧时触发计数。prev_y LINE_Y cy表示从下往上穿prev_y LINE_Y cy表示从上往下穿。参数上LINE_Y要根据实际画面调整一般放在画面中下部避开车辆刚进入画面时框不稳定的区域。计数去重靠 track_id 唯一性同一辆车不会重复计数除非 ID 切换。4.2 ID 切换导致重复计数的补救ByteTrack 在遮挡严重时仍可能给同一辆车分配新 ID导致重复计数。常见补救是在计数前做一次轨迹去重如果新 ID 的起始位置和某个刚消失的旧 ID 的结束位置在时空上接近就认为是同一辆车合并计数。简单做法是维护一个最近消失轨迹列表记录最后位置和时间戳新轨迹出现时做 IoU 或距离匹配。# 伪代码轨迹合并思路 recent_lost [] # 元素为 (last_box, frame_idx) def is_same_vehicle(new_box, frame_idx): for last_box, lost_idx in recent_lost: if frame_idx - lost_idx 15 and iou(new_box, last_box) 0.3: return True return False这个逻辑不复杂但能明显降低重复计数率。代价是要多维护一个列表且阈值需要按帧率和车速调。4.3 车型分类统计与流量报表输出车辆类别在检测阶段已经拿到计数时按类别分别累加即可。常见做法是维护一个defaultdict(int)键为类别名值为计数。每帧结束后把当前累计写入 CSV 或数据库方便后续做分时段流量报表。import csv from collections import defaultdict class_count defaultdict(int) # 在越线计数处加一行 class_count[model.names[cls]] 1 # 定时写文件 with open(flow.csv, a, newline) as f: writer csv.writer(f) writer.writerow([timestamp, class_count[car], class_count[bus], class_count[truck], class_count[motorcycle]])参数上写文件频率不要每帧都写按秒或按分钟聚合否则 IO 会成为瓶颈。类别名用model.names取避免硬编码。5. 避坑与排查车辆追踪计数里最容易翻车的五件事5.1 现象同一辆车 ID 频繁跳变计数虚高原因ByteTrack 的match_thresh太高或者检测框抖动大IoU 匹配失败也可能是track_high_thresh设太高遮挡时高分框消失轨迹直接断。解决先把match_thresh降到 0.7 试再把track_high_thresh降到 0.4。如果还跳检查检测模型在遮挡场景的召回必要时用自训练数据补遮挡样本。另外确认persistTrue没漏。5.2 现象车辆静止等红灯时被反复计数原因虚拟线画在了停止线附近车辆中心点在线上下来回抖动触发多次穿越。解决把虚拟线移到停止线之前或之后避开车辆排队区域或者在计数逻辑里加一个最小位移阈值只有中心点位移超过若干像素才判定穿越。常见做法是要求前后帧位移大于 5 像素。5.3 现象RK3588 上跑 YOLOv8ByteTrack 帧率骤降原因RKNN 推理和 ByteTrack 的 Python 后处理都在 CPU 上跟踪匹配是逐框循环框多时耗时线性增长。解决检测导出 RKNN INT8输入固定 640跟踪部分把 IoU 计算用 numpy 向量化不要写 Python 双重循环或者把跟踪也放到 NPU 上不现实至少保证检测占大头。实测 yolov8n RKNN 加 ByteTrackRK3588 单核能到 25~30 FPS。5.4 现象夜间或逆光场景检测框大量丢失原因训练数据以白天为主模型对低照度和强逆光泛化差conf 阈值设太高暗处车辆置信度上不来。解决补夜间和逆光数据重新训练或者在推理前做一次简单的直方图均衡或 Gamma 校正。conf 可以动态调夜间降到 0.25。不要指望一个白天模型通吃全天候。5.5 现象计数结果和人工抽查对不上差几个原因视频开头和结尾的车辆可能只出现半截被计数或漏计ID 切换导致的重复计数虚拟线位置导致跨道车辆被计两次。解决统计时忽略视频首尾各若干秒加轨迹合并逻辑虚拟线按车道分别画不要一条线横跨所有车道。差几个在工程上可接受但差百分之十以上就要查 ID 切换。6. 进阶技巧用轨迹缓存和方向判定把统计做稳前面把检测、跟踪、计数的主链路跑通了但真实路口还有两个进阶问题一是车辆跨道行驶时单条虚拟线会把一辆车计两次二是需要区分左转、直行、右转的流量。这两个都能靠轨迹缓存加方向判定解决。具体做法是不急着在越线瞬间计数而是把每条轨迹的中心点序列缓存下来等轨迹消失或达到一定长度后再用整条轨迹判断它穿过了哪几条车道线、方向角变化是多少。这样跨道车辆只会被计一次而且能按方向分类。# trajectory_cache.py缓存轨迹并做后处理计数 from collections import defaultdict import numpy as np traj defaultdict(list) # track_id - [(cx, cy), ...] def update_traj(tid, cx, cy): traj[tid].append((cx, cy)) # 限制缓存长度避免内存无限增长 if len(traj[tid]) 300: traj[tid].pop(0) def classify_direction(points): # 用首尾点计算主方向角 if len(points) 10: return unknown dx points[-1][0] - points[0][0] dy points[-1][1] - points[0][1] angle np.degrees(np.arctan2(dy, dx)) if -45 angle 45: return right elif 45 angle 135: return down elif -135 angle -45: return up else: return left参数说明缓存长度 300 帧在 30 FPS 下约 10 秒足够覆盖路口通行时间方向角阈值按画面坐标系调整arctan2的 y 轴向下所以角度含义和数学坐标系相反实际用时先画几条已知方向的轨迹验证一下。轨迹消失后不要立刻删保留一段时间用于和后续新轨迹做合并判断。另一个技巧是把计数结果和检测框面积关联大框bus、truck和小框car用不同的虚拟线偏移量因为大车中心点和实际越线位置偏差更大。这个偏移量可以按类别设一个经验值比如 truck 偏移 20 像素。我自己的习惯是每换一个路口先跑一段纯检测视频把检测框画出来看稳定性再跑跟踪看 ID 保持最后才开计数。跳过前两步直接上计数翻车概率极高。这套链路不复杂但参数和场景强相关耐心调比换模型管用。希望帮到你。本文还有配套的精品资源点击获取