Blender-Datasets-auto-generator-based-on-Blender

使用 Blenderproc 合成数据集。

资源

代码

Config

python
import bpy
import math, mathutils
import os, random
import json
import numpy as np
 
mode  = 'train' # FLAGS : train or val
worldPath = 'pathToYourBackgrounds/'
objsPath = 'pathToYour3DObjso/'
imgPath = f'/home/xxx/Documents/myDataset/images/{mode}/'
labelPath = f'/home/xxx/Documents/myDataset/labels/{mode}/'
kittiCalibsPath = '/home/xxx/Documents/myDataset/kittiCalibs/'
kittiLabelsPath = '/home/xxx/Documents/myDataset/kittiLabels/'
 
picsNum = 2000
# Number of objects in a scene
objsNum = 4
if objsNum > len(os.listdir(objsPath)):
    objsNum = len(os.listdir(objsPath)) 
cameraLens = 15  # 相机焦距
img_w = 960
img_h = 540
# Worlds changing frequency
freq_CTW = 10
objNameList = []

main()

  1. 函数 clearAll(): 清空场景中所有的对象。
  2. 函数 loadWorlds(): 加载不同的场景环境。
  3. 函数 loadObjs(): 加载需要渲染的物体。
  4. 函数 loadCamera(): 加载摄像机。
  5. scene.camera = scene.objects['Camera']: 将场景中的相机设置为所加载的相机。
  6. scene.render.resolution_xscene.render.resolution_y: 分别设置输出图像的宽度和高度。
  7. 函数 K = calibCamera(): 计算相机内部参数,即相机的内参矩阵。
  8. 函数 changeTheWorld(): 改变场景环境(比如更改场景光照等),使得每张图片看起来不完全一样。
  9. for i in range(picsNum): 循环输出多张图片。
  10. 函数 changeObjs(): 改变场景中的物体位置和姿态,使得每张图片中物体的位置和姿态发生变换。
  11. 函数 bougeLe(): 让场景中的物体随机移动一定距离(模拟物体在真实场景中的运动)。
  12. 函数 snapIt(scene, i): 对场景进行拍照,生成一张渲染后的图像。
  13. calId = f'{kittiCalibsPath}{i}.txt': 生成保存相机内参矩阵的文件名。
  14. with open(calId,'w',encoding='utf-8') as fc:: 打开一个文件进行写入相机内参矩阵。
  15. for p in K: fc.writelines(p): 写入相机内参矩阵。
  16. clearAll(): 清空场景中所有的对象。

其中,labelIt(i) 这一部分是待实现的标注代码,用于对生成的渲染图像进行标注。

python
def main():
    clearAll()
    loadWorlds()
    loadObjs()
    loadCamera()
    scene = bpy.context.scene  # 获取当前场景
    scene.camera = scene.objects['Camera']  # 将场景中的相机设置为所加载的相机
    scene.render.resolution_x = img_w  # 设置输出图像的宽度
    scene.render.resolution_y = img_h  # 设置输出图像的高度
    K = calibCamera()  # 计算相机内部参数,即相机的内参矩阵
    changeTheWorld()  # 改变场景环境(比如更改场景光照等),使得每张图片看起来不完全一样
    for i in range(picsNum):  #  循环输出多张图片
        if i % freq_CTW == 0:
            changeTheWorld()
        changeObjs()  # 改变场景中的物体位置和姿态,使得每张图片中物体的位置和姿态发生变换
        bougeLe()  # 让场景中的物体随机移动一定距离(模拟物体在真实场景中的运动)
        snapIt(scene, i)  # 对场景进行拍照,生成一张渲染后的图像
        labelIt(i)  # <- TODO
        calId =  f'{kittiCalibsPath}{i}.txt'  # 写入参数
        with open(calId,'w',encoding='utf-8') as fc:
            for p in K:
                fc.writelines(p)
    #clearAll() 
if __name__ == '__main__':
    main()

clearAll()

删除场景中的所有对象:

python
def clearAll():
    for obj in bpy.data.objects:
        bpy.data.objects.remove(obj)
    for img in bpy.data.images:
        bpy.data.images.remove(img)
    for ma in bpy.data.materials:
        bpy.data.materials.remove(ma)
    for me in bpy.data.meshes:
        bpy.data.meshes.remove(me)    
    for ng in bpy.data.node_groups:
        bpy.data.node_groups.remove(ng)
    for cd in bpy.data.cameras:
        bpy.data.cameras.remove(cd)

