Source code for kimodo.motion_rep.reps.kimodo_motionrep

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from typing import Optional

import einops
import torch
from torch import Tensor

from kimodo.tools import to_numpy

from ...geometry import cont6d_to_matrix, matrix_to_cont6d
from ...skeleton.kinematics import fk
from ...skeleton.transforms import global_rots_to_local_rots
from ...tools import ensure_batched
from ..conditioning import get_unique_index_and_data
from ..feature_utils import RotateFeatures, compute_heading_angle, compute_vel_xyz
from ..feet import foot_detect_from_pos_and_vel
from ..smooth_root import get_smooth_root_pos
from .base import MotionRepBase


[docs] class KimodoMotionRep(MotionRepBase): """Global root / global joints rotations representation, relative to a smooth root."""
[docs] def __init__( self, skeleton, fps, stats_path: Optional[str] = None, ): nbjoints = skeleton.nbjoints self.size_dict = { "smooth_root_pos": torch.Size([3]), "global_root_heading": torch.Size([2]), "local_joints_positions": torch.Size([nbjoints, 3]), "global_rot_data": torch.Size([nbjoints, 6]), "velocities": torch.Size([nbjoints, 3]), "foot_contacts": torch.Size([4]), } self.last_root_feature = "global_root_heading" self.local_root_size_dict = { "local_root_rot_vel": torch.Size([1]), "local_root_vel": torch.Size([2]), "global_root_y": torch.Size([1]), } super().__init__(skeleton, fps, stats_path)
@ensure_batched(local_joint_rots=5, root_positions=3, lengths=1) def __call__( self, local_joint_rots: torch.Tensor, root_positions: torch.Tensor, to_normalize: bool, to_canonicalize: bool = False, lengths: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Convert local rotations and root trajectory into smooth-root features. Args: local_joint_rots: Local joint rotation matrices ``[B, T, J, 3, 3]``. root_positions: Root positions ``[B, T, 3]``. to_normalize: Whether to normalize output features. to_canonicalize: Whether to canonicalize output features (False by default). lengths: Optional valid lengths for variable-length batches. Returns: Motion features with shape ``[B, T, motion_rep_dim]``. """ device = local_joint_rots.device if lengths is None: assert local_joint_rots.shape[0] == 1, "If lenghts is not provided, the input should not be batched." lengths = torch.tensor([local_joint_rots.shape[1]], device=device) ( global_joints_rots, global_joints_positions, local_joints_positions_origin_is_pelvis, ) = fk(local_joint_rots, root_positions, self.skeleton) root_heading_angle = compute_heading_angle(global_joints_positions, self.skeleton) global_root_heading = torch.stack([torch.cos(root_heading_angle), torch.sin(root_heading_angle)], dim=-1) smooth_root_pos = get_smooth_root_pos(root_positions) hips_offset = root_positions - smooth_root_pos hips_offset[..., 1] = root_positions[..., 1] local_joints_positions = local_joints_positions_origin_is_pelvis + hips_offset[:, :, None] velocities = compute_vel_xyz(global_joints_positions, self.fps, lengths=lengths) foot_contacts = foot_detect_from_pos_and_vel(global_joints_positions, velocities, self.skeleton, 0.15, 0.10) global_rot_data = matrix_to_cont6d(global_joints_rots) features, _ = einops.pack( [ smooth_root_pos, global_root_heading, local_joints_positions, global_rot_data, velocities, foot_contacts, ], "batch time *", ) if to_canonicalize: features = self.canonicalize(features, normalized=False) if to_normalize: features = self.normalize(features) return features
[docs] @ensure_batched(features=3, angle=1) def rotate(self, features: torch.Tensor, angle: torch.Tensor): """Rotate root/joint positional and rotational features by heading.""" # assume it is not normalized bs = features.shape[0] device = features.device [ smooth_root_pos, global_root_heading, local_joints_positions, global_rot_data, velocities, foot_contacts, ] = einops.unpack(features, self.ps, "batch time *") if not isinstance(angle, torch.Tensor): angle = torch.tensor(angle, device=device) if len(angle.shape) == 0: angle = angle.repeat(bs) RF = RotateFeatures(angle) new_features, _ = einops.pack( [ RF.rotate_positions(smooth_root_pos), RF.rotate_2d_positions(global_root_heading), RF.rotate_positions(local_joints_positions), RF.rotate_6d_rotations(global_rot_data), RF.rotate_positions(velocities), foot_contacts, ], "batch time *", ) return new_features
[docs] @ensure_batched(features=3, translation_2d=2) def translate_2d( self, features: torch.Tensor, translation_2d: torch.Tensor, ) -> torch.Tensor: """Translate smooth root planar position by ``(dx, dz)``.""" # only move on the ground # If we need a translate_3D function, we should not forget to move the local_joints_positions as well bs = features.shape[0] if len(translation_2d.shape) == 1: translation_2d = translation_2d.repeat(bs, 1) new_features = features.clone() new_smooth_root_pos = new_features[:, :, self.slice_dict["smooth_root_pos"]] new_smooth_root_pos[:, :, 0] += translation_2d[:, [0]] new_smooth_root_pos[:, :, 2] += translation_2d[:, [1]] return new_features
[docs] @ensure_batched(features=3) def inverse( self, features: torch.Tensor, is_normalized: bool, posed_joints_from="rotations", return_numpy: bool = False, ) -> torch.Tensor: """Decode smooth-root features into motion tensors.""" assert posed_joints_from in [ "rotations", "positions", ], "posed_joints_from should 'rotations' or 'positions'" if is_normalized: features = self.unnormalize(features) [ smooth_root_pos, global_root_heading, local_joints_positions, global_rot_data, velocities, foot_contacts, ] = einops.unpack(features, self.ps, "batch time *") global_rot_mats = cont6d_to_matrix(global_rot_data) local_rot_mats = global_rots_to_local_rots(global_rot_mats, self.skeleton) posed_joints_from_pos = local_joints_positions.clone() posed_joints_from_pos[..., 0] += smooth_root_pos[..., None, 0] posed_joints_from_pos[..., 2] += smooth_root_pos[..., None, 2] root_positions = posed_joints_from_pos[..., self.skeleton.root_idx, :] foot_contacts = foot_contacts > 0.5 if posed_joints_from == "rotations": _, posed_joints, _ = self.skeleton.fk( local_rot_mats, root_positions, ) else: posed_joints = posed_joints_from_pos output_tensor_dict = { "local_rot_mats": local_rot_mats, "global_rot_mats": global_rot_mats, "posed_joints": posed_joints, "root_positions": root_positions, "smooth_root_pos": smooth_root_pos, "foot_contacts": foot_contacts, "global_root_heading": global_root_heading, } if return_numpy: return to_numpy(output_tensor_dict) return output_tensor_dict
[docs] def create_conditions( self, index_dict: dict[Tensor], data_dict: dict[Tensor], length: int, to_normalize: bool, device: str, ): """Build sparse conditioning tensors for smooth-root representation.""" # create empty features and mask to be filled in observed_motion = torch.zeros(length, self.motion_rep_dim, device=device) motion_mask = torch.zeros(length, self.motion_rep_dim, dtype=bool, device=device) def _cat_indices(indices_list: list[Tensor]) -> Tensor: indices = torch.cat([torch.tensor(x) if not isinstance(x, Tensor) else x for x in indices_list]) return indices.to(device=device, dtype=torch.long) def _match_obs_dtype(tensor: Tensor) -> Tensor: return tensor.to(device=device, dtype=observed_motion.dtype) if (fname := "smooth_root_2d") in index_dict and index_dict[fname]: indices = _cat_indices(index_dict[fname]) indices, smooth_root_2d = get_unique_index_and_data(indices, torch.cat(data_dict[fname])) smooth_root_2d = _match_obs_dtype(smooth_root_2d) f_sliced = observed_motion[:, self.slice_dict["smooth_root_pos"]] f_sliced[indices, 0] = smooth_root_2d[:, 0] f_sliced[indices, 2] = smooth_root_2d[:, 1] m_sliced = motion_mask[:, self.slice_dict["smooth_root_pos"]] m_sliced[indices, 0] = True m_sliced[indices, 2] = True if (fname := "root_y_pos") in index_dict and index_dict[fname]: indices = _cat_indices(index_dict[fname]) indices, root_pos_Y = get_unique_index_and_data(indices, torch.cat(data_dict[fname])) root_pos_Y = _match_obs_dtype(root_pos_Y) f_sliced = observed_motion[:, self.slice_dict["smooth_root_pos"]] f_sliced[indices, 1] = root_pos_Y m_sliced = motion_mask[:, self.slice_dict["smooth_root_pos"]] m_sliced[indices, 1] = True if (fname := "global_root_heading") in index_dict and index_dict[fname]: indices = _cat_indices(index_dict[fname]) indices, global_root_heading = get_unique_index_and_data(indices, torch.cat(data_dict[fname])) global_root_heading = _match_obs_dtype(global_root_heading) f_sliced = observed_motion[:, self.slice_dict[fname]] f_sliced[indices] = global_root_heading m_sliced = motion_mask[:, self.slice_dict[fname]] m_sliced[indices] = True if (fname := "global_joints_rots") in index_dict and index_dict[fname]: indices_lst = _cat_indices(index_dict[fname]) indices_lst, global_joints_rots = get_unique_index_and_data(indices_lst, torch.cat(data_dict[fname])) global_joints_rots = _match_obs_dtype(global_joints_rots) global_rot_data = matrix_to_cont6d(global_joints_rots) f_sliced = observed_motion[:, self.slice_dict["global_rot_data"]] masking = torch.zeros(len(f_sliced) * self.nbjoints, 6, device=device, dtype=bool) masking[indices_lst.T[0] * self.nbjoints + indices_lst.T[1]] = True masking = masking.reshape(len(f_sliced), self.nbjoints * 6) f_sliced[masking] = global_rot_data.flatten() m_sliced = motion_mask[:, self.slice_dict["global_rot_data"]] m_sliced[masking] = True if (fname := "global_joints_positions") in index_dict and index_dict[fname]: indices_lst = _cat_indices(index_dict[fname]) indices_lst, global_joints_positions = get_unique_index_and_data(indices_lst, torch.cat(data_dict[fname])) global_joints_positions = _match_obs_dtype(global_joints_positions) T_indices = indices_lst[:, 0].contiguous() _test = motion_mask[T_indices, self.slice_dict["smooth_root_pos"]] if not _test[:, [0, 2]].all(): raise ValueError("For constraining global positions, the smooth root should also be constrained.") smooth_root_pos = observed_motion[T_indices, self.slice_dict["smooth_root_pos"]].clone() local_reference = smooth_root_pos.clone() local_reference[..., 1] = 0.0 local_joints_positions = global_joints_positions - local_reference f_sliced = observed_motion[:, self.slice_dict["local_joints_positions"]] masking = torch.zeros(len(f_sliced) * self.nbjoints, 3, device=device, dtype=bool) masking[indices_lst.T[0] * self.nbjoints + indices_lst.T[1]] = True masking = masking.reshape(len(f_sliced), self.nbjoints * 3) f_sliced[masking] = local_joints_positions.flatten() m_sliced = motion_mask[:, self.slice_dict["local_joints_positions"]] m_sliced[masking] = True if to_normalize: observed_motion = self.normalize(observed_motion) return observed_motion, motion_mask