3D点云重建0-05:MVSNet-源码解析(1)-数据集了解,预处理详解

以下链接是个人关于MVSNet(R-MVSNet)-多视角立体深度推导重建 所有见解,如有错误欢迎大家指出,我会第一时间纠正。有兴趣的朋友可以加微信:17575010159 相互讨论技术。若是帮助到了你什么,一定要记得点赞!因为这是对我最大的鼓励。 文末附带 \color{blue}{文末附带} 文末附带 公众号 − \color{blue}{公众号 -} 公众号 海量资源。 \color{blue}{ 海量资源}。 海量资源

3D点云重建0-00:MVSNet(R-MVSNet)–目录-史上最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/102852209

数据集介绍

从前面我们可以知道代码的训练是从mvsnet/train.py开始的,该小结我们不讲解训练代码,我们先来看看数据预处理,在这之前,相信大家已经下载好了数据了,不少哥们对数据集都比较迷糊吧,其实我开始也很懵逼,不多说先看看DTU数据集,本人的在这个路径下面mvsnet\training_data\dtu_training,可以看到如下(希望你的和我们长得一样,不然你完蛋了):
在这里插入图片描述
有3个文件夹,一个一个的进行介绍:
Cameras:该文件主要保存的是各种摄像头的参数,我们知道。摄像头不同,拍摄出来的效果肯定也是不一样的。那么这里保存的64个摄像头的信息。但是训练数据只使用了49个。所有在其中的train文件夹可以看到49个摄像头配置文件。

Rectified:该文件保存的是使用各种摄像头拍摄(49个)的图片,并且每种摄像头还使用7种不同的光照强度,比如我们来其中的一个子文件夹scan1_train:
3D点云重建0-05:MVSNet-源码解析(1)-数据集了解,预处理详解_第1张图片
其上每一行(如红色圈出部分)都是使用同一个摄像头拍摄的,但是他们拍摄时的光照强度是不一样的。并且他们命名也是有规则的,如rect_001_0_r5000.png,其中的_001_表示的是使用的是第0个摄像头参数(注意,这里要减去1),_0_代表的是第一种光照强度。所以我们在每个scan1_train看到的图片都是49x7=343张。

Depths:这里就是对应Rectified中图片的深度图以及对应的掩码了。首先来看深度图Depths\scan1_train,该文件下有两种格式的图片,pfm格式的表示深度图,png格式的表示掩码。先来说说pfm,如depth_map_0000.pfm,就表示就是前面Rectified\scan1_train\rect_001_x_r5000.png图片。这里我使用x代表光照强度。一个摄像头拍摄(此时摄像头不会移动)的图片,虽然光照强度不一样,但是拍摄物体的形状是一样的,所以其深度图是一样的,即:
在这里插入图片描述
7张图片对应一个一个深度图depth_map_0000.pfm如下:
3D点云重建0-05:MVSNet-源码解析(1)-数据集了解,预处理详解_第2张图片在这里大家注意一下,深度图和拍摄图是上下翻转的。其对应的掩码mask如下:
3D点云重建0-05:MVSNet-源码解析(1)-数据集了解,预处理详解_第3张图片
mask 图像是给来干嘛的了?主要是把背景和我们需要测量的物体分开来。在源码中利用mask,可以只对前景进行反向传播优化,而背景没有必要进行反向传播。

好了,到这里大家对数据集,有了解,我再提几个点dtu_training\Cameras\train保存的都是摄像头参数,摄像头参数包含了extrinsic(外参-矩阵形式),intrinsic(内参-矩阵形式)。这个参数前面提到过,是用两个面进行转化使用的,除此之外,大家看看下面这几个个博客:
针孔相机模型:https://www.cnblogs.com/majiale/p/9293499.html
深入解读相机矩阵:https://blog.csdn.net/lingchen2348/article/details/83052214
单应性矩阵的理解及求解:https://blog.csdn.net/liubing8609/article/details/85340015
从第上面的1,2篇博客,大家应该可以知道,这个3维空间映射到2维图像的大致过程(其实细节我也不了解)。然后通过第3篇博客,我们可以知道,2维到2维之间的,也可以映射的。为什么呢?比如我们的左眼和右眼,可以先把左眼映射到立体空间,然后又从立体空间映射到右眼。
好了,数据集的介绍就到这里了,下面我们来了解数据预处理的过程。

数据预处理-样本路径获取

从前面的博客,可以知道训练代码为MVSNet-master/mvsnet/train.py,进入查看可以看到如下(不太想分析的。直接后面看总结):

def main(argv=None):  # pylint: disable=unused-argument
    """ program entrance """
    # Prepare all training samples
    sample_list = gen_dtu_resized_path(FLAGS.dtu_data_root)
    # Shuffle
    random.shuffle(sample_list)
    # Training entrance.
    train(sample_list)

首先调用gen_dtu_resized_path,产生样本的路径,注意,此时还没有读取图片,改函数的实现在preprocess.py文件:

def gen_dtu_resized_path(dtu_data_folder, mode='training'):
    """ generate data paths for dtu dataset """
    sample_list = []

    # parse camera pairs,该文件保存的是所有摄像头类型的的参数,包含了最大,最小深度,偏转角度等
    cluster_file_path = dtu_data_folder + '/Cameras/pair.txt'
    # cluster_list = open(cluster_file_path).read().split()
    cluster_list = file_io.FileIO(cluster_file_path, mode='r').read().split()

    # 3 sets,把训练集和测试集,进行固定的划分,应该是为了别人更加好复现同样的效果
    training_set = [2, 6, 7, 8, 14, 16, 18, 19, 20, 22, 30, 31, 36, 39, 41, 42, 44,
                    45, 46, 47, 50, 51, 52, 53, 55, 57, 58, 60, 61, 63, 64, 65, 68, 69, 70, 71, 72,
                    74, 76, 83, 84, 85, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100,
                    101, 102, 103, 104, 105, 107, 108, 109, 111, 112, 113, 115, 116, 119, 120,
                    121, 122, 123, 124, 125, 126, 127, 128]

    validation_set = [3, 5, 17, 21, 28, 35, 37, 38, 40, 43, 56, 59, 66, 67, 82, 86, 106, 117]


    data_set = []
    if mode == 'training':
        data_set = training_set
    elif mode == 'validation':
        data_set = validation_set

    # for each dataset,循环每个子数据集
    for i in data_set:

        image_folder = os.path.join(dtu_data_folder, ('Rectified/scan%d_train' % i))
        cam_folder = os.path.join(dtu_data_folder, 'Cameras/train')
        depth_folder = os.path.join(dtu_data_folder, ('Depths/scan%d_train' % i))

        if mode == 'training':
            # for each lighting,对每个关照强度操作一次
            for j in range(0, 7):
                # for each reference image,对每个不同的摄像头配置都进行一次操作,
                # 这里的两层for循环时因为,每次scan%d_train子数据集中,使用了cluster_list[0]种摄像头配置进行拍摄,并且每种配置都使用了7种关照强度
                for p in range(0, int(cluster_list[0])):
                    paths = []
                    # ref image
                    ref_index = int(cluster_list[22 * p + 1])
                    ref_image_path = os.path.join(
                        image_folder, ('rect_%03d_%d_r5000.png' % ((ref_index + 1), j)))
                    ref_cam_path = os.path.join(cam_folder, ('%08d_cam.txt' % ref_index))
                    paths.append(ref_image_path)
                    paths.append(ref_cam_path)
                    # view images
                    for view in range(FLAGS.view_num - 1):
                        view_index = int(cluster_list[22 * p + 2 * view + 3])
                        view_image_path = os.path.join(
                            image_folder, ('rect_%03d_%d_r5000.png' % ((view_index + 1), j)))
                        view_cam_path = os.path.join(cam_folder, ('%08d_cam.txt' % view_index))
                        paths.append(view_image_path)
                        paths.append(view_cam_path)
                    # depth path
                    depth_image_path = os.path.join(depth_folder, ('depth_map_%04d.pfm' % ref_index))
                    paths.append(depth_image_path)
                    sample_list.append(paths)
        elif mode == 'validation':
 			......
 			......

    return sample_list

由于训练样本和验证样本的代码相差不大,我就只粘贴的训练的。其上可以看到,先把数据进行划分,按照scan来进行划分。其对于每个scan有两个循环:

            # for each lighting,对每个关照强度操作一次
            for j in range(0, 7):
                # for each reference image,对每个不同的摄像头配置都进行一次操作,
                # 这里的两层for循环时因为,每次scan%d_train子数据集中,使用了cluster_list[0]种摄像头配置进行拍摄,并且每种配置都使用了7种关照强度
                for p in range(0, int(cluster_list[0])):

通过两个循环,相当与把每个scan的所有图片都操作一遍。还有一个循环就是:

for view in range(FLAGS.view_num - 1):
	.....
	paths.append(view_image_path)
	paths.append(view_cam_path)

这个循环是为了选取多个视角图,也就是s img,当然还有拍摄该视角图的摄像头参数。那么最后gen_dtu_resized_path得到的是什么样的东西了?我们来看一个样本,也就是代码:

sample_list = gen_dtu_resized_path(FLAGS.dtu_data_root)

中的sample_list[0],改样本打印如下:

0 = {str}\\Rectified/scan2_train\\rect_001_0_r5000.png'
1 = {str}\\Cameras/train\\00000000_cam.txt'
2 = {str}\\Rectified/scan2_train\\rect_011_0_r5000.png'
3 = {str}\\Cameras/train\\00000010_cam.txt'
4 = {str}\\Rectified/scan2_train\\rect_002_0_r5000.png'
5 = {str}\\Cameras/train\\00000001_cam.txt'
6 = {str}\\Depths/scan2_train\\depth_map_0000.pfm'

可以看到,一个样本中需要3个视角图片,其中第1张为r img,剩下跟随的两张为s img,每张图片路径的后面都跟随了拍摄改视角图片的摄像头参数,最后就是r img对应的深度图。我们打印print(len(sample_list))为27097,样本的计算方式为,训练共119个scan,49个摄像头,7种光照。顾119x49x7=27097(没有真的去算,但是发现个位计算是对的,应该问题不大)。

