FAMI-Pose:Temporal Feature Alignment and Mutual Information Maximization for Video-Based Human Pose

本文提出了FAMI-Pose,一种解决多帧人体姿态估计中特征对齐问题的方法。通过全局变换和局部校准模块改善DCPose在快速运动和遮挡场景中的性能。FAMI-Pose利用互信息损失函数强化特征对齐,提高了姿态估计的准确性,并在多个基准数据集上取得最佳结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  要解析的文章题目是:Temporal Feature Alignment and Mutual Information Maximization for Video-Based Human Pose Estimation
  这是一篇2022年CVPR的人体姿态估计方向的论文,是之前DCPose的后续。作者这个团队从21年到23年连着三年CVPR,都是人体姿态估计和跟踪方向的。不得不说强的一批。由于项目等原因好久没看姿态跟踪的论文了,今年的代码还没出来,先看看去年的。

以前方法的问题

  作者21年提出的DCPose在快速运动和姿势遮挡等挑战性场景中表现不佳,如图所示,DCPose在遮挡场景中无法识别被遮挡人的右脚踝,导致结果不准确。下面的一个快速运动场景中,由于运动导致的帧间模糊,DCPose难以识别左手腕。作者认为现有方法往往直接聚合来自相邻帧的不对齐上下文导致了该问题,这些空间错位的特征可能会降低模型的性能。此外,现有方法仅使用均方误差(MSE)损失来监督热图的学习,缺乏有效的约束来保证从相邻帧获得信息增益以及在中间特征级别进行监督。
DCPose和FAMI-Pose

简介

  本文提出了一种新的方法,名为FAMI-Pose,用于解决多帧人体姿态估计中的特征对齐问题。该方法通过在中间特征级别进行监督,利用相邻帧中的信息来增强关键帧特征,从而提高姿态估计的准确性,还引入了一种新的信息论损失函数,叫互信息(Mutual Information),用于约束从相邻帧中提取的信息,以提高特征对齐的效果。实验结果表明,该方法在三个广泛使用的基准数据集上均取得了最先进的结果,并在PoseTrack2017数据集上排名第一。

方法

  FAMI-Pose方法通过两个关键组件来实现特征对齐:全局变换模块(Global Transformation)和局部校准模块(Local Calibration)。如图所示,FAMI-Pose首先使用全局变换模块对相邻帧的特征进行粗略的对齐,以解决空间偏移或抖动的问题。然后,使用局部校准模块对相邻帧的特征进行细化的对齐,以进一步提高特征对齐的准确性。在这个过程中,FAMI-Pose使用了互信息损失函数来约束从相邻帧中提取的信息,以提高特征对齐的效果。通过这些组件的协同作用,FAMI-Pose能够有效地实现多帧人体姿态估计中的特征对齐。
FAMI-Pose结构图

