一、介绍
车流统计是智能交通分析的基础,流量、流速、行驶轨迹都建立在"每辆车被持续稳定跟住"之上,这一步就是多目标跟踪(MOT),业界通用方案是"检测器 + 跟踪器"两段式。两段式里有一条经典路线 DeepSORT:只保留高置信度检测框,给每个框提取外观特征(ReID),靠"运动预测 + 外观相似度"把前后帧的目标关联起来。本项目重点跟的是车------而 DeepSORT 是在行人数据集上设计调参的(外观模型直接用行人重识别权重),车辆场景的车流密集交叉、转弯减速、遮挡形态都与行人不同,直接套用会遇到车辆场景特有的问题。
实际跑车流视频暴露出三个典型问题:刚建档的轨迹一帧漏检就被删除,检测恢复后只能换新 ID;轨迹丢失 2~3 帧后外观匹配失败,IoU 营救轮又进不去(原版只允许丢失 1 帧的轨迹参加),目标断裂换 ID;短暂遮挡时画面框闪断(输出层只给丢失后 1 帧的惯性补位)。它们分别对应后文的三项改进。
本项目的目标:让车辆目标的轨迹 ID 保持稳定------不频繁切换,短暂丢失(漏检、遮挡)后能找回原 ID 继续跟踪。
检测用 Ultralytics YOLO,跟踪用 DeepSORT,针对车流场景做了三项实测有效的跟踪改进(以命令行参数提供,默认值即改进值)。本文按以下顺序介绍:整体架构、检测器选型与参数、DeepSORT 轨迹算法流程与三项改进、改进测试方法、结果对比数据、四个测试分析工具的完整用法(附实际命令)。
二、整体架构
技术栈
| 模块 | 选型 |
|---|---|
| 检测器 | Ultralytics YOLO(库随项目内置 8.4.41,免安装) |
| 跟踪器 | DeepSORT(核心代码保留在 deep_sort/ 包:sort/ 轨迹算法 + deep/ ReID 特征提取,可单独阅读修改) |
| 外观特征 | torch resnet18 特征网络(原版权重 ckpt.t7,输出 128 维特征向量) |
| 视频处理 | OpenCV |
| 语言 | Python 3.x |
数据流水线
视频帧(每 N 帧) → Detector.detect() → Nx6 检测数组 [x1, y1, x2, y2, conf, cls]
→ xyxy_to_xywh() → 中心点格式 [xc, yc, w, h] + 置信度 + 类别
→ DeepSort.update() → ReID 特征提取 → 级联匹配 → 轨迹输出 Nx7 [x1, y1, x2, y2, cls, track_id, conf]
→ draw_tracking() → 绘制边框 + ID + 帧信息
→ 写入输出视频 + 逐帧结果 txt
设计要点三条:
- 检测与跟踪彻底解耦 :检测端
Detector.detect()输出标准 Nx6 数组,跟踪端DeepSort.update()只认"中心点框 + 置信度 + 类别 + 原图"------只要检测器能产出"框 + 置信度",不限 YOLO、不限框架,转成这个格式接上接口就能用,跟踪端代码零改动。换 YOLO 系模型只改--model一个参数。 - 跳帧不解码 :
--frame-interval控制每隔 N 帧处理一次,跳过的帧用 grab 只推进不解码(零解码成本),需要处理的帧才 retrieve 解码------同一套参数可以在"逐帧"与"抽帧"两种喂帧方式下跑,输出视频帧率按比例缩放保持时间轴一致。 - 每次运行全量落盘:输出 txt 逐帧记录每个检测框的去向(配上了哪条轨迹 / 被置信度门槛过滤),文件头写入本次全部配置参数,后续所有分析、人工核查、基线与改进对比都基于这份 txt,实验完全可追溯。
目录结构
AiMotDeepSort/
├── detector.py # YOLO 检测器封装类(加载模型、设备选择、帧推理、解析结果)
├── README.md # 工程说明(环境安装、快速开始、文档导航)
│
├── 📁 deep_sort/ # DeepSORT 核心实现(原版源码 + 接口封装)
│ ├── deep_sort.py # DeepSort 主类(检测 → 轨迹接口 + 输出组装)
│ ├── __init__.py # 工厂函数 DeepSortTracker(args) + xyxy_to_xywh 格式转换
│ │
│ ├── 📁 sort/ # 轨迹算法核心
│ │ ├── tracker.py # 主跟踪器(置信度过滤 → 级联匹配 + IoU 匹配 → 建档结算)
│ │ ├── kalman_filter.py # 卡尔曼滤波器(8 维状态)
│ │ ├── track.py # 单条轨迹(状态机 Tentative → Confirmed → Deleted)
│ │ ├── nn_matching.py # 外观特征最近邻距离度量(余弦距离)
│ │ ├── detection.py # 检测框 + 特征封装
│ │ ├── iou_matching.py # IoU 代价计算
│ │ ├── linear_assignment.py # 匈牙利分配 / 级联匹配
│ │ ├── preprocessing.py # 检测框 NMS
│ │ └── __init__.py
│ │
│ ├── 📁 deep/ # ReID 外观特征提取
│ │ ├── feature_extractor.py # Extractor(默认加载 ckpt.t7)/ FastReIDExtractor(备选)
│ │ ├── model.py # 特征网络(resnet18 结构 → 128 维)
│ │ ├── resnet.py # ResNet 骨干
│ │ └── 📁 checkpoint/ # ReID 权重目录
│ │ ├── ckpt.t7 # 默认外观权重(--reid-ckpt 默认值,本项目测试全部用它)
│ │ ├── original_ckpt.t7 # 原版权重备份(当前代码未引用)
│ │ ├── market_bot_R50.pth # FastReID 备选权重(当前代码未引用)
│ │ └── resnet18-5c106cde.pth # torchvision ResNet18 ImageNet 预训练骨干权重
│
├── 📁 ultralytics/ # 内置 Ultralytics YOLO 库(8.4.41,仅用于检测,免安装)
│
├── 📁 utils/ # 工具函数
│ ├── __init__.py
│ ├── draw.py # draw_tracking 轨迹绘制(边框、ID、帧信息)
│ ├── logger.py # 日志封装(控制台 + logs/app.log 双通道)
│ ├── paths.py # 路径工具(工程目录路径配置)
│ └── config.py # 配置读取(读 configs/system_config.json)
│
├── 📁 configs/ # 配置文件目录
│ └── system_config.json # 系统配置(日志级别等)
│
├── 📁 logs/ # 运行日志目录
│ └── app.log # 日志输出(utils/logger.py 写入)
│
├── 📁 test/ # 测试与分析脚本
│ ├── test_deepsort.py # 跟踪主测试脚本(检测 → 跟踪 → 可视化 → 输出)
│ ├── frame_viewer.py # 逐帧看片器(结果 txt 叠加原视频逐帧查看)
│ ├── analyze_track_results.py # 单次运行分析(7 张图 + markdown 报告)
│ ├── analyze_track_compare.py # 两次运行对比(参数对比表 + 8 张对比图 + 报告)
│ └── 📁 output/ # 运行输出目录(deepsort_<模型>_<视频>_<时间戳> 运行目录 + 两次对比目录,含视频 / txt / 图表 / 报告)
│
├── 📁 model/ # 模型文件目录
│ ├── yolov8s.pt # YOLOv8s 检测模型(默认模型,全部基线与改进数据基于它)
│ ├── yolo11s.pt # YOLO11s 检测模型
│ ├── yolov5su.pt # YOLOv5su 检测模型
│ └── yolo26s.pt # YOLO26s 检测模型
│
├── 📁 video/ # 视频文件目录
│ ├── dense.mp4 # 密集交叉场景测试视频
│ └── turn.mp4 # 转弯减速场景测试视频
│
└── 📁 doc/ # 项目文档目录
├── 项目介绍.md # 工程详解(架构、参数、算法、改进、结果、工具用法)
├── 项目结构.md # 本文件
├── deepsort源码分析.md # DeepSORT 逐类逐方法源码解读
├── 卡尔曼滤波预测过程.md # 预测 / 投影 / 修正 / 建档四函数实际数字推算
├── deepsort改进点.md # 三项改进的完整七段文档
└── *.png # 文档插图
三、检测器(ultralytics)
3.1 检测器选型
检测端走 ultralytics 框架,凡是符合 ultralytics 框架的 YOLO 检测器都能使用 ------model/ 内置四个系列的 s 档权重:
| 检测器 | 模型文件 | 说明 |
|---|---|---|
| YOLOv8 | model/yolov8s.pt | 经典款,本项目默认模型,全部基线与改进数据基于它 |
| YOLO11 | model/yolo11s.pt | 新一代 |
| YOLO26 | model/yolo26s.pt | 端到端无 NMS 头 |
| YOLOv5 | model/yolov5su.pt | v5 的 anchor-free 升级版(u 版) |
规模档位 :每个系列都有 n / s / m / l / x 五档------n 最小最快,x 最大最准。本项目统一用 s 档:速度比较快,检测精度也适合车流跟踪,是速度与精度的折中。换档只改文件名(如 yolov8n.pt、yolov8m.pt),命令不变。
3.2 检测器参数
跟检测器有关的参数都挂在命令行上:
| 参数 | 默认 | 说明 |
|---|---|---|
--model |
model/yolov8s.pt | 检测模型路径。任何 ultralytics 框架的 YOLO 权重都能填,换系列、换规模只改文件名 |
--conf |
0.25 | 检测端 置信度阈值,低于它的框根本不产出。注意与跟踪端 --min-confidence(默认 0.5)是两道不同的门槛:conf 0.25~0.5 之间的框会被检测出来、写进结果 txt(标记为被过滤),但进不了跟踪。调低检出更多低分框(全被跟踪端扔掉),调高漏检变多 |
--iou |
0.7 | 检测 NMS 的 IoU 阈值,影响同帧重复框的抑制强度 |
--classes |
2 | 类别过滤,填 COCO 类别 ID:2 = car。跟踪器不区分类别,同类目标可合并多个 ID,如跟全部车辆用 --classes 1 2 3 5 7(COCO 80 类里的全部车型);不传则不过滤 |
--imgsz |
640 | 推理输入尺寸(边长像素):填数字用该档位,填 0 自动用视频原始宽高(圆整到 32 的倍数)。实测:720p 用 640 够用(本项目全部测试即此);1080p 用 640 检出目标变少、置信度也变低;用原始分辨率检测最完整,CPU/算力不足时 FPS 掉得很低,GPU 可以支撑原分辨率检测------如果只有 CPU,建议用默认配置,适当提高一点也可以,具体看测试情况 |
本项目所有测试用的都是官方预训练好的通用模型(COCO 80 类),没有针对车辆检测专门训练,通用模型检车会有漏检;跟踪端的外观模型也是原版 DeepSORT 附带的行人重识别权重(ckpt.t7),检车属跨域使用------这两点是本项目后续优化的方向。
imgsz 对比效果 (dense.mp4,yolov8s,两次运行只差 --imgsz):
640×640(默认):

