人体关键点检测——网络解读:Real-time 2D Multi-Person Pose Estimation on CPU: Lightweight OpenPose

论文原文链接
论文源码地址

CPU上的实时2D多人姿势估计:轻量级OpenPose

原始OpenPose分析

与所有自下而上的方法类似,OpenPose 由两部分组成:

  • 神经网络推理提供两个张量:关键点热图及其成对关系(部分关联区域,pafs)。此输出被下采样 8 次。
  • 按人的实例分割结果对关键点进行分组。 它包括将张量上采样到原始图像大小、热力图峰值的关键点提取以及它们按实例分组。

OpenPose流程
网络首先提取特征,然后执行热力图和 pafs 的初始估计,之后执行 5 个细化阶段。它能够找到 18 种类型的关键点。然后分组程序从预定义的关键点对列表中搜索每个关键点的最佳对(按相关性),例如:左肘和左手腕,右臀部和右膝盖,左眼和左耳,等等,总共19对。流程如图 1 所示。在推理过程中,输入图像的大小被调整以匹配网络输入大小的高度,宽度被缩放以保持图像纵横比,然后填充为 8 的倍数。

优化部分

Refinement Stage的更改

COCO 验证集上 OpenPose 的准确性与复杂性

如表可见,后期阶段对 GFLOP 的改进较少,因此对于优化版本,我们将仅保留前两个阶段:初始阶段和单个细化阶段。

具体到代码中:

class PoseEstimationWithMobileNet(nn.Module):
    def __init__(self, num_refinement_stages=1, num_channels=128, num_heatmaps=19, num_pafs=38):

在初始化的时候,直接将堆叠次数num_refinement_stages = 1,具体后面会讲。

Backbone的更改

文章评估了 MobileNet 系列的网络以替换 VGG 特征提取器,并从 MobileNet v1开始。

以一种天真的方式,如果我们将所有层保持到最深,以匹配输出张量分辨率,则会导致精度显着下降。 这可能是由于浅层和弱特征表示。为了节省空间分辨率和重用主干权重,我们使用空洞卷积。

轻量级骨干选择研究(初始和细化阶段具有原始 OpenPose 设计)

轻量化RefinementStage

为了产生对关键点热图和 pafs的新估计,细化阶段从主干中获取特征,并与之前对关键点热图和 pafs 的估计相连接。 受这一事实的启发,我们决定在热图和 pafs 之间共享大部分计算,并在初始和细化阶段使用单个预测分支。 我们共享除了最后两个层之外的所有层,它们直接产生关键点热图和 pafs。

初始阶段的原始两个预测分支和建议的单个预测分支。
在代码中的InitialStage体现:

InitialStage
可以看到在trunk中共享了三层Conv2d卷积bn加relu层。

7*7卷积的替换

每个具有 7×7 内核大小的卷积都被一个具有相同感受野的卷积块替换,以捕获远程空间依赖关系。我们用这个块设计进行了一系列实验,并观察到有1×1、3×3和3×3内核大小的三个连续卷积就足够了,后者的膨胀参数等于2,以保留初始感受野。因为网络变得更深,我们为每个这样的块添加了残差连接。

在细化阶段设计用于替换卷积的卷积块,具有 7x7 内核大小
在代码中具体体现为:

RefinementStageBlock
使用深度可分离卷积的复杂度比使用 7×7 内核的卷积低 2.5 倍。

代码加原理讲解

整体结构

PoseEstimationWithMobileNet

原图3 * 368 * 368输入进来,首先通过MobileNet结构与Cpm结构获得backbonefeatures,然后通过InitialStage获得heatmaps和pafs。然后backbonefeatures与heatmaps和pafs共同cat拼接,通道数相加,送入RefinementStage中(默认为1),得到heatmaps和pafs。如果指定了堆叠次数,那么backbonefeatures要分别与heatmaps和pafs进行堆叠然后重复细化阶段的操作。

卷积部分

卷积部分
注意,conv_dw_no_bn使用的是Elu激活函数,关于Elu激活函数可以查看这篇博文