全局变换模块

  全局变换模块是FAMI-Pose方法中的一个组件,实现粗略的特征对齐。如图所示,在上述结构图的前面一部分,该模块通过学习一个仿射变换的参数来计算相邻帧之间的空间变换参数,使得帧特征可以粗略地对齐。该模块包括两个子模块:1)空间变换参数的估计网络,用于从输入特征对中估计仿射变换参数;2)全局仿射变换,用于对支持帧特征进行初步对齐。这部分的代码如下所示。这部分代码在posetimation\zoo\Alignment下面的Alignment_V15.py

 def forward(self, kf_x, sup_x, **kwargs):
	#iter_step = kwargs.get("iter_step")
	batch_size, num_sup = kf_x.shape[0], sup_x.shape[1] // 3
	
	sup_x = torch.cat(torch.chunk(sup_x, num_sup, dim=1), dim=0)
	
	x = torch.cat([kf_x, sup_x], dim=0)
	x_bb_hm, x_bb_feat = self.hrnet(x)
	x_bb_hm_list = torch.chunk(x_bb_hm, num_sup + 1, dim=0)
	x_bb_feat_list = torch.chunk(x_bb_feat[0], num_sup + 1, dim=0)
	
	kf_bb_hm, kf_bb_feat = x_bb_hm_list[0], x_bb_feat_list[0]
	sup_bb_hm_list, sup_bb_feat_list = x_bb_hm_list[1:], x_bb_feat_list[1:]
	
	aligned_sup_feat_list = []
	B, _, H, W = kf_bb_hm.shape
	
	for i in range(num_sup):
	    sup_bb_hm, sup_bb_feat = sup_bb_hm_list[i], sup_bb_feat_list[i]
	    feat_offset = self.feat_global_offset_layers(sup_bb_feat - kf_bb_feat)  # [B,2]
	    offset_params = torch.eye(3)[0:2].view(1, 2, 3).repeat(B, 1, 1).to(sup_bb_feat.device)  # [1,2,3]
	    offset_params[:, 0, 2], offset_params[:, 1, 2] = feat_offset[:, 0], feat_offset[:, 1]
	    global_aligned_feat = kornia.geometry.warp_affine(sup_bb_feat, offset_params, dsize=(H, W))
	
	    aligned_sup_feat_list.append(global_aligned_feat)
	    #...到这里就是

  首先是将图像输入到hrnet中,这里的hrnet是改过的,输出的是最后的热图和倒数第二层的中间特征图,也就是x_bb_hm和x_bb_feat,注意训练的时候hrnet的参数需要冻结。然后将当前帧和前后支持帧分开,num_sup就是支持帧数量。feat_global_offset_layers函数则是用于学习空间变换参数的估计网络,代码如下所示。ChainOfBasicBlocks是一个或多个基础块组成,这里的基础块是两个3*3卷积,也就是resnet18中的基础块。

self.feat_global_offset_layers = nn.Sequential(
    ChainOfBasicBlocks(48, 16, num_blocks=1),
    conv_bn_relu(16, 16, 3, 2, 1, 1),  # [96,72] -> [48,36]
    conv_bn_relu(16, 16, 3, 2, 1, 1),  # [48,36] -> [24,18]
    conv_bn_relu(16, 16, 3, 2, 1, 1),  # [24,18] -> [12,9]
    conv_bn_relu(16, 16, 3, 2, 1, 1),  # [12,9] -> [12,9]
    conv_bn_relu(16, 16, 3, 2, 1, 1),  # [24,18] -> [12,9]
    nn.Flatten(),
    nn.Linear(16 * 3 * 3, 64),
    nn.Linear(64, 64),
    nn.Linear(64, 2),
)
class ChainOfBasicBlocks(nn.Module):
    def __init__(self, input_channel, ouput_channel, kernel_height=None, kernel_width=None, dilation=None, num_blocks=1, groups=1, skip_norm=False, act='ReLU'):
        # def __init__(self, input_channel, ouput_channel, kernel_height, kernel_width, dilation, num_blocks, groups=1):
        super(ChainOfBasicBlocks, self).__init__()
        stride = 1
        if skip_norm:
            downsample = nn.Sequential(nn.Conv2d(input_channel, ouput_channel, kernel_size=1, stride=stride, bias=False, groups=groups))
        else:
            downsample = nn.Sequential(nn.Conv2d(input_channel, ouput_channel, kernel_size=1, stride=stride, bias=False, groups=groups),
                                       nn.BatchNorm2d(ouput_channel, momentum=BN_MOMENTUM))
        layers = []
        layers.append(BasicBlock(input_channel, ouput_channel, stride, downsample, groups, skip_norm=skip_norm, act=act))

        for i in range(1, num_blocks):
            layers.append(BasicBlock(ouput_channel, ouput_channel, stride, downsample=None, groups=groups, skip_norm=skip_norm, act=act))

        # return nn.Sequential(*layers)
        self.layers = nn.Sequential(*layers)

    def forward(self, input):
        return self.layers(input)

  feat_global_offset_layers 由ChainOfBasicBlocks和5个下采样卷积层组成,通过flatten函数展平,经过三个全连接层得到通道数为2的输出,即空间变换的参数。根据文中所述,空间变换的参数是一个2*3的参数矩阵形式,如下图所示。、
