YOLOv8+DeepSORT车辆跟踪计数实战:从检测到计数调优全解析
2026/9/14 2:01:30 网站建设 项目流程

简介:一款基于YOLOv8与DeepSort的智能车辆跟踪与计数系统毕业设计源码,面向计算机视觉方向学生及开发者,适用于毕业设计、课程设计或期末大作业等场景,可实现对视频中车辆的实时检测、跟踪与数量统计。压缩包共42个文件,含31个Python脚本、模型权重文件(.pt/.t7)、YAML配置文件、使用说明文档、测试视频及演示图片,整体约50MB,目录结构清晰,便于快速定位主程序与辅助模块。已有1355人学习下载,属于高分毕业设计项目,经过导师指导与严格调试,确保可直接运行。资源提供完整的主程序、DeepSort配置、工具脚本及使用说明,附带的测试视频和运行截图能帮助理解算法流程;通过学习可掌握YOLOv8目标检测与DeepSort多目标跟踪的整合方法,并基于现有代码快速开展二次开发或功能扩展。

1. 面对车辆计数需求,为什么是目标跟踪加 YOLOv8-deepsort

停车场入口、高速匝道、城市路口,这类场景的统计需求几乎一样:过去一小时进了多少辆车、哪个方向流量大。只用目标检测做,最常见的现象是每一帧车都能框出来,但上一帧标成 ID=5 的车,下一帧变成了 ID=8——检测器不带任何身份记忆,计数窗口一长,框与框完全对不上号。

目标跟踪 + YOLOv8-deepsort 这类项目解决的就是这件事。YOLOv8 每帧负责输出车辆检测框,DeepSORT 用卡尔曼滤波预测目标位置、用外观特征做跨帧匹配,把同一辆车从进入画面一直维系到离开画面。智能车辆跟踪加计数系统,本质上是“检测器输出跟踪器的输入,跟踪器输出计数器要用的 ID”。适合刚做完目标检测想补跟踪这一环的人,也适合拿到源码包但对着 DeepSORT 一堆参数不知道怎么调的人。

2. 先拆开看:YOLOv8 检测和 DeepSORT 跟踪各自在算什么

2.1 YOLOv8 的检测头:anchor-free、解耦头与 C2f

YOLOv8 属于 anchor-free 检测器,网络结构图里能看到 backbone 部分带 C2f 模块的跨阶段部分连接,head 部分把分类分支和回归分支解耦。老用户看“yolov8 网络结构图”时最常问 C2f 相比 C3 改了什么:C2f 在输出前多了一次 concat,梯度传导路径更丰富,深层特征表达能力更好,代价是计算量略涨。对车辆这种尺度相对稳定、姿态变化不大的目标,yolov8s 在精度和速度之间的位置最合适。

直接用预训练权重跑,COCO 80 类里就带了车辆相关类别:car 的类 ID 是 2,bus 是 5,truck 是 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, conf=0.4, iou=0.5, classes=[2, 5, 7], verbose=False)[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])

conf=0.4是检测置信度阈值,太低的框大概率是噪点;iou=0.5是 NMS 的 IoU 阈值,对密集车流可以适当降到 0.45;classes=[2,5,7]让模型跳过不相关类别,推理时间更短。如果车辆框不稳,问题往往不在 DeepSORT,而是这个阈值没先调好。

想换成自己训练的模型,训练入口就是常规命令:

yolo detect train data=custom.yaml model=yolov8s.pt epochs=100 imgsz=640

data.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、丢失后开新 ID

2.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 python=3.10 -y conda activate yolotrack # 先装 CUDA 版 pytorch,cu121 按本机 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 依赖,但如果顺序反了(先装它再装 torch),pip 可能给 torchreid 装上 CPU 版 torch,显卡闲置。装完确认一句:

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

输出 True 再往下。默认 embedder 是 mobilenet,不手动传 embeds 时它会对每个检测框抠图提特征。没有 GPU 也能跑,但帧率会掉到个位数,这种场景建议half=False,避免 CPU 上的半精度算子不兼容。

3.2 先用 CLI 验证检测,再写跟踪代码

先跑官方 CLI 看一眼当前视频上的表现,这一步能排除大量“代码没问题但效果不对”的情况:

yolo predict model=yolov8s.pt source=street.mp4 conf=0.4 save=True

跑完在 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_age=30, # track 丢失检测后保留的帧数 n_init=3, # 连续 n_init 帧命中才确认 ID max_cosine_distance=0.3, # 外观特征余弦距离阈值 nn_budget=100, # 每个 ID 保留的特征数量上限 half=True, # GPU 上半精度加速 bgr=True, # 传入的 frame 是 BGR 格式 ) cap = cv2.VideoCapture("street.mp4") while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame, conf=0.4, iou=0.5, classes=[2, 5, 7], verbose=False)[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, frame=frame) for track in tracks: if not track.is_confirmed(): continue print(track.track_id, track.to_ltrb())

重点说明两个参数。bgr=True是因为 deep-sort-realtime 内部会对 frame 做 ReID 特征提取,而 OpenCV 读出来的是 BGR,默认按 RGB 处理时特征偏色,关联准确率明显下降。is_confirmed()用来过滤尚未稳定确认的新 track——初始几帧跟踪器拿不准这是新目标还是误检,不过滤的话画面上全是乱跳的 ID。

3.3 用模型信息检查加载结果与网络结构

代码能跑起来后,顺手确认模型加载情况:

model = YOLO("yolov8s.pt") model.info() # 打印参数量、层数

想看“yolov8 网络结构图”时,最常见的做法是把模型导出成 ONNX 再拖进 netron:

yolo export model=yolov8s.pt format=onnx opset=12

导出后生成 yolov8s.onnx,在 netron 里能完整看到 C2f、SPPF、Detect 等模块的连接关系。后面要做 head 改进、剪枝或者对比不同配置,这份结构图是绕不开的参照。

4. 车辆跟踪计数系统:DeepSORT 参数、虚拟线与排错

4.1 DeepSORT 初始化参数选型与实践值

系统能跑起来后,最常被问的就是“DeepSORT 参数到底怎么设”。下面这张表是车辆计数场景下我用过的基准值,每条都对应一类实际问题:

参数基准值控制什么什么情况要调
max_age30检测丢失后 ID 保留多少帧遮挡常在 1 秒以上时,结合帧率上调到 45~60
n_init3新 track 连续命中几帧确认车小且远时降到 2,让 ID 更快建立
max_cosine_distance0.3外观匹配的余弦距离上限光照变化大时降到 0.25~0.2,误匹配减少
nn_budget100单个 ID 缓存特征数拥堵排队场景加到 150~200
halfTrue是否用 FP16 推理老显卡不支持时设 False

max_age要结合视频帧率算。假设 25 fps,max_age=30 表示 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_nms=True,对远处小车的框稳定性提升明显。

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 明细打出来逐帧核对是哪一段跟踪断掉,再决定是补检测阈值还是调跟踪参数。

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

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

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

立即咨询