☰
基于YOLOv8与ByteTrack的实时车辆检测追踪与计数实战解析
2026/10/10 14:15:49 网站建设 项目流程

简介:基于YOLOv8与ByteTrack的实时车辆检测追踪计数系统,适用于智能交通监控、城市道路车流统计、高速公路车流分析、停车场车辆管理、十字路口信号优化及智能安防等场景,面向需要车辆多目标检测与持续追踪的开发者、研究者和交通管理人员。该系统将YOLOv8的快速准确目标检测与ByteTrack的鲁棒跟踪相结合,可应对遮挡、复杂背景下的车辆识别与跨帧追踪,并自动完成流量计数。压缩包共含21个文件,其中8个Python脚本承载目标检测、卡尔曼滤波、数据匹配与主流程逻辑,YAML配置文件定义模型与跟踪参数,TXT与MD文档提供运行说明,MP4演示视频直观展示效果,另附DOCX扩展资料。整套资源约217KB,代码结构清晰,便于快速部署与二次开发。目前已有102人学习下载,适合希望掌握深度学习目标检测与多目标跟踪技术、快速搭建车流计数系统的入门及进阶用户。

1. 实时车辆检测追踪计数:YOLOv8负责“看见”,ByteTrack负责“认人”

做智能交通项目的人大概都遇到过这个尴尬:摄像头里一辆车好好地开着,结果系统数出了两辆。原因很简单——检测器只负责告诉你“这一帧哪里有车”,它根本不知道上一帧那辆车和这一帧这辆车是不是同一个目标。YOLOv8这类目标检测模型天生没有“记忆”,而车辆流量统计、十字路口车流分析、停车场进出管理这类任务,恰恰需要把每一帧的检测框串成一条完整的轨迹,再按轨迹去计数。这套基于YOLOv8和ByteTrack的实时车辆检测追踪计数系统,就是把“检测”和“多目标跟踪”两段能力拼成一条完整流水线:YOLOv8做深度学习目标检测,ByteTrack做跨帧数据关联,最终输出每辆车的运动轨迹和通行计数。适合正在做交通监控、车流统计相关项目,或者已经跑通了检测模型、想补上跟踪这块短板的工程师和毕设开发者。

2. 项目结构拆解:从main.py到bytetrack包,一条检测链的六份核心文件

2.1 打开压缩包先看哪些文件

拿到资源包后,别急着跑,先把目录结构过一遍。这套代码的结构很干净,核心代码全部在根目录和bytetrack子包里,资产文件单独放在assets目录下。我拆解过不少开源跟踪项目,ByteTrack标准的工程实现通常包含卡尔曼滤波、匹配、轨迹管理三块,这个包的划分恰好是教科书式的。

文件路径职责备注
main.py程序入口,读取视频流并驱动主循环改视频路径、选模型都在这里
object_tracking.py封装层,把YOLOv8检测和ByteTrack跟踪粘在一起想换检测器只需要改这个文件
bytetrack/byte_track.pyByteTrack核心类,管理轨迹的创建、更新、删除二次匹配的核心逻辑在这里
bytetrack/kalman_filter.py卡尔曼滤波器,预测目标下一帧位置8维状态量的匀速运动模型
bytetrack/matching.pyIoU距离计算与匈牙利匹配决定检测框和轨迹的配对关系
bytetrack/basetrack.py轨迹基类,定义Track对象的基本数据结构每个轨迹的ID、状态、帧计数都在这里初始化
cfg/requirements.txtPython依赖清单装环境用这一份就够了

第一次接触这类项目的人最容易犯的错是盯着main.py猛看,其实main.py里几乎不含算法逻辑,它就是读帧、调用object_tracking.py、画框、显示结果。真正决定跟踪效果的是bytetrack包里的那几百行。

2.2 一帧画面从进来到出去,中间发生了什么

这个项目的数据流可以用一条主循环串起来。以main.py的骨架为例:

