简介:面向人工智能及相关专业本科毕业设计场景,这份压缩包提供了一套基于 YOLOv5 的步态识别多目标跨镜头跟踪检测算法实现,核心方法为 YOLOv5+DeepSORT 框架负责目标检测与跟踪,并引入 GaitSet 算法完成步态识别,适合需要复现实验、研究跨镜头行人关联与重识别问题的开发者。资源共 343 个文件,约 22.23MB,以 Python 源码、YAML 配置、Markdown/RST 说明文档为主体,另含模型权重、Dockerfile、CUDA 扩展等支撑文件,结构贴近实际项目,便于按模块查阅与二次开发。目前已有 4449 人学习使用,热度较高。通过阅读源码可清晰梳理检测、跟踪、特征提取与跨镜头匹配的完整链路,并以此为基础进行指标调优、场景迁移或毕业设计论文中的方案论证。
1. 人工智能毕设里的步态识别,yolov5 并不是主角
人工智能本科毕业设计里,“步态识别”这四个字很容易被误读成一句口号:让机器通过走路姿势认出人。真正动手后你会发现,基于 yolov5 的步态识别多目标跨镜头跟踪检测算法系统里,yolov5 只是最前置的一环,它的任务是把行人从画面里框出来,至于“这个人是谁”,要靠后面的步态特征网络回答。这套系统的真正难点和多目标跨镜头跟踪检测的核心价值,在于目标走出镜头 A 后,进入镜头 B 时系统还能认出“刚才那个人”,而不是重新分配一个新编号。它适合做人脸不可见场景(背对、遮挡、低分辨率)的身份识别演示,也适合作为目标跟踪与跨镜头匹配的完整毕设骨架。下面按数据准备、检测训练、跟踪调参、跨镜头匹配、避坑排错依次展开。
2. 架构取舍:yolov5 管检测,步态识别管身份,跨镜头管 ID
2.1 先把 yolov5 放在正确的位置上:它不做识别,只产出行人框
很多毕设把 yolov5 当成“识别一切”的黑匣子,训练完就想让它直接输出“这是张三”。实际上 yolov5 网络结构图的最后输出只有三样东西:目标框坐标、置信度、类别 ID。在步态识别系统里,类别 ID 全部是 person,身份信息完全不在检测器的输出范围内。整套系统的数据流是这样的:视频帧进入 yolov5,输出 N 个行人 bbox;每个 bbox 被送到跟踪模块;跟踪模块负责把连续帧里的同一个行人串起来,得到临时轨迹 ID;步态识别模块从同一 ID 的整段轨迹里提取步态特征,输出“这个人是谁”;跨镜头匹配模块再把这些身份特征拿到其他相机画面里做比对。
选择 yolov5 而不是更新的检测器,三个务实理由。第一,预训练权重和社区资料足够多,COCO 上训练好的 yolov5s 可以直接拿来当行人检测的起点,小样本微调也能稳住。第二,部署和导出链路成熟,PyTorch 训练完可导出 ONNX 再转 TensorRT,后续想在树莓派 4b 或 RK3568 这类边缘盒子上部署,都有大量现成踩坑记录可以搜到。第三,超参数和后处理都集中在几个 yaml 和命令行参数里,毕设演示时调整置信度阈值、NMS 的 iou 阈值都方便解释,答辩现场改参数不心虚。yolov5s 和 yolov5m 的选择上,我一般推荐 s:显存占用低,视频实时性更好,检测精度对步态识别来说够用——步态识别对框的要求是“框住人且别框进半个路人”,不需要像素级抠边。
2.2 步态识别的两条主流路线:轮廓序列与骨架关键点,毕设选哪条
步态识别领域公认的做法有两类。一类是外观轮廓法,代表方法有 GaitSet、GaitPart 这类,输入是背景分割后的行人剪影序列,模型在时间维度上做特征聚合。优势是精度上限高,在 CASIA-B 这类标准数据集上效果好;劣势是对背景分割质量敏感,摄像头抖动、光照突变会让剪影直接变形,而且这条路线和 yolov5 的衔接是断裂的——你得在 yolov5 的框内再跑一遍语义分割才能拿到剪影。
另一类是骨架关键点法,用姿态估计模型从行人框内提取人体关键点坐标序列,再按步态周期把关键点角度序列编码成特征向量。它的实现门槛低很多,关键点坐标天然不受衣着颜色和镜头色彩偏差的影响,跨镜头匹配时非常占便宜。毕设系统里我强烈建议走骨架关键点路线:yolov5 的框直接喂给姿态估计模型,省掉背景分割这一步,整条链路在普通笔记本上能跑实时。用表格对比两条路线:
| 对比项 | 外观轮廓法(GEI/GaitSet) | 骨架关键点法 |
|---|---|---|
| 输入依赖 | 背景分割后的剪影 | 检测框内关键点 |
| 与 yolov5 衔接 | 需要额外分割模型 | 直接复用检测框 |
| 跨镜头鲁棒性 | 对光照、色彩偏差敏感 | 对光照、衣着更稳 |
| 实现难度 | 高 | 中 |
| 毕设演示友好度 | 中 | 高 |
2.3 跨镜头跟踪的本质:镜头换了,特征空间也换了
单镜头内的多目标跟踪,ByteTrack 或 deepSort 都能做得很稳,因为同一镜头里目标的表观变化是连续的。跨镜头的难点在于:镜头切换后,检测框里的行人换了一组相机参数,色彩白平衡、分辨率、视角角度全变了。如果直接比对像素级外观特征,同一个人的相似度可能还不如另一个人。这也是很多毕设做跨镜头跟踪时发现系统“翻车”的原因——镜头 B 里出来的人几乎全被当成新目标。
那步态特征为什么适合当跨镜头的线索?因为步态特征来自骨骼关键点的相对几何关系——手臂摆动角度、腿跨步距、躯干倾斜程度,这些量和相机的颜色映射没有直接关系。哪怕目标背对镜头、脸完全不可见,只要能看到身体一个摆动周期的轨迹,就能提取到有区分度的特征。正是这一点,让“步态识别”成为跨镜头跟踪里衔接两个镜头的最自然桥梁。下一章先把检测器训练起来,后面再讲怎么把步态特征接到跟踪模块上。
3. 数据集与 yolov5 训练:从 CASIA-B 到能跑通的最小闭环
3.1 数据选型:CASIA-B 和自建视频,哪个更贴近毕设目标
CASIA-B 是步态识别方向绕不开的公开数据集:124 名受试者、11 个视角、3 种行走状态(正常、带包、穿外套)。但这里有个容易被忽视的错位:CASIA-B 发布的是原始行走视频和提取好的剪影序列,并不带 yolov5 需要的行人框标注。想用 CASIA-B 直接训练 yolov5,你得先对一帧帧视频重新画框,124 人每人 11 视角下来工作量几千张图,毕设时间根本撑不住。
常见做法是拆成两个任务分开喂数据。检测任务拿 COCO 预训练权重做迁移学习,只微调 person 这一类,几百张自建的行走视频帧标注就够;识别任务用 CASIA-B 的轮廓序列或关键点序列训练步态分类模型。这样数据集各司其职:检测器负责框人,步态网络负责认人。如果你不想碰自建标注,另一个替代方案是用 YOLOv5 官方在行人检测上的预训练模型直接出框,但要注意这是“只识别不绑定 ID”,跟踪模块的特征来源会弱一截。我一般建议自己做小样本标注,理由很简单——毕设答辩会被问“数据哪来的、怎么标的”,有自己亲手做的标注集比只调官方权重更像一个完整的算法系统。
3.2 把视频帧转成 YOLO 训练标签:XML 转 TXT 脚本与四个边界坑
假设你已经有标注数据,标注工具用的是 labelImg,输出格式是 VOC 的 XML。yolov5 训练需要的是每张图片同名的一个 TXT,每行一个目标,格式是“class_id center_x center_y width height”,四个坐标全部归一化到 0~1。转换脚本不难,下面这个版本可以在项目根目录直接跑:
import os import xml.etree.ElementTree as ET def xml_to_yolo(xml_path, out_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) bndbox = obj.find("bndbox") x_min = float(bndbox.find("xmin").text) y_min = float(bndbox.find("ymin").text) x_max = float(bndbox.find("xmax").text) y_max = float(bndbox.find("ymax").text) center_x = (x_min + x_max) / 2.0 / img_w center_y = (y_min + y_max) / 2.0 / img_h box_w = (x_max - x_min) / img_w box_h = (y_max - y_min) / img_h lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": # class_names 顺序必须和 data.yaml 里的 names 顺序一致 classes = ["person"] xml_dir = "annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue stem = xml_file[:-4] xml_to_yolo(os.path.join(xml_dir, xml_file), os.path.join(out_dir, stem + ".txt"), classes)逻辑说明:脚本从 XML 里读出图片宽高和每个目标的坐标,把左上右下角点换算成中心点加宽高并做归一化。归一化是关键,直接输出像素坐标会让 yolov5 的数据加载阶段把框解释成归一化值,训练无法收敛。脚本里 class_names 的顺序就是数据集的 ID 映射,必须和后续 data.yaml 里的 names 字段完全一致,否则检测结果会张冠李戴。跑完转换后做一件事:随机抽 30 张图,用 OpenCV 把 TXT 里的坐标画回去看一眼。常见翻车是 xmin/xmax 解析顺序写反,画出来的框是反的。
训练时把图片和标签按目录结构放好:
dataset/ images/ train/ 0001.jpg 0002.jpg ... val/ 0501.jpg ... labels/ train/ 0001.txt 0002.txt ... val/ 0501.txt ...yolov5 要求 images 和 labels 目录同名,训练脚本按图片路径自动找同名 txt。这个目录约定是 yolov5 的坑之一:图片叫 img/0001.jpg,标签就得在 label/0001.txt,且父目录名要一致,否则会报找不到标签的错。
3.3 训练命令与后处理阈值:yolov5 超参数、NMS 和跟踪的联动
数据备好后,在 yolov5 仓库目录下写 data.yaml。单类别配置长这样:
path: ./dataset train: images/train val: images/val nc: 1 names: ["person"]path 字段指向数据集根目录,train 和 val 是相对 path 的图片路径。启动训练:
python train.py --img 640 --batch 8 --epochs 100 \ --data data.yaml --weights yolov5s.pt \ --hyp hyp.scratch-low.yaml --project runs/train参数说明:img 640 是训练分辨率,步态场景行人占比不小,640 够用,提高分辨率收益不明显但显存占用翻倍;batch 8 是 8GB 显存的稳妥值,显存不够先降 batch 而不是降 img;weights 用 yolov5s.pt 的 COCO 预训练权重,只输出 person 一类,迁移学习起步更快。hyp.scratch-low.yaml 是低数据增强的超参数文件,小样本场景建议用这个而不是默认的 hyp.scratch.yaml,因为默认增强里的 HSV 扰动和 mosaic 会把本来就少的标注样本变得过于难学,容易过拟合。
训练完看 runs/train/exp/results.png,重点看 val_mAP_0.5 曲线。如果训练集 loss 降得很好但 mAP 上不去,典型问题就是标注错位或样本太单一,回到第 3.2 节的画框检查步骤。推理时的后处理参数同样值得调:conf_thres 默认 0.25,在跟踪系统里这个值建议再低一档到 0.15~0.2,因为 tracking 模块能利用时序信息剔除单帧误检,但无法弥补漏检——行人一旦短时间消失,轨迹就会断裂。iou_thres 是 NMS 的 IOU 阈值,默认 0.45,行人之间遮挡不多时不用动。这两个后处理参数直接决定检测框的召回率和干净程度,而跟踪效果的上限取决于检测的稳定性。
4. 多目标跨镜头跟踪:deepSort 参数调优与步态特征接续 ID
4.1 deepSort 在毕设里的 3 个必调参数:max_dist、max_age、max_iou_dist
检测器输出的每一帧 bbox 列表,要送给跟踪器做帧间关联。常见做法是用 deepSort,它的核心思路是两级匹配:先按外观特征的余弦距离做匈牙利匹配,匹配不上的再按 IOU 做第二次匹配。毕设里最常踩的参数坑是这三个。
max_dist 控制外观特征匹配的允许距离,默认 0.2。设得太大,比如 0.6,不同人外观相似时会被串成同一个 ID,画面里会出现一个 ID 在两三个人身上跳来跳去。设得太小,比如 0.05,稍微一点遮挡或姿态变化就匹配不上,同一个 ID 会频繁切换成新 ID。一般从 0.3 起步,在自建视频上跑一遍,数一下 ID Switch 次数再微调——这个调参过程本身就是毕设可以写进论文的消融实验。
max_age 控制轨迹在丢失以后最多存活多少帧。默认值 30,意思是目标离开画面后最多再保留 30 帧,超过就从轨迹池里删掉。跨镜头场景里 max_age 要拉长一些,比如 50~70,因为目标从镜头 A 离开到出现在镜头 B 可能有几秒的空白,拉长 max_age 能提高跨镜头接续的成功概率,代价是误匹配的可能性也会上升,需要配合步态特征匹配来兜底。
max_iou_dist 控制第二次 IOU 匹配的阈值,默认 0.7。目标被短暂遮挡时检测框的 IOU 会下降,这个值设太低会让遮挡后的目标直接丢失。行人密集走廊场景,我一般调到 0.8。注意这三个参数在 deepSort 原论文里对应一组默认值,但毕设场景的摄像头高度、行人密度都不同,照抄默认值基本等于放弃调优空间。
4.2 用步态特征向量做跨镜头的 ID 复用
单镜头内的跟踪 ID 是临时 ID,目标离开镜头那一刻,这个 ID 就失效了。跨镜头要解决的核心问题是:镜头 B 里出现一个新的检测目标,它是不是镜头 A 里刚刚消失的那个人。解决的思路是给每个消失的 ID 保存一份步态特征,再对新出现的目标提取步态特征做相似度匹配。
提取步态特征这一步,真实工程里可以做得非常重,但毕设规模有一个轻量且能出效果的做法。检测框内用轻量级姿态估计模型(比如 HRNet 或 MoveNet)拿到 17 个关键点坐标,计算左右肩、左右髋、左右膝之间的角度序列;攒够一个步态周期(大约 0.8~1.2 秒,也就是 24~36 帧 30fps 视频)后,把每个周期的角度序列做时间平均池化,得到一条固定长度的特征向量。代码骨架如下:
import numpy as np def angle_between(p1, p2, p3): # p1/p2/p3 是 (x, y) 坐标,返回 p1-p2-p3 在 p2 处的夹角 v1 = p1 - p2 v2 = p3 - p2 cos = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-8) return np.arccos(np.clip(cos, -1.0, 1.0)) def compute_gait_vector(keypoint_sequence, frame_rate=30): # keypoint_sequence: [T, 17, 2],T 是帧数 angles = [] for frame in keypoint_sequence: left_hip = frame[11]; left_knee = frame[13]; left_ankle = frame[15] right_hip = frame[12]; right_knee = frame[14]; right_ankle = frame[16] angles.append(angle_between(left_hip, left_knee, left_ankle)) angles.append(angle_between(right_hip, right_knee, right_ankle)) angles = np.array(angles) # [T, 2] period = int(frame_rate * 1.0) # 取1秒窗口近似一个步态周期 n_periods = max(1, len(angles) // period) vec = angles[-period * n_periods:].reshape(n_periods, period, 2).mean(axis=0) return vec.flatten() # 固定维度: period*2 def cosine_sim(feat_a, feat_b): return float(np.dot(feat_a, feat_b) / (np.linalg.norm(feat_a) * np.linalg.norm(feat_b) + 1e-8))逻辑说明:这个方法没动用深度学习模型做特征提取,纯几何特征在跨镜头场景反而更稳,因为关键点角度不受镜头色彩偏移影响。代码里的 period 是 1 秒窗口,实际数据里人和人的步频差异很大,跑步和走路混在一起时窗口要缩短到 0.8 秒,否则相位错位会把平均池化结果打乱。跨镜头匹配时,对镜头 B 里的新检测目标,同样攒够一个步态周期再算 cosine_sim,超过阈值就分配镜头 A 里那个 ID,否则建立新 ID。阈值常用 0.85 起步,但不同摄像头角度下同一个人的步态向量余弦相似度波动很大,实际范围在 0.75~0.92 之间,这个值必须在你的具体场景里扫一遍再定。
4.3 串联主循环:检测、跟踪、特征提取、跨镜头匹配的每帧动作
把整条链路落成主循环伪代码,会让答辩时讲系统设计容易很多:
while cap.isOpened(): frame = cap.read() dets = model.detect(frame, conf_thres=0.2, iou_thres=0.45) tracks = deepsort.update(dets, frame) # tracks: 每个元素包含 bbox, track_id, 以及供特征使用的crop区域 for track in tracks: tid = track.track_id if track.is_confirmed(): feature_queue[tid].append(extract_keypoints(frame, track.bbox)) # 队列攒满一个周期后,计算步态向量 if len(feature_queue[tid]) >= PERIOD_FRAMES: gait_vecs[tid] = compute_gait_vector(feature_queue[tid]) feature_queue[tid].clear() # 跨镜头匹配:新出现的track且无步态向量时,与已保存的跨镜向量做匹配 for track in tracks: if gait_vecs.get(track.track_id) is None: best_id, best_sim = None, 0.0 for saved_tid, saved_vec in cross_camera_vecs.items(): sim = cosine_sim(compute_gait_vector(recent_keypoints[track.track_id]), saved_vec) if sim > best_sim: best_id, best_sim = saved_tid, sim if best_sim > MATCH_THRES: track.track_id = best_id # ID复用 else: cross_camera_vecs[track.track_id] = \ compute_gait_vector(recent_keypoints[track.track_id])逻辑说明:主循环里每个 track 维护一个关键点队列,攒够一个步态周期的帧数后计算一次特征向量并清空队列,避免重复计算。跨镜头匹配发生在目标刚出现且还没有步态特征的时候,用最近的关键点窗口补算特征,和已离开的跨镜头特征库做余弦相似度匹配。这段伪代码里 PERIOD_FRAMES 和 MATCH_THRES 是两个最该调的参数:PERIOD_FRAMES 太小特征不稳,太大则目标已经走出镜头 B 还没完成识别;MATCH_THRES 太小会串 ID,太大会看到同一个目标在多个镜头里出现多个 ID。建议在录制好的双镜头测试视频上,对这两个参数各取三档做交叉验证,记录 ID Switch 次数,选最小的那一组。
5. 避坑清单:小样本过拟合、标签错位与跨镜头 ID 漂移的排查
5.1 小样本训练过拟合:训练集 loss 好看,验证集 mAP 抬不起头
现象:训练时训练集 loss 稳步下降,但 results.png 里验证集 mAP 一直在 0.2 以下,检测器在没见过的角度和距离上漏检严重。
原因:一是 COCO 预训练权重被全量微调,backbone 的特征被小样本数据带偏;二是数据增强太强,样本又被反复扭曲,模型把增强后的噪声也记住了。
解决:冻结 backbone 前几层,只微调 neck 和 head;换 hyp.scratch-low.yaml;每类样本量低于 500 张时,mosaic 增强比例调低。yolov5 的 train.py 里通过 --freeze 参数控制冻结层数,我一般用 --freeze 10,把 backbone 整体冻住。
5.2 标签错位:loss 降不下去,画框发现框根本不在人身上
现象:训练了 50 个 epoch,box_loss 一直不降,随机抽图看推理结果,框和行人位置完全对不上。
原因:XML 转 TXT 时归一化公式写错了,或者 class_names 顺序和 data.yaml 不一致。
解决:回到第 3.2 节的转换脚本,在批量转换后加一个可视化校验函数——随机读 10 张图和对应的 TXT,用 OpenCV 的 rectangle 把归一化坐标还原成像素坐标画出来。这个校验步骤耗时五分钟,却是我见过最能省时间的后悔药。注意 yolov5 读取标签时不会报错,错位的标签只会表现为 loss 异常,定位问题要靠人工看图。
5.3 跨镜头 ID 漂移:同一个目标在两个镜头里变成两个 ID
现象:目标从镜头 A 走到镜头 B,B 里出现的新 ID 和 A 里的旧 ID 并存,系统不认为它们是同一个人。
原因:不同摄像头色彩白平衡差异大,如果跨镜头匹配用了外观特征 ReID,像素映射差异直接拉低相似度;或者 MATCH_THRES 设得太高,同一视角下微小的步态相位偏差就让余弦相似度跌破阈值。
解决:跨镜头匹配强制只用骨架角度特征,计算前先对关键点坐标做以髋关节为中心的位置归一化;再把 MATCH_THRES 下调 0.05~0.1,跑一遍双镜视频统计 ID Switch,取拐点值。这个坑被问的概率极高,建议在论文里放一张“跨镜头相似度分布图”,证明你的阈值不是拍脑袋定的。
5.4 视频实时性不达标:检测、跟踪、特征提取串行跑不满 25 帧
现象:笔记本上整套系统跑起来只有 8~10 帧,画面明显卡顿。
原因:主循环是串行执行,yolov5 的检测和姿态估计都吃 GPU,特征提取又占 CPU,三者挤在一条时间线上。
解决:优先把 img-size 从 640 降到 416,检测耗时接近减半;步态特征提取不要每帧做,每 5 帧做一次,中间帧用上一帧的关键点近似;显存允许的话把 batch 设成 2,用 PyTorch 的自动批处理让两帧的检测和特征提取并行。极致一点的做法是把姿态估计模型量化成 FP16,边缘设备上量化部署的收益远大于调参——毕设阶段先保证在演示机上跑满 20 帧就够。
5.5 步态队列长度玄学:时长窗口设不对,特征向量变成噪声
现象:同一个人的两次跨镜匹配,相似度在 0.6 和 0.95 之间剧烈跳动。
原因:步态向量的计算窗口没有和真实步态周期对齐,窗口里横跨了两个不同节奏的步态段,平均池化把相位信息抹掉了。
解决:先算出关键点角度序列的自相关峰值,用峰值间隔动态估计步态周期,再按这个周期截取窗口;窗口长度用 1.2 倍周期,而不是固定秒数。代码里 compute_gait_vector 的 period 参数改成动态计算后,匹配稳定性提升非常明显,这一步调好之后,MATCH_THRES 才能从玄学变成可解释的阈值。
6. 让毕设站得住:用 MOTA 和 ID Switch 证明你的系统真的有效
6.1 三个指标,别只拿 mAP 说事
检测器能拿 mAP,但整套步态跨镜头系统的核心指标是 MOTA、IDF1 和 ID Switch。MOTA 综合了漏检、误检和 ID 切换惩罚,数值低于 0.5 在学术上都算吃力;ID Switch 是跨镜头场景最该关注的指标——它直接统计一个目标的 ID 被错误切换的次数。建议在自备的测试视频上跑三组对比:纯 deepSort、deepSort 加单镜头步态特征、deepSort 加跨镜头步态特征匹配。记录每组的总 ID Switch 数和 MOTA,一张表格就能证明步态特征模块到底贡献了多少。这一步是论文里最有说服力的实验设计,也是答辩时最容易被追问的部分。
6.2 现场演示的两个保险
毕设答辩的现场演示,我有一条血泪经验:不要依赖现场摄像头。现场灯光、人员走动、图传延迟都会让系统在关键时刻翻车。录一段固定机位的双镜头视频,目标从镜头 A 走向镜头 B,故意做一次转身再继续走,这段素材能同时展示检测的稳定性、单镜跟踪的连续性、跨镜头 ID 的保持。另一个保险是提前把 conf_thres 参数暴露在配置文件里,演示时遇到突发遮挡导致漏检,当场调低一档再重跑,比在答辩台上改代码体面得多。
这套方案做完,你得到的不只是一个能跑的 demo,而是一条清晰的“检测→跟踪→特征→匹配”技术链路,每个环节都有可解释的参数和可验证的指标。我当年做类似题目时最大的教训是贪多:既想换更强检测器,又想改步态网络结构,结果每一块都不够深。如果你时间有限,先把检测稳定性做扎实、把跨镜头的相似度分布调明白,整篇论文就已经站得住了。希望帮到你。
本文还有配套的精品资源,点击获取