In Defense of Classical Image Processing: Fast Depth Completion on the CPU论文学习
978-1-5386-6481-0/18/$31.00 ©2018 IEEE
DOI 10.1109/CRV.2018.00013
介绍
本文旨在说明,与基于深度学习的方法相比,设计良好的经典图像处理算法在某些任务上仍然可以提供非常有竞争力的结果。
介绍深度补全的作用。
指出现在使用深度学习算法的弊端,1是需要大量的数据驱动,2是需要硬件支持即GPU,GPU模块也十分耗电。并指出如果没有扎实的理论基础盲目的建立深度模型效果不好,因此采用经典算法。
相关工作
深度补全目前有两个大方向,即引导深度补全和非引导深度补全。
引导深度补全
依靠彩色图像作为指导来完成深度图的绘制。以往的各种算法都提出了联合双边滤波对目标深度图[11]、[12]、[13]进行“补孔”。中值滤波器也被扩展到从彩色图像引导深度补全[14]。近年来,针对导向深度完成问题[15]、[16],出现了深度学习方法。这些方法已经被证明可以生成更高质量的深度图,但都是数据驱动的,需要大量的训练数据才能很好地实现。
算法
主要有以下8步,下面是对这整个步骤的学习:
1.Depth Inversion(深度反演)
第一步是对KITTI数据集中的深度数据进行膨胀操作。
OPENCV中的图像形态学—两个基本运算,腐蚀和膨胀。
二值形态学
腐蚀
粗略的说,腐蚀可以使目标区域范围“变小”,其实质造成图像的边界收缩,可以用来消除小且无意义的目标物。式子表达为:
该式子表示用结构B腐蚀A,需要注意的是B中需要定义一个原点,【而B的移动的过程与卷积核移动的过程一致,同卷积核与图像有重叠之后再计算一样】当B的原点平移到图像A的像元(x,y)时,如果B在(x,y)处,完全被包含在图像A重叠的区域,(也就是B中为1的元素位置上对应的A图像值全部也为1)则将输出图像对应的像元(x,y)赋值为1,否则赋值为0。
我们看一个演示图。
B依顺序在A上移动(和卷积核在图像上移动一样,然后在B的覆盖域上进行形态学运算),当其覆盖A的区域为[1,1;1,1]或者[1,0;1,1]时,(也就是B中‘1’是覆盖区域的子集)对应输出图像的位置才会为1。膨胀
粗略地说,膨胀会使目标区域范围“变大”,将于目标区域接触的背景点合并到该目标物中,使目标边界向外部扩张。作用就是可以用来填补目标区域中某些空洞以及消除包含在目标区域中的小颗粒噪声。
该式子表示用结构B膨胀A,将结构元素B的原点平移到图像像元(x,y)位置。如果B在图像像元(x,y)处与A的交集不为空(也就是B中为1的元素位置上对应A的图像值至少有一个为1),则输出图像对应的像元(x,y)赋值为1,否则赋值为0。
演示图为:
开运算就是先腐蚀再膨胀,闭运算就是先膨胀再腐蚀。
开操作可以平滑物体轮廓,断开狭窄的间断和消除细小的突出物。闭操作可以消弭狭窄的间断,消除小的孔洞。灰度形态学
腐蚀
那么灰度形态学中的腐蚀就是类似卷积的一种操作,用P减去结构元素B形成的小矩形,取其中最小值赋到对应原点的位置即可。
我们来看一个实例,进行加深对灰度形态学的理解。
假设我们有如下的图像A和结构元素B:
我们对输出图像的第一个元素的输出结果进行具体的展示,也就是原点对应的4的位置。输出图像其他的元素的值也都是这样得到的。我们会看到,B首先覆盖的区域就是被减数矩阵,然后在其差矩阵中求min(最小值)来作为原点对应位置的值。膨胀
根据上面对腐蚀的描述,我们对膨胀做出同样的描述,灰度形态学中的膨胀就是类似卷积的一种操作,用P加上B,然后取这个区域中的最大值赋值给结构元素B的原点所对应的位置。
相比较于原图像,因为腐蚀的结果要使得各像元比之前变得更小,所以适用于去除高峰噪声。而灰度值膨胀的结果会使得各像元比之前的变得更大,所以适用于去除低谷噪声。
在KITTI数据集中深度数据范围是0-80m,而缺失的空洞深度也是0,较近的物体深度接近0,如果直接进行膨胀操作会令较近物体的边缘信息丢失,作者在这里加入了深度反转,留出一个20m的缓冲区域, D i n v e r t e d = 100.0 − D i n p u t D_{inverted}=100.0-D_{input} Dinverted=100.0−Dinput,20m缓冲区用于偏移有效深度,以便在后续操作中屏蔽无效像素。
2.Custom Kernel Dilation(自定义内核扩张)
第一步是对有效深度数据旁边的缺失信息进行填补(膨胀操作的作用),因为这些像素最有可能与有效深度共享接近的深度值。接下来是对第一步使用的内核进行设计。作者对比了下面四个内核,选择了第四个。
3.Small Hole Closure(小洞关闭)
上面操作之后仍然有许多空洞,作者考虑了环境中物体的结构,注意到附近的膨胀深度块可以连接起来形成物体的边缘。用5×5的FULL核来做闭操作,这个步骤的作用是连接附近的深度值,可以看作是一组5×5像素的平面,从最远的位置堆叠到最近的位置 。
4.Small Hole Fill(小洞填充)
一些小到中尺寸的空洞在前几步不会被填充,为了填充这些空洞,先计算一个空像素掩码(a mask of empty pixels?)紧接着做一个7×7的FULL核膨胀操作,这个操作只会填充空像素,而保持先前计算过的有效像素不变。
5.Extension to Top of Frame(拓展到框架顶部)
为了考虑到高大的物体,如树木、杆子和延伸到激光雷达点顶部的建筑物,沿着每一列的顶部值外推到图像的顶部,提供了一个更密集的深度图输出。
6.Large Hole Fill(大孔填充)
用31×31的FULL核来填充剩下的大的空洞,保存原有有效像素不变。
7.Median and Gaussian Blur(中值和高斯模糊)
用5×5的中值内核来去除膨胀过程中存在的异常值,相当于去噪,在保持局部边缘的时候去除了异常值。最后用5×5的高斯模糊来平滑。
8.Depth Inversion(深度反演)
对应第一步,从编码中得到原始数据。
实验
用RMSE作为评判标准。
这部分与其他方法比较证明RMSE和MAE都是最优的,并且只需要CPU。
说了算法设计思路:
为了设计该算法,遵循贪婪设计过程。由于有效像素附近的空像素有可能共享相似的值,我们用更小到更大的填充孔来构造算法的顺序。这允许每个有效像素的有效面积缓慢增加,同时仍然保持局部结构。剩下的空白区域会被推断出来,但是会变得比以前小很多。最后一个模糊步骤用于减少输出噪声和平滑局部平面。
首先探讨了膨胀核尺寸的设计选择的影响,然后讨论了膨胀核的形状,最后讨论了膨胀后使用的模糊核。我们选择每个实验的最佳结果,继续下一步的设计。由于这种贪婪的设计方法,前两个关于内核大小和形状的实验不包括步骤7的模糊。最后的算法设计使用了每个实验中表现最好的设计,以达到最佳的结果。
代码学习
demo
import glob
import os
import sys
import time
import cv2
import numpy as np
import png
from ip_basic import depth_map_utils
from ip_basic import vis_utils
def main():
"""Depth maps are saved to the 'outputs' folder.
"""
##############################
# Options
##############################
# Validation set
input_depth_dir = os.path.expanduser(
'~/PycharmProjects/IPbasic/ip_basic-master/Kitti/depth/val_selection_cropped/velodyne_raw')
data_split = 'val'
# Test set
# input_depth_dir = os.path.expanduser(
# '~/Kitti/depth/test_depth_completion_anonymous/velodyne_raw')
# data_split = 'test'
# Fast fill with Gaussian blur @90Hz (paper result)
fill_type = 'fast'
extrapolate = True
blur_type = 'gaussian'
# Fast Fill with bilateral blur, no extrapolation @87Hz (recommended)
# fill_type = 'fast'
# extrapolate = False
# blur_type = 'bilateral'
# Multi-scale dilations with extra noise removal, no extrapolation @ 30Hz
# fill_type = 'multiscale'
# extrapolate = True
# blur_type = 'bilateral'
# Save output to disk or show process
save_output = True
##############################
# Processing
##############################
if save_output:
# Save to Disk
show_process = False
save_depth_maps = True
else:
if fill_type == 'fast':
raise ValueError('"fast" fill does not support show_process')//处理异常,异常会被传播到python解释器;
如果没有这句,程序会停止并显示异常的传播轨迹。
# Show Process
show_process = True
save_depth_maps = False
# Create output folder
this_file_path = os.path.dirname(os.path.realpath(__file__))//dirname返回文件路径;
realpath返回path的真实路径。
__file__表示文件当前路径;
__doc__表示文件描述;
在python中,当一个module作为整体被执行时,moduel.__name__的值是"__main__";
当一个module被其它module引用时,module.__name__将是module自己的名字;
当一个module被其它module引用时,其本身并不需要一个可执行的入口main了。
outputs_dir = this_file_path + '/outputs'
os.makedirs(outputs_dir, exist_ok=True)//区别在于,os.makedirs会递归的建立输入的路径,即使是上层的路径不存在,
它也会建立这个路径,而os.mkdir父级路径不存在,那么就会报错。exist_ok默认值为False,如果创建的文件夹存在会报错,
这里设成True可能是不会报错吧?
output_folder_prefix = 'depth_' + data_split
output_list = sorted(os.listdir(outputs_dir))//os.listdir返回指定路径下的文件和文件夹列表。
sort 是应用在 list 上的方法,sorted 可以对所有可迭代的对象进行排序操作。
list 的 sort 方法返回的是对已经存在的列表进行操作,而内建函数 sorted 方法
返回的是一个新的 list,而不是在原来的基础上进行的操作。
sorted(iterable, key=None, reverse=False) iterable -- 可迭代对象。
key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自
于可迭代对象中,指定可迭代对象中的一个元素来进行排序。reverse -- 排序规则,
reverse = True 降序 , reverse = False 升序(默认)。
if len(output_list) > 0:
split_folders = [folder for folder in output_list
if folder.startswith(output_folder_prefix)]//startswith() 方法用于检查字符串是否是以指定子字符串
开头,如果是则返回 True,否则返回 False。如果参数 beg 和 end 指定值,则在指定范围内检查。
if len(split_folders) > 0:
last_output_folder = split_folders[-1]//