资讯中心

YOLOv8+DeepSORT车辆跟踪计数实战:从检测到计数调优全解析

📅 2026/9/27 5:39:46
YOLOv8+DeepSORT车辆跟踪计数实战:从检测到计数调优全解析
简介一款基于YOLOv8与DeepSort的智能车辆跟踪与计数系统毕业设计源码面向计算机视觉方向学生及开发者适用于毕业设计、课程设计或期末大作业等场景可实现对视频中车辆的实时检测、跟踪与数量统计。压缩包共42个文件含31个Python脚本、模型权重文件.pt/.t7、YAML配置文件、使用说明文档、测试视频及演示图片整体约50MB目录结构清晰便于快速定位主程序与辅助模块。已有1355人学习下载属于高分毕业设计项目经过导师指导与严格调试确保可直接运行。资源提供完整的主程序、DeepSort配置、工具脚本及使用说明附带的测试视频和运行截图能帮助理解算法流程通过学习可掌握YOLOv8目标检测与DeepSort多目标跟踪的整合方法并基于现有代码快速开展二次开发或功能扩展。1. 面对车辆计数需求为什么是目标跟踪加 YOLOv8-deepsort停车场入口、高速匝道、城市路口这类场景的统计需求几乎一样过去一小时进了多少辆车、哪个方向流量大。只用目标检测做最常见的现象是每一帧车都能框出来但上一帧标成 ID5 的车下一帧变成了 ID8——检测器不带任何身份记忆计数窗口一长框与框完全对不上号。目标跟踪 YOLOv8-deepsort 这类项目解决的就是这件事。YOLOv8 每帧负责输出车辆检测框DeepSORT 用卡尔曼滤波预测目标位置、用外观特征做跨帧匹配把同一辆车从进入画面一直维系到离开画面。智能车辆跟踪加计数系统本质上是“检测器输出跟踪器的输入跟踪器输出计数器要用的 ID”。适合刚做完目标检测想补跟踪这一环的人也适合拿到源码包但对着 DeepSORT 一堆参数不知道怎么调的人。2. 先拆开看YOLOv8 检测和 DeepSORT 跟踪各自在算什么2.1 YOLOv8 的检测头anchor-free、解耦头与 C2fYOLOv8 属于 anchor-free 检测器网络结构图里能看到 backbone 部分带 C2f 模块的跨阶段部分连接head 部分把分类分支和回归分支解耦。老用户看“yolov8 网络结构图”时最常问 C2f 相比 C3 改了什么C2f 在输出前多了一次 concat梯度传导路径更丰富深层特征表达能力更好代价是计算量略涨。对车辆这种尺度相对稳定、姿态变化不大的目标yolov8s 在精度和速度之间的位置最合适。直接用预训练权重跑COCO 80 类里就带了车辆相关类别car 的类 ID 是 2bus 是 5truck 是 7。车辆计数只关心这三类推理时做类别过滤能大幅减少行人、自行车带来的误触发。下面是过滤后的最小检测代码from ultralytics import YOLO import cv2 model YOLO(yolov8s.pt) # 首次运行自动下载权重建议提前放好 .pt 文件 cap cv2.VideoCapture(street.mp4) # 视频源摄像头用 rtsp:// 地址即可 while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, conf0.4, iou0.5, classes[2, 5, 7], verboseFalse)[0] detections [] # 统一格式给跟踪器用 for box in results.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() score float(box.conf[0]) cls int(box.cls[0]) detections.append([x1, y1, x2, y2, score, cls])conf0.4是检测置信度阈值太低的框大概率是噪点iou0.5是 NMS 的 IoU 阈值对密集车流可以适当降到 0.45classes[2,5,7]让模型跳过不相关类别推理时间更短。如果车辆框不稳问题往往不在 DeepSORT而是这个阈值没先调好。想换成自己训练的模型训练入口就是常规命令yolo detect train datacustom.yaml modelyolov8s.pt epochs100 imgsz640data.yaml 里的 nc 要和实际类别数一致names 列表顺序决定了推理时的类别 ID。训练结束后 runs/detect/train*/results.png 会画出 loss 曲线这就是常说的“yolov8 画损失函数曲线图”。val loss 不再明显下降就可以停不用等满 100 epoch。2.2 DeepSORT 的跟踪核心卡尔曼滤波、级联匹配与外观特征DeepSORT 和 SORT 的最大区别是多了一条外观特征分支。SORT 只用框的位置和速度做匹配目标一被遮挡或者两辆车交错ID 就乱。DeepSORT 引入 ReID 网络提取目标的外观特征L2 归一化匹配时用余弦距离衡量“这张框里的车和记忆里的车是不是同一辆”再叠加卡尔曼滤波预测的位置最终用匈牙利算法完成最优分配。卡尔曼滤波目标跟踪在这里做两件事。第一是预测已知前几帧车辆的位置和速度预测这一帧它该出现在哪即使当前帧检测器漏检跟踪器也能凭预测维持续航。第二是平滑检测框本身有抖动滤波后的轨迹比原始框稳得多画出来不跳。级联匹配的顺序按 track 的最近连续命中帧数从高到低排列。长时间被遮挡的目标卡尔曼预测误差随时间增大如果让它和新目标公平竞争匹配机会很容易被抢走。先给“一直稳定跟踪着的目标”匹配再把剩余检测框分配给“遮挡后重新出现的旧 track”这是 DeepSORT 保持 ID 稳定性的关键设计。模块负责的事情输出常见失败模式YOLOv8 检测每帧找出车辆框和类别[x1, y1, x2, y2, conf, cls]漏检阈值过高、误检阈值过低DeepSORT 跟踪跨帧关联检测框维护 IDtrack_id 平滑后轨迹ID Switch、丢失后开新 ID2.3 检测与跟踪的接口从“每帧一堆框”到“每个目标一个身份”跟踪器的输入完全依赖检测器的输出格式。常见做法是把检测输出整理成[x1, y1, x2, y2, score, class_id]的列表整体传给 DeepSORT跟踪器内部把位置送进卡尔曼滤波器、把框对应的图像区域送进 ReID 网络提特征。这里最隐蔽的坑是坐标系的还原如果预处理做了 letterbox 缩放送到跟踪器前必须按缩放比例还原成原图坐标否则卡尔曼模型算出的轨迹中心和真实车辆中心对不上ID 会频繁切换。到这里数据流已经清楚视频帧 → YOLOv8 检测框 → DeepSORT 跟踪器 → 带 ID 的轨迹。下一章把它变成能跑起来的环境。3. yolov8 环境配置与最小可运行工程3.1 用 conda 建环境并安装依赖链跟踪计数项目的依赖比纯检测多一些主要坑在 torch 和 ReID embedder 的版本匹配上。按这个顺序装最省事conda create -n yolotrack python3.10 -y conda activate yolotrack # 先装 CUDA 版 pytorchcu121 按本机 CUDA 版本选 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 再装检测与跟踪相关包 pip install ultralytics deep-sort-realtime opencv-python包约束作用torch与 CUDA 版本匹配检测与跟踪共用的深度学习后端ultralytics8.x加载 YOLOv8 权重并推理deep-sort-realtime最新版DeepSORT 跟踪器及内置 ReID 特征opencv-python4.x视频读取、绘图、图像预处理pip 在装 deep-sort-realtime 时会自动处理 embedder 依赖但如果顺序反了先装它再装 torchpip 可能给 torchreid 装上 CPU 版 torch显卡闲置。装完确认一句python -c import torch; print(torch.__version__, torch.cuda.is_available())输出 True 再往下。默认 embedder 是 mobilenet不手动传 embeds 时它会对每个检测框抠图提特征。没有 GPU 也能跑但帧率会掉到个位数这种场景建议halfFalse避免 CPU 上的半精度算子不兼容。3.2 先用 CLI 验证检测再写跟踪代码先跑官方 CLI 看一眼当前视频上的表现这一步能排除大量“代码没问题但效果不对”的情况yolo predict modelyolov8s.pt sourcestreet.mp4 conf0.4 saveTrue跑完在 runs/detect/predict 下能看到带框的标注视频。车辆框有明显漏检就先在 CLI 层面调 conf、iou不要急着动 DeepSORT——跟踪器接收的检测质量决定了它输出的计数质量。确认检测稳定后写一个同时带检测和跟踪的最小脚本from ultralytics import YOLO from deep_sort_realtime.deepsort_tracker import DeepSort import cv2 model YOLO(yolov8s.pt) tracker DeepSort( max_age30, # track 丢失检测后保留的帧数 n_init3, # 连续 n_init 帧命中才确认 ID max_cosine_distance0.3, # 外观特征余弦距离阈值 nn_budget100, # 每个 ID 保留的特征数量上限 halfTrue, # GPU 上半精度加速 bgrTrue, # 传入的 frame 是 BGR 格式 ) cap cv2.VideoCapture(street.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame, conf0.4, iou0.5, classes[2, 5, 7], verboseFalse)[0] detections [] for box in results.boxes: detections.append([ *box.xyxy[0].tolist(), # x1, y1, x2, y2 float(box.conf[0]), # 检测置信度 int(box.cls[0]) # 类别 ID ]) tracks tracker.update_tracks(detections, frameframe) for track in tracks: if not track.is_confirmed(): continue print(track.track_id, track.to_ltrb())重点说明两个参数。bgrTrue是因为 deep-sort-realtime 内部会对 frame 做 ReID 特征提取而 OpenCV 读出来的是 BGR默认按 RGB 处理时特征偏色关联准确率明显下降。is_confirmed()用来过滤尚未稳定确认的新 track——初始几帧跟踪器拿不准这是新目标还是误检不过滤的话画面上全是乱跳的 ID。3.3 用模型信息检查加载结果与网络结构代码能跑起来后顺手确认模型加载情况model YOLO(yolov8s.pt) model.info() # 打印参数量、层数想看“yolov8 网络结构图”时最常见的做法是把模型导出成 ONNX 再拖进 netronyolo export modelyolov8s.pt formatonnx opset12导出后生成 yolov8s.onnx在 netron 里能完整看到 C2f、SPPF、Detect 等模块的连接关系。后面要做 head 改进、剪枝或者对比不同配置这份结构图是绕不开的参照。4. 车辆跟踪计数系统DeepSORT 参数、虚拟线与排错4.1 DeepSORT 初始化参数选型与实践值系统能跑起来后最常被问的就是“DeepSORT 参数到底怎么设”。下面这张表是车辆计数场景下我用过的基准值每条都对应一类实际问题参数基准值控制什么什么情况要调max_age30检测丢失后 ID 保留多少帧遮挡常在 1 秒以上时结合帧率上调到 45~60n_init3新 track 连续命中几帧确认车小且远时降到 2让 ID 更快建立max_cosine_distance0.3外观匹配的余弦距离上限光照变化大时降到 0.25~0.2误匹配减少nn_budget100单个 ID 缓存特征数拥堵排队场景加到 150~200halfTrue是否用 FP16 推理老显卡不支持时设 Falsemax_age要结合视频帧率算。假设 25 fpsmax_age30 表示 ID 在目标消失后最多维持 1.2 秒。车辆被大车完全挡住经常超过 2 秒max_age 太低就会“丢 ID 再重开新 ID”同一辆车被计数成两辆max_age 太高则会把已驶离画面的轨迹留很久一旦误检框出现在旧轨迹预测位置又会被错误续命。4.2 用虚拟线实现进出双向计数计数逻辑不复杂在画面里画一条线记录每个 ID 上一帧的中心点。某个 ID 的中心点从线的上侧移动到下侧视为入场反过来视为出场。注意图像坐标系 y 轴向下线的上方是 y 值小的一侧。LINE_Y 700 # 图像高度 1080 时画在 700 像素处 count_in 0 count_out 0 prev_pos {} # track_id - cy for track in tracks: if not track.is_confirmed(): continue track_id track.track_id ltrb track.to_ltrb() cx (ltrb[0] ltrb[2]) / 2 cy (ltrb[1] ltrb[3]) / 2 if track_id not in prev_pos: prev_pos[track_id] cy continue prev_cy prev_pos[track_id] if prev_cy LINE_Y and cy LINE_Y: # 从上往下穿线 count_in 1 prev_pos[track_id] cy elif prev_cy LINE_Y and cy LINE_Y: # 从下往上穿线 count_out 1 prev_pos[track_id] cy这里有实际项目里最容易踩的坑max_age 大时track 消失 1 秒再出现prev_pos[track_id]存的是消失前的旧位置。如果消失前车在线的上方、重新出现在下方就会误判一次穿越。对策是只在“这一帧真正关联上检测框”时才更新 prev_pos纯靠卡尔曼预测续命的帧不更新。判断是否真的关联上可以用当前帧检测框中心点与 track 输出框中心点的距离是否小于阈值来实现而不是无条件信任每次 update 的轨迹。4.3 计数结果按时间窗口聚合直接累加 count_in 适合短时展示交付时通常还要按 5 分钟、1 小时聚合。常见做法是把每次穿越写成带时间戳的记录而不是只维护一个累加数import csv, datetime with open(crossing.csv, a, newline) as f: writer csv.writer(f) writer.writerow([datetime.datetime.now().isoformat(), track_id, in, cx, cy])事后按时间段聚合就能画出流量曲线。如果只在内存里累加程序重启后历史丢失这个细节在课设验收时经常是加分项也方便直接喂给图表展示。4.4 高频排错检测正常但跟踪 ID 乱跳跟踪效果差先分清是检测还是关联的问题。定位手法把每帧检测框和跟踪框同时画在画面上逐帧看。检测框在但跟踪框没跟上说明卡尔曼预测或匹配环节有问题检测框本身频繁消失问题在 conf 阈值或遮挡。ID 频繁切换有两个典型原因。一是外观特征区分度不够max_cosine_distance 设得过大不同车辆的特征也能匹配上调小阈值并加大 nn_budget 能缓解。二是相邻车辆遮挡时两个 track 的预测框叠得太近误匹配没有银弹只能提高检测召回让跟踪器更早建立稳定轨迹。还有个隐蔽问题检测框坐标抖动剧烈时卡尔曼会被反复带偏把推理 imgsz 提到 960 并打开agnostic_nmsTrue对远处小车的框稳定性提升明显。5. 进阶用法结合车道参照物做车辆测速计数项目验收时用户常会追加一句“顺便测下速度”。检测跟踪已经给出轨迹点利用画面里已知长度的物体做像素到真实距离的换算即可。最方便的参照物是车道分界线国内高速和城市快速路的车道虚线通常是 6 米实线段加 9 米间隔实线段的像素长度直接在画面里量出来。import time PIXEL_DIST 180.0 # 画面上 6 米虚线对应的像素长度 PIXELS_PER_METER PIXEL_DIST / 6.0 prev_pts {} # track_id - (timestamp, cx, cy) def calc_speed(track_id, cx, cy): now time.time() if track_id not in prev_pts: prev_pts[track_id] (now, cx, cy) return None t0, x0, y0 prev_pts[track_id] dt now - t0 if dt 0.5: # 间隔太短像素位移小速度抖动大 return None dist_px ((cx - x0) ** 2 (cy - y0) ** 2) ** 0.5 speed_kmh (dist_px / PIXELS_PER_METER) / dt * 3.6 prev_pts[track_id] (now, cx, cy) return speed_kmh视野边缘的透视畸变会让同样速度的车在远处显得更慢所以测速只取靠近虚拟线的那一段画面像素换算也只对同一平面区域成立。稳妥做法是把某个 track 接近计数线前后 20 帧的测速结果做中位数滤波剔除明显偏离的异常值再上报。计数正确性验证同样依赖轨迹录一段固定场景的视频人工数一遍车辆进出量和系统输出对比同时统计 ID Switch 次数。ID 切换多时优先调大 max_age、调小 max_cosine_distance改到 ID Switch 不再下降为止。pixels per meter 的标定只对同一机位高度和俯仰角成立摄像头挪过就必须重新量一次参照物像素长度否则测速结果整体偏移 10% 以上很常见。计数对不上人工数的时段把 CSV 里对应时间窗口的 ID 明细打出来逐帧核对是哪一段跟踪断掉再决定是补检测阈值还是调跟踪参数。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案