空间变换参数
但是代码中生成参数只有两个,而且根据下面的代码,这两个参数对应的是第三列的两个数。

offset_params = torch.eye(3)[0:2].view(1, 2, 3).repeat(B, 1, 1).to(sup_bb_feat.device)  # [1,2,3]
offset_params[:, 0, 2], offset_params[:, 1, 2] = feat_offset[:, 0], feat_offset[:, 1]

开始生成的offset_params是[[1, 0, 0],[0, 1, 0]],然后将网络估计得到的参数赋给第三列。也就是说代码实现时默认只有平移变换,因此只用估计第三列的参数。

局部校准模块

  局部校准模块在全局变换模块的基础上进行细化的特征对齐。如上述特征图结构的右边所示。局部校准模块通过学习像素级的时空信息来对相邻帧的特征进行细化的对齐,从而进一步提高特征对齐的准确性。该模块包括两个子模块:1)自适应卷积核偏移网络,用于学习像素级的偏移参数;2)调制可变形卷积,用于根据偏移参数对相邻帧的特征进行mask调整。通过局部校准模块,FAMI-Pose能够进一步提高多帧人体姿态估计中的特征对齐的准确性。代码如下所示,也就是上面代码的后续操作。

agg_sup_feat = torch.cat(aligned_sup_feat_list, dim=1)
agg_sup_feat = self.sup_agg_block(agg_sup_feat)

# feature alignment
combined_feat = self.combined_feat_layers(torch.cat([agg_sup_feat, kf_bb_feat], dim=1))  # 48
dcn_offset = self.dcn_offset_1(combined_feat)
dcn_mask = self.dcn_mask_1(combined_feat)
combined_feat = self.dcn_1(combined_feat, dcn_offset, dcn_mask)

dcn_offset = self.dcn_offset_2(combined_feat)
dcn_mask = self.dcn_mask_2(combined_feat)
combined_feat = self.dcn_2(combined_feat, dcn_offset, dcn_mask)

dcn_offset = self.dcn_offset_3(combined_feat)
dcn_mask = self.dcn_mask_3(combined_feat)
aligned_sup_feat = self.dcn_3(agg_sup_feat, dcn_offset, dcn_mask)

dcn_offset = self.dcn_offset_4(aligned_sup_feat)
dcn_mask = self.dcn_mask_4(aligned_sup_feat)
aligned_sup_feat = self.dcn_4(aligned_sup_feat, dcn_offset, dcn_mask)

kf_sup_feat = torch.cat([kf_bb_feat, aligned_sup_feat], dim=1)
all_agg_features = self.init_feature_agg_block(kf_sup_feat)

final_hm = self.agg_final_layer(all_agg_features)

可以看到,先将上述得到的粗对齐特征进行特征提取,用sup_agg_block函数实现,该函数就是
ChainOfBasicBlocks,内部重复了两次基础块。生成的agg_sup_feat是文章中带一横的特征z。

self.sup_agg_block = ChainOfBasicBlocks(input_channel=48 * 4, ouput_channel=48, num_blocks=2)

之后通过combined_feat_layers函数将粗对齐的特征和当前帧的特征拼接后的特征进行特征提取

self.combined_feat_layers = ChainOfBasicBlocks(48 * 2, 48, (3, 3), (1, 1), (1, 1), num_blocks=1)

然后就是多次偏移和mask的特征提取生成

self.dcn_offset_1 = conv_bn_relu(48, n_offset_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                                 has_relu=False)
self.dcn_mask_1 = conv_bn_relu(48, n_mask_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                               has_relu=False)
self.dcn_1 = DeformConv2d(48, 48, 3, padding=3, dilation=3)

self.dcn_offset_2 = conv_bn_relu(48, n_offset_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                                 has_relu=False)
self.dcn_mask_2 = conv_bn_relu(48, n_mask_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                               has_relu=False)
self.dcn_2 = DeformConv2d(48, 48, 3, padding=3, dilation=3)

