Skip to content

Commit 3f62115

Browse files
committed
blender renderer
1 parent 5f53158 commit 3f62115

File tree

2 files changed

+331
-0
lines changed

2 files changed

+331
-0
lines changed

lib/blender_renderer.py

+319
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/usr/bin/env python3
2+
3+
import _init_paths
4+
import time
5+
import os
6+
import contextlib
7+
from math import radians
8+
import numpy as np
9+
from PIL import Image
10+
from tempfile import TemporaryFile
11+
from lib.utils import stdout_redirected
12+
from lib.config import cfg
13+
import bpy
14+
15+
16+
def voxel2mesh(voxels):
17+
cube_verts = [[0, 0, 0],
18+
[0, 0, 1],
19+
[0, 1, 0],
20+
[0, 1, 1],
21+
[1, 0, 0],
22+
[1, 0, 1],
23+
[1, 1, 0],
24+
[1, 1, 1]] # 8 points
25+
26+
cube_faces = [[0, 1, 2],
27+
[1, 3, 2],
28+
[2, 3, 6],
29+
[3, 7, 6],
30+
[0, 2, 6],
31+
[0, 6, 4],
32+
[0, 5, 1],
33+
[0, 4, 5],
34+
[6, 7, 5],
35+
[6, 5, 4],
36+
[1, 7, 3],
37+
[1, 5, 7]] # 12 face
38+
39+
cube_verts = np.array(cube_verts)
40+
cube_faces = np.array(cube_faces) + 1
41+
42+
l, m, n = voxels.shape
43+
44+
scale = 0.01
45+
cube_dist_scale = 1.1
46+
verts = []
47+
faces = []
48+
curr_vert = 0
49+
for i in range(l):
50+
for j in range(m):
51+
for k in range(n):
52+
# If there is a non-empty voxel
53+
if voxels[i, j, k] > 0:
54+
verts.extend(scale * (cube_verts + cube_dist_scale * np.array([[i, j, k]])))
55+
faces.extend(cube_faces + curr_vert)
56+
curr_vert += len(cube_verts)
57+
58+
return np.array(verts), np.array(faces)
59+
60+
61+
def write_obj(filename, verts, faces):
62+
""" write the verts and faces on file."""
63+
with open(filename, 'w') as f:
64+
# write vertices
65+
f.write('g\n# %d vertex\n' % len(verts))
66+
for vert in verts:
67+
f.write('v %f %f %f\n' % tuple(vert))
68+
69+
# write faces
70+
f.write('# %d faces\n' % len(faces))
71+
for face in faces:
72+
f.write('f %d %d %d\n' % tuple(face))
73+
74+
75+
class BaseRenderer:
76+
model_idx = 0
77+
78+
def __init__(self):
79+
# bpy.data.scenes['Scene'].render.engine = 'CYCLES'
80+
# bpy.context.scene.cycles.device = 'GPU'
81+
# bpy.context.user_preferences.system.compute_device_type = 'CUDA'
82+
# bpy.context.user_preferences.system.compute_device = 'CUDA_1'
83+
84+
# changing these values does affect the render.
85+
86+
# remove the default cube
87+
bpy.ops.object.select_pattern(pattern="Cube")
88+
bpy.ops.object.delete()
89+
90+
render_context = bpy.context.scene.render
91+
world = bpy.context.scene.world
92+
camera = bpy.data.objects['Camera']
93+
light_1 = bpy.data.objects['Lamp']
94+
light_1.data.type = 'HEMI'
95+
96+
# set the camera postion and orientation so that it is in
97+
# the front of the object
98+
camera.location = (1, 0, 0)
99+
camera.rotation_mode = 'ZXY'
100+
camera.rotation_euler = (0, radians(90), radians(90))
101+
102+
# parent camera with a empty object at origin
103+
org_obj = bpy.data.objects.new("RotCenter", None)
104+
org_obj.location = (0, 0, 0)
105+
org_obj.rotation_euler = (0, 0, 0)
106+
bpy.context.scene.objects.link(org_obj)
107+
108+
camera.parent = org_obj # setup parenting
109+
110+
# render setting
111+
render_context.resolution_percentage = 100
112+
world.horizon_color = (1, 1, 1) # set background color to be white
113+
114+
# set file name for storing rendering result
115+
self.result_fn = '%s/render_result_%d.png' % (cfg.DIR.RENDERING_PATH, os.getpid())
116+
bpy.context.scene.render.filepath = self.result_fn
117+
118+
self.render_context = render_context
119+
self.org_obj = org_obj
120+
self.camera = camera
121+
self.light = light_1
122+
self._set_lighting()
123+
124+
def initialize(self, models_fn, viewport_size_x, viewport_size_y):
125+
self.models_fn = models_fn
126+
self.render_context.resolution_x = viewport_size_x
127+
self.render_context.resolution_y = viewport_size_y
128+
129+
def _set_lighting(self):
130+
pass
131+
132+
def setViewpoint(self, azimuth, altitude, yaw, distance_ratio, fov):
133+
self.org_obj.rotation_euler = (0, 0, 0)
134+
self.light.location = (distance_ratio *
135+
(cfg.RENDERING.MAX_CAMERA_DIST + 2), 0, 0)
136+
self.camera.location = (distance_ratio *
137+
cfg.RENDERING.MAX_CAMERA_DIST, 0, 0)
138+
self.org_obj.rotation_euler = (radians(-yaw),
139+
radians(-altitude),
140+
radians(-azimuth))
141+
142+
def setTransparency(self, transparency):
143+
""" transparency is either 'SKY', 'TRANSPARENT'
144+
If set 'SKY', render background using sky color."""
145+
self.render_context.alpha_mode = transparency
146+
147+
def selectModel(self):
148+
bpy.ops.object.select_all(action='DESELECT')
149+
bpy.ops.object.select_pattern(pattern="RotCenter")
150+
bpy.ops.object.select_pattern(pattern="Lamp*")
151+
bpy.ops.object.select_pattern(pattern="Camera")
152+
bpy.ops.object.select_all(action='INVERT')
153+
154+
def printSelection(self):
155+
print(bpy.context.selected_objects)
156+
157+
def clearModel(self):
158+
self.selectModel()
159+
bpy.ops.object.delete()
160+
161+
# The meshes still present after delete
162+
for item in bpy.data.meshes:
163+
bpy.data.meshes.remove(item)
164+
for item in bpy.data.materials:
165+
bpy.data.materials.remove(item)
166+
167+
def setModelIndex(self, model_idx):
168+
self.model_idx = model_idx
169+
170+
def loadModel(self, file_path=None):
171+
if file_path is None:
172+
file_path = self.models_fn[self.model_idx]
173+
174+
if file_path.endswith('obj'):
175+
bpy.ops.import_scene.obj(filepath=file_path)
176+
elif file_path.endswith('3ds'):
177+
bpy.ops.import_scene.autodesk_3ds(filepath=file_path)
178+
elif file_path.endswith('dae'):
179+
# Must install OpenCollada. Please read README.md
180+
bpy.ops.wm.collada_import(filepath=file_path)
181+
else:
182+
raise Exception("Loading failed: %s Model loading for type %s not Implemented" %
183+
(file_path, file_path[-4:]))
184+
185+
def render(self, load_model=True, clear_model=True, resize_ratio=None,
186+
return_image=True, image_path=os.path.join(cfg.RENDERING.BLENDER_TMP_DIR, 'tmp.png')):
187+
""" Render the object """
188+
if load_model:
189+
self.loadModel()
190+
191+
# resize object
192+
self.selectModel()
193+
if resize_ratio:
194+
bpy.ops.transform.resize(value=resize_ratio)
195+
196+
self.result_fn = image_path
197+
bpy.context.scene.render.filepath = image_path
198+
bpy.ops.render.render(write_still=True) # save straight to file
199+
200+
if resize_ratio:
201+
bpy.ops.transform.resize(value=(1/resize_ratio[0],
202+
1/resize_ratio[1], 1/resize_ratio[2]))
203+
204+
if clear_model:
205+
self.clearModel()
206+
207+
if return_image:
208+
im = np.array(Image.open(self.result_fn)) # read the image
209+
210+
# Last channel is the alpha channel (transparency)
211+
return im[:, :, :3], im[:, :, 3]
212+
213+
214+
class ShapeNetRenderer(BaseRenderer):
215+
216+
def __init__(self):
217+
super().__init__()
218+
self.setTransparency('TRANSPARENT')
219+
220+
def _set_lighting(self):
221+
# Create new lamp datablock
222+
light_data = bpy.data.lamps.new(name="New Lamp", type='HEMI')
223+
224+
# Create new object with our lamp datablock
225+
light_2 = bpy.data.objects.new(name="New Lamp", object_data=light_data)
226+
bpy.context.scene.objects.link(light_2)
227+
228+
# put the light behind the camera. Reduce specular lighting
229+
self.light.location = (0, -2, 2)
230+
self.light.rotation_mode = 'ZXY'
231+
self.light.rotation_euler = (radians(45), 0, radians(90))
232+
self.light.data.energy = 0.7
233+
234+
light_2.location = (0, 2, 2)
235+
light_2.rotation_mode = 'ZXY'
236+
light_2.rotation_euler = (-radians(45), 0, radians(90))
237+
light_2.data.energy = 0.7
238+
239+
240+
class VoxelRenderer(BaseRenderer):
241+
242+
def __init__(self):
243+
super().__init__()
244+
self.setTransparency('SKY')
245+
246+
def _set_lighting(self):
247+
self.light.location = (0, 3, 3)
248+
self.light.rotation_mode = 'ZXY'
249+
self.light.rotation_euler = (-radians(45), 0, radians(90))
250+
self.light.data.energy = 0.7
251+
252+
# Create new lamp datablock
253+
light_data = bpy.data.lamps.new(name="New Lamp", type='HEMI')
254+
255+
# Create new object with our lamp datablock
256+
light_2 = bpy.data.objects.new(name="New Lamp", object_data=light_data)
257+
bpy.context.scene.objects.link(light_2)
258+
259+
light_2.location = (4, 1, 6)
260+
light_2.rotation_mode = 'XYZ'
261+
light_2.rotation_euler = (radians(37), radians(3), radians(106))
262+
light_2.data.energy = 0.7
263+
264+
def render_voxel(self, pred, thresh=0.4,
265+
image_path=os.path.join(cfg.RENDERING.BLENDER_TMP_DIR, 'tmp.png')):
266+
# Cleanup the scene
267+
self.clearModel()
268+
out_f = os.path.join(cfg.RENDERING.BLENDER_TMP_DIR, 'tmp.obj')
269+
occupancy = pred > thresh
270+
vertices, faces = voxel2mesh(occupancy)
271+
with contextlib.suppress(IOError):
272+
os.remove(out_f)
273+
write_obj(out_f, vertices, faces)
274+
275+
# Load the obj
276+
bpy.ops.import_scene.obj(filepath=out_f)
277+
bpy.context.scene.render.filepath = image_path
278+
bpy.ops.render.render(write_still=True) # save straight to file
279+
280+
im = np.array(Image.open(image_path)) # read the image
281+
282+
# Last channel is the alpha channel (transparency)
283+
return im[:, :, :3], im[:, :, 3]
284+
285+
286+
def main():
287+
"""Test function"""
288+
# Modify the following file to visualize the model
289+
dn = '/ShapeNet/ShapeNetCore.v1/02958343/'
290+
model_id = [line.strip('\n') for line in open(dn + 'models.txt')]
291+
file_paths = [os.path.join(dn, line, 'model.obj') for line in model_id]
292+
sum_time = 0
293+
renderer = ShapeNetRenderer()
294+
renderer.initialize(file_paths, 500, 500)
295+
for i, curr_model_id in enumerate(model_id):
296+
start = time.time()
297+
image_path = '%s/%s.png' % ('/tmp', curr_model_id[:-4])
298+
299+
az, el, depth_ratio = list(
300+
*([360, 5, 0.3] * np.random.rand(1, 3) + [0, 25, 0.65]))
301+
302+
renderer.setModelIndex(i)
303+
renderer.setViewpoint(30, 30, 0, 0.7, 25)
304+
305+
with TemporaryFile() as f, stdout_redirected(f):
306+
rendering, alpha = renderer.render(load_model=True,
307+
clear_model=True, image_path=image_path)
308+
309+
print('Saved at %s' % image_path)
310+
311+
end = time.time()
312+
sum_time += end - start
313+
if i % 10 == 0:
314+
print(sum_time/(10))
315+
sum_time = 0
316+
317+
318+
if __name__ == "__main__":
319+
main()

lib/utils.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import sys
12
import time
3+
from contextlib import contextmanager
24

35

46
class Timer(object):
@@ -25,3 +27,13 @@ def toc(self, average=True):
2527
return self.average_time
2628
else:
2729
return self.diff
30+
31+
32+
@contextmanager
33+
def stdout_redirected(new_stdout):
34+
save_stdout = sys.stdout
35+
sys.stdout = new_stdout
36+
try:
37+
yield None
38+
finally:
39+
sys.stdout = save_stdout

0 commit comments

Comments
 (0)