☰
YOLOv8+ByteTrack车辆检测与流量统计实战:从训练到部署
2026/9/27 1:49:03 网站建设 项目流程

简介:一套面向交通监控场景的车辆多目标检测、追踪与流量统计系统实现资料,基于YOLOv8与ByteTrack算法,适合计算机视觉学习者、智能交通项目开发者参考。内容整合深度学习检测与多目标跟踪,能完成视频中车辆的精准识别、连续追踪、数量统计以及违章停车等行为分析。资源共12个文件,以Python脚本、zip/zbak备份包、png预览图、mp4演示视频和md说明文档为主,压缩包约70.72MB,结构便于按代码、权重备份、演示素材分类查阅。其中py文件承担检测追踪主流程,md文件提供使用说明;配套素材含输入输出视频与界面截图,可对照验证效果,已有69人学习下载。通过主线代码、视频演示与文件说明,可快速搭建环境并复现检测追踪流程,理解ByteTrack轨迹关联机制与流量统计实现思路,为后续扩展或二次开发提供起点。

1. 这套车辆检测追踪系统到底在解决什么:YOLOv8+ByteTrack的组合逻辑

把摄像头架在路口或园区门口,你真正想要的不是“画面里有没有车”,而是“每一辆车从哪边来、往哪边去、这段路上总共有多少辆”。YOLOv8负责的是单帧检测——这一刻画面里每辆车的位置和类别;ByteTrack负责的是跨帧关联——上一秒的这辆车和下一秒的这辆车是不是同一辆。两者串起来,才能算出稳定的车辆ID,进而做断面流量统计。很多人一开始只跑检测模型,发现计数结果一塌胡涂,根因就是没有追踪层:只要车被遮挡两帧,再出现就被当成新车,重复计数直接翻倍。适合做这系统的人,是手头有监控视频或本地摄像头、想输出车流量曲线或做车辆轨迹分析的从业者,也包括拿这个方向做毕设的学生。反直觉的一点是:检测精度高并不等于追踪稳,实际项目里拖后腿的往往是漏检和ID频繁切换,而不是模型mAP不够。

2. 从选型到喂数据:YOLOv8凭什么适合车辆检测,数据集该怎么处理

2.1 YOLOv8的网络结构与车辆场景的选型理由

YOLOv8的骨干是C2f结构,把v5的C3做了更细的梯度流分支,特征复用能力更强;检测头改成了Anchor-Free解耦头,分类和回归分支分开输出。对车辆场景来说,两个特性很关键:一是Anchor-Free对小目标的定位更稳,像远处路口那种只有二三十像素宽的车,不再依赖手动调的锚框;二是解耦头让分类和回归的梯度互不干扰,夜间、雨天的低对比度样本不容易出现“框得准但类别错”的情况。

选哪个体量也要提前定。n/s版本在CPU上勉强能实时,m/l版本适合有独立显卡的机器,x版本追求极致精度但推理速度掉得明显。我做路口统计场景,一般用m版本起步:车辆是大目标,不需要x的容量,m的召回率在遮挡场景下比s高一截,推理速度在GTX 1660 Ti上也能跑到30ms左右。如果后续要部署到RK3588这类嵌入式平台,再蒸馏回n或s版本,不要一开始就盯着最大模型调参。

2.2 用labelme标注车辆数据并转成YOLO格式:转换脚本与四个边界坑

标注车辆数据集,常见做法是labelme或labelImg。车辆目标轮廓不规则,尤其公交车、卡车带挂厢,labelme的多边形标注比矩形更贴合,转出来的框也更干净。但YOLO训练需要的是归一化的中心点坐标和宽高,格式不兼容,必须写转换脚本。

import json import os import glob from PIL import Image def labelme_to_yolo(json_path, save_dir, class_map): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_path = data['imagePath'] img = Image.open(os.path.join(os.path.dirname(json_path), img_path)) img_w, img_h = img.size txt_name = os.path.splitext(os.path.basename(json_path))[0] + '.txt' out_lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_map: continue cls_id = class_map[label] points = shape['points'] xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h box_w = (x_max - x_min) / img_w box_h = (y_max - y_min) / img_h # 过滤极端小框和越界框,这部分是教训 if box_w < 1e-3 or box_h < 1e-3: continue x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_w = min(max(box_w, 0.0), 1.0) box_h = min(max(box_h, 0.0), 1.0) out_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(os.path.join(save_dir, txt_name), 'w') as f: f.write('\n'.join(out_lines)) class_map = {'car': 0, 'bus': 1, 'truck': 2} for jf in glob.glob('labelme_jsons/*.json'): labelme_to_yolo(jf, 'yolo_labels/', class_map)

