Skip to content

姿态估计

姿态估计(Pose Estimation)是定位图像或视频中人体关键点(关节、眼睛、耳朵等)位置的任务。它是动作识别、运动分析、AR 交互的基础技术。从 2016 年的 OpenPose 到今天的 MediaPipe 实时方案,姿态估计已从学术研究走向手机端实时运行。前置阅读:CNN 卷积网络、目标检测 YOLO。

把姿态估计想象成画火柴人:

  • 2D 姿态估计 = 在照片上画火柴人——标出每个关节的二维坐标 (x, y),连成骨架。关节越准,火柴人越像真人。
  • 3D 姿态估计 = 雕塑家捏泥人——不仅知道关节在画面上的位置,还知道离镜头多远 (x, y, z),还原出三维人体。
  • 多人姿态估计 = 在拥挤的操场上给每个人画火柴人——先区分谁是谁,再分别画骨架。

关键点之间不是孤立的:知道肩膀和肘部的位置,手腕的位置大概率在延长线上——这种骨骼先验(Skeleton Prior)是姿态估计的核心线索。人体骨骼遵循运动学约束:骨骼长度恒定、关节角度有物理极限、相邻关键点的相对位置遵循人体拓扑结构。

**热力图回归(Heatmap Regression)**是主流方法。对每个关键点,网络输出一张与原图分辨率相近(或下采样 1/4、1/2)的热力图,每个像素值表示”该关键点落在这里的概率”。最终关键点坐标取热力图峰值:

热力图回归流程:
输入图像 → CNN Backbone → 特征图 → 逐关键点 1×1 卷积 → K 张热力图
关键点 k 的坐标:
(x_k, y_k) = argmax_{(x,y)} Heatmap_k(x, y)
热力图目标 (Ground Truth) 生成:
以真实关键点 (x_k, y_k) 为中心,生成 2D 高斯分布:
GT(x, y) = exp(-((x - x_k)² + (y - y_k)²) / (2σ²))
σ 通常取 2-3 像素——越远离真实点,目标值越接近 0
训练损失:
L = MSE(predicted_heatmap, GT_heatmap)
或 L = KL 散度 (predicted || GT)

优点是对遮挡和复杂姿态鲁棒(一个点模糊时热力图会弥散,取峰值仍可定位),缺点是需要输出多通道特征图,计算量大。

**直接坐标回归(Direct Regression)**直接让网络输出每个关键点的 (x, y) 坐标值。更简单更快,但精度通常不如热力图,且对遮挡不够鲁棒。MediaPipe 用的是这种路线——为了极致的速度。

两种路线的本质区别:热力图是空间分布建模(输出的是空间上的概率场),坐标回归是点估计(直接回归数值)。空间分布天然携带不确定性信息——热力图的双峰意味着”可能在这里也可能在那里”,而坐标回归只能输出一个确定值。

直接用 argmax 取热力图峰值存在亚像素精度问题——输出的坐标只能在像素网格点上,精度被限制在 1 像素以内。Dark(Distribution-Aware Coordinate Representation)解码利用热力图峰值周围的分布做亚像素精细化:

Dark 解码原理:
设热力图峰值位置为 m,其相邻像素为 m-1 和 m+1
修正后的坐标:
x_refined = m + 0.5 * (H(m-1) - H(m+1)) / (H(m-1) - 2*H(m) + H(m+1))
直觉: 峰值左边比右边"高"说明真实峰在 m 偏左 → 用二阶差分修正
效果: 在 COCO 基准上可提升 1-2 AP,几乎是免费的精度提升
方法核心创新特点
OpenPosePart Affinity Field (PAF):学习关节之间的方向场来关联关键点到同一个人多人姿态估计里程碑
HRNet保持高分辨率特征不降采样,多分辨率并行融合精度最高,2D 姿态 SOTA
MediaPipe轻量 CNN + 直接坐标回归,端到端实时手机端 30fps,Google 出品
HigherHRNet自底向上,高分辨率热力图多人场景精度高
PoseformerTransformer 对时空序列建模视频姿态估计
DEKR逐关键点回归(Dense Regression),自底向上多人 3D 姿态
RTMPoseRTMDet 检测器 + 高效姿态网络2023 年实时高精度方案