1280×720(视频原分辨率,即 --imgsz 0):

左上角信息条的 num 是本帧检出目标数:原分辨率一档检出更多------远处小目标补齐;代价是速度,FPS 相应下降------原分辨率(填 0)检出最完整但算力开销大,GPU 可以支撑,只有 CPU 建议用默认 640。
四、轨迹算法(DeepSORT)
4.1 轨迹算法整体流程(每帧 update())
#mermaid-svg-9oIiwkmWTIfgFgWc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9oIiwkmWTIfgFgWc .error-icon{fill:#552222;}#mermaid-svg-9oIiwkmWTIfgFgWc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9oIiwkmWTIfgFgWc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9oIiwkmWTIfgFgWc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9oIiwkmWTIfgFgWc .marker.cross{stroke:#333333;}#mermaid-svg-9oIiwkmWTIfgFgWc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9oIiwkmWTIfgFgWc p{margin:0;}#mermaid-svg-9oIiwkmWTIfgFgWc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster-label text{fill:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster-label span{color:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster-label span p{background-color:transparent;}#mermaid-svg-9oIiwkmWTIfgFgWc .label text,#mermaid-svg-9oIiwkmWTIfgFgWc span{fill:#333;color:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc .node rect,#mermaid-svg-9oIiwkmWTIfgFgWc .node circle,#mermaid-svg-9oIiwkmWTIfgFgWc .node ellipse,#mermaid-svg-9oIiwkmWTIfgFgWc .node polygon,#mermaid-svg-9oIiwkmWTIfgFgWc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9oIiwkmWTIfgFgWc .rough-node .label text,#mermaid-svg-9oIiwkmWTIfgFgWc .node .label text,#mermaid-svg-9oIiwkmWTIfgFgWc .image-shape .label,#mermaid-svg-9oIiwkmWTIfgFgWc .icon-shape .label{text-anchor:middle;}#mermaid-svg-9oIiwkmWTIfgFgWc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9oIiwkmWTIfgFgWc .rough-node .label,#mermaid-svg-9oIiwkmWTIfgFgWc .node .label,#mermaid-svg-9oIiwkmWTIfgFgWc .image-shape .label,#mermaid-svg-9oIiwkmWTIfgFgWc .icon-shape .label{text-align:center;}#mermaid-svg-9oIiwkmWTIfgFgWc .node.clickable{cursor:pointer;}#mermaid-svg-9oIiwkmWTIfgFgWc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9oIiwkmWTIfgFgWc .arrowheadPath{fill:#333333;}#mermaid-svg-9oIiwkmWTIfgFgWc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9oIiwkmWTIfgFgWc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9oIiwkmWTIfgFgWc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9oIiwkmWTIfgFgWc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9oIiwkmWTIfgFgWc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9oIiwkmWTIfgFgWc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster text{fill:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc .cluster span{color:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9oIiwkmWTIfgFgWc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9oIiwkmWTIfgFgWc rect.text{fill:none;stroke-width:0;}#mermaid-svg-9oIiwkmWTIfgFgWc .icon-shape,#mermaid-svg-9oIiwkmWTIfgFgWc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9oIiwkmWTIfgFgWc .icon-shape p,#mermaid-svg-9oIiwkmWTIfgFgWc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9oIiwkmWTIfgFgWc .icon-shape .label rect,#mermaid-svg-9oIiwkmWTIfgFgWc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9oIiwkmWTIfgFgWc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9oIiwkmWTIfgFgWc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9oIiwkmWTIfgFgWc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
没过
是,被抑制
否,留下的检测
配上的
落单的确认轨迹
是
否,丢失太久
配上的
仍落单的轨迹
仍落单的检测
配上的轨迹
落单轨迹
否,tentative 未转正
是
本帧配上了检测
本帧没配上检测,连续没配上 ≤ 3 帧
连续没配上 > 3 帧
检测器输出本帧全部检测框
第一步:置信度过滤
conf > min_confidence(如 0.5)?
包装成 Detection 对象
含框坐标、置信度、ReID 外观特征
直接丢弃
det_status 记 -1 被过滤
NMS 过滤:与已留框重叠度
> nms_max_overlap(如默认 1.0)?
丢弃
det_status 记 -1 被过滤
第二步:卡尔曼预测
全部轨迹匀速外推一帧
age+1、time_since_update+1
第三步:两轮匹配
第一轮:外观级联匹配
确认轨迹 vs 全部检测
余弦距离阈值(如 0.2)+ 马氏门控
按丢失帧数分层,丢得少的优先
轨迹卡尔曼修正
外观特征暂存
丢失帧数 ≤ iou_match_tolerance(如 3)?
第二轮:IoU 兜底匹配
未确认轨迹 + 落单近期轨迹 vs 落单检测
1−IoU 代价,阈值(如 0.7,IoU≥0.3 才配)
出局本帧不参与匹配
mark_missed 丢失计数+1
第四步:新建 tentative 轨迹
分配新 ID,卡尔曼按检测框初始化
连续命中 n_init(如 3)次才转正
第五步:轨迹状态结算
hits+1、丢失清零
hits ≥ n_init(如 3)转 Confirmed
tentative 丢失超 tolerance(如 3)删除
确认态丢失超 max_age(如 70)删除
删除轨迹清出列表
确认轨迹本帧特征灌入外观库
输出筛选:是确认轨迹?
不输出
连续没配上检测的帧数
≤ max_output_age(如 3 帧)?
输出轨迹框
位置 = 卡尔曼修正后的检测位置
输出轨迹框
位置 = 卡尔曼惯性预测框
短暂遮挡时画面上的框不断
流程图文字说明:
第一步:置信度过滤与 NMS 过滤。检测器输出的检测框先过置信度门槛(如 0.5),没过的直接丢弃;通过的包装成 Detection 对象(含框坐标和 ReID 外观特征),再过 NMS 滤掉与已留框重叠超阈值的检测(nms_max_overlap 默认 1.0 即不滤,因 YOLO 已做过 NMS,可调小启用)。
第二步:卡尔曼预测。全部轨迹按匀速模型外推一帧到当前时刻,age 和丢失计数各 +1------匹配还没开始,位置先预测到位。
第三步:两轮匹配。先外观级联、再 IoU 兜底,分两轮进行。
第一轮:外观级联匹配。确认轨迹 vs 全部检测,余弦距离阈值(如 0.2)加马氏门控,按丢失帧数分层配对、丢得少的轨迹优先。配上的轨迹用检测框做卡尔曼修正;落单的确认轨迹看丢失帧数:没超过容忍值(如 3)的进第二轮,超了的出局(预测框飘了,IoU 不可信)。
第二轮:IoU 兜底匹配。未确认轨迹 + 第一轮落单的近期轨迹 vs 落单检测,纯框位置的 1−IoU 代价,阈值(如 0.7,即 IoU ≥ 0.3 才配)。这是短暂漏检目标的续命通道:外观轮没认出来,位置还压得够近就续上。仍落单的轨迹丢失计数 +1 等下帧,仍落单的检测去建档。
第四步:新建轨迹。两轮全落单的检测新建 tentative 轨迹、分配新 ID(永不复用),卡尔曼按检测框初始化;要连续命中 n_init(如 3)次才转正,转正考试没过(丢失超容忍值)直接删除------只活一两帧的轨迹八成是误检。
第五步:状态结算与输出。配上的轨迹修正、计数;落单的按状态判生死(tentative 超容忍值删、确认态超 max_age 删);删除的清出列表,确认轨迹的本帧特征灌入外观库。最后做输出筛选:只输出确认轨迹------本帧配上检测的,输出卡尔曼修正后的框;本帧没配上检测、但连续没配上帧数在 max_output_age(如 3 帧)内的,按卡尔曼惯性预测位置继续输出(短暂遮挡时画面上的框不闪断);tentative 未转正的和连续没配上超 3 帧的确认轨迹不输出。
4.2 原理简述
DeepSORT 的核心思想一句话:高置信度 + 外观特征路线------低分框是噪音直接扔,靠"运动预测(卡尔曼滤波)+ 外观相似度(ReID 特征最近邻)"双证据做关联,比纯几何 IoU 更能穿越遮挡。
- 卡尔曼滤波负责每条轨迹的运动预测:每帧先 predict 出预测框,拿预测框去和检测框匹配,配上后再 update 修正状态;
- 两轮匹配各有分工:外观级联是主力(确认轨迹凭"长得像"续接,还能容忍丢失几帧),IoU 匹配是兜底(级联没配上的短丢失轨迹 + 未确认轨迹凭位置重叠续接);
- 轨迹状态机三态:Tentative(未确认,攒够 n-init 次命中转正)→ Confirmed(确认,正式输出)→ Deleted(未确认丢失超容忍 / 确认丢失超 max-age)。
一个对后续改进很关键的事实:原版三条"容忍度"全部焊死且极紧------IoU 轮只收丢失 1 帧的确认轨迹、未确认轨迹丢 1 帧直接删、输出只给 1 帧惯性。车辆场景的短漏检、短遮挡反复撞在这三堵墙上,这就是三项改进的由来。
详细原理见两篇深度文档:
- deepsort源码分析.md(逐类逐方法源码解读)
- 卡尔曼滤波预测过程.md(预测/投影/修正/建档四函数实际数字推算)
4.3 改进一:IoU 匹配容忍丢失帧数(--iou-match-tolerance,dense.mp4)
问题 :原版 IoU 轮只允许刚丢失 1 帧 (time_since_update == 1)的确认轨迹参加。轨迹丢失 2~3 帧后,即使外观级联失败、检测框就在旁边,也进不了 IoU 营救轮------检测重新出现时只能新建轨迹,目标断裂换 ID。
方案 :== 1 放宽为 <= iou_match_tolerance(默认 3),并在 IoU 代价计算内把超容忍轨迹的代价置无穷(防止未确认轨迹绕过候选过滤)。
效果 :轨迹总 ID 92 → 76 ,2~3 处理帧空窗保住同 ID 11 处 → 28 处,零僵尸错抢。三个改进中效果最大。
bash
--iou-match-tolerance 3 # 默认即 3(改进值);传 1 = 原版行为
4.4 改进二:确认阶段容忍丢失帧数(--tentative-tolerance,dense.mp4)
问题 :未确认轨迹(刚建档还没攒够 3 次命中)只要一帧没配上就立即删除,没有容忍判断------"2 命中 + 短暂漏检"的目标被拆成两个 ID,且碎片永远无法确认上屏。
方案 :未确认轨迹丢失时先看 time_since_update > tentative_tolerance(默认 3)再决定删不删,短漏检不死。
效果 :轨迹总 ID 85 → 76 ,从未确认轨迹 24 → 15,目标更早确认上屏。效果次于改进一,且与改进一部分重叠( tentative 轨迹要"活着 + 匹配上"两道闸门同时打开才能救回)。
bash
--tentative-tolerance 3 # 默认即 3(改进值);传 0 = 原版行为
4.5 改进三:轨迹输出允许的最大丢失帧数(--max-output-age,dense.mp4)
问题 :输出门控固定"丢失超过 1 帧就不输出",短暂遮挡时画面框闪断,人眼看是目标消失又冒出来。
方案 :固定值 1 换成 max_output_age(默认 3),丢失后最多 3 个处理帧继续输出卡尔曼预测框。
效果 :不参与预测/匹配/轨迹生命周期,两份结果 txt 正文逐字节一致即为证明;惯性输出框 75 → 182,短暂遮挡画面框不闪断;代价是目标真正离开后"幽灵框"最多多显示 2 个处理帧。
bash
--max-output-age 3 # 默认即 3(改进值);传 1 = 原版行为
三项改进的完整内容(源码位置、改进代码、测试对比、分析意义)见:deepsort改进点.md。注意与"改进开关默认关闭"的工程不同:本工程三项改进的默认值就是改进值,不传参数即为改进后行为,跑原版行为要显式传原版参数值。
五、改进测试
验证铁律:同视频、同参数,基线与改进两次运行只差一个参数------其余参数全用默认值,基线组显式传原版值。
5.1 三组对照命令(dense.mp4,--frame-interval=5)
bash
# 改进一:IoU 匹配容忍丢失帧数
python .\test\test_deepsort.py --frame-interval=5 --iou-match-tolerance=3 # 改进组(3 即默认值)
python .\test\test_deepsort.py --frame-interval=5 --iou-match-tolerance=1 # 基线组(原版)
# 改进二:确认阶段容忍丢失帧数
python .\test\test_deepsort.py --frame-interval=5 --tentative-tolerance=3 # 改进组
python .\test\test_deepsort.py --frame-interval=5 --tentative-tolerance=0 # 基线组(原版)
# 改进三:轨迹输出允许的最大丢失帧数
python .\test\test_deepsort.py --frame-interval=5 --max-output-age=3 # 改进组
python .\test\test_deepsort.py --frame-interval=5 --max-output-age=1 # 基线组(原版)
5.2 核查与对比
基线和改进各跑一次后:
- 逐帧人工核查:用 frame_viewer.py 打开基线结果,肉眼确认问题点(哪辆车在第几帧换了 ID);再打开改进结果逐处对照;
- 自动对比:analyze_track_compare.py 把两次运行的 txt 全套指标画成对比图,轨迹生命周期图最直观(改进有没有压掉短命碎片轨迹一眼看出)。
两个工具的具体用法见第七章。
六、结果对比
6.1 改进一实测(dense.mp4,容忍 3 帧 vs 原版 1 帧)
| 指标 | 改进(3) | 原版(1) |
|---|---|---|
| 轨迹总 ID | 76 | 92 |
| 从未确认轨迹(❤️ 次命中) | 15 | 28 |
| 平均轨迹长度(次) | 16.9 | 14.0 |
| 长轨迹(≥10 次) | 44 | 43 |
| 2~3 处理帧空窗保住同 ID | 28 处 | 11 处 |
6.2 改进二实测(dense.mp4,容忍 3 帧 vs 原版丢 1 帧即删)
| 指标 | 改进(3) | 原版(0) |
|---|---|---|
| 轨迹总 ID | 76 | 85 |
| 从未确认轨迹 | 15 | 24 |
| 平均轨迹长度(次) | 16.9 | 15.2 |
| 长轨迹(≥10 次) | 44 | 43 |
6.3 改进三实测(dense.mp4,惯性 3 帧 vs 原版 1 帧)
| 指标 | 改进(3) | 原版(1) |
|---|---|---|
| 匹配/轨迹结果(txt 正文) | 完全一致 | 完全一致 |
| 确认轨迹匹配输出框 | 1147 | 1147 |
| 惯性输出框(卡尔曼预测) | 182 | 75 |
三项改进全部有效且方向正确:少产生碎片 ID、短空窗保住 ID、短暂遮挡画面框不闪断。局限:验证基于单一场景(dense.mp4 车流),参数值 3 的最优性未做扫描。
七、测试分析工具
test/ 目录四个脚本构成完整工具链:跑测试 → 逐帧看片 → 单次分析 → 两次对比。
7.1 跟踪主测试脚本(test_deepsort.py)
检测 → 跟踪 → 可视化 → 输出,一条命令跑完整流水线(第五章所有命令都是它)。输出目录名 = deepsort_<模型>_<视频>_<时间戳>(在 test/output/ 下),内含可视化视频 + 逐帧结果 txt(开头记录本次全部配置参数,保证可复现)。
完整参数表:
| 参数 | 默认 | 说明 |
|---|---|---|
--video |
video/dense.mp4 | 输入视频 |
--camera |
-1 | 摄像头索引(-1 用视频文件,0/1/2 用对应摄像头) |
--frame-interval |
1 | 帧间隔,每隔 N 帧处理一次,跳过的帧不解码(本项目测试用 5) |
--display |
关 | 弹出实时跟踪窗口 |
--model |
model/yolov8s.pt | 检测模型路径(选型与参数详见第三章) |
--conf |
0.25 | 检测端置信度阈值 |
--iou |
0.7 | 检测 NMS IoU 阈值 |
--classes |
2 | 类别过滤(2 = car,只跟车) |
--imgsz |
640 | 推理输入尺寸,0 = 视频原始宽高(圆整 32 倍数) |
--reid-ckpt |
deep_sort/deep/checkpoint/ckpt.t7 | ReID 外观模型权重 |
--max-dist |
0.2 | 外观余弦距离阈值(级联匹配门槛) |
--min-confidence |
0.5 | 跟踪端置信度门槛,低于它的检测框不进跟踪 |
--nms-max-overlap |
1.0 | 检测框级 NMS 重叠度,1.0 = 关闭(YOLO 已做 NMS) |
--max-iou-distance |
0.7 | IoU 匹配最大距离 |
--max-age |
70 | 确认轨迹最大保留帧数 |
--n-init |
3 | 轨迹确认所需命中帧数 |
--nn-budget |
100 | 每条轨迹外观特征库容量 |
--max-output-age |
3 | 改进三:输出允许的最大丢失帧数(原版 1) |
--tentative-tolerance |
3 | 改进二:未确认轨迹容忍丢失帧数(原版 0,丢 1 帧即删) |
--iou-match-tolerance |
3 | 改进一:IoU 匹配容忍丢失帧数(原版 1) |
结果 txt 格式(后续所有分析和核查都基于它):
frame:110, detect:22 , trackfilter:5 , match:17 , unmatch:0 ← 帧汇总
detect:1 , trackid:12 , conf:0.77, match:true , box:[235.3,504.3,333.8,566.5] ← 框1配上了轨迹12
detect:2 , trackid:35 , conf:0.66, match:true , box:[241.0,507.0,338.2,569.8] ← 框2配上了轨迹35(当帧新建也算配上)
detect:3 , trackid:N , conf:0.32, match:false, box:[...] ← 框3被置信度门槛过滤
帧汇总行的 match 计数含当帧新建轨迹的框;unmatch 恒为 0 属正常------DeepSORT 中每个通过置信度门槛的检测要么配上已有轨迹、要么新建轨迹,天然没有第三种去向;conf 显示值截断只舍不入,保证显示值不超过原始值。
7.2 逐帧看片器(frame_viewer.py)
把结果 txt 的检测框和 ID 叠加到原视频上逐帧查看,人工核查轨迹问题(纯看片,问题点手工记录)。
实际例子(核查 dense.mp4 默认参数运行):
bash
python test/frame_viewer.py --video video/dense.mp4 --txt test\output\deepsort_yolov8s_dense_1013_161513\dense.txt
# 打开窗口 → 按 g 输入处理帧序号回车跳转 → ←→ 单帧前后翻,盯同一辆车的 ID 数字变没变
| 按键 | 功能 |
|---|---|
← / → |
上一帧 / 下一帧(处理帧单步) |
↑ / ↓ |
大步进 |
空格 |
播放 / 暂停 |
g |
跳帧模式:输处理帧序号回车跳转 |
e |
设置步长 |
q |
退出 |
不传 --txt 则只看视频不画框;--scale 可设初始显示缩放。注意跳帧单位是处理帧序号(txt 里的 frame 行序),与源视频帧号在 frame-interval > 1 时不同。
看片效果(dense.mp4 运行结果):

