EscherNet / scripts /blender_script_mvs.py
kxic's picture
Upload folder using huggingface_hub
e371ddd verified
raw
history blame
12.6 kB
"""Blender script to render images of 3D models.
This script is used to render images of 3D models. It takes in a list of paths
to .glb files and renders images of each model. The images are from rotating the
object around the origin. The images are saved to the output directory.
Example usage:
blender -b -P blender_script.py -- \
--object_path my_object.glb \
--output_dir ./views \
--engine CYCLES \
--scale 0.8 \
--num_images 12 \
--camera_dist 1.2
Here, input_model_paths.json is a json file containing a list of paths to .glb.
"""
import argparse
import json
import math
import os
import random
import sys
import time
import urllib.request
import uuid
from typing import Tuple
from mathutils import Vector, Matrix
import numpy as np
import bpy
from mathutils import Vector
parser = argparse.ArgumentParser()
parser.add_argument(
"--object_path",
type=str,
required=True,
help="Path to the object file",
)
parser.add_argument("--output_dir", type=str, default="/home/")
parser.add_argument(
"--engine", type=str, default="CYCLES", choices=["CYCLES", "BLENDER_EEVEE"]
)
parser.add_argument("--scale", type=float, default=0.8)
parser.add_argument("--num_images", type=int, default=8)
parser.add_argument("--camera_dist", type=float, default=1.2)
argv = sys.argv[sys.argv.index("--") + 1 :]
args = parser.parse_args(argv)
print('===================', args.engine, '===================')
context = bpy.context
scene = context.scene
render = scene.render
cam = scene.objects["Camera"]
cam.location = (0, 1.2, 0)
cam.data.lens = 35
cam.data.sensor_width = 32
cam_constraint = cam.constraints.new(type="TRACK_TO")
cam_constraint.track_axis = "TRACK_NEGATIVE_Z"
cam_constraint.up_axis = "UP_Y"
# setup lighting
bpy.ops.object.light_add(type="AREA")
light2 = bpy.data.lights["Area"]
light2.energy = 3000
bpy.data.objects["Area"].location[2] = 0.5
bpy.data.objects["Area"].scale[0] = 100
bpy.data.objects["Area"].scale[1] = 100
bpy.data.objects["Area"].scale[2] = 100
render.engine = args.engine
render.image_settings.file_format = "PNG"
render.image_settings.color_mode = "RGBA"
render.resolution_x = 512
render.resolution_y = 512
render.resolution_percentage = 100
scene.cycles.device = "GPU"
scene.cycles.samples = 128
scene.cycles.diffuse_bounces = 1
scene.cycles.glossy_bounces = 1
scene.cycles.transparent_max_bounces = 3
scene.cycles.transmission_bounces = 3
scene.cycles.filter_width = 0.01
scene.cycles.use_denoising = True
scene.render.film_transparent = True
bpy.context.preferences.addons["cycles"].preferences.get_devices()
# Set the device_type
bpy.context.preferences.addons[
"cycles"
].preferences.compute_device_type = "CUDA" # or "OPENCL"
def sample_point_on_sphere(radius: float) -> Tuple[float, float, float]:
theta = random.random() * 2 * math.pi
phi = math.acos(2 * random.random() - 1)
return (
radius * math.sin(phi) * math.cos(theta),
radius * math.sin(phi) * math.sin(theta),
radius * math.cos(phi),
)
def sample_spherical(radius=3.0, maxz=3.0, minz=0.):
correct = False
while not correct:
vec = np.random.uniform(-1, 1, 3)
vec[2] = np.abs(vec[2])
vec = vec / np.linalg.norm(vec, axis=0) * radius
if maxz > vec[2] > minz:
correct = True
return vec
def sample_spherical(radius_min=1.5, radius_max=2.0, maxz=1.6, minz=-0.75):
correct = False
while not correct:
vec = np.random.uniform(-1, 1, 3)
# vec[2] = np.abs(vec[2])
radius = np.random.uniform(radius_min, radius_max, 1)
vec = vec / np.linalg.norm(vec, axis=0) * radius[0]
if maxz > vec[2] > minz:
correct = True
return vec
def randomize_camera():
elevation = random.uniform(0., 90.)
azimuth = random.uniform(0., 360)
distance = random.uniform(0.8, 1.6)
return set_camera_location(elevation, azimuth, distance)
def set_camera_location(elevation, azimuth, distance):
# from https://blender.stackexchange.com/questions/18530/
# x, y, z = sample_spherical(radius_min=1.5, radius_max=2.2, maxz=2.2, minz=-2.2) # FIXME: too Far? # four for this
x, y, z = sample_spherical(radius_min=1.7, radius_max=2.0, maxz=1.1, minz=0.9) # one for this
# x, y, z = sample_spherical(radius_min=1.2, radius_max=2.0, maxz=2.2, minz=-2.2)
# x, y, z = 0, -1.2, 0
camera = bpy.data.objects["Camera"]
camera.location = x, y, z
direction = - camera.location
rot_quat = direction.to_track_quat('-Z', 'Y')
camera.rotation_euler = rot_quat.to_euler()
return camera
def set_camera_location_old(elevation, azimuth, distance):
# from https://blender.stackexchange.com/questions/18530/
x, y, z = sample_spherical(radius_min=1.5, radius_max=2.2, maxz=2.2, minz=-2.2) # FIXME: too Far? # four for this
# x, y, z = sample_spherical(radius_min=1.2, radius_max=2.0, maxz=2.2, minz=-2.2)
# x, y, z = 0, -1.2, 0
camera = bpy.data.objects["Camera"]
camera.location = x, y, z
direction = - camera.location
rot_quat = direction.to_track_quat('-Z', 'Y')
camera.rotation_euler = rot_quat.to_euler()
return camera
def randomize_lighting() -> None:
reset_lighting()
# light2.energy = random.uniform(300, 600)
# bpy.data.objects["Area"].location[0] = random.uniform(-1., 1.)
# bpy.data.objects["Area"].location[1] = random.uniform(-1., 1.)
# bpy.data.objects["Area"].location[2] = random.uniform(0.5, 1.5)
def reset_lighting() -> None:
light2.energy = 800
bpy.data.objects["Area"].location[0] = 0
bpy.data.objects["Area"].location[1] = 0
bpy.data.objects["Area"].location[2] = 1
def reset_scene() -> None:
"""Resets the scene to a clean state."""
# delete everything that isn't part of a camera or a light
for obj in bpy.data.objects:
if obj.type not in {"CAMERA", "LIGHT"}:
bpy.data.objects.remove(obj, do_unlink=True)
# delete all the materials
for material in bpy.data.materials:
bpy.data.materials.remove(material, do_unlink=True)
# delete all the textures
for texture in bpy.data.textures:
bpy.data.textures.remove(texture, do_unlink=True)
# delete all the images
for image in bpy.data.images:
bpy.data.images.remove(image, do_unlink=True)
# load the glb model
def load_object(object_path: str) -> None:
"""Loads a glb model into the scene."""
if object_path.endswith(".glb"):
bpy.ops.import_scene.gltf(filepath=object_path, merge_vertices=True)
elif object_path.endswith(".fbx"):
bpy.ops.import_scene.fbx(filepath=object_path)
elif object_path.endswith(".obj"):
bpy.ops.import_scene.obj(filepath=object_path, axis_forward="Y", axis_up="Z")
else:
raise ValueError(f"Unsupported file type: {object_path}")
def scene_bbox(single_obj=None, ignore_matrix=False):
bbox_min = (math.inf,) * 3
bbox_max = (-math.inf,) * 3
found = False
for obj in scene_meshes() if single_obj is None else [single_obj]:
found = True
for coord in obj.bound_box:
coord = Vector(coord)
if not ignore_matrix:
coord = obj.matrix_world @ coord
bbox_min = tuple(min(x, y) for x, y in zip(bbox_min, coord))
bbox_max = tuple(max(x, y) for x, y in zip(bbox_max, coord))
if not found:
raise RuntimeError("no objects in scene to compute bounding box for")
return Vector(bbox_min), Vector(bbox_max)
def scene_root_objects():
for obj in bpy.context.scene.objects.values():
if not obj.parent:
yield obj
def scene_meshes():
for obj in bpy.context.scene.objects.values():
if isinstance(obj.data, (bpy.types.Mesh)):
yield obj
# function from https://github.com/panmari/stanford-shapenet-renderer/blob/master/render_blender.py
def get_3x4_RT_matrix_from_blender(cam):
# bcam stands for blender camera
# R_bcam2cv = Matrix(
# ((1, 0, 0),
# (0, 1, 0),
# (0, 0, 1)))
# Transpose since the rotation is object rotation,
# and we want coordinate rotation
# R_world2bcam = cam.rotation_euler.to_matrix().transposed()
# T_world2bcam = -1*R_world2bcam @ location
#
# Use matrix_world instead to account for all constraints
location, rotation = cam.matrix_world.decompose()[0:2]
R_world2bcam = rotation.to_matrix().transposed()
# Convert camera location to translation vector used in coordinate changes
# T_world2bcam = -1*R_world2bcam @ cam.location
# Use location from matrix_world to account for constraints:
T_world2bcam = -1*R_world2bcam @ location
# # Build the coordinate transform matrix from world to computer vision camera
# R_world2cv = R_bcam2cv@R_world2bcam
# T_world2cv = R_bcam2cv@T_world2bcam
# put into 3x4 matrix
RT = Matrix((
R_world2bcam[0][:] + (T_world2bcam[0],),
R_world2bcam[1][:] + (T_world2bcam[1],),
R_world2bcam[2][:] + (T_world2bcam[2],)
))
return RT
def normalize_scene():
bbox_min, bbox_max = scene_bbox()
scale = 1 / max(bbox_max - bbox_min)
for obj in scene_root_objects():
obj.scale = obj.scale * scale
# Apply scale to matrix_world.
bpy.context.view_layer.update()
bbox_min, bbox_max = scene_bbox()
offset = -(bbox_min + bbox_max) / 2
for obj in scene_root_objects():
obj.matrix_world.translation += offset
bpy.ops.object.select_all(action="DESELECT")
def save_images(object_file: str) -> None:
"""Saves rendered images of the object in the scene."""
os.makedirs(args.output_dir, exist_ok=True)
reset_scene()
# load the object
load_object(object_file)
object_uid = os.path.basename(object_file).split(".")[0]
normalize_scene()
# export mesh
mesh_save_path = os.path.join(args.output_dir, object_uid + "_norm.obj")
bpy.ops.export_scene.obj(filepath=mesh_save_path)
# create an empty object to track
empty = bpy.data.objects.new("Empty", None)
scene.collection.objects.link(empty)
cam_constraint.target = empty
randomize_lighting()
for i in range(args.num_images):
# # set the camera position
# theta = (i / args.num_images) * math.pi * 2
# phi = math.radians(60)
# point = (
# args.camera_dist * math.sin(phi) * math.cos(theta),
# args.camera_dist * math.sin(phi) * math.sin(theta),
# args.camera_dist * math.cos(phi),
# )
# # reset_lighting()
# cam.location = point
# set camera
if i == 0:
camera = set_camera_location(None, None, None)
else:
camera = set_camera_location_old(None, None, None)
# render the image
render_path = os.path.join(args.output_dir, object_uid, f"{i:03d}.png")
scene.render.filepath = render_path
bpy.ops.render.render(write_still=True)
# save camera RT matrix
RT = get_3x4_RT_matrix_from_blender(camera)
print(RT)
RT_path = os.path.join(args.output_dir, object_uid, f"{i:03d}.npy")
np.save(RT_path, RT)
# RT_path = os.path.join(args.output_dir, object_uid, f"{i:03d}.npy")
# mat = camera.matrix_world
# np.save(RT_path, mat)
# print(mat)
def download_object(object_url: str) -> str:
"""Download the object and return the path."""
# uid = uuid.uuid4()
uid = object_url.split("/")[-1].split(".")[0]
tmp_local_path = os.path.join("tmp-objects", f"{uid}.glb" + ".tmp")
local_path = os.path.join("tmp-objects", f"{uid}.glb")
# wget the file and put it in local_path
os.makedirs(os.path.dirname(tmp_local_path), exist_ok=True)
urllib.request.urlretrieve(object_url, tmp_local_path)
os.rename(tmp_local_path, local_path)
# get the absolute path
local_path = os.path.abspath(local_path)
return local_path
if __name__ == "__main__":
try:
start_i = time.time()
if args.object_path.startswith("http"):
local_path = download_object(args.object_path)
else:
local_path = args.object_path
save_images(local_path)
end_i = time.time()
print("Finished", local_path, "in", end_i - start_i, "seconds")
# delete the object if it was downloaded
if args.object_path.startswith("http"):
os.remove(local_path)
except Exception as e:
print("Failed to render", args.object_path)
print(e)