# main.py 主循环骨架(路径和解码部分略) import cv2 from object_tracking import ObjectTracking if __name__ == "__main__": video_path = "assets/video/demo.mp4" tracker_app = ObjectTracking(video_path) # 初始化检测器+跟踪器 while True: frame = tracker_app.get_frame() if frame is None: break results = tracker_app.update(frame) # 检测 -> 跟踪 -> 计数全在这里 tracker_app.draw(frame, results) # 画框、画轨迹、叠加计数文本 cv2.imshow("vehicle tracking", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break tracker_app.release()

这段代码的逻辑顺序值得说清楚。ObjectTracking类在初始化时就加载了YOLOv8权重和ByteTrack实例,get_frame()只是从视频里取下一帧,真正的重头戏在update()里:先让YOLOv8跑一次前向推理拿到这一帧的检测框,再把检测框列表交给ByteTrack的update()方法,ByteTrack内部会完成卡尔曼预测、IoU匹配、轨迹更新,最后返回带ID的轨迹列表。draw()负责把结果可视化出来。

注意waitKey(1)后面的ord("q"),这是OpenCV窗口的退出键位。很多新手把视频跑起来之后不知道怎么正常退出,直接关窗口导致进程卡死,就是这个细节没注意。

2.3 检测器和跟踪器解耦,是这个项目最值得抄的地方

object_tracking.py作为中间层,把YOLOv8和ByteTrack彻底分隔开了。检测器输出的是“这一帧有哪些目标,框在哪”,跟踪器接收的是“一批检测框,输出带ID的轨迹”。这套解耦设计意味着你可以把YOLOv8换成YOLOX、RT-DETR,只要保持输出格式是[x1, y1, x2, y2, score, class_id]的列表,ByteTrack完全无感。反过来,把ByteTrack换成DeepSORT也只是动object_tracking.py内部的事。

这种设计在实际项目中非常实用。我接过一个停车场项目,甲方指定的检测模型是训练好的YOLOv5s,当时就是只改了几行数据格式转换,就把跟踪模块原封不动接上了。做毕设的同学答辩时能讲清楚这一层解耦逻辑,比背一遍YOLOv8网络结构得分高得多。

3. ByteTrack源码逐段读:卡尔曼滤波和二次匹配是轨迹连续性的命门

3.1 kalman_filter.py:八维状态量如何预测下一帧位置

很多教程把卡尔曼滤波讲成一团黑匣子,其实在ByteTrack里它做的事情非常具体:用一个匀速运动模型,预测“当前这个轨迹在下一帧应该出现在哪里”。先看初始化部分:

# bytetrack/kalman_filter.py 核心结构 import numpy as np class KalmanFilterXYAH: def __init__(self): self._dt = 1.0 self._std_weight_pos = 1.0 / 20 self._std_weight_vel = 1.0 / 160 # 状态向量:x, y, a, h, vx, vy, va, vh # 前4维是位置,后4维是速度 self._motion_mat = np.eye(8, 8) for i in range(4): self._motion_mat[i, i + 4] = self._dt

这8个状态量分别是中心点x坐标、中心点y坐标、宽高比a、高度h,外加对应的四个速度分量。跟常见的SORT实现不一样,ByteTrack用的不是x, y, w, h,而是x, y, a, h。原因很简单:车辆在行驶中宽度变化不如高度变化稳定,而宽高比在绝大多数视角下是相对恒定的,用它做运动模型的状态量,预测误差更小。

predict和update两个方法才是卡尔曼滤波的核心:

def predict(self, mean, covariance): # mean是上一帧的均值向量,covariance是协方差矩阵 # 用运动矩阵做状态传播,位置和速度一起外推 std_pos = self._std_weight_pos * mean[3] std_vel = self._std_weight_vel * mean[3] motion_cov = np.diag(np.square(np.r_[std_pos, std_pos, std_pos, std_pos, std_vel, std_vel, std_vel, std_vel])) mean = np.dot(self._motion_mat, mean) covariance = np.linalg.multi_dot((self._motion_mat, covariance, self._motion_mat.T)) return mean, covariance + motion_cov def update(self, mean, covariance, measurement): # measurement是检测框的x, y, a, h,用来修正预测结果 # 计算卡尔曼增益,融合预测值和观测值 projected_mean, projected_cov = self.project(mean, covariance) chol_factor, lower = scipy.linalg.cho_factor(projected_cov, lower=True) kalman_gain = scipy.linalg.cho_solve((chol_factor, lower), np.dot(covariance, self._projection_mat.T).T).T innovation = measurement - projected_mean new_mean = mean + np.dot(innovation, kalman_gain.T) new_covariance = covariance - np.linalg.multi_dot((kalman_gain, projected_cov, kalman_gain.T)) return new_mean, new_covariance