打开后停在第 1 帧、处于暂停态。窗口左上角信息条黑底黄字实时显示 frame:当前处理帧/总处理帧数、step:10(步长)、PAUSE(播放状态)和按键提示;画面上每辆车叠加绿色检测框,框左上角的绿色数字就是轨迹 ID。看片时重点盯两件事:翻帧时某辆车的数字突然变了 = 换 ID(断裂重建,改进一、二的针对对象),翻帧时某辆车的框闪断消失又出现 = 短暂遮挡输出闪断(改进三的针对对象)------配合 g 跳到问题帧、← / → 单帧前后翻,即可逐处核查第五章的对比结果。
7.3 单次运行分析(analyze_track_results.py)
对一次运行的 txt 做整体画像。全部输出保存在 txt 同目录:7 张 png 图 + 1 份 markdown 分析报告(dense_report.md),报告内嵌全部图表,每张图都附解读。
报告图表清单:
| 图表 | 文件 | 看什么 |
|---|---|---|
| 图1 逐帧指标数量 | *_frame_overview.png | 每帧检测数 / 被过滤数 / 匹配成功数 / 未匹配数四条曲线------检测稳不稳、框有没有被用上 |
| 图2 性能指标逐帧变化 | *_performance_indicators.png | 被过滤率 / 匹配率 / 未匹配率三条比例曲线 |
| 图3 轨迹路径图 | *_track_paths.png | 每条轨迹中心点的移动路径(起点/终点标记)------轨迹抖动、断裂一眼看出 |
| 图4 轨迹生命周期 | *_track_lifecycle.png | 每条轨迹 ID 的存活时间段横条------短命碎片轨迹直接暴露 |
| 图5 轨迹 ID 出现频率 | *_track_id_frequency.png | 每个轨迹 ID 出现的帧数柱状图 |
| 图6 轨迹置信度散点 | *_track_confidence_scatter.png | 每条轨迹的置信度分布------哪条轨迹长期低置信度 |
| 图7 置信度分布 | *_confidence_distribution.png | 匹配 / 被过滤框按置信度区间并排统计 |
实际例子:
bash
python test/analyze_track_results.py test\output\deepsort_yolov8s_dense_1013_161513\dense.txt
# → 同目录生成 dense_*.png + dense_report.md
报告图表实测效果(dense.mp4 默认参数运行):
图1 逐帧指标数量:

图2 性能指标逐帧变化:

图3 轨迹路径图:

图4 轨迹生命周期:

图5 轨迹 ID 出现频率:

图6 轨迹置信度散点:

图7 置信度分布:

适合场景:拿到一次新结果(比如第一次换视频或换参数跑),先整体看画像------检测数是否稳定、置信度分布是否健康、被过滤的框多不多------再决定要不要深入逐帧排查。
7.4 两次运行对比(analyze_track_compare.py)
把基线和改进的两次运行放一起对比。输出保存在 test/output/ 下以两次运行名命名的目录 (<txtA名>_<nameA>_vs_<txtB名>_<nameB>):1 张参数配置对比表 + 8 张对比图(compare_*.png)+ 1 份对比报告(compare_report.md)。
报告图表清单:
| 图表 | 文件 | 看什么 |
|---|---|---|
| 表1 参数配置对比 | compare_config_compare.png | 两次运行的检测器与跟踪全部参数并排列出,不同的行自动标红------两次运行到底差在哪一眼看出 |
| 图1 逐帧指标数量对比 | compare_frame_overview_compare.png | 检测数 / 过滤数 / 匹配数 / 未匹配数逐帧并排 |
| 图2 性能指标对比 | compare_performance_indicators_compare.png | 匹配率等比例曲线逐帧并排 |
| 图3 轨迹路径对比 | compare_track_paths_compare.png | 两次运行的轨迹路径并排 |
| 图4 轨迹生命周期对比 | compare_track_lifecycle_compare.png | 轨迹存活时间段并排------改进有没有压掉碎片轨迹,这张最直观 |
| 图5 ID 出现频率对比 | compare_track_id_frequency_compare.png | 每个轨迹 ID 的出现帧数并排 |
| 图6 置信度散点对比 | compare_track_confidence_scatter_compare.png | 每条轨迹的置信度分布并排 |
| 图7 置信度分布对比 | compare_confidence_distribution_compare.png | 匹配 / 被过滤框的置信度区间分布并排 |
| 图8 关键性能指标对比 | compare_key_indicators_compare.png | 两次运行的关键指标汇总对比------改动有没有效先看这张 |
实际例子(改进一验证:基线组对改进组):
基线组显式传原版值(--iou-match-tolerance=1,其余参数默认),改进组即默认参数运行(默认 3),两次只差这一个参数:
bash
python test/analyze_track_compare.py --txt_path_a test\output\deepsort_yolov8s_dense_1016_161126\dense.txt --name_a baseline --txt_path_b test\output\deepsort_yolov8s_dense_1013_161513\dense.txt --name_b improve1
# → test/output/ 下生成 dense_baseline_vs_dense_improve1/ 目录,内含 compare_*.png + compare_report.md
--name_a/--name_b 不只用在图表显示名,输出目录名也用它拼,建议用英文避免路径出现中文。
报告图表实测效果(dense.mp4 改进一:基线 1 vs 改进 3):
表1 参数配置对比:

