前言

  • 以场景点云为中心的点云任务(如重建、配准等)常常采用3D相机采集数据,而以物体点云为中心的点云任务大多是从3D模型离散化得到的,因为真实环境采集到的物体点云需要与环境分割并去噪的,较为麻烦,不利于大规模数据集的制作。
  • 常见的开源点云数据集:
    • 场景点云:3DMatch
    • 物体点云:ModelNet40、ShapeNet
  • 考虑到物体点云的采集困难,一些在仿真环境下获取点云的方式被提出,本篇博客重点介绍使用Python脚本控制Blender来采集某一视角下的深度图,接着生成点云,注意,由于视角受限,单帧点云只显示了物体的局部外观,需要多个不同位姿下相机拍摄后重建才能得到完整的物体点云,甚至需要点云补全。

MVP 数据集介绍

  • Github项目链接:paul007pl/MVP_Benchmark: MVP Benchmark for Multi-View Partial Point Cloud Completion and Registration (github.com)
  • MVP_Benchmark 是一个点云补全和配准等任务的竞赛,官方在ShapeNet模型上使用Blender深度相机制作了点云数据集,其中分别包括点云补全和点云配准数据集,在以3D模型为中心的球面上均匀分布26个固定位姿的深度相机,分别拍摄得到26个局部点云。
  • 数据集下载:
  • MVP点云配准数据集中的局部点云的可视化如下:
    • 点云对均为部分点云,点数均为 2048;
    • 不同视角拍摄的点云的原始位姿相同,经过随机的齐次变换后送入网络进行训练。

如何制作MVP数据集

环境配置

pip install image
pip install imath
sudo apt-get install libopenexr-dev zlib1g-dev # 安装依赖
pip install openexr

编写脚本

脚本一:采集深度图

  • 控制Blender采集深度图。
  • 脚本执行命令:
blender -b -P render_depth.py # 注意:终端执行即可,这个py文件不使用python命令执行,而是blender的命令,blender的可视化窗口不需要打开
import bpy
import mathutils
import numpy as np
import os
import sys
import time


def random_pose():
    angle_x = np.random.uniform() * 2 * np.pi
    angle_y = np.random.uniform() * 2 * np.pi
    angle_z = np.random.uniform() * 2 * np.pi
    Rx = np.array([[1, 0, 0],
                   [0, np.cos(angle_x), -np.sin(angle_x)],
                   [0, np.sin(angle_x), np.cos(angle_x)]])
    Ry = np.array([[np.cos(angle_y), 0, np.sin(angle_y)],
                   [0, 1, 0],
                   [-np.sin(angle_y), 0, np.cos(angle_y)]])
    Rz = np.array([[np.cos(angle_z), -np.sin(angle_z), 0],
                   [np.sin(angle_z), np.cos(angle_z), 0],
                   [0, 0, 1]])
    R = np.dot(Rz, np.dot(Ry, Rx))
    # Set camera pointing to the origin and 1 unit away from the origin
    t = np.expand_dims(R[:, 2]*10, 1)
    pose = np.concatenate([np.concatenate([R, t], 1), [[0, 0, 0, 1]]], 0)
    return pose


def setup_blender(width, height, focal_length):
    # camera
    camera = bpy.data.objects['Camera']
    camera.data.angle = np.arctan(width / 2 / focal_length) * 2

    # render layer
    scene = bpy.context.scene
    scene.render.filepath = 'buffer'
    scene.render.image_settings.color_depth = '16'
    scene.render.resolution_percentage = 100
    scene.render.resolution_x = width
    scene.render.resolution_y = height

    # compositor nodes 合成器节点
    scene.use_nodes = True
    tree = scene.node_tree
    rl = tree.nodes.new('CompositorNodeRLayers')
    output = tree.nodes.new('CompositorNodeOutputFile')
    output.base_path = ''
    output.format.file_format = 'OPEN_EXR'
    tree.links.new(rl.outputs['Depth'], output.inputs[0])

    # remove default cube
    bpy.data.objects['Cube'].select_set(True)
    bpy.ops.object.delete()

    return scene, camera, output