loadWorlds()

world 是一系列 *.hdr 文件并存放在 worldPath 中,加载它们:

python
def loadWorlds():
    world = bpy.context.scene.world
    world.use_nodes = True
    enode = bpy.context.scene.world.node_tree.nodes.new('ShaderNodeTexEnvironment')
    worldFiles = os.listdir(worldPath)
    for file in worldFiles:
        bpy.data.images.load(worldPath + file)

loadObjs()

objs 是一系列 *.blend 文件并存放在 objsPath 中,加载它们:

python
def loadObjs():
    objsList = os.listdir(objsPath)
    for objName in objsList:
        file_path = os.path.join(objsPath, objName)
        objN = objName.split('.')[0]  # 获取物体名称(去除后缀)
        objNameList.append(objN)  # 将物体名称添加到物体名称列表中
        # 加载 .obj 文件并将其中包含的物体(对象)添加到当前场景中
        with bpy.data.libraries.load(file_path,link=False) as (data_from, data_to):
            # 只将以当前物体名称开头的对象添加到当前场景中,避免将不需要的对象添加到场景中
            data_to.objects = [name for name in data_from.objects if name.startswith(objN)]
    # 该部分是注释掉的代码,原本是想将已经加载的物体名称保存到 YAML 文件中,但是最终没有实现。
    #with open(cocoYaml,'w',encoding='utf-8') as fc:
        #yaml.dump(objNameList,fc)  

loadCamera()

python
def loadCamera():
    camera_data = bpy.data.cameras.new(name='Camera')  # 创建一个新的相机对象
    camera_data.lens = cameraLens  # 设置相机的焦距
    camera_object = bpy.data.objects.new('Camera', camera_data)  # 绑定相机对象
    camera_object.rotation_euler[0] = math.pi / 2  # 使相机的 Z 轴朝向场景中的物体(而非朝上)
    bpy.context.scene.collection.objects.link(camera_object)  # 将相机对象添加到场景中
    for obj in bpy.data.objects:  # 循环遍历场景中的所有对象
        if obj.name != 'Camera':  # 如果对象名称不是相机,则将其添加为相机的子对象,使得在相机移动时,所有与之相关的对象也会跟着移动
            obj.parent = bpy.data.objects['Camera'] 

calibCamera()

python
def calibCamera():
    # 获取场景中名为 Camera 的相机对象,并获取该相机的数据对象
    cam = bpy.data.objects['Camera']
    camd = cam.data
    # 获取相机的焦距(单位:毫米)
    f_in_mm = camd.lens
    # 获取场景的参数,包括图像分辨率和缩放比例。
    # 其中 resolution_x_in_px 和 resolution_y_in_px 分别表示图像的宽度和高度(单位:像素),而 scale 表示缩放比例
    scene = bpy.context.scene
    resolution_x_in_px = scene.render.resolution_x
    resolution_y_in_px = scene.render.resolution_y
    scale = scene.render.resolution_percentage / 100
    # 获取相机的传感器宽度和高度(单位:毫米),以及像素纵横比
    sensor_width_in_mm = camd.sensor_width
    sensor_height_in_mm = camd.sensor_height
    pixel_aspect_ratio = scene.render.pixel_aspect_x / scene.render.pixel_aspect_y
    # 如果相机的传感器安装方式是垂直(即竖放),则传感器高度为固定值,宽度会随着像素纵横比的变化而变化;否则,传感器宽度为固定值,高度会随着像素纵横比的变化而变化
    if (camd.sensor_fit == 'VERTICAL'):
        # the sensor height is fixed (sensor fit is horizontal), 
        # the sensor width is effectively changed with the pixel aspect ratio
        # 根据相机的传感器大小以及像素纵横比等参数,计算出两个尺度参数 s_u 和 s_v
        s_u = resolution_x_in_px * scale / sensor_width_in_mm / pixel_aspect_ratio 
        s_v = resolution_y_in_px * scale / sensor_height_in_mm
    else: # 'HORIZONTAL' and 'AUTO'
    # the sensor width is fixed (sensor fit is horizontal), 
        # the sensor height is effectively changed with the pixel aspect ratio
        pixel_aspect_ratio = scene.render.pixel_aspect_x / scene.render.pixel_aspect_y
        s_u = resolution_x_in_px * scale / sensor_width_in_mm
        s_v = resolution_y_in_px * scale * pixel_aspect_ratio / sensor_height_in_mm
  
        # Parameters of intrinsic calibration matrix K
    	# 计算出相机的内参矩阵 K 中的元素,并分别放在 alpha_u、alpha_v、u_0、v_0 和 skew 变量中
        alpha_u = f_in_mm * s_u
        alpha_v = f_in_mm * s_v
        u_0 = resolution_x_in_px * scale / 2
        v_0 = resolution_y_in_px * scale / 2
        skew = 0 # only use rectangular pixels
        # K = Matrix(
        # ((alpha_u, skew,    u_0),
        # (    0  ,  alpha_v, v_0),
        # (    0  ,    0,      1 )))
        # 将内参矩阵 K 的各个元素和其他参数组成了一个矩阵(实际上只使用了矩阵中的第三行),并存储在名为 calList 的列表中。其中用到了 f-string 来方便地生成字符串。
        calList = [[f'P0: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n'],
                   [f'P1: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n'],
                   [f'P2: {alpha_u} {skew} {u_0} 0.0 0.0 {alpha_v} {v_0} 0.0 0.0 0.0 1.0 0.0\n'],
                   [f'P3: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n'],
                   [f'R0_rect: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n'],
                   [f'Tr_velo_to_cam: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n'],
                   [f'Tr_imu_to_velo: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0\n']]
        return calList

