☰
YOLOv5+HRnet姿态估计项目实战:从环境搭建到关键点检测全流程
2026/10/14 9:44:24 网站建设 项目流程

简介:整套YOLOv5人体姿态估计工程文件,基于HRnet与SimDR完成图片、视频及摄像头场景下的关键点实时检测,面向深度学习姿态估计入门及进阶开发者,帮助省去自行配置参数与下载权重的繁琐过程。压缩包内含2000个文件,以1867个Python脚本为主体,并包含C/C++源文件与头文件、文本配置、Markdown文档及JSON等资源,整体约841.53MB,目录覆盖项目准备、目标检测、姿态估计、结果演示和报错分析五大模块。已有1757人学习下载,内容从PyCharm克隆项目、环境配置、添加SPPF模块,到修改yaml并绘制骨骼关键点均有完整说明,同时提供照片、视频和实时摄像头的结果演示。针对路径问题、matplotlib.use('Agg')写入文件、Upsample的recompute_scale_factor属性及gbk编码异常等常见报错,也整理了具体分析与解决思路,可直接对照排查,适合希望快速跑通完整流程并继续二次开发的读者。

1. 从YOLOv5到HRnet:姿态估计项目的正确打开方式

把目标检测和人体关键点检测串成一条完整链路,是YOLOv5姿态估计这类项目最实在的价值所在。只用YOLOv5,模型只能告诉你「画面里有个人」,但这个人手抬到哪、腿弯到什么角度,它完全不知道。接上HRnet之后,模型会在检测框内部继续回归出人体关键点坐标,再按骨骼连接关系把它画成骨架图,整条链路跑通之后,动作分析、健身计数、跌倒检测这类功能才有了底层支撑。这个项目用YOLOv5负责「框住人」,再用HRnet负责「找出骨架」,配合SimDR完成关键点回归,既可以处理静态图片,也能直接跑视频和摄像头实时画面。我实际拆过这份工程文件之后可以明确说:这份资源适合已经有PyTorch基础、想快速跑通姿态估计完整流程的人,不需要从零训练模型,把路径改对、权重放妥就能出结果,真正的难点反而不在模型本身,而在工程环境里的各种细节。

2. 项目准备:环境搭建、权重落位与一张图验证

2.1 克隆工程与Python环境配置

拿到这份工程文件之后,第一步不是急着配置模型参数,而是把Python环境和目录结构先理顺。对于PyTorch系的检测项目来说,环境里最容易出问题的就是torch和torchvision版本不匹配,其次是numpy版本过高导致某些旧代码报错。我一般会新建一个虚拟环境,单独给这个项目用,而不是直接装在base环境里,这样翻车了可以整个删掉重建,相当于给自己留了后悔药。

git clone <项目仓库地址> # 建议用SSH方式克隆,避免HTTPS拉取时的权限问题 cd <项目目录> python -m venv venv # 创建独立虚拟环境 source venv/bin/activate # Linux/Mac激活;Windows下用 venv\Scripts\activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt

第一行克隆命令之后一定要确认仓库是否完整,有时候网络中断会造成对象文件缺失,后续导入模块时会莫名其妙报ModuleNotFoundError。创建独立虚拟环境这一步建议不要跳过。之前有位同事在生产环境里图省事直接往base环境装,结果把另一个项目的tensorflow依赖搞坏了,最后花了整整一天排查。

安装PyTorch时之所以单独指定index-url,是因为默认pip源安装的torch往往不带CUDA支持,安装完之后虽然能导入,但检测速度会慢一个数量级。这里CUDA版本号需要和你的显卡驱动对应,如果显卡比较老,改成cu118甚至cpu版本都可以,推理慢一点但流程能跑通。

2.2 权重文件放置规则与路径统一

这份资源的一个核心卖点就是「不用自己重新训练权重」。YOLOv5的检测权重负责输出人体边界框,HRnet的权重负责在框内回归关键点坐标,两者是不可互换的。工程文件里权重文件通常比较大,有些同学从网盘下载之后随意解压到一个临时目录,然后程序一运行就报FileNotFoundError,本质就是代码里的路径和你实际的权重路径没有统一。