数据预处理-迭代器介绍

前面已经分下完了gen_dtu_resized_path,现在我们来看看train(sample_list),进入代码如下:

def train(traning_list):
    """ training mvsnet """
    training_sample_size = len(traning_list)
    if FLAGS.regularization == 'GRU':
        training_sample_size = training_sample_size * 2
    print ('sample number: ', training_sample_size)

    with tf.Graph().as_default(), tf.device('/cpu:0'):

        ########## data iterator #########
        # training generators
        # 获取数训练数据的迭代器,进入MVSGenerator可以看到详细解析
        training_generator = iter(MVSGenerator(traning_list, FLAGS.view_num))
        generator_data_type = (tf.float32, tf.float32, tf.float32)
        # dataset from generator
        training_set = tf.data.Dataset.from_generator(lambda: training_generator, generator_data_type)
        training_set = training_set.batch(FLAGS.batch_size)
        training_set = training_set.prefetch(buffer_size=1)
        # iterators
        training_iterator = training_set.make_initializable_iterator()
        .....
        ......

该篇博客不讲解网络结构,所以后面的代码我就没贴出来了,主要来看看数据迭代器的对象,怎么生成了的,又做了什么即上面代码中MVSGenerator的实现:

class MVSGenerator:
    """ data generator class, tf only accept generator without param """
    def __init__(self, sample_list, view_num):
        self.sample_list = sample_list
        self.view_num = view_num
        self.sample_num = len(sample_list)
        self.counter = 0
    
    def __iter__(self):
        while True:
            for data in self.sample_list: 
                start_time = time.time()

                ###### read input data ######
                images = []
                cams = []
                # 获得各个视角的图片,这里的图片包含了一张r img,以及view_num-1张s img
                # 以及拍摄该写图片对应的摄像头参数
                for view in range(self.view_num):
                    image = center_image(cv2.imread(data[2 * view]))
                    # cam[2,4,4]  其中cam[0]存放的是Cameras/train/下txt文件的extrinsic参数(拍摄图片时,摄像头相关的参数)
                    # cam[1]存放的是Cameras/train/下txt文件的intrinsic(拍摄图片时,摄像头相关的参数)
                    cam = load_cam(open(data[2 * view + 1]))
                    cam[1][3][1] = cam[1][3][1] * FLAGS.interval_scale
                    # 图片的像素,和图片对应的摄像头参数,一一对应加入列表
                    images.append(image)
                    cams.append(cam)

                # data[2 * self.view_num]这里表示的深度图的路径,即Depths下的深度图
                print(data[2 * self.view_num])
                # 读取深度图
                depth_image = load_pfm(open(data[2 * self.view_num], 'rb'))

                # mask out-of-range depth pixels (in a relaxed range)
                # 根据摄像头参数文件,确定配置深度图的最低和最高深度,然后把深度图中,
                # 其深度像素不在最低和最高深度之间的像素去全部清0,这样就得到了深度图对应的mask图
                depth_start = cams[0][1, 3, 0] + cams[0][1, 3, 1]
                depth_end = cams[0][1, 3, 0] + (FLAGS.max_d - 2) * cams[0][1, 3, 1]

                # depth_image[128,160,1]
                depth_image = mask_depth_image(depth_image, depth_start, depth_end)

                # return mvs input
                self.counter += 1
                duration = time.time() - start_time
                images = np.stack(images, axis=0)
                cams = np.stack(cams, axis=0)
                print('Forward pass: d_min = %f, d_max = %f.' % \
                    (cams[0][1, 3, 0], cams[0][1, 3, 0] + (FLAGS.max_d - 1) * cams[0][1, 3, 1]))
                # 返回图片像素,以及拍摄该图的摄像头参数,以及拍摄获得的深度图。
                yield (images, cams, depth_image) 

                # return backward mvs input for GRU
                if FLAGS.regularization == 'GRU':
                    self.counter += 1
                    start_time = time.time()
                    cams[0][1, 3, 0] = cams[0][1, 3, 0] + (FLAGS.max_d - 1) * cams[0][1, 3, 1]
                    cams[0][1, 3, 1] = -cams[0][1, 3, 1]
                    duration = time.time() - start_time
                    print('Back pass: d_min = %f, d_max = %f.' % \
                        (cams[0][1, 3, 0], cams[0][1, 3, 0] + (FLAGS.max_d - 1) * cams[0][1, 3, 1]))
                    yield (images, cams, depth_image) 

其实很简单,就看他的返回就知道了:

yield (images, cams, depth_image) 

就是返回根据sample_list的路径读取的像素,摄像矩阵参数,以及深度图。这里注意一下cams的形状为[3,2,4,4],3表示视角图,2表示摄像头的外参和内参。

结语

那么,数据预处理,就介绍到这里,接下来,就是对网络的讲解了,大家给个赞啊,看我写得这么认真的份上。

在这里插入图片描述

你可能感兴趣的:(3维重建-点云,3维重建,3D点云,深度估算)