3D双目感知深度估计之PSMNet解读
3D视觉感知之双目深度估计PSMNet: Pyramid Stereo Matching Network
论文地址: [1] Pyramid Stereo Matching Network (arxiv.org)
代码地址: JiaRenChang/PSMNet: Pyramid Stereo Matching Network (CVPR2018) (github.com)
Git链接: 计算机知识汇总
课程来源 : 深蓝学院-环境感知
1. 背景
3D感知任务相比于2D感知任务的情况更为复杂,而相比于单目相机双目相机的感知能力拥有以下几个特点:
- 优点
- 双目感知无需依赖强烈的先验知识和几何约束
- 能够解决透视变化带来的歧义性(通俗的讲就是照片是由3D真实世界投影到2D图像然后再转换成3D,由于深度信息的丢失本身就十分困难)
- 无需依赖物体检测的结果,对任意障碍物均有效
- 缺点
- 硬件: 摄像头需要精确配准,车辆运行过程中同样需要保持
- 软件: 算法需要同时处理来自两个摄像头的数据,计算复杂度较高
而双目相机是如何实现3D视觉感知的呢?如下图:
B : 两个相机之间的距离
f : 相机的焦距
d: 视差(左右两张图象上同一个3d点之间的距离)
z: 物体相对于相机的深度,也是我们需要求解的值。
根据几何的知识我们可以得到 视差d 与 深度z是成反比的,所以双目相机的3D感知其实就是基于视差的估计来的,那么接下来核心来了,我们应该怎么得到每个像素点的视差呢?PSMNet 横空出世,它是一个利用端到端的卷积神经网络学习如何从输入的pair图像中获取每个像素点的视差
PSMNet 在原文中提到了以下几个亮点:
- 端到端无需后处理的双目深度估计方法
- 利用空间金字塔池化模块(SPP)和空洞卷积有效地整合全局上下文信息,从而提高了深度估计的准确性。
- 采用三维卷积神经网络(3D CNN)stacked hourglass 对cost map进行正则化处理,进一步提高了深度估计的精度。
- 使用堆叠多个hourglass网络,并结合中间监督,进一步优化了3D CNN模块的性能。
2. 网络结构
整体的网络结构如上图所示,首先网络的输入是成对的双目相机拍摄出来的左右片,通过一系列权重共享的特征提取网络提取特征,然后叠加构建costmap,然后经过一个3D卷积最后通过上采样恢复到原始输入大小的特征图即可。此文将网络结构分成4个模块然后会分别进行介绍
2.1 特征提取
第一个CNN模块,比较简单就是一个带残差模块和空洞卷积的卷积神经网络,将原始的H * W * 3 (kitti数据集里是375 X 1242 * 3)的图像下采样至 H/4 * W/4 * 128。所以整体的分辨率降低了4倍。
第二个是SPP模块,这里可以看到下面有4个branch这里用的就是4个不同大小尺度的averagepooling,去收集不同分辨率下的局部的信息,然后通过双线性插值恢复到原始图像的1/4大小,然后与输出进SPP网络的原始输入进行拼接,这样SPP网络最后的输出就整合了全局(CNN网络的输出)以及局部(4个branch的pooling)的信息。
PS: 这里查了查相关资料有关于AveragePooling和MaxPooling的区别,主要来说如下:
- AveragePooling: 更有利于保留图像背景信息,一般用在
- 当你需要用整合局部的信息的时候就用针对于像素级别的任务比如说分割任务
- 下采样倍数较大的深层网络
- 需要保留细节特征,利用全局的信息
- MaxPooling:更有利于保留图像纹理信息,相当于做了特征选择,选出了辨识度更好的特征,一般用在
- 需要边缘的信息的任务,比如说分割类的任务
2.2 构建Cost Volume
在介绍构建Cost Volume之前,这里还需要估计引入一个概念就是视差的范围:前文提到计算深度就是匹配视差其关键在于计算匹配误差,即对于对于左视图的同一个物体我们只要找到其右视图的水平方向偏移的像素点,我们就可以知道其深度。因此接下来的几点是一个重点:
- 由于需要感知的深度范围有限,所以我们需要感知的视差的范围也是有限的(eg, 相机的深度范围是1 - 100m,对应的视差范围可能是1-10个pixel)因此对于视差我们就在可能的视差范围内搜寻值就可以了
- 对于每一个可能的视差(范围有限上一点提到的1-10个pixel),计算匹配误差,因此得到的三维的误差数据称为Cost Volume。
- 计算匹配误差时考虑像素点附近的局部区域(提取邻域的信息),比如对局部区域内所有对应像素值的差进行求和
- 通过Cost Volume可以得到每个像素处的视差(对应最小匹配误差的𝑑𝑑),从而得到深度值。
好的有了以上的观点我们就可以继续回到PSMNet的costVolume 的构建了。
由2.2部分的输出的左右图像分别大小分别是H’ * W’ * C(其实已经下采样到了1/4) 然后对D(程序中是192)个可能的视差范围将左右的特征图重合的部分拼接,不足的部分padding0,从而得到一个新的4维的特征张量H’ * W’ *D * 2C 。
这里的含义可以看成拼接后的特征图同一个物体的cost比较小,不同的物体差异较大。所以就是计算左右特征图的在left[i:]与right[:-i]的相似度。
对应的代码如下:
#matching
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_()).cuda()
for i in range(self.maxdisp//4): # 0 - 47
if i > 0 :
cost[:, :refimg_fea.size()[1], i, :,i:] = refimg_fea[:,:,:,i:] # LEFT
cost[:, refimg_fea.size()[1]:, i, :,i:] = targetimg_fea[:,:,:,:-i] #RIGHT
else: # i == 0
cost[:, :refimg_fea.size()[1], i, :,:] = refimg_fea
cost[:, refimg_fea.size()[1]:, i, :,:] = targetimg_fea
cost = cost.contiguous()
2.3 三维卷积
对于一个4D(H’ * W’ *D * 2C)的输入,作者采用了两种三维卷积的机制分别是basic 和Stacked hourglass,且其作用均为对比左右特征图的在同一个位置的视差差异。
2.3.1 Basic
这个结构就是多层跨链接的3D卷积,且卷积核为(3 * 3 * 3),可以看出其利用到了每个像素点周围邻域的信息(空间信息)也利用到了多个视差的信息,所以相比于只对比一个视差更加鲁棒。最后同一个线性插补上采样恢复原始分辨率,从而计算每个像素的深度值。下列是basic模块的代码实现
2.3.2 Stacked hourglass
这是作者提出一个相比于basic更加复杂的结构,是一个堆叠3次的hourglass结构,同样的这种hourglass的结构有3个好处:
- 能获取不同感受野的信息
- 利用skip连接可以在不同以及自身的结构内传递信息,更加鲁棒
- 与basic只有一个输出不同,stacked hourglass 在每个hourglass 结构都接了一个单独的输出,且在训练阶段加权求和得到总loss(具体权重参考第3部分)
if self.training: # training 需要加上所有的cost
cost1 = F.upsample(cost1, [self.maxdisp,left.size()[2],left.size()[3]], mode='trilinear')
cost2 = F.upsample(cost2, [self.maxdisp,left.size()[2],left.size()[3]], mode='trilinear')
cost1 = torch.squeeze(cost1,1)
pred1 = F.softmax(cost1,dim=1)
pred1 = disparityregression(self.maxdisp)(pred1)
cost2 = torch.squeeze(cost2,1)
pred2 = F.softmax(cost2,dim=1)
pred2 = disparityregression(self.maxdisp)(pred2)
cost3 = F.upsample(cost3, [self.maxdisp,left.size()[2],left.size()[3]], mode='trilinear')
cost3 = torch.squeeze(cost3,1)
pred3 = F.softmax(cost3,dim=1)
pred3 = disparityregression(self.maxdisp)(pred3)
2.4 视差匹配
这里有两种做法:
- 硬分类: 直接取cost最小的视差值作为输出,但是这样有个缺点就是如果实际中最小值与第二小的值差别特别小,那么真实的视差应该处于二者之间,所以作者采用了一种软分类的机制。
- 软分类: 对网络输出的d个cost的值进行加权求和,权重就是输出的cost值,cost值越大权重越小。
class disparityregression(nn.Module):
def __init__(self, maxdisp):
super(disparityregression, self).__init__()
self.disp = torch.Tensor(np.reshape(np.array(range(maxdisp)),[1, maxdisp,1,1])).cuda()
def forward(self, x):
out = torch.sum(x*self.disp.data,1, keepdim=True) # 加权求和
return out
3. 损失函数
这里用的就是回归模型里比较常用的smoothL1,其是一种结合了L1和L2 的结合体,不会像L2对离群点敏感且容易梯度爆炸也不会像L1一样在0处不可导。
这里还要highligh一点就是之前提到的stacked hourglass操作里有三个预测头,在训练的时候这三个输出也对应着不同的loss,作者对其进行了调参结果如下:
在源代码里体现如下:
if args.model == 'stackhourglass':
output1, output2, output3 = model(imgL,imgR)
output1 = torch.squeeze(output1,1)
output2 = torch.squeeze(output2,1)
output3 = torch.squeeze(output3,1)
# 三个loss 是加权平均 分别是 0.5 / 0.7 / 1.0
loss = 0.5*F.smooth_l1_loss(output1[mask], disp_true[mask], size_average=True) + \
0.7*F.smooth_l1_loss(output2[mask], disp_true[mask], size_average=True) + \
F.smooth_l1_loss(output3[mask], disp_true[mask], size_average=True)
4. 效果
下图是原论文中截至于2018年3月,在kitti2015数据集上的效果,其中All 和 Noc 分别代表了所有像素点和未遮挡像素点的误差。 D1-bg / D1-fg / D1-all 分别代表的是背景/前景/所有点的误差百分比。可以看出效果还是很不错的,效率上由于引入3D卷积的操作时间上可能有待提供高。
5. 失败case与提升
原因 & 改进:
虽然考虑了邻域的信息但没用考虑高层的语义信息,无法理解场景 -> 用物体检测和语义分割的结果进行后处理,或者多个任务同时进行训练。或者增加注意力机制增加网络对纹理信息的理解提高深度的一致性能。
![在这里插入图片描述](https://img-blog.csdnimg.cn/a8b9d21b560c442ead6a1363b1dfc312.png#pic_center)
原因 & 改进:
远距离的视差值较小,在离散的图像像素上难以区分 -> 提高图像的空间分辨率,使得远距离物体也有较多的 像素覆盖;增加基线长度,从而增加视差的范围
原因 & 改进:
低纹理或者低光照的区域内无法有效提取特征,用于计算匹配误差 -> 提高摄像头的动态范围,或者采用可以测距的传感器
改进方向总结:
- 针对3D卷积的 stacked hourglass 和 深层次的SPP结构 , 会影响整体的效率 -> 一种基于 PSMNet 改进的立体匹配算法,作者提出一种浅层的ASPP和替代stacked hourglass的3个级联的残差结构 从而提高效率。
- 针对cost volume 的建立,只是直接concat,并没有考虑到相关性 -> Group-wise Correlation Stereo Network , 利用了相互之间的关系,将左特征和右特征沿着通道维度分成多组,在视差水平上对每组之间计算相关图,然后打包所有相关图以形成4D cost,这样一来,便可为后续的3D聚合网络提供更好的相似性度量,
原作者给的代码里生成的深度图是灰度图,不利于肉眼对比效果,需要将灰度图转换成自设定的彩色图,对应的代码可以参考。
def disp_map(disp):
map = np.array([
[0, 0, 0, 114],
[0, 0, 1, 185],
[1, 0, 0, 114],
[1, 0, 1, 174],
[0, 1, 0, 114],
[0, 1, 1, 185],
[1, 1, 0, 114],
[1, 1, 1, 0]
])
# grab the last element of each column and convert into float type, e.g. 114 -> 114.0
# the final result: [114.0, 185.0, 114.0, 174.0, 114.0, 185.0, 114.0]
bins = map[0:map.shape[0] - 1, map.shape[1] - 1].astype(float)
# reshape the bins from [7] into [7,1]
bins = bins.reshape((bins.shape[0], 1))
# accumulate element in bins, and get [114.0, 299.0, 413.0, 587.0, 701.0, 886.0, 1000.0]
cbins = np.cumsum(bins)
# divide the last element in cbins, e.g. 1000.0
bins = bins / cbins[cbins.shape[0] - 1]
# divide the last element of cbins, e.g. 1000.0, and reshape it, final shape [6,1]
cbins = cbins[0:cbins.shape[0] - 1] / cbins[cbins.shape[0] - 1]
cbins = cbins.reshape((cbins.shape[0], 1))
# transpose disp array, and repeat disp 6 times in axis-0, 1 times in axis-1, final shape=[6, Height*Width]
ind = np.tile(disp.T, (6, 1))
tmp = np.tile(cbins, (1, disp.size))
# get the number of disp's elements bigger than each value in cbins, and sum up the 6 numbers
b = (ind > tmp).astype(int)
s = np.sum(b, axis=0)
bins = 1 / bins
# add an element 0 ahead of cbins, [0, cbins]
t = cbins
cbins = np.zeros((cbins.size + 1, 1))
cbins[1:] = t
# get the ratio and interpolate it
disp = (disp - cbins[s]) * bins[s]
disp = map[s, 0:3] * np.tile(1 - disp, (1, 3)) + map[s + 1, 0:3] * np.tile(disp, (1, 3))
return disp
def disp_to_color(disp, max_disp=None):
# grab the disp shape(Height, Width)
h, w = disp.shape
# if max_disp not provided, set as the max value in disp
if max_disp is None:
max_disp = np.max(disp)
# scale the disp to [0,1] by max_disp
disp = disp / max_disp
# reshape the disparity to [Height*Width, 1]
disp = disp.reshape((h * w, 1))
# convert to color map, with shape [Height*Width, 3]
disp = disp_map(disp)
# convert to RGB-mode
disp = disp.reshape((h, w, 3))
disp = disp * 255.0
return disp
def tensor_to_color(disp_tensor, max_disp=192):
"""
The main target is to convert the tensor to image format
so that we can load it into tensor-board.add_image()
Args:
disp_tensor (Tensor): disparity map
in (BatchSize, Channel, Height, Width) or (BatchSize, Height, Width) layout
max_disp (int): the max disparity value
Returns:
tensor_color (numpy.array): the converted disparity color map
in (3, Height, Width) layout, value range [0,1]
"""
if disp_tensor.ndimension() == 4:
disp_tensor = disp_tensor[0, 0, :, :].detach().cpu()
elif disp_tensor.ndimension() == 3:
disp_tensor = disp_tensor[0, :, :].detach().cpu()
else:
disp_tensor = disp_tensor.detach().cpu()
disp = disp_tensor.numpy()
disp_color = disp_to_color(disp, max_disp) / 255.0
disp_color = disp_color.transpose((2, 0, 1))
return disp_color
更多推荐
所有评论(0)