项目根目录/ │ ├── weights/ │ ├── yolov5m.pt # YOLOv5检测权重 │ ├── hrnet_w32.pth # HRnet关键点回归权重 │ └── simdr_w32.pth # SimDR辅助权重(可选) │ ├── models/ │ ├── yolo.py # YOLOv5模型定义 │ └── hrnet.py # HRnet模型定义 │ └── data/ ├── coco.yaml # 数据集配置 └── 测试图片.jpg

路径问题在跨平台时尤其明显。Windows的路径分隔符是反斜杠,而代码里写死的正斜杠路径在Windows上依然能解析,反过来就一定会报错。我自己习惯的做法是把所有涉及权重的路径集中在一个config.py里统一管理,不让任何路径散落在业务代码中间,这样后面换机器部署时只需要改一个文件。

还有个容易忽视的细节:权重文件和模型架构是绑定的。这份工程里如果对应的是yolov5m的模型定义,但你在目录里放了一个yolov5s的权重文件,程序不会第一时间报错,而是会在第一次推理时输出维度不一致的崩溃信息,排查起来比直接报错更烦。

2.3 处理一张测试图片验证链路通断

环境配置完成、权重落位之后,不要急着上摄像头,先用一张测试图验证整条链路是否通断。

import torch import cv2 from models.hrnet import HRNet from models.yolo import YOLODetector # 加载模型,device选择顺序:CUDA优先,其次CPU device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') detector = YOLODetector(weights='weights/yolov5m.pt', device=device) hrnet = HRNet(weights='weights/hrnet_w32.pth', device=device) img = cv2.imread('data/测试图片.jpg') boxes = detector.detect(img, conf_thres=0.5) # 置信度阈值设0.5,低于此值的框丢弃 keypoints = hrnet.predict(img, boxes) # HRnet在检测框内预测关键点 vis_img = hrnet.draw_skeleton(img, keypoints) # 绘制骨骼和关键点 cv2.imwrite('output/result.jpg', vis_img)

这段代码里,detect函数的conf_thres参数用来控制检测框的置信度阈值,调低了可能会检出更多假框,调高了又容易漏检。在姿态估计场景里,检测框的误检比漏检影响更大,因为一旦框错了位置,后面的关键点回归就全部建立在错误区域上,所以阈值在0.5左右比较合适。draw_skeleton函数内部会判断每个关键点的置信度分数,分数低的关键点不会绘制,避免在画面上留下大量无意义的噪点。

运行这一步时如果出现中文路径相关的报错,大概率是Windows下的编码问题,后面会在报错排查章节里专门讲。

3. 目标检测段:边界框获取与SPPF模块改造

3.1 检测器的推理链路:从图像输入到框坐标输出

在姿态估计项目里,YOLOv5承担的角色是「候选区生成器」。它不是整个项目的终点,而是HRnet的前置依赖。推理流程可以拆成三个环节:图像预处理、模型前向推理、后处理解析。预处理部分,网络输入尺寸的选取直接影响检测精度与速度的平衡,常见做法是将图像resize到640×640,这个尺寸在mAP和推理速度之间折中得比较好。

YOLOv5的原生输出是一个张量,张量的维度是(batch, 25200, 85),其中25200表示三个特征图尺度上的候选框总数,85的前5个值是框的xywh和objectness置信度,后面80个值是COCO数据集的类别概率。如果你在数据加载阶段加上了多尺度训练或者直接用了别的输入尺寸,这个25200会随之改变,所以调试阶段打印输出张量的shape是第一个排查点。

注意,这里有一个关键的坐标转换步骤。YOLOv5输出的边界框坐标是基于输入尺寸的绝对像素值,不是归一化坐标。如果直接把它传给HRnet而不做任何换算,HRnet就会在错误的位置上找关键点。正确方式是先还原到原始图像尺寸,或者统一到一个中间分辨率。

3.2 边界框解析逻辑与置信度过滤

从原始输出张量到一组干净的边界框坐标,中间要经过解码、过滤、非极大值抑制三个步骤。