代码里有一个值得注意的细节:std_pos = self._std_weight_pos * mean[3],也就是说过程噪声的强度是和目标高度挂钩的。目标越大,位置预测的不确定性越高,给运动模型的噪声也就越大。这是ByteTrack对车辆场景的一个隐式适配——大车(卡车、公交车)在画面中的位移幅度更大,用固定噪声反而会让预测偏保守。

参数说明:_std_weight_pos控制位置噪声,值越大预测越“发散”,轨迹越容易被新检测框拉走;_std_weight_vel控制速度噪声,值越大越容易跟丢。代码里默认的1/20和1/160是原作者调好的经验值,没有特别强的理由不建议先动它。

3.2 matching.py:IoU距离和匈牙利算法如何做配对

卡尔曼滤波预测出“轨迹应该在的位置”后,需要用这一帧的检测框去跟预测位置做匹配。ByteTrack在这里用的是IoU距离加匈牙利算法。IoU距离的定义是1 - IoU,交并比越高,距离越接近0,匹配越可靠。

# bytetrack/matching.py 关键逻辑 from scipy.optimize import linear_sum_assignment def iou_batch(bboxes1, bboxes2): # 输入是两组xyxy坐标框,输出是两两之间的IoU矩阵 bboxes2 = np.expand_dims(bboxes2, 0) bboxes1 = np.expand_dims(bboxes1, 1) xx1 = np.maximum(bboxes1[..., 0], bboxes2[..., 0]) yy1 = np.maximum(bboxes1[..., 1], bboxes2[..., 1]) xx2 = np.minimum(bboxes1[..., 2], bboxes2[..., 2]) yy2 = np.minimum(bboxes1[..., 3], bboxes2[..., 3]) w = np.maximum(0.0, xx2 - xx1) h = np.maximum(0.0, yy2 - yy1) wh = w * h area1 = (bboxes1[..., 2] - bboxes1[..., 0]) * (bboxes1[..., 3] - bboxes1[..., 1]) area2 = (bboxes2[..., 2] - bboxes2[..., 0]) * (bboxes2[..., 3] - bboxes2[..., 1]) union = area1 + area2 - wh return wh / union def linear_assignment(cost_matrix): # 匈牙利算法求全局最优匹配,返回匹配对、未匹配行、未匹配列 row_idx, col_idx = linear_sum_assignment(cost_matrix) matches = np.stack((row_idx, col_idx), axis=1) # 过滤掉代价大于阈值的“假匹配” return matches, unmatched_rows, unmatched_cols

匈牙利算法解决的是“全局最优”问题:当有多个轨迹和多个检测框时,不能只看单独一对的IoU,而是要找出整体代价最小的配对方案。这里有一个容易踩的坑:linear_sum_assignment默认会把所有行都匹配出去,即使某个匹配的代价非常大。所以工程上一定要在匹配后加一步——把IoU距离大于阈值(比如0.8,即IoU低于0.2)的配对全部丢弃,否则会出现轨迹和相距十万八千里的检测框硬凑成一对的情况。

3.3 byte_track.py:高分数框先匹配,低分数框再“捡漏”

ByteTrack这个名字里的“Byte”代表的是它对低置信度检测框的态度。其他跟踪算法用阈值过滤掉低分框,ByteTrack反而把低分框保留下来,做二次匹配。这个设计针对的是遮挡场景——车辆被路牌挡住一两帧时,检测器给出的分数会骤降,但仍然会产生一个位置大致正确的框。如果把低分框直接丢弃,轨迹只能靠卡尔曼预测硬撑,几帧之后就会跟丢。

