| """MEI (Motion Euclidean-Invariant) representation. |
| |
| Converts between SMPL-H parameters and the 138-dimensional MEI vector, |
| which provides SE(2) invariance (ground-plane translation + yaw rotation). |
| |
| MEI vector layout (138D): |
| [0:2] CoM planar velocity in local frame (v_x, v_z) 2D |
| [2:3] Heading angular velocity (d_theta / dt) 1D |
| [3:6] Root position relative to CoM pivot, local frame 3D |
| [6:12] Root rotation (heading-relative), 6D continuous 6D |
| [12:138] Body joint rotations, 21 joints x 6D 126D |
| --------------------------------------------------------------- |
| Total 138D |
| |
| Coordinate convention (local frame, after heading alignment): |
| X-axis: right |
| Y-axis: up |
| Z-axis: forward (heading direction) |
| """ |
|
|
| import numpy as np |
| from .geometry import ( |
| axis_angle_to_matrix, |
| matrix_to_axis_angle, |
| matrix_to_rotation_6d, |
| rotation_6d_to_matrix, |
| yaw_rotation_matrix, |
| wrap_angle, |
| ) |
|
|
| MEI_DIM = 138 |
| NUM_JOINTS = 22 |
|
|
| |
| SMPL_JOINT_NAMES = [ |
| "Pelvis", "L_Hip", "R_Hip", "Spine1", |
| "L_Knee", "R_Knee", "Spine2", "L_Ankle", |
| "R_Ankle", "Spine3", "L_Foot", "R_Foot", |
| "Neck", "L_Collar", "R_Collar", "Head", |
| "L_Shoulder", "R_Shoulder", "L_Elbow", "R_Elbow", |
| "L_Wrist", "R_Wrist", |
| ] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _MASS_WEIGHTS = np.array([ |
| 1, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| 0, |
| ], dtype=np.float64) |
| JOINT_MASS_WEIGHTS = _MASS_WEIGHTS / _MASS_WEIGHTS.sum() |
|
|
|
|
| |
| |
| |
|
|
| def compute_com(joints: np.ndarray) -> np.ndarray: |
| """Compute center of mass from 22-joint positions. |
| |
| Args: |
| joints: (T, 22, 3) world-frame joint positions. |
| |
| Returns: |
| com: (T, 3) center of mass. |
| """ |
| w = JOINT_MASS_WEIGHTS.reshape(1, -1, 1) |
| return (joints * w).sum(axis=1) |
|
|
|
|
| def compute_com_ground(joints: np.ndarray) -> np.ndarray: |
| """CoM projected onto the ground plane (Y = 0). |
| |
| Args: |
| joints: (T, 22, 3) world-frame joint positions. |
| |
| Returns: |
| com_ground: (T, 3) with Y component set to 0. |
| """ |
| com = compute_com(joints) |
| com[:, 1] = 0.0 |
| return com |
|
|
|
|
| def compute_heading(joints: np.ndarray) -> np.ndarray: |
| """Extract heading angle from joint positions (HumanML3D convention). |
| |
| The heading (facing direction) is determined from the average of |
| hip and shoulder lateral (left-to-right) vectors, cross-producted |
| with the Y-up axis to obtain the forward direction in the XZ plane. |
| |
| To avoid 180-degree flips, each frame's heading is compared with |
| the previous frame and the closer of {heading, heading + pi} is kept. |
| |
| Args: |
| joints: (T, 22, 3) world-frame joint positions. |
| |
| Returns: |
| heading: (T,) heading angles in radians. |
| """ |
| r_hip = SMPL_JOINT_NAMES.index("R_Hip") |
| l_hip = SMPL_JOINT_NAMES.index("L_Hip") |
| sdr_r = SMPL_JOINT_NAMES.index("R_Shoulder") |
| sdr_l = SMPL_JOINT_NAMES.index("L_Shoulder") |
|
|
| |
| across1 = joints[:, r_hip] - joints[:, l_hip] |
| across2 = joints[:, sdr_r] - joints[:, sdr_l] |
| across = across1 + across2 |
|
|
| |
| |
| |
| forward = np.cross(np.array([[0, 1, 0]]), across, axis=-1) |
| xz_norm = np.linalg.norm(forward, axis=-1) |
| forward = forward / np.maximum(xz_norm[:, np.newaxis], 1e-12) |
|
|
| raw_heading = np.arctan2(forward[:, 0], forward[:, 2]) |
|
|
| |
| heading = np.empty_like(raw_heading) |
| heading[0] = raw_heading[0] |
| for t in range(1, len(heading)): |
| |
| if xz_norm[t] < 1e-3: |
| heading[t] = heading[t - 1] |
| continue |
| h = raw_heading[t] |
| h_flip = h + np.pi |
| |
| diff_h = abs(wrap_angle(h - heading[t - 1])) |
| diff_flip = abs(wrap_angle(h_flip - heading[t - 1])) |
| heading[t] = h if diff_h <= diff_flip else wrap_angle(h_flip) |
| return heading |
|
|
|
|
| |
| |
| |
|
|
| def smplh_to_mei( |
| root_orient: np.ndarray, |
| body_pose: np.ndarray, |
| joints: np.ndarray, |
| fps: float = 30.0, |
| ) -> np.ndarray: |
| """Convert SMPL-H parameters to MEI representation. |
| |
| Args: |
| root_orient: (T, 3) root rotation in axis-angle. |
| body_pose: (T, 21, 3) or (T, 63) body joint rotations in axis-angle. |
| joints: (T, 22, 3) world-frame 3D joint positions (from SMPL-H FK). |
| joints[:, 0] is the pelvis (root) world position. |
| fps: frame rate (default 30). |
| |
| Returns: |
| mei: (T, 138) MEI representation. |
| """ |
| T = root_orient.shape[0] |
| if body_pose.ndim == 2 and body_pose.shape[-1] == 63: |
| body_pose = body_pose.reshape(T, 21, 3) |
|
|
| |
| heading = compute_heading(joints) |
|
|
| |
| com_ground = compute_com_ground(joints) |
|
|
| |
| heading_vel = np.zeros(T) |
| if T > 1: |
| heading_vel[0] = wrap_angle(heading[1] - heading[0]) * fps |
| heading_vel[1:] = wrap_angle(heading[1:] - heading[:-1]) * fps |
|
|
| |
| R_heading = yaw_rotation_matrix(heading) |
| R_inv = np.transpose(R_heading, (0, 2, 1)) |
|
|
| com_vel_global = np.zeros_like(com_ground) |
| if T > 1: |
| com_vel_global[0] = (com_ground[1] - com_ground[0]) * fps |
| com_vel_global[1:] = (com_ground[1:] - com_ground[:-1]) * fps |
|
|
| com_vel_local = np.einsum("tij,tj->ti", R_inv, com_vel_global) |
| com_vel_planar = com_vel_local[:, [0, 2]] |
|
|
| |
| root_offset_global = joints[:, 0] - com_ground |
| root_offset_local = np.einsum("tij,tj->ti", R_inv, root_offset_global) |
|
|
| |
| root_rotmat = axis_angle_to_matrix(root_orient) |
| root_rotmat_local = np.einsum("tij,tjk->tik", R_inv, root_rotmat) |
| root_rot6d = matrix_to_rotation_6d(root_rotmat_local) |
|
|
| |
| body_rotmat = axis_angle_to_matrix(body_pose) |
| body_rot6d = matrix_to_rotation_6d(body_rotmat).reshape(T, -1) |
|
|
| |
| mei = np.concatenate([ |
| com_vel_planar, |
| heading_vel[:, None], |
| root_offset_local, |
| root_rot6d, |
| body_rot6d, |
| ], axis=-1) |
|
|
| assert mei.shape == (T, MEI_DIM), f"Expected ({T}, {MEI_DIM}), got {mei.shape}" |
| return mei |
|
|
|
|
| |
| |
| |
|
|
| def mei_to_smplh( |
| mei: np.ndarray, |
| fps: float = 30.0, |
| initial_heading: float = 0.0, |
| initial_com: np.ndarray = np.array([0.0, 0.0]) |
| ) -> dict: |
| """Convert MEI representation back to SMPL-H parameters. |
| |
| Because MEI is SE(2)-invariant, the absolute ground-plane position and |
| yaw are lost. They are recovered from *initial_heading* and |
| *initial_com* (both default to zero, placing the motion at the origin |
| facing +Z). |
| |
| Args: |
| mei: (T, 138) MEI representation. |
| fps: frame rate (default 30). |
| initial_heading: heading angle at frame 0 (radians). |
| initial_com: (2,) XZ position of CoM at frame 0, default [0, 0]. |
| |
| Returns: |
| dict with: |
| root_orient: (T, 3) axis-angle root rotation. |
| body_pose: (T, 63) axis-angle body joint rotations. |
| transl: (T, 3) global root translation. |
| """ |
| T = mei.shape[0] |
|
|
| |
| com_vel_planar = mei[:, 0:2] |
| heading_vel = mei[:, 2] |
| root_offset_local = mei[:, 3:6] |
| root_rot6d = mei[:, 6:12] |
| body_rot6d = mei[:, 12:138] |
|
|
| |
| heading = np.zeros(T) |
| heading[0] = initial_heading |
| for t in range(1, T): |
| heading[t] = heading[t - 1] + heading_vel[t] / fps |
| heading = wrap_angle(heading) |
|
|
| |
| R_heading = yaw_rotation_matrix(heading) |
|
|
| com_vel_local_3d = np.zeros((T, 3)) |
| com_vel_local_3d[:, 0] = com_vel_planar[:, 0] |
| com_vel_local_3d[:, 2] = com_vel_planar[:, 1] |
|
|
| com_vel_global = np.einsum("tij,tj->ti", R_heading, com_vel_local_3d) |
|
|
| com_ground = np.zeros((T, 3)) |
| com_ground[0, 0] = initial_com[0] |
| com_ground[0, 2] = initial_com[1] |
| for t in range(1, T): |
| com_ground[t] = com_ground[t - 1] + com_vel_global[t] / fps |
|
|
| |
| root_offset_global = np.einsum("tij,tj->ti", R_heading, root_offset_local) |
| transl = com_ground + root_offset_global |
|
|
| |
| root_rotmat_local = rotation_6d_to_matrix(root_rot6d) |
| root_rotmat = np.einsum("tij,tjk->tik", R_heading, root_rotmat_local) |
| root_orient = matrix_to_axis_angle(root_rotmat) |
|
|
| |
| body_rotmat = rotation_6d_to_matrix(body_rot6d.reshape(T, 21, 6)) |
| body_pose = matrix_to_axis_angle(body_rotmat).reshape(T, 63) |
|
|
| return { |
| "root_orient": root_orient, |
| "body_pose": body_pose, |
| "transl": transl, |
| } |
|
|