这段脚本的逻辑是把labelme的JSON里每个多边形取外接矩形,再归一化成YOLO格式。四个边界坑值得说明:一是labelme的imagePath是相对路径,如果JSON和图片不在同一目录,读图会失败,建议脚本里用JSON文件所在目录拼接;二是多边形的点顺序不影响外接矩形结果,但如果有自交多边形,外接矩形会偏大,标注时尽量按顺时针或逆时针画;三是类别映射表里如果有背景类或ignore类,要在脚本里过滤掉,否则训练时背景类会污染损失;四是极端的细长框,比如远处只露出半个车头,宽高比超过10:1,这类样本建议直接删掉,模型学到的是“一条线”而不是“一辆车”。

2.3 处理数据集用于YOLOv8训练:目录划分、样本清洗与类别不平衡

处理数据集用于YOLOv8训练,核心是三件事:目录结构、样本划分、质量清洗。目录结构按照ultralytics的约定来,images和labels两个根目录,下面再分train和val。划分比例我一般用9:1或8:2,车流视频是连续帧,如果按时间顺序直接切,train和val会包含同一辆车的相邻帧,val精度虚高。正确做法是每隔10帧抽一帧组成候选集,再随机打乱后划分,这样能保证验证集和训练集的目标不完全重叠。

样本清洗往往被忽略。车流视频里大量帧是“没有车”的空路面,这类帧不是完全没有价值,但占比超过30%会让模型偏向预测背景。我一般会把空帧比例压到10%以内,并且刻意保留一部分“只有半辆车”的截断样本——路口场景里车被栏杆或绿化带挡一半是常态,全删掉会让模型在真实场景漏检。类别不平衡方面,小轿车数量远多于公交车和卡车,如果直接用原始分布训练,bus和truck的召回率会很难看。常见做法是每一类的帧数都采样到接近同一个量级,bus少就多抽几帧,或对bus做简单的左右翻转和亮度扰动扩充。

3. 训练自己的车辆检测模型:环境搭建、参数含义与损失曲线排查

3.1 在Ubuntu 20.04上把YOLOv8环境跑通:CPU版本也能先验证

很多人在环境这一步就卡住,尤其手里只有CPU机器。其实ultralytics包在CPU上完全能跑训练和推理,只是慢。先建conda环境,装PyTorch CPU版和ultralytics:

conda create -n yolov8 python=3.9 -y conda activate yolov8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics

装完后先别急着训练,用预训练权重验证环境通了没有。这一步非常关键,能区分“环境问题”和“代码问题”。下载yolov8n.pt或yolov8m.pt权重,对一张车辆图做推理:

from ultralytics import YOLO model = YOLO('yolov8n.pt') results = model.predict('test_car.jpg', conf=0.25, save=True) for r in results: boxes = r.boxes print('检测到目标数:', len(boxes))

CPU版本跑推理一张图可能要几百毫秒,这正常,不需要焦虑。如果这一步输出目标数为0,优先检查图片路径和conf阈值,不是模型问题。环境验证通过后,再进入训练阶段。

3.2 yolov8模型训练参数含义:epochs、batch、imgsz、patience、device怎么设

训练参数里最容易翻车的三个是imgsz、batch和patience。imgsz默认640,但如果你的视频源是1080p,路口远处的车在原图上可能只有30像素,缩到640后更小。我一般先用640训练一轮,再用832或960微调几十个epoch,小目标召回率能涨2~3个点。batch大小取决于显存,GTX 1660 Ti 6G跑yolov8m只能用batch=8,跑s可以用16。patience是早停轮数,设20到30比较合理,太小会在loss还没收敛时就停。

yolo train \ model=yolov8m.pt \ data=vehicle.yaml \ epochs=150 \ imgsz=640 \ batch=8 \ device=0 \ patience=20 \ lr0=0.01 \ lrf=0.01 \ project=vehicle_training \ name=exp1

这段命令里,data指向你自己写的vehicle.yaml,里面写train/val路径和类别列表。model=yolov8m.pt表示在COCO预训练权重上微调,比从随机初始化快很多。lr0是初始学习率,0.01是默认值,如果发现loss震荡,降到0.005重跑。lrf是最终学习率系数,0.01表示训练结束时学习率降到初始值的百分之一。device=0指第一块显卡,CPU机器改成device=cpu,但训练时间会拉长到十倍以上。还有两个参数建议加上:close_mosaic=10,表示最后10个epoch关闭马赛克增强,让模型在真实分布上收敛;cos_lr=True用余弦学习率,比阶梯下降更平滑。

