Spaces:
Running on Zero
Running on Zero
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from typing import List, Optional, Union | |
| import einops | |
| import torch | |
| from ..geometry import cont6d_to_matrix, matrix_to_cont6d | |
| from ..tools import ensure_batched | |
| def diff_angles(angles, fps: float): | |
| """Computes differences between angles. | |
| Args: | |
| angles (Tensor): [..., T] the batched sequences of rotation angles in radians. | |
| Returns: | |
| Tensor: [..., T-1] the difference between consecutive angles | |
| """ | |
| cos = torch.cos(angles) | |
| sin = torch.sin(angles) | |
| cos_diff = cos[..., 1:] * cos[..., :-1] + sin[..., 1:] * sin[..., :-1] | |
| sin_diff = sin[..., 1:] * cos[..., :-1] - cos[..., 1:] * sin[..., :-1] | |
| # should be close to angles.diff() but more robust | |
| # multiply by fps = 1 / dt | |
| angles_diff = fps * torch.arctan2(sin_diff, cos_diff) | |
| return angles_diff | |
| def compute_vel_xyz( | |
| positions: torch.Tensor, | |
| fps: float, | |
| lengths: Optional[torch.Tensor] = None, | |
| ): | |
| """Compute the velocities from positions: dx/dt. | |
| Works with batches. The last velocity is duplicated to keep the same size. | |
| Args: | |
| positions (torch.Tensor): [..., T, J, 3] xyz positions of a human skeleton | |
| fps (float): frame per seconds | |
| lengths (Optional[torch.Tensor]): [...] size of each input batched. If not provided, positions should not be batched | |
| Returns: | |
| velocity (torch.Tensor): [..., T, J, 3] velocities computed from the positions | |
| """ | |
| device = positions.device | |
| if lengths is None: | |
| assert positions.shape[0] == 1, "If lenghts is not provided, the input should not be batched." | |
| lengths = torch.tensor([len(positions)], device=device) | |
| # compute velocities with fps | |
| velocity = fps * (positions[:, 1:] - positions[:, :-1]) | |
| # pading the velocity vector | |
| vel_pad = torch.zeros_like(velocity[:, 0]) | |
| velocity, _ = einops.pack([velocity, vel_pad], "batch * nbjoints dim") | |
| # repeat the last velocities | |
| # with special care for different lengths with batches | |
| # Use gather/scatter instead of in-place advanced indexing (TRT-compatible) | |
| nj, nd = velocity.shape[2], velocity.shape[3] | |
| # clamp: for length-1 sequences src would be -1 (invalid for gather); | |
| # index 0 is the zero pad, so the copy is a harmless no-op there | |
| src_idx = (lengths - 2).clamp(min=0).long().view(-1, 1, 1, 1).expand(-1, 1, nj, nd) | |
| dst_idx = (lengths - 1).long().view(-1, 1, 1, 1).expand(-1, 1, nj, nd) | |
| velocity = velocity.scatter(1, dst_idx, torch.gather(velocity, 1, src_idx)) | |
| return velocity | |
| def compute_vel_angle( | |
| root_rot_angles: torch.Tensor, | |
| fps: float, | |
| lengths: Optional[torch.Tensor] = None, | |
| ): | |
| """Compute the local root rotation velocity: dtheta/dt. | |
| Args: | |
| root_rot_angles (torch.Tensor): [..., T] rotation angle (in radian) | |
| fps (float): frame per seconds | |
| lengths (Optional[torch.Tensor]): [...] size of each input batched. If not provided, root_rot_angles should not be batched | |
| Returns: | |
| local_root_rot_vel (torch.Tensor): [..., T] local root rotation velocity (in radian/s) | |
| """ | |
| device = root_rot_angles.device | |
| if lengths is None: | |
| assert root_rot_angles.shape[0] == 1, "If lenghts is not provided, the input should not be batched." | |
| lengths = torch.tensor([len(root_rot_angles)], device=device) | |
| local_root_rot_vel = diff_angles(root_rot_angles, fps) | |
| pad_rot_vel_angles = torch.zeros_like(root_rot_angles[:, 0]) | |
| local_root_rot_vel, _ = einops.pack( | |
| [local_root_rot_vel, pad_rot_vel_angles], | |
| "batch *", | |
| ) | |
| # repeat the last rotation angle | |
| # with special care for different lengths with batches | |
| # Use gather/scatter instead of in-place advanced indexing (TRT-compatible) | |
| # clamp: for length-1 sequences src would be -1 (invalid for gather); | |
| # index 0 is the zero pad, so the copy is a harmless no-op there | |
| src_idx = (lengths - 2).clamp(min=0).unsqueeze(-1).long() # [B, 1] | |
| dst_idx = (lengths - 1).unsqueeze(-1).long() # [B, 1] | |
| local_root_rot_vel = local_root_rot_vel.scatter(1, dst_idx, torch.gather(local_root_rot_vel, 1, src_idx)) | |
| return local_root_rot_vel | |
| def compute_heading_angle(posed_joints: torch.Tensor, skeleton): | |
| """Compute the heading direction from the joint positions, by looking at the hip vector. | |
| Args: | |
| posed_joints (torch.Tensor): [B, T, J, 3] global positions | |
| skeleton (SkeletonBase): skeleton of the human, used to find location of hips | |
| Returns: | |
| heading (torch.Tensor): [B] heading angle | |
| """ | |
| # compute root heading for the sequence from hip positions | |
| r_hip, l_hip = skeleton.hip_joint_idx | |
| diff = posed_joints[:, :, r_hip] - posed_joints[:, :, l_hip] | |
| heading_angle = torch.atan2(diff[..., 2], -diff[..., 0]) | |
| return heading_angle | |
| def length_to_mask( | |
| length: Union[torch.Tensor, List], | |
| max_len: Optional[int] = None, | |
| device=None, | |
| ) -> torch.Tensor: | |
| if isinstance(length, list): | |
| if device is None: | |
| device = "cpu" | |
| length = torch.tensor(length, device=device) | |
| if device is not None: | |
| assert device == length.device | |
| device = length.device | |
| if max_len is None: | |
| max_len = max(length) | |
| mask = torch.arange(max_len, device=device).expand(len(length), max_len) < length.unsqueeze(1) | |
| return mask | |
| class RotateFeatures: | |
| """Helper that applies a global heading rotation to motion features.""" | |
| def __init__(self, angle: torch.Tensor): | |
| """Precompute 2D and 3D rotation matrices for a batch of angles. | |
| Args: | |
| angle: Rotation angle(s) in radians, shaped ``[B]``. | |
| """ | |
| self.angle = angle | |
| cos, sin = torch.cos(angle), torch.sin(angle) | |
| one, zero = torch.ones_like(angle), torch.zeros_like(angle) | |
| # 2D rotation transposed (sin are -sin) | |
| self.corrective_mat_2d_T = torch.stack((cos, sin, -sin, cos), -1).reshape(angle.shape + (2, 2)) | |
| # 3D rotation on Y axis | |
| self.corrective_mat_Y = torch.stack((cos, zero, sin, zero, one, zero, -sin, zero, cos), -1).reshape( | |
| angle.shape + (3, 3) | |
| ) | |
| self.corrective_mat_Y_T = self.corrective_mat_Y.transpose(-2, -1).contiguous() | |
| def rotate_positions(self, positions: torch.Tensor): | |
| """Rotate 3D positions around the Y axis.""" | |
| return positions @ self.corrective_mat_Y_T | |
| def rotate_2d_positions(self, positions_2d: torch.Tensor): | |
| """Rotate 2D ``(x, z)`` vectors in the ground plane.""" | |
| return positions_2d @ self.corrective_mat_2d_T | |
| def rotate_rotations(self, rotations: torch.Tensor): | |
| """Left-multiply global rotation matrices by the heading correction.""" | |
| # "Rotate" the global rotations | |
| # which means add an extra Y rotation after the transform | |
| # so at the left R' = R_y R | |
| # (since we use the convention x' = R x) | |
| # "bik,btdkj->btdij" | |
| B, T, J = rotations.shape[:3] | |
| BTJ = B * T * J | |
| return ( | |
| self.corrective_mat_Y[:, None, None].expand(B, T, J, 3, 3).reshape(BTJ, 3, 3) @ rotations.reshape(BTJ, 3, 3) | |
| ).reshape(B, T, J, 3, 3) | |
| def rotate_6d_rotations(self, rotations_6d: torch.Tensor): | |
| """Rotate 6D rotation features via matrix conversion.""" | |
| return matrix_to_cont6d(self.rotate_rotations(cont6d_to_matrix(rotations_6d))) | |