def postprocess(prediction, conf_thres=0.5, iou_thres=0.45): # prediction: (1, 25200, 85) 原始输出 xc = prediction[..., 4] > conf_thres # 按objectness置信度初筛 output = [] for i, pred in enumerate(prediction): pred = pred[xc[i]] # 只保留置信度达标的框 if len(pred) == 0: output.append(torch.zeros((0, 6))) continue box = xywh2xyxy(pred[:, :4]) # 把中心点宽高格式转为左上右下角点格式 cls_conf, cls_pred = pred[:, 5:].max(dim=1, keepdim=True) detections = torch.cat((box, pred[:, 4:5], cls_conf, cls_pred.float()), dim=1) # 按类别做非极大值抑制,同一目标上重叠框只保留置信度最高的 keep = nms(detections[:, :4], detections[:, 4], iou_thres) output.append(detections[keep]) return output

这段代码是整个目标检测后处理的核心逻辑。xywh2xyxy这一步非常容易踩坑,YOLOv5输出的框是(center_x, center_y, width, height),而绘制和裁剪操作需要的是(x1, y1, x2, y2)格式,没有转换的话,画出来的框位置会整体偏移。非极大值抑制的iou阈值,在多人姿态估计场景下建议比纯目标检测稍微调严一点,设到0.45左右,因为两个紧紧挨着的人如果阈值过松,其中一个的框会被抑制,那个人就直接丢了没姿态可测。

参数调优时有个经验值:检测框的置信度阈值不要设得和纯检测任务一样高。姿态估计的下游任务对误报更敏感,但漏报更致命,因为漏掉一个人就意味着那一路的关键点数据完全为空。所以conf_thres用0.3到0.5之间是合理区间,不建议超过0.6。

3.3 SPPF模块在工程中的实际作用与改动

这份工程文件里单独提到了SPPF模块的添加,SPPF全称是Spatial Pyramid Pooling Fast,是YOLOv5中用来增大感受野的模块。它的思路是对输入特征图依次做多次最大池化,然后把不同尺度的池化结果拼接起来,这样同一个网络就能同时捕捉到小范围和大范围的目标上下文信息。

输入特征图(C, H, W) │ ├── MaxPool(k=5, s=1, p=2) ──┐ │ │ │ │ MaxPool(k=5, s=1, p=2)───┤ 拼接 → 输出特征图(4*C, H, W) │ │ │ │ MaxPool(k=5, s=1, p=2)───┘ └─────────────────────────────┘

在姿态估计这个场景中,SPPF模块的意义在于人体大小变化极大。摄像头里离得远的人可能只有几十个像素高,离得近的人则充满整个画面,多尺度池化让检测器对这两种极端情况都有响应能力。如果直接用原版YOLOv5而不加SPPF,小目标人体漏检率会明显上升。

这里有个常见误区:有些同学认为SPPF是必须修改模型结构才能加的。实际上如果你直接用的是较新版本的YOLOv5,SPPF已经是内置模块,不需要额外改动。这份工程文件里提到添加SPPF,更多是为了兼容旧版本代码或者是在改造backbone时保留这个结构。改完之后一定要重新加载权重,因为模型结构变了之后,权重文件里对应的层名也会变化,不匹配的话会在torch.load时直接抛StateDict错误。

4. 姿态估计段:HRnet关键点回归与骨骼绘制

4.1 HRnet的模型结构与yaml配置改动

HRnet的核心特点是它在整个前向过程中始终保持高分辨率特征图,而不是像很多分类网络那样先下采样再上采样。高分辨率特征图的保留对关键点定位至关重要,因为关键点回归本质上是一个逐像素预测问题,关键点的坐标误差受限于特征图的空间分辨率。网络通过并联多个不同分辨率的子网络,并在每个阶段做跨分辨率的信息融合,让高低分辨率的特征互相补充。

工程文件里提到的修改yaml文件,主要是为了告诉HRnet模型几个关键配置:输入图像尺寸、输出关键点数量、以及训练时采用的数据集格式。这里有一个需要特别清醒的点:如果你直接用官方预训练权重,yaml里的num_classes必须和权重训练时的配置保持一致,也就是17个COCO关键点,不能改。你改yaml的真实目的只是「告诉代码到哪里找权重、输入尺寸是多少」,而不是调整模型结构。