changeTheWorld()

python
def changeTheWorld():
    while True:
        # 随机选择一张 hdr 贴图
        wd = random.choice(bpy.data.images)
        if wd.name.endswith('hdr'):
            break
    # 将其设置为环境纹理贴图
    bpy.context.scene.world.node_tree.nodes['Environment Texture'].image = wd

changeObjs()

python
def changeObjs():
    # 遍历场景中的所有物体,将它们从当前激活的 collection 中删除,除了名为 "Camera" 的相机
    for obj in bpy.context.collection.objects:
        if obj.name != 'Camera':
            bpy.context.collection.objects.unlink(obj)
    # 定义一个空列表 nameList
    nameList = []
    # 使用 while 循环随机选择场景中的物体,直到选择的数量达到 objsNum(objsNum 是一个变量,需要在函数之外定义),并将其添加到当前激活的 collection 中
    while len(nameList) < objsNum:
        obj = random.choice(bpy.data.objects)
        if not (obj.name in nameList) and obj.name != 'Camera':
            bpy.context.collection.objects.link(obj)
            nameList.append(obj.name)

bougeLe()

python
def bougeLe():
    # 遍历场景中的所有物体,对于除了名为 "Camera" 的相机对象之外的所有对象,将其选中,然后使用 while 循环计算一个比例尺来设置对象的位置
    for obj in bpy.data.objects:
        if obj.name != 'Camera':
            obj.select_set(True)
            # 使用 while 循环是因为在某些情况下,计算比例尺可能会出现异常而导致程序崩溃,因此使用 try 和 excerpt 语句可以让程序不断地重试直到成功为止
            while True:              
                try:
                    scale = math.sqrt(max(obj.dimensions)) * bpy.data.objects['Camera'].data.lens
                    obj.location = (0, 0, -0.08 * scale)
                    break
                excerpt:
                    continue
            # 随机变换对象的位置、旋转和缩放,并传递一些参数来控制变换的幅度和随机性
            bpy.ops.object.randomize_transform(random_seed = random.randint(0, 100), loc=(0.24, 0.1, 0.05), rot=(3, 3, 3), scale=(1, 1, 1))  
        else:
            # 对于名为 "Camera" 的相机对象,使用 random.uniform 方法生成随机的旋转角度,并将其赋值给对象的 Z 轴旋转角度,以模拟相机的扫描动态
            obj.rotation_euler[2] = 4 * random.uniform(-0.7, 0.7)

snapIt()

python
def snapIt(scene, idNum):
    for obj in bpy.data.objects:
        if obj.name != 'Camera':
            # 只选择相机
            obj.select_set(False)
    # 是通过将 imgPath 和 idNum 进行字符串拼接而得到的文件路径和文件名,用于保存渲染图像
    imId = f'{imgPath}{idNum}.png'
    scene.render.filepath = (imId) 
    # 调用 bpy.ops.render.render 方法对场景进行渲染,最终将渲染结果保存为一个 PNG 格式的图像文件
    bpy.ops.render.render(write_still=True)