简介:针对YOLOv5与DeepSORT的多目标跟踪仿真记录资源,面向计算机视觉初学者或需要快速搭建行人检测与跟踪流程的开发者。项目以视频中的人物为对象,逐帧执行目标检测、ID分配与轨迹绘制,并统计停留时间、平均速度等信息,可扩展用于客流分析、安防监控等场景。压缩包内共4个文件:两个Python脚本分别实现带MHCNN人脸模糊与不带人脸模糊的跟踪方案,便于对比隐私保护效果;一个Markdown说明文档用于梳理运行方法与参数;一个License文件明确使用规范。整体仅19KB,轻量紧凑,适合直接阅读代码与快速移植。目前已有144人学习下载。通过该资源,使用者可理解YOLOv5检测与DeepSORT跟踪的串联方式,掌握轨迹字典构建、定时CSV输出等工程细节,并借助可选的MHCNN模块了解在跟踪流程中集成隐私保护功能的基本思路。
1. 基于YOLOv5和DeepSORT的多目标跟踪仿真与记录:先把检测和记录打通,再谈跟踪效果
把目标检测和跟踪串起来,还要把每一个目标从出现到消失的轨迹完整落盘,这件事单独看并不复杂,一旦放到仿真环境里做,很多人第一轮就会翻车。所谓“仿真与记录”,不是指拿一套合成数据集跑完就丢,而是用一段可复现的视频流或仿真场景,让YOLOv5做检测、DeepSORT做帧间关联,并把每帧每个目标的目标ID、边框坐标和置信度结构化地记录下来,方便后面做轨迹回放、MOTA评估和ID切换分析。
这篇文章写给三类人:正在做交通流量统计、安防联动或者无人机视角目标分析的工程师;想在本地复现YOLOv5加DeepSORT多目标跟踪效果的初学者;以及已经把模型训练好、但发现跟踪结果无法量化评估的从业者。核心结论先说在前面:多目标跟踪的瓶颈通常不在跟踪器,而在检测置信度阈值、后处理过滤和记录格式这三个容易被忽视的环节。
2. YOLOv5和DeepSORT的分工:检测是上限,关联是下限
2.1 YOLOv5只负责“看见”,DeepSORT负责“认出是同一个人”
YOLOv5这个环节解决的是一个静态问题:给定一帧图像,输出图像里有哪些目标、每个目标的位置是什么。它的输出一般是四类信息:边框坐标、置信度、类别ID、类别标签。DeepSORT在这个基础上解决的是一个动态问题:上一帧里那个ID为3的目标,在这一帧里应该还是ID为3,而不是新建一个ID 4。
很多第一次接触这套技术栈的人,会默认把YOLOv5的检测框直接当成跟踪结果,导致相邻两帧之间同一个人的ID不停地变。原因在于目标检测是逐帧独立的,它不记得任何历史信息。DeepSORT的输入不只是这一帧的检测框,还包括上一帧的跟踪状态、目标的外观特征向量以及运动状态。只有把这些信息都接上,多目标跟踪才成立。
在接入DeepSORT之前,YOLOv5的输出还需要经过后处理。常见做法是对每个检测框执行NMS去重,然后按照conf_thres过滤掉低置信度框,再按iou_thres剔除同一目标上的重复框。这一步决定了进入跟踪器的检测质量。我一般会把conf_thres设得比纯检测场景高一点,比如0.35到0.45,因为低置信度的抖动框会严重污染DeepSORT的级联匹配结果。
2.2 级联匹配与卡尔曼滤波:跟踪器内部的三个关键参数
DeepSORT的“SORT”部分依赖卡尔曼滤波,它用上一帧的目标位置和速度,外推出这一帧目标可能出现的位置,并给出预测边框。卡尔曼滤波器维护的状态包含边框中心坐标、宽高比、高度以及对应的速度分量。这个预测框再和这一帧的检测框做匹配,匹配依据是马氏距离和外观余弦距离的加权和。
级联匹配是DeepSORT区别于SORT的核心机制。它优先把近期连续确认过的轨迹和当前检测框进行匹配,这样能够显著减少ID切换。实际使用中我会关注三个参数:max_dist控制匹配的最大距离,max_age控制轨迹丢失后允许存活的最大帧数,n_init控制轨迹在确认之前最少需要连续匹配多少帧。这三个参数直接决定了一个目标被遮挡几帧后被判定为“新目标”,以及一个误检到底有多大的概率被当成真目标跟踪下去。
如果你打算做DeepSORT改进,大多数改动也集中在这三条线上:换一个更强的ReID特征提取网络;把马氏距离和余弦距离的加权方式换成自适应权重;或者把线性卡尔曼滤波换成更复杂的运动模型。但无论怎么改,检测器的输出质量决定了整个跟踪系统的上限,这一点无法回避。
2.3 仿真输入与真实视频回放的差别:请把时间戳当成一等公民
在这一类仿真任务里,输入通常有两种形式。第一种是使用CARLA、AirSim这类仿真器生成连续帧,第二种是回放一段真实监控或驾驶视频,把它当作“数据源”做离线测试。两种做法在跟踪算法层面没有本质区别,但在帧率稳定性上有明显差异。仿真器生成的视频帧通常帧率恒定,而真实视频本身帧率是固定的,但你的推理耗时会导致每帧之间的处理间隔抖动。
如果只把视频帧喂给YOLOv5和DeepSORT,而不记录每一帧对应的全局帧号和时间戳,后期做轨迹回放时会发现目标位置和视频内容对不上,看上去像画面卡顿,实际是记录阶段把时间信息漏掉了。正确做法是从视频流读取时就把帧号和时间戳写进数据结构里,与检测结果、跟踪结果一起落盘。
在仿真环境里还有一个容易踩坑的地方:YOLOv5的检测后处理会输出三个尺度的预测框,经过NMS后已经做了尺度归一,DeepSORT拿到的检测框坐标通常是原图像素坐标。如果你在仿真的虚拟相机输出中直接使用了模型输出的坐标而忘了做像素坐标映射,跟踪框会整体偏移一个固定量,这个误差在静止场景里不容易被发现,一旦相机移动就会暴露。
2.4 从输入帧到跟踪结果的完整数据流
为了让整个链路更直观,下面这个伪代码展示了从一帧画面到DeepSORT更新的核心流程:
# 伪代码:体现检测结果如何进入跟踪器 frame = video_capture.read() # YOLOv5 前向推理,得到原始预测 predictions = yolo_model(frame) # 后处理:NMS 去除重复框,conf_thres 过滤低置信度 keep = nms(predictions, conf_thres=0.35, iou_thres=0.45) filtered = predictions[keep] # 组装检测器输出:列表里每个元素是 [x1, y1, x2, y2, conf, cls] detections = [ [box[0], box[1], box[2], box[3], score, cls_id] for box, score, cls_id in filtered ] # 送入 DeepSORT,更新轨迹 tracks = tracker.update(detections, frame)这段伪代码中的nms和conf_thres是两个最值得反复调节的点。NMS的iou_thres设置过高会导致同一个目标输出多个重叠框,跟踪器会把这些重叠框当作多个目标处理,短时间出现大量新ID;设置过低会把同一个目标压成一个框,但如果目标之间重叠较大,也会误删。我更倾向于把iou_thres保持在0.45到0.5之间,然后靠conf_thres和max_age配合来控制跟踪稳定性。
3. 在本地跑通最小跟踪链路:conda环境配置与对接命令
3.1 用conda建一个尽量干净的环境
YOLOv5和DeepSORT这套组合最大的环境问题是PyTorch版本的兼容性。YOLOv5官方源码更新比较频繁,很多自带requirements文件写得比较激进,而DeepSORT的常见开源实现往往依赖较老的特征提取权重。我的习惯是先创建一个独立的conda环境,只装必须的包,不给系统Python环境留任何机会被搞乱。
# 创建独立环境,Python 3.9 是目前兼容性最好的选择 conda create -n mot python=3.9 -y conda activate mot # 进入 YOLOv5 源码目录,安装依赖 cd yolov5 pip install -r requirements.txt # DeepSORT 常用依赖 pip install numpy opencv-python scipy scikit-learn tqdm matplotlib为什么选Python 3.9而不是3.11或3.12?因为YOLOv5的某些依赖在旧版本里没有预编译包,如果从源码编译会浪费大量时间。Python 3.9有最多兼容的wheel,基本可以做到一条pip命令装完。如果你用的是树莓派5这类边缘设备,这个环境配置过程要更加保守,先把torch版本确认好再装其他包,否则容易出现依赖冲突导致whl安装失败。
3.2 用YOLOv5先跑一遍检测,确认后处理参数没毛病
在接入DeepSORT之前,先把YOLOv5的检测单独跑通,并且确认它的输出形式。这里有一个容易被忽略的点:YOLOv5的detect.py默认保存的是归一化后的label文件,不是可视化图像里直接可用的坐标。对跟踪任务来说,我们更关心每一帧检测框的原始坐标和置信度,所以需要在接入前把输出格式调整成x1 y1 x2 y2 conf cls。
# 在视频上直接跑 YOLOv5 检测 python detect.py \ --source demo.mp4 \ --weights yolov5s.pt \ --conf-thres 0.35 \ --iou-thres 0.45 \ --save-txt \ --save-conf \ --project ./detect_output这里的--save-txt会生成每个视频帧对应的txt文件,文件里每一行是类别ID、归一化坐标和置信度。--save-conf会在txt里追加confidence字段。先跑这一步的目的不是看检测效果,而是确认模型权重能加载、视频能被正常解码、后处理参数不会把大量真实目标过滤掉。如果你在这个阶段发现置信度很低的目标数量特别多,说明模型在你的场景里没有充分适配,不要急着调跟踪器,先回到训练数据看一眼。
3.3 把YOLOv5检测结果喂给DeepSORT:最小可复现脚本
正式对接时,我会把YOLOv5的推理封装成一个函数,让它返回[x1, y1, x2, y2, conf, cls]列表。DeepSORT的常见实现会提供一个跟踪器类,核心接口是update(detections, frame)。这个接口接收的是边框列表和原图,返回的是[x1, y1, x2, y2, track_id, conf]形式的结果。
import cv2 import torch from deep_sort import DeepSort # 初始化跟踪器,ckpt.t7 是 ReID 特征提取权重 tracker = DeepSort( model_path="deep_sort_pytorch/deep_sort/deep/checkpoint/ckpt.t7", max_dist=0.2, max_iou_distance=0.7, max_age=70, n_init=3, nn_budget=100, ) # YOLOv5 推理函数,返回过滤后的检测框 def get_detections(frame): results = yolo_model(frame)[0] dets = [] for det in results.boxes: x1, y1, x2, y2 = det.xyxy[0].tolist() conf = det.conf.item() cls = det.cls.item() # 这里根据实际场景过滤类别,比如只要 person 和 car if cls in valid_class_ids and conf >= 0.35: dets.append([x1, y1, x2, y2, conf, cls]) return dets cap = cv2.VideoCapture("demo.mp4") while True: ret, frame = cap.read() if not ret: break detections = get_detections(frame) # tracker.update 返回跟踪结果,包含目标 ID outputs = tracker.update(np.asarray(detections), frame) if outputs is not None: for track in outputs: x1, y1, x2, y2, track_id, conf = track # 这里做绘制和记录 # 这里写逐帧处理后的视频 cap.release()这个脚本有两个值得注意的地方。第一个是tracker.update传入的检测框坐标必须是原图像素坐标,如果你从YOLOv5的datasets接口里拿到的框是归一化坐标,要做逆变换。第二个是nn_budget参数,它控制特征队列里保存的历史特征数量,如果设置过小,稍微遮挡几帧就会因为特征不足而重新匹配失败;设置过大又会让余弦距离计算变慢。我一般会放在100到150之间,这个区间在多数场景里性价比最高。
4. 训练自己的YOLOv5模型:让跟踪不再追着“未知目标”跑
4.1 数据集目录结构与标注格式
很多人把多目标跟踪效果差归结为DeepSORT调参问题,但实际上更常见的原因是YOLOv5检测器在你的场景里召回率太低。如果行人经常侧身、遮挡,或者目标尺寸很小,训练集如果不包含这些样本,检测器就会漏检。漏检不会导致ID切换,但会导致目标轨迹断掉,DeepSORT在max_age限制之后会把该目标判定为离开,然后重新分配ID。所以想让跟踪稳定,先把训练数据做扎实。
做数据集的第一步是把目录结构固定下来,YOLOv5的默认约定是images和labels两个目录平行,且图片和标签的绝对路径可以互相推导。下面是一个典型结构:
custom_data/ images/ train/ frame_0001.jpg frame_0002.jpg val/ frame_0500.jpg labels/ train/ frame_0001.txt frame_0002.txt val/ frame_0500.txtlabels里每行对应一个目标,格式是class_id x_center y_center width height,四个坐标值都是相对图片宽高的归一化值。比如一张1280乘720的图上,一个宽200像素、高400像素、中心点为(400, 300)的行人,对应的一行应该是:
0 0.3125 0.4167 0.15625 0.5556如果你用的是LabelImg这类标注工具,它默认输出的是YOLO格式。标注癖好上有一点要克制:不要为了让检测器“更好学”而把目标框标得过大,宁可多标一些紧贴目标的框,也不要给目标加上大片背景。边框过大会让后续DeepSORT的ReID特征提取器截取到大量背景像素,导致同一个目标在不同帧里的特征差异变大,最终拉着余弦距离一起漂移。
4.2 训练命令与yolov5超参数调整
训练自己的数据集时,最基础的做法是从YOLOv5官方预训练权重开始微调。常见命令如下:
python train.py \ --data data/custom.yaml \ --weights yolov5s.pt \ --epochs 150 \ --batch-size 16 \ --imgsz 640 \ --hyp data/hyps/hyp.scratch-low.yaml \ --device 0其中data/custom.yaml里最关键的是nc和names两个字段,必须和你的标签文件保持一致。很多人会忽略的一个点是验证集也要单独对应一套标注,如果验证集目录里没有labels,训练会直接报错。
yolov5超参数调整是一个容易被过度折腾的点。默认的hyp.scratch-low.yaml适合绝大多数中低算力环境,里面的lr0初始学习率、momentum动量、weight_decay正则系数都比较保守。如果你的数据集不大,只有几千张图,建议先保持默认超参数训练一遍,不要一上来就调学习率。真正值得调的是hsv_h、hsv_s这类数据增强参数,它们对光线变化明显的室外场景影响很大。
训练结束后,用best.pt替换原先的yolov5s.pt。替换后不要忘记跑一遍验证脚本,确认各类别AP是否正常。这里有一个容易被忽视的细节:DeepSORT不关心YOLOv5的类别信息,它只用边框和特征做匹配,但你的过滤代码里如果写死了COCO的类别ID,换成自己训练模型后会出现所有检测框全被过滤掉的情况。
4.3 训练完成后接入跟踪器的类别过滤与置信度适配
自己训练模型接入DeepSORT时,最关键的一步是写一个类别ID到是否参与跟踪的白名单映射。比如我只做行人和车辆跟踪,而自己的模型类别顺序是[0, 1, 2],那么过滤逻辑要按训练配置调整。
# custom.yaml 中定义的类别列表 VALID_CLASS_NAMES = ["person", "car", "truck"] # 类别过滤白名单 VALID_CLASS_IDS = [0, 1, 2] for det in results.boxes: cls_id = int(det.cls.item()) conf = det.conf.item() if cls_id in VALID_CLASS_IDS and conf >= conf_thres: dets.append([x1, y1, x2, y2, conf, cls_id])这里我还想提一个经验值:如果你在边缘设备上部署自己的YOLOv5模型,比如树莓派5,建议把imgsz降到480或更小,训练时保持640,推理时再缩放。这样做的原因是跟踪器对坐标精度不是特别敏感,只要检测框不出现明显偏移,DeepSORT的匹配仍然能正常工作。检测框抖动带来的偏差会被卡尔曼滤波器吸收一部分,但每帧都跳到完全不同的位置时,再强的滤波器也兜不住。
5. 避坑:帧率骤降、ID切换和记录错位的常见问题
5.1 目标ID频繁切换,轨迹在几分钟内碎成几十段
现象:一段百米长的行人视频,同一目标每隔几秒就换一个新ID,MOTA值惨不忍睹,轨迹文件里出现大量短命轨迹。
原因:最常见的是YOLOv5的置信度阈值太低。低阈值会把树荫、车窗反光这类误检框送进DeepSORT,误检框本身在空间上不稳定,跟踪器偶尔能匹配成功,但在下一帧又匹配不到,产生大量新轨迹。另一个原因是ReID特征提取器质量不够,当目标穿相同衣服或者密集人群遮挡时,余弦距离无法区分两个目标。
解决:先把conf_thres从0.2逐步提到0.4,观察ID切换数是否明显下降。如果仍然高频切换,再调高max_cosine_distance的阈值或者换一个更强的ReID权重。还有一个习惯是统计轨迹平均长度,如果平均长度不足5帧,基本可以确定是检测质量问题,而不是跟踪参数问题。
5.2 跟踪框整体滞后画面,回放视频无法对齐
现象:在仿真视频上做离线处理后,把结果重新渲染成视频,发现目标框的位置总是比真实目标慢了半拍,尤其目标转向时体现最明显。
原因:这是记录阶段犯了方向性错误。很多脚本为了追求处理速度,直接跳过读取帧的PTS时间戳,用一个增量帧号代替。如果视频源本身有可变帧率,或者你的推理耗时不稳定,输出的“第100帧”在时间上并不对应视频的第100帧。
解决:把视频读取循环改成基于时间戳驱动,记录时同时保存视频帧序号和绝对时间戳。具体做法是在cap.read()之后记录frame_count += 1和cap.get(cv2.CAP_PROP_POS_MSEC),这两个值跟跟踪结果一起写入记录文件。后续做回放或者轨迹分析时,统一以时间戳为准。
5.3 检测到了目标但DeepSORT完全没有输出
现象:YOLOv5可视化里检测框足够多,但DeepSORT返回结果一直为空。
原因:跟踪器接口内部会先做置信度过滤,很多DeepSORT实现里有一个独立于YOLOv5的min_confidence参数,默认设置为0.3或更高。当你把YOLOv5的检测框经过NMS后直接传入时,如果底层实现用np.asarray(detections)读取时类型不对,也可能导致过滤全部失效。
解决:先检查tracker.update返回值的形式,然后确认传入的每个检测框元素顺序是否和跟踪器预期一致。常见做法是打印传入检测框的形状和前几个数值。如果内部置信度阈值不明确,直接把detections里的conf字段置为1.0,只靠自己已经过滤过的检测框,这样能排除掉深坑。
5.4 小目标从头框到位,但大量ID丢失
现象:远处的行人只有20像素高,检测框在帧间时常抖动,跟踪结果里这类目标经常被ID切换或者直接丢失。
原因:DeepSORT的ReID特征提取器对输入尺寸有固定要求,多数实现会先把检测框裁剪出来再缩放到固定尺寸。如果原检测框太窄太扁,缩放后会丢掉大量长宽比信息,特征区分度下降。此外小目标本身检测框的像素波动比例更大,卡尔曼滤波器很难在速度估计上稳定下来。
解决:给检测框加一个正方形的裁剪扩展,把框高度和宽度都拉伸到同一个尺寸,让目标在图像中保持比例。扩展系数在0.2到0.3之间比较合适。对于特别小的目标,可以单独提高特征提取器的输入分辨率,但这会带来额外计算开销,需要权衡。
5.5 训练自己数据后,跟踪结果类别全部错乱
现象:模型检测效果正常,但跟踪结果里所有ID对应的目标类别信息和检测结果不一致,或者输出轨道文件里的类别全变了。
原因:这是ID映射表的问题。YOLOv5训练时的类别顺序和推理后DeepSORT内部保存的类别列表没有对齐。如果你用过COCO预训练权重做迁移,再替换成自己的权重后没有修改过滤逻辑,就会出现类别索引错位。
解决:建立一个从模型类别名到跟踪输出类别的显式映射,并在接入代码里打印一次类别表做核对。更稳妥的方式是跟踪结果里不存类别名,只存类别ID,所有类别名称在最后分析和可视化时才映射。这样即使类别ID变了,原始数据也不会错。
6. 记录与验证:把轨迹存成结构化文件,再看MOTA和ID切换怎么算
6.1 约定好的记录格式:帧号、目标ID、坐标和置信度
记录这一步看起来简单,但格式不统一会导致后期分析的时候反复返工。我强烈建议整个跟踪实验从第一天开始就用一个固定格式,下面是一个用CSV记录跟踪轨迹的最小实现:
import csv with open("tracking_record.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["frame_id", "track_id", "x1", "y1", "x2", "y2", "conf"]) # 在每一帧拿到跟踪结果后执行写入 writer.writerow([frame_id, track_id, x1, y1, x2, y2, conf])这里有一件事需要提醒:DeepSORT输出里的track_id是跨帧稳定的,但并不是回归现实世界某个物理目标的唯一标识,它只是在当前这次运行里保持稳定。如果你调了跟踪参数再跑一次,同一个物理目标可能拿到完全不同的ID。因此在记录文件里也要把这次运行对应的模型权重、conf_thres、max_age等参数一并写入一个meta文件,否则后人再次打开这个记录文件时,无法判断ID在什么条件下产生的。
6.2 用轨迹回放和MOTA验证跟踪效果
验证跟踪效果不能只看渲染出来的视频。我一般会用记录文件做两个分析。第一个是轨迹连续性分析,统计每个track_id出现的帧数、平均每帧置信度、第一次出现和最后一次出现的时间点。如果大量轨迹都只出现1到3帧,说明整个跟踪链路没有生效。第二个是MOTA评估,虽然MOTA的计算脚本有公开版本,但手工从CSV里也能粗略估算:把检测结果和真值做框相似度匹配后,统计误检、漏检和ID切换的次数。需要明确的是,MOTA不是越高越好,它惩罚漏检和ID切换,但对微小的定位误差没那么敏感。做多目标跟踪仿真时,我建议把MOTA和ID切换数放在一起看,因为如果MOTA很高但ID切换也高,说明检测质量优秀但关联策略薄弱。
6.3 每轮实验留下一个可追溯的配置记录
我自己的习惯是实验目录里除了trajectory文件,还有一个跟踪参数快照。通过conda环境记录和代码版本号,把每次实验的参数都保留下来。这样做的好处是,当你发现某个参数组合效果特别好,可以精准回退到这个配置,而不是靠记忆重试。多目标跟踪的调试过程里,最容易让人花费大量时间的是“参数感”很强的那些调整,比如max_dist从0.2改成0.25,num帧后ID切换率下降了,但到底因为什么下降,常常需要回看记录文件才能找到答案。
希望这篇文章能把你在YOLOv5加DeepSORT这套链路上遇到的大部分坑提前拆掉,让你在仿真与记录这条路上少一点“玄学调参”,多一点可复现的脚印。
本文还有配套的精品资源,点击获取