【AI视觉软件 03】基于 Ultralytics + DeepSORT 的多目标跟踪与改进(支持YOLO全系列,重点车跟踪)

一、介绍

车流统计是智能交通分析的基础,流量、流速、行驶轨迹都建立在"每辆车被持续稳定跟住"之上,这一步就是多目标跟踪(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

设计要点三条:

  1. 检测与跟踪彻底解耦 :检测端 Detector.detect() 输出标准 Nx6 数组,跟踪端 DeepSort.update() 只认"中心点框 + 置信度 + 类别 + 原图"------只要检测器能产出"框 + 置信度",不限 YOLO、不限框架,转成这个格式接上接口就能用,跟踪端代码零改动。换 YOLO 系模型只改 --model 一个参数。
  2. 跳帧不解码 :--frame-interval 控制每隔 N 帧处理一次,跳过的帧用 grab 只推进不解码(零解码成本),需要处理的帧才 retrieve 解码------同一套参数可以在"逐帧"与"抽帧"两种喂帧方式下跑,输出视频帧率按比例缩放保持时间轴一致。
  3. 每次运行全量落盘:输出 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 帧惯性。车辆场景的短漏检、短遮挡反复撞在这三堵墙上,这就是三项改进的由来。

详细原理见两篇深度文档:

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 核查与对比

基线和改进各跑一次后:

  1. 逐帧人工核查:用 frame_viewer.py 打开基线结果,肉眼确认问题点(哪辆车在第几帧换了 ID);再打开改进结果逐处对照;
  2. 自动对比: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 多目标跟踪与改进
往期回顾 链接
数据集 数据集汇总
软件开发 深度学习软件开发汇总
相关推荐
GJGCY1 小时前
金融AI Agent厂商怎么选?系统集成、API/RPA、权限审计与异常恢复能力评估
人工智能·经验分享·ai·金融·智能体
D_C_tyu1 小时前
AI Vibe Coding | AI能力评估提示词参考
人工智能·提示词·vibe coding
IT_陈寒1 小时前
Python的列表推导式用不好真的会爆内存你知道吗
前端·人工智能·后端
ZhengEnCi1 小时前
vLLM是什么:PagedAttention 让大模型推理快 2-4 倍,还能撑起更长思维链
人工智能·性能优化·llm
注塑机数据采集联网2 小时前
注塑机生产管控MES系统应用
大数据·运维·服务器·人工智能·物联网
kyriewen2 小时前
我算了笔账:AI 编程工具一年烧掉两台 MacBook
前端·人工智能·程序员
云和数据.ChenGuang2 小时前
Python 常见特殊方法详解:__new__、__init__、__del__、__str__、__repr__ 与 __getattribute__
开发语言·人工智能·python·1024程序员节
一个简单的TTS开发者2 小时前
#有声小说多角色对话 TTS 配置与选型指南
人工智能·开源·indextts
没落之王2 小时前
历史系统建模:宋初太祖太宗「再立天庭」秩序重构推演
人工智能·算法·重构