MMPose 手部关键点回归实战:RHD 数据集上基于 DeepPose + ResNet-50 的 Top-down 2D 姿态估计全解析
MMPose 手部关键点回归实战:RHD 数据集上基于 DeepPose + ResNet-50 的 Top-down 2D 姿态估计全解析
MMPose 在 configs/hand_2d_keypoint/topdown_regression/ 目录下提供了基于回归范式的 Top-down 手部 2D 关键点检测方案,其中 RHD(Rendered Handpose Dataset)上的 ResNet-50 配置是理解"直接回归坐标"这一范式的最佳入口。本文以 resnet_rhd2d.md 为核心骨架,逐段拆解其完整训练配置、数据管线、模型结构与评估指标,并结合 regression_head.py 与 regression_label.py 等源码揭示底层实现原理。读完本文,你将能够独立复现 RHD 上手部关键点回归模型的训练与测试,并理解如何将同一配置迁移到 OneHand10K 等其他手部数据集。
一、文档背景:三种技术栈的组合
resnet_rhd2d.md 的第一部分通过三段 <details> 折叠块引用了支撑该配置的三篇基础工作,这也是该模型在 MMPose Model Zoo 中归属的三大技术来源:
- DeepPose(CVPR'2014)——由 Toshev 等人提出的"基于深度神经网络的人体姿态估计",开创了直接用全连接网络回归关键点坐标的范式。该配置中的
RegressionHead正是这一范式的现代实现。 - ResNet(CVPR'2016)——He 等人提出的深度残差学习,作为特征提取主干。本配置使用 50 层版本(ResNet-50),并加载 torchvision 预训练权重。
- RHD(ICCV'2017)——Zimmermann 与 Brox 提出的"从单张 RGB 图像估计 3D 手部姿态"数据集,是评估手部关键点检测的标准化基准,提供 2D 与 3D 标注。
这三篇工作的 BibTeX 引用均保留在文档中,若在论文中复用该模型或数据集,可直接引用。该配置在 MMPose Model Zoo 中被归类于 "DeepPose" 集合(见 resnet_rhd2d.yml 中的 In Collection: DeepPose 字段)。
二、RHD 测试集上的核心结果
文档正文给出该模型在 RHD 测试集上的量化表现,这是理解配置效果的核心数据,完整继承如下:
| Arch | Input Size | PCK@0.2 | AUC | EPE | ckpt | log |
|---|---|---|---|---|---|---|
| deeppose_resnet_50 | 256x256 | 0.988 | 0.865 | 3.32 | ckpt | log |
对应说明:
- 模型命名
td-reg_res50_8xb64-210e_rhd2d-256x256中,"td-reg" 表示 Top-down 回归(topdown + regression),"8xb64" 表示 8 卡、每卡 batch size 64,"210e" 表示训练 210 个 epoch,"rhd2d" 表示数据集,"256x256" 为输入分辨率。 - 三个评估指标分别衡量定位精度(PCK@0.2)、整体相似度(AUC)与平均像素误差(EPE),其具体含义见本文第六节。其中
AUC还是 训练配置 中 checkpoint 保存的依据(save_best='AUC')。
三、训练配置逐段精解
该模型的完整配置位于 td-reg_res50_8xb64-210e_rhd2d-256x256.py,下面按功能块逐一解析。
3.1 运行期与优化器设置
_base_ = ['../../../_base_/default_runtime.py']
# runtime
train_cfg = dict(max_epochs=210, val_interval=10)
# optimizer
optim_wrapper = dict(optimizer=dict(
type='Adam',
lr=5e-4,
))
- 配置继承自仓库基础的 default_runtime.py,复用其中默认的日志、checkpoint、visualizer 等运行期组件。
- 训练 210 个 epoch,每 10 个 epoch 在验证集上评估一次。
- 优化器使用 Adam,初始学习率 5e-4。手部关键点任务数据规模适中,Adam 配合较低学习率可稳定收敛。
3.2 学习率调度
param_scheduler = [
dict(
type='LinearLR', begin=0, end=500, start_factor=0.001,
by_epoch=False), # warm-up
dict(
type='MultiStepLR',
begin=0,
end=210,
milestones=[170, 200],
gamma=0.1,
by_epoch=True)
]
采用两段式调度:
- 预热(warm-up):前 500 个 iteration(
by_epoch=False表示按 iteration 计),学习率从初始值的 0.1% 线性上升到目标值,避免训练初期不稳定。 - 阶梯下降:在 epoch 170 与 200 处分别将学习率乘以 0.1(gamma),用于收敛后期精细调整。
此外配置了 auto_scale_lr = dict(base_batch_size=512),当实际训练总 batch size 与基准值 512 不同时,MMEngine 会自动按比例缩放学习率。
3.3 关键点编解码器(Codec)设置
# codec settings
codec = dict(type='RegressionLabel', input_size=(256, 256))
MMPose 3.x 将标签生成与结果解码抽象为 Codec 组件。本配置使用 RegressionLabel,其核心逻辑位于 mmpose/codecs/regression_label.py:
- 编码(encode):将关键点坐标除以输入尺寸
[w, h],归一化到[0, 1]区间,得到 shape 为(N, K, D)(N 为实例数、K 为关键点数、D=2 为坐标维度)的keypoint_labels;同时依据坐标是否落在图像范围内且可见性大于 0.5,生成 0/1 的keypoint_weights。 - 解码(decode):将网络输出的归一化坐标乘以
[w, h]还原到原图坐标空间,并返回默认全 1 的置信度分数。
这一"归一化—回归—反归一化"的闭环,是回归范式区别于热图范式(输出空间为热图)的本质特征。
3.4 模型结构
model = dict(
type='TopdownPoseEstimator',
data_preprocessor=dict(
type='PoseDataPreprocessor',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
bgr_to_rgb=True),
backbone=dict(
type='ResNet',
depth=50,
init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50'),
),
neck=dict(type='GlobalAveragePooling'),
head=dict(
type='RegressionHead',
in_channels=2048,
num_joints=21,
loss=dict(type='SmoothL1Loss', use_target_weight=True),
decoder=codec),
test_cfg=dict(
flip_test=True,
flip_mode='heatmap',
shift_heatmap=True,
))
各部分职责:
- TopdownPoseEstimator:Top-down 姿态估计器,接收检测框裁剪出的单实例图像块,输出该实例的关键点。
- PoseDataPreprocessor:图像归一化,mean/std 使用 ImageNet 统计值,
bgr_to_rgb=True匹配 OpenCV 读取的 BGR 输入。 - ResNet-50 主干:加载
torchvision://resnet50预训练权重,在 ImageNet 分类任务上预训练的特征对手部外观具有良好迁移性。 - GlobalAveragePooling 颈部:将
(B, 2048, 8, 8)的特征图池化为(B, 2048)的全局特征向量,为全连接回归做准备。这是回归范式与热图范式的关键差异——热图范式需要保留空间分辨率,回归范式则压缩为全局向量。 - RegressionHead:输入 2048 维特征,输出
num_joints × 2 = 42维坐标。其forward实现极为精简(见 regression_head.py):x = torch.flatten(x, 1); x = self.fc(x); return x.reshape(-1, self.num_joints, 2),即单层全连接nn.Linear(in_channels, num_joints * 2)直接输出归一化坐标。 - SmoothL1Loss:使用带目标权重的平滑 L1 损失,对离群值更鲁棒,
use_target_weight=True表示对不可见关键点的损失进行加权屏蔽。 - flip_test 测试时增强:推理时同时前向原图与水平翻转图,将翻转结果的坐标按
flip_indices映射回原图空间后取平均,提升预测稳定性。
3.5 RHD 数据集定义与 21 点关键点顺序
dataset_type = 'Rhd2DDataset'
data_mode = 'topdown'
data_root = 'data/rhd/'
Rhd2DDataset 实现于 mmpose/datasets/datasets/hand/rhd2d_dataset.py,继承自 BaseCocoStyleDataset,其元信息指向 configs/base/datasets/rhd2d.py。RHD 手部关键点共 21 个,定义如下:
0: 'wrist', 1: 'thumb4', 2: 'thumb3',
3: 'thumb2', 4: 'thumb1', 5: 'forefinger4',
6: 'forefinger3', 7: 'forefinger2', 8: 'forefinger1',
9: 'middle_finger4', 10: 'middle_finger3', 11: 'middle_finger2',
12: 'middle_finger1', 13: 'ring_finger4', 14: 'ring_finger3',
15: 'ring_finger2', 16: 'ring_finger1', 17: 'pinky_finger4',
18: 'pinky_finger3', 19: 'pinky_finger2', 20: 'pinky_finger1'
特别注意事项(元信息中明确标注):RHD 的手指关键点顺序是从指尖到手掌(tip to palm),而 COCO-WholeBody-Hand、FreiHand、CMU Panoptic HandDB 等数据集的顺序恰好相反。若要将 RHD 与其他手部数据集混合训练,必须先统一关键点顺序,否则会导致对应关系错乱。
3.6 数据管线(Pipeline)
训练与验证阶段使用不同管线:
train_pipeline = [
dict(type='LoadImage'),
dict(type='GetBBoxCenterScale'),
dict(type='RandomFlip', direction='horizontal'),
dict(
type='RandomBBoxTransform', rotate_factor=180,
scale_factor=(0.7, 1.3)),
dict(type='TopdownAffine', input_size=codec['input_size']),
dict(type='GenerateTarget', encoder=codec),
dict(type='PackPoseInputs')
]
val_pipeline = [
dict(type='LoadImage'),
dict(type='GetBBoxCenterScale'),
dict(type='TopdownAffine', input_size=codec['input_size']),
dict(type='PackPoseInputs')
]
- 训练增强:水平随机翻转(RandomFlip)+ 旋转与缩放(RandomBBoxTransform,旋转范围 ±180°、尺度因子 0.7~1.3)。±180° 的旋转范围对手部这类可任意旋转的物体非常关键,也是手部任务与人体任务的重要差异。
- TopdownAffine:根据 bbox 中心与尺度将图像仿射变换到
256×256输入尺寸。 - GenerateTarget:调用
encoder=codec(即RegressionLabel)生成归一化坐标标签。 - PackPoseInputs:打包为模型输入格式。
3.7 数据加载器与评估器
训练加载 annotations/rhd_train.json(batch size 64、打乱采样),验证与测试加载 annotations/rhd_test.json(batch size 32、不打乱),均使用 2 个 worker 与 persistent_workers=True。
评估器同时配置三个指标:
val_evaluator = [
dict(type='PCKAccuracy', thr=0.2),
dict(type='AUC'),
dict(type='EPE'),
]
test_evaluator = val_evaluator
四、回归范式在源码中的落点
理解该配置最好的方式是在源码中追踪"回归"的完整链路,MMPose 的测试代码可帮助你验证理解:
- 头(Head):regression_head.py 定义了
RegressionHead,其 docstring 明确说明"该头由全连接层组成,直接预测坐标,源自 DeepPose (Toshev et al., 2014)"。前向过程仅含 flatten + Linear + reshape 三步。 - Codec:regression_label.py 的 encode/decode 实现坐标的归一化与还原,是训练标签与推理输出之间的转换枢纽。
- 测试验证:tests/test_models/test_heads/test_regression_heads/test_regression_head.py 中
test_init断言head.fc.weight.shape == (17 * 2, 1024)(验证全连接层输出维度),test_predict验证keypoints输出与 GT 形状一致,test_tta验证flip_test=True时的测试时增强逻辑——这些单测直接印证了上文对结构与 TTA 行为的分析。 - 标签编码测试:tests/test_codecs/test_regression_label.py 覆盖了
RegressionLabel的归一化编码与解码还原逻辑。
五、训练与测试命令
数据准备:将 RHD 数据集按官方结构放置于 data/rhd/ 下,包含 annotations/rhd_train.json 与 annotations/rhd_test.json(具体下载与转换步骤参考 prepare_datasets.md)。
训练(完整命令语法参见 train_and_test.md):
python tools/train.py configs/hand_2d_keypoint/topdown_regression/rhd2d/td-reg_res50_8xb64-210e_rhd2d-256x256.py
多卡分布式训练:
bash tools/dist_train.sh configs/hand_2d_keypoint/topdown_regression/rhd2d/td-reg_res50_8xb64-210e_rhd2d-256x256.py 8
测试(需传入 checkpoint 路径):
python tools/test.py configs/hand_2d_keypoint/topdown_regression/rhd2d/td-reg_res50_8xb64-210e_rhd2d-256x256.py ${CHECKPOINT_FILE} --work-dir work_dirs/rhd2d
测试将输出 PCK@0.2、AUC、EPE 三项指标,与文档结果表对照即可验证复现效果。
六、三项评估指标解读
| 指标 | 全称 | 含义 | 数值说明 |
|---|---|---|---|
| PCK@0.2 | Percentage of Correct Keypoints | 预测点与 GT 的距离小于阈值(此处为 0.2 × 手部尺寸)的关键点比例 | 0.988 表示约 98.8% 的关键点预测正确 |
| AUC | Area Under Curve | 在归一化误差阈值序列上累积的 PCK 曲线下面积,综合反映不同精度下的表现 | 0.865,越高越好 |
| EPE | End-Point Error | 所有关键点预测坐标与 GT 坐标的平均欧氏距离(像素) | 3.32 像素,越低越好 |
三者从"达标率、整体曲线、绝对误差"三个角度刻画模型精度。由于 checkpoint 保存以 AUC 为准(save_best='AUC', rule='greater'),训练过程中会自动保留验证 AUC 最优的权重。
七、跨数据集迁移:与 OneHand10K 配置对照
同一套回归范式还提供了 OneHand10K 版本 td-reg_res50_8xb64-210e_onehand10k-256x256.py,与 RHD 配置相比仅有三处差异:
dataset_type改为'OneHand10KDataset',data_root改为'data/onehand10k/',标注文件换为onehand10k_train.json/onehand10k_test.json;- 模型、优化器、调度器、管线、评估器完全相同——这正是"配置即模板"的体现;
- 其文档 resnet_onehand10k.md 记录的结果为 PCK@0.2 = 0.990、AUC = 0.485、EPE = 34.21。
两个数据集的 AUC 与 EPE 差异巨大(RHD 为 0.865 / 3.32,OneHand10K 为 0.485 / 34.21),但这不代表模型在 OneHand10K 上更差,而是数据集规模、难度与 bbox 尺寸定义不同所致——这也提醒读者在跨数据集对比指标时必须先对齐评估协议。两者结果汇总见 topdown_regression/README.md 中的 "Results and Models" 章节。
八、总结
resnet_rhd2d.md 看似只是一张结果表,但其背后是一套完整的、可复现的 Top-down 回归式手部关键点方案:
- 范式层面:回归范式(DeepPose 路线)以
RegressionHead的单层全连接直接输出归一化坐标,配以RegressionLabelCodec 完成归一化编码与解码,相比热图范式省去了热图后处理,结构更简洁。 - 配置层面:210 个 epoch、Adam + 5e-4、预热 + 阶梯下降、±180° 旋转增强、SmoothL1Loss、flip_test TTA,共同构成了手部回归任务的一套成熟超参数模板,可无缝迁移到 OneHand10K 等其他手部数据集。
- 验证层面:文档结果表(PCK@0.2 = 0.988、AUC = 0.865、EPE = 3.32)与配置、源码、单测相互印证,保证了从理论到复现的闭环一致性。