YOLOv8-deepsort车辆计数实战:检测跟踪、参数调优与端侧部署

📅 发布时间:2026/9/10 13:49:37
YOLOv8-deepsort车辆计数实战:检测跟踪、参数调优与端侧部署
简介这套基于YOLOv8与DeepSORT的车辆智能分析方案面向计算机视觉开发者和智能交通方向学习者旨在解决视频场景中的车辆目标检测、跨帧稳定跟踪以及实时数量统计等实际问题。方案将YOLOv8的高精度检测能力与DeepSORT的关联跟踪机制相互融合能够为每辆车辆分配固定编号并输出连续轨迹再根据跟踪结果完成车流计数适合车流量统计、道路监控、智慧交通实验等多种应用场景。资源以ZIP压缩包形式提供整体大小约293.75MB共包含349个文件其中既有86个Python源码和163个编译后的pyc字节码文件也有38个YAML配置文件以及样例视频、模型权重、说明文档和辅助脚本目录结构清晰方便按模块阅读、运行和二次开发。目前已有1634人学习使用。借助这套资料使用者可以快速获得一套可运行的车辆检测与跟踪工程实现深入理解检测器与跟踪器之间的衔接逻辑掌握计数功能的设计思路并利用配置文件和数据样例灵活适配不同的道路场景与相机视角。1. 为什么车辆计数要先解决跟踪YOLOv8 只负责“看见”固定摄像头拍一辆车连续经过 30 帧如果只看单帧检测就会把这辆车数成 30 辆。车辆计数真正依赖的是跟踪器给出的唯一 ID而不是每一帧的检测结果。标题里的 YOLOv8-deepsort 把两套成熟算法串成一个流水线YOLOv8 输出每一帧的车辆目标检测框DeepSORT 把这些框关联成持续存在的轨迹计数逻辑只需要盯着轨迹 ID 变化。最反直觉的是绝大多数计数误差不是 YOLOv8 没识别出车而是跟踪器把一辆车拆成了两个 ID、又把两辆车并成一个 ID。这篇内容适合正在做交通流量统计、停车场出入管理或园区车辆调度的工程师沿着“检测—跟踪—计数”顺序把流程跑通并解决掉藏在参数里的坑。2. 从 YOLOv8 到 deep sort车辆跟踪的数据流与模型结构2.1 为什么 SORT 不够用从检测框到轨迹的第一次升级DeepSORT 的前身是 2016 年的 SORTSimple Online and Realtime Tracking。SORT 的思路是检测框之间直接做 IOU 匹配再交给卡尔曼滤波预测下一帧位置。这个方案速度快、实现简单但对遮挡和检测抖动非常敏感车辆被大车挡住三帧等再出现时检测框的 IOU 和旧轨迹完全不相交轨迹直接丢失恢复后会被当作新车计数从这个位置开始就错乱了。DeepSORT 在 SORT 基础上补了一个外观特征分支每个检测框内的图像被一个小的卷积网络编码成一维向量匹配时同时计算运动特征的马氏距离和外观特征的余弦距离。车辆比行人更容易出现“同类车长得几乎一样”的情况所以外观特征在车辆场景里并不总是可靠但如果视频中车辆颜色、型号有明显差异这个分支能大幅降低 ID 切换。我的判断是室外交通场景保留外观分支停车场深色车辆居多的场景可以调高余弦距离的权重而不是直接关掉它。2.2 YOLOv8 检测输出与 DeepSORT 输入的坐标转换YOLOv8 和 DeepSORT 之间是纯数据对接关系。如果使用 ultralytics 库做推理results[0].boxes.data 是一个形状为 [N, 6] 的张量每一行依次是 x1、y1、x2、y2、置信度、类别。DeepSORT 的 update 方法接收的输入通常是 [框中心 x框中心 y框宽框高置信度]而且置信度必须放在最后一列。这里存的坐标不只有格式问题。DeepSORT 内部要用框的宽高去裁剪特征图如果传入的是归一化坐标或极坐标跟踪不会报错但匹配结果完全失效也要把类别列删掉否则被混进输出的张量里后续的卡尔曼状态维数对不上。常见做法是在喂给跟踪器之前先完成坐标转换和类别过滤import numpy as np def xyxy2xywh(boxes_xyxy): xywh np.zeros_like(boxes_xyxy) xywh[:, 0] (boxes_xyxy[:, 0] boxes_xyxy[:, 2]) / 2 # 中心 x xywh[:, 1] (boxes_xyxy[:, 1] boxes_xyxy[:, 3]) / 2 # 中心 y xywh[:, 2] boxes_xyxy[:, 2] - boxes_xyxy[:, 0] # 框宽 xywh[:, 3] boxes_xyxy[:, 3] - boxes_xyxy[:, 1] # 框高 return xywh # results 为 YOLOv8 推理结果conf 阈值在推理时已过滤 dets results[0].boxes.data.cpu().numpy() dets dets[np.isin(dets[:, 5], [2, 5, 7])] # car, bus, truck xywh xyxy2xywh(dets[:, :4]) detections np.hstack([xywh, dets[:, 4:5]])上述代码中np.isin 会生成布尔索引一次把 car、bus、truck 三个类别都留在结果里。有人会在这里写成三个独立条件功能相同但执行次数更多更重要的是 np.isin 不会改变原数组的列顺序后续 hstack 拼出来的矩阵列结构是固定的。2.3 YOLOv8 的网络结构里C2f 和检测头为什么适合车辆检测YOLOv8 的网络结构图里骨干网最显眼的改动就是 C2f 模块。它把输入沿通道方向分成两支其中一支经过若干个 Bottleneck 后与另一支做 concat除了最后一层以外Bottleneck 的输出每一步都在参与特征复用。论文视角说这是让梯度传播路径变短、深层特征保留更多细节。换成车辆检测的视角马路上车辆尺度波动很大近处一辆车占半个画面远处一辆车不到 32 像素。C2f 在浅层保留了空间位置在深层保留了语义这让小目标检测的召回率比前代 C3 结构有可感知的提升。检测头也是选择它的理由。YOLOv8 用的是解耦输出头分类和回归分支各自走一个卷积不用锚框直接回归中心点和宽高。传统锚框方案的锚框比例是预设的车辆横向 3:2、纵向 1:2、公交车 2:1一套锚框很难同时讨好anchor-free 检测器把模型从“拟合先验”里解放出来。这是车辆场景在工程上选 YOLOv8 的实际理由。2.3.1 先想清楚要不要用 YOLOv8 训练自己的数据集官方预训练权重在 COCO 上训练过car、bus、truck 这三个类别不需要训练就可以用。多数做车辆计数的项目第一次跑通用的就是预训练模型。只有在以下三种情况才需要重训机位是正俯视视角车身只露出顶面检测的是集装箱、工程机械等 COCO 里没有的长尾车型夜间补光后图像风格和白天差异过大。热词搜索里很多人在问 YOLOv8 怎么训练自己的数据集我的建议是先用预训练模型跑通整个链路看到跟踪和计数效果以后再标注几百张俯视图做增量微调否则容易把第一版项目的进度压在数据标注上。3. 用 YOLOv8-deepsort 在本地跑通车辆计数的代码级最小实现3.1 环境与模型准备GPU 不是必须分辨率与帧率才是决定项热词里反复出现“需要用到 GPU 吗”可以直接给结论验证流程不需要做实时计数需要。DeepSORT 的卡尔曼滤波、级联匹配和特征提取都在 CPU 上执行真正决定帧率上限的是 YOLOv8 推理速度。用 yolov8n 在 720p 视频上纯 CPU 推理常见表现是 5 到 10 FPS离线处理能接受假如要接入现场摄像头并实时输出计数建议用支持 CUDA 的 GPU至少让 YOLOv8 的部分跑在 GPU 上。环境搭建步骤按常见的深度学习工程方式准备就可以安装 PyTorch、CUDA 版 torchvision再安装 ultralyticsDeepSORT 部分拉取一份可用的开源实现放入项目目录。pip install ultralytics opencv-pythontorch 和 torchvision 的版本要匹配YOLOv8 对 torch 版本要求不苛刻但版本差距过大会在加载权重时报出算子不支持的错。DeepSORT 的依赖更简单通常只需要 torchvision 和 scipy。3.2 主循环里的数据流检测、过滤、转坐标、跟踪把主程序写成一段能照抄的骨架比逐个贴 API 更直观。下面这个循环完成了输入视频到输出视频的完整通路计数逻辑先留个空位import cv2 import numpy as np from ultralytics import YOLO from deep_sort_pytorch.deep_sort import DEEPSORT # 不同实现的导入路径不同 # 初始化 YOLOv8video 推理时直接传路径也可以 model YOLO(yolov8s.pt) # 初始化 DeepSORT具体参数含义见 3.3 节 tracker DEEPSORT(max_age30, min_hits3, max_cos_dist0.3, nn_budget100) cap cv2.VideoCapture(traffic.mp4) writer cv2.VideoWriter(output.mp4, cv2.VideoWriter_fourcc(*mp4v), 25, (1920, 1080)) while cap.isOpened(): ret, frame cap.read() if not ret: break # 第 1 步YOLOv8 推理conf 和 iou 是两个直接影响计数精度的入口参数 results model(frame, conf0.4, iou0.5, verboseFalse) dets results[0].boxes.data.cpu().numpy() # [N, 6]x1,y1,x2,y2,conf,cls # 第 2 步剔除非车辆类别车辆在 COCO 中为 car2, bus5, truck7 if len(dets) 0: continue vehicles dets[np.isin(dets[:, 5], [2, 5, 7])] # 第 3 步坐标由 xyxy 转 xywh并只保留 [cx, cy, w, h, conf] xywh xyxy2xywh(vehicles[:, :4]) detections np.hstack([xywh, vehicles[:, 4:5]]) # 第 4 步交给 DeepSORT 更新轨迹返回值通常为 [x1,y1,x2,y2,id] tracks tracker.update(detections, frame) for track in tracks: x1, y1, x2, y2, ident track center ((x1 x2) / 2, (y1 y2) / 2) # 计数逻辑写在这里见第 4 章 cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID {int(ident)}, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) writer.write(frame) cap.release() writer.release()这段代码值得解释的细节有三个。第一model(frame, conf0.4, iou0.5) 里的 conf 在 YOLOv8 早期版本叫 conf_thres不同版本 API 有差异接入前先查一下当前版本支持什么写法。第二tracker.update 的返回值不是所有实现都一样有的实现返回 NDArray有的返回 dict拿到后先打印类型再按对应方式解析 ID。第三DEEPSORT 初始化参数在 3.3 节给出建议值实际项目中这些参数属于“第一版就能用、第二版才需要精调”的类型。注意不同实现的 DeepSORT 对 update 的返回值定义并不统一接入新代码前先打印 track 的类型和长度。3.3 deep sort 常见初始化参数的含义与车辆场景建议值不同开源实现的参数名略有差异但核心参数基本一致参数常见默认值作用车辆场景建议max_age30目标丢失后轨迹保留的帧数30~50路口遮挡严重取 50min_hits / n_init3新轨迹需要连续匹配成功几帧才被确认3过低会出现闪现的噪声轨迹max_cos_dist0.2外观特征的最大余弦距离越小要求越严格0.3~0.5车辆外观趋同导致默认值过紧nn_budget100每个轨迹保留的特征向量个数50~100特征太多匹配慢max_cos_dist 是车辆场景最容易被忽视的参数。行人重识别特征在同一个人变换角度后仍相对接近但车的颜色、反光、车牌角度都会让特征离得很远。如果一辆车在画面里反复变道特征匹配失败后会退化成纯 IOU 匹配ID 切换概率上升。建议值 0.3 到 0.5 的意思是“外观不好区分时多相信一点位置信息”。3.4 为什么第一步只需要让“检测框上的 ID”保持住拿到一个能跑的 YOLOv8-deepsort 版本后先别急着写车辆计数逻辑。在视频上把画了框和 ID 的中间结果完整看一遍理想情况是一辆车从进入画面到离开ID 始终是同一个。只要有 ID 跳变计数逻辑写得再正确也无济于事。这个阶段的排查方法很简单找一段车辆少、行驶方向单一的视频观察 ID 序列是否连续递增且不重复跳变明显时优先调 max_age再动 conf。4. 车辆计数的置信度、IOU 与跟踪生命周期三个必调参数与常见坑4.1 confidence 阈值漏检和误检在车辆计数里的代价并不对称YOLOv8 的 conf 参数控制检测框的保留条件。置信度阈值调低会留下很多背景噪声框DeepSORT 会把这些噪声框当成新目标并分配新 ID车辆计数会被“幽灵车”拉高阈值调高远处的小目标被滤掉车辆轨迹断裂计数被拉低。两者都不好但后果不同误检多产生的是一次性噪声轨迹跟踪器的 min_hits 可以滤掉大部分漏检会让一辆车数成两辆这是计数项目里最致命的问题。调阈值有一个直接有效的笨办法把视频抽出一帧用 conf0.1 跑一次检测把所有框画出来统计那些压在树上、路灯上、路面标识上的假框再把 conf 逐步往上加直到假框基本消失。这个值就是当前机位的合理起点。不要直接用 0.25 这类默认值车辆场景里的远距离目标本身置信度就在 0.2 到 0.4 之间。目标检测评价指标里的 mAP 在这个环节基本帮不上忙。mAP 衡量的是整个测试集上的检测精度和召回跟具体机位、具体远近尺度没有直接关系。计数项目的调参基准应该是“这一条计数线上发生过多少次真实事件”。4.2 deep sort 三个生命周期参数max_age、min_hits、max_cos_dist4.2.1 max_age 决定车辆被遮挡后还能不能回来车辆被大型车短暂遮挡、进入立柱盲区、或者检测置信度连续几帧低于阈值时轨迹并没有立即删除而是进入“丢失”状态。max_age 是轨迹在丢失状态下保留的帧数超过这个值轨迹才会被删除。对这个参数最直觉的理解是车辆消失的帧数如果小于 max_age那么它再次出现时还能拿回原来的 ID大于 max_age就会被当作新车。城市道路上一辆轿车被公交车遮挡 2 到 3 秒按 30FPS 算就是 60 到 90 帧。max_age 给到 30 只覆盖 1 秒明显不够推荐在 30 到 50 之间。4.2.2 min_hits 滤掉噪声轨迹却不适合所有场景新产生的轨迹要连续若干帧都匹配上检测才会被确认为正式轨迹这就是 min_hits。它的作用是把 4.1 节残留的噪声框挡在计数逻辑外面。min_hits 设成 3一辆车第一次进入画面到第 3 帧之前存在一个半瞬态轨迹可能造成计数延迟 2 到 3 帧。如果视频里进出车辆速度很快60km/h 的车速下 25FPS 的每帧移动约 0.66 米延迟几帧意味着计数线位置偏差几米。极端场景下 min_hits 设 1但要接受偶尔多计的噪声框。4.2.3 max_cos_dist 在车辆场景里的特殊问题车辆外观的相似度天然高黑色轿车和黑色 SUV 在特征空间可能比同一辆车在两个光照条件下的距离还近。所以车辆场景的 max_cos_dist 不能照搬行人重识别项目的默认值。我的调法是把默认 0.2 调到 0.4然后观察夜间场景。夜间车辆的特征受车灯和反射影响很大同一辆车出地库前后特征差距明显调到 0.4 后 ID 保持时间有明显改善。4.3 基于虚拟线的车辆计数逻辑方向判定与重复计数消除计数模块在跟踪器之上。最常用的做法是画一条虚拟线车辆质心从线的一侧运动到另一侧就算一次通过。方向由质心跨越线的方向决定。下面这段代码是直接把计数逻辑嵌入到跟踪循环里的实现line_a (0, 600) # 计数线起点 line_b (1280, 600) # 计数线终点 MAX_POINTS 20 # 每个轨迹只保留最近 20 个质心 # 计算点在线哪一侧正负表示方向 def side(p): ta (line_b[0] - line_a[0]) * (p[1] - line_a[1]) tb (line_b[1] - line_a[1]) * (p[0] - line_a[0]) return ta - tb hist {} # ID - 质心历史 counter {in: 0, out: 0} for track in tracks: x1, y1, x2, y2, ident track cx, cy (x1 x2) / 2, (y1 y2) / 2 tid int(ident) hist.setdefault(tid, []).append((cx, cy)) if len(hist[tid]) MAX_POINTS: hist[tid].pop(0) if len(hist[tid]) 2: prev hist[tid][-2] curr hist[tid][-1] if side(prev) * side(curr) 0: # 前后两帧位于线两侧时判定为跨线 if side(curr) 0: counter[out] 1 else: counter[in] 1这段逻辑本身没有任何花哨的地方真正的问题是内存和误判。hist 如果无限追加视频长跑几小时就会积累出内存问题所以要限制每个 ID 只保留最近 20 个质心跨线条件用side(prev) * side(curr) 0比分别判断正负更快且能把刚好压在线上重复计数的情况挡在外面。车辆停在线旁、原地掉头、倒车入库这些情况会让质心来回穿越虚拟线。如果业务是“进出双向统计”建议在线两侧各加一个“过线后的冷却时间”比如同一个 ID 在 5 秒内只计一次。如果业务只关心“驶入地库的车数”则计数条件要加方向的判定。4.3.1 车辆被遮挡后的技术与业务两难停车场道闸是经典的坑车在道闸前停了 10 秒如果这 10 秒内车被道闸杆或记录仪干扰检测框消失max_age 耗尽后轨迹被删。杆一抬、车子驶入DeepSORT 会视为新车计数变成两次。工程上两种常用补救办法把 max_age 调到能覆盖停车等待时间的帧数或者在横杆前后画两道线只统计“完全越过第二道线且没有在杆前停留超过 max_age 的轨迹”。后一种做法的误差来源更可控。4.3.2 “已测”二字背后建议用事件级指标验证车辆计数是一个离散事件统计系统用目标检测的评价指标 mAP 来验收是非常不匹配的。常见做法是人工标注一条 5 分钟视频里所有跨越计数线的事件程序输出的事件按时间窗口与人工结果做对齐然后计算事件级的精确率和召回率。精确率反映“多计了多少”召回率反映“漏计了多少”。如果两者差距过大回到 4.1 的阈值去调整。按我的经验第一版能跑到精确率 0.9 以上就算“已测”过关剩下的误差来源大多是跟踪 ID 切换。5. 把 YOLOv8-deepsort 落地到 RK3588 与端侧提速技巧5.1 先用 ONNX 导出后端推理不一定需要 PyTorchYOLOv8 自带导出接口model.export(formatonnx, opset12)。在 ultralytics 环境下这一行代码会完成权重转换。导出 ONNX 后再换 onnxruntime 推理速度通常比 PyTorch eager 模式提升不少。要注意 RKNN 工具链对算子的支持范围优先锁定 opset 12 或 13输入尺寸固定为 640x640避免动态尺寸带来的兼容问题。导出后要验证用同一个视频分别跑两个后端比对检测框数量和坐标而不是只看能否加载模型。5.2 RK3588 部署时的常见分工NPU 跑检测CPU 跑跟踪RK3588 提供 6 TOPS 的 NPU 算力YOLOv8 的卷积部分可以转换到 RKNN 格式后在 NPU 上推理YOLOv8 目标检测流程的耗时主要被 NPU 消化。DeepSORT 的卡尔曼滤波和级联匹配是纯数值计算不适合搬上 NPU常规做法是留在 CPU 上跑。工程结构一般是采集线程只负责读帧NPU 推理线程输出检测框DeepSORT 线程拿到检测框后更新轨迹最后一个线程负责绘制和计数。四线程之间的数据用队列传递检测帧率和跟踪帧率解耦。部署在 RK3588 时会遇到两个高频问题。模型转换时如果提示不支持的算子优先检查模型里是否带了 NMS 后处理节点YOLOv8 导出的 ONNX 可能包含 NMS转换前建议把后处理留在 CPU 端实现输入的图像缩放方式要保持 letterbox 一致否则检测框坐标映射到 1080p 原图上会整体偏移。5.3 检测降频用卡尔曼预测补足中间帧端侧算力有限时不必每一帧都跑 YOLOv8。常见做法是每 3 帧或每 5 帧做一次推理中间帧用 DeepSORT 的卡尔曼预测输出来绘制框这样视频看起来仍然是连续的计数逻辑也不需要改动。这个技巧能显著拉高端侧 FPS缺点是最多延迟几帧发现新的目标。车辆计数场景下目标出现位置是逐渐靠近计数线的延迟影响通常可以接受。另一个低成本的优化是降低检测端输入分辨率。监控画面是 1080pYOLOv8 输入缩放到 640x640 后远处车辆只有几十个像素收益有限把输入从 640x640 缩到 416x416检测召回率略有下降但端到端实时性有明显提升。如果硬要把远处小目标保住就要换用更大的输入和更强的模型这是计算量和召回率之间的一个工程取舍。本文还有配套的精品资源点击获取