# bytetrack/byte_track.py 二次匹配核心流程(简化) class ByteTrack: def update(self, output_results, img_info): # output_results: 检测框 [x1, y1, x2, y2, score, class_id] # 按分数拆成两组:高分框和低分框 high_score = output_results[output_results[:, 4] > self.track_thresh] # 默认0.5 low_score = output_results[output_results[:, 4] <= self.track_thresh] # 第一步:所有轨迹先跟高分框做一次匹配 matched_high, unmatched_track, unmatched_high = \ self.match(moved_tracks, high_score) # 第二步:还没配上对的轨迹,拿低分框再来一轮 matched_low, unmatched_track, unmatched_low = \ self.match(unmatched_track, low_score) # 第三步:处理三条漏网之鱼 # 1. 完全没配上对的轨迹:标记为丢失,超过max_time_lost就删除 # 2. 完全没配上对的高分框:当作新目标,创建新轨迹 # 3. 剩下的低分框:丢弃,不产生新轨迹

这段逻辑是整个项目最精华的部分。track_thresh默认0.5并不算高,车辆在正常光照下的检测置信度通常能到0.8以上,0.5这个阈值卡的是“稍微有点遮挡”和“完全看不清”的边界。低分框参与匹配的前提是它和高分框共享同一批轨迹——先让高分框把轨迹都“占住”,剩下的空位才轮到低分框,这样既保住了遮挡目标的ID,又防止低分框乱抢轨迹。

max_time_lost这个参数决定了轨迹在丢失目标后还能存活多少帧。取值太小车流一断就丢ID,取值太大又会让画面里出现大量“幽灵轨迹”。车辆场景一般取30到60之间比较稳,我后面在避坑章节会专门讲这个参数的调法。

4. 跑通与调优:环境、配置参数与替换自己的数据集

4.1 环境搭建注意事项

这套代码的依赖不多,requirements.txt里列了几项核心包:opencv-python负责图像读写和绘制,numpy处理矩阵运算,scipy提供匈牙利算法,ultralytics提供YOLOv8推理,还有一个lap库用于线性分配加速。安装时有一个隐藏的坑是ultralytics和torch的版本匹配问题,YOLOv8的推理依赖PyTorch,而PyTorch的CUDA版本必须和显卡驱动匹配。

# 创建独立虚拟环境并安装依赖 conda create -n vehicle_track python=3.9 -y conda activate vehicle_track # 如果只需要CPU推理,一行命令搞定 pip install -r requirements.txt # 如果需要GPU加速,先单独装匹配CUDA版本的torch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt

逻辑说明:第一条命令创建干净环境是为了隔离依赖,防止和别的项目出现包冲突。--index-url指定PyTorch的CUDA 11.8版本下载源,是当前兼容性最好的组合之一。装完依赖后,第一次跑demo先不要急着动参数,直接用assets/video目录下的示例视频验证流程能走通。

注意lap这个包在Windows上有时会编译失败,遇到这种情况换个Python 3.9版本通常能解决,或者用源码编译。实测数据是:用GTX 1660Ti跑720p视频,YOLOv8s加ByteTrack大概能保持25到35FPS,这个帧率做实时监控勉强够用;如果换成CPU,基本只有个位数FPS。

4.2 核心参数逐项拆解:conf、track_thresh、match_thresh、max_time_lost

这个项目里有几个容易混淆的阈值参数,我见过不少人把YOLOv8的conf当成ByteTrack的track_thresh调了半天发现没效果。它们分别作用在两个阶段,必须分开理解。

参数常见默认值作用阶段调参建议
conf0.25YOLOv8检测器控制检测框的生成门槛;调低了会多出大量误检,调高了会漏掉远处小车
track_thresh0.5ByteTrack分档高于此值算高分框,优先匹配;取0.4~0.6之间做遮挡场景微调
match_thresh0.8ByteTrack匹配IoU距离阈值,低于该值才接受匹配;取0.7~0.9,值越小匹配越严格
max_time_lost30轨迹生命周期目标消失后保留轨迹的帧数;高速路取30,城市拥堵路况建议调到60
fps视频帧率卡尔曼更新频率不一致会导致轨迹位置偏移,务必和视频实际帧率一致

