Monodpeth之源码解读

文  | 陈十三
首发 | 一只在路上的哈士奇
ID  | super_Mrchen
关注可了解更多。问题或建议,请公众号留言

参考

0x00 背景

研一浑浑噩噩,到了研二才后知后觉,需要努力,需要发论文.
导师研究光场及深度学习中的立体匹配,建议我先拿2017年的Unsupervised Monocular Depth Estimation with Left-Right Consistency这篇论文作为入门,单目深度估计.
先撸代码,再撸论文…

中文注释后的代码已经上传Github, 传送门: https://github.com/Lebhoryi/learn_monodepth

0x01 主函数

在main()函数里面是这样用的。

  • 给定参数

  • 训练或者测试

def main(_):
    params = monodepth_parameters(
        encoder=args.encoder,
        height=args.input_height,
        width=args.input_width,
        batch_size=args.batch_size,
        num_threads=args.num_threads,
        num_epochs=args.num_epochs,
        do_stereo=args.do_stereo,
        wrap_mode=args.wrap_mode,
        use_deconv=args.use_deconv,
        alpha_image_loss=args.alpha_image_loss,
        disp_gradient_loss_weight=args.disp_gradient_loss_weight,
        lr_loss_weight=args.lr_loss_weight,
        full_summary=args.full_summary)
    if args.mode == 'train':
        train(params)
    elif args.mode == 'test':
        test(params)
monodepth_parameters = namedtuple('parameters', 
                        'encoder, '
                        'height, width, '
                        'batch_size, '
                        'num_threads, '
                        'num_epochs, '
                        'do_stereo, '
                        'wrap_mode, '
                        'use_deconv, '
                        'alpha_image_loss, '
                        'disp_gradient_loss_weight, '
                        'lr_loss_weight, '
                        'full_summary')

monodepth_parameters: 调用monodepth_model.py中的monodepth_parameters参数,创建一个monodepth_parameters对象,并赋值
namedtuple: namedtuple(typename, field_names), 定义一个namedtuple类型的monodepth_parameters,类似元组的对象,并包含encoder, height, width, batch_size, num_threads, num_epochs, do_stereo, wrap_mode, use_deconv, alpha_image_loss, disp_gradient_loss_weight, lr_loss_weight, full_summary属性。

0x02 加载数据

monodepth_main.py

 # 加载数据,依次获得一个batch的数据,每次四个线程
data_loader = MonodepthDataloader(args.data_path, args.filenames_file, params, args.dataset, args.mode)

调用monodepth_dataloader.py中的MonodepthDataloader类函数。

直接从文件中读取数据,
1.使用tf.train.string_input_producer函数把我们需要的全部文件打包为一个tf内部的queue类型,之后tf开文件就从这个queue中取目录了,要注意一点的是这个函数的shuffle参数默认是True,也就是你传给他文件顺序是1234,但是到时候读就不一定。
2.搞一个reader,不同reader对应不同的文件结构,比如度bin文件tf.FixedLengthRecordReader就比较好,因为每次读等长的一段数据。如果要读什么别的结构也有相应的reader。
3.用reader的read方法,这个方法需要一个IO类型的参数,就是我们上边string_input_producer输出的那个queue了,reader从这个queue中取一个文件目录,然后打开它经行一次读取,reader的返回是一个tensor(这一点很重要,我们现在写的这些读取代码并不是真的在读数据,还是在画graph,和定义神经网络是一样的,这时候的操作在run之前都不会执行,这个返回的tensor也没有值,他仅仅代表graph中的一个结点)。

key, value = reader.read(files)

4.对这个tensor做些数据与处理,比如CIFAR1-10中label和image数据是糅在一起的,这里用slice把他们切开,切成两个tensor(注意这个两个tensor是对应的,一个image对一个label,对叉了后便训练就完了),然后对image的tensor做data augmentation。

input_queue = tf.train.string_input_producer([filenames_file], shuffle=False)
line_reader = tf.TextLineReader()
_, line = line_reader.read(input_queue)

split_line = tf.string_split([line]).values