OpenPose 是多人姿态估计的开创者。它的核心思想是 PAE/PAF(Part Affinity Field,部件亲和场):

  1. 检测所有关键点:用 CNN(VGG backbone)预测每个关节类型的热力图,得到全图所有关键点候选。

  2. 学习骨骼方向场:对每条骨骼(如肘→腕),训练一个 2D 向量场,表示该骨骼的方向和位置。

    PAF 的数学定义:
    对骨骼 (j1 → j2),在骨骼覆盖的每个像素 p 上:
    L_c(p) = v (v 是从 j1 指向 j2 的单位向量)
    不在骨骼上的像素:
    L_c(p) = 0
    匹配分数:
    对候选关键点对 (d_j1, d_j2),沿连线积分:
    E = ∫_0^1 L_c(p(s)) · (d_j2 - d_j1) / ||d_j2 - d_j1|| ds
    p(s) = (1-s)·d_j1 + s·d_j2 是连线上的点
    E 越大 → 方向场与连线方向越一致 → 越可能属于同一个人
  3. 二分图匹配:用 PAF 方向场的一致性,把属于同一个人的关键点匹配成骨架——解决”哪个肘连哪个腕”的归属问题。对每种骨骼类型(如左肘→左腕)独立做二分图最大权匹配(Hungarian 算法)。

传统 CNN(如 ResNet)不断降采样(224→112→56→28→14→7)再恢复,高分辨率信息在降采样过程中被丢失。对于姿态估计这种需要精确空间定位的任务,这是致命的。HRNet 的创新是并行多分辨率:

HRNet 架构:
Stage 1: 单条高分辨率分支(1/4 分辨率)
│
Stage 2: 新增 1/8 分辨率分支(两条并行)
│ ←── 跨分辨率融合:多尺度→高分辨率,高分辨率→多尺度
│
Stage 3: 新增 1/16 分辨率分支(三条并行)
│ ←── 跨分辨率融合
│
Stage 4: 新增 1/32 分辨率分支(四条并行)
│ ←── 跨分辨率融合
│
输出: 从最高分辨率分支输出热力图
跨分辨率融合方式:
低→高: 上采样(双线性插值)+ 1×1 卷积(调整通道数)
高→低: 3×3 卷积(stride=2 降采样)+ 通道调整
四条分支的特征图加在一起 → 每条分支都得到全尺度信息

HRNet 始终保持一条全分辨率分支,低分辨率分支提供语义信息,高分辨率分支保留空间细节——最终输出的热力图质量远超 ResNet 主干。HRNet 之后也成为语义分割、人脸对齐等密集预测任务的通用主干。

训练配置 (HRNet-W32, COCO Keypoint):
- Backbone: HRNet-W32(32 表示高分辨率分支的通道数)
- 输入分辨率: 256 × 192(常见配置)
- 数据增强:
• 随机旋转 (±30°)
• 随机缩放 (0.75 ~ 1.25)
• 随机水平翻转 (注意左右关键点要交换!)
• 随机颜色抖动 (亮度、对比度、饱和度)
• 随机遮挡增强 (Random Erasing / Cutout)
- 损失函数: OKS-based Loss
OKS (Object Keypoint Similarity) 是姿态估计的标准评估指标:
OKS = Σ exp(-d_i² / (2·s²·k_i²)) / Σ δ(v_i > 0)
其中 d_i 是预测关键点与真实点的欧氏距离,s 是目标尺度,
k_i 是每个关键点类型的容忍常数(眼、鼻比肩、髋更难定位 → k 更小)
- 优化器: Adam, lr=1e-3
- 学习率调度: Warmup (500 epoch) + Cosine Annealing
- 训练: 210 epochs,batch_size=128

从 2D 提升到 3D 有两条路:

  • 2D→3D 提升(Lifting-based):先用 2D 模型得到关键点坐标,再用一个小网络(MLP 或 Transformer)推断深度。代表方法如 PoseFormer、SimpleBaseline。优点是可以利用海量 2D 标注,缺点是 2D→3D 是病态问题(多个 3D 姿态可以投影到同一 2D)。

    PoseFormer 的 3D 提升原理:
    输入: 连续 T 帧的 2D 关键点序列 (T × 17 × 2)
    → 线性嵌入到高维空间 (T × 17 × D)
    → Spatio-Temporal Transformer:
    空间注意力: 关键点之间互相注意(建模骨骼约束)
    时间注意力: 不同帧之间互相注意(利用时序平滑性)
    → 输出: 中心帧的 3D 关键点 (17 × 3)
  • 直接 3D 回归:直接从图像回归 3D 坐标,需要 3D 标注数据(如 Human3.6M 数据集)。端到端但依赖昂贵的 3D MoCap 标注。

  • SMPL 人体模型:参数化人体模型(Skinned Multi-Person Linear model),用少量参数(姿态参数 72 维 = 24 个关节 × 3 轴旋转 + 体型参数 10 维)控制一个可微分的 3D 人体网格。可以恢复出完整人体表面,而不仅仅是关键点。

    SMPL 参数:
    姿态参数 θ: 72 维 (24 个关节 × 3 轴旋转角度)
    控制关节弯曲——每个关节用一个轴角(Axis-Angle)表示
    体型参数 β: 10 维 (PCA 系数)
    控制高矮胖瘦——前几个主成分解释了大部分体型变化
    SMPL 前向过程:
    1. 用 θ 变换 24 个关节的朝向 → 得到 posed skeleton
    2. 用 posed skeleton 对蒙皮权重做线性混合蒙皮(LBS):
    v'_i = Σ_j w_j(v_i) · (R_j · (v_i - t_j) + t_j)
    w_j(v_i) 是顶点 v_i 受关节 j 影响的权重
    3. 输出: 6890 个顶点的人体网格 (Mesh)