提示:conf和track_thresh是两个阶段独立的参数。conf管“YOLOv8是否输出这个框”,track_thresh管“ByteTrack是否信任这个框”。遮挡严重时优先调低track_thresh而不是conf,因为低分框本来就是ByteTrack设计用来处理遮挡的。

还有一个容易被忽略的参数是min_box_area,ByteTrack默认会过滤掉面积太小的检测框,防止把树叶晃动、远处行人等小目标也跟踪起来。在车辆场景下,如果摄像头架得高,远处车辆在画面里可能只有几十个像素,这时候需要把min_box_area调小甚至关掉,否则车流在远端会被“吞掉”。

4.3 换成自己的视频和自有模型

demo跑通之后,第二步就是把示例视频换成自己的素材。常见做法是直接在main.py里改video_path参数,或者加一个命令行入口。示例如下:

# main.py 中支持命令行传入视频路径和模型权重 import argparse parser = argparse.ArgumentParser() parser.add_argument("--video", type=str, default="assets/video/demo.mp4") parser.add_argument("--weights", type=str, default="yolov8s.pt") parser.add_argument("--classes", nargs="+", type=int, default=[2, 5, 7]) args = parser.parse_args()

classes参数值得单独解释一下。COCO数据集中2代表car,5代表bus,7代表truck。只跟踪这三类就可以过滤掉行人、自行车、摩托车等干扰目标。如果做高速公路车流分析,把摩托车(class 3)也加进去;做停车场管理,通常只需要car这一类比。用大白话说,这个参数决定了“哪些东西能参与计数”。

如果你手里有自己训练的车辆检测权重,比如用YOLOv8在BDD100K车辆检测数据集上微调过的模型,直接把weights路径换成best.pt就行。前提是检测输出格式保持[x1, y1, x2, y2, score, class_id]不变,并且class_id的语义要和过滤逻辑对应上。很多人在这一步翻车:自己训练的模型类别索引和COCO不一样,结果把公交车当成了小汽车参与计数,完全乱了套。

5. 实测避坑:车辆检测追踪最常见的五个翻车点

5.1 同一辆车被数成两辆,车流量虚高

现象:视频里一辆车稳定直行,但计数结果从1跳到3,之后再跳回2,最终统计数字明显多于实际车辆数。

原因:车辆在行驶中被遮挡或者检测置信度波动,导致ByteTrack判定轨迹丢失并创建了新轨迹。旧轨迹的ID在max_time_lost超时后被删除,新轨迹拿到一个新ID,计数逻辑把两个ID当成两辆车。这是所有跟踪计数系统最典型的ID Switch问题。

解决:先把max_time_lost从默认的30调大到60,给轨迹更长的存活时间。然后检查计数逻辑,如果是在update回调里对每个新ID都加计数,改成“当轨迹第一次出现时计数,ID变化时做位置连续性校验”。我一般会在轨迹创建时记录初始位置,如果新轨迹和刚删除的旧轨迹中心距离小于一个车身长度,就直接继承旧ID而非新建ID。

5.2 检测框在路口抖动,轨迹画成锯齿

现象:车停在路口等红灯时,检测框忽大忽小,画面上的轨迹线来回抖动,计数结果在边界处反复横跳。

原因:YOLOv8的检测框本身有微小抖动,每帧的置信度在阈值附近波动,导致NMS筛选出的框不稳定。ByteTrack的卡尔曼滤波会做位置预测,但它紧跟检测结果,检测框抖动直接传导到轨迹上。

解决:在object_tracking.py里对输出的框做平滑,我常用EMA指数移动平均,权重取0.5左右。另一个更省事的方案是降低帧率对抖动的影响:把主循环改成每2帧推理一次,中间帧用卡尔曼预测补齐。实测这两种方法配合,轨迹抖动能减少80%以上。

5.3 CPU环境只有2~3 FPS,完全没法实时

现象:代码在笔记本上跑demo视频,画面一顿一顿的,帧率不到个位数,计数结果也明显滞后。

原因:YOLOv8s在CPU上的推理速度大约在200到500ms一帧,加上ByteTrack的Python循环,帧率上不去很正常。这不是代码问题,是硬件瓶颈。

