import os #if 'PYOPENGL_PLATFORM' not in os.environ: # os.environ['PYOPENGL_PLATFORM'] = 'egl' import torch import numpy as np import pyrender import trimesh import cv2 from yacs.config import CfgNode from typing import List, Optional def cam_crop_to_full(cam_bbox, box_center, box_size, img_size, focal_length=5000.): # Convert cam_bbox to full image img_w, img_h = img_size[:, 0], img_size[:, 1] cx, cy, b = box_center[:, 0], box_center[:, 1], box_size w_2, h_2 = img_w / 2., img_h / 2. bs = b * cam_bbox[:, 0] + 1e-9 tz = 2 * focal_length / bs tx = (2 * (cx - w_2) / bs) + cam_bbox[:, 1] ty = (2 * (cy - h_2) / bs) + cam_bbox[:, 2] full_cam = torch.stack([tx, ty, tz], dim=-1) return full_cam def get_light_poses(n_lights=5, elevation=np.pi / 3, dist=12): # get lights in a circle around origin at elevation thetas = elevation * np.ones(n_lights) phis = 2 * np.pi * np.arange(n_lights) / n_lights poses = [] trans = make_translation(torch.tensor([0, 0, dist])) for phi, theta in zip(phis, thetas): rot = make_rotation(rx=-theta, ry=phi, order="xyz") poses.append((rot @ trans).numpy()) return poses def make_translation(t): return make_4x4_pose(torch.eye(3), t) def make_rotation(rx=0, ry=0, rz=0, order="xyz"): Rx = rotx(rx) Ry = roty(ry) Rz = rotz(rz) if order == "xyz": R = Rz @ Ry @ Rx elif order == "xzy": R = Ry @ Rz @ Rx elif order == "yxz": R = Rz @ Rx @ Ry elif order == "yzx": R = Rx @ Rz @ Ry elif order == "zyx": R = Rx @ Ry @ Rz elif order == "zxy": R = Ry @ Rx @ Rz return make_4x4_pose(R, torch.zeros(3)) def make_4x4_pose(R, t): """ :param R (*, 3, 3) :param t (*, 3) return (*, 4, 4) """ dims = R.shape[:-2] pose_3x4 = torch.cat([R, t.view(*dims, 3, 1)], dim=-1) bottom = ( torch.tensor([0, 0, 0, 1], device=R.device) .reshape(*(1,) * len(dims), 1, 4) .expand(*dims, 1, 4) ) return torch.cat([pose_3x4, bottom], dim=-2) def rotx(theta): return torch.tensor( [ [1, 0, 0], [0, np.cos(theta), -np.sin(theta)], [0, np.sin(theta), np.cos(theta)], ], dtype=torch.float32, ) def roty(theta): return torch.tensor( [ [np.cos(theta), 0, np.sin(theta)], [0, 1, 0], [-np.sin(theta), 0, np.cos(theta)], ], dtype=torch.float32, ) def rotz(theta): return torch.tensor( [ [np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1], ], dtype=torch.float32, ) def create_raymond_lights() -> List[pyrender.Node]: """ Return raymond light nodes for the scene. """ thetas = np.pi * np.array([1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0]) phis = np.pi * np.array([0.0, 2.0 / 3.0, 4.0 / 3.0]) nodes = [] for phi, theta in zip(phis, thetas): xp = np.sin(theta) * np.cos(phi) yp = np.sin(theta) * np.sin(phi) zp = np.cos(theta) z = np.array([xp, yp, zp]) z = z / np.linalg.norm(z) x = np.array([-z[1], z[0], 0.0]) if np.linalg.norm(x) == 0: x = np.array([1.0, 0.0, 0.0]) x = x / np.linalg.norm(x) y = np.cross(z, x) matrix = np.eye(4) matrix[:3,:3] = np.c_[x,y,z] nodes.append(pyrender.Node( light=pyrender.DirectionalLight(color=np.ones(3), intensity=1.0), matrix=matrix )) return nodes class Renderer: def __init__(self, cfg: CfgNode, faces: np.array): """ Wrapper around the pyrender renderer to render MANO meshes. Args: cfg (CfgNode): Model config file. faces (np.array): Array of shape (F, 3) containing the mesh faces. """ self.cfg = cfg self.focal_length = cfg.EXTRA.FOCAL_LENGTH self.img_res = cfg.MODEL.IMAGE_SIZE # add faces that make the hand mesh watertight faces_new = np.array([[92, 38, 234], [234, 38, 239], [38, 122, 239], [239, 122, 279], [122, 118, 279], [279, 118, 215], [118, 117, 215], [215, 117, 214], [117, 119, 214], [214, 119, 121], [119, 120, 121], [121, 120, 78], [120, 108, 78], [78, 108, 79]]) faces = np.concatenate([faces, faces_new], axis=0) self.camera_center = [self.img_res // 2, self.img_res // 2] self.faces = faces self.faces_left = self.faces[:,[0,2,1]] def __call__(self, vertices: np.array, camera_translation: np.array, image: torch.Tensor, full_frame: bool = False, imgname: Optional[str] = None, side_view=False, rot_angle=90, mesh_base_color=(1.0, 1.0, 0.9), scene_bg_color=(0,0,0), return_rgba=False, ) -> np.array: """ Render meshes on input image Args: vertices (np.array): Array of shape (V, 3) containing the mesh vertices. camera_translation (np.array): Array of shape (3,) with the camera translation. image (torch.Tensor): Tensor of shape (3, H, W) containing the image crop with normalized pixel values. full_frame (bool): If True, then render on the full image. imgname (Optional[str]): Contains the original image filenamee. Used only if full_frame == True. """ if full_frame: image = cv2.imread(imgname).astype(np.float32)[:, :, ::-1] / 255. else: image = image.clone() * torch.tensor(self.cfg.MODEL.IMAGE_STD, device=image.device).reshape(3,1,1) image = image + torch.tensor(self.cfg.MODEL.IMAGE_MEAN, device=image.device).reshape(3,1,1) image = image.permute(1, 2, 0).cpu().numpy() renderer = pyrender.OffscreenRenderer(viewport_width=image.shape[1], viewport_height=image.shape[0], point_size=1.0) material = pyrender.MetallicRoughnessMaterial( metallicFactor=0.0, alphaMode='OPAQUE', baseColorFactor=(*mesh_base_color, 1.0)) camera_translation[0] *= -1. mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy()) if side_view: rot = trimesh.transformations.rotation_matrix( np.radians(rot_angle), [0, 1, 0]) mesh.apply_transform(rot) rot = trimesh.transformations.rotation_matrix( np.radians(180), [1, 0, 0]) mesh.apply_transform(rot) mesh = pyrender.Mesh.from_trimesh(mesh, material=material) scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0], ambient_light=(0.3, 0.3, 0.3)) scene.add(mesh, 'mesh') camera_pose = np.eye(4) camera_pose[:3, 3] = camera_translation camera_center = [image.shape[1] / 2., image.shape[0] / 2.] camera = pyrender.IntrinsicsCamera(fx=self.focal_length, fy=self.focal_length, cx=camera_center[0], cy=camera_center[1], zfar=1e12) scene.add(camera, pose=camera_pose) light_nodes = create_raymond_lights() for node in light_nodes: scene.add_node(node) color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA) color = color.astype(np.float32) / 255.0 renderer.delete() if return_rgba: return color valid_mask = (color[:, :, -1])[:, :, np.newaxis] if not side_view: output_img = (color[:, :, :3] * valid_mask + (1 - valid_mask) * image) else: output_img = color[:, :, :3] output_img = output_img.astype(np.float32) return output_img def vertices_to_trimesh(self, vertices, camera_translation, mesh_base_color=(1.0, 1.0, 0.9), rot_axis=[1,0,0], rot_angle=0, is_right=1): # material = pyrender.MetallicRoughnessMaterial( # metallicFactor=0.0, # alphaMode='OPAQUE', # baseColorFactor=(*mesh_base_color, 1.0)) vertex_colors = np.array([(*mesh_base_color, 1.0)] * vertices.shape[0]) if is_right: mesh = trimesh.Trimesh(vertices.copy() + camera_translation, self.faces.copy(), vertex_colors=vertex_colors) else: mesh = trimesh.Trimesh(vertices.copy() + camera_translation, self.faces_left.copy(), vertex_colors=vertex_colors) # mesh = trimesh.Trimesh(vertices.copy(), self.faces.copy()) rot = trimesh.transformations.rotation_matrix( np.radians(rot_angle), rot_axis) mesh.apply_transform(rot) rot = trimesh.transformations.rotation_matrix( np.radians(180), [1, 0, 0]) mesh.apply_transform(rot) return mesh def render_rgba( self, vertices: np.array, cam_t = None, rot=None, rot_axis=[1,0,0], rot_angle=0, camera_z=3, # camera_translation: np.array, mesh_base_color=(1.0, 1.0, 0.9), scene_bg_color=(0,0,0), render_res=[256, 256], focal_length=None, is_right=None, ): renderer = pyrender.OffscreenRenderer(viewport_width=render_res[0], viewport_height=render_res[1], point_size=1.0) # material = pyrender.MetallicRoughnessMaterial( # metallicFactor=0.0, # alphaMode='OPAQUE', # baseColorFactor=(*mesh_base_color, 1.0)) focal_length = focal_length if focal_length is not None else self.focal_length if cam_t is not None: camera_translation = cam_t.copy() camera_translation[0] *= -1. else: camera_translation = np.array([0, 0, camera_z * focal_length/render_res[1]]) mesh = self.vertices_to_trimesh(vertices, np.array([0, 0, 0]), mesh_base_color, rot_axis, rot_angle, is_right=is_right) mesh = pyrender.Mesh.from_trimesh(mesh) # mesh = pyrender.Mesh.from_trimesh(mesh, material=material) scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0], ambient_light=(0.3, 0.3, 0.3)) scene.add(mesh, 'mesh') camera_pose = np.eye(4) camera_pose[:3, 3] = camera_translation camera_center = [render_res[0] / 2., render_res[1] / 2.] camera = pyrender.IntrinsicsCamera(fx=focal_length, fy=focal_length, cx=camera_center[0], cy=camera_center[1], zfar=1e12) # Create camera node and add it to pyRender scene camera_node = pyrender.Node(camera=camera, matrix=camera_pose) scene.add_node(camera_node) self.add_point_lighting(scene, camera_node) self.add_lighting(scene, camera_node) light_nodes = create_raymond_lights() for node in light_nodes: scene.add_node(node) color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA) color = color.astype(np.float32) / 255.0 renderer.delete() return color def render_rgba_multiple( self, vertices: List[np.array], cam_t: List[np.array], rot_axis=[1,0,0], rot_angle=0, mesh_base_color=(1.0, 1.0, 0.9), scene_bg_color=(0,0,0), render_res=[256, 256], focal_length=None, is_right=None, ): renderer = pyrender.OffscreenRenderer(viewport_width=render_res[0], viewport_height=render_res[1], point_size=1.0) # material = pyrender.MetallicRoughnessMaterial( # metallicFactor=0.0, # alphaMode='OPAQUE', # baseColorFactor=(*mesh_base_color, 1.0)) if is_right is None: is_right = [1 for _ in range(len(vertices))] mesh_list = [pyrender.Mesh.from_trimesh(self.vertices_to_trimesh(vvv, ttt.copy(), mesh_base_color, rot_axis, rot_angle, is_right=sss)) for vvv,ttt,sss in zip(vertices, cam_t, is_right)] scene = pyrender.Scene(bg_color=[*scene_bg_color, 0.0], ambient_light=(0.3, 0.3, 0.3)) for i,mesh in enumerate(mesh_list): scene.add(mesh, f'mesh_{i}') camera_pose = np.eye(4) # camera_pose[:3, 3] = camera_translation camera_center = [render_res[0] / 2., render_res[1] / 2.] focal_length = focal_length if focal_length is not None else self.focal_length camera = pyrender.IntrinsicsCamera(fx=focal_length, fy=focal_length, cx=camera_center[0], cy=camera_center[1], zfar=1e12) # Create camera node and add it to pyRender scene camera_node = pyrender.Node(camera=camera, matrix=camera_pose) scene.add_node(camera_node) self.add_point_lighting(scene, camera_node) self.add_lighting(scene, camera_node) light_nodes = create_raymond_lights() for node in light_nodes: scene.add_node(node) color, rend_depth = renderer.render(scene, flags=pyrender.RenderFlags.RGBA) color = color.astype(np.float32) / 255.0 renderer.delete() return color def add_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0): # from phalp.visualize.py_renderer import get_light_poses light_poses = get_light_poses() light_poses.append(np.eye(4)) cam_pose = scene.get_pose(cam_node) for i, pose in enumerate(light_poses): matrix = cam_pose @ pose node = pyrender.Node( name=f"light-{i:02d}", light=pyrender.DirectionalLight(color=color, intensity=intensity), matrix=matrix, ) if scene.has_node(node): continue scene.add_node(node) def add_point_lighting(self, scene, cam_node, color=np.ones(3), intensity=1.0): # from phalp.visualize.py_renderer import get_light_poses light_poses = get_light_poses(dist=0.5) light_poses.append(np.eye(4)) cam_pose = scene.get_pose(cam_node) for i, pose in enumerate(light_poses): matrix = cam_pose @ pose # node = pyrender.Node( # name=f"light-{i:02d}", # light=pyrender.DirectionalLight(color=color, intensity=intensity), # matrix=matrix, # ) node = pyrender.Node( name=f"plight-{i:02d}", light=pyrender.PointLight(color=color, intensity=intensity), matrix=matrix, ) if scene.has_node(node): continue scene.add_node(node)