简介:本资源面向计算机、人工智能、通信工程等专业的在校学生与开发者,提供一套基于Yolov5-Deepsort-Fastreid重构的视频行人多目标跟踪(MOT)与行人重识别(ReID)特征提取完整方案,适合毕业设计、课程设计、项目立项演示及进阶学习使用。压缩包共37个文件,约38KB,以31个Python源码文件为核心,辅以2个yaml配置、2个txt依赖与说明、1个md文档及gitignore等,涵盖模型定义、配置管理、特征提取接口与运行依赖,目录结构清晰,便于按模块阅读与二次开发。资源已通过测试运行,功能完整,可直接复现行人检测、跟踪与ReID特征提取流程,并支持在此基础上修改扩展以实现其他功能。目前已有85人学习关注,适合需要快速搭建MOT与ReID实验环境、理解Deepsort与Fastreid集成思路的读者参考使用。
1. 从一段卡顿的跟拍视频说起:Yolov5+Deepsort+Fastreid 到底在解决什么
你手上有一段园区门口的行人跟拍视频,想统计每个人在画面里待了多久、去了哪个方向、有没有折返。直接拿 Yolov5 逐帧检测,框会跳,同一个人在第 30 帧是 id=1,第 31 帧可能变成 id=7,轨迹碎成一地。这就是 MOT(多目标跟踪)要解决的问题:让同一个目标在连续帧里保持同一个身份。而 ReID(行人重识别)解决的是更狠的一层——当目标被遮挡、走出画面再回来,甚至换了一个摄像头,还能不能认出“这还是刚才那个人”。
标题里的 Yolov5-Deepsort-Fastreid 就是这三件事的组合拳:Yolov5 负责每帧把行人框出来,Deepsort 负责把框和已有轨迹做关联、维持 ID 稳定,Fastreid 负责提供比 Deepsort 原生外观特征更强的 ReID 特征,让关联在遮挡和交叉时更抗造。这套组合在工程里非常常见,因为它把检测、跟踪、特征提取拆成了三个可独立替换的模块,你调其中一个不会把另外两个搞崩。
适合谁看:已经跑通过 Yolov5 推理、想把手里的检测结果升级成带 ID 的轨迹;或者跑过 Deepsort 但发现 ID switch 太多、想换更强 ReID 特征的人。下面按“先跑通、再拆接口、再调参、最后避坑”的顺序讲,代码和参数都给到能直接抄的程度。
2. 三模块拆解与最小可跑链路:Yolov5 出框、Deepsort 关联、Fastreid 提特征
2.1 为什么不是端到端,而是三段式拼接
很多人第一反应是找个端到端 MOT 模型一把梭。但实际落地时,三段式反而更稳。原因很直接:Yolov5 的检测精度和速度你已经调熟了,换检测器意味着重新标数据、重新调 anchor;Deepsort 的卡尔曼滤波和匈牙利匹配逻辑是透明的,出问题能定位到具体哪一步;Fastreid 作为独立特征提取器,可以单独换 backbone、单独在行人数据集上微调,不影响检测和跟踪。
三段式的数据流是这样的:Yolov5 每帧输出[x1,y1,x2,y2,conf,cls]的检测框列表,过滤出 person 类后送给 Deepsort。Deepsort 对每个框做卡尔曼预测,同时用 Fastreid 提取该框对应的 128/256/512 维外观特征,和轨迹库里已有特征做余弦距离匹配。匹配上就更新轨迹,匹配不上就新建轨迹,连续 N 帧没匹配上就删除轨迹。
这里的关键接口是:Deepsort 原本自带一个简单的 CNN 特征提取器,你要做的是把它替换成 Fastreid 的推理接口。替换点通常在deep_sort/deep_sort/deep/feature_extractor.py或类似位置,把_get_features方法的实现换成调用 Fastreid 的InferenceEngine。
2.2 最小可跑链路:从视频到带 ID 的输出
先保证三段能串起来,再谈优化。下面是一个最小推理脚本的骨架,假设你已经装好 Yolov5、Deepsort、Fastreid 三个仓库。
import cv2 import torch import numpy as np from yolov5.models.experimental import attempt_load from yolov5.utils.general import non_max_suppression, scale_coords from deep_sort.deep_sort import DeepSort from fastreid.config import get_cfg from fastreid.engine import DefaultPredictor # 1. 加载 Yolov5 yolo = attempt_load('yolov5s.pt', map_location='cuda:0') yolo.eval() # 2. 加载 Fastreid 配置与权重 cfg = get_cfg() cfg.merge_from_file('fastreid/configs/Market1501/bagtricks_R50.yml') cfg.MODEL.WEIGHTS = 'fastreid_market1501.pth' cfg.MODEL.DEVICE = 'cuda:0' reid_predictor = DefaultPredictor(cfg) # 3. 初始化 Deepsort,传入 Fastreid 提取器 deepsort = DeepSort( reid_predictor, # 关键:把 Fastreid 作为特征提取器注入 max_dist=0.2, # 余弦距离阈值,越小越严格 min_confidence=0.3, # 检测置信度下限 max_iou_distance=0.7, # IoU 匹配阈值 max_age=70, # 轨迹最大存活帧数 n_init=3 # 连续命中多少帧才确认轨迹 ) cap = cv2.VideoCapture('test.mp4') while cap.isOpened(): ret, frame = cap.read() if not ret: break img = cv2.resize(frame, (640, 640)) img_t = torch.from_numpy(img).permute(2,0,1).float().unsqueeze(0).cuda() / 255.0 pred = yolo(img_t)[0] pred = non_max_suppression(pred, conf_thres=0.4, iou_thres=0.5)[0] if pred is not None: pred[:, :4] = scale_coords(img_t.shape[2:], pred[:, :4], frame.shape).round() # 只保留 person 类,COCO 里 person 的 cls=0 person_mask = pred[:, -1] == 0 dets = pred[person_mask][:, :4].cpu().numpy() confs = pred[person_mask][:, 4].cpu().numpy() # 送入 Deepsort,内部会调用 Fastreid 提特征 outputs = deepsort.update(dets, confs, frame) for x1, y1, x2, y2, track_id in outputs: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0,255,0), 2) cv2.putText(frame, f'ID:{track_id}', (int(x1), int(y1)-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) cv2.imshow('MOT', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:Yolov5 负责出框,non_max_suppression做后处理去重,scale_coords把 640 尺度下的框映射回原图。Deepsort 的update接收检测框、置信度和原图,内部对每个框裁剪后调用 Fastreid 提特征,再和轨迹库做匹配。参数说明:max_dist=0.2是余弦距离阈值,调大召回高但容易串 ID,调小则容易断轨;max_age=70表示轨迹丢失后保留 70 帧,给遮挡后重现留缓冲;n_init=3表示连续 3 帧匹配上才确认新轨迹,能过滤掉一闪而过的误检。
2.3 Fastreid 特征提取接口的对接细节
Fastreid 的DefaultPredictor接收的是 BGR 图像,输出是归一化后的特征向量。Deepsort 内部裁剪出的行人框尺寸不一,直接送进 Fastreid 会因 resize 导致特征不稳定。常见做法是在送入前统一 padding 到固定长宽比,比如 256x128,保持行人的宽高比不变,短边补零。
def extract_reid_feature(predictor, img, bbox): x1, y1, x2, y2 = map(int, bbox) crop = img[max(0,y1):y2, max(0,x1):x2] if crop.size == 0: return np.zeros(2048, dtype=np.float32) # 统一 resize 到 256x128,保持宽高比 h, w = crop.shape[:2] target_h, target_w = 256, 128 scale = min(target_h/h, target_w/w) new_h, new_w = int(h*scale), int(w*scale) resized = cv2.resize(crop, (new_w, new_h)) canvas = np.zeros((target_h, target_w, 3), dtype=np.uint8) top = (target_h - new_h) // 2 left = (target_w - new_w) // 2 canvas[top:top+new_h, left:left+new_w] = resized feat = predictor(canvas) # Fastreid 输出归一化特征 return feat.flatten()这段代码的关键是 padding 而不是直接拉伸,直接拉伸会改变行人身体比例,ReID 特征会明显变差。Fastreid 输出的特征维度取决于 backbone,R50 通常是 2048 维,经过 BNNeck 后取 2048 维做余弦距离即可。如果你换更小的 backbone,维度会变,Deepsort 里的特征库维度要同步改,否则会报维度不匹配。
3. 把接口源码拆开改:Deepsort 替换特征提取器与 Fastreid 配置调参
3.1 Deepsort 里特征提取的调用链
Deepsort 的跟踪逻辑在tracker.py,特征提取在nn_matching.py和feature_extractor.py。原始 Deepsort 用的是一个简化版 CNN,输入 128x64,输出 128 维。你要替换成 Fastreid,需要改三个地方:一是feature_extractor.py里的__init__不再加载原 CNN,而是接收 Fastreid predictor;二是_get_features方法改成调用上面的extract_reid_feature;三是nn_matching.py里的NearestNeighborDistanceMetric的维度参数要改成 Fastreid 的输出维度。
# 修改后的 feature_extractor.py 核心部分 class FastReIDExtractor: def __init__(self, predictor, feature_dim=2048): self.predictor = predictor self.feature_dim = feature_dim def __call__(self, imgs, bboxes): # imgs: 原图列表,bboxes: 对应的框列表 features = [] for img, bbox in zip(imgs, bboxes): feat = extract_reid_feature(self.predictor, img, bbox) features.append(feat) return np.array(features)然后在tracker.py初始化时,把self.extractor指向FastReIDExtractor实例。注意 Deepsort 的update方法里会调用self.extractor(imgs, bboxes),所以你的__call__签名要和原来一致,否则会报参数错误。
3.2 Fastreid 配置文件里必须改的四个参数
Fastreid 的配置文件在configs/下,以 Market1501 的bagtricks_R50.yml为例。直接拿来做推理,有四个参数必须确认:
| 参数 | 默认值 | 推理时建议 | 作用 |
|---|---|---|---|
| MODEL.WEIGHTS | 空 | 指向你的 .pth | 不设就是随机权重,特征全是噪声 |
| MODEL.DEVICE | cuda | cuda 或 cpu | 和你的环境一致 |
| TEST.IMS_PER_BATCH | 64 | 1 或 2 | 推理时 batch 太大会爆显存 |
| INPUT.SIZE_TEST | [256,128] | [256,128] | 和训练时一致,改了特征会漂 |
如果你用的是自己训练的 ReID 模型,还要确认MODEL.BACKBONE.NAME和MODEL.HEADS.NAME与训练时一致,否则加载权重会报 key 不匹配。常见做法是先用官方 Market1501 权重跑通,再换自己的模型。
3.3 用 Yolov5 训练自己的数据集时,检测类别要同步
如果你的场景不是 COCO 的 person,而是工服、安全帽等自定义类别,Yolov5 需要重新训练。训练时注意data.yaml里的nc和names,推理时过滤类别的索引要对应改。比如你只关心worker类,且它在 names 里排第 0,那person_mask = pred[:, -1] == 0就要改成对应索引。
# data.yaml 示例 train: ./images/train val: ./images/val nc: 2 names: ['worker', 'helmet']训练命令用python train.py --data data.yaml --weights yolov5s.pt --epochs 100 --batch-size 16 --img 640。训练完后,推理脚本里attempt_load加载你的best.pt,类别过滤索引改成worker对应的 0。这一步不做,Deepsort 会把安全帽也当行人跟踪,ID 会乱。
4. 避坑与排查:ID 跳变、特征维度不匹配、显存溢出
4.1 现象:同一个人 ID 频繁跳变,轨迹碎成多段
原因通常有三个:一是max_dist设得太大,不同人的特征被误匹配;二是 Fastreid 特征没有做归一化,余弦距离计算失真;三是检测框抖动严重,裁剪出的行人区域每帧差异大,特征不稳定。解决:先把max_dist降到 0.15 试,确认 Fastreid 输出前做了 L2 归一化,再检查 Yolov5 的conf_thres是否太低导致框在目标边缘跳动。如果还不行,在 Deepsort 的匹配阶段加入 IoU 代价作为辅助,而不是只靠外观距离。
4.2 现象:报错 “Feature dimension mismatch”
原因:Deepsort 的NearestNeighborDistanceMetric初始化时指定的维度和你实际提取的特征维度不一致。比如你初始化时写了 128,但 Fastreid 输出 2048。解决:在初始化DeepSort时把feature_dim参数改成 Fastreid 的实际输出维度。R50 是 2048,R34 可能是 2048 或 512,具体看MODEL.HEADS.EMBEDDING_DIM。改完还要清空轨迹库,因为旧特征维度不对。
4.3 现象:跑几十帧后显存溢出
原因:Deepsort 的轨迹库会保留每条轨迹的历史特征,max_age设得太大时,轨迹数量膨胀,特征库占用显存持续增长。另外 Fastreid 如果每帧对每个框都跑一次推理,batch 虽小但调用频繁,显存碎片会累积。解决:把max_age从 70 降到 30 左右,轨迹确认后只保留最近 10 帧的特征做匹配;Fastreid 推理时用torch.no_grad()包住,并定期torch.cuda.empty_cache()。如果还紧张,把 Fastreid 换成更小的 backbone,比如 ResNet34 或 OSNet。
4.4 现象:遮挡后重新出现,ID 变成新的
原因:max_age太短,轨迹在遮挡期间被删除了;或者 ReID 特征在遮挡时提取的是被遮挡的部分,特征质量差,匹配不上。解决:适当增大max_age到 50-70,给遮挡留缓冲;同时在 Deepsort 匹配时,对丢失轨迹的匹配阈值放宽,比如max_dist从 0.2 放到 0.3,只对丢失轨迹放宽,正常轨迹保持严格。这个策略在nn_matching.py里改distance计算时的阈值分支即可。
4.5 现象:树莓派 5 上部署时帧率掉到个位数
原因:Yolov5s 在树莓派 5 上 CPU 推理已经吃力,再加 Fastreid R50 基本跑不动。解决:Yolov5 换yolov5n并导出 ONNX 用 ONNX Runtime 推理;Fastreid 换 OSNet x0.25 或 MobileNet backbone,导出 ONNX;Deepsort 的卡尔曼滤波部分用 numpy 实现,不占 GPU。整体帧率能回到 5-8 FPS,满足离线分析够用。如果要求实时,建议用边缘加速棒或换更轻量的跟踪方案。
5. 进阶技巧:用 ReID 特征做跨摄像头轨迹拼接与验证
单摄像头跑通后,最有价值的进阶方向是跨摄像头轨迹拼接。思路是:每个摄像头独立跑 Yolov5+Deepsort+Fastreid,得到带 ID 的轨迹和每条轨迹的特征均值。然后在后端把不同摄像头同一时间段的轨迹特征做余弦距离比对,距离小于阈值的认为是同一个人,把 ID 映射统一。
具体做法:对每条确认轨迹,取最近 20 帧的 Fastreid 特征做平均,得到该轨迹的“身份特征”。跨摄像头匹配时,用匈牙利算法对两组轨迹特征做全局最优匹配,阈值设在 0.25-0.3 之间。匹配上后,把摄像头 B 的轨迹 ID 映射到摄像头 A 的 ID 上,实现跨镜追踪。
验证方法:拿一段有重叠区域的视频,人为标注几个人在两个摄像头里的出现时间段,跑完拼接后看 ID 是否一致。如果 ID 一致率高但偶尔串,检查是不是特征均值受遮挡帧污染,可以改用中位数或只取质量最高的 10 帧特征。
我自己的习惯是:每次换 ReID 模型或改 Deepsort 参数,先跑一段 30 秒的固定视频,把 ID switch 次数和轨迹完整度记下来,和上一版对比。没有这个基线,调参就是玄学。希望帮到你。
本文还有配套的精品资源,点击获取