资源
代码
Config
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()
- 函数
clearAll(): 清空场景中所有的对象。 - 函数
loadWorlds(): 加载不同的场景环境。 - 函数
loadObjs(): 加载需要渲染的物体。 - 函数
loadCamera(): 加载摄像机。 scene.camera = scene.objects['Camera']: 将场景中的相机设置为所加载的相机。scene.render.resolution_x和scene.render.resolution_y: 分别设置输出图像的宽度和高度。- 函数
K = calibCamera(): 计算相机内部参数,即相机的内参矩阵。 - 函数
changeTheWorld(): 改变场景环境(比如更改场景光照等),使得每张图片看起来不完全一样。 for i in range(picsNum): 循环输出多张图片。- 函数
changeObjs(): 改变场景中的物体位置和姿态,使得每张图片中物体的位置和姿态发生变换。 - 函数
bougeLe(): 让场景中的物体随机移动一定距离(模拟物体在真实场景中的运动)。 - 函数
snapIt(scene, i): 对场景进行拍照,生成一张渲染后的图像。 calId = f'{kittiCalibsPath}{i}.txt': 生成保存相机内参矩阵的文件名。with open(calId,'w',encoding='utf-8') as fc:: 打开一个文件进行写入相机内参矩阵。for p in K: fc.writelines(p): 写入相机内参矩阵。clearAll(): 清空场景中所有的对象。
其中,labelIt(i) 这一部分是待实现的标注代码,用于对生成的渲染图像进行标注。
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()
删除场景中的所有对象:
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 中,加载它们:
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 中,加载它们:
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()
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()
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 calListchangeTheWorld()
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 = wdchangeObjs()
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()
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()
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)