简介一份面向人工智能本科毕业设计的步态识别多目标跨镜头跟踪检测系统源码基于YOLOv5DeepSORT完成目标检测与跟踪结合GaitSet算法实现步态特征识别适用于监控场景下的跨镜头多目标持续追踪研究。资源共343个文件以173个Python脚本为核心算法实现52个YAML文件用于模型与训练配置另有Markdown说明文档、Shell部署脚本、C/CUDA扩展模块等覆盖从数据预处理、模型训练到推理部署的完整链路压缩包仅22.23MB结构清晰便于查阅。已有4449人学习下载。适合本科毕业设计、课程项目或算法研究者参考可帮助理解跨镜头跟踪与步态识别的工程化实现包含完整源码、配置文件与部署脚本能够直接作为实验基础或二次开发起点。1. yolov5DeepSORTGaitSet跨镜头步态识别这个题目到底在解决什么问题跨镜头跟踪是监控场景里最尴尬的一环。单镜头内的多目标跟踪早就有成熟方案一旦目标走出画面再进入另一路摄像头ID 就断了。最直接的补救是换装重识别但监控场景下人脸看不清、衣服换个角度就变样真正稳定的是步态——人走路的姿态很难刻意伪装。这套毕业设计源码做的就是这件事用 yolov5 做目标检测DeepSORT 做单镜头内的跟踪再用 GaitSet 提取步态特征做跨镜头匹配整个链路里最核心的 GNN 传播模块也以源码形式放在包里。如果你正在做人脸、行人重识别或多目标跟踪相关的课设、竞赛或者想快速拿一套能跑的跨镜头跟踪基线这份资源值得先看目录再动手。2. 三段式架构检测、单镜头跟踪、跨镜头重识别怎么接2.1 yolov5 检测模块不是拿来就跑先确认输出格式这套系统的第一层是 yolov5。检测模块负责把每一帧画面里的行人框出来输出的是x1, y1, x2, y2, conf, cls这种格式的张量。常见的做法是用 yolov5s 或者你自己训练的权重推理时把检测结果直接喂给 DeepSORT。需要注意一点yolov5 输出的坐标是像素坐标DeepSORT 的输入要求也是像素坐标不需要归一化但需要确保检测框没有被 NMS 二次压缩过。# 以 YOLOv5 的 detect 输出为例做检测结果到跟踪器的对接 import cv2 import torch model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) def detect_frame(frame): results model(frame, size640) dets results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] person_dets dets[dets[:, 5] 0] # COCO 类别 0 是 person return person_dets[:, :5] # 只要坐标和置信度这里的size640是推理尺寸可以用imgsz参数覆盖person_dets[:, :5]之所以只要前五列是因为 DeepSORT 的update()接口接收的是[x1, y1, x2, y2, score]数组。如果你用的是自定义数据集COCO 类别索引就不是 0 了需要在类别映射表里找到 person 对应的索引否则跟踪器会把车和树都当成目标去跟踪。2.2 DeepSORT 跟踪层卡尔曼滤波和匈牙利匹配的参数含义DeepSORT 是整个系统里承上启下的部分。它接收 yolov5 的检测框为每个目标维护一个轨迹状态内含卡尔曼滤波预测的位置、速度、外观特征。核心参数有三个max_dist控制外观特征的最大余弦距离max_iou_dist控制匹配时 IoU 的阈值max_age决定轨迹丢失多少帧后删除。毕设场景下这三个参数往往被忽略直接用默认值但换数据集后它们是最影响跟踪稳定性的。from deep_sort_realtime.deepsort_tracker import DeepSort tracker DeepSort( max_dist0.2, # 余弦距离阈值越小越严格 max_iou_dist0.7, # IoU 匹配阈值 max_age30, # 轨迹丢失多少帧后删除 nn_budget100, # 外观特征样本上限 embeddermobilenet # 提取外观特征的网络 ) def update_tracks(frame, detections): tracks tracker.update_tracks(detections, frameframe) return [(t.track_id, t.to_ltrb()) for t in tracks if t.is_confirmed()]max_dist0.2的意思是外观特征余弦距离大于 0.2 的检测框和轨迹不会匹配这个值在光线变化大的监控场景下往往需要放宽到 0.3。nn_budget100控制每个轨迹保留多少个历史外观特征超过上限会淘汰最早的这个参数在长时间跟踪里影响很大——目标走了十分钟后回头如果特征池太小外观信息已经被挤出去了。embeddermobilenet是轻量级特征提取器速度和精度兼顾如果追求重识别准确率可以换osnet但推理耗时几乎翻倍。2.3 GaitSet 步态识别层跨镜头匹配的身份特征来源DeepSORT 只解决单镜头内的 ID 维持跨镜头后外观特征已经不可靠这时候就要靠步态特征兜底。GaitSet 的做法是把一个行走序列的多帧轮廓图拆成若干组对每组做集合池化再用水平金字塔映射生成步态特征。这套源码里步态部分的关键是它的数据组织方式每个行人的一组轮廓图放在同一个目录下序列长度和信息帧的选择直接影响特征质量。在工程对接上GaitSet 的输出是一个 256 维或 512 维的特征向量这个向量和 DeepSORT 的外观特征向量不在同一个空间里不能直接拼在一起做距离度量。常见的做法是分段判断目标在镜头 A 丢失前缓存它最后 20 帧的步态特征目标在镜头 B 出现后先让 DeepSORT 跑上 10 帧稳定轨迹再提取这段轨迹的步态特征与镜头 A 的缓存特征做余弦相似度。相似度超过阈值就判定为同一个 ID然后把这个新的轨迹 ID 映射回原来的全局 ID。这一步在源码里对应的是 GNN 模块介入之前的预处理。3. GNN跨镜头关联源码走读从 build_adjacency_matrix 到 gnn_propagate3.1 文件清单理清这套源码跨镜头部分到底做了什么打开压缩包顶层文件里值得先看的是以下几类gnn_propagate.cpp、build_adjacency_matrix.cpp以及对应的.cu文件它们是跨镜头关联的核心setup.cfg、isort.cfg、.flake8只是工程配置Dockerfile解决的是环境一致性。市面上大多数 yolov5DeepSORT 项目到单镜头跟踪就结束了这里的gnn_propagate和build_adjacency_matrix才是真正的毕设亮点。build_adjacency_matrix负责构建图结构。图的节点是来自不同镜头的轨迹片段边的权重是轨迹之间的相似度。相似度由两部分组成外观特征的余弦相似度和时间空间上的共现约束。gnn_propagate在这个图上做特征的迭代传播让每个轨迹节点的特征吸收邻居节点的信息从而缓解单镜头内外观特征不够判别性的问题。这个思路来源于图神经网络在行人重识别上的应用毕设答辩时是一个很好的切入点。3.2 build_adjacency_matrix 的源码逻辑与参数build_adjacency_matrix.cpp做的事情可以拆解成三步读取所有轨迹片段的外观特征和时空信息计算两两之间的初始相似度再根据阈值和时空约束生成稀疏邻接矩阵。核心是相似度计算时两个损失的权衡——外观距离用余弦距离时空距离用起始帧和结束帧的重叠程度。下面的代码展示了 CPU 版本的骨架逻辑。// build_adjacency_matrix.cpp 骨架逻辑 #include vector #include cmath struct Tracklet { int camera_id; int start_frame; int end_frame; std::vectorfloat feature; // 外观特征GaitSet 或 ReID 提取 }; // 构建邻接矩阵 std::vectorstd::vectorfloat BuildAdjacencyMatrix( const std::vectorTracklet tracklets, float sim_threshold, float time_window) { int n tracklets.size(); std::vectorstd::vectorfloat adj(n, std::vectorfloat(n, 0.0f)); for (int i 0; i n; i) { for (int j i 1; j n; j) { // 同镜头不连接跨镜头关联不需要同镜头内的边 if (tracklets[i].camera_id tracklets[j].camera_id) continue; // 时间窗口约束两个轨迹在时间上必须有重叠或接近 int overlap std::min(tracklets[i].end_frame, tracklets[j].end_frame) - std::max(tracklets[i].start_frame, tracklets[j].start_frame); if (overlap time_window) continue; // 外观特征余弦相似度 float cos_sim CosineSimilarity(tracklets[i].feature, tracklets[j].feature); if (cos_sim sim_threshold) { adj[i][j] cos_sim; adj[j][i] cos_sim; } } } return adj; }sim_threshold的取值决定了图的稀疏程度。值设太高很多真实匹配的边被切断GNN 传播时信息传不到值设太低图变成稠密图计算量和噪声同时上升。一般取 0.7 到 0.75 之间的值具体要看步态特征的质量——如果 GaitSet 用的是 CASIA-B 数据集预训练权重特征判别性尚可这个阈值可以稍微放开到 0.65。time_window这个参数很容易被误解成时间差阈值实际上它是时间重叠度单位是帧跨镜头场景中两个镜头的时间可能不同步所以录入时最好先做时间对齐。3.3 gnn_propagate 的传播过程与 CUDA 加速gnn_propagate在邻接矩阵上运行多个图卷积层每一层把邻居节点的特征加权融合到当前节点。整个传播过程涉及矩阵乘法数据量大时用 CPU 会非常慢所以源码里额外提供了_kernel.cu的 CUDA 实现。GPU 版本的核心是在 kernel 函数里并行遍历每一个节点聚合邻居特征。// gnn_propagate_kernel.cu 核函数伪代码 __global__ void PropagateKernel( const float* features, // [N, D] 特征矩阵 const float* adj, // [N, N] 邻接矩阵 float* out_features, // [N, D] 输出特征 int N, int D) { int row blockIdx.x * blockDim.x threadIdx.x; if (row N) return; for (int d 0; d D; d) { float sum 0.0f; for (int col 0; col N; col) { sum adj[row * N col] * features[col * D d]; } out_features[row * D d] sum; } }这段 kernel 代码的瓶颈在内存访问上。adj是稀疏矩阵但 kernel 里仍然用稠密方式遍历N 是轨迹数量一般不会超过几千所以问题不大如果轨迹上万条N 的平方就不只是计算量的问题内存也会爆掉——一个 10000 x 10000 的 float 矩阵就是 400MB两个就是 800MB。毕设规模下不需要优化存储格式但如果你要拿去跑更大的数据集建议把adj改造成 CSR 格式再喂给 kernel。图中的注释也说明了传播的一层只做了线性聚合没有加非线性激活函数这是刻意为之的——多跨镜头关联的特征空间不需要引入太多非线性保持线性传播的稳定性比拟合能力更重要。3.4 编译与调用setup.cfg 和 .flake8 在这里的作用这些 .cu 文件不能直接用 Python 调用需要先编译成 PyTorch 的 C 扩展。常见做法是写一个setup.py文件把gnn_propagate.cpp和gnn_propagate_kernel.cu通过torch.utils.cpp_extension.CUDAExtension编译。源码包里没列出setup.py但列出的setup.cfg和.flake8说明项目原本是用 setuptools 管理的可以依此补全编译入口。# 编译自定义 CUDA 算子的常用命令 python setup.py build_ext --inplace编译前先确认三件事CUDA 版本和 PyTorch 版本匹配、GPU 算力在 6.0 以上GTX 10 系及以上、TORCH_CUDA_ARCH_LIST环境变量设置了正确的算力。我见过不少人在这一步卡住报错大多是undefined symbol或者gcc: error: unrecognized command line option前者是编译顺序错了后者是 GCC 版本太新需要降级到 7 以下。setup.cfg里的[metadata]和[options]字段定义了包的版本、依赖和入口如果编译不通过先检查它和setup.py里的参数是否一致。4. 环境部署与训练自己的数据集Dockerfile 和参数配置4.1 Dockerfile 里的环境是怎么搭起来的这套源码自带 Dockerfile这是最省心的一环。读 Dockerfile 的目的是搞明白项目依赖了哪些系统库和 Python 包避免在本地环境里缺这个缺那个。典型的多阶段构建流程是第一步拉一个 CUDA 官方镜像比如nvidia/cuda:11.3.1-cudnn8-devel-ubuntu20.04第二步安装 Python 3.8 和系统依赖第三步用 pip 装 PyTorch、torchvision 和项目所需的包最后把源码目录复制进容器。# 构建并进入容器 docker build -t gait_track:latest . docker run -it --gpus all --shm-size8g gait_track:latest /bin/bash--shm-size8g是容易忽略的参数。PyTorch 的 DataLoader 在多进程模式下会用共享内存做数据缓存容器默认的/dev/shm只有 64MB数据加载稍微大一点就报Bus error或out of shared memory这个坑在监控视频数据集上特别常见。如果宿主机的内存有限也可以把num_workers设为 0但那样训练速度会明显下降。4.2 数据集组织方式yolov5、DeepSORT、GaitSet 三种数据格式整套系统用到三种不同的数据标注格式这是最容易混乱的地方需要分别准备。模块数据格式目录组织关键点yolov5YOLO txt 格式每行cls x y w himages/和labels/按 train/val 分坐标是归一化后的不是像素值DeepSORT检测结果文本每行frame_id, track_id, x, y, w, h, confMOT 风格训练时不需要标注需要的是检测框和 IDGaitSet轮廓图序列每段序列一个文件夹subject/sequence/按行人 ID 分需要先对原图做前景分割提取二值轮廓GaitSet 的数据准备是最耗时的。你需要一个分割模型把行人从背景中抠出来或者用背景减除算法生成轮廓图。轮廓图的尺寸建议统一为 64x44这是 GaitSet 训练时的常用尺寸。轮廓的质量直接决定步态特征的好坏——轮廓上有空洞、有背景残留特征就会带噪声。如果你手头的是 CASIA-B 数据集可以直接用官方的预处理脚本如果是自采数据需要先过一遍检测和分割再生成训练集。4.3 超参数调整yolov5 训练自己的数据集yolov5 训练时的超参数文件是data/hyps/hyp.scratch-low.yaml里面包含了 lr、mosaic、mixup 等参数。用预训练权重做迁移学习时建议把lr0从默认的 0.01 降到 0.001因为你的数据集规模和类别数都变了过大的学习率会破坏预训练特征。# hyp.scratch-low.yaml 关键参数训练行人检测时推荐修改 lr0: 0.001 # 初始学习率迁移学习用小一点 lrf: 0.01 # 最终学习率比例 warmup_epochs: 3.0 # 预热轮数 mosaic: 1.0 # mosaic 增强行人数据集可以保留 fliplr: 0.5 # 水平翻转对行人检测友好# 训练命令 python train.py --data person.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --device 0--batch 16是在 8GB 显存的 GPU 上比较稳妥的值显存不够就减半。--img 640和推理时的size640保持一致训练和推理尺寸不一致会掉点。5. 避坑指南跨镜头关联里最容易翻车的五个点5.1 权重文件和 .cu 文件不匹配编译过了但结果全错现象编译成功import成功但 GNN 传播的输出全是 NaN。原因gnn_propagate_kernel.cu是在一个特定 PyTorch 版本下写的和你当前的 PyTorch 版本在张量内存布局或计算图处理上不兼容。解决先跑一个 2 节点 3 特征的小用例用 CPU 实现的结果和 GPU 实现的结果做逐位对比如果差异巨大降级 PyTorch 版本或者检查是否有重复定义的宏。5.2 时间对齐没做跨镜头匹配率直接砍半现象镜头 A 和镜头 B 记录的是同一场景但时间戳相差了十几秒GNN 的邻接矩阵里几乎找不到跨越两个相机的边。原因time_window参数判断的是轨迹的时间重叠度两个镜头的时间不同步导致同一个目标在 A 镜头结束和 B 镜头开始的帧号完全对不上。解决在预处理阶段做一个时间偏移校准用同一时刻出现在两个画面中的目标哪怕是不同 ID估算帧号偏差然后统一到同一个时间轴再跑关联流程。5.3 GaitSet 特征没有归一化相似度计算失灵现象余弦相似度算出来全部在 0.99 以上设什么阈值都没区分度。原因GaitSet 输出的特征向量模长差异巨大直接算余弦相似度等于在比模长而不是比方向。解决在特征输入 GNN 之前强制做 L2 归一化让每个特征向量的模长为 1。import torch import torch.nn.functional as F def normalize_features(features): # features: [N, D] 原始步态特征 return F.normalize(features, p2, dim1)5.4 小目标检测漏检轨迹断裂后跟踪器无法恢复现象监控画面里的人离摄像头远身体只有 20 像素高yolov5 经常漏检DeepSORT 的轨迹频繁丢失。原因yolov5 在 COCO 上的小目标 AP 本来就低20 像素的行人已经超出了常规检测能力。解决一是把推理尺寸从 640 提高到 1280显存够的话二是对漏检帧做插值。最稳妥的做法还是换一个针对小目标优化的检测器分支或者给检测结果加一帧的容忍——上一帧有检测框、这一帧没有先沿用上一帧位置做卡尔曼预测。5.5 图里节点全是负样本步态识别退化成换装重识别现象跨镜头的匹配准确率还不如纯 ReID加了 GNN 反而掉点。原因负样本不同 ID 的轨迹在图里占了大多数GNN 传播时每个节点的特征都被大量负样本的噪声特征污染了正样本的信息被稀释。解决在build_adjacency_matrix里提高相似度阈值或者在构建图的时候加上一个先验约束——只有空间上可能衔接的镜头对才允许连边。最直接的办法是统计每个目标的轨迹平均长度去掉那些小于 10 帧的短轨迹——步态特征本身就需要连续帧才能稳定提取。6. 跨镜头轨迹拼接的落盘验证用两个摄像头实测一轮拿到源码后第一步不是改代码而是先验证跨镜头拼接的质量。我习惯的做法是准备两个模拟镜头的数据同一个场景里两个不同角度的录屏中间有 3 秒的交接区。跑完整个流程后看两件事。第一件事是检查轨迹文件里有没有出现「同一个全局 ID 在不同镜头里时间上重叠」的情况。如果 ID 5 在镜头 A 的第 100 帧和镜头 B 的第 100 帧同时出现说明 GNN 匹配错了。第二件事是可视化拼接结果把两个镜头的画面拼在一起用同一个颜色画同一个人这个办法笨但直接比任何指标都直观。import cv2 import numpy as np # 读取两个摄像头的轨迹输出 # 格式: frame_id, global_id, camera_id, x, y, w, h cam_a np.loadtxt(track_cam_a.txt, delimiter,) cam_b np.loadtxt(track_cam_b.txt, delimiter,) # 按全局 ID 把轨迹拆开检测重叠 ids_a set(cam_a[:, 1]) ids_b set(cam_b[:, 1]) overlap_errors [] for gid in ids_a ids_b: frames_a set(cam_a[cam_a[:, 1] gid][:, 0]) frames_b set(cam_b[cam_b[:, 1] gid][:, 0]) if frames_a frames_b: overlap_errors.append(gid) print(f重叠轨迹数: {len(overlap_errors)})overlap_errors如果超过 3%说明邻接矩阵的边建得太宽松了优先调整sim_threshold和时间窗口。如果这个指标没问题再计算匹配的准确率——手动标注一段视频里真实匹配的轨迹对数和 GNN 输出的匹配结果做对比。做完这两步你才真正摸透了这套源码的输出和评价维度后面无论改哪个模块都有基准可对照。这套流程里的每一步我都吃过亏。以前偷懒直接拿默认配置跑结果跨镜头 ID 切换率惨不忍睹检查了半天才发现是时间偏移和数据归一化的问题。那之后每次做跨镜头实验我都强制先验证数据对齐再跑完整链路。希望这篇笔记能帮你少走几步弯路。本文还有配套的精品资源点击获取