跑起来之后,训练日志会每个epoch打印一次box loss、cls loss、dfl loss和验证集mAP。我第一次跑的时候只看mAP,结果mAP到0.85就停了,回调loss曲线才发现过拟合。所以要看全:如果训练loss持续下降但验证loss在某个epoch后反弹,就是过拟合,把epochs减到反弹点附近,或加大mosaic概率。

3.3 画损失函数曲线图:从results.csv里找出训练异常

ultralytics每次训练会在项目目录下保存results.csv,里面每个epoch的train loss、val loss、mAP都有。用pandas读出来画两条曲线,是排查训练问题最直接的手段。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('vehicle_training/exp1/results.csv') df.columns = [c.strip() for c in df.columns] # 去掉列名多余空格 plt.figure(figsize=(10, 5)) plt.plot(df['epoch'], df['train/box_loss'], label='train box loss') plt.plot(df['epoch'], df['val/box_loss'], label='val box loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.grid(True) plt.savefig('loss_curve.png', dpi=150)

看到三个典型形态要注意:一是train loss降但val loss不降,说明过拟合,优先加数据增强或提前早停;二是两条loss都在降但波动剧烈,说明学习率偏高,下个run把lr0降到0.003;三是mAP在15个epoch内冲到0.9以上,先别高兴,大概率是训练集和验证集划分不当,存在相似帧泄露,回到第2章按抽帧方式重新划分。损失曲线是黑匣子的窥视窗,不看曲线就调参等于盲调,这是我的血泪经验。

4. 接上ByteTrack追踪器:多目标追踪的关联逻辑与参数调优

4.1 ByteTrack为什么适合车流场景:低分框再利用与两种关联机制

ByteTrack的思路和传统SORT最大的区别,是把检测框分成两类:高分框和低分框。高分框直接和已有轨迹做IOU匹配;低分框不扔掉,而是等高分框匹配完之后,再拿剩余轨迹来匹配一轮。这个设计非常对症车辆场景:车流里频繁出现遮挡,被挡的车检测置信度会掉到0.1~0.3,传统追踪器直接丢掉这些框,轨迹就断了,车再出来就被当成新目标;ByteTrack用低分框续上轨迹,ID就能保住。

它不用ReID特征,纯粹靠位置IOU和卡尔曼滤波预测。有些人觉得“不用外观特征会不会很容易跟丢”,在车辆场景恰好不是问题:车和车长得像,但同一辆车在两帧之间位置变化很小,IOU关联够用;相反,DeepSORT的ReID特征在车辆上区分度不高,还额外增加耗时。所以ByteTrack在这个场景是性价比最优解。还有一个细节是轨迹的confirmed/unconfirmed状态管理:新检测框生成的是未确认轨迹,连续两帧都匹配上才转成确认轨迹,这样能过滤掉单帧误检。

4.2 把ByteTrack接入YOLOv8检测流:最小可跑的追踪代码

接ByteTrack不需要改检测模型,只需要把YOLOv8输出的框喂给追踪器。这里用官方ByteTrack实现做示例:

import numpy as np from ultralytics import YOLO from bytetrack import BYTETrackerArgs, BYTETracker class VehicleTracker: def __init__(self, det_model_path, conf_thresh=0.25): self.det = YOLO(det_model_path) self.conf_thresh = conf_thresh # ByteTrack建议参数:track_thresh=0.5, match_thresh=0.8 self.tracker = BYTETracker( BYTETrackerArgs(track_thresh=0.5, match_thresh=0.8) ) def update(self, frame): results = self.det.predict(frame, conf=self.conf_thresh, verbose=False)[0] dets = [] for box in results.boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() score = float(box.conf[0]) cls_id = int(box.cls[0]) if cls_id != 0: # 只追踪car类 continue dets.append([x1, y1, x2, y2, score]) if len(dets) == 0: return [] dets = np.array(dets) # ByteTrack的update接收xyxy+score的二维数组 online_targets = self.tracker.update(dets, frame.shape[:2]) return online_targets # 使用示例 tracker = VehicleTracker('best_vehicle.pt') frame = cv2.imread('frame_001.jpg') targets = tracker.update(frame) for t in targets: tlwh = t.tlwh # 左上角x, 左上角y, 宽, 高 track_id = t.track_id print(f'车辆ID:{track_id}, 位置:{tlwh}')

