简介:一套结合OpenPose与YOLOv5的人体姿态识别完整项目,面向具备计算机视觉与机器学习基础的开发者、研究者,用于解决多人关键点检测、实时姿态估计及工程化部署等实际问题。压缩包共716个文件、约85.59MB,内部以C++头文件/源文件(hpp、cpp)、Visual Studio工程配置(sln、vcxproj、filters)及CMake构建脚本为主体,同时包含Python脚本、Markdown说明文档、PDF资料以及jpg/png/mp4示例与演示文件,文件类型丰富,源码、构建配置、说明文档与示例素材均有覆盖。项目将OpenPose的多人二维关键点检测能力与YOLOv5的实时检测优势结合,源码覆盖数据预处理、网络训练、后处理、模型评估等环节,同时提供数据标注、可视化及性能评估工具,并在文档中给出安装配置指南与运行说明,方便用户复现和二次开发。目前已累计382人学习,适合希望深入理解姿态识别内部机制,并在运动分析、交互设计、安全监控等场景落地算法的实践者。
1. 姿态识别不是关键点检测:为什么YOLOv5+OpenPose的先后顺序决定成败
“姿态识别”这个标题很容易让人误以为就是把人框出来、把骨架点画上去。真正落地时恰恰相反:YOLOv5负责回答“画面里人在哪”,OpenPose负责回答“这个人的每个关节在哪个像素”,而标题里的“人体姿态识别算法”还要再往上一层,把关节坐标变成“站、坐、蹲、抬手、跌倒”这些具体语义。把这三步拆开,是因为只靠OpenPose在背景复杂、人多的场景里会把两个人的骨架连在一起;先让YOLOv5把人框出来再做关键点,精度和速度都能明显提升。这套方案适合做安防跌倒检测、健身动作计数、人机交互的工程同学,也是从“能跑通”到“能交付”之间性价比很高的一条路线。
2. YOLOv5与OpenPose怎么分工:检测框的质量决定了关键点上限
2.1 为什么把YOLOv5放在OpenPose前面:先框人再取点
OpenPose自带的人体检测器在干净、人少的图里够用,一旦进了监控、商场、公交站这类背景杂物多、行人重叠的场景,漏检和误检就一起冒出来。漏检意味着后面关键点直接没有输入,误检则意味着PAF阶段会去组合一个根本不存在的人体结构。把YOLOv5放在最前面,本质上是用一个更容易训练、更容易换数据集的检测网络先解决“人是什么”,再让OpenPose专注于脖子、肩膀、肘部这些细粒度定位。
选型上常见取舍是换YOLOv8或RTMDet,但YOLOv5的优势在于生态旧、资料多、转ONNX和TensorRT的踩坑案例全。很多做姿态识别的源码项目里,YOLOv5已经把检测结果整理成了xyxy格式,后面接OpenPose只需要处理一个二维数组。YOLOv5的CSPDarknet骨架在精度与速度之间比较平衡,yolov5s权重在CPU上也能跑到几十毫秒一帧,对“人体检测”这种单一类别任务绰绰有余。
这里有个容易被忽略的点:YOLOv5给出的person框往往紧贴人体边缘。直接拿这个框去裁图,手肘、膝盖、脚踝很容易被切断,OpenPose在这些关节上的响应会直接归零。所以裁图时一定要做外扩(padding),外扩比例一般取框宽高的20%到30%。这个细节看着小,却是后面大量“关键点漂移”问题的根源。
2.2 OpenPose输出的是坐标点,不是骨架图:BODY_25结构与PAF
OpenPose推理返回的是一组多维数组。以默认的BODY_25模型为例,每个人的结果包含25个关键点,索引从0到24,分别是nose、neck、左右肩、左右肘、左右腕、mid-hip、左右髋、左右膝、左右踝,再加上眼睛、耳朵和脚趾、脚踝的精细点。每个关键点是一个(x, y, confidence)三元组,x和y是像素坐标,confidence范围是0到1。被遮挡或超出画面时,confidence会明显下降甚至直接是0。
很多第一次用OpenPose的人以为输出就是一张画好骨架的图,这个理解偏差是后面一系列问题的主要来源。姿态识别算法的下游必须从“点”开始,而不是从“图”开始。比如判断一个人是否蹲下,要算的是膝关节角度,这个角度来自髋、膝、踝三个点的坐标,而不是看图上骨架长什么样。同样,低头看手机时鼻子和脖子会重叠,侧身时一边的肩膀和手腕互相遮挡,关键点缺失是常态,不是异常。姿态识别必须从数据结构上接受缺失点,而不是假设每个点都存在。
PAF(部分亲和场)是OpenPose能在多人场景工作的关键。它给每个骨段生成一个方向向量场,网络在预测关键点的同时预测每段骨骼的方向,最后通过图优化把属于同一个人的关键点连起来。这也是为什么OpenPose比“先检测20个关节再组装”的模型更稳。但PAF也带来了参数敏感性:scale_number、scale_gap、part_candidates_threshold、number_people_max这几个参数每次都要细调。多尺度推理对小目标有帮助,但每多一个scale,耗时几乎线性上涨;在固定摄像头场景里,确认人体尺度范围后把scale_number降到1,推理速度提升非常明显。part_candidates_threshold默认在0.2到0.3之间,调太高会丢点,调太低会出现大量杂点,一般我先固定0.25。
2.3 先跑通默认模型再做微调:最省时间的切入方式
不要一上来就训练自定义模型。先用官方权重把链路的每一环跑通,理解输出格式,等关键点坐标和阈值判断都稳定了,再针对缺检场景微调YOLOv5。这样做的原因是OpenPose的骨干网络本身训练成本很高,重新训练关键点模型需要大量标注数据,而大多数项目真正缺的不是关键点精度,而是检测框质量、输入分辨率、阈值策略这三个外围环节。
多人场景还有个策略问题。把YOLOv5检测到的每个person框分别送入OpenPose,会出现同一人的骨架在相邻两个框里重复检测,而且另一个人被部分裁进来时,PAF会把两个人连起来。我的处理办法是:同帧人数大于3时用整帧一次跑OpenPose,不逐框裁;人数少时逐框裁,速度快,关键点也干净。YOLOv5后处理里的conf和iou在大多数场景里分别调到0.25和0.45,是这套方案里比较常用的底线值。
3. 最小可复现链路:用YOLOv5裁出人体,再交给OpenPose提取关键点
3.1 环境准备:把yolov5s权重和OpenPose的Python接口跑通
先准备YOLOv5环境。常见做法是直接clone官方YOLOv5仓库,然后按requirements安装依赖:
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt装好后下载yolov5s.pt权重,然后写推理脚本。这里我习惯用torch.hub方式加载,省去维护本地仓库的麻烦:
import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.conf = 0.25 # 置信度阈值,低于该值的检测框会被丢弃 model.iou = 0.45 # NMS的IoU阈值,控制重叠框的合并力度 model.classes = [0] # 只保留COCO类别0:person results = model(frame, size=640) df = results.pandas().xyxy[0] person_boxes = df[df['name'] == 'person'].reset_index(drop=True)model.classes = [0]是在模型后处理阶段直接过滤类别,比拿完整检测结果再过滤省掉大量无关节点的计算。conf=0.25和iou=0.45是YOLOv5后处理里最常用的超参数组合。如果摄像头装在俯视角度或目标普遍偏小,conf建议先降到0.2,否则漏检会非常严重。
OpenPose部分,源码项目里通常会封装一个OpenPoseDetector类,把模型初始化和关键点提取藏在里面。常见的调用方式是这样的:
from src.openpose import OpenPoseDetector detector = OpenPoseDetector( model_path='models/pose/coco/pose_iter_440000.caffemodel' ) keypoints, _ = detector.run(cropped_img)提醒一下:这不是OpenPose官方仓库自带的通用API,不同项目封装的类路径和初始化参数可能不一样。关键要理解它的输入输出约定:输入一张彩色图,返回每个人的关键点数组,形状通常是(N, 25, 3),N是人数,25是BODY_25,3是x、y、confidence。
3.2 裁图与坐标还原:两个坐标系之间的唯一桥梁
YOLOv5在原图上输出坐标,OpenPose在裁图上输出坐标,中间差了一个offset。很多源码工程里的坑不在模型,而在这一步坐标换算。裁图函数我一般这样写:
def crop_person(frame, box, pad_ratio=0.25, min_side=240): x1, y1, x2, y2 = int(box['xmin']), int(box['ymin']), int(box['xmax']), int(box['ymax']) w, h = x2 - x1, y2 - y1 # 外扩,避免手肘膝盖被裁掉 px, py = int(w * pad_ratio), int(h * pad_ratio) x1 = max(0, x1 - px) y1 = max(0, y1 - py) x2 = min(frame.shape[1], x2 + px) y2 = min(frame.shape[0], y2 + py) # 过小的框直接放弃,关键点精度不够没有意义 if (x2 - x1) < min_side: return None, None return frame[y1:y2, x1:x2], (x1, y1)pad_ratio=0.25是把检测框四周各外扩25%,给OpenPose留出看到完整肢体的空间。min_side=240是个经验值,低于这个像素高度的person框,OpenPose算出的关节坐标误差会大到没有实用价值,不如直接标记low_resolution。
拿到裁图后:
cropped, offset = crop_person(frame, person_boxes.iloc[0]) if cropped is None: continue kps, _ = detector.run(cropped) # 关键点坐标还原到原图 kps[:, 0] += offset[0] kps[:, 1] += offset[1]这里只加offset,confidence列不用动。容易犯的错是把offset加了两遍,或者忘了OpenPose内部对输入图做了resize。如果OpenPose返回的坐标是网络输入尺寸下的坐标,还需要先按缩放比例还原:
scale_x = cropped.shape[1] / net_input_width scale_y = cropped.shape[0] / net_input_height kps[:, 0] *= scale_x kps[:, 1] *= scale_y具体要不要乘,取决于项目源码里封装时有没有处理过。稳妥的做法是先用一张已知坐标的图做一次端到端验证,确认坐标落到原图的正确位置后再写业务逻辑。
3.3 相邻帧漏检兜底:用上一帧的检测框顶一下
视频流里YOLOv5偶尔会漏掉一帧,如果漏检发生在关键动作切换的瞬间,下游状态机就会丢掉一次状态变化。最轻量的兜底方案是保留上一帧的person box:
last_box = None for frame in video_frames: results = model(frame, size=640) df = results.pandas().xyxy[0] person_boxes = df[df['name'] == 'person'].reset_index(drop=True) if len(person_boxes) == 0 and last_box is not None: person_boxes = last_box.copy() if len(person_boxes) > 0: last_box = person_boxes.copy()这个做法只适合目标没有大幅移动的场景。如果行人快速横穿画面,上一帧的框和当前帧位置偏差过大,OpenPose会在一个空区域里瞎算,反而产生更离谱的骨架点。我一般只在确认目标静止或缓慢移动的场合用它,比如坐姿识别、工位行为分析。这是典型的“没有银弹”的工程取舍。
4. 姿态识别算法实装:从关键点坐标到“站/蹲/坐”的可解释规则管线
4.1 三类不依赖绝对坐标的几何特征:角度、比例、偏移
拿到关键点坐标后,第一步不是直接判断动作,而是先把坐标转换成不受画面位置影响的特征。理由很简单:同一个站立姿势,站在画面左边和右边,x坐标完全不同;摄像头有俯仰角时,同样的身高在画面里的像素高度也不一样。关节角度、比例、偏移这三类特征对这些问题天然鲁棒。
以BODY_25为例,先定义索引映射:
JOINT = { 'nose': 0, 'neck': 1, 'RShoulder': 2, 'RElbow': 3, 'RWrist': 4, 'LShoulder': 5, 'LElbow': 6, 'LWrist': 7, 'MidHip': 8, 'RHip': 9, 'RKnee': 10, 'RAnkle': 11, 'LHip': 12, 'LKnee': 13, 'LAnkle': 14, 'REye': 15, 'LEye': 16, 'REar': 17, 'LEar': 18, }然后写一个通用的三点夹角函数,用来算肘关节角、膝关节角、肩髋连线角度:
def calc_angle(a, b, c): """从三点坐标计算角度,b点是角点""" a, b, c = map(np.array, (a, b, c)) v1, v2 = a - b, c - b cos = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6) return np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) # 右膝关节角:髋-膝-踝 knee_angle = calc_angle(kps[JOINT['RHip']], kps[JOINT['RKnee']], kps[JOINT['RAnkle']])1e-6加在分母上,防止两个向量长度为零时除零。膝盖角在站立时接近170到180度,蹲下时小于90度,坐下时通常在80到110度,是一个非常直接的特征。
比例特征用髋部相对高度。把髋部到脚踝的像素距离除以脖子到脚踝的像素距离,得到一个基本不受人物远近影响的比例:
body_height = np.linalg.norm(kps[JOINT['neck']] - kps[JOINT['RAnkle']]) hip_height = np.linalg.norm(kps[JOINT['MidHip']] - kps[JOINT['RAnkle']]) hip_ratio = hip_height / (body_height + 1e-6)这个hip_ratio在站立时大约0.55到0.65,坐下或蹲下时会明显低于0.5,是区分“站着”和“坐着”的一个强特征。偏移特征我常用的是肩中点和髋中点的水平位移差,用来判断弯腰方向。
4.2 规则阈值不是拍脑袋:用标注样本观察,而不是凭感觉设
很多人拿到源码第一件事就是改阈值,改来改去发现这个场景能用,换个摄像头又废了。我的经验是:先采集当前摄像头视角下30到50帧已知动作的样本,把关键点坐标打印出来,观察角度和比例的分布,再定阈值。比如一个斜上方45度的监控视角,站立时膝关节角可能是160度,坐下时可能只有70度,和侧面平视场景的数据差别很大。
一个比较常见的初始阈值表是这样:
| 动作 | 膝关节角度 | 髋高比 | 补充条件 |
|---|---|---|---|
| 站立 | >150度 | >0.55 | 肩髋偏移小 |
| 坐下 | 80~110度 | 0.35~0.50 | 髋部明显低于站立时 |
| 蹲下 | <85度 | 0.25~0.40 | 膝盖超过脚尖水平位置 |
这张表只能作为起点。固定摄像头场景里,真正要做的是把阈值写成配置文件,用一个标注脚本批量验证,而不是在代码里硬编码。我见过太多项目源码里阈值散落在各个函数里,改一个数要全局搜索,这就是后续维护最大的坑。
4.3 置信度过滤与缺失点处理:不硬补缺失,而是进入过渡态
OpenPose给出的每个关键点都有confidence,这个值本身就是一个信号。我会把低于0.3的关键点直接视为缺失:
conf = kps[:, 2] if conf[JOINT['RKnee']] < 0.3 or conf[JOINT['RAnkle']] < 0.3: state = 'transition' else: knee_angle = calc_angle(kps[JOINT['RHip']], kps[JOINT['RKnee']], kps[JOINT['RAnkle']]) state = classify_pose(knee_angle, hip_ratio)这里的关键点是:缺失时不要用上一帧的数据去插值硬补,而是给一个transition过渡状态。原因很简单,动作切换的中间几帧骨架本身就是在变化的,插值补出来的数据会把“站起来”和“坐下去”中间那段过渡变成一段假的稳定状态,导致状态机切换延迟。过渡态会让整个逻辑更接近真实的人的物理动作。
分类函数classify_pose里规则本身不复杂,复杂的是让规则在连续帧上保持稳定。所以我会再加一个滑动窗口:最近5帧里同一状态出现3次以上才真正切换。这个“先滤波,再判定”的顺序,比单纯调角度阈值有效得多。
5. 姿态识别项目最容易翻车的五个现场:现象、原因与排查方案
5.1 关键点大量为0或乱飘:YOLOv5的框把人“裁骨折”了
现象:OpenPose只在胸口附近散着三四个点,手腕、膝盖完全不出现,或者时好时坏。
原因:YOLOv5给出的person框太贴合身体,外扩比例不够,OpenPose根本看不到被裁掉的肢体。这是最普遍的问题,和模型好坏没有关系。
解决:把裁图函数的pad_ratio从0.1调到0.25到0.3,再检查一下框边缘。我的习惯是把裁出来的图直接保存成调试图像,肉眼确认关节都在画面内。另外保证ROI长边不低于240像素,过小的目标对OpenPose没有意义。
5.2 OpenPose一帧要跑两秒:没有利用检测框裁剪输入
现象:视频离线分析要跑一晚上,摄像头实时流完全没法用。
原因:OpenPose的VGG前端在全图推理时计算量极大。直接把1080p整帧传进去,网络要扫描整张图找所有可能的人,耗时自然爆炸。
解决:先YOLOv5裁图,再把ROI的长边限制在400像素以内。这是整套方案里收益最大的一次改动,速度能提升一个数量级。如果还要更快,换OpenPose的轻量化版本或转ONNX,边缘设备上还要做量化,但那是另一条路。
5.3 站/坐/蹲分类在临界点疯狂跳变:阈值判定太“脆”了
现象:同一动作在不同帧里来回跳变,视频看着像抽搐。
原因:关键点坐标本身有1到2像素的抖动,算出来的角度在阈值边界附近来回震荡,单帧判定必然不稳定。
解决:用滑动窗口中位数加状态机切换。连续三帧判定为同一状态才真正切换,而不是每帧都更新。这是这类项目里最常见的玄学问题,调阈值调半天,最后发现问题是出在缺少时序平滑。
5.4 画面里明明有人但整套链路没反应:YOLOv5漏检引发连锁反应
现象:人站在画面里,但姿态识别结果一直是空。
原因:YOLOv5本身没检测到人,后面所有逻辑全部空转。常见诱因是目标太小、遮挡严重、逆光,或者conf阈值设得过高。
解决:先用results.pandas()打印置信度看具体数值,如果普遍低于0.4,说明模型对这个视角本身就不自信。把model.conf降到0.2,或者换yolov5m、yolov5x权重。如果是俯视、低照度这类特殊视角,光调阈值已经不够了,需要收集自己的数据微调YOLOv5,这一步属于“yolov5训练自己的数据集”范畴,第6章会讲训练参数。
5.5 骨架画出来歪得离谱:坐标系还原少乘了一次缩放或漏了offset
现象:关键点整体偏移几十像素,或者骨架出现“X形”错位。
原因:OpenPose内部对输入图做了resize,输出坐标没有恢复到裁图尺寸;或者裁图offset在加回原图时用错了坐标。
解决:把坐标还原拆成两步——先乘resize比例,再加offset。每一步用可视化单独验证,正确结果是把关键点画在原图上时,骨架和人体轮廓应该严丝合缝。不要图省事把两个操作合并成一个公式,否则出问题很难定位。
6. 进阶:用OKS误差评估替代“肉眼调参”,把自采数据训进YOLOv5
6.1 用OKS评估关键点质量:先把OpenPose的上限测出来
调参调得再细,不如先知道自己手里这套模型的上限。关键点检测的评估指标不是IoU,而是OKS(Object Keypoint Similarity)。它会把预测点和标注点的距离按人体尺度归一化,距离越近得分越高。做法是抽20张代表性图像,用标注工具标出关键点真值,再跑一遍YOLOv5+OpenPose链路,逐点计算OKS。如果某个关节的OKS均值低于0.7,说明OpenPose在这个关节上的输出本身就不可靠,后面不管怎么调规则阈值都白搭。
6.2 针对场景微调YOLOv5:俯视与低照度场景的补救手段
如果OKS评估发现问题出在检测漏检,而不是关键点本身,就需要用自己的数据微调YOLOv5。常见训练命令长这样:
python train.py --img 640 --batch 16 --epochs 50 \ --data custom.yaml --weights yolov5s.pt \ --hyp data/hyps/hyp.scratch-low.yamlcustom.yaml里包含训练集和验证集路径以及类别数,这里类别只有person一个。img 640和batch 16是兼顾显存和精度的常见选择。hyp.scratch-low.yaml里的学习率、anchor等超参数先不动,等第一轮训练完看验证集mAP,再针对性调lr0和anchor。微调YOLOv5的价值不在让检测更准,而在于减少漏检,这才是和OpenPose配合时真正需要补的那块短板。
6.3 让我少走冤枉路的一个习惯
我自己最早在这个方案上翻车,就是把OpenPose当成一个“什么图都能应付”的黑匣子,结果检测框、输入分辨率、置信度阈值三层有一层没控住,下游就整体崩坏。后来养成的习惯是:每次改一个阈值之前,先问自己一句“这个阈值对应的现象是什么”,能用OKS和可视化数据说明,就不靠感觉硬调。把坐标系验证和OKS评估做成脚本放在项目里,比任何参数经验都管用。希望帮到你。
本文还有配套的精品资源,点击获取