【三十四周】文献阅读:DeepPose: 通过深度神经网络实现人类姿态估计
目录
- 摘要
- Abstract
- DeepPose: 通过深度神经网络实现人类姿态估计
- 研究背景
- 创新点
- 方法论
- 归一化
- 网络结构
- 级联细化流程
- 代码实践
- 局限性
- 实验结果
- 总结
摘要
人体姿态估计旨在通过图像定位人体关节,是计算机视觉领域的核心问题之一。传统方法多基于局部检测与图模型,虽在效率上表现优异,但受限于局部特征与有限的关节交互建模,难以应对遮挡、小关节及复杂姿态等挑战。DeepPose提出了一种基于深度神经网络(DNN)的整体回归框架,通过级联DNN结构实现高精度关节定位。首先输入整张图像,通过7层卷积网络直接回归所有关节的归一化坐标。然后基于初始预测,从高分辨率的局部子图像中进一步修正关节位置。实验表明,DeepPose在FLIC、LSP等数据集上超越当时最优方法,尤其在肢体检测精度(PCP)和关节定位准确率(PDJ)上提升显著。其核心创新在于利用DNN的全局上下文建模能力,结合级联机制逐步细化局部细节,实现了无需手工设计特征或复杂图模型的高效姿态估计。
Abstract
Human pose estimation aims to locate human joints in images. It is a key problem in computer vision. Traditional methods often use local detection and graphical models. These methods are efficient but face challenges like occlusion, small joints, and complex poses. This is because they rely on limited local features and joint interaction modeling.DeepPose proposed a holistic regression framework based on deep neural networks (DNNs). It uses a cascaded DNN structure to achieve high-precision joint localization. First, the entire image is input into a 7-layer convolutional network. This network directly predicts normalized coordinates for all joints. Then, based on initial predictions, joint positions are refined using high-resolution local sub-images.Experiments show DeepPose outperforms the best methods at the time on datasets like FLIC and LSP. It improves significantly in limb detection accuracy (Percentage of Correct Parts, PCP) and joint localization accuracy (Percentage of Detected Joints, PDJ). The key innovation is using DNNs to model global context. Combined with a cascading mechanism, it refines local details step by step. This allows efficient pose estimation without manually designed features or complex graphical models.
DeepPose: 通过深度神经网络实现人类姿态估计
Title: DeepPose: Human Pose Estimation via Deep Neural Networks
Author: Alexander Toshev, Christian Szegedy
Source: IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2014
Link:https://openaccess.thecvf.com/content_cvpr_2014/html/Toshev_DeepPose_Human_Pose_2014_CVPR_paper.html
研究背景
人体姿态估计的难点源于人体关节的高度灵活性、局部遮挡、小尺寸关节的模糊性以及复杂背景干扰。早期研究多采用基于部件的模型(如Pictorial Structures),通过树状结构建模关节关系,虽计算高效,但仅能捕捉局部特征,难以建模全局姿态。例如,当右臂被遮挡时,传统方法可能因依赖局部检测器而失效,而人类却能通过整体姿态(如左臂位置或躯干朝向)推断被遮挡关节的位置。
为解决这一问题,部分研究尝试通过全局分类器或最近邻匹配提升全局推理能力,但受限于线性模型的表达能力或数据规模,实际效果有限。与此同时,深度学习在图像分类与目标检测中的突破(如AlexNet)启发了研究者探索其在姿态估计中的应用。
对上图而言,许多关节几乎看不见。但是我们可以猜测左臂在左图中的位置,因为我们看到了姿势的其余部分,并预测了人的运动。同样地,右边的人的左半个身体根本看不见。这些都是需要整体推理的例子。论文作者认为DNN可以自然地提供这种类型的推理。然而,如何将DNN应用于高精度、细粒度的关节定位仍是一个开放问题。DeepPose的提出填补了这一空白,首次将DNN回归与级联细化结合,为姿态估计提供了一种端到端的解决方案。
创新点
- 基于深度神经网络:不需要显式设计零件的特征表示和检测器;不需要显式设计模型拓扑结构和关节之间的相互作用。
- 整体回归框架:摒弃传统局部检测与图模型,直接以整张图像为输入,通过DNN回归所有关节坐标,充分利用全局上下文信息。
- 级联细化机制:通过多阶段DNN逐步修正关节位置,前一阶段的预测用于裁剪高分辨率子图像,使后续网络专注于局部细节,提升定位精度。
- 通用网络架构:沿用AlexNet的7层卷积结构,证明通用DNN无需特定领域设计即可胜任复杂姿态估计任务,简化了模型开发流程。
方法论
归一化
本论文中首先要对输入图像进行归一化。通过归一化,不同尺度的图像(如远距离拍摄的小人体和近距离拍摄的大人体)会被映射到相同的相对坐标系中,使得模型更容易学习关节的相对位置关系。并且归一化后的坐标范围通常在 [0,1] 之间,这使得训练过程更加稳定,避免了因坐标值过大或过小导致的梯度问题。
公式如下所示:
N
(
y
i
;
b
)
=
(
1
/
b
w
0
0
1
/
b
h
)
(
y
i
−
b
c
)
N(\mathbf{y}_i; b) = \begin{pmatrix} 1/b_w & 0 \\ 0 & 1/b_h \end{pmatrix} (\mathbf{y}_i - b_c)
N(yi;b)=(1/bw001/bh)(yi−bc)
其中:
- 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 是边界框的高度。
- N ( y i ; b ) N(\mathbf{y}_i; b) N(yi;b) 是归一化后的关节坐标。
一个简单的例子:
如果边界框的中心是 (100,200),而关节坐标是 (120,220),那么中心化后的坐标是 (20,20)。如果边界框的宽度是 200,高度是 400,那么缩放后的坐标是 (20/200,20/400)=(0.1,0.05)。最终,归一化后的坐标 是一个相对值,表示关节在边界框中的位置比例。例如,(0.1,0.05) 表示关节位于边界框中心右侧 10% 宽度、上方 5% 高度的位置。
网络结构
模型输入为220×220
的归一化图像,经过7层卷积网络(含5个卷积层与2个全连接层)输出2k维向量,对应k个关节的归一化坐标。网络结构借鉴AlexNet,但将分类损失替换为L2回归损失,直接最小化预测与真实坐标的欧氏距离。训练时,通过数据增强生成大量随机裁剪图像,并采用自适应梯度下降优化参数。
级联细化流程
初始阶段:以整张图像或检测框为输入,预测初始关节位置。
细化阶段:以每个关节的预测位置为中心,裁剪σ倍于躯干直径的子图像(如FLIC中σ=1.0,LSP中σ=2.0),输入相同结构的DNN回归位置偏移量。
迭代优化:重复细化过程(通常3个阶段),逐步缩小搜索区域,利用高分辨率子图像提升精度。
例如网络结构图所示,若初始预测的右肘位置偏离真实值,第二阶段网络将聚焦于该区域的高分辨率图像,修正偏移量;第三阶段进一步细化,直至关节坐标收敛。
代码实践
- 骨干网络使用预训练的resnet-50,将最后的全连接层进行修改:
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) # 调用主函数
预测结果如图所示:
局限性
- 计算成本高:训练需分布式集群(100个节点耗时3天),限制了实际部署。
- 依赖初始检测框:若人体检测框不准确(如FLIC依赖面部检测),可能影响后续回归。
- 左右混淆问题:背面拍摄时,模型可能混淆左右关节(如上图最后一列)。
- 分辨率限制:输入尺寸固定为220×220,对极小或密集人群场景适应性不足。
实验结果
LSP数据集:在PCP指标上,DeepPose对上下肢的检测率(如上肢0.56、下肢0.71)显著优于Dantone(0.49)、Johnson(0.58)等方法。PDJ曲线显示,在0.2倍躯干直径阈值下,DeepPose的关节检测率较次优方法提升10%-15%。
FLIC数据集:肘部与手腕的PDJ在0.2阈值下分别达到0.9与0.75,较传统方法提升20%以上。
下图绿色为真实姿态标签,红色分别为几个阶段的预测姿态:
总结
DeepPose作为将深度学习运用到人体姿态估计的开山之作,通过整体回归+级联细化的工作流程,开创了深度学习在人体姿态估计中的新范式。其证明通用卷积网络无需复杂结构调整即可建模全局姿态;通过级联机制将粗粒度推理与细粒度修正解耦,兼顾效率与精度;在多个数据集上实现SOTA性能,尤其在遮挡与小关节场景下表现突出。