解决:先换更小的模型,把yolov8s.pt换成yolov8n.pt,推理速度能提升3倍左右;然后把推理分辨率从640降低到416或者320。如果监控摄像头多路接入,最靠谱的方案是显卡推理,GTX 1660Ti这一档就能把YOLOv8s跑到30FPS以上,再高分辨率建议上TensorRT加速,我在第6章会写具体做法。

5.4 卡车和公交车被拆成多个框,一个目标占了两三条轨迹

现象:画面里一辆大挂车驶过,屏幕上同时出现两个框分别锁定车头和车身,计数数量在这个瞬间增加了2到3。

原因:YOLOv8对大目标的检测框有时会分裂成两部分,NMS阈值(IoU参数)设置得太严,两个高置信度的重叠框没有被合并,ByteTrack就把它们当成了两个独立目标。

解决:把YOLOv8 NMS的IoU阈值从默认的0.45调到0.3,让重叠框更容易被合并。另外在ByteTrack匹配阶段加入框面积约束:新轨迹和现有轨迹的框面积比例超过3倍或者小于1/3时,不建立匹配关系,这样卡车即使被检出两个框,也不会同时存活。

5.5 摄像头角度太斜,远处车辆直接消失

现象:高速公路场景下,画面顶部的远处车道经常没车,到了画面中部突然冒出一辆车开始被跟踪,好像车是从天上掉下来的。

原因:检测器对远距离小目标的召回率本身就低,加上摄像头仰角大时车辆在画面中像素太少,YOLOv8在默认分辨率下根本识别不出来。

解决:第一优先级是把推理分辨率从640提到960或1280,小目标检测效果差距非常明显;第二优先级是给画面顶部区域单独裁剪放大,把这段区域作为独立ROI送进检测器再融合结果;第三优先级是摄像头安装时尽量压低角度,让车辆在画面中占据更大像素面积。这三点按顺序做,远端的检测率能提升一大截。

6. 进阶验证与优化:用计数精度和FPS两个指标收口

6.1 用虚拟检测线验证计数准确性

数ID不是唯一的计数方式,更可靠的做法是在画面里画一条虚拟检测线,按轨迹穿越方向计数。这样做的好处是:每个目标只在穿越线的那一刻计数一次,天然去重,不依赖轨迹ID是否稳定。

# 计数逻辑:轨迹中心点穿越检测线时累加 line_y = int(frame.shape[0] * 0.6) # 检测线放在画面60%高度处 for track in active_tracks: cx, cy = track.center if track.last_cy < line_y <= cy: # 从上往下穿线 count_down += 1 track.ever_counted = True elif track.last_cy > line_y >= cy: # 从下往上穿线 count_up += 1 track.ever_counted = True track.last_cy = cy

验证方法是抽一段30秒视频,人工数一遍真实车辆数,再和系统输出对比。如果偏差超过5%,优先排查检测漏检而不是跟踪参数。这个习惯比在参数上反复赌运气靠谱得多。

6.2 推理优化:ONNX导出与边缘设备部署

把YOLOv8导出成ONNX再转TensorRT,是当前最实用的提速手段。用yolo export model=yolov8s.pt format=onnx完成导出,再通过TensorRT生成engine文件,在支持TensorRT的显卡上推理延迟能降到原来的一半以下。如果想上RK3588这类边缘计算盒子,路线是导出ONNX后再转成RKNN格式,配合NPU推理,功耗和性能兼顾。这套代码里的跟踪部分纯CPU计算,移植到边缘设备不需要改跟踪逻辑,只替换检测器推理后端即可。

6.3 计数结果的工程闭环

做交通项目最终要输出报表,建议在main.py里把每辆车的轨迹首帧时间、尾帧时间、ID、穿越方向落成CSV。这样回放时能按时间段聚合车流量,也能定位“哪一分钟计数异常”。从那以后我每次换视频源,第一件事就是先跑前30帧盯ID切换次数,如果一帧内ID变化超过阈值,就回头查detect置信度和max_time_lost,而不是直接调跟踪参数。这套检查习惯比任何参数抄作业都管用,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询