手部有 21 个关键点(每根手指 4 个关节 × 5 指 + 手腕)。手部姿态估计的特殊挑战:

  • 手指细小:在普通分辨率图像中,一根手指只有几个像素宽,关键点间距极小。
  • 自遮挡严重:不同视角下手指数互相遮挡,从正面看可能只看到掌心和指尖。
  • 姿态空间大:手指组合的自由度极高(21 个关键点 × 3 维 = 63 维空间),难以完全覆盖训练。

MediaPipe Hand 和 FrankMocap 是常用方案。手部姿态是手势识别、手语翻译、AR 手部特效的基础。2023 年后,基于 Transformer 的手部姿态估计(如 HandFormer)在手物交互(Hand-Object Interaction)场景中取得显著进展。

将时序上的姿态序列送入时序模型(RNN/LSTM 或 Transformer),可以识别”走路""挥手""坐下”等动作。相比直接用 RGB 视频做动作识别,姿态序列更轻量且对背景不敏感。

基于姿态的动作识别流程:
输入: 视频帧序列 (T 帧)
│
├─ 逐帧姿态估计: 每帧得到 (17 或 25 个关键点 × 2D/3D 坐标)
│ → 姿态序列 (T × K × 2)
│
├─ 时序建模:
│ ├─ TCN (Temporal Convolutional Network): 时间维卷积
│ ├─ Transformer: 自注意力捕捉长程时序依赖
│ └─ ST-GCN (Spatio-Temporal Graph Convolutional Network):
│ 把人体骨骼表示为图(关节=节点,骨骼=边)
│ 在图上做时空卷积——天然适合骨骼拓扑
│
└─ 输出: 动作类别 (走路/跑步/跳跃/...)

ST-GCN(Spatio-Temporal Graph Convolutional Network,时空图卷积网络)是骨骼动作识别的主流方法。它把人体骨骼定义为一个图(Graph):关节是节点,骨骼连接是边,在时空两个维度上做图卷积——空间维聚合相邻关节信息,时间维聚合相邻帧信息。

COCO 格式 17 个人体关键点:鼻、左眼、右眼、左耳、右耳、左肩、右肩、左肘、右肘、左腕、右腕、左髋、右髋、左膝、右膝、左踝、右踝。

关键点连通关系定义了人体骨骼拓扑:

COCO 骨骼连接 (17 个关键点之间的 19 条边):
面部: 鼻—左眼, 鼻—右眼, 左眼—左耳, 右眼—右耳
上肢左: 左肩—左肘, 左肘—左腕
上肢右: 右肩—右肘, 右肘—右腕
躯干: 左肩—右肩, 左肩—左髋, 右肩—右髋, 左髋—右髋
下肢左: 左髋—左膝, 左膝—左踝
下肢右: 右髋—右膝, 右膝—右踝
这些连接关系构成一个图(Graph),是 ST-GCN 等图卷积网络的输入拓扑

