姿态估计这两年其实已经进入“柴米油盐”阶段了,大部分做视觉的工程师手里都有现成的人体关键点方案,但回到那篇让多人实时2D姿态估计真正落地的工作,几乎所有人绕不开同一个名字:OpenPose。我最早接触它的时候,脑子里全是问号——四肢交叉、人物重叠、人数不固定,一张下班高峰期的地铁照片里,网络凭什么分得清谁是谁的胳膊,谁是谁的腿?这篇笔记是我重读《Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields》之后的完整梳理,重点拆解PAF(Part Affinity Fields,部位亲和场)到底做了什么,为什么它能解决“关键点关联”这个核心难题。适合刚入门姿态估计、准备复现论文、或者已经在用OpenPose但没细读原理解的人。
读这篇论文之前,我建议先把心态调整一下:它并没有用特别复杂的数学,也没有堆太多花哨模块,真正难的是理解“为什么要这么设计”。一旦理解了PAF存在的意义,后边的网络结构、训练细节、工程优化全部水到渠成。
1. 整体设计思路拆解
1.1 为什么多人姿态估计这么难
先从一个单人姿态估计说起。单人场景下,模型只需要回归出一个人身上所有关键点的位置。一张图里只有一个人,不存在“关键点到底属于谁”的问题,所有检测出的点直接连起来就是骨架。很多早期的优秀工作,比如堆叠沙漏网络,都是在这个前提下做的,效果也相当不错。
但现实场景里几乎不会有“一个人站在纯色背景前朝你张开双臂”这种配合度。真实的摄像头视角下,要么是一群人在走动,要么是几个人挤在一起交谈,甚至一个人被另一个人挡住半边身体。此时姿态估计就变成两件事:第一,把所有人身上的关键点找出来;第二,把属于同一个人的关键点正确连起来。
第二件事的难度被很多人低估了。两个离得很近的人,各自的手肘和手腕在像素上可能贴在一起;一个人双手交叉抱在胸前时,左肘和右腕的距离,看起来比左肘和左腕的距离还近。如果只依赖关键点的空间坐标做聚类,这种歧义几乎无法消除。OpenPose这篇论文要解决的核心问题,就是这种“关联歧义”。
1.2 Top-Down与Bottom-Up:两条路线的博弈
在OpenPose之前,多人姿态估计大致有两种处理思路。
一种是Top-Down(自上而下):先用目标检测模型把画面里的每个人框出来,再对每个框分别做单人姿态估计。优势是每个关键点都明确归属于框内的这个人,不会搞混;劣势也很明显,推理时间随人数增加线性上升。十个人的画面,就要跑十次单人姿态估计器,实时性基本无从谈起。而且一旦人挨得太近,检测框互相重叠,Top-Down方法会陷入“先有鸡还是先有蛋”的困境——框不好,姿态一定不好;姿态不好,框的质量也无从提升。
另一种就是Bottom-Up(自下而上):先从整张图里一次性检测出所有关键点,再把关键点聚合成个体骨架。它的计算量基本不随人数线性增长,实时性好;难点在于分组。OpenPose选择了Bottom-Up路线,并且用PAF把“分组”这个抽象问题转化成了可学习的向量场估计问题。
| 对比维度 | Top-Down方法 | Bottom-Up方法(OpenPose) |
|---|---|---|
| 处理顺序 | 先检测人,再检测关键点 | 先检测关键点,再关联成骨架 |
| 推理速度 | 随人数线性下降 | 基本恒定,适合实时场景 |
| 密集场景 | 检测框相互干扰严重 | 对重叠更鲁棒 |
| 核心难点 | 检测框质量 | 关键点关联匹配 |
1.3 为什么单单靠关键点位置不够
假设现在图里有两个人的左肘和左腕,四个候选点已经检测出来了。问题是,哪两个点应该连成一条左臂?
最直觉的做法是,计算左肘到两个左腕的距离,选更近的那个。看起来很有道理,但一旦两人站在一起,左腕A离左肘B的距离,可能比左腕B离左肘B更近。距离最近原则必然连错。
另一些方法会试着用“肢体外观”做匹配,比如比较两处关键点周围的小图像块颜色、纹理是否一致。这听着也合理,万一两人穿着相同颜色的衣服,整条逻辑直接失效。而且图像块只能描述局部,无法表达肢体空间上的连贯性。
PAF的突破口就在于:它不仅仅告诉你“这个位置是什么”,还告诉你“这个位置上的肢体朝哪个方向延伸”。沿着某条真实的左臂走,路径上每个像素都提供一个指向手腕的方向信息;如果这条路径确实连接了同一个人,那么沿路径收集到的方向信息应该高度一致。这样,“两个关键点是否属于同一个人”就变成了“两点之间的连线与路径上每个位置的向量方向是否匹配”。匹配问题变成了积分问题,连续且可导,自然可以放进网络里学习。
这就好比你去快递柜取件,只知道柜子里放了一堆包裹(关键点位置)远远不够。真正有效的办法是看包裹上的快递面单(方向向量),上面写着它应该被送到哪个地址。只要面单相互匹配,包裹自然就归到一起。
2. 核心细节解析与实操要点
2.1 置信图:怎么告诉网络关键点在哪
OpenPose的网络输出分两条分支:一条负责预测置信图(Confidence Maps),另一条负责预测PAF。先看置信图。
对每个关键点类别(鼻子、左肘、右腕等),网络会输出一张二维热图,每个像素的数值表示该位置存在该类型关键点的置信度。训练时,Ground Truth置信图是在每个真实关键点位置生成一个高斯峰。高斯峰不是只把标注点那个单一像素设为1,而是让周围区域也有一定响应,这样可以容忍标注误差,也给网络的回归一个平滑的优化目标。
比较关键的一个细节是多人场景下如何融合不同人的高斯峰。如果两个人都被标注了左肘,且位置比较接近,生成真值热图时应该取两个高斯的元素级最大值,而不是平均。平均会把两个峰变成一个相对模糊、峰值下降的“双峰混合体”,导致训练目标不明确;取最大值则保留了两个峰值信号,网络需要学习预测出两个响应高峰,对应两个人的左肘。这个细节我第一次读的时候直接跳过了,后来在生成训练数据时发现模型在人靠近的场景下热图响应总是糊成一片,回来看论文才发现自己把max操作错写成了average。
置信图的分辨率通常远低于输入图像。记输入图如果被缩放成368x368,那么输出的热图大概是46x46左右;后续再从低分辨率热图上采样回原图做关键点坐标提取。低分辨率会牺牲一些定位精度,但换来的是计算量的极大节省,属于实时性上的必要妥协。
2.2 PAF的地面真值到底是怎么生成的
PAF的名字里带“Affinity(亲和)”,因为它表达的是两个关键点之间的亲和关系。每一种肢体(左臂、右腿、躯干等)对应一组PAF通道,每个通道里存的是一个二维向量场。
训练真值生成方式非常巧妙。对标注好的某一个具体人物的肢体,比如从脖子NEck到左髋LHip,假设这个人的这两个关键点坐标分别是N和H。对于画面中任意一个像素p,先计算它在N到H这条线段上的投影位置。如果投影落在线段范围内,且p到线段的垂直距离小于某个阈值(简单理解为肢体半宽度),就认为p属于这个人的躯干区域。此时p位置上的真值PAF向量就是N指向H的单位方向向量;如果p不在任何人的肢体区域内,则向量为零向量。
对于有多个人重叠覆盖同一个像素的情况,PAF真值是所有覆盖该像素的人的向量的平均值。为什么要平均而不是像置信图那样取最大?我自己理解是这样的:PAF后面要用来做关键点匹配,它关心的是“从某个关键点出发,沿路径走,方向是否一致”。如果位置被多人覆盖,取平均方向能够同时保留两种可能的连接方向,让匹配阶段有机会把不同的人区分开;取最大值在数学上反而不容易定义“哪个最大”,因为向量是二维的,没有一个天然的标量排序。
这个阈值决定了肢体覆盖区域有多宽。太宽,PAF会侵占相邻肢体的区域,两个肢体方向互相干扰;太窄,匹配时沿线段采样的点太少,积分的统计稳定性变差。论文里的选取依实验而定,我复现时习惯把肢体宽度设成身高的约0.02到0.03倍,整体影响不是特别敏感,但不能极端取值。
2.3 网络结构,为什么要多个Stage迭代
OpenPose的网络骨架分成三段。前端先用VGG的前几层卷积提取图像特征,得到一个共享的高层特征表示。这个前端给后续的多阶段推理打底,相当于给整幅图做了一次通用理解,把大量底层纹理信息转化成更高层级的语义特征。
之后是核心的迭代阶段。每个阶段CNN同时吃三路输入:VGG输出的共享特征、上一阶段输出的置信图、上一阶段输出的PAF。然后内部再分出两个分支,一个分支继续预测置信图,一个分支继续预测PAF。这样的循环阶段在论文里设置了多个,数量增加会提高精度,但计算量也线性增长。最初论文中训练时用了较多阶段,后续工程版为了实时性有所裁剪,这也是大家在源码里看到阶段数可能对不上的原因。
为什么要迭代,而不是一次性输出最终结果?我的理解是,肢体关联本质上有很强的空间上下文依赖。拿到一个局部区域的PAF,你可能还确定不了左右手臂怎么配对,需要把视野放大到躯干甚至整个人,才敢下结论。多阶段叠加让网络通过重复处理逐步扩大有效感知范围,实现了从局部细节到全局语义的“渐进式确认”。这个过程有点像人眼认人,先看到一只手,不放心,再看肩膀和脸,确认角度和尺度,最后才决定手和肩膀属于谁。一次性卷积输出容易在模糊区域草率判断,多阶段输出给了网络反悔、修正的机会。
2.4 损失函数的几个坑
每个阶段的两个分支都会和真值计算L2损失,然后所有阶段的损失求和。这里的关键词是“所有阶段都算Loss”。如果不加中间监督,只对最后一个阶段算损失,前面阶段梯度经过多次循环反向传播之后可能衰减,训练初期模型会学得非常慢。中间监督强制前面每一个阶段都输出当前最合理的置信图和PAF,梯度路径变短,训练便稳定了很多。
Ground Truth里还会带一个Mask。很多公开数据集的标注并不是完全稠密的,有些人的某些关键点可能被遮挡或未标注。这些位置如果硬算损失,会给网络传递错误信号。所以未标注区域对应的热图和PAF的损失权重直接置零。我见过有同学复现时把这层mask漏了,结果训练出来的模型在遮挡区域疯狂“脑补”出根本不存在的关键点,原因就是没标注区域被当成了“该区域没有关键点”的负样本。
损失函数本身没什么花哨的,就是朴素的L2距离。但它的梯度在预测靠近0或1的时候依然保持稳定,配合多阶段监督,在工程上实现起来非常简单,这也是OpenPose能够在各种平台被快速复现的原因之一。
3. 实操过程与核心环节实现
3.1 从头生成训练数据的完整步骤
复现OpenPose,找个现成预训练模型跑demo其实不难,真正实用是把PAF的Ground Truth数据管线搭对。我建议顺序是:先准备骨架定义和关键点标注,再去实现置信图和PAF的生成,最后接入网络训练。
骨架定义是一切的基础。以常见的关键点格式为例,基本的关节列表包括鼻子、双眼、双耳、双肩、双肘、双腕、双胯、双膝、双踝。需要用肢体连接关系定义所谓的“骨架树”。生成PAF真值时,只需要遍历每个肢体;生成置信图时,只需要遍历每个关键点类型。很多代码错误都出在肢体列表和关键点编号对不上,比如左腿连了右胯,模型训练再久也学不出合理的腿部PAF。
关键点标注通常来自公开数据集,格式一般是每个目标的关节点坐标与可见性标记。读取标注后,第一步要做的是统一坐标:原图和网络输入分辨率之间有个缩放比例,真值坐标必须同步缩放,否则热图的峰位置和PAF的肢体端点全偏了。这个小坑我曾经在一个新数据集上踩过,训练Loss能降到很低,但预测关节点总是有恒定偏移,排查了两天才发现是坐标缩放时丢了一个浮点精度。
下一步生成置信图。对每个人的每个可见关键点,在对应的通道上画一个二维高斯。坐标越界、高斯被裁剪的情况要单独处理,否则训练数据边界区域会出现半个高斯峰,影响边缘人物的检测。
再生成PAF。写一个“点到线段”的函数,计算像素位置到肢体线段的垂直距离和投影参数。投影在0到1之间且垂直距离小于阈值,就说明这个像素属于当前肢体范围,把方向向量写进对应通道。多人重叠时累加并归一化,注意要除以该像素被覆盖的次数,也就是计算平均向量。
这一整套代码量其实不大,几十行就能跑通,但正确性验证很关键。我的习惯是把生成的置信图和PAF可视化,叠加在原图上,直接肉眼看:热图的峰是否立在关节上,PAF的向量箭头是否顺着肢体方向排开。如果可视化出了问题,训练再好的网络也没用。
3.2 网络训练的工程化配置
网络训练输入分辨率通常设置为固定的宽高比,比如高368。输入图像的宽度可以跟着数据集分布来定,如果拍摄场景偏宽,固定比例会导致大量目标被挤压变形,影响关键点定位精度。
数据增强方面,随机旋转和随机缩放是最有效的两种。旋转角度控制在这个量级,太大容易让肢体出现不自然的弯曲;缩放范围根据目标尺寸分布来确定。随机水平翻转时要注意交换左右关节点标签,比如把左肘和右肘互换。这个处理如果漏掉,模型会对镜像场景产生严重偏见。
优化器一般选择带动量的SGD,学习率采用阶梯式衰减。批次大小受显存限制,多阶段网络的前向和反向占用显存很高,如果显存不够用,可以降低输入分辨率。但输入分辨率降到256以下后,小目标关键点的定位精度会明显下降,所以要平衡。
多阶段带来的另一个工程问题是显存中的中间变量数量很大。每个阶段都会保留置信图和PAF用于后续计算,阶段越多显存开销越大。训练时用完整阶段数如果爆显存,可以先在较少的阶段上跑通整个训练流程,最后再加大阶段数微调,能节省很多调试时间。
3.3 从置信图和PAF到最终骨架,匹配逻辑拆解
推理阶段可以拆成四步。第一步,网络输出置信图和PAF。第二步,从置信图里提取每个关键点类型的候选坐标,通常对热图做非极大值抑制,留下局部峰值点,同时记录每个候选点的置信度。第三步,对所有相邻肢体类型的候选点做匹配,也就是用PAF计算每两个候选点之间是否具备足够强的肢体关联。第四步,把所有匹配成功的肢体按人体骨架图拼起来,输出多人的完整骨架。
第三步匹配的具体做法是这样的:假设现在要匹配“左肘”和“左腕”这两个关键点,左肘的候选点集有E个点,左腕的候选点集有W个点。对每一对组合,连接这两个候选点,并在这条连线上均匀采样若干位置。对每个采样位置,利用PAF场读取该位置对应的二维向量,然后计算这个向量与“从肘到腕的方向向量”的内积。内积值越大,说明PAF在该处指示的方向越倾向于这个连接。将所有采样位置的数值累加平均,得到这对候选点的匹配分。如果匹配分超过某个阈值,就认为该连接成立。
这一步就是PAF论文最体现功力之处。把“两个点应该连一条边”的离散问题,变成了连续向量场上的积分问题。我读第一遍时对这个设计最感叹,因为它把空间方向信息组织成一个可供网络学习和数值计算的结构,让最终的匹配既能利用全局上下文,又能保持实时计算。
最后一步是多人体骨架组装。因为同一类型的肢体在不同的候选组合之间存在竞争关系,比如一个左肘候选可能同时和两个左腕候选都有很高的匹配分,但现实中一个人的左肘只能连一个左腕。OpenPose将匹配建模为二分图最大权重匹配问题,通过匈牙利算法或者简化的贪婪策略求解。
需要注意的是,论文在匹配过程中强调了一个先验:一个关键点只能被一条肢体使用一次,但可以在不同肢体类型中重复出现。比如左肩既参与左臂的构建,也参与躯干的构建,这是允许的。这种逐肢体类型独立匹配的策略虽然严格意义上不是全局最优,但在实测中速度和结果都非常可靠,这也是它会成为大规模工程方案的原因。
3.4 多尺度推理与实时性取舍
论文在实验里提到一个提升精度的手段:多尺度测试。推理时把输入图像缩放到若干个不同的尺寸,分别送入网络,再把各尺度产生的置信图和PAF对齐并做融合。小尺度输入能捕捉整体轮廓,大尺度输入能保留关节细节,两者叠加通常能提升不少精度,尤其对小尺寸人物和动态模糊场景。
但多尺度测试的代价是成倍的计算量。我在实际部署时很少用多尺度,因为实时场景根本等不起。如果追求精度,可以在服务端批量推理时做多尺度;如果是嵌入式设备或者手机端,直接用单尺度,更现实的做法是用轻量级前端替换VGG,把推理帧率从十几帧提到三十帧以上。OpenPose的设计让前端特征提取部分和后端迭代部分解耦,换一个轻量前端并不需要改动PAF阶段,这给工程优化留了很大余地。
影响实时性的另一大因素是输入分辨率。实测下来,输入分辨率从368降到256,单帧耗时能减少将近一半,精度下降幅度在近距离场景里基本可接受,但远距离小目标会明显吃亏。工业项目里经常要跟产品和客户商量这个度,没有绝对最优值,只有场景里最合适的值。
3.5 我的快速复现检查清单
按照我自己的习惯,复现一个论文不是把代码跑起来就算结束,而是设置几个检查节点,层层验证。
第一层:可视化Ground Truth。必须能清晰地看到热图峰值立在关节标注点上,PAF箭头沿着肢体方向整齐排列。第二层:训练前期观察中间监督效果。第一个阶段应当能输出结构相对简单的置信图,后续阶段逐渐细化,如果所有阶段输出基本一样,大概率是网络容量或者特征复用出了问题。第三层:推理时单独检查置信图分支,不接匹配逻辑,人工看关键点检测是否准确。如果这一步已经有大面积漏检,问题一定出在数据或者前端而不是PAF。只有置信图质量达标的条件下,再去调PAF匹配阈值,才有意义。第四层:找一个重叠严重的测试图,比如几个人站在一起伸手拿东西,如果骨架连线正确率高,说明PAF确实学到了关联信息。
这套检查清单帮我在自定义数据集上快速定位过好几个问题,也推荐给想真正吃透OpenPose的人。
4. 常见问题与排查技巧实录
很多读者复现OpenPose之后跟我反馈,问题集中在一张表里。我把自己遇到过的现象、原因和排查方向整理成了下面的排查表,供参考。
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 训练Loss收敛后关键点依旧偏移 | Ground Truth坐标缩放精度丢失,或高斯生成时坐标未取整 | 可视化训练数据的置信图叠加原图 |
| 遮挡区域出现凭空关键点 | 训练mask未正确应用,未标注区域被当负样本 | 检查损失计算里的mask维度是否与关键点通道对应 |
| 多人靠近时骨架连线交叉错乱 | PAF训练真值里肢体宽度阈值过大,向量区域互相污染 | 可视化PAF,缩小肢体宽度阈值 |
| 检测出的关键点抖动剧烈 | 置信图阈值设置过低,把噪声峰也当成了候选点;或者NMS窗口过小 | 调高阈值,检查NMS窗口大小 |
| 小目标人物关键点全部漏检 | 输入分辨率太低或数据增强里缺少尺度变化 | 扩大随机缩放范围,或采用多尺度推理 |
| 多人匹配时同一人体被拆成好几段 | 二分图匹配阈值过高,部分肢体的匹配分不达标 | 降低匹配阈值,观察各肢体类型的匹配分分布 |
| 曲线连线方向明显反了 | 骨架定义里肢体端点的起点和终点搞反,PAF训练真值方向错掉 | 强行可视化一对肢体,检查箭头方向 |
4.1 PAF阈值到底怎么调
PAF推断阶段有两个核心阈值:一个是匹配分阈值,若两个候选点间的积分分数低于该值,则拒绝连接;另一个是关键点阈值,热图峰值低于该值时不会被选为候选点。
匹配分阈值的调整依赖数据分布。照片里的人动作幅度越大、肢体交缠越多,阈值就越要放宽,否则大量真实的肢体连接会因为局部方向不一致而匹配不上。但阈值太低会把不同人的肢体误连起来。我通常的调整思路是:先用一个较宽松的阈值把所有可能的连接全部保留,查看输出骨架中有多少是明显错误连接的,再逐步提高阈值直到错误率下降到一个可接受范围。论文里推荐的默认值可以作为起点,但工程落地时一定要针对自己的场景重新标定。
4.2 为什么PAF梯度正常但匹配还是经常错
如果在训练过程中确认置信图和PAF的可视化结果都很合理,但推理时还是出现连线错误,大概率是匹配环节的细节没对齐。
一个容易忽略的点是采样点个数和采样方式。两个候选点距离较远时,如果只在连线上均匀采几个点,PAF向量在中间某个局部异常值可能会直接扭曲积分结果。我建议适当增加采样点数,至少几十个以上,让积分结果更平滑。另一个点是对齐方向:计算PAF与“源点到目标点”方向的内积时,必须保证PAF的通道顺序和肢体骨架定义的端点顺序一致。如果网络输出的PAF通道是“左肘到左腕”的方向,而匹配代码里读成了“左腕到左肘”的方向,内积就变成负值,匹配必然失败。
4.3 自顶向下和自底向上能结合吗
这个问题很多人问过我。OpenPose的自底向上路线在速度和基建上胜出,但论单人的精确关键点定位,通常不如精心设计的自顶向下方法。工业中一种常见做法是两者结合:用轻量目标检测去粗筛候选区域,在区域内用单人姿态估计模型得到高精度结果,再用OpenPose级别的PAF做全局去重和互相校正。当然这样会牺牲一部分帧率,属于精度优先项目的折中方案。
论文笔记的价值在这里就体现出来了。只读结论不看上下文的人,容易把OpenPose当作一个万能方案,实际上它最擅长的是多人、实时、中等精度这三者的平衡点。理解了这个,你才能在业务里做出正确的技术选型。
4.4 再谈一个数据细节:重叠人物的PAF精度
最后分享一个我在自定义数据集上反复踩过的坑。当画面中有两个几乎重叠的人时,Ground Truth PAF在重叠区域会被平均。考虑这样一种情况:两个人的手臂方向完全相反,重叠区域的平均向量几乎为零向量。训练时网络会学到“这里没有肢体方向”,但推理时恰恰需要在这里区分出两条相反的手臂,这就达不到效果。
当时我提升重叠场景精度的手段有两个。第一个是训练时大幅度增加重叠人物的样本比例,让网络见到足够多“重叠但方向相反”的例子,强行把它学出来。第二个是把PAF分支的网络宽度加大一点,增大分支自身的容量。实测这两个调整能把重叠场景的错误连接率降低不少,但始终做不到完全消除。这也是为什么后来的一些姿态估计工作会引入可学习的分组机制或者Transformer做全局推理。对初学者来说,意识到PAF的局限性同样重要,它能帮你判断什么时候够用,什么时候需要拜托其他方案。
我的实际使用体会
跑通OpenPose的demo其实并没有多难,但真正把PAF理解透之后,我才意识到它最了不起的地方不在于网络结构,而在于把“关键点关联”这样一个离散排序问题巧妙地转译成了连续向量场上的积分问题。直到现在,我遇到多人姿态估计任务时,第一反应仍然是先想想能不能用向量场或者类似的连续表示去建模关系,这套思路对很多场景都有参考价值。
如果你想真正掌握这套方法,我的建议非常直接:不要只在别人写好的框架里跑测试,而是花一个周末把自己关在房间里,从Ground Truth生成开始,完整写出置信图和PAF的数据管线,然后再去接触网络结构和训练代码。等你能从零生成一条正确的人体骨架,再去谈部署和优化,心里就有底了。后面遇到新的姿态估计论文,也能很快就看懂它到底改进了什么、在哪里动了刀。