361 lines
14 KiB
Python
361 lines
14 KiB
Python
import os
|
|
import json
|
|
import bpy
|
|
import time
|
|
import gc
|
|
import numpy as np
|
|
import mathutils
|
|
|
|
|
|
class CADBlenderUtils:
|
|
|
|
TABLE_NAME: str = "table"
|
|
CAMERA_NAME: str = "Camera"
|
|
CAMERA_RIGHT_NAME: str = "CameraRight"
|
|
CAMERA_OBJECT_NAME: str = "CameraObject"
|
|
DISPLAY_TABLE_NAME: str = "display_table"
|
|
MESH_FILE_NAME: str = "mesh.obj"
|
|
|
|
@staticmethod
|
|
def get_obj_path(obj_dir, name):
|
|
return os.path.join(obj_dir, name, CADBlenderUtils.MESH_FILE_NAME)
|
|
|
|
@staticmethod
|
|
def load_obj(name, mesh_path, scale=1):
|
|
print(mesh_path)
|
|
bpy.ops.wm.obj_import(filepath=mesh_path)
|
|
loaded_object = bpy.context.selected_objects[-1]
|
|
loaded_object.name = name
|
|
loaded_object.data.name = name
|
|
loaded_object.scale = (scale, scale, scale)
|
|
bpy.ops.rigidbody.object_add()
|
|
return loaded_object
|
|
|
|
@staticmethod
|
|
def get_obj(name):
|
|
return bpy.data.objects.get(name)
|
|
|
|
@staticmethod
|
|
def get_obj_pose(name):
|
|
obj = CADBlenderUtils.get_obj(name)
|
|
return np.asarray(obj.matrix_world)
|
|
|
|
@staticmethod
|
|
def add_plane(name, location, orientation, size=10):
|
|
bpy.ops.mesh.primitive_plane_add(size=size, location=location)
|
|
plane = bpy.context.selected_objects[-1]
|
|
plane.name = name
|
|
plane.rotation_euler = orientation
|
|
bpy.ops.rigidbody.object_add()
|
|
bpy.context.object.rigid_body.type = "PASSIVE"
|
|
|
|
@staticmethod
|
|
def setup_scene(init_light_and_camera_config, table_model_path, binocular_vision):
|
|
bpy.context.scene.render.engine = "BLENDER_EEVEE"
|
|
bpy.context.scene.display.shading.show_xray = False
|
|
bpy.context.scene.display.shading.use_dof = False
|
|
bpy.context.scene.display.render_aa = "OFF"
|
|
bpy.context.scene.view_settings.view_transform = "Standard"
|
|
|
|
bpy.context.scene.eevee.use_ssr = False # 关闭屏幕空间反射
|
|
bpy.context.scene.eevee.use_bloom = False # 关闭辉光
|
|
bpy.context.scene.eevee.use_gtao = False # 关闭环境光遮蔽
|
|
bpy.context.scene.eevee.use_soft_shadows = False # 关闭软阴影
|
|
bpy.context.scene.eevee.use_shadows = False # 关闭所有阴影
|
|
bpy.context.scene.world.use_nodes = False # 如果你不需要环境光,关闭环境节点
|
|
|
|
# bpy.context.scene.eevee.use_sss = False # 关闭次表面散射
|
|
|
|
# 2. 设置最低的采样数
|
|
bpy.context.scene.eevee.taa_render_samples = 1
|
|
bpy.context.scene.eevee.taa_samples = 1
|
|
CADBlenderUtils.init_light_and_camera(
|
|
init_light_and_camera_config, binocular_vision
|
|
)
|
|
|
|
@staticmethod
|
|
def set_camera_params(camera, config, binocular_vision):
|
|
|
|
camera_object = bpy.data.objects.new(CADBlenderUtils.CAMERA_OBJECT_NAME, None)
|
|
bpy.context.collection.objects.link(camera_object)
|
|
cameras = [bpy.data.objects.get("Camera")]
|
|
camera.location = [0, 0, 0]
|
|
camera.rotation_euler = [0, 0, 0]
|
|
camera.parent = camera_object
|
|
if binocular_vision:
|
|
left_camera = cameras[0]
|
|
right_camera = left_camera.copy()
|
|
right_camera.name = CADBlenderUtils.CAMERA_RIGHT_NAME
|
|
right_camera.data = left_camera.data.copy()
|
|
right_camera.data.name = CADBlenderUtils.CAMERA_RIGHT_NAME
|
|
bpy.context.collection.objects.link(right_camera)
|
|
right_camera.parent = camera_object
|
|
right_camera.location = [config["eye_distance"] / 2, 0, 0]
|
|
left_camera.location = [-config["eye_distance"] / 2, 0, 0]
|
|
binocular_angle = config["eye_angle"]
|
|
half_angle = np.radians(binocular_angle / 2)
|
|
|
|
left_camera.rotation_euler[1] = -half_angle
|
|
right_camera.rotation_euler[1] = half_angle
|
|
cameras.append(right_camera)
|
|
|
|
for camera in cameras:
|
|
camera.data.clip_start = config["near_plane"]
|
|
camera.data.clip_end = config["far_plane"]
|
|
|
|
bpy.context.scene.render.resolution_x = config["resolution"][0]
|
|
bpy.context.scene.render.resolution_y = config["resolution"][1]
|
|
sensor_height = 24.0
|
|
focal_length = sensor_height / (
|
|
2 * np.tan(np.radians(config["fov_vertical"]) / 2)
|
|
)
|
|
camera.data.lens = focal_length
|
|
camera.data.sensor_width = (
|
|
sensor_height * config["resolution"][0] / config["resolution"][1]
|
|
)
|
|
camera.data.sensor_height = sensor_height
|
|
|
|
@staticmethod
|
|
def init_light_and_camera(init_light_and_camera_config, binocular_vision):
|
|
|
|
camera = CADBlenderUtils.get_obj(CADBlenderUtils.CAMERA_NAME)
|
|
CADBlenderUtils.set_camera_params(
|
|
camera,
|
|
init_light_and_camera_config[CADBlenderUtils.CAMERA_NAME],
|
|
binocular_vision,
|
|
)
|
|
|
|
@staticmethod
|
|
def get_obj_diag(name):
|
|
obj = CADBlenderUtils.get_obj(name)
|
|
return np.linalg.norm(obj.dimensions)
|
|
|
|
@staticmethod
|
|
def matrix_to_blender_pose(matrix):
|
|
location = matrix[:3, 3]
|
|
rotation_matrix = matrix[:3, :3]
|
|
rotation_matrix_blender = mathutils.Matrix(rotation_matrix.tolist())
|
|
rotation_euler = rotation_matrix_blender.to_euler()
|
|
return location, rotation_euler
|
|
|
|
@staticmethod
|
|
def set_camera_at(pose):
|
|
camera = CADBlenderUtils.get_obj(CADBlenderUtils.CAMERA_OBJECT_NAME)
|
|
location, rotation_euler = CADBlenderUtils.matrix_to_blender_pose(pose)
|
|
|
|
camera.location = location
|
|
camera.rotation_euler = rotation_euler
|
|
|
|
@staticmethod
|
|
def get_object_bottom_z(obj):
|
|
vertices = [v.co for v in obj.data.vertices]
|
|
vertices_world = [obj.matrix_world @ v for v in vertices]
|
|
min_z = min([v.z for v in vertices_world])
|
|
return min_z
|
|
|
|
@staticmethod
|
|
def render_normal_and_depth(
|
|
output_dir, file_name, binocular_vision=False, target_object=None
|
|
):
|
|
# use pass z
|
|
bpy.context.scene.view_layers["ViewLayer"].use_pass_z = True
|
|
target_cameras = [CADBlenderUtils.CAMERA_NAME]
|
|
if binocular_vision:
|
|
target_cameras.append(CADBlenderUtils.CAMERA_RIGHT_NAME)
|
|
|
|
for cam_name in target_cameras:
|
|
bpy.context.scene.camera = CADBlenderUtils.get_obj(cam_name)
|
|
cam_suffix = "L" if cam_name == CADBlenderUtils.CAMERA_NAME else "R"
|
|
scene = bpy.context.scene
|
|
scene.render.filepath = ""
|
|
|
|
mask_dir = os.path.join(output_dir, "normal")
|
|
if not os.path.exists(mask_dir):
|
|
os.makedirs(mask_dir)
|
|
|
|
scene.render.filepath = os.path.join(
|
|
output_dir, mask_dir, f"{file_name}_{cam_suffix}.exr"
|
|
)
|
|
|
|
scene.render.image_settings.file_format = "OPEN_EXR"
|
|
scene.render.image_settings.color_mode = "RGB"
|
|
bpy.context.scene.view_settings.view_transform = "Raw"
|
|
scene.render.image_settings.color_depth = "16"
|
|
bpy.context.scene.render.filter_size = 1.5
|
|
scene.render.resolution_percentage = 100
|
|
scene.render.use_overwrite = False
|
|
scene.render.use_file_extension = False
|
|
scene.render.use_placeholder = False
|
|
scene.use_nodes = True
|
|
tree = scene.node_tree
|
|
|
|
for node in tree.nodes:
|
|
tree.nodes.remove(node)
|
|
|
|
rl = tree.nodes.new("CompositorNodeRLayers")
|
|
|
|
map_range = tree.nodes.new("CompositorNodeMapRange")
|
|
map_range.inputs["From Min"].default_value = 0.01
|
|
map_range.inputs["From Max"].default_value = 5
|
|
map_range.inputs["To Min"].default_value = 0
|
|
map_range.inputs["To Max"].default_value = 1
|
|
tree.links.new(rl.outputs["Depth"], map_range.inputs[0])
|
|
|
|
output_depth = tree.nodes.new("CompositorNodeOutputFile")
|
|
|
|
depth_dir = os.path.join(output_dir, "depth")
|
|
if not os.path.exists(depth_dir):
|
|
os.makedirs(depth_dir)
|
|
output_depth.base_path = depth_dir
|
|
output_depth.file_slots[0].path = f"{file_name}_{cam_suffix}.####"
|
|
output_depth.format.file_format = "PNG"
|
|
output_depth.format.color_mode = "BW"
|
|
output_depth.format.color_depth = "16"
|
|
tree.links.new(map_range.outputs[0], output_depth.inputs[0])
|
|
bpy.ops.render.render(write_still=True)
|
|
|
|
msg = "success"
|
|
return msg
|
|
|
|
@staticmethod
|
|
def render_mask(
|
|
output_dir, file_name, binocular_vision=False, target_object=None
|
|
):
|
|
target_cameras = [CADBlenderUtils.CAMERA_NAME]
|
|
if binocular_vision:
|
|
target_cameras.append(CADBlenderUtils.CAMERA_RIGHT_NAME)
|
|
|
|
for cam_name in target_cameras:
|
|
bpy.context.scene.camera = CADBlenderUtils.get_obj(cam_name)
|
|
cam_suffix = "L" if cam_name == CADBlenderUtils.CAMERA_NAME else "R"
|
|
scene = bpy.context.scene
|
|
scene.render.filepath = ""
|
|
|
|
mask_dir = os.path.join(output_dir, "mask")
|
|
if not os.path.exists(mask_dir):
|
|
os.makedirs(mask_dir)
|
|
|
|
scene.render.filepath = os.path.join(
|
|
output_dir, mask_dir, f"{file_name}_{cam_suffix}.png"
|
|
)
|
|
scene.render.image_settings.color_depth = "8"
|
|
scene.render.resolution_percentage = 100
|
|
scene.render.use_overwrite = False
|
|
scene.render.use_file_extension = False
|
|
scene.render.use_placeholder = False
|
|
|
|
|
|
bpy.ops.render.render(write_still=True)
|
|
|
|
msg = "success"
|
|
return msg
|
|
|
|
@staticmethod
|
|
def save_cam_params(scene_dir, idx, binocular_vision=False):
|
|
camera = CADBlenderUtils.get_obj(CADBlenderUtils.CAMERA_NAME)
|
|
extrinsic = np.array(camera.matrix_world)
|
|
cam_data = camera.data
|
|
focal_length = cam_data.lens
|
|
sensor_width = cam_data.sensor_width
|
|
sensor_height = cam_data.sensor_height
|
|
resolution_x = bpy.context.scene.render.resolution_x
|
|
resolution_y = bpy.context.scene.render.resolution_y
|
|
intrinsic = np.zeros((3, 3))
|
|
intrinsic[0, 0] = focal_length * resolution_x / sensor_width # fx
|
|
intrinsic[1, 1] = focal_length * resolution_y / sensor_height # fy
|
|
intrinsic[0, 2] = resolution_x / 2.0 # cx
|
|
intrinsic[1, 2] = resolution_y / 2.0 # cy
|
|
intrinsic[2, 2] = 1.0
|
|
cam_object = CADBlenderUtils.get_obj(CADBlenderUtils.CAMERA_OBJECT_NAME)
|
|
extrinsic_cam_object = np.array(cam_object.matrix_world)
|
|
data = {
|
|
"extrinsic": extrinsic.tolist(),
|
|
"extrinsic_cam_object": extrinsic_cam_object.tolist(),
|
|
"intrinsic": intrinsic.tolist(),
|
|
"far_plane": camera.data.clip_end,
|
|
"near_plane": camera.data.clip_start,
|
|
}
|
|
if binocular_vision:
|
|
right_camera = CADBlenderUtils.get_obj(CADBlenderUtils.CAMERA_RIGHT_NAME)
|
|
extrinsic_right = np.array(right_camera.matrix_world)
|
|
print("result:", extrinsic_right)
|
|
|
|
data["extrinsic_R"] = extrinsic_right.tolist()
|
|
|
|
cam_params_dir = os.path.join(scene_dir, "camera_params")
|
|
if not os.path.exists(cam_params_dir):
|
|
os.makedirs(cam_params_dir)
|
|
cam_params_path = os.path.join(cam_params_dir, f"{idx}.json")
|
|
with open(cam_params_path, "w") as f:
|
|
json.dump(data, f, indent=4)
|
|
|
|
@staticmethod
|
|
def reset_objects_and_platform():
|
|
all_objects = bpy.data.objects
|
|
keep_objects = {
|
|
"plane_floor",
|
|
"plane_ceil",
|
|
"plane_wall_1",
|
|
"plane_wall_2",
|
|
"plane_wall_3",
|
|
"plane_wall_4",
|
|
}
|
|
keep_objects.add(CADBlenderUtils.CAMERA_OBJECT_NAME)
|
|
keep_objects.add(CADBlenderUtils.CAMERA_NAME)
|
|
keep_objects.add(CADBlenderUtils.CAMERA_RIGHT_NAME)
|
|
keep_objects.add(CADBlenderUtils.TABLE_NAME)
|
|
|
|
for obj in all_objects:
|
|
if obj.name not in keep_objects:
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
for block in bpy.data.meshes:
|
|
if block.users == 0:
|
|
bpy.data.meshes.remove(block)
|
|
for block in bpy.data.materials:
|
|
if block.users == 0:
|
|
bpy.data.materials.remove(block)
|
|
for block in bpy.data.images:
|
|
if block.users == 0:
|
|
bpy.data.images.remove(block)
|
|
|
|
gc.collect()
|
|
bpy.context.scene.frame_set(0)
|
|
|
|
@staticmethod
|
|
def save_scene_info(scene_root_dir, display_table_config, target_name):
|
|
all_objects = bpy.data.objects
|
|
no_save_objects = {
|
|
"plane_floor",
|
|
"plane_ceil",
|
|
"plane_wall_1",
|
|
"plane_wall_2",
|
|
"plane_wall_3",
|
|
"plane_wall_4",
|
|
}
|
|
no_save_objects.add(CADBlenderUtils.CAMERA_OBJECT_NAME)
|
|
no_save_objects.add(CADBlenderUtils.CAMERA_NAME)
|
|
no_save_objects.add(CADBlenderUtils.CAMERA_RIGHT_NAME)
|
|
no_save_objects.add(CADBlenderUtils.TABLE_NAME)
|
|
scene_info = {}
|
|
for obj in all_objects:
|
|
if (
|
|
obj.name not in no_save_objects
|
|
and obj.name != CADBlenderUtils.DISPLAY_TABLE_NAME
|
|
):
|
|
obj_info = {
|
|
"location": list(obj.location),
|
|
"rotation_euler": list(obj.rotation_euler),
|
|
"scale": list(obj.scale),
|
|
}
|
|
scene_info[obj.name] = obj_info
|
|
scene_info[CADBlenderUtils.DISPLAY_TABLE_NAME] = display_table_config
|
|
scene_info["target_name"] = target_name
|
|
scene_info_path = os.path.join(scene_root_dir, "scene_info.json")
|
|
with open(scene_info_path, "w") as outfile:
|
|
json.dump(scene_info, outfile)
|
|
|
|
@staticmethod
|
|
def save_blend(scene_root_dir):
|
|
blend_path = os.path.join(scene_root_dir, "scene.blend")
|
|
bpy.ops.wm.save_as_mainfile(filepath=blend_path) |