下面用 matplotlib 绘制 COCO 17 个关键点及其骨骼连接,直观展示人体拓扑结构。不同颜色代表不同身体部位(面部、躯干、四肢),每个关键点标注了索引编号和名称。

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D
# ── COCO 17 keypoints (name, x, y) — a clean, realistic humanoid pose ──
keypoints = {
"nose": (0.50, 0.88),
"left_eye": (0.46, 0.92),
"right_eye": (0.54, 0.92),
"left_ear": (0.40, 0.89),
"right_ear": (0.60, 0.89),
"left_shoulder": (0.34, 0.74),
"right_shoulder": (0.66, 0.74),
"left_elbow": (0.22, 0.58),
"right_elbow": (0.78, 0.58),
"left_wrist": (0.14, 0.42),
"right_wrist": (0.86, 0.42),
"left_hip": (0.40, 0.46),
"right_hip": (0.60, 0.46),
"left_knee": (0.37, 0.26),
"right_knee": (0.63, 0.26),
"left_ankle": (0.35, 0.06),
"right_ankle": (0.65, 0.06),
}
kp_names = [
"nose", "left_eye", "right_eye", "left_ear", "right_ear",
"left_shoulder", "right_shoulder", "left_elbow", "right_elbow",
"left_wrist", "right_wrist", "left_hip", "right_hip",
"left_knee", "right_knee", "left_ankle", "right_ankle",
]
# ── COCO skeleton connections (index pairs) ──
edges = [
(0, 1), (0, 2), (1, 3), (2, 4), # face
(5, 6), (5, 7), (7, 9), # left arm
(6, 8), (8, 10), # right arm
(5, 11), (6, 12), (11, 12), # torso
(11, 13), (13, 15), # left leg
(12, 14), (14, 16), # right leg
]
# Color groups for body parts
face_color, torso_color = "#E91E63", "#2196F3"
left_arm_color, right_arm_color = "#4CAF50", "#FF9800"
left_leg_color, right_leg_color = "#9C27B0", "#00BCD4"
edge_colors = {
(0,1): face_color, (0,2): face_color, (1,3): face_color, (2,4): face_color,
(5,6): torso_color, (5,11): torso_color, (6,12): torso_color, (11,12): torso_color,
(5,7): left_arm_color, (7,9): left_arm_color,
(6,8): right_arm_color, (8,10): right_arm_color,
(11,13): left_leg_color, (13,15): left_leg_color,
(12,14): right_leg_color, (14,16): right_leg_color,
}
kp_colors = [face_color]*5 + [torso_color]*2 + [left_arm_color, right_arm_color,
left_arm_color, right_arm_color, torso_color, torso_color,
left_leg_color, right_leg_color, left_leg_color, right_leg_color]
fig, ax = plt.subplots(figsize=(9, 5.5))
ax.set_facecolor("#FAFAFA")
# Draw skeleton edges
for (i, j) in edges:
x_vals = [keypoints[kp_names[i]][0], keypoints[kp_names[j]][0]]
y_vals = [keypoints[kp_names[i]][1], keypoints[kp_names[j]][1]]
ax.plot(x_vals, y_vals, color=edge_colors[(i, j)], linewidth=4,
solid_capstyle="round", zorder=1, alpha=0.85)
# Draw keypoints as circles
xs = [keypoints[n][0] for n in kp_names]
ys = [keypoints[n][1] for n in kp_names]
ax.scatter(xs, ys, c=kp_colors, s=180, zorder=2, edgecolors="white", linewidths=1.5)
# Label each keypoint
label_offsets = {
"nose":(0,-0.055),"left_eye":(0,0.035),"right_eye":(0,0.035),
"left_ear":(-0.055,0),"right_ear":(0.055,0),
"left_shoulder":(-0.055,0.025),"right_shoulder":(0.055,0.025),
"left_elbow":(-0.055,0),"right_elbow":(0.055,0),
"left_wrist":(-0.055,0),"right_wrist":(0.055,0),
"left_hip":(-0.055,0),"right_hip":(0.055,0),
"left_knee":(-0.055,0),"right_knee":(0.055,0),
"left_ankle":(-0.055,0),"right_ankle":(0.055,0),
}
for idx, name in enumerate(kp_names):
x, y = keypoints[name]
dx, dy = label_offsets[name]
ax.annotate(f"{idx}: {name}", (x+dx, y+dy), fontsize=7.5,
ha="center", va="center", fontstyle="italic", color="#333333")
# Legend
legend_elements = [
Line2D([0],[0], marker='o', color='w', markerfacecolor=face_color, markersize=10, label='Face'),
Line2D([0],[0], marker='o', color='w', markerfacecolor=torso_color, markersize=10, label='Torso'),
Line2D([0],[0], marker='o', color='w', markerfacecolor=left_arm_color, markersize=10, label='Left Arm'),
Line2D([0],[0], marker='o', color='w', markerfacecolor=right_arm_color, markersize=10, label='Right Arm'),
Line2D([0],[0], marker='o', color='w', markerfacecolor=left_leg_color, markersize=10, label='Left Leg'),
Line2D([0],[0], marker='o', color='w', markerfacecolor=right_leg_color, markersize=10, label='Right Leg'),
]
ax.legend(handles=legend_elements, loc='upper left', fontsize=8, framealpha=0.9, ncol=2)
ax.set_xlim(-0.05, 1.05); ax.set_ylim(-0.05, 1.05)
ax.invert_yaxis(); ax.set_aspect('equal')
ax.set_xlabel("X", fontsize=9); ax.set_ylabel("Y", fontsize=9)
ax.set_title("COCO 17-Keypoint Human Skeleton Topology",
fontsize=12, fontweight="bold", pad=12)
ax.grid(True, alpha=0.2, linestyle='--')
plt.tight_layout()
plt.savefig("pose-estimation-keypoints.png", dpi=180, bbox_inches="tight", facecolor="white")