# models/hrnet_w32.yaml MODEL: PRETRAINED: 'weights/hrnet_w32.pth' # 预训练权重路径,必须存在 NUM_KEYPOINTS: 17 # COCO标准17个关键点 INPUT_SIZE: [192, 256] # 输入宽高,按HRNet惯例保持宽192、高256 HEATMAP_SIZE: [48, 64] # 热图分辨率,输入尺寸的1/4 SIGMA: 2 # 高斯核标准差,关键点标注生成热图时用

这段配置里,HEATMAP_SIZE和SIGMA这两个参数在仅做推理预测时不参与计算,但只要你想在现有权重上做任何微调,这两个参数就需要和训练时保持一致,否则关键点的ground truth热图对不上。输入尺寸从192×256改成384×512会小幅提升关键点定位精度,但推理时间几乎翻倍,实时场景里不太划算。

4.2 关键点坐标折算:从热图到原图坐标

HRnet的输出是一个热图张量,尺寸为(num_keypoints, heatmap_height, heatmap_width),热图上的每个像素值表示该位置出现某个关键点的概率。直接从热图里取最大值所在的坐标,只能得到热图坐标系下的位置,需要两步换算才能还原到原始图像坐标。

def get_keypoints(heatmap, box, input_size, original_size): # heatmap: (17, 64, 48) 模型输出 # box: (x1, y1, x2, y2) 在原始图像坐标系下的检测框 batch_keypoints = [] for hm in heatmap: score, idx = torch.max(hm.view(-1), dim=0) # 取热图全局最大值 y, x = idx // hm.shape[1], idx % hm.shape[1] # 反推热图坐标 # 亚像素精修:在最大值附近做局部加权平均,可提升1~2像素精度 x = max(0, min(x, hm.shape[1] - 1)) y = max(0, min(y, hm.shape[0] - 1)) x = (x + 0.5) * (input_size[0] / hm.shape[1]) # 放大到输入尺寸 y = (y + 0.5) * (input_size[1] / hm.shape[0]) # 从输入尺寸坐标系映射到检测框局部坐标系 box_w, box_h = box[2] - box[0], box[3] - box[1] orig_x = box[0] + (x / input_size[0]) * box_w orig_y = box[1] + (y / input_size[1]) * box_h batch_keypoints.append((orig_x, orig_y, score)) return batch_keypoints

这里最关键的逻辑是坐标的两次映射。第一次乘以0.5是为了对齐像素中心,避免系统性的偏移误差;第二次从输入尺寸映射到检测框坐标,是把关键点从网络的输入分辨率拉回到原始图像上。很多跑出来的骨架整体错位一个方向,全是这一个公式写错的锅。

4.3 绘制骨骼关键点:连接线与置信度过滤

关键点坐标拿到之后,还需要按人体结构的连接关系把它们画成骨架。COCO数据集的17个关键点有固定的序号规则,0是鼻子,1到4分别是左右眼和左右耳,5和6是肩膀,然后一路向下到脚踝。骨骼绘制本质上就是按这个序号规则连线。

