
简介这份资源围绕视频行人多目标跟踪MOT与行人重识别ReID任务基于Yolov5-Deepsort-Fastreid源码进行重构整理出可运行的MOT与ReID特征提取代码及配套接口面向计算机、人工智能、通信工程、自动化等专业的在校学生、教师与企业开发者可用于毕业设计、课程设计、项目立项演示或进阶学习。压缩包共37个文件以31个Python源码为主体辅以2个yaml配置、2个txt说明、1个md文档及gitignore等整体约38KB目录涵盖configs、models、src等模块结构清晰便于按功能检索。资源内代码均经过测试运行成功并附详细文档与全部资料已有85人学习关注。读者可据此快速理解Yolov5检测、Deepsort跟踪与Fastreid特征提取的衔接方式掌握MOT与ReID接口的调用与改造思路并在此基础上修改扩展完成自己的毕设或课设任务。1. 从一份 Yolov5-Deepsort-Fastreid 源码说起视频行人 MOT 与 ReID 特征提取到底难在哪如果你手头正好有一份「基于 Yolov5-Deepsort-Fastreid 源码重构了视频行人 MOT 和行人 ReID 特征提取代码」的项目包第一反应大概率是检测有 Yolov5跟踪有 Deepsort特征有 Fastreid三块都是现成的拼起来不就行了真跑过视频行人 MOT 的人都知道翻车点从来不在单个模型而在三件事的接口对齐检测框怎么喂给跟踪器、跟踪轨迹怎么触发 ReID 特征提取、ReID 特征怎么回灌给匹配代价矩阵。这三步任何一步错位ID Switch 就会飙升画面里同一个人被反复分配新 IDMOT 指标直接崩。这份源码重构的价值恰恰在于把「检测-跟踪-重识别」这条链路的接口重新梳理了一遍让视频行人 MOT 和行人 ReID 特征提取不再是三个孤立脚本的硬拼。它适合两类人一类是想在自建视频上跑通多目标跟踪、但被 Deepsort 和 Fastreid 的输入输出格式卡住的工程新手另一类是想把 ReID 特征真正接进跟踪匹配、而不是只拿来做离线检索的熟手。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. Yolov5 检测层从权重加载到检测框后处理的接口约定2.1 为什么检测层要单独抽出来做接口很多人把 Yolov5 直接塞进跟踪主循环里每帧调一次model(frame)看似省事实则埋了两个雷。第一Yolov5 默认输出的是归一化的xywh加置信度加类别而 Deepsort 要的是[x1, y1, x2, y2, conf, cls]的绝对坐标格式中间少一步转换跟踪器拿到的框全是错的。第二视频里行人检测不需要每帧都跑全类别把classes限定为 0person能省掉大量后处理时间尤其在你用 Yolov5s 跑 1080p 视频时这一项能砍掉近三成耗时。重构后的检测层一般会封装成一个Detector类对外只暴露detect(frame) - boxes一个方法。这样做的好处是后面你想把 Yolov5 换成 Yolov5 的改进版或者别的检测器只要保持boxes格式不变跟踪和 ReID 部分一行都不用动。这就是接口约定的意义。2.2 加载权重与推理的最小代码import torch import numpy as np class Yolov5Detector: def __init__(self, weights_path, devicecuda, conf_thres0.4, iou_thres0.5): # 加载本地权重避免每次联网拉取 self.model torch.hub.load(ultralytics/yolov5, custom, pathweights_path, force_reloadFalse) self.model.to(device).eval() self.conf_thres conf_thres self.iou_thres iou_thres self.device device def detect(self, frame): # 只检测 person 类减少后处理负担 self.model.classes [0] self.model.conf self.conf_thres self.model.iou self.iou_thres results self.model(frame, size640) # results.xyxy[0] 格式为 [x1, y1, x2, y2, conf, cls] dets results.xyxy[0].cpu().numpy() return dets这段代码的关键在results.xyxy[0]它直接给出绝对坐标的[x1, y1, x2, y2, conf, cls]省掉了自己写xywh2xyxy的麻烦。conf_thres设 0.4 是行人检测的常用起点画面里人小、遮挡多时可以降到 0.3但别低于 0.25否则误检框会拖垮后面的跟踪。iou_thres控制 NMS 的合并力度行人密集场景建议 0.5太低了会把并排走的两个人合并成一个框。2.3 检测框后处理里最容易忽略的一步拿到dets之后别急着往跟踪器里塞。先做一次宽高比过滤行人框的宽高比通常在 0.2 到 0.6 之间超出这个范围的框大概率是误检或者非行人目标。再加一步最小高度过滤低于 40 像素的框在 ReID 特征提取时根本提不出有效特征留着只会污染轨迹。def filter_person_boxes(dets, min_h40, ratio_range(0.2, 0.6)): keep [] for d in dets: x1, y1, x2, y2 d[:4] w, h x2 - x1, y2 - y1 if h min_h: continue ratio w / max(h, 1e-6) if ratio ratio_range[0] or ratio ratio_range[1]: continue keep.append(d) return np.array(keep) if keep else np.empty((0, 6))这两步过滤看着简单但在实际视频里能砍掉大量抖动框。我一般会把过滤后的框数量打印出来如果某一帧突然从十几个掉到两三个说明阈值卡太狠了得回头调。3. Deepsort 跟踪层代价矩阵、级联匹配与轨迹生命周期3.1 Deepsort 的匹配逻辑为什么依赖检测质量Deepsort 的核心是卡尔曼滤波预测轨迹位置再用代价矩阵把预测框和当前检测框做匹配。代价矩阵由两部分组成马氏距离衡量运动一致性余弦距离衡量外观一致性。检测框一旦抖动或者漏检卡尔曼滤波的预测就会漂移马氏距离那一项直接失真匹配就会乱套。所以上一章强调的检测框过滤不是可选项是 Deepsort 能跑稳的前提。级联匹配是 Deepsort 的另一个关键设计它优先匹配那些最近刚更新过的轨迹把长时间没匹配上的轨迹放到后面。这样做的目的是减少 ID Switch因为一个被遮挡几帧的人重新出现时他的轨迹还在「新鲜」队列里更容易被正确匹配回去。理解这一点你就明白为什么max_age这个参数不能设太小。3.2 把检测框喂进 Deepsort 的标准流程from deep_sort.utils.parser import get_config from deep_sort.deep_sort import DeepSort cfg get_config() cfg.merge_from_file(deep_sort/configs/deep_sort.yaml) deepsort DeepSort(cfg.DEEPSORT.REID_CKPT, max_distcfg.DEEPSORT.MAX_DIST, min_confidencecfg.DEEPSORT.MIN_CONFIDENCE, nms_max_overlapcfg.DEEPSORT.NMS_MAX_OVERLAP, max_iou_distancecfg.DEEPSORT.MAX_IOU_DISTANCE, max_agecfg.DEEPSORT.MAX_AGE, n_initcfg.DEEPSORT.N_INIT, nn_budgetcfg.DEEPSORT.NN_BUDGET, use_cudaTrue) def update_tracker(deepsort, dets, frame): # dets 格式 [x1, y1, x2, y2, conf, cls] xywhs [] confs [] clss [] for d in dets: x1, y1, x2, y2 d[:4] w, h x2 - x1, y2 - y1 xywhs.append([(x1 x2) / 2, (y1 y2) / 2, w, h]) confs.append(d[4]) clss.append(d[5]) outputs deepsort.update(np.array(xywhs), np.array(confs), np.array(clss), frame) return outputs # 格式 [x1, y1, x2, y2, track_id]这里有个容易踩的坑Deepsort 的update方法内部会自己跑一次 NMS如果你在检测层已经做过 NMS这里要把nms_max_overlap设成 1.0 来跳过重复处理否则两次 NMS 叠加会把正常框也滤掉。max_age我一般设 70对应 25fps 视频里大约 3 秒的遮挡容忍设太小会导致遮挡后 ID 直接断掉设太大又会让消失的人「阴魂不散」。3.3 轨迹生命周期里 n_init 和 nn_budget 怎么定n_init是一条轨迹要连续匹配上多少帧才被确认为稳定轨迹默认 3。在行人频繁进出画面的场景里设 2 能更快确认新出现的人但误检也会更容易变成轨迹。nn_budget是每条轨迹保留的历史外观特征数量默认 100设大了占显存设小了外观匹配不稳定。我的经验是 1080p 视频、几十个人的场景nn_budget保持 100 就够显存紧张时降到 50 也能接受。4. Fastreid 特征提取层ReID 特征怎么接进跟踪匹配4.1 Fastreid 在整条链路里的位置Fastreid 负责把每个行人框转成一个高维特征向量这个向量就是 ReID 特征。在 Deepsort 里外观代价矩阵用的就是当前检测框特征和轨迹历史特征之间的余弦距离。所以 Fastreid 的输出质量直接决定了外观匹配的准确度。重构代码里通常会把 Fastreid 封装成一个独立的特征提取器输入是裁剪后的行人图输出是归一化后的特征向量。这里要区分两个概念MOT 里的 ReID 特征提取是「在线」的每帧都要对当前检测框提特征速度要求高而离线 ReID 检索是「批量」的可以慢慢跑。这份源码重构的重点是在线提取所以模型选型上一般用 Fastreid 的轻量 backbone比如 ResNet50-IBN 或者 SBS 系列而不是堆很深的网络。4.2 特征提取的代码接口与批量推理import torch import torch.nn.functional as F from fastreid.config import get_cfg from fastreid.engine import DefaultPredictor class ReIDExtractor: def __init__(self, config_file, weights_path, devicecuda): cfg get_cfg() cfg.merge_from_file(config_file) cfg.MODEL.WEIGHTS weights_path cfg.MODEL.DEVICE device self.predictor DefaultPredictor(cfg) def extract(self, frame, boxes): # boxes 格式 [x1, y1, x2, y2] crops [] for b in boxes: x1, y1, x2, y2 map(int, b[:4]) crop frame[max(0, y1):y2, max(0, x1):x2] if crop.size 0: continue crops.append(crop) if not crops: return np.empty((0, 2048)) # 批量推理比逐张快很多 feats self.predictor(crops) feats F.normalize(feats, dim1) return feats.cpu().numpy()DefaultPredictor内部会做 resize、归一化和 GPU 推理输出维度取决于你用的 Fastreid 配置常见是 2048 或 512。F.normalize这一步不能省Deepsort 算余弦距离时假设特征已经归一化不归一化的话距离尺度会乱。批量推理是提速的关键一次传 16 到 32 个 crop 比逐张跑快好几倍但要注意显存crop 数量太多会 OOM。4.3 特征怎么回灌给 Deepsort 的代价矩阵Deepsort 原版用的是自己的小 ReID 网络换成 Fastreid 之后需要在DeepSort.update里把外观特征的来源替换掉。常见做法是重写_match方法里的外观代价计算把 Fastreid 提取的特征传进去。具体来说轨迹对象维护一个features列表每次匹配成功后把当前特征 append 进去匹配时用当前检测特征和轨迹特征列表算最小余弦距离。def cosine_distance(a, b): # a: (N, D), b: (M, D)均已归一化 return 1 - np.dot(a, b.T) # 在匹配阶段用 Fastreid 特征替换默认外观特征 cost_matrix cosine_distance(det_features, track_features)这里有个参数要注意Deepsort 的max_dist控制外观距离的阈值默认 0.2换成 Fastreid 特征后这个值往往需要调大因为 Fastreid 的特征分布和原版网络不同。我一般从 0.3 开始试观察 ID Switch 数量再微调。5. 避坑与排查MOT 和 ReID 联调时最常见的 5 个翻车点5.1 现象所有检测框的 track_id 每帧都在变原因检测框格式没对齐Deepsort 拿到的xywh是归一化值而不是绝对坐标导致卡尔曼滤波预测和检测框完全对不上。解决在喂给deepsort.update之前打印一组框的数值确认中心点和宽高是像素级绝对坐标不是 0 到 1 之间的小数。5.2 现象ReID 特征提取报维度不匹配原因Fastreid 配置文件里的输入尺寸和实际 crop 尺寸不一致或者DefaultPredictor的输出维度和你代码里写死的维度对不上。解决先单独跑一次extractor.extract打印feats.shape把这个维度同步到 Deepsort 的特征容器初始化里别硬编码 2048。5.3 现象视频跑几帧就卡死或者显存爆掉原因Yolov5、Deepsort 的 ReID 网络、Fastreid 三个模型同时占显存加上每帧都保留大量 crop 做批量推理显存峰值很容易超。解决把 Fastreid 的批量大小从 32 降到 8 或 16Yolov5 用半精度推理Deepsort 的nn_budget降到 50。如果还爆把 Fastreid 单独放到另一块卡上。5.4 现象遮挡后同一个人被分配了新 ID原因max_age设太小轨迹在遮挡期间被销毁了人重新出现时只能新建轨迹。解决把max_age从默认 30 提到 70 甚至 100同时确认 Fastreid 特征在轨迹里保留足够多nn_budget别低于 50。另外检查n_init设成 1 会让新轨迹立刻确认但误检也会变多建议保持 3。5.5 现象ReID 特征看起来正常但匹配就是不准原因特征没有做 L2 归一化或者余弦距离计算时用了未归一化的向量导致距离尺度失真。解决在extract返回前强制F.normalize并在代价矩阵计算前再确认一次两个输入都已归一化。这个坑很隐蔽因为特征值本身看起来没问题只有距离算出来偏大或偏小。6. 进阶技巧用 ReID 特征做轨迹后处理与 ID 修正跑通整条链路之后真正拉开效果差距的是后处理。一个实用技巧是把每条轨迹的所有 ReID 特征存下来视频跑完后做一次全局聚类把那些被错误拆分的轨迹合并回去。具体做法是对每对轨迹计算特征集合之间的最小余弦距离低于阈值的判定为同一人然后把 track_id 统一。这一步能修掉不少在线跟踪时因为遮挡造成的 ID Switch。from scipy.cluster.hierarchy import linkage, fcluster def merge_tracks_by_reid(track_features, dist_thres0.25): # track_features: dict {track_id: np.array(N, D)} ids list(track_features.keys()) # 计算轨迹间最小距离矩阵 n len(ids) dist_mat np.zeros((n, n)) for i in range(n): for j in range(i 1, n): fi track_features[ids[i]] fj track_features[ids[j]] d cosine_distance(fi, fj).min() dist_mat[i, j] dist_mat[j, i] d # 层次聚类合并 Z linkage(dist_mat[np.triu_indices(n, 1)], methodaverage) labels fcluster(Z, tdist_thres, criteriondistance) merge_map {ids[i]: labels[i] for i in range(n)} return merge_map这个后处理的阈值dist_thres需要根据你的 Fastreid 模型在验证集上的表现来定0.25 是个保守起点。跑完之后把merge_map应用到输出视频的标注上肉眼能明显看到 ID 跳变减少。另一个技巧是给 ReID 特征加时间衰减轨迹里越早的特征权重越低匹配时更依赖最近几帧的外观。这在行人换衣服或者光照突变时特别有用。实现上就是在算余弦距离时给特征列表加一个指数衰减权重最近的特征权重接近 1最早的降到 0.3 左右。我自己踩过最深的坑是早期图省事把 Fastreid 特征直接塞进 Deepsort 却没改max_dist结果外观匹配几乎不起作用ID Switch 比不用 ReID 还高。后来养成习惯每换一次特征提取器先单独跑一段视频把匹配距离的分布打出来再定阈值。这个习惯帮我省了很多来回调参的时间。希望帮到你。本文还有配套的精品资源点击获取