COCO 17-Keypoint Human Skeleton Topology

下图将 COCO 17 个关键点及 19 条骨骼连接以火柴人形式可视化,按身体部位用不同颜色标注,帮助理解人体拓扑结构。

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
names = ["nose", "left_eye", "right_eye", "left_ear", "right_ear",
"left_shoulder", "right_shoulder", "left_elbow", "right_elbow",
"left_wrist", "right_wrist", "left_hip", "right_hip",
"left_knee", "right_knee", "left_ankle", "right_ankle"]
kpts = np.array([
[0.0, 0.0], [-0.15, -0.12], [0.15, -0.12], [-0.35, -0.05], [0.35, -0.05],
[-0.6, 0.25], [0.6, 0.25], [-0.95, 0.75], [0.95, 0.75],
[-1.15, 1.2], [1.15, 1.2], [-0.4, 1.25], [0.4, 1.25],
[-0.5, 2.1], [0.5, 2.1], [-0.55, 2.95], [0.55, 2.95],
])
skeleton = [(0,1),(0,2),(1,3),(2,4),(5,7),(7,9),(6,8),(8,10),
(5,6),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16)]
face = {0,1,2,3,4}; arms = {5,6,7,8,9,10}
torso = {11,12}; legs = {13,14,15,16}
color_map = {0:"#FF9800", 1:"#2196F3", 2:"#4CAF50", 3:"#9C27B0"}
def part_color(idx):
if idx in face: return "#FF9800"
if idx in arms: return "#2196F3"
if idx in torso: return "#4CAF50"
return "#9C27B0"
fig, ax = plt.subplots(figsize=(7, 9))
for i, j in skeleton:
ax.plot([kpts[i,0], kpts[j,0]], [kpts[i,1], kpts[j,1]],
color="#555", linewidth=3, solid_capstyle="round", alpha=0.6)
for idx in range(17):
ax.scatter(kpts[idx,0], kpts[idx,1], c=part_color(idx),
s=180, zorder=2, edgecolors="white", linewidths=1.5)
ax.annotate(names[idx], (kpts[idx,0], kpts[idx,1]),
fontsize=7, ha="center",
bbox=dict(boxstyle="round,pad=0.1", facecolor="white",
edgecolor="gray", alpha=0.7))
ax.set_title("COCO 17-Keypoint Human Skeleton Topology",
fontsize=12, fontweight="bold")
ax.set_aspect("equal")
ax.set_xlim(-1.8, 1.8)
ax.set_ylim(3.4, -0.6)
ax.axis("off")
plt.tight_layout()
plt.savefig("pose-estimation-keypoints.png", dpi=180,
bbox_inches="tight", facecolor="white")

COCO 17 关键点骨架

