基于 YOLO + ByteTrack 的实时人车流量计数系统实现
前言
在很多商业和工业场景中,“目标检测”往往只是第一步。比如在景区、商场或交通路口,仅仅知道“画面里有多少人和车”是不够的,客户通常需要更精确的数据:
- “今天有多少人进入了商场,多少人离开了?”
- “这个路口从左往右开过去了多少辆车?”
要实现这种统计,我们需要把 目标检测(Object Detection) 升级为 多目标跟踪(Multi-Object Tracking, MOT) ,并在帧与帧之间记住每一个目标的身份(ID)。
本文将带你使用 YOLO (v8/v11) 与当前最前沿的 ByteTrack 跟踪算法 ,配合巧妙的几何数学计算,从零实现一个 人流与车流双向越界实时计数系统 。
一、核心技术原理
1. 为什么选择 ByteTrack?
传统的跟踪算法(如 DEEPSORT)依赖昂贵的外观特征提取网络,不仅运行慢,而且在目标被遮挡时容易丢失 ID。
ByteTrack (发表于 ECCV 2022)的核心思想非常优雅: “不放弃低置信度的检测框” 。
- 第一步(Association 1) :在高置信度检测框中与已有的跟踪轨迹进行匹配。
- 第二步(Association 2) :对于那些因为遮挡、模糊导致置信度降低的低分框,再次与上一帧的未匹配轨迹进行二次匹配。
flowchart TD
D["YOLO 检测输出框集合"] --> Split{"按置信度分数阈值分流"}
Split -->|Score >= 高阈值| D_high["高分检测框 D_high"]
Split -->|低阈值 <= Score < 高阈值| D_low["低分检测框 D_low (被遮挡/模糊)"]
T_prev["上一帧跟踪轨迹 Tracks (卡尔曼滤波预测)"] --> Match1{"第一轮匈牙利匹配 (IoU 距离)"}
D_high --> Match1
Match1 -->|匹配成功| Matched1["更新轨迹状态与卡尔曼滤波器"]
Match1 -->|轨迹未匹配| Unmatched_T["未匹配轨迹 Tracks"]
Match1 -->|框未匹配| Unmatched_D["未匹配高分框 -> 初始化为候选新轨迹"]
Unmatched_T --> Match2{"第二轮匈牙利匹配 (低分框拯救)"}
D_low --> Match2
Match2 -->|匹配成功| Matched2["保留并更新轨迹 (拯救遮挡目标)"]
Match2 -->|仍未匹配轨迹| Lost["标记为丢失轨迹 (保留 N 帧后删除)"]
classDef proc fill:#f8fafc,stroke:#3b82f6,stroke-width:2px;
class D,Split,D_high,D_low,T_prev,Match1,Matched1,Unmatched_T,Unmatched_D,Match2,Matched2,Lost proc;
这种“变废为宝”的设计,让 ByteTrack 在小目标、遮挡和运动模糊场景下拥有极强的鲁棒性,且 无需任何深度特征提取器,推理速度极快 。
2. 双向越界判定:向量叉乘法
我们要统计“跨过某条虚拟线段”的物体数量,如何通过代码精准判定?
如果使用简单的坐标大小对比,由于目标移动速度不同,很容易在跨线瞬间发生“漏检”或“重复计数”。最严谨、工业界最通用的方法是 几何向量叉乘法 。
假设我们在屏幕上绘制了一条虚拟检测线段,起点为 $A(x_A, y_A)$,终点为 $B(x_B, y_B)$。
对于物体的中心点(或足部中点),我们在前一帧的坐标记为 $P_{\text{prev}}(x_1, y_1)$,当前帧的坐标记为 $P_{\text{curr}}(x_2, y_2)$。
要判定物体的运动线段 $P_{\text{prev}}P_{\text{curr}}$ 是否穿过了检测线段 $AB$,我们需要满足两个基本条件:
- 快速排斥实验 (Bounding Box Check) :两条线段的包围盒必须相交。
- 跨立实验 (Straddle Check) :线段 $AB$ 的两个端点在线段 $P_{\text{prev}}P_{\text{curr}}$ 的两侧,且线段 $P_{\text{prev}}P_{\text{curr}}$ 的两个端点也在线段 $AB$ 的两侧。
这可以通过向量的 叉乘 (Cross Product) 高效判定。数学充要条件如下(两条线段端点互相跨立,必须同时满足):
$$(\vec{AB} \times \vec{AP_{\text{prev}}}) \cdot (\vec{AB} \times \vec{AP_{\text{curr}}}) \le 0$$
$$(\vec{P_{\text{prev}}P_{\text{curr}}} \times \vec{P_{\text{prev}}A}) \cdot (\vec{P_{\text{prev}}P_{\text{curr}}} \times \vec{P_{\text{prev}}B}) \le 0$$
当叉乘结果发生符号翻转时,即说明物体“跨越”了线段;通过叉乘的具体正负符号,即可进一步判定物体是顺向进入还是逆向离开。
二、开发环境搭建
只需安装以下三个 Python 核心库即可开始:
1 | pip install ultralytics opencv-python numpy |
注意 :
ultralytics库中已经内置封装了 ByteTrack 跟踪器,我们不需要下载额外的 ByteTrack 代码仓库,这极大简化了工程部署难度。
三、完整实现源码
新建文件 object_counting.py,粘贴以下经过工业级优化的完整代码。代码中包含了平滑轨迹绘制、动态双向计数和优雅的 OpenCV 半透明看板展示:
1 | import cv2 |
四、核心代码精讲
1. model.track 究竟做了什么?
在上一篇文章中,我们使用的是普通的检测方法 model(frame)。在这段代码里,我们将其换成了支持持续跟踪的 model.track() 方法。
1 | results = model.track(source=frame, persist=True, tracker="bytetrack.yaml") |
persist=True:这是激活跟踪器的关键。它告诉 YOLO 在处理当前帧时,要参考上一帧的跟踪状态,并确保同一个物体的box.id保持不变。tracker="bytetrack.yaml":Ultralytics 内置了两种跟踪器,即"bytetrack.yaml"和"botsort.yaml"。相比之下,ByteTrack 速度更快,适合实时性要求高、算力相对有限的场景。
2. 为什么选择足部中心点 cy = int(y2) 而不是几何中心?
对于人流和车流,它们都是在地面上移动的。由于相机的透视投影关系,物体的上半身容易在跨线前发生倾斜,从而导致越界判定不准。
因此,选用 检测框底边中点(足部/轮胎贴地位置) 作为基准点,其跨线判定的物理位置最为准确。
3. 如何解决“同个 ID 往返跨线”的情况?
代码中使用了一个 counted_ids 集合进行去重:
1 | if ... and track_id not in counted_ids: |
这是一种 单次越界去重 机制。在一些更为复杂的场景中,若需要允许同一个目标在往返跨越时多次计数,可以改为记录目标的“前侧状态”(例如用一个字典记录 {track_id: "IN_SIDE"})。一旦检测到物体的状态从 IN_SIDE 变为 OUT_SIDE,则触发一次计数并更新状态,从而支持往返多次累计计数。
五、工业部署调优与提速策略
多线程读取与跳帧机制
对于高分辨率摄像头,可以使用多线程方式读取视频流,避免主推理线程因为 OpenCV 的read()发生 I/O 阻塞。同时,在保证精度的前提下采用 隔帧检测 (例如每 2 帧调用一次 YOLO 推理,其余帧采用快速光流或在上一帧的跟踪位置上预测),可大幅将 FPS 提高 50% 以上。合理控制置信度与早停阈值
ByteTrack 的强大在于匹配。但如果视频中背景极为嘈杂,过低的第一阶段阈值会引入过多负样本噪声。在model.track()中,建议将第一层过滤置信度设为0.3~0.4之间。多摄像头并行与服务器端集成
在企业落地时,可通过multiprocessing为每个摄像头启动一个独立的 Python 推理进程,最终通过 Redis 消息队列或 RabbitMQ 将计数数据实时写入后端的 PostgreSQL 数据库或报警后台。
结语
通过 YOLO + ByteTrack + 几何叉乘判定,我们只用了 100 多行代码就构建起了一套逻辑严密、抗遮挡、不惧漏检的高性能计数系统。
下一篇文章中,我们将带大家把 YOLO 塞入更宏大的工业级安防场景—— 结合 GB28181 和 JT1078 国标监控协议的视频流,实现多路高并发实时 AI 安全告警系统 ,敬请期待!