from torch import nn
def conv(in_channels, out_channels, kernel_size=3, padding=1, bn=True, dilation=1, stride=1, relu=True, bias=True):
    modules = [nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias)]
    if bn:
        modules.append(nn.BatchNorm2d(out_channels))
    if relu:
        modules.append(nn.ReLU(inplace=True))
    return nn.Sequential(*modules)

def conv_dw(in_channels, out_channels, kernel_size=3, padding=1, stride=1, dilation=1):
    return nn.Sequential(
        nn.Conv2d(in_channels, in_channels, kernel_size, stride, padding, dilation=dilation, groups=in_channels, bias=False),
        nn.BatchNorm2d(in_channels),
        nn.ReLU(inplace=True),

        nn.Conv2d(in_channels, out_channels, 1, 1, 0, bias=False),
        nn.BatchNorm2d(out_channels),
        nn.ReLU(inplace=True),
    )

def conv_dw_no_bn(in_channels, out_channels, kernel_size=3, padding=1, stride=1, dilation=1):
    return nn.Sequential(
        nn.Conv2d(in_channels, in_channels, kernel_size, stride, padding, dilation=dilation, groups=in_channels, bias=False),
        nn.ELU(inplace=True),

        nn.Conv2d(in_channels, out_channels, 1, 1, 0, bias=False),
        nn.ELU(inplace=True),
    )

Cpm部分

Cpm

Cpm部分使用了残差结构,首先经过1 * 1的卷积调整通道数,然后使用三组dw卷积进行特征提取,最后通过3 * 3的卷积整合特征。具体代码如下:

class Cpm(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.align = conv(in_channels, out_channels, kernel_size=1, padding=0, bn=False)
        self.trunk = nn.Sequential(
            conv_dw_no_bn(out_channels, out_channels),
            conv_dw_no_bn(out_channels, out_channels),
            conv_dw_no_bn(out_channels, out_channels)
        )
        self.conv = conv(out_channels, out_channels, bn=False)

    def forward(self, x):
        x = self.align(x)
        x = self.conv(x + self.trunk(x))
        return x

InitialStage部分


conv*3的部分使用了论文中所说的共享参数,之后接两个输出,分别使用了两层卷积,hidden_channels=512,得到heatmaps以及pafs特征图。具体代码如下:

class InitialStage(nn.Module):
    def __init__(self, num_channels, num_heatmaps, num_pafs):
        super().__init__()
        self.trunk = nn.Sequential(
            conv(num_channels, num_channels, bn=False),
            conv(num_channels, num_channels, bn=False),
            conv(num_channels, num_channels, bn=False)
        )
        self.heatmaps = nn.Sequential(
            conv(num_channels, 512, kernel_size=1, padding=0, bn=False),
            conv(512, num_heatmaps, kernel_size=1, padding=0, bn=False, relu=False)
        )
        self.pafs = nn.Sequential(
            conv(num_channels, 512, kernel_size=1, padding=0, bn=False),
            conv(512, num_pafs, kernel_size=1, padding=0, bn=False, relu=False)
        )

    def forward(self, x):
        trunk_features = self.trunk(x)
        heatmaps = self.heatmaps(trunk_features)
        pafs = self.pafs(trunk_features)
        return [heatmaps, pafs]

RefinementStage部分

RefinementStageBlock部分

RefinementStageBlock
使用了1 * 1, 两层3 * 3卷积来代替7 * 7卷积,好处是减少了参数量,但同时增加了网络的深度,运行时容易报显存不足的问题。注意:第二层卷积的dilation=2,增大了空洞卷积的感受野。

class RefinementStageBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.initial = conv(in_channels, out_channels, kernel_size=1, padding=0, bn=False)
        self.trunk = nn.Sequential(
            conv(out_channels, out_channels),
            conv(out_channels, out_channels, dilation=2, padding=2)
        )

    def forward(self, x):
        initial_features = self.initial(x)
        trunk_features = self.trunk(initial_features)
        return initial_features + trunk_features

RefinementStage部分

RefinementStage
重复堆叠block五次进行特征提取,同样使用两层卷积来生成heatmaps和pafs,这里图中没有画。

class RefinementStage(nn.Module):
    def __init__(self, in_channels, out_channels, num_heatmaps, num_pafs):
        super().__init__()
        self.trunk = nn.Sequential(
            RefinementStageBlock(in_channels, out_channels),
            RefinementStageBlock(out_channels, out_channels),
            RefinementStageBlock(out_channels, out_channels),
            RefinementStageBlock(out_channels, out_channels),
            RefinementStageBlock(out_channels, out_channels)
        )
        self.heatmaps = nn.Sequential(
            conv(out_channels, out_channels, kernel_size=1, padding=0, bn=False),
            conv(out_channels, num_heatmaps, kernel_size=1, padding=0, bn=False, relu=False)
        )
        self.pafs = nn.Sequential(
            conv(out_channels, out_channels, kernel_size=1, padding=0, bn=False),
            conv(out_channels, num_pafs, kernel_size=1, padding=0, bn=False, relu=False)
        )

    def forward(self, x):
        trunk_features = self.trunk(x)
        heatmaps = self.heatmaps(trunk_features)
        pafs = self.pafs(trunk_features)
        return [heatmaps, pafs]

PoseEstimationWithMobileNet部分

PoseEstimationWithMobileNet
MobileNetV1部分如下代码所示:

self.model = nn.Sequential(
            conv(     3,  32, stride=2, bias=False),
            conv_dw( 32,  64),
            conv_dw( 64, 128, stride=2),
            conv_dw(128, 128),
            conv_dw(128, 256, stride=2),
            conv_dw(256, 256),
            conv_dw(256, 512),  # conv4_2
            conv_dw(512, 512, dilation=2, padding=2),
            conv_dw(512, 512),
            conv_dw(512, 512),
            conv_dw(512, 512),
            conv_dw(512, 512)   # conv5_5
        )

具体堆叠细则之前已经讲过,这里直接放上代码:

class PoseEstimationWithMobileNet(nn.Module):
    def __init__(self, num_refinement_stages=1, num_channels=128, num_heatmaps=19, num_pafs=38):
        super().__init__()
        self.model = nn.Sequential(
            conv(     3,  32, stride=2, bias=False),
            conv_dw( 32,  64),
            conv_dw( 64, 128, stride=2),
            conv_dw(128, 128),
            conv_dw(128, 256, stride=2),
            conv_dw(256, 256),
            conv_dw(256, 512),  # conv4_2
            conv_dw(512, 512, dilation=2, padding=2),
            conv_dw(512, 512),
            conv_dw(512, 512),
            conv_dw(512, 512),
            conv_dw(512, 512)   # conv5_5
        )
        self.cpm = Cpm(512, num_channels)

        self.initial_stage = InitialStage(num_channels, num_heatmaps, num_pafs)
        self.refinement_stages = nn.ModuleList()
        for idx in range(num_refinement_stages):
            self.refinement_stages.append(RefinementStage(num_channels + num_heatmaps + num_pafs, num_channels,
                                                          num_heatmaps, num_pafs))

    def forward(self, x):
        backbone_features = self.model(x)
        backbone_features = self.cpm(backbone_features)

        stages_output = self.initial_stage(backbone_features)
        for refinement_stage in self.refinement_stages:
            stages_output.extend(
                refinement_stage(torch.cat([backbone_features, stages_output[-2], stages_output[-1]], dim=1)))

        return stages_output

注意num_refinement_stages=1的初始值,修改此值加深堆叠深度。

网络解读部分就到此结束啦!完结撒花!

附上效果图:

检测结果

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
上一篇 2022年5月18日 下午12:39
下一篇 2022年5月18日 下午12:45

相关推荐

本站注重文章个人版权,不会主动收集付费或者带有商业版权的文章,如果出现侵权情况只可能是作者后期更改了版权声明,如果出现这种情况请主动联系我们,我们看到会在第一时间删除!本站专注于人工智能高质量优质文章收集,方便各位学者快速找到学习资源,本站收集的文章都会附上文章出处,如果不愿意分享到本平台,我们会第一时间删除!