TensorFlow提供两种类型的拼接:

  • tf.concat(values, axis, name='concat'):按照指定的已经存在的轴进行拼接

  • tf.stack(values, axis=0, name='stack'):按照指定的新建的轴进行拼接

  • tf.slice(input_, begin, size, name=None):按照指定的下标范围抽取连续区域的子集

  • tf.gather(params, indices, validate_indices=None, name=None):按照指定的下标集合从axis=0中抽取子集,适合抽取不连续区域的子集

  • tf.split(value, num_or_size_splits, axis=0, num=None, name="split"): 分割value, 分成value/num_or_size_splits份

  • tf.string_split(source, delimiter=' '): 拆分source是一维数组,用于将一组字符串按照delimiter拆分为多个元素,返回值为一个SparseTensor

举例:
假如有两个字符串,source[0]是“hello world”,source[1]是“a b c”,那么输出结果如下:

  • st.indices: [0, 0; 0, 1; 1, 0; 1, 1; 1, 2]
  • st.values: [‘hello’, ‘world’, ‘a’, ‘b’, ‘c’]
  • st.dense_shape:[2, 3]
  • tf.string_join(inputs, separator=None, name=None):拼接

def read_image(self, image_path):
   # tf.decode_image does not return the image size, this is an ugly workaround to handle both jpeg and png
   # Q: [0] 不知道什么意思
   path_length = string_length_tf(image_path)[0]

新的读取数据的方式, tf.train.shuffle_batch()

capacity = min_after_dequeue + (num_threads + a small safety margin) * batch_size
# capacity: An integer.The maximum number of elements in the queue.
# 容量: 一个整数, 队列中的最大的元素数
min_after_dequeue = 2048
capacity = min_after_dequeue + 4 * params.batch_size
# 读取一个文件并且加载一个张量中的batch_size行
# 从[left_image, right_image]利用 params.num_threads 个线程读取 params.batch_size 行
# min_after_dequeue:当一次出列操作完成后, 队列中元素的最小数量, 往往用于定义元素的混合级别
self.left_image_batch, self.right_image_batch = tf.train.shuffle_batch([left_image, right_image],
                        params.batch_size, capacity, min_after_dequeue, params.num_threads)

0x03 网络模型


FlowNet

大概是像这样的一个东西, 这是FlowNet, 然后在基础上变种为DispNet, 适合grand truth data, 作者再次对DispNet基础上修改.有兴趣的同学可以去看看


monodepth_main.py中:

model = MonodepthModel(params, args.mode, left_splits[i], right_splits[i], reuse_variables, i)

调用monodepth_model.py中的MonodepthModel类:

# 类的初始化函数
def __init__(self, params, mode, left, right, reuse_variables=None, model_index=0):
    self.params = params
    self.mode = mode # mode:train或者test
    self.left = left # left,right:是left_image_batch,right_image_batch。左右图以batch形式传进来
    self.right = right
    self.model_collection = ['model_' + str(model_index)]
    self.reuse_variables = reuse_variables
    self.build_model()
    self.build_outputs()
    if self.mode == 'test':
        return
    self.build_losses() # build_losses():创建损失函数
    self.build_summaries() # build_summaries():可视化工具

  • left,right:是left_image_batch,right_image_batch。左右图以batch形式传进来。
  • mode:train或者test。
  • params:传进来很多参数设置。
  • build_model():创建模型
  • build_outputs():创建输出
  • build_losses():创建损失函数
  • build_summaries():可视化工具

创建模型build_model(self):

作者主要调用了slim里面的基础CNN操作;

  1. 生成左图金字塔:尺度为4
  2. 如果训练则生成右图金字塔,如果做stereo,则把左右图在channel维上叠在一起作为模型输入。否则把左图作为模型输入
  3. 根据params里面的设定,选择vgg或者resnet50作为编码器。

# 生成左图金字塔
# Q: 什么是图片金字塔?
# A:图片金字塔就是原图+1/x的原图, 返回新的列表.
# Q: 为什么要有图片金字塔?
self.left_pyramid = self.scale_pyramid(self.left, 4)

编码与解码

编码与解码

作者的网络架构,其中k是卷积核大小,s是步幅,chns每层的输入和输出通道的数量,输入和输出是每层相对于输入图像的缩减因子,并且输入对应于每个层的输入,其中+是串联和* 卷积是层的a2×上采样。


if self.params.encoder == 'vgg':
    self.build_vgg()
elif self.params.encoder == 'resnet50':
    self.build_resnet50()
else:
    return None
  1. 以VGG为例,可以看到conv1-conv7都是标准的vgg。
  2. skip指的是把conv1-conv7引出来。
  3. 在decoder中,upconv指采用反卷积或者上采样的方法逐步恢复原来的尺度。而skip引出的结果用来与decoder里面的feature maps在第三维也就是channel维叠起来后做upconv。这样逐步upconv
  4. 最后利用iconv得到视差图。

