双目深度算法——基于Cost Volume的方法(GC-Net / PSM-Net / GA-Net)

Posted Leo-Peng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了双目深度算法——基于Cost Volume的方法(GC-Net / PSM-Net / GA-Net)相关的知识,希望对你有一定的参考价值。

双目深度算法——基于Cost Volume的方法(GC-Net / PSM-Net / GA-Net)

双目深度算法——基于Cost Volume的方法(GC-Net / PSM-Net / GA-Net)

在之前的工作中有接触过Stereo Depth这个方向,在读书期间也有用过ZED这样的传感器,但是一直没有对这个方向进行过系统的学习,因此我打算这段时间花点时间学习下这方面的知识,之前有写过一篇相关的文档双目视觉深度——SGM中的动态规划,SGM主要思想是基于动态优化,NN发展后逐渐衍生出了基于Feature进行Match的方法,再后来就是基于Correlation和Cost Volume方法的提出,以及最近这两年比较火的基于Transformer的方法。

本篇博客主要介绍以Cost Volume为基础的相关网络框架。以GC-Net为例搞清楚什么是Cost Volume,然后根据介绍两篇基于GC-Net优化的网络。

1. GC-Net

GC-Net发表于2017年CVPR,原论文名为《End-to-End Learning of Geometry and Context for Deep Stereo Regression》,是最开始提出Cost Volume的方法之一

1.1 网络结构及损失函数

GC-Net网络结构如下图所示:

具体的参数如下表:

网络主要分为三部分:用于提取Feature的2D卷积、Cost Volume的构造以及3D卷积,下面我们结合代码来开拿下具体是如何实现的(代码参考的是zyf12389/GC-Net,因为是复现的代码,代码实现和原论文可能稍微有些细微差别):

特征提取部分第一层首先对输入图片 H × W × 3 H\\times W\\times 3 H×W×3进行一个下采样,然后接了一个ResNet的Backbone,从ResNet输出的特征大小为 H 2 × W 2 × F \\fracH2\\times \\fracW2 \\times F 2H×2W×F,代码如下:

imgl0=F.relu(self.bn0(self.conv0(imgLeft)))
imgr0=F.relu(self.bn0(self.conv0(imgRight)))

imgl_block=self.res_block(imgl0)
imgr_block=self.res_block(imgr0)

imgl1=self.conv1(imgl_block)
imgr1=self.conv1(imgr_block)

然后就是利用特征构建Cost Volume,构建Cost Volume的目的是为了将视差这一概念表达在网络结构中,这其实很好理解,如下图所示,如果我们仅仅将两张图的图像特征简单地叠到一起,网络怎么知道A处和B处有特征有对应关系呢,可想而知是很困难的,而我们如果能够根据视差将A处和B处的的特征进行对齐,然后再进行卷积,网络学习到A处和B处的对应关系就会简单很多,而Cost Volume就是将所有可能的视差遍历一遍,让网络更容易学习到不同视差下的特征的对应关系:

Cost Volume部分的代码如下:

cost_volum = self.cost_volume(imgl1,imgr1)
def cost_volume(self,imgl,imgr):
    xx_list = []
    pad_opr1 = nn.ZeroPad2d((0, self.maxdisp, 0, 0))
    xleft = pad_opr1(imgl)
    for d in range(self.maxdisp):  # maxdisp+1 ?
        pad_opr2 = nn.ZeroPad2d((d, self.maxdisp - d, 0, 0))
        xright = pad_opr2(imgr)
        xx_temp = torch.cat((xleft, xright), 1)
        xx_list.append(xx_temp)
    xx = torch.cat(xx_list, 1)
    xx = xx.view(self.batch, self.maxdisp, 64, int(self.height / 2), int(self.width / 2) + self.maxdisp)
    xx0=xx.permute(0,2,1,3,4)
    xx0 = xx0[:, :, :, :, :int(self.width / 2)]
    return xx0

我们可以看到实际的构建流程是:
(1)先利用ZeroPad2d将左图特征从 H 2 × W 2 × D \\fracH2\\times \\fracW2 \\times D 2H×2W×D大小填充为 H 2 × ( W 2 + M ) × D \\fracH2\\times (\\fracW2 +M) \\times D 2H×(2W+M)×D大小,其中 D D D为入参定义的最大视差;
(2)然后将右图特征也填充为 H 2 × ( W 2 + M ) × F \\fracH2\\times (\\fracW2 +M) \\times F 2H×(2W+M)×F大小,与左图特征不同的是,左图特征是将填充集中在原始特征的右侧,而右图则是按照视差从小达到的变化逐渐调整原始特征左右的填充列数;
(3)接着将左右特征按行方向Concat到一起;
(4)最后拼接成的特征图调整为 D × 2 F × H 2 × W 2 D \\times 2F \\times \\fracH2\\times \\fracW2 D×2F×2H×2W大小,具体流程如下图所示:

绿色和橙色分别表示左图和右图的图像feature,灰色代表zero padding的部分,由于我参考的代码是复现代码,我担心复现的老哥写的代码有Bug,因此我又参考了PSM-Net的官方实现,如下所示:

cost = Variable(torch.FloatTensor(refimg_fea.size()[0], refimg_fea.size()[1]*2, self.maxdisp/4,  refimg_fea.size()[2],  refimg_fea.size()[3]).zero_(), volatile= not self.training).cuda()