这段代码的关键在BYTETrackerArgs和update的输入格式。track_thresh=0.5是划分高低分框的阈值,检测置信度高于0.5的框参与第一轮匹配,低于0.5但高于conf_thresh的进入第二轮补充匹配。match_thresh=0.8是IOU关联的阈值,太高容易拒掉遮挡帧的匹配导致ID切换,太低会把并排的两辆车关联成一辆。update输入的数组格式必须是xyxy+score,顺序不能错,否则卡尔曼滤波的观测维度会乱。frame.shape[:2]传入的是H和W,ByteTrack内部用它做边界约束,防止预测框跑到画面外太远。

实际项目里我不会让YOLOv8每帧都跑全图,因为ByteTrack的卡尔曼预测本身能处理短期遮挡,检测帧率可以降一半。比如摄像头是25fps,检测只跑12fps,中间帧直接用上一帧的轨迹位置做线性预测,车流统计的精度几乎不掉,但推理负载直接减半。

4.3 追踪参数调优:track_thresh、match_thresh、帧率与ID切换的关系

追踪参数调优是玄学最集中的地方,但有一条主线:先固定一个变量,再调另一个。给出一组我常用的参数组合参考:

参数建议范围场景说明
track_thresh0.4 ~ 0.6夜间或雨天调低到0.4,低分框多了续轨迹能力更强;白天拥堵场景调高到0.6减少误检
match_thresh0.7 ~ 0.9车流密集、互相遮挡严重时降到0.7;道路空旷时调到0.9不会丢
conf_thresh0.2 ~ 0.3低于track_thresh,只参与低分框匹配,不参与确认轨迹
max_time_lost30 ~ 60轨迹丢失多少帧后删除,车被遮挡超过这个数就会重新分配ID
检测帧率10 ~ 25帧率越低,match_thresh要越低,否则帧间位移大,IOU匹配不上

ID切换频繁时,先看max_time_lost和match_thresh,不要动检测模型。我之前遇到一个场景:车流量统计总是偏多,排查发现每辆车在画面里平均被计数1.7次,根因是车经过画面中段的树冠遮挡区,轨迹丢失后被分配了新ID。把max_time_lost从15调到50之后,计数误差从70%降到8%。帧率的影响也要注意:如果用15fps的视频源,两帧之间车辆位移更大,IOU重叠变小,match_thresh还按0.8设就容易断轨迹,降到0.7往往就能解决。另外ByteTrack的det_thresh和置信度是两套阈值,很多人混淆:det_thresh决定哪些框进入追踪器,而YOLOv8的conf只决定检测阶段保留哪些框。我一般把conf设为0.3,track_thresh设为0.5,这样0.3~0.5之间的低分框还能参与第二轮的轨迹续接。

5. 交通流量统计逻辑与常见问题排查

5.1 用虚拟线圈做断面计数:双线方案与方向判定

有了稳定的轨迹ID,流量统计就不难了。最常用的是虚拟线圈法:在画面里画一条线,每帧检查所有车辆的框底边中心点是否穿越这条线。只画一条线的问题是:车在线上来回晃动或停在线上时会被多次计数。所以我一般用双线方案:两条平行的计数线,间距大约是车辆宽度的1.5倍。车辆中心点先经过线A再经过线B,才计为一辆;反向经过则记录为对向车流。

class LineCounter: def __init__(self, line_y, direction='down'): self.line_y = line_y self.direction = direction self.crossed_ids = set() self.count = 0 def update(self, targets): for t in targets: track_id = t.track_id if track_id in self.crossed_ids: continue cx = t.tlwh[0] + t.tlwh[2] / 2 cy = t.tlwh[1] + t.tlwh[3] # 框底边中心y坐标 if self.direction == 'down' and cy > self.line_y: self.crossed_ids.add(track_id) self.count += 1

代码逻辑是按框底边中心点穿越水平线来计数,因为底边比中心点更贴近车辆与地面的接触点,受车身高度和透视变形影响更小。当车道不是水平而是斜的,做法是不用固定y值,改成判断点和线的有向距离符号变化,效果等价。每个track_id只计一次,靠crossed_ids集合去重。这套逻辑跑起来很简单,但真正的坑不在计数代码里,而在前面追踪质量的稳定性。

5.2 避坑清单:4条血泪经验,按现象→原因→解决排列

现象一:同一辆车被重复计数多次,车流量虚高50%以上。原因:车辆经过画面中央时被临时遮挡,ByteTrack的轨迹丢失后重新分配了新的track_id,计数器把它当成一辆新车。这类问题在车流视频里最常见,不是计数逻辑错了,而是追踪层断链。 解决:优先调大max_time_lost到50~60帧;其次检查match_thresh是否过高,把0.8降到0.7,让遮挡后的框更容易续上轨迹;最后考虑是否使用了隔帧检测,如果检测帧率太低,中间帧缺失导致位置跳变,轨迹更容易断。