import cv2
import mediapipe as mp # pip install mediapipe
mp_pose = mp.solutions.pose
mp_drawing = mp.solutions.drawing_utils
# BlazePose 配置: 33 个关键点 (比 COCO 17 更丰富,含手部/脚部细节)
pose = mp_pose.Pose(
static_image_mode=False, # 视频流模式(利用时序平滑)
model_complexity=1, # 0=Lite, 1=Full, 2=Heavy
enable_segmentation=False, # 是否输出人体分割
min_detection_confidence=0.5,
min_tracking_confidence=0.5,
)
cap = cv2.VideoCapture("walk.mp4") # 打开视频
while cap.isOpened():
ok, frame = cap.read()
if not ok:
break
# MediaPipe 需要 RGB 输入,OpenCV 默认 BGR
results = pose.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if results.pose_landmarks: # 检测到人体
# 每个关键点: 归一化坐标 (x, y) ∈ [0,1] + 深度 z (相对值) + 可见度
for idx, lm in enumerate(results.pose_landmarks.landmark):
print(f"关键点{idx}: ({lm.x:.3f}, {lm.y:.3f}, z={lm.z:.3f}, vis={lm.visibility:.2f})")
# 在画面上绘制骨架
mp_drawing.draw_landmarks(
frame, results.pose_landmarks, mp_pose.POSE_CONNECTIONS
)
cv2.imshow("Pose", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
from mmpose.apis import init_model, inference_topdown # pip install mmpose
from mmdet.apis import init_detector, inference_detector
det_model = init_detector("yolox_config.py", "yolox.pth", device="cuda:0")
pose_model = init_model("hrnet_config.py", "hrnet_w32.pth", device="cuda:0")
bboxes = inference_detector(det_model, "person.jpg") # 先检测人体框
results = inference_topdown(pose_model, "person.jpg", bboxes)
# results 包含每个检测到的人体 17 个关键点坐标和置信度
import torch
import torch.nn as nn
import torch.nn.functional as F
class HeatmapPoseNet(nn.Module):
"""简化版热力图回归姿态估计网络。
输入: (B, 3, H, W) 图像
输出: (B, K, H/4, W/4) 热力图, K = 关键点数量
"""
def __init__(self, num_keypoints=17, heatmap_stride=4):
super().__init__()
self.stride = heatmap_stride
# 简化 backbone (实际用 HRNet 或 ResNet)
self.backbone = nn.Sequential(
# Stage 1: 3→64, stride=2
nn.Conv2d(3, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
# Stage 2: 64→128, stride=2
nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
# Stage 3: 128→256, stride=2 (总 stride=8)
nn.Conv2d(128, 256, 3, stride=2, padding=1), nn.BatchNorm2d(256), nn.ReLU(),
)
# 热力图预测头: 逐关键点 1×1 卷积
self.heatmap_head = nn.Conv2d(256, num_keypoints, kernel_size=1)
def forward(self, x):
feat = self.backbone(x) # (B, 256, H/8, W/8)
heatmap = self.heatmap_head(feat) # (B, K, H/8, W/8)
return heatmap
@torch.no_grad()
def predict_keypoints(self, heatmap):
"""从热力图提取关键点坐标 (带亚像素 Dark 解码)。"""
B, K, H, W = heatmap.shape
# argmax 找峰值
heatmap_flat = heatmap.view(B, K, -1)
max_idx = heatmap_flat.argmax(dim=-1) # (B, K)
max_val = heatmap_flat.max(dim=-1)[0] # (B, K) 置信度
y = (max_idx // W).float()
x = (max_idx % W).float()
# Dark 亚像素修正 (简化版)
for b in range(B):
for k in range(K):
px, py = int(x[b, k]), int(y[b, k])
if 0 < px < W-1 and 0 < py < H-1:
dx = 0.5 * (heatmap[b,k,py,px-1] - heatmap[b,k,py,px+1]) / \
(heatmap[b,k,py,px-1] - 2*heatmap[b,k,py,px] + heatmap[b,k,py,px+1] + 1e-6)
dy = 0.5 * (heatmap[b,k,py-1,px] - heatmap[b,k,py+1,px]) / \
(heatmap[b,k,py-1,px] - 2*heatmap[b,k,py,px] + heatmap[b,k,py+1,px] + 1e-6)
x[b, k] += dx.item()
y[b, k] += dy.item()
return x * self.stride, y * self.stride, max_val # 缩放回原图坐标
def generate_heatmap(keypoints, img_size, sigma=2, stride=4):
"""从关键点坐标生成高斯热力图标签。
keypoints: (K, 2) 真实关键点坐标(原图尺度)
返回: (K, H/stride, W/stride) 热力图
"""
H, W = img_size[0] // stride, img_size[1] // stride
K = len(keypoints)
heatmap = torch.zeros(K, H, W)
for k in range(K):
cx, cy = keypoints[k] # 原图坐标
cx, cy = cx / stride, cy / stride # 缩放到热力图尺度
# 只在以 (cx, cy) 为中心的局部区域生成高斯(避免计算全图)
for y in range(max(0, int(cy-3*sigma)), min(H, int(cy+3*sigma))):
for x in range(max(0, int(cx-3*sigma)), min(W, int(cx+3*sigma))):
d2 = (x - cx)**2 + (y - cy)**2
heatmap[k, y, x] = torch.exp(torch.tensor(-d2 / (2 * sigma**2)))
return heatmap
  • 自顶向下 vs 自底向上:自顶向下(Top-down,先检测人再逐个估姿态)精度高但速度随人数线性下降;自底向上(Bottom-up,先检测所有关键点再匹配)速度与人数无关但精度略低。OpenPose 是自底向上代表,HRNet+检测器是自顶向下代表。

    速度对比 (n = 图中人数):
    Top-down: O(n) —— 每个人都要单独跑一次姿态网络
    Bottom-up: O(1) —— 全图跑一次,关键点匹配是后处理
    选择策略: 少人场景(≤5)用 Top-down 保精度;多人/拥挤场景用 Bottom-up 保速度
  • 数据增强:旋转、缩放、水平翻转是标配。对于手部姿态,还需要做遮挡增强(随机遮挡部分手指)来提升鲁棒性。水平翻转时注意左右对称的关键点要交换(左肩↔右肩,左肘↔右肘……),否则标签就错了。

  • 热力图后处理:直接取 argmax 会有亚像素精度损失。用 Dark 解码(根据峰值周围像素的分布做二次拟合)可以提升精度。

  • 实时性优化:MediaPipe BlazePose 专为手机设计,GPU 上可达 30+ fps。如果用 HRNet,需要做模型剪枝或蒸馏来加速。2023 年的 RTMPose 通过知识蒸馏 + 模型量化,在保持高精度的同时实现了手机端实时。

  • 3D 精度挑战:单目 3D 姿态存在深度歧义(前后难以区分)。多视角相机可以消除歧义但部署成本高。

  • 训练分辨率与推理分辨率要一致:热力图回归对输入分辨率敏感。训练时用 256×192,推理时也应用同尺寸输入(或等比缩放后做坐标逆变换),否则精度会下降。

  • 健身与运动指导:Keep、Nike Training Club 用姿态估计实时纠正深蹲、俯卧撑的动作角度——你的手机就是私人教练。
  • 动作捕捉(MoCap):影视和游戏制作中替代昂贵的光学动捕设备,用普通摄像头就能采集人体动作数据。
  • 安防行为分析:检测跌倒、打架、异常徘徊等行为——养老院跌倒报警、公共场所安保监控。
  • 虚拟主播(VTuber):实时追踪面部和身体动作,驱动虚拟形象——B站、YouTube 虚拟主播的技术基础。
  • AR 滤镜与特效:Snapchat、抖音的全身特效(虚拟衣服、骨骼变形)依赖实时姿态估计。
  • 手语翻译:识别手部姿态序列,翻译成文字或语音——听障人士的沟通工具。

2024-2025 年,姿态估计开始与视觉大模型融合:

  • SAM-Pose:利用 Segment Anything Model(SAM)的强大分割能力辅助姿态估计——SAM 提供精确的人体轮廓,姿态网络在轮廓内做精确关键点回归,显著提升遮挡场景的表现。
  • FoundationPose / UMAP:在大规模无标注视频上自监督预训练的通用人体姿态基础模型,可以 Zero-shot 迁移到新场景(如运动分析、医疗康复),不再需要为每个新场景重新标注数据。
  • DWPose / SapiensPose:全身姿态估计(Whole-Body Pose),将身体、手部(21 点×2)、面部(68+ 点)统一到一个模型中输出,总计 133+ 个关键点。Meta 的 Sapiens(2024)展示了在 3 亿张人体图像上预训练的全能人体理解模型。

传统的 3D 人体网格恢复(HMR)依赖 3D 标注数据且精度有限。2024-2025 年的新趋势:

  • 4DHumans / HMR 2.0:Google 的 4DHumans 从单目视频恢复 4D(时空连贯)人体网格,利用 ViT backbone + 时序优化,在野外视频上实现了接近 MoCap 精度的结果。
  • SMPLer-X / PIXIE:将身体姿态、手部姿态、面部表情统一回归的全身 3D 人体恢复方案——从单张图像恢复完整的人体网格 + 表情 + 手势。
  • *-neural- / 生成式人体先验:用生成模型学习人体姿态的先验分布,用于约束 3D 重建的合理性,有效解决遮挡和截断场景。

从单帧姿态估计走向视频时序优化:

  • BiPPose / PoseFormer V2:利用双向 Transformer 对视频序列建模,前后帧信息互相补充——遮挡帧可以从前后帧”借”信息,大幅降低遮挡导致的误差。
  • VIBE / MEVA / TCMR:从视频中重建 3D 人体运动序列的代表性方法,结合对抗训练(GAN)和时序平滑约束。
  • 运动质量评估(Motion Quality Assessment):2025 年新方向——不仅估计姿态,还评估动作质量(如康复训练动作是否标准、运动技术是否规范),用大模型做细粒度动作理解。

受 CLIP 等多模态模型启发,2025 年的动作识别走向开放词汇(Open-Vocabulary):

  • Open-vocabulary Action Recognition:不再局限于预定义的几百个动作类别,而是用自然语言描述要识别的动作(如”正在用右手拿杯子喝水”),模型输出匹配分数。
  • Video-LLM 理解动作:结合视频大模型(如 VideoLLaMA、GPT-4o),可以直接用自然语言问答的方式分析视频中的动作——“这个人做了几次深蹲?""他的左膝角度够不够深?”

姿态估计是端侧 AI(手机/IoT)最重要的应用之一:

  • RTMPose / RTMO(2023-2024):RealTime Multiple Object Pose,通过蒸馏 + 量化 + 高效架构设计,在手机 NPU 上实现 100+ fps 的多人姿态估计。是当前端侧部署的最优方案。
  • Yolo-Pose / YOLOv8-Pose:将姿态估计与目标检测统一到 YOLO 架构中,一次前向传播同时输出人体框和关键点,极简部署。
类库语言说明
MediaPipePython/C++Google 出品,跨平台实时姿态估计,手机端友好
MMPosePython商汤开源的姿态估计工具箱,算法齐全,学术首选
OpenPoseC++/PythonCMU 开发的多人姿态估计经典框架,PAF 方法原版实现
AlphaPosePython上交开源的多人姿态估计,自顶向下方案
mmpose + mmdetectionPython组合使用,先检测人体再做姿态估计
trt-posePython/TensorRTNVIDIA 优化的实时姿态估计,部署在 Jetson 边缘设备
RTMPosePythonOpenMMLab 的实时高精度方案,支持端侧部署
术语英文解释
姿态估计Pose Estimation定位人体关键点(关节)位置的任务
关键点Keypoint / Landmark人体上的标注点,如肩膀、肘、膝等关节
热力图回归Heatmap Regression为每个关键点输出一张概率热力图,取峰值定位
直接回归Direct Regression直接让网络输出关键点的 (x, y) 坐标值
部件亲和场Part Affinity Field (PAF)OpenPose 提出的骨骼方向场,用于关联关键点到同一个人
HRNetHigh-Resolution Network并行多分辨率融合网络,2D 姿态估计精度最高
SMPLSkinned Multi-Person Linear参数化人体网格模型,用少量参数控制 3D 人体
OKSObject Keypoint Similarity姿态估计标准评估指标,类似目标检测中的 IoU
自顶向下Top-down先检测每个人再做单人姿态估计的策略
自底向上Bottom-up先检测所有关键点再匹配到个人的策略
动作识别Action Recognition从时序姿态或视频序列中识别动作类别
ST-GCNSpatio-Temporal Graph Convolutional Network时空图卷积网络,在骨骼图上做动作识别
COCO 关键点格式COCO Keypoint Format定义 17 个人体关键点的标准标注规范
人体网格恢复Human Mesh Recovery (HMR)从图像/视频恢复 3D 人体网格(含 SMPL 参数)
  • Cao et al.,「OpenPose: Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields」(TPAMI 2019):OpenPose 论文,多人姿态估计的奠基工作,PAF 方法详解。
  • Sun et al.,「Deep High-Resolution Representation Learning for Human Pose Estimation」(CVPR 2019):HRNet 论文,提出并行高分辨率网络,2D 姿态估计精度革命性提升。
  • Loper et al.,「SMPL: A Skinned Multi-Person Linear Model」(SIGGRAPH Asia 2015):SMPL 人体参数模型论文,3D 人体重建的基础。
  • Bazarevsky et al.,「BlazePose: On-device Real-time Body Pose Tracking」(arXiv 2020):Google MediaPipe 姿态方案,手机端实时运行。
  • Zheng et al.,「PoseFormer: Spatial-Temporal Transformer for 3D Human Pose Estimation」(ICCV 2021):Transformer 做 3D 姿态时序提升。
  • Goel et al.,「Humans in 4D: Reconstructing and Tracking Humans with Transformers」(ICCV 2023):4DHumans,单目视频 4D 人体重建的突破。
  • MMPose GitHub:https://github.com/open-mmlab/mmpose——最全面的姿态估计工具箱,含 30+ 算法实现。