第二十四周:OpenPose:使用部分亲和场的实时多人2D姿态估计
OpenPose
- 摘要
- Abstract
- 文章信息
- 引言
- 方法
- 同时进行检测和关联
- 关键部位检测的置信图
- PAF
- 使用PAF进行多人解析
- 关键代码
- 实验结果
- 创新与不足
- 总结
摘要
本篇博客介绍了一种实时多人2D姿态估计框架——OpenPose,其核心思想是通过自底向上的全局关联策略,解决传统方法在多人场景下面临的计算效率低与关键点误匹配问题。针对多人姿态中肢体拓扑关联的复杂性,提出部分亲和场(PAF)技术,以向量场代表关键点间的空间方向关系,结合双分支卷积网络同步输出关键点置信图与PAF场,通过路径积分与二分图匹配算法实现跨个体的精准聚类;针对小尺度关键点检测精度不足,设计多阶段级联网络,逐层细化预测结果以提升遮挡区域的鲁棒性。其优势在于开源易用性、多部位(身体/手部/面部)联合检测能力及遮挡场景下的稳定表现,但存在计算资源消耗高、密集人群误匹配等局限。未来研究可围绕轻量化模型部署、时序动作连续性建模及三维姿态扩展展开。
Abstract
This blog introduces OpenPose, a real-time multi-person 2D pose estimation framework. Its core innovation lies in a bottom-up global association strategy, which addresses the inefficiency and keypoint mismatch issues of traditional methods in multi-person scenarios. To tackle the complexity of limb topological association, the framework proposes Part Affinity Fields (PAF), a technique that represents spatial directional relationships between keypoints through vector fields. By integrating a dual-branch convolutional network, OpenPose simultaneously generates keypoint confidence maps and PAF fields. These outputs are then processed via path integration and bipartite graph matching algorithms to achieve precise cross-individual clustering. Furthermore, to enhance the accuracy of small-scale keypoint detection under occlusion, the framework employs a multi-stage cascaded network architecture that iteratively refines predictions. OpenPose demonstrates strengths in open-source accessibility, multi-part joint detection (body, hands, and face), and robust performance in occluded scenarios. However, limitations include high computational resource demands and mismatches in dense crowds. Future research directions may focus on lightweight model deployment, temporal motion continuity modeling, and extensions to 3D pose estimation.
文章信息
Title:OpenPose: Realtime Multi-Person 2D Pose Estimation Using Part Affinity Fields
Author: Cao, Z (Cao, Zhe) ; Hidalgo, G (Hidalgo, Gines) ; Simon, T (Simon, Tomas) ; Wei, SE (Wei, Shih-En); Sheikh, Y (Sheikh, Yaser)
Source:https://webofscience.clarivate.cn/wos/alldb/full-record/WOS:000597206900012
引言
人体的2D姿态估计,在定位人体的关键点或者部分关键点,主要集中在寻找个体的身体部位。预测多人的姿态面临着挑战:首先:每张图像中可能包含未知数量的人,且图像中的人可能以任意尺度出现在任何位置;第二:人与人之间的交互使空间推理变得复杂,如接触、遮挡等会使体关键点部分之间的关联变得困难;第三:运行时间复杂度随图像中人数的增加而增加,给实时性带来挑战。
姿态估计分为两种方法:自顶向下和自底向上
自顶向下:先使用目标检测器检测出图像中的人,然后对检测出的每个人分别进行姿态估计,是采用“检测→分割→关键点预测”的串行流程。这种方法的效果依赖目标检测的效果,如果图中出现重叠度较高的两个人,则可能会因非极大值抑制而遗漏其中一个人,后续的姿态估计也会受影响。且这种方法计算成本与人数成正比,实时性较差。
自底向上:先检测图像中所有可能的关键点,再通过分组算法将这些关键点关联到不同的个体,是通过“关键点检测→聚类分组”的并行流程。这种方法对遮挡和多人重叠场景更鲁棒,无需依赖检测框。但这种方法并不直接使用来自其他身体部位和其他人的全局上下文信息,在此论文提出之前,最终的图像解析需要耗费大量时间去做全局推理,实时性差。
openpose这篇论文就是采用自底向上的方法,文中提出了部分亲和场(Part Affinity Fields, PAF)方法,通过向量场建模肢体方向,解决多人场景下关键点的正确关联问题。
方法
上图展示了整体的工作流程。整个网络以大小为 w
×
\times
× h 的彩色图像作为输入,输出图像中每个人的关键点及连接的信息。
- 将图像输入卷积神经网络提取特征
- 利用第一步得到的特征图同时预测关键点热度图和PAF信息
- 将关键点进行二分匹配,选择有最大得分的连接方式进行连接
- 组合所有连接得到图像中所有人的全身姿势
预测身体关键点位置的网络输出为一组2D置信图S,人体有J个关键点,每个关键点对应一个置信图,则每组有J个置信图,即
S
=
(
S
1
,
S
2
,
S
3
.
.
.
,
S
J
)
S=(S_1,S_2,S_3...,S_J)
S=(S1,S2,S3...,SJ),
S
j
∈
R
w
×
h
,
j
∈
{
1
,
2...
,
J
}
S_j \in R^{w\times h},j \in {\{1,2...,J\}}
Sj∈Rw×h,j∈{1,2...,J}。
预测PAF信息的网络输出一组2D矢量场L,编码各个部位之间的关联度。每个肢体是一种连接,需要一个矢量场,2D矢量分为 x 和 y 方向,所以C个关键点的连接方式(C个肢体)有C个向量场,
L
=
(
L
1
,
L
2
.
.
.
,
L
C
)
L = (L_1,L_2...,L_C)
L=(L1,L2...,LC),
L
c
∈
R
w
×
h
×
2
,
c
∈
{
1
,
2...
,
C
}
L_c \in R^{w \times h \times 2},c \in {\{1,2...,C\}}
Lc∈Rw×h×2,c∈{1,2...,C}。
同时进行检测和关联
如上图所示,上部分分支网络(Branch 1)用来预测置信图,下部分分支网络(Branch 2)用来预测亲和场 PAF。每一个分支都是一个迭代的预测结构。这种连续重复的阶段优化了预测结果,每个阶段都会有中继监督。
图像首先通过卷积网络进行分析(如使用VGG-19的前10层来初始化并且微调),生成了一组特征图F,作为stage1的输入。在第一阶段,网络产生一组关键点置信度图
S
1
=
ρ
1
(
F
)
S^1=\rho ^1(F)
S1=ρ1(F)和一组PAF:
L
1
=
ϕ
1
(
F
)
L^1=\phi ^1(F)
L1=ϕ1(F),其中
ρ
1
\rho^1
ρ1和
ϕ
1
\phi ^1
ϕ1是stage 1的预测网络,在后续的每个阶段中,前一个阶段的两个分支预测与原始图像特征F进行拼接(concat),作为后一阶段的输入,用来产生更好的预测结果。
其中,
ρ
t
\rho ^t
ρt和
ϕ
t
\phi ^t
ϕt是stage t的预测网络。
上图展示了置信度图和PAF在不同阶段的优化结果。为了指导迭代预测置信度图和PAF,在每个阶段后都使用损失函数。在预测结果与真实标签之间使用L2损失,并对损失函数进行空间加权,来解决一些数据并不是由所有人的标签的问题。stage t 的两个分支的损失函数描述如下:
其中,
S
j
∗
S^*_j
Sj∗是真实关键点置信图,
L
c
∗
L^*_c
Lc∗是真实亲和向量场。
W
W
W是二值掩码,当图像位置p没有标注时,
W
(
p
)
=
0
W(p)=0
W(p)=0。这个掩码用于避免在训练时惩罚(降低)真实积极的预测。在每个stage后的中继监督通过周期性的补充梯度,解决了梯度消失的问题。总体损失为:
关键部位检测的置信图
为了评估训练阶段的
f
s
f_s
fs ,用标注的2D关键点生成真实置信图
S
∗
S^*
S∗。每个置信图是特定身体部位在任意像素位置的置信度的2D表示。每张置信图包含图像中每个人k的可见特定关键部位j的峰。
对于图像中包含多人的情况,为每个人生成置信图
S
j
,
k
∗
S^*_{j,k}
Sj,k∗,
x
j
,
k
∈
R
2
x_{j,k}\in R^2
xj,k∈R2是图中第k个人的第j个关键点的真实位置,则在位置
p
∈
R
2
p\in R^2
p∈R2处:
这其实就是一个高斯运算,其中,
σ
\sigma
σ控制峰值的范围。
网络预测的置信图是通过max运算符汇总的各个置信度图:
即不管图像中有多少人,对于每种关键点都只生成一张置信图,取最大值操作只是在置信图中保留了最高置信度的位置,而不会改变其他关键点的检测结果。
测试时,预测置信度图,并通过执行非最大抑制来获得候选关键点。
PAF
通过预测置信图可以得到预测的候选关键点,如何将它们组合成未知数量的人的全身姿势是一个难题。
需要检测出相邻关键点之间联系的置信测量,即 这些关键点是否属于同一个人。
一种可能的方法是,检测肢体上每对部位之间的附加中点,并检查其在候选部位检测之间的发生率,如上图 b所示。但当图像中的人比较聚集时,这些中点很可能支持错误的关联(如上图 b中的绿色线所示)。导致这种错误的原因:1.这种方法只编码了每个肢体的位置,没编码方向。2.这种方法将肢体的支撑区域减少到一个点。
为了解决上述问题,论文提出了部位亲和场的方法,它编码了肢体支撑区域的位置和方向信息,如上图 c所示。对于位于特定肢体区域的每个像素,部分亲和场编码了一个部位到另一个部位的方向,并且每种肢体都有相应的亲和场来连接它关联的两个身体部位。
以上图中的肢体为例,
X
j
1
,
k
X_{j1,k}
Xj1,k和
X
j
2
,
k
X_{j2,k}
Xj2,k是图像中人 k 的肢体 c 对应的两个关键点
j
1
j1
j1和
j
2
j2
j2的位置。如果点p位于这个肢体上,则点p的PAF标签为从
j
1
j1
j1到
j
2
j2
j2的单位向量,如果p不在这个肢体上,则该点处的亲和场值为零向量。公式表示如下:
其中 v 是单位向量,因为不能连接方式不能与大小有关(图像中的肢体的尺度可能不同),仅与方向有关。
在肢体上的点p定义为在一定距离阈值之内的点集,满足:
其中
σ
l
\sigma _l
σl是肢体宽度。
肢体长度为:
部分亲和场是图像中所有人的肢体c的平均值:
其中
n
c
(
P
)
n_c(P)
nc(P)是所有k个人在位置p处的非零向量的个数(即不同人的肢体重叠处的像素平均值)
在预测时,通过沿着连接候选部位的线段计算对应部位亲和场的积分来测量候选部位之间的关联,对于两个候选关键点
d
j
1
d_{j1}
dj1和
d
j
2
d_{j2}
dj2:
其中
p
(
u
)
=
(
1
−
u
)
d
j
1
+
u
d
j
2
p(u)=(1-u)d_{j1}+ud_{j2}
p(u)=(1−u)dj1+udj2。实际中,通过取样u的等间距值,求和对应L值来计算积分。
使用PAF进行多人解析
上一小节提到的部分亲和场是计算两个关键点间联系的得分的,并没有给出解决多个候选部位之间如何连接以形成肢体的问题。
对于每个关键点,可以有多个候选点,这是由于图片中存在多人或者错误预测情况,如下图中 (b)。这些候选关键点定义了一个有可能为肢体的集合。用在PAF上线积分计算的结果来给每个候选肢体打分。找出最优划分的问题,这是一个k维匹配问题,即一个NP-Hard问题。
论文中提出贪婪松弛,可以不断产生更准确的匹配。论文中猜测这是因为由于PAF网络更大的感受野,成对关联的得分隐式的编码了全局上下文。
首先获取图中多人的人体检测的候选关键点
D
j
D_j
Dj,
D
j
=
{
d
j
m
:
f
o
r
j
∈
{
1
,
2...
J
}
,
m
∈
{
1
,
2...
N
j
}
}
D_j=\{d^m_j :for j\in {\{1,2...J\}},m\in {\{1,2...N_j\}}\}
Dj={djm:forj∈{1,2...J},m∈{1,2...Nj}},其中
N
j
N_j
Nj是关键点
j
j
j的候选数量,
d
j
m
∈
R
2
d^m_j \in R^2
djm∈R2是关键点
j
j
j的第
m
m
m个检测框位置。
需要找出正确的每对的肢体连接,图的节点是检测的身体关键点的候选点
D
j
1
D_{j1}
Dj1,
D
j
2
D_{j2}
Dj2,它的边是检测点之间所有可能的成对连接方式。每一个边的权重由公式求 E EE 来确定,即关键点的亲和的总和。二分图的匹配是通过两边没有共享一个节点的方式,来选出边的子集。我们的目标是在边中找到一个有最大权重的匹配,
其中,
E
c
E_c
Ec是c类肢体的的匹配的总体权重,
Z
c
Z_c
Zc是 c类肢体的 Z的子集,
E
m
n
E_{mn}
Emn是关键点
d
j
1
m
d^m_{j1}
dj1m和
d
j
2
m
d^m_{j2}
dj2m之间的亲和度。
当需要找到多个人完整的姿态时,确定 Z 是一个 k 维的匹配问题。
首先,选择一个最小边数来获取人体姿势的一块棵生成树的骨架,而不是使用完整的图;然后,进一步的将这个匹配问题分解成二分图匹配的集合,同时分别确定相邻树节点的匹配。这两个“松弛”被简单的分解为:
对于所有肢体连接的候选,组合这些共享同一个关键点的连接,来得到多人完整的姿势。
关键代码
以下是参考pytorch中openpose的搭建:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: Donny You(youansheng@gmail.com)
import torch
import torch.nn as nn
from model.pose.loss.loss import BASE_LOSS_DICT
from lib.model.module_helper import ModuleHelper
# 定义 OpenPose 网络类,继承自 nn.Module
class OpenPose(nn.Module):
def __init__(self, configer):
# 调用父类的构造函数
super(OpenPose, self).__init__()
# 保存配置信息
self.configer = configer
# 根据配置信息获取骨干网络
self.backbone = ModuleHelper.get_backbone(
backbone=self.configer.get('network.backbone'),
pretrained=self.configer.get('network.pretrained')
)
# 初始化 PoseModel 模型,输入通道数为骨干网络的特征数
self.pose_model = PoseModel(configer, self.backbone.get_num_features())
# 获取有效的损失字典
self.valid_loss_dict = configer.get('loss', 'loss_weights', configer.get('loss.loss_type'))
def forward(self, data_dict):
# 将输入图像数据通过骨干网络
x = self.backbone(data_dict['img'])
# 将骨干网络的输出通过 PoseModel 模型,得到 paf_out 和 heatmap_out
paf_out, heatmap_out = self.pose_model(x)
# 构建输出字典,包含最后一层的 paf 和 heatmap 输出
out_dict = dict(paf=paf_out[-1], heatmap=heatmap_out[-1])
# 如果是测试阶段,直接返回输出字典
if self.configer.get('phase') == 'test':
return out_dict
# 初始化损失字典
loss_dict = dict()
# 遍历 paf_out 的每个输出
for i in range(len(paf_out)):
# 如果对应的 paf_loss 在有效损失字典中
if 'paf_loss{}'.format(i) in self.valid_loss_dict:
# 计算 paf 损失,包括参数、损失类型和权重
loss_dict['paf_loss{}'.format(i)] = dict(
params=[paf_out[i]*data_dict['maskmap'], data_dict['vecmap']*data_dict['maskmap']],
type=torch.cuda.LongTensor([BASE_LOSS_DICT['mse_loss']]),
weight=torch.cuda.FloatTensor([self.valid_loss_dict['paf_loss{}'.format(i)]])
)
# 遍历 heatmap_out 的每个输出
for i in range(len(heatmap_out)):
# 如果对应的 heatmap_loss 在有效损失字典中
if 'heatmap_loss{}'.format(i) in self.valid_loss_dict:
# 计算 heatmap 损失,包括参数、损失类型和权重
loss_dict['heatmap_loss{}'.format(i)] = dict(
params=[heatmap_out[i]*data_dict['maskmap'], data_dict['heatmap']*data_dict['maskmap']],
type=torch.cuda.LongTensor([BASE_LOSS_DICT['mse_loss']]),
weight=torch.cuda.FloatTensor([self.valid_loss_dict['heatmap_loss{}'.format(i)]])
)
return out_dict, loss_dict
# 定义 PoseModel 类,继承自 nn.Module
class PoseModel(nn.Module):
def __init__(self, configer, in_channels):
# 调用父类的构造函数
super(PoseModel, self).__init__()
# 保存配置信息和输入通道数
self.configer = configer
self.in_channels = in_channels
# 获取模型字典
model_dict = self._get_model_dict(self.configer, in_channels)
# 从模型字典中获取各个模块
self.model0 = model_dict['block_0']
self.model1_1 = model_dict['block1_1']
self.model2_1 = model_dict['block2_1']
self.model3_1 = model_dict['block3_1']
self.model4_1 = model_dict['block4_1']
self.model5_1 = model_dict['block5_1']
self.model6_1 = model_dict['block6_1']
self.model1_2 = model_dict['block1_2']
self.model2_2 = model_dict['block2_2']
self.model3_2 = model_dict['block3_2']
self.model4_2 = model_dict['block4_2']
self.model5_2 = model_dict['block5_2']
self.model6_2 = model_dict['block6_2']
# 初始化卷积层的权重和偏置
for m in self.modules():
if isinstance(m, nn.Conv2d):
m.weight.data.normal_(0, 0.01)
if m.bias is not None:
m.bias.data.zero_()
@staticmethod
def _make_layers(layer_dict):
# 初始化层列表
layers = []
# 遍历除最后一层外的所有层
for i in range(len(layer_dict) - 1):
layer = layer_dict[i]
for k in layer:
v = layer[k]
# 如果是池化层
if 'pool' in k:
layers += [nn.MaxPool2d(kernel_size=v[0], stride=v[1], padding=v[2])]
else:
# 构建卷积层和 ReLU 激活函数
conv2d = nn.Conv2d(in_channels=v[0], out_channels=v[1], kernel_size=v[2], stride=v[3], padding=v[4])
layers += [conv2d, nn.ReLU(inplace=True)]
# 处理最后一层
layer = list(layer_dict[-1].keys())
k = layer[0]
v = layer_dict[-1][k]
conv2d = nn.Conv2d(in_channels=v[0], out_channels=v[1], kernel_size=v[2], stride=v[3], padding=v[4])
layers += [conv2d]
return nn.Sequential(*layers)
@staticmethod
def _get_model_dict(configer, in_channels):
# 获取 paf 和 heatmap 的输出通道数
paf_out = configer.get('network', 'paf_out')
heatmap_out = configer.get('network', 'heatmap_out')
# 初始化块字典
blocks = {}
# 定义 block_0
block_0 = [{'conv4_3_CPM': [in_channels, 256, 3, 1, 1]}, {'conv4_4_CPM': [256, 128, 3, 1, 1]}]
# 定义 block1_1 和 block1_2
blocks['block1_1'] = [{'conv5_1_CPM_L1': [128, 128, 3, 1, 1]}, {'conv5_2_CPM_L1': [128, 128, 3, 1, 1]},
{'conv5_3_CPM_L1': [128, 128, 3, 1, 1]}, {'conv5_4_CPM_L1': [128, 512, 1, 1, 0]},
{'conv5_5_CPM_L1': [512, paf_out, 1, 1, 0]}]
blocks['block1_2'] = [{'conv5_1_CPM_L2': [128, 128, 3, 1, 1]}, {'conv5_2_CPM_L2': [128, 128, 3, 1, 1]},
{'conv5_3_CPM_L2': [128, 128, 3, 1, 1]}, {'conv5_4_CPM_L2': [128, 512, 1, 1, 0]},
{'conv5_5_CPM_L2': [512, heatmap_out, 1, 1, 0]}]
# 定义 block2 到 block6
for i in range(2, 7):
blocks['block%d_1' % i] = [{'Mconv1_stage%d_L1' % i: [128 + paf_out + heatmap_out, 128, 7, 1, 3]},
{'Mconv2_stage%d_L1' % i: [128, 128, 7, 1, 3]},
{'Mconv3_stage%d_L1' % i: [128, 128, 7, 1, 3]},
{'Mconv4_stage%d_L1' % i: [128, 128, 7, 1, 3]},
{'Mconv5_stage%d_L1' % i: [128, 128, 7, 1, 3]},
{'Mconv6_stage%d_L1' % i: [128, 128, 1, 1, 0]},
{'Mconv7_stage%d_L1' % i: [128, paf_out, 1, 1, 0]}]
blocks['block%d_2' % i] = [{'Mconv1_stage%d_L2' % i: [128 + paf_out + heatmap_out, 128, 7, 1, 3]},
{'Mconv2_stage%d_L2' % i: [128, 128, 7, 1, 3]},
{'Mconv3_stage%d_L2' % i: [128, 128, 7, 1, 3]},
{'Mconv4_stage%d_L2' % i: [128, 128, 7, 1, 3]},
{'Mconv5_stage%d_L2' % i: [128, 128, 7, 1, 3]},
{'Mconv6_stage%d_L2' % i: [128, 128, 1, 1, 0]},
{'Mconv7_stage%d_L2' % i: [128, heatmap_out, 1, 1, 0]}]
layers = []
for block in block_0:
for key in block:
v = block[key]
if 'pool' in key:
layers += [nn.MaxPool2d(kernel_size=v[0], stride=v[1], padding=v[2])]
else:
conv2d = nn.Conv2d(in_channels=v[0], out_channels=v[1], kernel_size=v[2], stride=v[3], padding=v[4])
layers += [conv2d, nn.ReLU(inplace=True)]
models = {
'block_0': nn.Sequential(*layers)
}
# 构建其他模块
for k in blocks:
v = blocks[k]
models[k] = PoseModel._make_layers(v)
return models
def forward(self, x):
# 通过 model0 进行前向传播
out1 = self.model0(x)
out1_1 = self.model1_1(out1)
out1_2 = self.model1_2(out1)
# 拼接输出
out2 = torch.cat([out1_1, out1_2, out1], 1)
out2_1 = self.model2_1(out2)
out2_2 = self.model2_2(out2)
out3 = torch.cat([out2_1, out2_2, out1], 1)
out3_1 = self.model3_1(out3)
out3_2 = self.model3_2(out3)
out4 = torch.cat([out3_1, out3_2, out1], 1)
out4_1 = self.model4_1(out4)
out4_2 = self.model4_2(out4)
out5 = torch.cat([out4_1, out4_2, out1], 1)
out5_1 = self.model5_1(out5)
out5_2 = self.model5_2(out5)
out6 = torch.cat([out5_1, out5_2, out1], 1)
out6_1 = self.model6_1(out6)
out6_2 = self.model6_2(out6)
# 收集 paf 和 heatmap 的输出
paf_out = [out1_1, out2_1, out3_1, out4_1, out5_1, out6_1]
heatmap_out = [out1_2, out2_2, out3_2, out4_2, out5_2, out6_2]
return paf_out, heatmap_out
if __name__ == "__main__":
print(OpenPose(1))
其中,OpenPose 类是整个 OpenPose 网络模型的封装,主要负责整合骨干网络、姿态模型以及损失计算,PoseModel 类是 OpenPose 网络中具体的姿态估计模型,负责构建和执行从特征图到 PAF 和热图输出的计算过程。
实验结果
与其他方法比较,openpose有最快的速度和相对好的mAP。
同时,也展现了其运行时间随着图像中人数的增加的变化很小,意味着openpose在多人姿态估计上的表现优异。
创新与不足
OpenPose采用了自底向上的方法来评估人体姿态。文中提出部分亲和场,通过向量场建模肢体方向,解决多人场景下关键点的正确关联问题。PAF为每个肢体定义了一个方向向量场,不仅指示关键点之间的空间关系,还编码了肢体方向信息。相比传统的中点匹配或几何约束方法,PAF能有效处理肢体重叠、遮挡和复杂背景,显著提升多人姿态估计的鲁棒性。传统方法(如Mask R-CNN)处理时间随人数线性增长,而OpenPose在多人场景下仍保持高效。通过多阶段(Stage)迭代逐步细化预测结果。每个阶段将前一阶段的置信图、PAF场与原始特征拼接,通过中间监督(Intermediate Supervision)补充梯度,缓解梯度消失问题,确保预测精度逐级提升。
但在面对遮挡与极端姿势时效果不佳,对严重遮挡、快速动作或非常规姿势(如舞蹈动作)的检测鲁棒性不足,易导致关键点误关联或漏检。且OpenPose对复杂手部姿态(如手指弯曲)和脚部细节(如鞋型)的检测常出现模糊或错误。
总结
OpenPose作为首个实现实时多人全身姿态估计的自底向上的方法,其工作流程围绕全局特征解析→关键点关联→拓扑合成展开:首先通过VGG-19骨干网络提取图像特征,并采用多阶段级联的双分支网络同步预测关键点置信图(Confidence Maps)和部分亲和场(PAF),其中置信图定位所有人体关节点的概率分布,PAF则以向量场形式编码相邻关键点的空间关联性;随后对置信图的峰值进行非极大值抑制提取候选关键点,并基于PAF对每对候选点沿连接路径积分计算亲和度得分,利用二分图匹配算法(如匈牙利算法)将离散关键点聚类为完整的个体姿态;最后结合预定义的肢体拓扑规则验证连接合理性,输出涵盖身体、手部及面部的多粒度姿态数据。其核心价值在于兼顾实时性与多尺度检测能力,既能实现高效推理,又能联合输出身体、手部及面部的精细化运动特征。然而,该框架仍受限于计算资源消耗较高、复杂遮挡下关键点分组易出错等缺陷,未来研究可围绕轻量化模型压缩、多模态时序信息融合以及抗遮挡关联算法展开优化。