for i in range(self.maxdisp/4):
    if i > 0 :
     cost[:, :refimg_fea.size()[1], i, :,i:] = refimg_fea[:,:,:,i:]
     cost[:, refimg_fea.size()[1]:, i, :,i:] = targetimg_fea[:,:,:,:-i]
    else:
     cost[:, :refimg_fea.size()[1], i, :,:] = refimg_fea
     cost[:, refimg_fea.size()[1]:, i, :,:] = targetimg_fea
cost = cost.contiguous()

仔细对比可以发现其实构建的Cost Volume是大同小异的,如下图所示:

区别是,PSM-Net官方代码构建的Cost Volume中“橙色”特征在没有和“绿色”特征重合部分填充的是0,而GC-Net复现代码中“橙色”特征保留的是原始的图像特征,以我以往的经验来说,这种区别应该对最终结构的影响不大。

通过上述Cost Volume,我们得到的是 2 F 2F 2F D × H 2 × W 2 D \\times \\fracH2\\times \\fracW2 D×2H×2W大小的3D特征图,紧接着我们就是在上述3D特征图进行3D卷积,如下所示:

conv3d_out=F.relu(self.bn3d_1(self.conv3d_1(cost_volum)))
conv3d_out=F.relu(self.bn3d_2(self.conv3d_2(conv3d_out)))

conv3d_block_1=self.block_3d_1(cost_volum)
conv3d_21=F.relu(self.bn3d_3(self.conv3d_3(cost_volum)))
conv3d_block_2=self.block_3d_2(conv3d_21)
conv3d_24=F.relu(self.bn3d_4(self.conv3d_4(conv3d_21)))
conv3d_block_3=self.block_3d_3(conv3d_24)
conv3d_27=F.relu(self.bn3d_5(self.conv3d_5(conv3d_24)))
conv3d_block_4=self.block_3d_4(conv3d_27)

deconv3d=F.relu(self.debn1(self.deconv1(conv3d_block_4))+conv3d_block_3)
deconv3d=F.relu(self.debn2(self.deconv2(deconv3d))+conv3d_block_2)
deconv3d=F.relu(self.debn3(self.deconv3(deconv3d))+conv3d_block_1)
deconv3d=F.relu(self.debn4(self.deconv4(deconv3d))+conv3d_out)

为了减小计算量,3D卷积的过程是一个类似U-Net的结构,先对特征图进行Down Sample,然后通过再通过转置卷积进行Up Sample,对于转置卷积不了解的同学可以参考下计算机视觉算法——图像分割网络总结,其中我对2D转置卷积的原理和计算过程进行了详细的分析,这里就不再赘述(其中ConvTranspose3d的output_padding设置为1是为了保证输入输出特征图大小一致)。

最后就是从输出特征中获取视差,论文中还特地提到了他们建立的是一种Soft Argmin获取视差,3D卷积最后的输出特征图大小为 D × H × W D \\times H\\times W D×H×W,我们很直接想到的一种方式是在输出特征的视差维度上取Argmin或者Argmax,但是论文中提到,这种方式无法获得亚像素级的视差以且不可微,因此论文通过Soft Argmin获取视差,其实就是在视差维度进行Softmax后然后进行加权平均: s o f t _ a r g m a x = Σ d = 0 D m a x d × σ ( − c d ) soft\\_argmax=\\Sigma^D_max_d=0d\\times\\sigma(-c_d) soft_argmax=Σd=0Dmaxd×σ(cd)其中, d d d为视差, σ \\sigma σ为Softmax操作, c d c_d cd为输出的特征,具体代码如下:

loss_mul_list = []
for d in range(maxdisp):
    loss_mul_temp = Variable(torch.Tensor(np.ones([batch, 1, h, w]) * d)).cuda()
    loss_mul_list.append(loss_mul_temp)
loss_mul = torch.cat(loss_mul_list, 1)

x = net(left_image, right_image)
result = torch.sum(x.mul(loss_mul), 1)

获得视差后最后构建 L 1 L1 L1损失,至此就完成了整个网络结构的介绍。

2. PSM-Net

PSM-Net原论文名为《Pyramid Stereo Matching Network》,发表于2018年,PSM-Net的网络Pipeline和和GC-Net的几乎一致,只是在特征提取的BackBone和3D卷积部分使用了特殊结构,使得网络具备更大的感受野,从而在一些细节上能取得更好的效果,如下所示:

在计算速度上,PSM-Net也要优于GC-Net

2.1 网络结构及损失函数

PSM-Net网络结构如下图所示:

从上图中可以看出,在特征提取部分,PSM-Net将原本的ResNet更换为了SPP Modlue,在3D卷积部分,PSM-Net将原本类似U-Net的网络结构更换为了Stacked Hourglass结构,除此之外,在Loss方面,PSM-Net将原本的L1 Loss更换为了Smooth L1 Loss,公式如下: L ( d , d ^ ) = 1 N ∑ i = 1 N smooth ⁡ L 1 ( d i − d ^ i ) L(d, \\hatd)=\\frac1N \\sum_i=1^N \\operatornamesmooth_L_1\\left(d_i-\\hatd_i\\right) L(d,d^)=N1i=1NsmoothL1以上是关于双目深度算法——基于Cost Volume的方法(GC-Net / PSM-Net / GA-Net)的主要内容,如果未能解决你的问题,请参考以下文章

双目视觉深度——GC-Net算法详解 / Cost Volume模块详解

双目深度算法——基于Transformer的方法(STTR)

双目深度算法——基于Transformer的方法(STTR)

双目深度算法——基于Transformer的方法(STTR)

双目深度算法——基于Correlation的方法(DispNet / iResNet / AANet)

双目深度算法——基于Correlation的方法(DispNet / iResNet / AANet)