Spaces:
Runtime error
Runtime error
| import torch | |
| import numpy as np | |
| from typing import Union, List, overload | |
| from wis3d import Wis3D | |
| from lib.platform import PM | |
| from lib.utils.geometry.rotation import axis_angle_to_matrix | |
| class HWis3D(Wis3D): | |
| ''' Abstraction of Wis3D for human motion. ''' | |
| def __init__( | |
| self, | |
| out_path : str = PM.outputs / 'wis3d', | |
| seq_name : str = 'debug', | |
| xyz_pattern : tuple = ('x', 'y', 'z'), | |
| ): | |
| seq_name = seq_name.replace('/', '-') | |
| super().__init__(out_path, seq_name, xyz_pattern) | |
| def add_text(self, text:str): | |
| ''' | |
| Add an item of vertices whose name is used to put the text message. *Dirty use!* | |
| ### Args | |
| - text: str | |
| ''' | |
| fake_verts = np.array([[0, 0, 0]]) | |
| self.add_point_cloud( | |
| vertices = fake_verts, | |
| colors = None, | |
| name = text, | |
| ) | |
| def add_text_seq(self, texts:List[str], offset:int=0): | |
| ''' | |
| Add an item of vertices whose name is used to put the text message. *Dirty use!* | |
| ### Args | |
| - texts: List[str] | |
| - The list of text messages. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| fake_verts = np.array([[0, 0, 0]]) | |
| for i, text in enumerate(texts): | |
| self.set_scene_id(i + offset) | |
| self.add_point_cloud( | |
| vertices = fake_verts, | |
| colors = None, | |
| name = text, | |
| ) | |
| def add_image_seq(self, imgs:List[np.ndarray], name:str, offset:int=0): | |
| ''' | |
| Add an item of vertices whose name is used to put the image. *Dirty use!* | |
| ### Args | |
| - imgs: List[np.ndarray] | |
| - The list of images. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| for i, img in enumerate(imgs): | |
| self.set_scene_id(i + offset) | |
| self.add_image( | |
| image = img, | |
| name = name, | |
| ) | |
| def add_motion_mesh( | |
| self, | |
| verts : Union[torch.Tensor, np.ndarray], | |
| faces : Union[torch.Tensor, np.ndarray], | |
| name : str, | |
| offset: int = 0, | |
| ): | |
| ''' | |
| Add sequence of vertices and face(s) to the wis3d viewer. | |
| ### Args | |
| - verts: torch.Tensor or np.ndarray, (L, V, 3), L ~ sequence length, V ~ number of vertices | |
| - faces: torch.Tensor or np.ndarray, (F, 3) or (L, F, 3), F ~ number of faces, L ~ sequence length | |
| - name: str | |
| - The name of the point cloud. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| assert (len(verts.shape) == 3), 'The input `verts` should have 3 dimensions: (L, V, 3).' | |
| assert (verts.shape[-1] == 3), 'The last dimension of `verts` should be 3.' | |
| if isinstance(verts, np.ndarray): | |
| verts = torch.from_numpy(verts) | |
| if isinstance(faces, torch.Tensor): | |
| faces = faces.detach().cpu().numpy() | |
| if len(faces.shape) == 2: | |
| faces = faces[None].repeat(verts.shape[0], 0) | |
| assert (len(faces.shape) == 3), 'The input `faces` should have 2 or 3 dimensions: (F, 3) or (L, F, 3).' | |
| assert (faces.shape[-1] == 3), 'The last dimension of `faces` should be 3.' | |
| assert (verts.shape[0] == faces.shape[0]), 'The first dimension of `verts` and `faces` should be the same.' | |
| L, _, _ = verts.shape | |
| verts = verts.detach().cpu() | |
| # Add vertices frame by frame. | |
| for i in range(L): | |
| self.set_scene_id(i + offset) | |
| self.add_mesh( | |
| vertices = verts[i], | |
| faces = faces[i], | |
| name = name, | |
| ) # type: ignore | |
| # Reset Wis3D scene id. | |
| self.set_scene_id(0) | |
| def add_motion_verts( | |
| self, | |
| verts : Union[torch.Tensor, np.ndarray], | |
| name : str, | |
| offset: int = 0, | |
| ): | |
| ''' | |
| Add sequence of vertices to the wis3d viewer. | |
| ### Args | |
| - verts: torch.Tensor or np.ndarray, (L, V, 3), L ~ sequence length, V ~ number of vertices | |
| - name: str | |
| - The name of the point cloud. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| assert (len(verts.shape) == 3), 'The input `verts` should have 3 dimensions: (L, V, 3).' | |
| assert (verts.shape[-1] == 3), 'The last dimension of `verts` should be 3.' | |
| if isinstance(verts, np.ndarray): | |
| verts = torch.from_numpy(verts) | |
| L, _, _ = verts.shape | |
| verts = verts.detach().cpu() | |
| # Add vertices frame by frame. | |
| for i in range(L): | |
| self.set_scene_id(i + offset) | |
| self.add_point_cloud( | |
| vertices = verts[i], | |
| colors = None, | |
| name = name, | |
| ) | |
| # Reset Wis3D scene id. | |
| self.set_scene_id(0) | |
| def add_motion_skel( | |
| self, | |
| joints : Union[torch.Tensor, np.ndarray], | |
| bones : Union[list, torch.Tensor], | |
| colors : Union[list, torch.Tensor], | |
| name : str, | |
| offset : int = 0, | |
| threshold : float = 0.5, | |
| ): | |
| ''' | |
| Add sequence of joints with specific skeleton to the wis3d viewer. | |
| ### Args | |
| - joints: torch.Tensor or np.ndarray, shape = (L, J, 3) or (L, J, 4), L ~ sequence length, J ~ number of joints | |
| - bones: list | |
| - A list of bones of the skeleton, i.e. the edge in the kinematic trees. | |
| - colors: list | |
| - name: str | |
| - The name of the point cloud. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| - threshold: float, default = 0.5 | |
| - Threshold to filter the confidence of the joints. It's useless when no confidence provided. | |
| ''' | |
| assert (len(joints.shape) == 3), 'The input `joints` should have 3 dimensions: (L, J, 3).' | |
| assert (joints.shape[-1] == 3 or joints.shape[-1] == 4), 'The last dimension of `joints` should be 3 or 4.' | |
| if isinstance(joints, np.ndarray): | |
| joints = torch.from_numpy(joints) | |
| if isinstance(bones, List): | |
| bones = torch.tensor(bones) | |
| if isinstance(colors, List): | |
| colors = torch.tensor(colors) | |
| # Get the sequence length. | |
| joints = joints.detach().cpu() # (L, J, 3) or (L, J, 4) | |
| L, J, D = joints.shape | |
| if D == 4: | |
| conf = joints[:, :, 3] | |
| joints = joints[:, :, :3] | |
| else: | |
| conf = None | |
| # Add vertices frame by frame. | |
| for i in range(L): | |
| self.set_scene_id(i + offset) | |
| bones_s = joints[i][bones[:, 0]] | |
| bones_e = joints[i][bones[:, 1]] | |
| if conf is not None: | |
| mask = torch.logical_and(conf[i][bones[:, 0]] > threshold, conf[i][bones[:, 1]] > threshold) | |
| bones_s, bones_e = bones_s[mask], bones_e[mask] | |
| if len(bones_s) > 0: | |
| self.add_lines( | |
| start_points = bones_s, | |
| end_points = bones_e, | |
| colors = colors, | |
| name = name, | |
| ) | |
| # Reset Wis3D scene id. | |
| self.set_scene_id(0) | |
| def add_vec_seq( | |
| self, | |
| vecs : torch.Tensor, | |
| name : str, | |
| offset : int = 0, | |
| seg_num : int = 16, | |
| ): | |
| ''' | |
| Add directional line sequence to the wis3d viewer. | |
| The line will be gradient colored, and the direction of the vector is visualized as from dark to light. | |
| ### Args | |
| - vecs: torch.Tensor, (L, 2, 3) or (L, N, 2, 3), L ~ sequence length, N ~ vectors counts in one frame, | |
| then give the start 3D point and end 3D point. | |
| - name: str | |
| - The name of the vector. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| - seg_num: int, default = 16 | |
| - The number of segments for gradient color, will just change the visualization effect. | |
| ''' | |
| if len(vecs.shape) == 3: | |
| vecs = vecs[:, None, :, :] # (L, 2, 3) -> (L, 1, 2, 3) | |
| assert (len(vecs.shape) == 4), 'The input `vecs` should have 3 or 4 dimensions: (L, 2, 3) or (L, N, 2, 3).' | |
| assert (vecs.shape[-2:] == (2, 3)), f'The last two dimension of `vecs` should be (2, 3), but got vecs.shape = {vecs.shape}.' | |
| # Get the sequence length. | |
| L, N, _, _ = vecs.shape | |
| vecs = vecs.detach().cpu() | |
| # Cut the line into segments. | |
| steps_delta = (vecs[:, :, [1]] - vecs[:, :, [0]]) / (seg_num + 1) # (L, N, 1, 3) | |
| steps_cnt = torch.arange(seg_num + 1).reshape((1, 1, seg_num + 1, 1)) # (1, 1, seg_num+1, 1) | |
| segs = steps_delta * steps_cnt + vecs[:, :, [0]] # (L, N, seg_num+1, 3) | |
| start_pts = segs[:, :, :-1] # (L, N, seg_num, 3) | |
| end_pts = segs[:, :, 1:] # (L, N, seg_num, 3) | |
| # Prepare the gradient colors. | |
| grad_colors = torch.linspace(0, 255, seg_num).reshape((1, seg_num, 1)).repeat(N, 1, 3) # (N, seg_num, 3) | |
| # Add vertices frame by frame. | |
| for i in range(L): | |
| self.set_scene_id(i + offset) | |
| self.add_lines( | |
| start_points = start_pts[i].reshape(-1, 3), | |
| end_points = end_pts[i].reshape(-1, 3), | |
| colors = grad_colors.reshape(-1, 3), | |
| name = name, | |
| ) | |
| # Reset Wis3D scene id. | |
| self.set_scene_id(0) | |
| def add_traj( | |
| self, | |
| positions : torch.Tensor, | |
| name : str, | |
| offset : int = 0, | |
| ): | |
| ''' | |
| Visualize the the positions change across the time as trajectory. The newer position will be brighter. | |
| ### Args | |
| - positions: torch.Tensor, (L, 3), L ~ sequence length | |
| - name: str | |
| - The name of the trajectory. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| assert (len(positions.shape) == 2), 'The input `positions` should have 2 dimensions: (L, 3).' | |
| assert (positions.shape[-1] == 3), 'The last dimension of `positions` should be 3.' | |
| # Get the sequence length. | |
| L, _ = positions.shape | |
| positions = positions.detach().cpu() | |
| traj = positions[[0]] # (1, 3) | |
| # Prepare the gradient colors. | |
| grad_colors = torch.linspace(208, 48, L).reshape((L, 1)).repeat(1, 3) # (L, 3) | |
| for i in range(L): | |
| traj = torch.cat((traj, positions[[i]]), dim=0) # (i+2, 3) | |
| self.set_scene_id(i + offset) | |
| self.add_lines( | |
| start_points = traj[:-1], | |
| end_points = traj[1:], | |
| colors = grad_colors[-(i+1):], | |
| name = name, | |
| ) | |
| # Reset Wis3D scene id. | |
| self.set_scene_id(0) | |
| def add_sphere_sensors( | |
| self, | |
| positions : torch.Tensor, | |
| radius : Union[torch.Tensor, float], | |
| activities : torch.Tensor, | |
| name : str, | |
| ): | |
| ''' | |
| Draw the sphere sensors with different colors to represent the activities. The color is from white to red. | |
| ### Args | |
| - positions: torch.Tensor, (N, 3), N ~ number of sensors | |
| - radius: torch.Tensor or float, (N,), N ~ number of sensors | |
| - activities: torch.Tensor, (N) | |
| - The activities of the sensors, from 0 to 1. | |
| - name: str | |
| - The name of the spheres. | |
| ''' | |
| assert (len(positions.shape) == 2), 'The input `positions` should have 2 dimensions: (N, 3).' | |
| assert (positions.shape[-1] == 3), 'The last dimension of `positions` should be 3.' | |
| N, _ = positions.shape | |
| if isinstance(radius, float): | |
| radius = torch.Tensor(radius).reshape(1).repeat(N) # (N) | |
| elif len(radius.shape) == 0: | |
| radius = radius.reshape(1).repeat(N) | |
| colors = torch.ones(size=(N, 3)).float() | |
| colors[:, 0] = 255 | |
| colors[:, 1] = (1 - activities) ** 2 * 255 | |
| colors[:, 2] = (1 - activities) ** 2 * 255 | |
| self.add_spheres( | |
| centers = positions, | |
| radius = radius, | |
| colors = colors, | |
| name = name, | |
| ) | |
| def add_sphere_sensors_seq( | |
| self, | |
| positions : torch.Tensor, | |
| radius : Union[torch.Tensor, float], | |
| activities : torch.Tensor, | |
| name : str, | |
| offset : int = 0, | |
| ): | |
| ''' | |
| Draw the sphere sensors with different colors to represent the activities. The color is from white to red. | |
| ### Args | |
| - positions: torch.Tensor, (L, N, 3), N ~ number of sensors | |
| - radius: torch.Tensor or float, (L, N,), N ~ number of sensors | |
| - activities: torch.Tensor, (L, N) | |
| - The activities of the sensors, from 0 to 1. | |
| - name: str | |
| - The name of the spheres. | |
| - offset: int, default = 0 | |
| - The offset for the sequence index. | |
| ''' | |
| assert (len(positions.shape) == 3), 'The input `positions` should have 3 dimensions: (L, N, 3).' | |
| assert (positions.shape[-1] == 3), 'The last dimension of `positions` should be 3.' | |
| L, N, _ = positions.shape | |
| for i in range(L): | |
| self.set_scene_id(i + offset) | |
| self.add_sphere_sensors( | |
| positions = positions[i], | |
| radius = radius, | |
| activities = activities[i], | |
| name = name, | |
| ) | |
| # ===== Overriding methods from original Wis3D. ===== | |
| def add_lines( | |
| self, | |
| start_points: torch.Tensor, | |
| end_points : torch.Tensor, | |
| colors : Union[list, torch.Tensor] = None, | |
| name : str = None, | |
| thickness : float = 0.01, | |
| resolution : int = 4, | |
| ): | |
| ''' | |
| Add lines by points. Overriding the original `add_lines` method to use mesh to provide browser from crash. | |
| ### Args | |
| - start_points: torch.Tensor, (N, 3), N ~ number of lines | |
| - end_points: torch.Tensor, (N, 3), N ~ number of lines | |
| - colors: list or torch.Tensor, (N, 3) | |
| - The color of the lines, from 0 to 255. | |
| - name: str | |
| - The name of the vector. | |
| - thickness: float, default = 0.01 | |
| - The thickness of the lines. | |
| - resolution: int, default = 3 | |
| - The 'line' was actually a poly-cylinder, and the resolution how it looks like a cylinder. | |
| ''' | |
| if isinstance(colors, List): | |
| colors = torch.tensor(colors) | |
| assert (len(start_points.shape) == 2), 'The input `start_points` should have 2 dimensions: (N, 3).' | |
| assert (len(end_points.shape) == 2), 'The input `end_points` should have 2 dimensions: (N, 3).' | |
| assert (start_points.shape == end_points.shape), 'The input `start_points` and `end_points` should have the same shape.' | |
| # ===== Prepare the data. ===== | |
| N, _ = start_points.shape | |
| device = start_points.device | |
| dir = end_points - start_points # (N, 3) | |
| dis = torch.norm(dir, dim=-1, keepdim=True) # (N, 1) | |
| dir = dir / dis # (N, 3) | |
| K = resolution + 1 # the first & the last point share the position | |
| # Find out directions that are negative to the y-axis. | |
| vec_y = torch.Tensor([[0, 1, 0]]).float().to(device) # (1, 3) | |
| neg_mask = (dir @ vec_y.transpose(-1, -2) < 0).squeeze() # (N,) | |
| # ===== Get the ending surface vertices of the cylinder. ===== | |
| # 1. Get the surface vertices template in x-z plain. | |
| radius = torch.linspace(0, 2*torch.pi, K) # (K,) | |
| v_ending_temp = \ | |
| torch.stack( | |
| [torch.cos(radius), torch.zeros_like(radius), torch.sin(radius)], | |
| dim = -1 | |
| ) # (K, 3) | |
| v_ending_temp *= thickness # (K, 3) | |
| v_ending_temp = v_ending_temp[None].repeat(N, 1, 1) # (N, K, 3) | |
| # 2. Rotate the template plane to the direction of the line. | |
| rot_axis = torch.linalg.cross(vec_y, dir) # (N, 3) | |
| rot_axis[neg_mask] *= -1 | |
| rot_mat = axis_angle_to_matrix(rot_axis) # (N, 3, 3) | |
| v_ending_temp = v_ending_temp @ rot_mat.transpose(-1, -2) | |
| v_ending_temp = v_ending_temp.to(device) | |
| # 3. Move the template plane to the start and end points and get the cylinder vertices. | |
| v_cylinder_start = v_ending_temp + start_points[:, None] # (N, K, 3) | |
| v_cylinder_end = v_ending_temp + end_points[:, None] # (N, K, 3) | |
| # Swap the start and end points for the negative direction to adjust the normal direction. | |
| v_cylinder_start[neg_mask], v_cylinder_end[neg_mask] = v_cylinder_end[neg_mask], v_cylinder_start[neg_mask] | |
| v_cylinder = torch.cat([v_cylinder_start, v_cylinder_end], dim=1) # (N, 2*K, 3) | |
| # ===== Calculate the face index. ===== | |
| idx = torch.arange(0, 2*K, device=device).to(device) # (2*K,) | |
| idx_s, idx_e = idx[:K], idx[K:] | |
| f_cylinder = torch.cat([ | |
| # Two ending surface. | |
| torch.stack([idx_s[0].repeat(K-2), idx_s[1:-1], idx_s[2:]], dim=-1), | |
| torch.stack([idx_e[0].repeat(K-2), idx_e[2:], idx_e[1:-1]], dim=-1), | |
| # The side surface. | |
| torch.stack([idx_e[:-1], idx_s[1:], idx_s[:-1]], dim=-1), | |
| torch.stack([idx_e[:-1], idx_e[1:], idx_s[1:]], dim=-1), | |
| ], dim=0) # (4*K-4, 3) | |
| f_cylinder = f_cylinder[None].repeat(N, 1, 1) # (N, 4*K-4, 3) | |
| # ===== Calculate the face index. ===== | |
| if colors is not None: | |
| c_cylinder = colors / 255.0 # (N, 3) | |
| c_cylinder = c_cylinder[:, None].repeat(1, 2*K, 1) # (N, 2*K, 3) | |
| else: | |
| c_cylinder = None | |
| N, V = v_cylinder.shape[:2] | |
| v_cylinder = v_cylinder.reshape(-1, 3) # (N*(2*K), 3) | |
| # ===== Manually match the points index before flatten. ===== | |
| f_cylinder = f_cylinder + torch.arange(0, N, device=device).unsqueeze(1).unsqueeze(1) * V | |
| f_cylinder = f_cylinder.reshape(-1, 3) # (N*(4*K-4), 3) | |
| if c_cylinder is not None: | |
| c_cylinder = c_cylinder.reshape(-1, 3) # (N*(2*K), 3) | |
| self.add_mesh( | |
| vertices = v_cylinder, | |
| vertex_colors = c_cylinder, | |
| faces = f_cylinder, | |
| name = name, | |
| ) |