pyrender_visualizer.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import os
  2. import sys
  3. os.environ['PYOPENGL_PLATFORM'] = 'egl'
  4. import numpy as np
  5. import trimesh
  6. from pyrender import PerspectiveCamera,\
  7. DirectionalLight, SpotLight, PointLight,\
  8. MetallicRoughnessMaterial,\
  9. Primitive, Mesh, Node, Scene,\
  10. OffscreenRenderer
  11. import tqdm
  12. from fairmotion.ops import conversions, math, motion as motion_ops
  13. from fairmotion.utils import utils
  14. import pyrender
  15. import matplotlib.pyplot as plt
  16. import matplotlib.animation as animation
  17. from fairmotion.ops.conversions import E2R, R2Q
  18. def _get_cam_rotation(p_cam, p_obj, vup):
  19. z = p_cam - p_obj
  20. z /= np.linalg.norm(z)
  21. x = np.cross(vup, z)
  22. x /= np.linalg.norm(x)
  23. y = np.cross(z, x)
  24. return np.array([x, y, z]).transpose()
  25. class MocapViewerOffline(animation.FuncAnimation):
  26. """
  27. MocapViewerOffline is an extension of the glut_viewer.Viewer class that implements
  28. requisite callback functions -- render_callback, keyboard_callback,
  29. idle_callback and overlay_callback.
  30. ```
  31. python fairmotion/viz/bvh_visualizer.py \
  32. --bvh-files $BVH_FILE1
  33. ```
  34. To visualize more than 1 motion sequence side by side, append more files
  35. to the `--bvh-files` argument. Set `--x-offset` to an appropriate float
  36. value to add space separation between characters in the row.
  37. ```
  38. python fairmotion/viz/bvh_visualizer.py \
  39. --bvh-files $BVH_FILE1 $BVH_FILE2 $BVH_FILE3 \
  40. --x-offset 2
  41. ```
  42. To visualize asfamc motion sequence:
  43. ```
  44. python fairmotion/viz/bvh_visualizer.py \
  45. --asf-files tests/data/11.asf \
  46. --amc-files tests/data/11_01.amc
  47. ```
  48. """
  49. def __init__(
  50. self,
  51. motion,
  52. cam_pos,
  53. v_up_str,
  54. play_speed=1.0,
  55. scale=1.0,
  56. thickness=1.0,
  57. hide_origin=False
  58. ):
  59. animation.FuncAnimation.__init__(self, fig=plt.figure(figsize=(5, 5)), func=self.animate,
  60. frames=len(motion.poses), interval=50, blit=False)
  61. self.motion = motion
  62. self.play_speed = play_speed
  63. self.hide_origin = hide_origin
  64. self.file_idx = 0
  65. self.cur_time = 0.0
  66. self.scale = scale
  67. self.thickness = thickness
  68. self.cam_p = np.array(cam_pos)
  69. self.up_axis = utils.str_to_axis(v_up_str)
  70. self.ground_node = None
  71. self.init_pyrender()
  72. self.pt_pool = []
  73. self.cap_pool = []
  74. plt.axis('off')
  75. self.ims = None
  76. self.progress = tqdm.tqdm(total=len(motion.poses)) # Initialise
  77. def render_point(self, at_index, p, scale=1.0, radius=1.0, color=[1.0, 0.0, 0.0]):
  78. if at_index >= len(self.pt_pool):
  79. # create enough to allow at_index to work
  80. for i in range(len(self.pt_pool), at_index + 1): # must include at_index too
  81. # get primitive first
  82. sphere_trimesh = trimesh.creation.icosphere(radius=radius, subdivisions=1)
  83. sphere_face_colors = np.zeros(sphere_trimesh.faces.shape)
  84. sphere_face_colors[:] = np.array(color)
  85. sphere_trimesh.visual.face_colors = sphere_face_colors
  86. sphere_mesh = Mesh.from_trimesh(sphere_trimesh, smooth=False)
  87. sphere_node = Node(mesh=sphere_mesh,
  88. name="sphere_" + str(i)) # , translation=np.array([-0.1, -0.10, 0.05]))
  89. self.scene.add_node(sphere_node)
  90. self.pt_pool.append(sphere_node)
  91. # okay now we know the node exists, just change it's position
  92. self.pt_pool[at_index].scale = [scale] * 3
  93. self.pt_pool[at_index].translation = p
  94. def render_capsule(self, at_index, p, Q, length, scale=1.0, color=[1.0, 0.0, 0.0]):
  95. if at_index >= len(self.cap_pool):
  96. # create enough to allow at_index to work
  97. for i in range(len(self.cap_pool), at_index + 1): # must include at_index too
  98. # get primitive first
  99. sphere_trimesh = trimesh.creation.capsule(height=1.0, radius=1.0, count=[8, 8])
  100. sphere_face_colors = np.zeros(sphere_trimesh.faces.shape)
  101. sphere_face_colors[:] = np.array(color)
  102. sphere_trimesh.visual.face_colors = sphere_face_colors
  103. sphere_mesh = Mesh.from_trimesh(sphere_trimesh, smooth=False)
  104. sphere_node = Node(mesh=sphere_mesh,
  105. name="capsule_" + str(i)) # , translation=np.array([-0.1, -0.10, 0.05]))
  106. self.scene.add_node(sphere_node)
  107. self.cap_pool.append(sphere_node)
  108. # okay now we know the node exists, just change it's position
  109. self.cap_pool[at_index].scale = [0.1, 0.1, length]
  110. self.cap_pool[at_index].translation = p
  111. self.cap_pool[at_index].rotation = Q
  112. # print("Q: " + str(Q))
  113. def _render_pose(self, pose, color):
  114. skel = pose.skel
  115. capnum = 0
  116. for ipt, j in enumerate(skel.joints):
  117. T = pose.get_transform(j, local=False)
  118. pos = 0.4 * conversions.T2p(T)
  119. self.render_point(ipt, pos, radius=0.03 * self.scale, color=color)
  120. if j.parent_joint is not None:
  121. # returns X that X dot vec1 = vec2
  122. pos_parent = 0.5 * conversions.T2p(
  123. pose.get_transform(j.parent_joint, local=False)
  124. )
  125. p = 0.4 * (pos_parent + pos)
  126. l = np.linalg.norm(pos_parent - pos)
  127. # l=2.0
  128. r = 0.1 * self.thickness
  129. R = math.R_from_vectors(np.array([0, 0, 1]), pos_parent - pos)
  130. self.render_capsule(capnum,
  131. p,
  132. # conversions.p2T(p),
  133. R2Q(R),
  134. l / 2.0,
  135. # r * self.scale,
  136. 0.1,
  137. color=color
  138. )
  139. capnum += 1
  140. def _render_characters(self, colors, frame):
  141. # t = self.cur_time % motion.length()
  142. skel = self.motion.skel
  143. # pose = motion.get_pose_by_frame(motion.time_to_frame(t))
  144. pose = self.motion.get_pose_by_frame(frame)
  145. # pose = motion.get_pose_by_frame(0)
  146. color = colors[0 % len(colors)]
  147. self._render_pose(pose, color)
  148. def render_ground(self, size=[20.0, 20.0],
  149. dsize=[1.0, 1.0],
  150. color=[0.0, 0.0, 0.0, 1.0],
  151. line_width=1.0,
  152. axis="y",
  153. origin=True,
  154. use_arrow=False,
  155. lighting=False):
  156. if self.ground_node is None:
  157. lx = size[0]
  158. lz = size[1]
  159. dx = dsize[0]
  160. dz = dsize[1]
  161. nx = int(lx / dx) + 1
  162. nz = int(lz / dz) + 1
  163. grid_pts = np.zeros((2 * nx + 2 * nz, 3))
  164. colors = np.zeros((2 * nx + 2 * nz, 4))
  165. colors[:] = np.array(color)
  166. if axis is "x":
  167. linei = 0
  168. for i in np.linspace(-0.5 * lx, 0.5 * lx, nx):
  169. grid_pts[2 * linei] = [0, i, -0.5 * lz]
  170. grid_pts[2 * linei + 1] = [0, i, 0.5 * lz]
  171. linei += 1
  172. for i in np.linspace(-0.5 * lz, 0.5 * lz, nz):
  173. grid_pts[2 * linei] = [0, -0.5 * lx, i]
  174. grid_pts[2 * linei + 1] = [0, 0.5 * lx, i]
  175. linei += 1
  176. elif axis is "y":
  177. linei = 0
  178. for i in np.linspace(-0.5 * lx, 0.5 * lx, nx):
  179. grid_pts[2 * linei] = [i, 0, -0.5 * lz]
  180. grid_pts[2 * linei + 1] = [i, 0, 0.5 * lz]
  181. linei += 1
  182. for i in np.linspace(-0.5 * lz, 0.5 * lz, nz):
  183. grid_pts[2 * linei] = [-0.5 * lx, 0, i]
  184. grid_pts[2 * linei + 1] = [0.5 * lx, 0, i]
  185. linei += 1
  186. elif axis is "z":
  187. linei = 0
  188. for i in np.linspace(-0.5 * lx, 0.5 * lx, nx):
  189. grid_pts[2 * linei] = [i, -0.5 * lz, 0.]
  190. grid_pts[2 * linei + 1] = [i, 0.5 * lz, 0.]
  191. linei += 1
  192. for j, i in enumerate(np.linspace(-0.5 * lz, 0.5 * lz, nz)):
  193. grid_pts[2 * linei] = [-0.5 * lx, i, 0.]
  194. grid_pts[2 * linei + 1] = [0.5 * lx, i, 0.]
  195. linei += 1
  196. grid = pyrender.Primitive(grid_pts, color_0=colors, mode=1) # 1->LINES
  197. grid = pyrender.Mesh([grid])
  198. self.ground_node = Node(mesh=grid, name="ground_plane")
  199. self.scene.add_node(self.ground_node)
  200. def render_callback(self, frame_num):
  201. self.render_ground(
  202. size=[100, 100],
  203. color=[0.8, 0.8, 0.8, 1.0],
  204. axis="y", # utils.axis_to_str(self.motions[0].skel.v_up_env),
  205. origin=not self.hide_origin,
  206. use_arrow=True,
  207. )
  208. colors = [
  209. np.array([123, 174, 85]) / 255.0, # green
  210. np.array([255, 255, 0]) / 255.0, # yellow
  211. np.array([85, 160, 173]) / 255.0, # blue
  212. ]
  213. self._render_characters(colors, frame_num)
  214. color, depth = self.r.render(self.scene)
  215. return color
  216. def animate(self, frame_num):
  217. # print("Rendering frame %d..."%(frame_num))
  218. self.progress.update(frame_num)
  219. color = self.render_callback(frame_num)
  220. if self.ims is None:
  221. self.ims = plt.imshow(color, animated=True)
  222. else:
  223. self.ims.set_array(color)
  224. return self.ims,
  225. def idle_callback(self):
  226. time_elapsed = self.time_checker.get_time(restart=False)
  227. self.cur_time += self.play_speed * time_elapsed
  228. self.time_checker.begin()
  229. def init_pyrender(self):
  230. # ==============================================================================
  231. # Light creation
  232. # ==============================================================================
  233. self.spot_l = pyrender.SpotLight(color=np.ones(3), intensity=3.0,
  234. innerConeAngle=np.pi / 16.0,
  235. outerConeAngle=np.pi / 6.0)
  236. # ==============================================================================
  237. # Camera creation
  238. # ==============================================================================
  239. cam = PerspectiveCamera(yfov=np.pi / 2.0)
  240. R = _get_cam_rotation(self.cam_p,
  241. np.zeros((3)),
  242. self.up_axis)
  243. self.cam_pose = conversions.Rp2T(R, self.cam_p)
  244. # ==============================================================================
  245. # Scene creation
  246. # ==============================================================================
  247. self.scene = Scene(ambient_light=np.array([0.1, 0.1, 0.1, 1.0]))
  248. self.spot_l_node = self.scene.add(self.spot_l, pose=self.cam_pose, name="spot_light")
  249. self.cam_node = self.scene.add(cam, pose=self.cam_pose, name="camera")
  250. self.r = OffscreenRenderer(viewport_width=320, viewport_height=240)
  251. if __name__ == "__main__":
  252. from fairmotion.data import bvh
  253. filename = sys.argv[1]
  254. v_up_env = utils.str_to_axis('y')
  255. motion = bvh.load(
  256. file=filename,
  257. v_up_skel=v_up_env,
  258. v_face_skel=utils.str_to_axis('z'),
  259. v_up_env=v_up_env,
  260. scale=1.0)
  261. R = E2R([-np.pi / 2.0, 0.0, 0.0])
  262. motion = motion_ops.rotate(motion, R)
  263. motion = motion_ops.translate(motion, [0, -20.0, 0])
  264. viewer = MocapViewerOffline(
  265. motion=motion,
  266. cam_pos=[0.0, 1.1, -2.3],
  267. v_up_str='y',
  268. scale=20.33
  269. )
  270. viewer.to_html5_video()