现象二:两辆并排或前后紧贴的车被合并成一个ID,统计结果偏少。原因:match_thresh设得过大,或者检测框在拥堵情况下重叠严重,ByteTrack的IOU匹配把两个目标的框关联到了同一条轨迹上。 解决:把match_thresh从0.8降到0.7甚至0.65,让两个重叠框更容易被区分开;同时检查检测模型在密集场景的召回率,如果mAP不低但密集场景漏检,考虑用更高的imgsz(960)微调模型。

现象三:夜间车灯导致检测框发散、位置抖动。原因:night场景下大灯产生的光晕让车辆的框不稳定,框的宽高和中心点每帧都在变,导致计数线穿越判定出错。 解决:在检测前加一个图像预处理,做自动白平衡或CLAHE对比度增强;但不要做太强的去噪,否则会抹掉小目标车身的纹理特征。更稳妥的路线是在数据集里加入夜间帧,对车灯区域的标注框做得比日间略大一圈,让模型学习到光晕和车身的真实边界关系。

现象四:计数线附近车辆走走停停,一辆车被计成多辆。原因:车停在计数区域时,检测框在“有”和“无”之间抖动,track_id时有时无,每出现一次就穿过线一次。 解决:加入时间维度的消抖——同一ID的框必须在3帧以上都穿越计数线才计入,或者记录穿越时间戳,10秒内同一ID的位置只计数一次。这属于业务逻辑层的修正,和追踪参数无关,但能解决追踪器解决不了的实际问题。

5.3 统计结果验证:用什么指标判断系统可用

流量统计系统的核心指标不是mAP,而是计数误差率。验证方法是找一段10分钟的视频,人工数出真实车流量,再对比系统输出。我一般分三个维度验证:总数误差、单车道误差、ID切换次数。总数误差在5%以内是可接受水平,超过10%就需要检查追踪层;单车道误差如果集中在某个车道,可能是透视变形下计数线位置不合适;ID切换次数直接看追踪器输出的ID数量,如果实际是30辆车但系统总共分配了40个ID,说明平均每辆车被切了1.3次,需要回去调追踪参数。

6. 部署到边缘设备与实时性验证:从x86到RK3588的优化路径

6.1 模型导出与量化:TensorRT和RKNN的转换要点

训练好的模型只是起点,落到嵌入式平台才是硬仗。以RK3588为例,ultralytics支持导出RKNN格式,但需要先转ONNX再转RKNN。转换过程中最容易翻车的是算子的兼容性:YOLOv8的DFL(Distribution Focal Loss)模块在部分NPU上不支持或支持得不好,需要在导出前把模型结构里的DFL层替换成等效的卷积实现,或者改用RKnpu提供的yolov8适配版本。

量化校准是另一个关键点。RKNN-Toolkit的量化需要准备一批校准图片,我一般从训练集里抽300张覆盖白天、夜间、雨天、逆光等场景。校准图片的分布直接影响INT8量化后的精度损失,如果只拿白天图片校准,夜间场景精度会掉3到5个点。量化后必须做精度回退检查:拿同一段测试视频分别跑FP16和INT8模型,对比两边的平均置信度和mAP,一般要求mAP下降不超过2%。超过这个数就考虑混合量化,对DFL和最后的解耦头保留FP16,其余层用INT8。

6.2 端到端验证方法:一镜到底的视频测试与指标落表

部署完成后的验证方式,我习惯用一段10分钟、1080p、25fps的路口视频,包含白天拥挤、夜间稀疏、雨天等不同片段。用系统跑完整段视频,记录三个数字:平均处理帧率(FPS)、车流量计数总数、ID切换总次数。帧率要能达到设备实际采集帧率的80%以上才谈得上实时;计数误差率按第5章的标准控制在5%以内。如果帧率达标但误差超标,优先看是不是跳帧太狠导致IOU关联不上;如果帧率不达标,从量化精度和检测分辨率两个方向压,先试960转640的输入尺寸,再试全INT8量化。

最后说个习惯:每换一个部署场景,我都会把该场景的“错误样本”单独存下来——漏检、误检、ID切换各建一个文件夹,跑完视频后抽帧查看。很多想当然的“参数调优”其实都是盲调,而错误样本看得多了,会发现八成问题集中在固定几个位置:树荫、路灯杆、车道线附近。模型看懂了这些位置的特殊性,系统的稳定性才算真正过关。希望这套从数据到部署的路径能帮到你,至少在踩坑时少走几段弯路。

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

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

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

立即咨询