SKELETON = [ (0, 1), (0, 2), (1, 3), (2, 4), # 面部连线 (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), # 手臂与躯干 (5, 11), (6, 12), (11, 12), # 髋部与躯干 (11, 13), (13, 15), (12, 14), (14, 16) # 腿部 ] COLORS = [ (255, 0, 0), (255, 0, 0), (255, 0, 0), (255, 0, 0), (0, 255, 0), (0, 255, 0), (0, 0, 255), (0, 0, 255), (255, 255, 0), (255, 255, 0), (0, 255, 255), (0, 255, 255) ] def draw_skeleton(img, keypoints, conf_threshold=0.3): for idx, (x, y, score) in enumerate(keypoints): if score > conf_threshold: cv2.circle(img, (int(x), int(y)), 3, COLORS[idx % len(COLORS)], -1) for start, end in SKELETON: sx, sy, ss = keypoints[start] ex, ey, es = keypoints[end] if ss > conf_threshold and es > conf_threshold: cv2.line(img, (int(sx), int(sy)), (int(ex), int(ey)), (0, 255, 0), 2) return img

绘制时最容易忽略的是置信度过滤条件,很多同学把关键点都画出来了,但同一根骨骼的两个端点置信度差很多,低置信度的端点位置本身就是模型瞎猜的,连出来的线自然也是错的。挂一个conf_threshold参数,低于阈值的关键点不画也不连线,画面会干净非常多。

5. 常见报错排查:五类高频问题的定位与修复

5.1 gbk codec can't decode byte:文件编码引发的路径崩溃

现象:程序运行到读取图片或加载yaml时报错,错误信息形如UnicodeDecodeError: 'gbk' codec can't decode byte 0x...,发生在Windows环境下的概率极高。

原因:Windows的默认编码是gbk,但项目代码里用open()读取的文件可能是UTF-8编码,尤其是yaml配置文件和包含中文路径的文本文件。PyTorch和OpenCV在读取文件时如果显式指定了编码,就不会触发这个问题;但如果代码里写的是open(path, 'r')这种不带编码参数的方式,Python就会尝试用系统默认编码解析,UTF-8的中文字符在gbk下解码直接崩溃。

解决:推荐在项目根目录写一个sitecustomize.py,或者直接在所有open调用里强制指定编码。更省事的方式是在入口文件顶部设置环境变量。

import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') # 所有涉及open()的地方都显式传encoding='utf-8' with open('data/coco.yaml', 'r', encoding='utf-8') as f: config = yaml.safe_load(f)

从那以后我每次配置新环境都先加这一段,这个坑几乎成了Windows下跑PyTorch项目的必经之路。

5.2 matplotlib.use('Agg') 报错:无显示环境下的后端切换

现象:在Linux服务器上跑代码时,一执行到plt.show()就直接报Matplotlib is currently using agg, which is a non-GUI backend或者干脆闪退,图片没显示出来。

原因:matplotlib默认的后端聚合器Agg是专门用于生成图片文件的,它不依赖任何图形界面。在Linux服务器或通过SSH远程连接的环境里,没有X Window显示服务,任何绘图操作都无法渲染到屏幕,但写文件是可以的。

解决:代码里已经写了matplotlib.use('Agg')这行,说明原作者已经考虑到无显示环境,你要做的是确认这行代码在导入matplotlib之后、调用任何绘图函数之前执行,并且所有可视化结果通过savefig保存而不是plt.show展示。

import matplotlib matplotlib.use('Agg') # 强制使用无GUI后端,只用于写文件 import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) plt.imshow(cv2.cvtColor(vis_img, cv2.COLOR_BGR2RGB)) plt.axis('off') plt.savefig('output/result_plt.png', dpi=150, bbox_inches='tight') plt.close()

一定要记得plt.close()释放当前图形对象,否则循环里连续绘图会导致内存持续增长,最后程序直接卡死。bboi_inches='tight'会让保存的图片自动裁剪掉多余白边,输出结果更干净。

5.3 AttributeError: 'Upsample' object has no attribute 'recompute_scale_factor'

现象:加载某个PyTorch老版本训练的权重时,前向推理跑到Upsample层就报AttributeError: 'Upsample' object has no attribute 'recompute_scale_factor',通常是新旧版本兼容问题。

原因:PyTorch 1.6到1.9之间的Upsample实现里有一个recompute_scale_factor属性,用来控制是否重新计算缩放因子。到了PyTorch 1.10之后,这个属性被移除了,但老权重文件里的state_dict可能还保留着旧层的状态键,加载时新旧结构对不上。

解决:最直接的办法是把PyTorch降级到权重对应的版本。但如果你的环境里其他项目依赖新版本torch,不方便降级,可以给Upsample类重新挂上这个属性作为兼容层,或者用torch.serialization.add_safe_globals把旧版Upsample标记为兼容。遇到这种问题,我一般先检查torchvision和torch的版本配对,大概率是版本错位,而不是代码逻辑问题。

5.4 YOLOv5训练警告:functional.py相关提示

现象:训练时终端频繁打出和functional.py相关的Warning,例如UserWarning: torch.meshgrid: in an upcoming release或者torch.broadcast_tensors相关提示。程序还能继续跑,但日志刷屏严重,影响看其他信息。

原因:YOLOv5官方代码为了兼容不同PyTorch版本做了很多版本适配,老版本代码在新版本torch下会触发废弃API警告。本质上不影响推理结果,但说明你的torch版本比代码预期的要新,部分API行为已经变化。

解决:不追求消除所有警告的话可以直接忽略。但如果你想清理日志,可以升级YOLOv5代码到最新release版本,或者单独消除这类警告。

import warnings warnings.filterwarnings('ignore', category=UserWarning, module='torch') # 在import torch之后、训练代码之前加上这行

这条属于典型的「知道它无害但很烦」的报错,升级依赖通常能根治,但有风险,毕竟工程文件里的其他代码可能是基于旧API写法写的。

5.5 路径问题:明明文件存在却报FileNotFoundError

现象:权重文件确实放在了指定目录下,但程序运行时依然报找不到文件。在Windows上尤其常见。

原因:最常见的场景是代码和文件在同一个目录,但工作目录不是项目根目录。Python的相对路径是相对于当前工作目录解析的,而不是相对于代码文件所在目录。在IDE里运行和命令行运行时,工作目录可能不一样。

解决:不要依赖相对路径,在入口代码段强制把工作目录切换到项目根目录。

import os import sys ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) os.chdir(ROOT_DIR) # 切换工作目录到项目根目录 sys.path.insert(0, ROOT_DIR) # 确保根目录在模块搜索路径中