上卷积

先做最近邻上采样, 然后做卷积,步长为1

def upconv(self, x, num_out_layers, kernel_size, scale):
    upsample = self.upsample_nn(x, scale)
    conv = self.conv(upsample, num_out_layers, kernel_size, 1)
    return conv

上采样

最近邻上采样调用的是tensorflow.image里面的resize_nearest_neightbor函数。之后图片放大ratio

def upsample_nn(self, x, ratio):
    s = tf.shape(x)
    h = s[1]
    w = s[2]
    return tf.image.resize_nearest_neighbor(x, [h * ratio, w * ratio])

生成视差图

作者在upconv之后用了一个CNN加sigmoid函数,乘以0.3之后作为视差图

def get_disp(self, x):
    """生成视差图"""
    disp = 0.3 * self.conv(x, 2, 3, 1, tf.nn.sigmoid)
    return disp

生成输出

反向采样过程

def build_outputs(self):
    '''一次 loop 的输出'''
    # STORE DISPARITIES
    # 生成 dr 和 dl
    with tf.variable_scope('disparities'):
        # 将四个尺度的视差图排成队列
        self.disp_est  = [self.disp1, self.disp2, self.disp3, self.disp4]
        # 从视差图队列中取出左(右)视差图(最后输出的视差图的0通道是左图, 1 通道是右图),
        # 用tf.expand_dims()加上通道轴,变成[batch,height,width,1]形状的tensor
        self.disp_left_est  = [tf.expand_dims(d[:,:,:,0], 3) for d in self.disp_est]
        self.disp_right_est = [tf.expand_dims(d[:,:,:,1], 3) for d in self.disp_est]

    # 如果是测试模式,之后的代码部分不运行
    if self.mode == 'test':
        return

    # 原图估计, 调用生成左(右)图估计函数
    with tf.variable_scope('images'):
        # 通过上面生成的 dr 和 dl 生成图片 I`r 和 I`l (backward sampling)
        self.left_est  = [self.generate_image_left(self.right_pyramid[i], self.disp_left_est[i])  for i in range(4)]
        self.right_est = [self.generate_image_right(self.left_pyramid[i], self.disp_right_est[i]) for i in range(4)]

    # LR CONSISTENCY 左右一致性
    # 用右视差图中的视差通过视差索引找到左视差图上的点, 然后再通过做视差图点上的视差索引生成新的右视差图.
    # 就可以用右视差图和新的右视差图产生衡量一致性的项.
    with tf.variable_scope('left-right'):
        self.right_to_left_disp = [self.generate_image_left(self.disp_right_est[i], self.disp_left_est[i])  for i in range(4)]
        self.left_to_right_disp = [self.generate_image_right(self.disp_left_est[i], self.disp_right_est[i]) for i in range(4)]

    # DISPARITY SMOOTHNESS
    with tf.variable_scope('smoothness'):
        self.disp_left_smoothness  = self.get_disparity_smoothness(self.disp_left_est,  self.left_pyramid)
        self.disp_right_smoothness = self.get_disparity_smoothness(self.disp_right_est, self.right_pyramid)

主要包括四个部分:

  1. 视差图:包括用来生成左图和生成右图的视差图, 生成d^r, d^l
  2. 原图估计:通过左(右)原图和右(左)视差图生成右(左)图的估计, 生成图片 Ir 和 Il
  3. 一致性:通过右(左)视差图和左(右)视差图生成新的右(左)视差图:计算用来计算左右图一致性,
  4. 平滑性:通过左(右)原图和左(右)图估计计算平滑项

生成损失

损失
损失函数包括以下几个部分:

  1. 原图和重建的图之间的差异,用L1范数表示
  2. 原图和重建的图的SSIM,并且左右图加权求和
  3. 视差图平滑损失
  4. 左右图一致性损失
self.total_loss = self.image_loss + self.params.disp_gradient_loss_weight * self.disp_gradient_loss + self.params.lr_loss_weight * self.lr_loss

不足:

  1. 精髓部分, 作者改进的损失函数部分没有仔细考察;
  2. 双线性差值不是很理解;
  3. Tensorflow中的队列尚未仔细揣摩;
  4. 测试部分代码未查阅
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值