Spaces:
Running on Zero
Running on Zero
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from einops import einsum | |
| import gem.utils.matrix as matrix | |
| from gem.utils.pylogger import Log | |
| from gem.utils.rotation_conversions import ( | |
| euler_angles_to_matrix, | |
| matrix_to_quaternion, | |
| matrix_to_rotation_6d, | |
| quaternion_to_axis_angle, | |
| ) | |
| def qbetween(v0, v1): | |
| """Quaternion (w, x, y, z) that rotates unit vector v0 to unit vector v1.""" | |
| assert v0.shape[-1] == 3 | |
| assert v1.shape[-1] == 3 | |
| v = torch.cross(v0, v1, dim=-1) | |
| w = torch.sqrt((v0**2).sum(dim=-1, keepdim=True) * (v1**2).sum(dim=-1, keepdim=True)) + ( | |
| v0 * v1 | |
| ).sum(dim=-1, keepdim=True) | |
| # Handle anti-parallel vectors (v0 ≈ -v1): use y-axis as fallback rotation axis | |
| y_vec = torch.zeros_like(v) | |
| y_vec[..., 1] = 1.0 | |
| mask = (v.norm(dim=-1) == 0) & (w.sum(dim=-1).abs() <= 1e-4) | |
| v = v.clone() | |
| v[mask] = y_vec[mask] | |
| q = torch.cat([w, v], dim=-1) | |
| return q / q.norm(dim=-1, keepdim=True).clamp(min=1e-8) | |
| def homo_points(points): | |
| """ | |
| Args: | |
| points: (..., C) | |
| Returns: (..., C+1), with 1 padded | |
| """ | |
| return F.pad(points, [0, 1], value=1.0) | |
| def apply_Ts_on_seq_points(points, Ts): | |
| """ | |
| perform translation matrix on related point | |
| Args: | |
| points: (..., N, 3) | |
| Ts: (..., N, 4, 4) | |
| Returns: (..., N, 3) | |
| """ | |
| points = torch.torch.einsum("...ki,...i->...k", Ts[..., :3, :3], points) + Ts[..., :3, 3] | |
| return points | |
| def apply_T_on_points(points, T): | |
| """ | |
| Args: | |
| points: (..., N, 3) | |
| T: (..., 4, 4) | |
| Returns: (..., N, 3) | |
| """ | |
| points_T = torch.einsum("...ki,...ji->...jk", T[..., :3, :3], points) + T[..., None, :3, 3] | |
| return points_T | |
| def T_transforms_points(T, points, pattern): | |
| """manual mode of apply_T_on_points | |
| T: (..., 4, 4) | |
| points: (..., 3) | |
| pattern: "... c d, ... d -> ... c" | |
| """ | |
| return einsum(T, homo_points(points), pattern)[..., :3] | |
| def project_p2d(points, K=None, is_pinhole=True): | |
| """ | |
| Args: | |
| points: (..., (N), 3) | |
| K: (..., 3, 3) | |
| Returns: shape is similar to points but without z | |
| """ | |
| points = points.clone() | |
| if is_pinhole: | |
| z = points[..., [-1]] | |
| z.masked_fill_(z.abs() < 1e-6, 1e-6) | |
| points_proj = points / z | |
| else: # orthogonal | |
| points_proj = F.pad(points[..., :2], (0, 1), value=1) | |
| if K is not None: | |
| # Handle N | |
| if len(points_proj.shape) == len(K.shape): | |
| p2d_h = torch.einsum("...ki,...ji->...jk", K, points_proj) | |
| else: | |
| p2d_h = torch.einsum("...ki,...i->...k", K, points_proj) | |
| else: | |
| p2d_h = points_proj[..., :2] | |
| return p2d_h[..., :2] | |
| def gen_uv_from_HW(H, W, device="cpu"): | |
| """Returns: (H, W, 2), as float. Note: uv not ij""" | |
| grid_v, grid_u = torch.meshgrid(torch.arange(H), torch.arange(W)) | |
| return ( | |
| torch.stack( | |
| [grid_u, grid_v], | |
| dim=-1, | |
| ) | |
| .float() | |
| .to(device) | |
| ) # (H, W, 2) | |
| def unproject_p2d(uv, z, K): | |
| """we assume a pinhole camera for unprojection | |
| uv: (B, N, 2) | |
| z: (B, N, 1) | |
| K: (B, 3, 3) | |
| Returns: (B, N, 3) | |
| """ | |
| xy_atz1 = (uv - K[:, None, :2, 2]) / K[:, None, [0, 1], [0, 1]] # (B, N, 2) | |
| xyz = torch.cat([xy_atz1 * z, z], dim=-1) | |
| return xyz | |
| def cvt_p2d_from_i_to_c(uv, K): | |
| """ | |
| Args: | |
| uv: (..., 2) or (..., N, 2) | |
| K: (..., 3, 3) | |
| Returns: the same shape as input uv | |
| """ | |
| if len(uv.shape) == len(K.shape): | |
| xy = (uv - K[..., None, :2, 2]) / K[..., None, [0, 1], [0, 1]] | |
| else: # without N | |
| xy = (uv - K[..., :2, 2]) / K[..., [0, 1], [0, 1]] | |
| return xy | |
| def cvt_to_bi01_p2d(p2d, bbx_lurb): | |
| """ | |
| p2d: (..., (N), 2) | |
| bbx_lurb: (..., 4) | |
| """ | |
| if len(p2d.shape) == len(bbx_lurb.shape) + 1: | |
| bbx_lurb = bbx_lurb[..., None, :] | |
| bbx_wh = bbx_lurb[..., 2:] - bbx_lurb[..., :2] | |
| bi01_p2d = (p2d - bbx_lurb[..., :2]) / bbx_wh | |
| return bi01_p2d | |
| def cvt_from_bi01_p2d(bi01_p2d, bbx_lurb): | |
| """Use bbx_lurb to resize bi01_p2d to p2d (image-coordinates) | |
| Args: | |
| p2d: (..., 2) or (..., N, 2) | |
| bbx_lurb: (..., 4) | |
| Returns: | |
| p2d: shape is the same as input | |
| """ | |
| bbx_wh = bbx_lurb[..., 2:] - bbx_lurb[..., :2] # (..., 2) | |
| if len(bi01_p2d.shape) == len(bbx_wh.shape) + 1: | |
| p2d = (bi01_p2d * bbx_wh.unsqueeze(-2)) + bbx_lurb[..., None, :2] | |
| else: | |
| p2d = (bi01_p2d * bbx_wh) + bbx_lurb[..., :2] | |
| return p2d | |
| def cvt_p2d_from_bi01_to_c(bi01, bbxs_lurb, Ks): | |
| """ | |
| Args: | |
| bi01: (..., (N), 2), value in range (0,1), the point in the bbx image | |
| bbxs_lurb: (..., 4) | |
| Ks: (..., 3, 3) | |
| Returns: | |
| c: (..., (N), 2) | |
| """ | |
| i = cvt_from_bi01_p2d(bi01, bbxs_lurb) | |
| c = cvt_p2d_from_i_to_c(i, Ks) | |
| return c | |
| def cvt_p2d_from_pm1_to_i(p2d_pm1, bbx_xys): | |
| """ | |
| Args: | |
| p2d_pm1: (..., (N), 2), value in range (-1,1), the point in the bbx image | |
| bbx_xys: (..., 3) | |
| Returns: | |
| p2d: (..., (N), 2) | |
| """ | |
| return bbx_xys[..., :2] + p2d_pm1 * bbx_xys[..., [2]] / 2 | |
| def uv2l_index(uv, W): | |
| return uv[..., 0] + uv[..., 1] * W | |
| def l2uv_index(L, W): | |
| v = torch.div(L, W, rounding_mode="floor") | |
| u = L % W | |
| return torch.stack([u, v], dim=-1) | |
| def transform_mat(R, t): | |
| """ | |
| Args: | |
| R: Bx3x3 array of a batch of rotation matrices | |
| t: Bx3x(1) array of a batch of translation vectors | |
| Returns: | |
| T: Bx4x4 Transformation matrix | |
| """ | |
| # No padding left or right, only add an extra row | |
| if len(R.shape) > len(t.shape): | |
| t = t[..., None] | |
| return torch.cat([F.pad(R, [0, 0, 0, 1]), F.pad(t, [0, 0, 0, 1], value=1)], dim=-1) | |
| def matrix_to_axis_angle(R): | |
| """use pytorch3d so3_log_map | |
| Args: | |
| aa: (*, 3, 3) | |
| Returns: | |
| R: (*, 3) | |
| """ | |
| return quaternion_to_axis_angle(matrix_to_quaternion(R)) | |
| def ransac_PnP(K, pts_2d, pts_3d, err_thr=10): | |
| """solve pnp""" | |
| dist_coeffs = np.zeros(shape=[8, 1], dtype="float64") | |
| pts_2d = np.ascontiguousarray(pts_2d.astype(np.float64)) | |
| pts_3d = np.ascontiguousarray(pts_3d.astype(np.float64)) | |
| K = K.astype(np.float64) | |
| try: | |
| _, rvec, tvec, inliers = cv2.solvePnPRansac( | |
| pts_3d, | |
| pts_2d, | |
| K, | |
| dist_coeffs, | |
| reprojectionError=err_thr, | |
| iterationsCount=10000, | |
| flags=cv2.SOLVEPNP_EPNP, | |
| ) | |
| rotation = cv2.Rodrigues(rvec)[0] | |
| pose = np.concatenate([rotation, tvec], axis=-1) | |
| pose_homo = np.concatenate([pose, np.array([[0, 0, 0, 1]])], axis=0) | |
| inliers = [] if inliers is None else inliers | |
| return pose, pose_homo, inliers | |
| except cv2.error: | |
| Log.warning("CV ERROR") | |
| return np.eye(4)[:3], np.eye(4), [] | |
| def ransac_PnP_batch(K_raw, pts_2d, pts_3d, err_thr=10): | |
| fit_R, fit_t = [], [] | |
| for b in range(K_raw.shape[0]): | |
| pose, _, inliers = ransac_PnP(K_raw[b], pts_2d[b], pts_3d[b], err_thr=err_thr) | |
| fit_R.append(pose[:3, :3]) | |
| fit_t.append(pose[:3, 3]) | |
| fit_R = np.stack(fit_R, axis=0) | |
| fit_t = np.stack(fit_t, axis=0) | |
| return fit_R, fit_t | |
| def get_nearby_points(points, query_verts, padding=0.0, p=1): | |
| """ | |
| points: (S, 3) | |
| query_verts: (V, 3) | |
| """ | |
| if p == 1: | |
| max_xyz = query_verts.max(0)[0] + padding | |
| min_xyz = query_verts.min(0)[0] - padding | |
| idx = ( | |
| (((points - min_xyz) > 0).all(dim=-1) * ((points - max_xyz) < 0).all(dim=-1)) | |
| .nonzero() | |
| .squeeze(-1) | |
| ) | |
| nearby_points = points[idx] | |
| elif p == 2: | |
| raise NotImplementedError("p=2 KNN requires pytorch3d") | |
| return nearby_points | |
| def unproj_bbx_to_fst(bbx_lurb, K, near_z=0.5, far_z=12.5): | |
| B = bbx_lurb.size(0) | |
| uv = bbx_lurb[:, [[0, 1], [2, 1], [2, 3], [0, 3], [0, 1], [2, 1], [2, 3], [0, 3]]] | |
| if isinstance(near_z, float): | |
| z = uv.new([near_z] * 4 + [far_z] * 4).reshape(1, 8, 1).repeat(B, 1, 1) | |
| else: | |
| z = torch.cat( | |
| [ | |
| near_z[:, None, None].repeat(1, 4, 1), | |
| far_z[:, None, None].repeat(1, 4, 1), | |
| ], | |
| dim=1, | |
| ) | |
| c_frustum_points = unproject_p2d(uv, z, K) # (B, 8, 3) | |
| return c_frustum_points | |
| def convert_bbx_xys_to_lurb(bbx_xys): | |
| """ | |
| Args: bbx_xys (..., 3) -> bbx_lurb (..., 4) | |
| """ | |
| size = bbx_xys[..., 2:] | |
| center = bbx_xys[..., :2] | |
| lurb = torch.cat([center - size / 2, center + size / 2], dim=-1) | |
| return lurb | |
| def convert_lurb_to_bbx_xys(bbx_lurb): | |
| """ | |
| Args: bbx_lurb (..., 4) -> bbx_xys (..., 3) be aware that it is squared | |
| """ | |
| size = (bbx_lurb[..., 2:] - bbx_lurb[..., :2]).max(-1, keepdim=True)[0] | |
| center = (bbx_lurb[..., :2] + bbx_lurb[..., 2:]) / 2 | |
| return torch.cat([center, size], dim=-1) | |
| def get_bbx_xys(i_j2d, i_j2d_mask=None, bbx_ratio=None, do_augment=False, base_enlarge=1.2): | |
| """ | |
| Args: | |
| i_j2d: (B, L, J, 3) [x,y,c] or (B, L, J, 2) [x,y] | |
| i_j2d_mask: (B, L, J) boolean mask indicating valid joints, if None use all joints | |
| bbx_ratio: [width, height] ratio for the bounding box | |
| do_augment: whether to apply random augmentation | |
| base_enlarge: factor to enlarge the bounding box | |
| Returns: | |
| bbx_xys: (B, L, 3) [center_x, center_y, size] | |
| """ | |
| if bbx_ratio is None: | |
| bbx_ratio = [192, 256] | |
| # Apply mask if provided | |
| if i_j2d_mask is not None: | |
| # Create a masked version of i_j2d for min/max calculations | |
| # For min calculation, set masked-out joints to large positive values | |
| # For max calculation, set masked-out joints to large negative values | |
| mask_expanded = i_j2d_mask.unsqueeze(-1) # (B, L, J, 1) | |
| # Create copies for min and max calculations | |
| i_j2d_for_min = i_j2d.clone() | |
| i_j2d_for_max = i_j2d.clone() | |
| # Set coordinates of masked joints appropriately | |
| invalid_mask = ~mask_expanded.expand_as(i_j2d[..., :2]) | |
| i_j2d_for_min[..., :2][invalid_mask] = float("inf") # For min, set to large positive | |
| i_j2d_for_max[..., :2][invalid_mask] = float("-inf") # For max, set to large negative | |
| # Calculate min/max using the filtered joints | |
| min_x = i_j2d_for_min[..., 0].min(-1)[0] | |
| max_x = i_j2d_for_max[..., 0].max(-1)[0] | |
| min_y = i_j2d_for_min[..., 1].min(-1)[0] | |
| max_y = i_j2d_for_max[..., 1].max(-1)[0] | |
| else: | |
| # Use all joints | |
| min_x = i_j2d[..., 0].min(-1)[0] | |
| max_x = i_j2d[..., 0].max(-1)[0] | |
| min_y = i_j2d[..., 1].min(-1)[0] | |
| max_y = i_j2d[..., 1].max(-1)[0] | |
| center_x = (min_x + max_x) / 2 | |
| center_y = (min_y + max_y) / 2 | |
| # Size | |
| h = max_y - min_y # (B, L) | |
| w = max_x - min_x # (B, L) | |
| if True: # fit w and h into aspect-ratio | |
| aspect_ratio = bbx_ratio[0] / bbx_ratio[1] | |
| mask1 = w > aspect_ratio * h | |
| h[mask1] = w[mask1] / aspect_ratio | |
| mask2 = w < aspect_ratio * h | |
| w[mask2] = h[mask2] * aspect_ratio | |
| # apply a common factor to enlarge the bounding box | |
| bbx_size = torch.max(h, w) * base_enlarge | |
| if do_augment: | |
| B, L = bbx_size.shape[:2] | |
| device = bbx_size.device | |
| if True: | |
| scaleFactor = torch.rand((B, L), device=device) * 0.3 + 1.05 # 1.05~1.35 | |
| txFactor = torch.rand((B, L), device=device) * 1.6 - 0.8 # -0.8~0.8 | |
| tyFactor = torch.rand((B, L), device=device) * 1.6 - 0.8 # -0.8~0.8 | |
| else: | |
| scaleFactor = torch.rand((B, 1), device=device) * 0.3 + 1.05 # 1.05~1.35 | |
| txFactor = torch.rand((B, 1), device=device) * 1.6 - 0.8 # -0.8~0.8 | |
| tyFactor = torch.rand((B, 1), device=device) * 1.6 - 0.8 # -0.8~0.8 | |
| raw_bbx_size = bbx_size / base_enlarge | |
| bbx_size = raw_bbx_size * scaleFactor | |
| center_x += raw_bbx_size / 2 * ((scaleFactor - 1) * txFactor) | |
| center_y += raw_bbx_size / 2 * ((scaleFactor - 1) * tyFactor) | |
| return torch.stack([center_x, center_y, bbx_size], dim=-1) | |
| def get_bbx_xys_from_xyxy(bbx_xyxy, base_enlarge=1.2): | |
| """ | |
| Args: | |
| bbx_xyxy: (N, 4) [x1, y1, x2, y2] | |
| Returns: | |
| bbx_xys: (N, 3) [center_x, center_y, size] | |
| """ | |
| i_p2d = torch.stack([bbx_xyxy[:, [0, 1]], bbx_xyxy[:, [2, 3]]], dim=1) # (L, 2, 2) | |
| bbx_xys = get_bbx_xys(i_p2d[None], base_enlarge=base_enlarge)[0] | |
| return bbx_xys | |
| def normalize_kp2d(obs_kp2d, bbx_xys, clamp_scale_min=False): | |
| """ | |
| Args: | |
| obs_kp2d: (B, L, J, 3) [x, y, c] | |
| bbx_xys: (B, L, 3) | |
| Returns: | |
| obs: (B, L, J, 3) [x, y, c] | |
| """ | |
| obs_xy = obs_kp2d[..., :2] # (B, L, J, 2) | |
| center = bbx_xys[..., :2] | |
| scale = bbx_xys[..., [2]] | |
| # Mark keypoints outside the bounding box as invisible | |
| xy_max = center + scale / 2 | |
| xy_min = center - scale / 2 | |
| invisible_mask = ( | |
| (obs_xy[..., 0] < xy_min[..., None, 0]) | |
| + (obs_xy[..., 0] > xy_max[..., None, 0]) | |
| + (obs_xy[..., 1] < xy_min[..., None, 1]) | |
| + (obs_xy[..., 1] > xy_max[..., None, 1]) | |
| ) | |
| scale = scale.clamp(min=1e-2) | |
| normalized_obs_xy = 2 * (obs_xy - center.unsqueeze(-2)) / scale.unsqueeze(-2) | |
| if obs_kp2d.shape[-1] > 2: | |
| obs_conf = obs_kp2d[..., 2] # (B, L, J) | |
| obs_conf = obs_conf * ~invisible_mask | |
| return torch.cat([normalized_obs_xy, obs_conf[..., None]], dim=-1) | |
| else: | |
| return normalized_obs_xy | |
| # ================== AZ/AY Transformations ================== # | |
| def compute_T_ayf2az(joints, inverse=False): | |
| """ | |
| Args: | |
| joints: (B, J, 3), in the start-frame, az-coordinate | |
| Returns: | |
| if inverse == False: | |
| T_af2az: (B, 4, 4) | |
| else : | |
| T_az2af: (B, 4, 4) | |
| """ | |
| t_ayf2az = joints[:, 0, :].detach().clone() | |
| t_ayf2az[:, 2] = 0 # do not modify z | |
| RL_xy_h = joints[:, 1, [0, 1]] - joints[:, 2, [0, 1]] # (B, 2), hip point to left side | |
| RL_xy_s = joints[:, 16, [0, 1]] - joints[:, 17, [0, 1]] # (B, 2), shoulder point to left side | |
| RL_xy = RL_xy_h + RL_xy_s | |
| I_mask = RL_xy.pow(2).sum(-1) < 1e-4 # do not rotate, when can't decided the face direction | |
| if I_mask.sum() > 0: | |
| Log.warn(f"{I_mask.sum()} samples can't decide the face direction") | |
| x_dir = F.pad(F.normalize(RL_xy, 2, -1), (0, 1), value=0) # (B, 3) | |
| y_dir = torch.zeros_like(x_dir) | |
| y_dir[..., 2] = 1 | |
| z_dir = torch.cross(x_dir, y_dir, dim=-1) | |
| R_ayf2az = torch.stack([x_dir, y_dir, z_dir], dim=-1) # (B, 3, 3) | |
| R_ayf2az[I_mask] = torch.eye(3).to(R_ayf2az) | |
| if inverse: | |
| R_az2ayf = R_ayf2az.transpose(1, 2) # (B, 3, 3) | |
| t_az2ayf = -einsum(R_ayf2az, t_ayf2az, "b i j , b i -> b j") # (B, 3) | |
| return transform_mat(R_az2ayf, t_az2ayf) | |
| else: | |
| return transform_mat(R_ayf2az, t_ayf2az) | |
| def compute_T_ayfz2ay(joints, inverse=False): | |
| """ | |
| Args: | |
| joints: (B, J, 3), in the start-frame, ay-coordinate | |
| Returns: | |
| if inverse == False: | |
| T_ayfz2ay: (B, 4, 4) | |
| else : | |
| T_ay2ayfz: (B, 4, 4) | |
| """ | |
| t_ayfz2ay = joints[:, 0, :].detach().clone() | |
| t_ayfz2ay[:, 1] = 0 # do not modify y | |
| RL_xz_h = joints[:, 1, [0, 2]] - joints[:, 2, [0, 2]] # (B, 2), hip point to left side | |
| RL_xz_s = joints[:, 16, [0, 2]] - joints[:, 17, [0, 2]] # (B, 2), shoulder point to left side | |
| RL_xz = RL_xz_h + RL_xz_s | |
| I_mask = RL_xz.pow(2).sum(-1) < 1e-4 # do not rotate, when can't decided the face direction | |
| if I_mask.sum() > 0: | |
| Log.warn(f"{I_mask.sum()} samples can't decide the face direction") | |
| x_dir = torch.zeros_like(t_ayfz2ay) # (B, 3) | |
| x_dir[:, [0, 2]] = F.normalize(RL_xz, 2, -1) | |
| y_dir = torch.zeros_like(x_dir) | |
| y_dir[..., 1] = 1 # (B, 3) | |
| z_dir = torch.cross(x_dir, y_dir, dim=-1) | |
| R_ayfz2ay = torch.stack([x_dir, y_dir, z_dir], dim=-1) # (B, 3, 3) | |
| R_ayfz2ay[I_mask] = torch.eye(3).to(R_ayfz2ay) | |
| if inverse: | |
| R_ay2ayfz = R_ayfz2ay.transpose(1, 2) | |
| t_ay2ayfz = -einsum(R_ayfz2ay, t_ayfz2ay, "b i j , b i -> b j") | |
| return transform_mat(R_ay2ayfz, t_ay2ayfz) | |
| else: | |
| return transform_mat(R_ayfz2ay, t_ayfz2ay) | |
| def compute_T_ay2ayrot(joints): | |
| """ | |
| Args: | |
| joints: (B, J, 3), in the start-frame, ay-coordinate | |
| Returns: | |
| T_ay2ayrot: (B, 4, 4) | |
| """ | |
| t_ayrot2ay = joints[:, 0, :].detach().clone() | |
| t_ayrot2ay[:, 1] = 0 # do not modify y | |
| B = joints.shape[0] | |
| euler_angle = torch.zeros((B, 3), device=joints.device) | |
| yrot_angle = torch.rand((B,), device=joints.device) * 2 * torch.pi | |
| euler_angle[:, 0] = yrot_angle | |
| R_ay2ayrot = euler_angles_to_matrix(euler_angle, "YXZ") # (B, 3, 3) | |
| R_ayrot2ay = R_ay2ayrot.transpose(1, 2) | |
| t_ay2ayrot = -einsum(R_ayrot2ay, t_ayrot2ay, "b i j , b i -> b j") | |
| return transform_mat(R_ay2ayrot, t_ay2ayrot) | |
| def compute_root_quaternion_ay(joints): | |
| """ | |
| Args: | |
| joints: (B, J, 3), in the start-frame, ay-coordinate | |
| Returns: | |
| root_quat: (B, 4) from z-axis to fz | |
| """ | |
| joints_shape = joints.shape | |
| joints = joints.reshape((-1,) + joints_shape[-2:]) | |
| t_ayfz2ay = joints[:, 0, :].detach().clone() | |
| t_ayfz2ay[:, 1] = 0 # do not modify y | |
| RL_xz_h = joints[:, 1, [0, 2]] - joints[:, 2, [0, 2]] # (B, 2), hip point to left side | |
| RL_xz_s = joints[:, 16, [0, 2]] - joints[:, 17, [0, 2]] # (B, 2), shoulder point to left side | |
| RL_xz = RL_xz_h + RL_xz_s | |
| I_mask = RL_xz.pow(2).sum(-1) < 1e-4 # do not rotate, when can't decided the face direction | |
| if I_mask.sum() > 0: | |
| Log.warn(f"{I_mask.sum()} samples can't decide the face direction") | |
| x_dir = torch.zeros_like(t_ayfz2ay) # (B, 3) | |
| x_dir[:, [0, 2]] = F.normalize(RL_xz, 2, -1) | |
| y_dir = torch.zeros_like(x_dir) | |
| y_dir[..., 1] = 1 # (B, 3) | |
| z_dir = torch.cross(x_dir, y_dir, dim=-1) | |
| z_dir[..., 2] += 1e-9 | |
| pos_z_vec = torch.tensor([0, 0, 1]).to(joints.device).float() # (3,) | |
| root_quat = qbetween(pos_z_vec[None], z_dir) # (B, 4) | |
| root_quat = root_quat.reshape(joints_shape[:-2] + (4,)) | |
| return root_quat | |
| # ================== Transformations between two sets of features ================== # | |
| def similarity_transform_batch(S1, S2): | |
| """ | |
| Computes a similarity transform (sR, t) that solves the orthogonal Procrutes problem. | |
| Args: | |
| S1, S2: (*, L, 3) | |
| """ | |
| assert S1.shape == S2.shape | |
| S_shape = S1.shape | |
| S1 = S1.reshape(-1, *S_shape[-2:]) | |
| S2 = S2.reshape(-1, *S_shape[-2:]) | |
| S1 = S1.transpose(-2, -1) | |
| S2 = S2.transpose(-2, -1) | |
| # --- The code is borrowed from WHAM --- | |
| # 1. Remove mean. | |
| mu1 = S1.mean(axis=-1, keepdims=True) # axis is along N, S1(B, 3, N) | |
| mu2 = S2.mean(axis=-1, keepdims=True) | |
| X1 = S1 - mu1 | |
| X2 = S2 - mu2 | |
| # 2. Compute variance of X1 used for scale. | |
| var1 = torch.sum(X1**2, dim=1).sum(dim=1) | |
| # 3. The outer product of X1 and X2. | |
| K = X1.bmm(X2.permute(0, 2, 1)) | |
| # 4. Solution that Maximizes trace(R'K) is R=U*V', where U, V are | |
| # singular vectors of K. | |
| U, s, V = torch.svd(K) | |
| # Construct Z that fixes the orientation of R to get det(R)=1. | |
| Z = torch.eye(U.shape[1], device=S1.device).unsqueeze(0) | |
| Z = Z.repeat(U.shape[0], 1, 1) | |
| Z[:, -1, -1] *= torch.sign(torch.det(U.bmm(V.permute(0, 2, 1)))) | |
| # Construct R. | |
| R = V.bmm(Z.bmm(U.permute(0, 2, 1))) | |
| # 5. Recover scale. | |
| scale = torch.cat([torch.trace(x).unsqueeze(0) for x in R.bmm(K)]) / var1 | |
| # 6. Recover translation. | |
| t = mu2 - (scale.unsqueeze(-1).unsqueeze(-1) * (R.bmm(mu1))) | |
| # reshape back | |
| scale = scale.reshape(*S_shape[:-2], 1, 1) | |
| R = R.reshape(*S_shape[:-2], 3, 3) | |
| t = t.reshape(*S_shape[:-2], 3, 1) | |
| return (scale, R), t | |
| def kabsch_algorithm_batch(X1, X2): | |
| """ | |
| Computes a rigid transform (R, t) | |
| Args: | |
| X1, X2: (*, L, 3) | |
| """ | |
| assert X1.shape == X2.shape | |
| X_shape = X1.shape | |
| X1 = X1.reshape(-1, *X_shape[-2:]) | |
| X2 = X2.reshape(-1, *X_shape[-2:]) | |
| # 1. Compute centroids | |
| centroid_X1 = torch.mean(X1, dim=-2, keepdim=True) | |
| centroid_X2 = torch.mean(X2, dim=-2, keepdim=True) | |
| # 2. Center the points | |
| X1_centered = X1 - centroid_X1 | |
| X2_centered = X2 - centroid_X2 | |
| # 3. Compute covariance matrix | |
| H = torch.matmul(X1_centered.transpose(-2, -1), X2_centered) | |
| # 4. SVD | |
| U, S, Vt = torch.linalg.svd(H) | |
| # 5. Compute rotation matrix | |
| R = torch.matmul(Vt.transpose(-2, -1), U.transpose(-2, -1)) | |
| # Fix reflection | |
| d = (torch.det(R) < 0).unsqueeze(-1).unsqueeze(-1) | |
| Vt = torch.where(d, -Vt, Vt) | |
| R = torch.matmul(Vt.transpose(-2, -1), U.transpose(-2, -1)) | |
| # 6. Compute translation vector | |
| t = centroid_X2.transpose(-2, -1) - torch.matmul(R, centroid_X1.transpose(-2, -1)) | |
| # ------- | |
| # reshape back | |
| R = R.reshape(*X_shape[:-2], 3, 3) | |
| t = t.reshape(*X_shape[:-2], 3, 1) | |
| return R, t | |
| # ===== WHAM cam_angvel ===== # | |
| def compute_cam_angvel(R_w2c, padding_last=True): | |
| """ | |
| R_w2c : (F, 3, 3) | |
| """ | |
| # R @ R0 = R1, so R = R1 @ R0^T | |
| cam_angvel = matrix_to_rotation_6d(R_w2c[1:] @ R_w2c[:-1].transpose(-1, -2)) # (F-1, 6) | |
| assert padding_last | |
| cam_angvel = torch.cat([cam_angvel, cam_angvel[-1:]], dim=0) # (F, 6) | |
| return cam_angvel.float() | |
| def compute_cam_tvel(t_w2c, padding_last=True): | |
| """ | |
| t_w2c : (F, 3) | |
| """ | |
| cam_tvel = t_w2c[1:] - t_w2c[:-1] | |
| assert padding_last | |
| cam_tvel = torch.cat([cam_tvel, cam_tvel[-1:]], dim=0) # (F, 3) | |
| return cam_tvel.float() | |
| def compute_cam_tcw2_vel(T_w2c, padding_last=True): | |
| """ | |
| T_w2c : (F, 4, 4) | |
| """ | |
| T_c2w = T_w2c.inverse() | |
| t_c2w = T_c2w[:, :3, 3] | |
| cam_tvel = t_c2w[1:] - t_c2w[:-1] | |
| assert padding_last | |
| cam_tvel = torch.cat([cam_tvel, cam_tvel[-1:]], dim=0) # (F, 3) | |
| return cam_tvel.float() | |
| def ransac_gravity_vec(xyz, num_iterations=100, threshold=0.05, verbose=False): | |
| # xyz: (L, 3) | |
| N = xyz.shape[0] | |
| max_inliers = [] | |
| norms = xyz.norm(dim=-1) # (L,) | |
| for _ in range(num_iterations): | |
| # random select a sample | |
| sample_index = np.random.randint(N) | |
| sample = xyz[sample_index] # (3,) | |
| # compute the angle difference between all points and the sample | |
| dot_product = (xyz * sample).sum(dim=-1) # (L,) | |
| angles = dot_product / norms * norms[sample_index] # (L,) | |
| angles = torch.clamp(angles, -1, 1) # prevent numerical errors | |
| angles = torch.acos(angles) | |
| # determine the inliers | |
| inliers = xyz[angles < threshold] | |
| if len(inliers) > len(max_inliers): | |
| max_inliers = inliers | |
| if len(max_inliers) == N: | |
| break | |
| if verbose: | |
| Log.debug(f"Inliers: {len(max_inliers)} / {N}") | |
| result = max_inliers.mean(dim=0) | |
| return result, max_inliers | |
| def sequence_best_cammat(w_j3d, c_j3d, cam_rot): | |
| # get best camera estimation along the sequence, requires static camera | |
| # w_j3d: (L, J, 3) | |
| # c_j3d: (L, J, 3) | |
| # cam_rot: (L, 3, 3) | |
| L, J, _ = w_j3d.shape | |
| root_in_w = w_j3d[:, 0] # (L, 3) | |
| root_in_c = c_j3d[:, 0] # (L, 3) | |
| cam_mat = matrix.get_TRS(cam_rot, root_in_w) # (L, 4, 4) | |
| cam_pos = matrix.get_position_from(-root_in_c[:, None], cam_mat)[:, 0] # (L, 3) | |
| cam_mat = matrix.set_position(cam_mat, cam_pos) # (L, 4, 4) | |
| w_j3d_expand = w_j3d[None].expand(L, -1, -1, -1) # (L, L, J, 3) | |
| w_j3d_expand = w_j3d_expand.reshape(L, -1, 3) # (L, L*J, 3) | |
| # get reproject error | |
| w_j3d_expand_in_c = matrix.get_relative_position_to(w_j3d_expand, cam_mat) # (L, L*J, 3) | |
| w_j2d_expand_in_c = project_p2d(w_j3d_expand_in_c) # (L, L*J, 2) | |
| w_j2d_expand_in_c = w_j2d_expand_in_c.reshape(L, L, J, 2) # (L, L, J, 2) | |
| c_j2d = project_p2d(c_j3d) # (L, J, 2) | |
| error = w_j2d_expand_in_c - c_j2d[None] # (L, L, J, 2) | |
| error = error.norm(dim=-1).mean(dim=-1) # (L, L) | |
| error = error.mean(dim=-1) # (L,) | |
| ind = error.argmin() | |
| return cam_mat[ind], ind | |
| def get_sequence_cammat(w_j3d, c_j3d, cam_rot): | |
| # w_j3d: (L, J, 3) | |
| # c_j3d: (L, J, 3) | |
| # cam_rot: (L, 3, 3) | |
| L, J, _ = w_j3d.shape | |
| root_in_w = w_j3d[:, 0] # (L, 3) | |
| root_in_c = c_j3d[:, 0] # (L, 3) | |
| cam_mat = matrix.get_TRS(cam_rot, root_in_w) # (L, 4, 4) | |
| cam_pos = matrix.get_position_from(-root_in_c[:, None], cam_mat)[:, 0] # (L, 3) | |
| cam_mat = matrix.set_position(cam_mat, cam_pos) # (L, 4, 4) | |
| return cam_mat | |
| def ransac_vec(vel, min_multiply=20, verbose=False): | |
| # xyz: (L, 3) | |
| # remove outlier velocity | |
| N = vel.shape[0] | |
| vel_1 = vel[None].expand(N, -1, -1) # (L, L, 3) | |
| vel_2 = vel[:, None].expand(-1, N, -1) # (L, L, 3) | |
| dist_mat = (vel_1 - vel_2).norm(dim=-1) # (L, L) | |
| big_identity = torch.eye(N, device=vel.device) * 1e6 | |
| dist_mat_ = dist_mat + big_identity | |
| threshold = dist_mat_.min() * min_multiply | |
| inner_mask = dist_mat < threshold # (L, L) | |
| inner_num = inner_mask.sum(dim=-1) # (L, ) | |
| ind = inner_num.argmax() | |
| result = vel[inner_mask[ind]].mean(dim=0) # (3,) | |
| if verbose: | |
| Log.debug(inner_mask[ind].sum().item()) | |
| return result, inner_mask[ind] | |
| def as_identity(R): | |
| is_I = matrix_to_axis_angle(R).norm(dim=-1) < 1e-5 | |
| R[is_I] = torch.eye(3)[None].expand(is_I.sum(), -1, -1).to(R) | |
| return R | |
| def normalize_T_w2c(T_w2c): | |
| if T_w2c.ndim == 2: | |
| T_w2c = T_w2c[None] | |
| L = T_w2c.shape[0] | |
| device = T_w2c.device | |
| norm_T_c2w = torch.eye(4)[None].repeat(L, 1, 1).to(device) | |
| T_c2w = T_w2c.inverse() | |
| R_c2w = as_identity(T_c2w[:, :3, :3]) | |
| t_c2w = T_c2w[:, :3, 3] | |
| # align the first frame | |
| R0_c2w = R_c2w[:1] | |
| t0_c2w = t_c2w[:1] | |
| norm_R_c2w = R0_c2w.mT @ R_c2w | |
| norm_t_c2w = (R0_c2w.mT @ (t_c2w - t0_c2w)[..., None])[..., 0] | |
| norm_T_c2w[:, :3, :3] = norm_R_c2w | |
| norm_T_c2w[:, :3, 3] = norm_t_c2w | |
| norm_T_w2c = norm_T_c2w.inverse() | |
| norm_T_w2c[:, :3, :3] = as_identity(norm_T_w2c[:, :3, :3]) | |
| norm_T_w2c[:, 3, :3] = 0 | |
| return norm_T_w2c | |