这段放在整个程序最前面,路径问题基本能一次性消除。还有个隐蔽场景:yaml里配置的路径带空格或中文,OpenCV读取时会解析失败,把这些文件统一重命名为纯英文路径是最省心的处理方式。

6. 实时摄像头检测:跳帧策略与跨场景迁移的工程习惯

摄像头实时推理和静态图片推理在代码层面只有几行差异,但工程难度不在逻辑上,而在性能保障上。直接用同一个检测函数逐帧处理视频流,大概率FPS不到10,骨架图一卡一卡的,完全没法用。我一般会从三个方向做优化。

第一,跳帧策略。人体姿态在相邻两帧之间的变化很小,关键点坐标不会因为人没动就跑到另一个位置去。检测和关键点回归每处理一帧就足够,中间帧直接用上一帧的关键点结果绘制即可,把宝贵的时间省给真正需要计算的帧。

frame_count = 0 SKIP_FRAMES = 2 # 每2帧做一次完整推理,其余帧复用结果 while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_count % SKIP_FRAMES == 0: boxes = detector.detect(frame, conf_thres=0.4) keypoints = hrnet.predict(frame, boxes) vis_frame = hrnet.draw_skeleton(frame, keypoints) cv2.imshow('Pose Estimation', vis_frame) frame_count += 1 if cv2.waitKey(1) & 0xFF == ord('q'): break

第二,输入尺寸收窄。HRnet默认输入是192×256,实时场景里可以缩减到160×192,精度损失大概一个百分点,但推理时间能降下来不少。代价是这个尺寸下小目标人的关键点会更容易抖动,如果你检测的场景里人离摄像头比较远,建议保持原尺寸不要动。

第三,多线程分离。把视频读取和模型推理放到不同的线程里,避免解码和推理互相等待。实际操作上,我会把cap.read()放进一个单独线程里预读下一帧,主线程只做推理和绘制。这个优化在摄像头帧率较低时效果明显,视频文件的提升反而不大。

最后提醒一下:换了一台机器或者换了一个摄像头之后,不要只改路径又重新跑一遍,最好先用手头现有的测试图片验证模型权重加载没问题,再切到摄像头实时模式。我原来经常跳着步骤来,结果发现换了摄像头之后画面偏色导致检测框大量误检,排查了大半天才意识到是白平衡参数没调,从那以后我每次换场景都会强制先做一轮静态图回归测试再上实时,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询