图1 逐帧指标数量对比:

图2 性能指标对比:

图3 轨迹路径对比:

图4 轨迹生命周期对比:

图5 ID 出现频率对比:

图6 置信度散点对比:

图7 置信度分布对比:

图8 关键性能指标对比:

适合场景:任何两次运行对比------基线对改进参数、参数调前对调后、模型对模型。
八、总结
本项目围绕车流场景搭建了一套检测与跟踪解耦的多目标跟踪系统:检测端基于 Ultralytics 框架,任意 YOLO 系列模型即插即换,并用 frame-interval 提供跳帧不解码的喂帧方式;跟踪端保留 DeepSORT 原版核心代码(sort/ 轨迹算法 + deep/ ReID 特征提取),接口层独立封装。针对车流密集交叉场景下轨迹 ID 不稳定的三类问题,实现了 IoU 匹配容忍、确认阶段容忍、输出惯性帧数三项改进,全部以命令行参数提供,默认值即改进值,显式传原版值即可回到原版行为。同时配套了从运行测试、逐帧核查、单次分析到两次运行对比的完整工具链。
三项改进均经过同视频同参数的基线对照实验验证有效:IoU 匹配容忍与确认阶段容忍解决了目标断裂换 ID 的问题,输出惯性帧数解决了短暂遮挡画面框闪断的问题(且不影响任何跟踪结果)。
项目仍有局限:检测端使用官方通用预训练模型,未针对车辆专门训练,检车漏检偏多;跟踪端外观模型沿用原版 DeepSORT 附带的行人重识别权重(ckpt.t7),检车属跨域使用,外观匹配的判别力打折扣;长时遮挡(超出 max-age 保留期)后的目标重识别尚未解决。
后续优化方向:一是专门训练车辆检测模型,提高检车能力、减少漏检;二是换用车辆重识别权重(或在本场景微调外观模型),让外观级联匹配对车辆真正有效;三是长时遮挡后的重识别,需要更强的外观记忆与检索机制。
九、 链接
| 描述 | 链接 |
|---|---|
| 代码 | 基于 Ultralytics + DeepSORT 多目标跟踪与改进 |
| 往期回顾 | 链接 |
| 数据集 | 数据集汇总 |
| 软件开发 | 深度学习软件开发汇总 |