if __name__ == '__main__':

    model_dir = "写你自己的"
    output_dir = "写你自己的"
    num_scans = 10 # 扫描次数,自定义即可

    # 设置深度图的宽高和相机焦点,可以修改深度图和点云的分辨率
    nn = 10
    width = 160 * nn
    height = 120 * nn
    focal = 100 * nn
    scene, camera, output = setup_blender(width, height, focal)
    intrinsics = np.array([[focal, 0, width / 2], [0, focal, height / 2], [0, 0, 1]])

    model_list = ['模型名称1', '模型名称1', '模型名称1'] # 不包含后缀名
    open('blender.log', 'w+').close()
    os.system('rm -rf %s' % output_dir)
    os.makedirs(output_dir)
    np.savetxt(os.path.join(output_dir, 'intrinsics.txt'), intrinsics, '%f')

    for model_id in model_list:
        start = time.time()
        exr_dir = os.path.join(output_dir, 'exr', model_id)
        pose_dir = os.path.join(output_dir, 'pose', model_id)
        os.makedirs(exr_dir)
        os.makedirs(pose_dir)

        # Redirect output to log file
        old_os_out = os.dup(1)
        os.close(1)
        os.open('blender.log', os.O_WRONLY)

        # Import mesh model
        model_path = os.path.join(model_dir, model_id + '.ply') # 我的3D模型后缀名是 ply
        bpy.ops.import_mesh.ply(filepath=model_path)

        # Rotate model by 90 degrees around x-axis (z-up => y-up) to match ShapeNet's coordinates
        bpy.ops.transform.rotate(value=-np.pi / 2, orient_axis='X')

        # Render
        for i in range(num_scans):
            scene.frame_set(i)
            pose = random_pose()
            camera.matrix_world = mathutils.Matrix(pose)
            output.file_slots[0].path = os.path.join(exr_dir, '#.exr')
            bpy.ops.render.render(write_still=True)
            np.savetxt(os.path.join(pose_dir, '%d.txt' % i), pose, '%f')

        # Clean up
        bpy.ops.object.delete()
        for m in bpy.data.meshes:
            bpy.data.meshes.remove(m)
        for m in bpy.data.materials:
            m.user_clear()
            bpy.data.materials.remove(m)

        # Show time
        os.close(1)
        os.dup(old_os_out)
        os.close(old_os_out)
        print('%s done, time=%.4f sec' % (model_id, time.time() - start))

脚本二:深度图生成点云

  • 脚本执行命令:
    python process_exr.py param1 param2 param3
    
  • 参数解读:
    • param1: 你的路径/pcn/render/output/intrinsics.txt
    • param2: 输出到某个文件夹
    • param3: 扫描的数量,与脚本一中 num_scans 的值相同
import Imath
import OpenEXR
import argparse
import array
import numpy as np
import os
import open3d as o3d


def read_exr(exr_path, height, width):
    file = OpenEXR.InputFile(exr_path)
    depth_arr = array.array('f', file.channel('R', Imath.PixelType(Imath.PixelType.FLOAT)))
    depth = np.array(depth_arr).reshape((height, width))
    depth[depth < 0] = 0
    depth[np.isinf(depth)] = 0
    return depth


def depth2pcd(depth, intrinsics, pose):
    inv_K = np.linalg.inv(intrinsics)
    inv_K[2, 2] = -1
    depth = np.flipud(depth) # 将矩阵进行上下翻转
    y, x = np.where(depth < 65504) # 返回索引
    # image coordinates -> camera coordinates
    points = np.dot(inv_K, np.stack([x, y, np.ones_like(x)] * depth[y, x], 0))
    # camera coordinates -> world coordinates
    points = np.dot(pose, np.concatenate([points, np.ones((1, points.shape[1]))], 0)).T[:, :3]
    return points


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    # parser.add_argument('list_file')
    parser.add_argument('intrinsics_file')
    parser.add_argument('output_dir')
    parser.add_argument('num_scans', type=int, default=1)
    args = parser.parse_args()

    model_list = ['模型名称1', '模型名称1', '模型名称1'] # 不包含后缀名

    intrinsics = np.loadtxt(args.intrinsics_file)
    width = int(intrinsics[0, 2] * 2)
    height = int(intrinsics[1, 2] * 2)

    for model_id in model_list:
        depth_dir = os.path.join(args.output_dir, 'depth', model_id)
        pcd_dir = os.path.join(args.output_dir, 'pcd', model_id)
        os.makedirs(depth_dir, exist_ok=True)
        os.makedirs(pcd_dir, exist_ok=True)
        for i in range(args.num_scans):
            exr_path = os.path.join(args.output_dir, 'exr', model_id, '%d.exr' % i)
            pose_path = os.path.join(args.output_dir, 'pose', model_id, '%d.txt' % i)

            depth = read_exr(exr_path, height, width)
            depth_img = o3d.geometry.Image(np.uint16(depth * 1000))
            o3d.io.write_image(os.path.join(depth_dir, '%d.png' % i), depth_img)

            pose = np.loadtxt(pose_path)
            points = depth2pcd(depth, intrinsics, pose)
            pcd = o3d.geometry.PointCloud()
            pcd.points = o3d.utility.Vector3dVector(points)
            o3d.io.write_point_cloud(os.path.join(pcd_dir, '%d.pcd' % i), pcd)

实验结果