self.dcn_offset_3 = conv_bn_relu(48, n_offset_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                                 has_relu=False)
self.dcn_mask_3 = conv_bn_relu(48, n_mask_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                               has_relu=False)
self.dcn_3 = DeformConv2d(48, 48, 3, padding=3, dilation=3)

self.dcn_offset_4 = conv_bn_relu(48, n_offset_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                                 has_relu=False)
self.dcn_mask_4 = conv_bn_relu(48, n_mask_channel, 3, 1, padding=3, dilation=3, has_bn=False,
                               has_relu=False)
self.dcn_4 = DeformConv2d(48, 48, 3, padding=3, dilation=3)

使用常规的3*3空洞卷积进行mask和offset的生成,再使用可变形卷积DCN进行聚合,四次之后得到aligned_sup_feat,也就是文章中的带俩横线的特征z,即细化后的支持帧特征。下图是一横和两横特征z的原文。
在这里插入图片描述
最后将当前帧特征和支持帧特征进行拼接,这部分和结构图不一样,结构图中是sum操作,这里是concat。

kf_sup_feat = torch.cat([kf_bb_feat, aligned_sup_feat], dim=1)
all_agg_features = self.init_feature_agg_block(kf_sup_feat)

final_hm = self.agg_final_layer(all_agg_features)

最后输入到agg_final_layer函数中,即ChainOfBasicBlocks,内部重复了两次基础块。这里就是结构图中的Detection Head。

self.sup_agg_block = ChainOfBasicBlocks(input_channel=48 * 4, ouput_channel=48, num_blocks=2)

到这里forward基本结束了,接下来是loss函数,也就是本文的又一个创新点。

互信息损失函数

  互信息函数是一种用于衡量两个随机变量之间的相关性的函数。互信息函数的值越大,表示两个随机变量之间的相关性越强。
在这里插入图片描述
这个跟知识蒸馏的KL散度挺像的,也可以认为是一种特殊的kl散度形式。当然在代码里确实也是用kl散度去计算损失的,不过我没弄懂这个温度咋是个0.05,一般不是软化标签要大于1吗?

def feat_feat_mi_estimation(self, F1, F2):
    """
    F1: [B,48,96,72]
        F2: [B,48,96,72]
        F1 -> F2
    """
    batch_size = F1.shape[0]
    temperature = 0.05
    F1 = F1.reshape(batch_size, 48, -1).reshape(batch_size * 48, -1)
    F2 = F2.reshape(batch_size, 48, -1).reshape(batch_size * 48, -1)
    mi = kl_div(input=self.softmax(F1.detach() / temperature), target=self.softmax(F2 / temperature))

    return mi

实验结果

  这就不说了,当然是霸榜!去年的。

总结与展望

  这篇文章还是关于如何利用帧间信息来解决帧退化、运动模糊等问题。使用特征对齐的方式由粗到细的提取特征还是值得学习的。看代码的话基本也了解大体思路,代码中有两个问题:一个是最后结构图中的sum改成了concat,一个是kl散度计算的时候的温度是0.05这样一个小数,不太明白。
  关于姿态估计方面的损失函数,MSE确实不太适合,对整个热图无差别计算,感觉怪怪的。最近的好多文章也都是采用kl散度这种形式来代替MSE,但还是感觉热图中的一些信息有冗余情况。因为最后热图只用到了最大响应点和次大响应点,当然热图这种形式吧也是没办法。像文章中进行高层特征的kl散度对比可能更有意义,因为中间的特征图确实是应该这样去计算损失。
  那么本文的热图形式其实就是个假的形式,因为loss计算的都是高层语义特征之间的差异。那么这样的话其实是可以将SimCC这种方法应用到姿态跟踪当中。输出两个一维热图的形式在resnet这种编码解码分开的网络中可以减少参数量和计算量,说不定就可以把跟踪的网络放到移动设备中。当然这样不太好刷榜,因为二维热图形式性能更高。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值