【第二十五周】:DeepPose:通过深度神经网络实现人体姿态估计
DeepPose
- 摘要
- Abstract
- 文章信息
- 引言
- DeepPose
- 归一化
- 网络结构
- 初始网络(粗略估计所有关节点位置)
- 精细化级联网络(分别修正每个关节点的位置)
- 疑问与解决
- 代码实践
- 总结
摘要
这篇博客介绍了DeepPose,这是首个基于深度神经网络(DNN)的人体姿态估计框架,其核心思想是通过端到端回归直接预测人体关节坐标,摒弃了传统方法依赖手工特征和图形模型的局限性。针对传统算法在遮挡、复杂背景和小关节定位中的不足,DeepPose提出级联回归的方法:初始阶段利用全局低分辨率图像预测粗略关节点位置,后续级联阶段通过高分辨率局部图像块逐步修正误差,实现从粗到细的优化。为消除尺度差异,DeepPose对输入图像使用了归一化,且对级联网络进行独立训练,每个阶段参数分离以适配不同的输入尺度。该方法在LSP、FLIC等数据集上较传统方法定位误差降低20%-30%,尤其在遮挡场景下鲁棒性显著提升。但级联结构计算成本较高,且依赖初始预测的准确性。未来改进方向可结合热力图回归与级联优化,或引入轻量化网络提升实时性。
Abstract
This blog introduces DeepPose, the first deep neural network (DNN)-based framework for human pose estimation. Its core innovation lies in directly predicting joint coordinates through end-to-end regression, eliminating the reliance on handcrafted features and graphical models inherent in traditional methods. To address the limitations of conventional algorithms in handling occlusions, complex backgrounds, and small joint localization, DeepPose proposes a cascaded regression approach: the initial stage predicts rough joint positions using global low-resolution images, while subsequent cascaded stages iteratively refine errors via high-resolution local image patches, achieving coarse-to-fine optimization. To mitigate scale variance, DeepPose employs input normalization and independently trains cascaded networks with separated parameters to adapt to different input scales. Evaluations on datasets like LSP and FLIC demonstrate a 20%-30% reduction in localization errors compared to traditional methods, with marked robustness improvements in occlusion scenarios. However, the cascaded structure incurs higher computational costs and heavily depends on the accuracy of initial predictions. Future improvements may integrate heatmap regression with cascaded refinement or adopt lightweight networks for real-time performance. This work pioneers a regression-based paradigm for pose estimation, bridging the gap between global context awareness and local precision.
文章信息
Title:DeepPose: Human Pose Estimation via Deep Neural Networks
Author: Alexander Toshev, Christian Szegedy
Source:https://arxiv.org/abs/1312.4659
引言
人体姿态估计问题,被定义为人体关节定位问题。其难点在于人体关节的灵活性、某些关节小而模糊及遮挡问题。如下图,许多关节不可见,而我们可以通过看到的人物姿势和其余可见关节预测不可见的关节,并预测人体姿态。这也说明了全局推理的必要性。
早期的研究基本是基于部件模型(如Pictorial Structures)仅能建模身体局部关系,无法全局推理整体姿态 。例如,遮挡的关节位置需通过其他可见部位推测,但传统模型缺乏这种整体关联能力。为保持推理效率,一些模型采用树状结构拓扑,仅建模相邻关节的约束关系,忽视远距离关节的相互作用(如头与脚的关联)。另外,传统方法多将姿态估计视为分类问题,生成离散的姿态类别,难以实现连续空间中的高精度定位。
2012年AlexNet在ImageNet上的成功验证了深度神经网络(DNN)在图像分类任务中的潜力 。此后,DNN逐步扩展至目标检测(如R-CNN)和定位任务,但姿态估计尚未被充分探索 。
作者利用深度神经网络来进行人体姿态评估,将姿态估计视为一个联合回归问题。通过全卷积网络(如AlexNet)提取整图特征,可隐式学习关节间的空间依赖关系。另外,作者提出级联的方法来对关节点位置进行多次修正以提高精准率。
DeepPose
归一化
人体在图像中的尺度差异(如近景全身像与远景小尺寸人像)会导致关节点绝对坐标的数值范围差异巨大。通过边界框归一化,将关节点坐标转换为相对于人体边界框中心的比例值,可统一不同尺度下的坐标分布,使模型无需适应绝对像素值的变化,专注于学习姿态的空间关系。直接回归关节点绝对坐标需要模型同时学习空间位置和尺度变换,而归一化将问题简化为相对位置回归,归一化后的坐标仅需预测相对偏移量,模型更易收敛。另外,归一化后的数据分布更均匀,梯度下降过程中参数的更新方向更稳定,可有避免止训练时梯度爆炸问题。
用边框b对于图像进行归一化:
其中,
y
i
{\mathbf{y}}_{i}
yi 表示第
i
i
i 个关节在图像中的绝对坐标,具体为
(
x
i
,
y
i
)
(x_i,y_i)
(xi,yi);
b
b
b 是边框信息,定义为
b
=
(
b
c
,
b
w
,
b
h
)
b=(b_c,b_w,b_h)
b=(bc,bw,bh),
b
c
b_c
bc 是边界框的中心坐标
(
b
c
x
,
b
c
y
)
(b_{cx},b_{cy})
(bcx,bcy),
b
w
b_w
bw 和
b
h
b_h
bh 是边框的宽和高。
按照上面的公式可以将关节点的坐标转化为相对坐标,相对坐标在[-0.5,0.5]范围内,实现中也可加上0.5,归一化到[0,1]。
网络结构
作者将姿态估计视为回归问题,并用深度神经网络来预测关节点位置。
上图展示了DeepPose的总体结构:左边是初始阶段,粗略预测全局所有的关节点位置,其中蓝色是卷积层,绿色是全连接层,池化层和归一层没有需要学习的参数,图中没有展示。图中右边是精细化级联网络,其结构与初始阶段的网络完全一致,但每一级的网络参数是独立的,精细化级联网络对上一级预测的关节点位置进行修正,得到更准确的位置。
在初始阶段(Initial Stage),模型通过归一化人体边界框坐标构建尺度不变的输入,利用卷积网络直接回归所有关节点的坐标;在后续级联阶段(Cascade),模型基于初始预测结果对局部图像区域进行精细化裁剪,通过多层级联DNN逐步修正关节点位移量,最终输出精确的关节点位置。
下面分别介绍初始网络和精细化计量网络的具体细节:
初始网络(粗略估计所有关节点位置)
用 C 表示卷积层,用LRN表示局部响应标准化层,用P 表示池化层,用 F 表示全连接层。只有C 层和F 层包含可学习的参数,而其余的是无参数的。
对于 C 层,尺寸定义为宽度×高度×深度,其中前两个维度具有空间意义,而深度定义了滤波器的数量。如果我们将每层的大小写在括号中,那么网络可以简洁地描述为
C
(
55
×
55
×
96
)
−
L
R
N
−
P
−
C
(
27
×
27
×
256
)
−
L
R
N
−
P
−
C
(
13
×
13
×
384
)
−
C
(
13
×
13
×
384
)
−
C
(
13
×
13
×
256
)
−
P
−
F
(
4096
)
−
F
(
4096
)
C\left( {{55} \times {55} \times {96}}\right) - {LRN} - P - C\left( {{27} \times {27} \times {256}}\right) -{LRN} - P - C\left( {{13} \times {13} \times {384}}\right) - C\left( {{13} \times {13} \times {384}}\right) -C\left( {{13} \times {13} \times {256}}\right) - P - F\left( {4096}\right) - F\left( {4096}\right)
C(55×55×96)−LRN−P−C(27×27×256)−LRN−P−C(13×13×384)−C(13×13×384)−C(13×13×256)−P−F(4096)−F(4096)
前两个C层的滤波器大小为
11
×
11
11\times 11
11×11 和
5
×
5
5\times 5
5×5,其余三个层的大小为
3
×
3
3\times 3
3×3。
池化操作在三层之后应用,尽管分辨率降低,但有助于提高性能。
网络的输入是
220
×
220
220\times 220
220×220的图像,通过步幅为4的方式输入网络,记人体关键点个数为
k
k
k,则初始网络的输出为
2
k
2k
2k 维向量。
网络的结构可以替换成其他网络,文章用的是AlexNet,还可替换成resent等其他网络。
损失函数:
不想AlexNet那样使用分类损失,而是在最后一层网络层上训练线性回归,通过最小化预测值与真实姿态向量之间的
L
2
L_2
L2 距离来预测姿态向量。
先将训练集
D
D
D 进行归一化:
最优网络参数的
L
2
L_2
L2 损失函数为:
为了清晰起见,公式写出对各个关节的优化。需要注意的是,即使某些图像中并非所有关节都被标注(存在遮挡或缺失时),上述目标函数仍然可以使用,只是求和中的相应项将被省略。
精细化级联网络(分别修正每个关节点的位置)
上一小节的初始网络是以整个图像作为输入,要预测图像中所有的关节点(单人)。但此网络在观察细节方面的能力有限——它学习的是在粗尺度上捕捉姿态属性的滤波器,这些滤波器不足以精确定位人体关节。为了实现更高的精度,作者提出级级联的姿态回归器。
在第一阶段,级联从估计初始姿态开始,如上一节所述。在后续阶段,额外的深度神经网络(DNN)回归器被训练来预测关节位置从前一阶段到真实位置的位移。因此,每个后续阶段可以被视为对当前预测姿态的细化。
每个后续阶段都以上一阶段预测出或修正后的关节位置为中心从原始高分辨率图像中裁剪出子图像,并在此子图像上应用该关节的姿态位移回归器。通过这种方式,后续的姿态回归器可以看到更高分辨率的图像,从而学习到更精细尺度的特征,最终实现更高的精度。
级联的所有阶段的网络结构都完全相同,但学习的参数不同。
对于总共
S
S
S个级联阶段(包括初始阶段)中的第
s
∈
{
1
,
…
,
S
}
s \in \{ 1,\ldots ,S\}
s∈{1,…,S}阶段,用
θ
s
{\theta }_{s}
θs表示学习到的网络参数。姿态位移回归器读取
ψ
(
x
;
θ
s
)
\psi \left( {x;{\theta }_{s}}\right)
ψ(x;θs)。为了细化给定的关节位置
y
i
{\mathbf{y}}_{i}
yi ,考虑一个关节边界框
b
i
{b}_{i}
bi,该边界框捕获围绕
y
i
:
b
i
(
y
;
σ
)
=
(
y
i
,
σ
diam
(
y
)
,
σ
diam
(
y
)
)
{\mathbf{y}}_{i} : {b}_{i}\left( {\mathbf{y};\sigma }\right) = \left( {{\mathbf{y}}_{i},\sigma \operatorname{diam}\left( \mathbf{y}\right) ,\sigma \operatorname{diam}\left( \mathbf{y}\right) }\right)
yi:bi(y;σ)=(yi,σdiam(y),σdiam(y))的子图像,其中心为第
i
i
i个关节,尺寸为姿态直径按
σ
\sigma
σ缩放。姿态的直径
diam
(
y
)
\operatorname{diam}\left( \mathbf{y}\right)
diam(y)定义为人体躯干上相对关节之间的距离,例如左肩和右髋,具体取决于具体的姿态定义和数据集。
那么,在阶段
s
=
1
s = 1
s=1中,从边界框
b
0
{b}^{0}
b0开始,该边界框要么包围整个图像,要么是通过人体检测器获得的,则初始姿态为:
在每个后续阶段
s
≥
2
s \geq 2
s≥2中,对于所有关节
i
∈
{
1
,
…
,
k
}
i \in \{ 1,\ldots ,k\}
i∈{1,…,k},首先通过在前一阶段(s - 1)定义的子图像
b
i
(
s
−
1
)
{b}_{i}^{\left( s - 1\right) }
bi(s−1)上应用回归器,回归到一个细化位移
y
i
s
−
y
i
(
s
−
1
)
{\mathbf{y}}_{i}^{s} -{\mathbf{y}}_{i}^{\left( s - 1\right) }
yis−yi(s−1)。然后,估计新的关节框
b
i
s
{b}_{i}^{s}
bis:
在
s
≥
2
s \geq 2
s≥2 阶段中,训练方式与初始阶段基本相同,只是每个关节点使用不同的边界框来进行归一化。
疑问与解决
Q:初始阶段是对所有关节点进行粗略的预测,输出是
2
k
2k
2k维向量,而后续的级联精细化阶段只需要
2
2
2维向量表示对关节点的修正信息,那为什么初始阶段和后续级联阶段的网络结构完全一致?
A:初始阶段和后续级联阶段所需的输出的维度不一样,级联阶段网络仍输出
2
k
2k
2k维向量,但仅使用当前处理的关节点的修正量(Δx, Δy),其他维度被忽略。这种设计通过动态选择输出通道实现功能适配。例如,处理右肘关节点,级联阶段仅提取输出向量中对应右肘的Δx和Δy,其余维度不参与计算。训练时,每个级联阶段仅对当前处理的关键点的两个输出维度(Δx, Δy)计算损失,其他维度在损失函数中被赋予权重0,从而避免无关维度的干扰
代码实践
在实现中,骨干网络是利用图像分类网络,如AlexNet、ResNet等,下面是以ResNet为骨干网络进行实现的DeepPose:
- 骨干网络:ResNet,最后一层全连接层改动,使其输出 2 k 2k 2k维向量
def create_deep_pose_model(num_keypoints: int) -> nn.Module:
# 加载预训练的ResNet-50模型,使用ImageNet数据集上的预训练权重
res50 = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# 获取原始ResNet-50模型中全连接层的输入特征数
in_features = res50.fc.in_features
# 替换原有的全连接层为一个新的线性层,输出大小为num_keypoints*2,
# 因为每个关键点需要两个坐标值(x,y)
res50.fc = nn.Linear(in_features=in_features, out_features=num_keypoints * 2)
return res50 # 返回修改后的模型
- 训练
import os
import torch
import torch.amp
from torch.utils.data import DataLoader # 数据加载器
from torch.utils.tensorboard import SummaryWriter # TensorBoard写入器,用于记录训练过程
import transforms # 自定义的数据变换模块
from model import create_deep_pose_model # 自定义函数,用于创建DeepPose模型
from datasets import WFLWDataset # 自定义数据集类
from train_utils.train_eval_utils import train_one_epoch, evaluate # 训练和评估的实用函数
# 定义获取命令行参数的解析器函数
def get_args_parser(add_help=True):
import argparse # 命令行选项、参数和子命令解析器
parser = argparse.ArgumentParser(description="PyTorch DeepPose Training", add_help=add_help) # 创建解析器实例
parser.add_argument("--dataset_dir", type=str, default="/home/wz/datasets/WFLW", help="WFLW dataset directory") # 数据集目录
parser.add_argument("--device", type=str, default="cuda:0", help="training device, e.g. cpu, cuda:0") # 训练设备
parser.add_argument("--save_weights_dir", type=str, default="./weights", help="save dir for model weights") # 模型权重保存目录
parser.add_argument("--save_freq", type=int, default=10, help="save frequency for weights and generated imgs") # 权重保存频率
parser.add_argument("--eval_freq", type=int, default=5, help="evaluate frequency") # 评估频率
parser.add_argument('--img_hw', default=[256, 256], nargs='+', type=int, help='training image size[h, w]') # 训练图像尺寸
parser.add_argument("--epochs", type=int, default=210, help="number of epochs of training") # 训练周期数
parser.add_argument("--batch_size", type=int, default=32, help="size of the batches") # 批次大小
parser.add_argument("--num_workers", type=int, default=8, help="number of workers, default: 8") # 数据加载线程数
parser.add_argument("--num_keypoints", type=int, default=98, help="number of keypoints") # 关键点数量
parser.add_argument("--lr", type=float, default=5e-4, help="Adam: learning rate") # 学习率
parser.add_argument('--lr_steps', default=[170, 200], nargs='+', type=int, help='decrease lr every step-size epochs') # 学习率衰减步骤
parser.add_argument("--warmup_epoch", type=int, default=10, help="number of warmup epoch for training") # 预热轮数
parser.add_argument('--resume', default='', type=str, help='resume from checkpoint') # 从检查点恢复训练
parser.add_argument('--test_only', action="store_true", help='Only test the model') # 仅测试模式
return parser
# 主函数
def main(args):
torch.manual_seed(1234) # 固定随机种子以确保结果可复现
dataset_dir = args.dataset_dir # 数据集目录
save_weights_dir = args.save_weights_dir # 权重保存目录
save_freq = args.save_freq # 权重保存频率
eval_freq = args.eval_freq # 评估频率
num_keypoints = args.num_keypoints # 关键点数量
num_workers = args.num_workers # 数据加载线程数
epochs = args.epochs # 训练周期数
bs = args.batch_size # 批次大小
start_epoch = 0 # 初始训练轮次
img_hw = args.img_hw # 图像尺寸
os.makedirs(save_weights_dir, exist_ok=True) # 创建保存权重的目录
if "cuda" in args.device and not torch.cuda.is_available(): # 如果指定使用GPU但不可用,则回退到CPU
device = torch.device("cpu")
else:
device = torch.device(args.device)
print(f"using device: {device} for training.") # 打印使用的设备信息
# tensorboard writer,用于记录训练过程中的信息
tb_writer = SummaryWriter()
# 创建DeepPose模型实例,并将其移动到指定设备
model = create_deep_pose_model(num_keypoints)
model.to(device)
# 配置数据集和数据加载器
data_transform = {
"train": transforms.Compose([ # 训练数据变换
transforms.AffineTransform(scale_factor=(0.65, 1.35), rotate=45, shift_factor=0.15, fixed_size=img_hw),
transforms.RandomHorizontalFlip(0.5),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]),
"val": transforms.Compose([ # 验证数据变换
transforms.AffineTransform(scale_prob=0., rotate_prob=0., shift_prob=0., fixed_size=img_hw),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
}
train_dataset = WFLWDataset(root=dataset_dir, train=True, transforms=data_transform["train"]) # 创建训练数据集
val_dataset = WFLWDataset(root=dataset_dir, train=False, transforms=data_transform["val"]) # 创建验证数据集
train_loader = DataLoader(train_dataset, batch_size=bs, shuffle=True, pin_memory=True, num_workers=num_workers, collate_fn=WFLWDataset.collate_fn, persistent_workers=True) # 创建训练数据加载器
val_loader = DataLoader(val_dataset, batch_size=bs, shuffle=False, pin_memory=True, num_workers=num_workers, collate_fn=WFLWDataset.collate_fn, persistent_workers=True) # 创建验证数据加载器
# 定义优化器
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
# 定义学习率调度器
warmup_scheduler = torch.optim.lr_scheduler.LinearLR(optimizer=optimizer, start_factor=0.01, end_factor=1.0, total_iters=len(train_loader) * args.warmup_epoch)
multi_step_scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer=optimizer, milestones=[len(train_loader) * i for i in args.lr_steps], gamma=0.1)
lr_scheduler = torch.optim.lr_scheduler.ChainedScheduler([warmup_scheduler, multi_step_scheduler])
if args.resume: # 如果有恢复训练的检查点
assert os.path.exists(args.resume)
checkpoint = torch.load(args.resume, map_location='cpu')
model.load_state_dict(checkpoint['model'])
optimizer.load_state_dict(checkpoint['optimizer'])
lr_scheduler.load_state_dict(checkpoint['lr_scheduler'])
start_epoch = checkpoint['epoch'] + 1
print("the training process from epoch{}...".format(start_epoch))
if args.test_only: # 如果仅进行测试
evaluate(model=model, epoch=start_epoch, val_loader=val_loader, device=device, tb_writer=tb_writer, affine_points_torch_func=transforms.affine_points_torch, num_keypoints=num_keypoints, img_hw=img_hw)
return
for epoch in range(start_epoch, epochs): # 开始训练循环
# 训练一个周期
train_one_epoch(model=model, epoch=epoch, train_loader=train_loader, device=device, optimizer=optimizer, lr_scheduler=lr_scheduler, tb_writer=tb_writer, num_keypoints=num_keypoints, img_hw=img_hw)
# 每隔一定周期进行评估
if epoch % eval_freq == 0 or epoch == args.epochs - 1:
evaluate(model=model, epoch=epoch, val_loader=val_loader, device=device, tb_writer=tb_writer, affine_points_torch_func=transforms.affine_points_torch, num_keypoints=num_keypoints, img_hw=img_hw)
# 按照设定的频率保存模型权重
if epoch % save_freq == 0 or epoch == args.epochs - 1:
save_files = {'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'lr_scheduler': lr_scheduler.state_dict(), 'epoch': epoch}
torch.save(save_files, os.path.join(save_weights_dir, f"model_weights_{epoch}.pth"))
if __name__ == '__main__':
args = get_args_parser().parse_args() # 解析命令行参数
main(args) # 调用主函数
测试人脸检测:
预测结果:
总结
DeepPose作为首个将深度神经网络引入人体姿态估计的经典框架,通过级联回归结构实现了单人关键点的高效检测。其核心流程分为两个阶段:在初始阶段(Initial Stage),模型通过归一化人体边界框坐标构建尺度不变的输入,利用卷积网络直接回归输入图像中所有关节点的位置;在后续级联阶段(Cascade),模型基于上一阶段预测结果对局部图像区域进行精细化裁剪,通过多层级联DNN逐步修正关节点偏移量,最终输出精确的关节点位置。这种端到端的回归方法简化了传统流程,但存在对局部上下文细节捕捉不足的局限性,且依赖高分辨率输入导致计算成本较高。未来研究可结合多尺度特征融合提升遮挡场景下的鲁棒性,或引入轻量化设计优化实时性,同时探索与多模态数据(如骨骼几何关系)的协同建模以突破单帧图像的空间约束。