sjtu_smartcar@lgds:~/Desktop/xmh/robotics/human2hu...
تم الإنشاء في: ٨ أبريل ٢٠٢٥
تمت الإجابة باستخدام O3 Mini High بواسطة Chat01
تم الإنشاء في: ٨ أبريل ٢٠٢٥
تمت الإجابة باستخدام O3 Mini High بواسطة Chat01
sjtu_smartcar@lgds:~/Desktop/xmh/robotics/human2humanoid$ cd /home/sjtu_smartcar/Desktop/xmh/robotics/human2humanoid ; /usr/bin/env /home/sjtu_smartcar/anaconda3/envs/omnih2o/bin/python /home/sjtu_smartcar/.vscode-server/extensions/ms-python.debugpy-2025.6.0-linux-x64/bundled/libs/debugpy/adapter/../../debugpy/launcher 53919 -- scripts/vis/vis_motion_ym.py
Importing module 'gym_38' (/home/sjtu_smartcar/Desktop/xmh/robotics/isaacgym/python/isaacgym/_bindings/linux-x86_64/gym_38.so)
Setting GYM_USD_PLUG_INFO_PATH to /home/sjtu_smartcar/Desktop/xmh/robotics/isaacgym/python/isaacgym/_bindings/linux-x86_64/usd/plugInfo.json
PyTorch version 2.4.1+cu121
Device count 1
/home/sjtu_smartcar/Desktop/xmh/robotics/isaacgym/python/isaacgym/_bindings/src/gymtorch
Using /home/sjtu_smartcar/.cache/torch_extensions/py38_cu121 as PyTorch extensions root...
Emitting ninja build file /home/sjtu_smartcar/.cache/torch_extensions/py38_cu121/gymtorch/build.ninja...
Building extension module gymtorch...
Allowing ninja to set a default number of workers... (overridable by setting the environment variable MAX_JOBS=N)
ninja: no work to do.
Loading extension module gymtorch...
2025-04-09 00:45:57,999 - INFO - logger - logger initialized
MOVING MOTION DATA TO GPU, USING CACHE: False
MOVING MOTION DATA TO GPU, USING CACHE: False
loading data/ym/stable_punch_v2.pkl
Not connected to PVD
+++ Using GPU PhysX
Physics Engine: PhysX
Physics Device: cuda:0
GPU Pipeline: enabled
[Error] [carb.windowing-glfw.plugin] GLFW initialization failed.
[Error] [carb.windowing-glfw.plugin] GLFW window creation failed!
[Error] [carb.gym.plugin] Failed to create Window in CreateGymViewerInternal
*** Failed to create viewer 应该怎么export当前终端的display=0
在 Linux 环境下,窗口显示依赖于 X server,通过环境变量 DISPLAY 来指定图形输出的目标屏幕。你可以在终端中执行以下命令来设置 DISPLAY 为 :0:
bashexport DISPLAY=:0
这条命令会把当前终端的 DISPLAY 环境变量设置为 :0,通常对应本地桌面的默认显示屏。如果你是在本机上运行且已启动 X server,这样应该可以解决由于 DISPLAY 未正确设置而导致的 GLFW 窗口创建失败问题。
检查 X Server 是否正在运行
确保本机已经运行了 X server。你可以使用 echo $DISPLAY 命令来确认当前 DISPLAY 值是否生效,以及使用例如 ps -e | grep X 来确认 X server 进程是否存在。
远程连接情况
如果你是在远程机器上操作并希望显示图形界面,请确保使用了 X11 forwarding,即通过 ssh -X 或 ssh -Y 参数连接。如果没有开启 X forwarding,仅设置 export DISPLAY=:0 通常无法成功,因为远程机器本身可能没有物理显示器或 X server。
无头 (headless) 环境
如果你是在没有物理显示器的无头服务器上运行,但仍需要创建窗口显示,可以考虑使用虚拟帧缓存工具,例如 Xvfb。安装好后,你可以通过如下命令启动:
bashXvfb :0 -screen 0 1024x768x24 & export DISPLAY=:0
这会在后台启动一个虚拟显示环境,从而允许需要 GUI 的程序正常运行。
通过上述方法设置 DISPLAY 后,再次运行你的程序应该就能成功创建 GLFW 窗口了。
def get_motion_state(self, motion_ids, motion_times, offset=None):
n = len(motion_ids)
num_bodies = self._get_num_bodies()
textmotion_len = self._motion_lengths[motion_ids] num_frames = self._motion_num_frames[motion_ids] dt = self._motion_dt[motion_ids] frame_idx0, frame_idx1, blend = self._calc_frame_blend(motion_times, motion_len, num_frames, dt) # print("non_interval", frame_idx0, frame_idx1) f0l = frame_idx0 + self.length_starts[motion_ids] f1l = frame_idx1 + self.length_starts[motion_ids] if "dof_pos" in self.__dict__: local_rot0 = self.dof_pos[f0l] local_rot1 = self.dof_pos[f1l] #print(local_rot0) else: local_rot0 = self.lrs[f0l] local_rot1 = self.lrs[f1l] body_vel0 = self.gvs[f0l] body_vel1 = self.gvs[f1l] body_ang_vel0 = self.gavs[f0l] body_ang_vel1 = self.gavs[f1l] rg_pos0 = self.gts[f0l, :] rg_pos1 = self.gts[f1l, :] dof_vel0 = self.dvs[f0l] dof_vel1 = self.dvs[f1l] vals = [local_rot0, local_rot1, body_vel0, body_vel1, body_ang_vel0, body_ang_vel1, rg_pos0, rg_pos1, dof_vel0, dof_vel1] for v in vals: assert v.dtype != torch.float64 blend = blend.unsqueeze(-1) blend_exp = blend.unsqueeze(-1) if offset is None: rg_pos = (1.0 - blend_exp) * rg_pos0 + blend_exp * rg_pos1 # ZL: apply offset else: rg_pos = (1.0 - blend_exp) * rg_pos0 + blend_exp * rg_pos1 + offset[..., None, :] # ZL: apply offset body_vel = (1.0 - blend_exp) * body_vel0 + blend_exp * body_vel1 body_ang_vel = (1.0 - blend_exp) * body_ang_vel0 + blend_exp * body_ang_vel1 if "dof_pos" in self.__dict__: # H1 joints dof_vel = (1.0 - blend) * dof_vel0 + blend * dof_vel1 dof_pos = (1.0 - blend) * local_rot0 + blend * local_rot1 else: dof_vel = (1.0 - blend_exp) * dof_vel0 + blend_exp * dof_vel1 local_rot = torch_utils.slerp(local_rot0, local_rot1, torch.unsqueeze(blend, axis=-1)) dof_pos = self._local_rotation_to_dof_smpl(local_rot) #print(dof_pos) rb_rot0 = self.grs[f0l] rb_rot1 = self.grs[f1l] rb_rot = torch_utils.slerp(rb_rot0, rb_rot1, blend_exp) return_dict = {} if "gts_t" in self.__dict__: rg_pos_t0 = self.gts_t[f0l] rg_pos_t1 = self.gts_t[f1l] rg_rot_t0 = self.grs_t[f0l] rg_rot_t1 = self.grs_t[f1l] body_vel_t0 = self.gvs_t[f0l] body_vel_t1 = self.gvs_t[f1l] body_ang_vel_t0 = self.gavs_t[f0l] body_ang_vel_t1 = self.gavs_t[f1l] if offset is None: rg_pos_t = (1.0 - blend_exp) * rg_pos_t0 + blend_exp * rg_pos_t1 else: rg_pos_t = (1.0 - blend_exp) * rg_pos_t0 + blend_exp * rg_pos_t1 + offset[..., None, :] rg_rot_t = torch_utils.slerp(rg_rot_t0, rg_rot_t1, blend_exp) body_vel_t = (1.0 - blend_exp) * body_vel_t0 + blend_exp * body_vel_t1 body_ang_vel_t = (1.0 - blend_exp) * body_ang_vel_t0 + blend_exp * body_ang_vel_t1 return_dict['rg_pos_t'] = rg_pos_t return_dict['rg_rot_t'] = rg_rot_t return_dict['body_vel_t'] = body_vel_t return_dict['body_ang_vel_t'] = body_ang_vel_t if flags.real_traj: q_body_ang_vel0, q_body_ang_vel1 = self.q_gavs[f0l], self.q_gavs[f1l] q_rb_rot0, q_rb_rot1 = self.q_grs[f0l], self.q_grs[f1l] q_rg_pos0, q_rg_pos1 = self.q_gts[f0l, :], self.q_gts[f1l, :] q_body_vel0, q_body_vel1 = self.q_gvs[f0l], self.q_gvs[f1l] q_ang_vel = (1.0 - blend_exp) * q_body_ang_vel0 + blend_exp * q_body_ang_vel1 q_rb_rot = torch_utils.slerp(q_rb_rot0, q_rb_rot1, blend_exp) q_rg_pos = (1.0 - blend_exp) * q_rg_pos0 + blend_exp * q_rg_pos1 q_body_vel = (1.0 - blend_exp) * q_body_vel0 + blend_exp * q_body_vel1 rg_pos[:, self.track_idx] = q_rg_pos rb_rot[:, self.track_idx] = q_rb_rot body_vel[:, self.track_idx] = q_body_vel body_ang_vel[:, self.track_idx] = q_ang_vel return_dict.update({ "root_pos": rg_pos[..., 0, :].clone(), "root_rot": rb_rot[..., 0, :].clone(), "dof_pos": dof_pos.clone(), "root_vel": body_vel[..., 0, :].clone(), "root_ang_vel": body_ang_vel[..., 0, :].clone(), "dof_vel": dof_vel.view(dof_vel.shape[0], -1), "motion_aa": self._motion_aa[f0l], "rg_pos": rg_pos, "rb_rot": rb_rot, "body_vel": body_vel, "body_ang_vel": body_ang_vel, "motion_bodies": self._motion_bodies[motion_ids], "motion_limb_weights": self._motion_limb_weights[motion_ids], }) return return_dict 非常详细解释
下面提供对该函数 get_motion_state 的逐行、详细解释,帮助你理解代码中的每个步骤和背后的逻辑。
函数目的
该函数用于计算给定时刻下“运动状态”的中间插值结果,主要针对人体动作或机器人运动数据。函数对指定的多个动作(通过 motion_ids 标识)在特定时刻(motion_times)下的状态进行求解,返回一个包含位置、旋转、速度、角速度等多个部分信息的字典。函数内部使用了线性插值(Lerp)以及球面线性插值(Slerp)来平滑地过渡状态。
输入参数
motion_ids:用于选择哪些动作的索引,可能是列表或张量。motion_times:表示动作序列中时间位置的张量,用以计算两个关键帧之间的插值比例。offset(可选):对全局位置做额外偏移,一般在需要修正根节点位置时使用。pythonn = len(motion_ids) num_bodies = self._get_num_bodies()
n),同时调用内部方法 _get_num_bodies() 来获取该运动数据中涉及的身体个数(例如:人体的各个关节、身体部位)。pythonmotion_len = self._motion_lengths[motion_ids] num_frames = self._motion_num_frames[motion_ids] dt = self._motion_dt[motion_ids]
motion_len:每个动作的总时长或长度。num_frames:每个动作的总帧数。dt:动作数据的时间间隔,即每一帧的时间步长。pythonframe_idx0, frame_idx1, blend = self._calc_frame_blend(motion_times, motion_len, num_frames, dt)
_calc_frame_blend 方法计算当前时间在对应动作内属于哪两个帧之间,并计算插值因子 blend(通常在 0 到 1 之间)。
frame_idx0:当前时间所在区间的起始帧索引。frame_idx1:相邻的下一帧索引。blend:比例因子,用于两帧数据之间的过渡(例如:当 blend 为 0.2 时,则 80% 取自 frame_idx0,20% 取自 frame_idx1)。pythonf0l = frame_idx0 + self.length_starts[motion_ids] f1l = frame_idx1 + self.length_starts[motion_ids]
self.length_starts[motion_ids]),这通常用于当多个动作数据被存储在同一个大数组中时,用以确定各个动作数据的起始位置。接下来,从存储所有数据的大张量中提取起始帧 (f0l) 和终止帧 (f1l) 对应的各类物理状态数据:
pythonif "dof_pos" in self.__dict__: local_rot0 = self.dof_pos[f0l] local_rot1 = self.dof_pos[f1l] else: local_rot0 = self.lrs[f0l] local_rot1 = self.lrs[f1l]
dof_pos 属性。
lrs(可能代表 local rotations,即局部旋转)。pythonbody_vel0 = self.gvs[f0l] body_vel1 = self.gvs[f1l] body_ang_vel0 = self.gavs[f0l] body_ang_vel1 = self.gavs[f1l]
gvs 一般指的是全局速度(Global Velocities),gavs 指的是全局角速度(Global Angular Velocities),pythonrg_pos0 = self.gts[f0l, :] rg_pos1 = self.gts[f1l, :]
gts 通常存储全局位置信息(Global Translations);通过两个帧的索引获取位置信息。pythondof_vel0 = self.dvs[f0l] dof_vel1 = self.dvs[f1l]
dvs(可能代表 degrees-of-freedom velocities)中获取关节速度数据。pythonvals = [local_rot0, local_rot1, body_vel0, body_vel1, body_ang_vel0, body_ang_vel1, rg_pos0, rg_pos1, dof_vel0, dof_vel1] for v in vals: assert v.dtype != torch.float64
pythonblend = blend.unsqueeze(-1) blend_exp = blend.unsqueeze(-1)
blend 进行 unsqueeze 操作,即在最后一个维度上增加维度,目的是为了保证在后续计算中能够广播(broadcast)求值。在某些插值计算中需要额外维度来匹配数据的形状。blend(稍后用于部分线性插值),另一个 blend_exp 用在需要额外维度扩展的情况(例如在插值计算中涉及矩阵或多维张量)。pythonif offset is None: rg_pos = (1.0 - blend_exp) * rg_pos0 + blend_exp * rg_pos1 # ZL: apply offset else: rg_pos = (1.0 - blend_exp) * rg_pos0 + blend_exp * rg_pos1 + offset[..., None, :] # ZL: apply offset
offset 参数,则在插值结果上加上该偏移。注释中 “ZL” 可能是开发者的标记或说明。pythonbody_vel = (1.0 - blend_exp) * body_vel0 + blend_exp * body_vel1 body_ang_vel = (1.0 - blend_exp) * body_ang_vel0 + blend_exp * body_ang_vel1
根据对象内部是否存在 dof_pos 属性,采用不同的计算方式:
情况一:存在 dof_pos(H1 joints)
pythonif "dof_pos" in self.__dict__: dof_vel = (1.0 - blend) * dof_vel0 + blend * dof_vel1 dof_pos = (1.0 - blend) * local_rot0 + blend * local_rot1
dof_vel 和关节位置 dof_pos。blend(而非 blend_exp),这可能是因为数据的形状本身已符合要求。情况二:不存在 dof_pos
pythonelse: dof_vel = (1.0 - blend_exp) * dof_vel0 + blend_exp * dof_vel1 local_rot = torch_utils.slerp(local_rot0, local_rot1, torch.unsqueeze(blend, axis=-1)) dof_pos = self._local_rotation_to_dof_smpl(local_rot)
local_rot0 和 local_rot1 计算旋转插值。这种方法特别适用于处理旋转(如四元数插值),避免了线性插值可能带来的非线性问题。_local_rotation_to_dof_smpl 将计算得到的局部旋转转换成关节位置(或角度)形式。pythonrb_rot0 = self.grs[f0l] rb_rot1 = self.grs[f1l] rb_rot = torch_utils.slerp(rb_rot0, rb_rot1, blend_exp)
grs(global rotations 的缩写),对两帧进行球面插值,计算出过渡状态下的根身体旋转。pythonif "gts_t" in self.__dict__: rg_pos_t0 = self.gts_t[f0l] rg_pos_t1 = self.gts_t[f1l] rg_rot_t0 = self.grs_t[f0l] rg_rot_t1 = self.grs_t[f1l] body_vel_t0 = self.gvs_t[f0l] body_vel_t1 = self.gvs_t[f1l] body_ang_vel_t0 = self.gavs_t[f0l] body_ang_vel_t1 = self.gavs_t[f1l] if offset is None: rg_pos_t = (1.0 - blend_exp) * rg_pos_t0 + blend_exp * rg_pos_t1 else: rg_pos_t = (1.0 - blend_exp) * rg_pos_t0 + blend_exp * rg_pos_t1 + offset[..., None, :] rg_rot_t = torch_utils.slerp(rg_rot_t0, rg_rot_t1, blend_exp) body_vel_t = (1.0 - blend_exp) * body_vel_t0 + blend_exp * body_vel_t1 body_ang_vel_t = (1.0 - blend_exp) * body_ang_vel_t0 + blend_exp * body_ang_vel_t1 return_dict['rg_pos_t'] = rg_pos_t return_dict['rg_rot_t'] = rg_rot_t return_dict['body_vel_t'] = body_vel_t return_dict['body_ang_vel_t'] = body_ang_vel_t
gts_t 属性(通常带有后缀 _t 表示目标(target)或转换之后的状态),那么对这些相关的状态数据(位置、旋转、速度、角速度)进行类似的插值计算。flags.real_traj 条件处理真实轨迹数据pythonif flags.real_traj: q_body_ang_vel0, q_body_ang_vel1 = self.q_gavs[f0l], self.q_gavs[f1l] q_rb_rot0, q_rb_rot1 = self.q_grs[f0l], self.q_grs[f1l] q_rg_pos0, q_rg_pos1 = self.q_gts[f0l, :], self.q_gts[f1l, :] q_body_vel0, q_body_vel1 = self.q_gvs[f0l], self.q_gvs[f1l] q_ang_vel = (1.0 - blend_exp) * q_body_ang_vel0 + blend_exp * q_body_ang_vel1 q_rb_rot = torch_utils.slerp(q_rb_rot0, q_rb_rot1, blend_exp) q_rg_pos = (1.0 - blend_exp) * q_rg_pos0 + blend_exp * q_rg_pos1 q_body_vel = (1.0 - blend_exp) * q_body_vel0 + blend_exp * q_body_vel1 rg_pos[:, self.track_idx] = q_rg_pos rb_rot[:, self.track_idx] = q_rb_rot body_vel[:, self.track_idx] = q_body_vel body_ang_vel[:, self.track_idx] = q_ang_vel
flags.real_traj 为真时,说明需要使用“真实轨迹”(real trajectory)的数据。q_ 前缀数据(例如 q_gavs, q_grs 等)可能是对原始数据经过某种修正或额外计算后得到的版本。rg_pos、rb_rot、body_vel、body_ang_vel 的特定索引(self.track_idx),这通常是为了确保轨迹数据在某个关键点上采用更精确的数据。pythonreturn_dict.update({ "root_pos": rg_pos[..., 0, :].clone(), "root_rot": rb_rot[..., 0, :].clone(), "dof_pos": dof_pos.clone(), "root_vel": body_vel[..., 0, :].clone(), "root_ang_vel": body_ang_vel[..., 0, :].clone(), "dof_vel": dof_vel.view(dof_vel.shape[0], -1), "motion_aa": self._motion_aa[f0l], "rg_pos": rg_pos, "rb_rot": rb_rot, "body_vel": body_vel, "body_ang_vel": body_ang_vel, "motion_bodies": self._motion_bodies[motion_ids], "motion_limb_weights": self._motion_limb_weights[motion_ids], })
return_dict。root_pos、root_rot、root_vel、root_ang_vel 分别取自对应张量的第一个“身体”部分(通常代表根节点)。dof_pos 和 dof_vel 分别为关节的插值位置和速度。motion_aa、motion_bodies、motion_limb_weights 可能保存一些描述运动信息的辅助数据。.clone() 以确保返回的是数据的副本,避免后续被原始数据修改。pythonreturn return_dict
该函数的核心工作可归纳为以下几点:
_calc_frame_blend 得到两个相邻帧的索引及线性插值因子。dof_pos 属性,选择直接使用或通过局部旋转转换得到关节角度。_t 后缀的属性)或需要真实轨迹数据(flags.real_traj 为真),则分别计算并更新相关状态。通过这些步骤,该函数能够在给定时间点上计算出完整的、多部分的运动状态,这对于动作重建、动画插值或运动控制系统中的实时计算具有重要意义。
(self, motion_ids, motion_times 这个是什么
在你提供的函数 get_motion_state(self, motion_ids, motion_times, offset=None) 中,motion_ids 和 motion_times 是该函数的两个输入参数。下面是对这两个参数的详细解释:
motion_ids 用于指定当前要处理的多个运动数据(例如动作轨迹、人体动作、机器人动作等)的标识符。例如,如果有多个动作或轨迹数据存储在一个数组或数据集中,通过 motion_ids 可以选择这些动作的特定子集。这个列表的每个元素都是一个整数,指向某一特定动作的索引。
motion_times 是一个表示时间点的数组(或张量)。每个时间点对应一个时间值,用于指示在给定时间下的运动数据状态。motion_times[i] 表示第 motion_ids[i] 对应的动作在时间 motion_times[i] 下的状态)。motion_times 通常与 motion_ids 配合使用,通过时间插值计算每个动作在不同时间点的状态。这些时间点将用于确定该动作数据所在的帧,以及计算从一个帧到下一个帧的过渡(即插值)。
假设你有多个动作的数据集,每个动作有不同的帧数和时间间隔,并且你希望获取其中某些动作在特定时刻的状态:
pythonmotion_ids = [0, 2, 4] # 动作数据的ID(例如:第0、2、4个动作) motion_times = [1.5, 2.0, 3.0] # 对应动作在这些时刻的状态(例如:时间1.5s、2.0s和3.0s时的状态)
在这个例子中,motion_ids 表示你要选择的动作数据,motion_times 表示你要在这些特定时间点(如1.5秒、2秒、3秒)获取对应动作的状态。
这些信息将用于计算每个动作在对应时间点下的各种运动参数,例如位置、速度、角度等。
motion_len = motion_lib.get_motion_length(motion_id).item()
motion_time = time_step % motion_len 详细解释这些
下面对这两行代码做详细解释:
pythonmotion_len = motion_lib.get_motion_length(motion_id).item()
调用函数获取长度:
motion_lib.get_motion_length(motion_id) 这一部分调用了模块 motion_lib 中的函数 get_motion_length,它接收一个参数 motion_id。该函数的作用是返回与这个 motion_id 对应的动作长度(或运动长度),这个长度可能表示运动持续时间或帧数。
数据类型转换:
返回的结果通常可能是一个包含单一数值的张量(例如在使用 PyTorch 时)。调用 .item() 方法用于从这个张量中提取出一个 Python 原生数值。这样后续使用时可以直接作为数字进行数学运算,而不必担心张量的数据类型问题。
最终结果:
最终变量 motion_len 存储的是一个 Python 数字,代表所对应动作的长度(例如:持续时间或者总帧数)。
pythonmotion_time = time_step % motion_len
变量含义:
time_step:通常代表当前的时间步或时间值,可能是程序运行时的时间戳或者当前帧所对应的时间。motion_len:前一步骤获得的动作总长度。取模操作(%):
这里使用 % 运算符,即取模运算。它的作用是计算 time_step 除以 motion_len 后的余数。
time_step 小于 motion_len,那么 motion_time 直接等于 time_step;time_step 大于或等于 motion_len,那么 motion_time 会是 time_step 在长度 motion_len 内的“余数”。这种操作通常用于处理循环或周期性的运动序列,从而让时间在超过运动总长度后自动“回绕”,形成循环播放的效果。举例说明:
比如,如果 motion_len 为 10(可能代表10秒或者10帧),而 time_step 为 23,那么:
pythonmotion_time = 23 % 10 # 结果为 3
这意味着在周期为 10 的循环运动中,23 秒对应的状态实际上是第 3 秒的状态。
动作长度获取:
第一行通过调用 motion_lib.get_motion_length(motion_id) 函数,并用 .item() 转换结果得到一个 Python 数值 motion_len。这个值表示一个动作的总时长或帧数。
周期性时间处理:
第二行利用取模运算符 % 计算 time_step 除以 motion_len 后的余数,得到的 motion_time 表示当前时间在动作循环周期内的位置。这在处理循环播放或周期性运动时非常常见和有用。
这种设计允许你即使在 time_step 超过动作总长度的情况下,也能始终定位到动作周期内的正确时刻,从而实现平滑且连续的动画或运动效果。
多个motionid在motiontime里面构成一个连续的动作吗
这要看你的输入和系统如何使用这些参数。一般来说,motion_ids 和 motion_times 只是用来在各个独立的动作片段(motion clip)里选取当前时刻的状态,并不是说多个 motion_id 会自动拼接成一个长连续的动作。
具体来说:
因此,如果你传入多个 motion_id 和对应的 motion_time,这通常意味着你在并行获取多个独立动作片段在特定时刻下的状态,而不是系统自动将它们拼接成一个连续的动作。如果你希望得到连续的动作序列,通常需要在外部逻辑中安排这些动作之间的过渡或插值,明确指定各个剪辑之间如何连接、如何平滑过渡等。
简单回答:
多个 motion_id 和 motion_time 通常表示多个独立动作片段在各自时间位置下的状态,它们不会自动构成一个连续的动作,除非你在系统层面上专门实现了动作拼接或过渡。
def load_motion_with_skeleton(ids, motion_data_list, skeleton_trees, gender_betas, fix_height, mesh_parsers, masterfoot_config, target_heading, max_len, queue, pid):
# ZL: loading motion with the specified skeleton. Perfoming forward kinematics to get the joint positions
np.random.seed(np.random.randint(5000)* pid)
res = {}
assert (len(ids) == len(motion_data_list))
for f in range(len(motion_data_list)):
curr_id = ids[f] # id for this datasample
curr_file = motion_data_list[f]
if not isinstance(curr_file, dict) and osp.isfile(curr_file):
key = motion_data_list[f].split("/")[-1].split(".")[0]
curr_file = joblib.load(curr_file)[key]
textseq_len = curr_file['root_trans_offset'].shape[0] if max_len == -1 or seq_len < max_len: start, end = 0, seq_len else: start = random.randint(0, seq_len - max_len) end = start + max_len trans = to_torch(curr_file['root_trans_offset']).clone()[start:end] pose_aa = to_torch(curr_file['pose_aa'][start:end]).clone() dt = 1/curr_file['fps'] B, J, N = pose_aa.shape ##### ZL: randomize the heading ###### # if (not flags.im_eval) and (not flags.test): # # if True: # random_rot = np.zeros(3) # random_rot[2] = np.pi * (2 * np.random.random() - 1.0) # random_heading_rot = sRot.from_euler("xyz", random_rot) # pose_aa = pose_aa.reshape(B, -1) # pose_aa[:, :3] = torch.tensor((random_heading_rot * sRot.from_rotvec(pose_aa[:, :3])).as_rotvec()) # trans = torch.matmul(trans, torch.from_numpy(random_heading_rot.as_matrix().T)) ##### ZL: randomize the heading ###### if not target_heading is None: start_root_rot = sRot.from_rotvec(pose_aa[0, 0]) heading_inv_rot = sRot.from_quat(torch_utils.calc_heading_quat_inv(torch.from_numpy(start_root_rot.as_quat()[None, ]))) heading_delta = sRot.from_quat(target_heading) * heading_inv_rot pose_aa[:, 0] = torch.tensor((heading_delta * sRot.from_rotvec(pose_aa[:, 0])).as_rotvec()) trans = torch.matmul(trans, torch.from_numpy(heading_delta.as_matrix().squeeze().T)) # trans, trans_fix = MotionLibSMPL.fix_trans_height(pose_aa, trans, curr_gender_beta, mesh_parsers, fix_height_mode = fix_height) curr_motion = mesh_parsers.fk_batch(pose_aa[None, ], trans[None, ], return_full= True, dt = dt) curr_motion = EasyDict({k: v.squeeze() if torch.is_tensor(v) else v for k, v in curr_motion.items() }) if curr_id==0: print(pose_aa[None, ][0][0]) print(curr_motion['dof_pos'][0]) res[curr_id] = (curr_file, curr_motion) if not queue is None: queue.put(res) else: return res 解释这一段以及mesh_parsers.fk_batch的作用
下面对该代码段做详细解释,并重点说明其中调用的
pythonmesh_parsers.fk_batch
函数的作用。
函数 load_motion_with_skeleton 主要用于加载一批“运动数据”(motion data)并根据指定的骨骼结构(skeleton)计算正向运动学(Forward Kinematics, FK),从而获得关节位置和其他相关运动状态信息。
pythonnp.random.seed(np.random.randint(5000) * pid)
pythonres = {} assert (len(ids) == len(motion_data_list))
res 用于保存处理结果。pythonfor f in range(len(motion_data_list)): curr_id = ids[f] # 当前数据样本的ID curr_file = motion_data_list[f]
curr_id 记录当前样本对应的标识符;curr_file 则可以是一个字典或者文件路径。pythonif not isinstance(curr_file, dict) and osp.isfile(curr_file): key = motion_data_list[f].split("/")[-1].split(".")[0] curr_file = joblib.load(curr_file)[key]
curr_file 不是字典且为一个存在的文件路径,则根据文件名(去掉路径和扩展名)生成 key,通过 joblib.load 加载数据,并取出对应 key 的数据。pythonseq_len = curr_file['root_trans_offset'].shape[0] if max_len == -1 or seq_len < max_len: start, end = 0, seq_len else: start = random.randint(0, seq_len - max_len) end = start + max_len
curr_file['root_trans_offset'](根节点相对于平移量)的第一维度获取。max_len 等于 -1 或者当前序列长度小于 max_len,则整个序列都被使用;否则,从序列中随机选取一个长度为 max_len 的子序列。pythontrans = to_torch(curr_file['root_trans_offset']).clone()[start:end] pose_aa = to_torch(curr_file['pose_aa'][start:end]).clone() dt = 1 / curr_file['fps']
dt 为每秒的帧数的倒数,即每一帧对应的时间间隔。pythonB, J, N = pose_aa.shape
pose_aa 的形状为 (B, J, N):
代码中有一段注释掉的随机化 heading 的代码,目前启用了下面的代码块对目标 heading 进行对齐:
pythonif not target_heading is None: start_root_rot = sRot.from_rotvec(pose_aa[0, 0]) heading_inv_rot = sRot.from_quat(torch_utils.calc_heading_quat_inv(torch.from_numpy(start_root_rot.as_quat()[None, ]))) heading_delta = sRot.from_quat(target_heading) * heading_inv_rot pose_aa[:, 0] = torch.tensor((heading_delta * sRot.from_rotvec(pose_aa[:, 0])).as_rotvec()) trans = torch.matmul(trans, torch.from_numpy(heading_delta.as_matrix().squeeze().T))
torch_utils.calc_heading_quat_inv 计算根节点朝向的逆旋转,用以与目标朝向对齐。target_heading(通常是四元数形式)计算 heading_delta,即从当前朝向转换到目标朝向所需要的旋转调整量。pose_aa[:, 0])进行更新,使其沿目标方向旋转。trans 按照同样的旋转调整,确保整体运动和旋转一致。pythoncurr_motion = mesh_parsers.fk_batch(pose_aa[None, ], trans[None, ], return_full=True, dt=dt)
mesh_parsers.fk_batch 的作用:
该函数用于批量计算正向运动学(Forward Kinematics, FK)。
输入参数:
pose_aa[None, ]:在最前面添加一个 batch 维度,使得输入形状适用于 batch 处理。这里的 pose_aa 包含所有关节的姿态(轴角表示)。trans[None, ]:同样对平移数据添加 batch 维度。通常表示根节点或全局的平移。return_full=True 表示要求返回完整的信息(可能包含关节位置、旋转、骨骼层级关系信息以及其他辅助数据),而不仅仅是部分数据。dt 是时间步长,可能在某些计算中用于动态插值或计算速度等。返回内容:
dof_pos 表示自由度的位置信息)以及其他关键信息。这一步正是依据给定的骨骼结构(通常在 mesh_parsers 内部已经配置好,例如与 skeleton_trees 或 mesh 模型相关)计算出最终骨骼在全局空间中的姿态。后续处理:
pythoncurr_motion = EasyDict({k: v.squeeze() if torch.is_tensor(v) else v for k, v in curr_motion.items()})
将返回的字典中所有的 Torch 张量使用 squeeze() 去除多余的维度,然后封装成一个 EasyDict 对象(这样可以用点操作符访问字典中的键),方便后续调用和调试。
pythonif curr_id == 0: print(pose_aa[None, ][0][0]) print(curr_motion['dof_pos'][0])
pythonres[curr_id] = (curr_file, curr_motion)
pythonif not queue is None: queue.put(res) else: return res
mesh_parsers 是一个传入的对象,里面包含与网格(mesh)和骨骼相关的处理工具。它的 fk_batch 方法实现了如下功能:
正向运动学计算(Forward Kinematics):
给定关节的局部旋转(在这里使用轴角表示,即 pose_aa)和根节点(或整体)的平移(trans),该方法根据事先定义好的骨骼结构或骨骼树(skeleton_trees)计算出每个关节的全局位置和旋转信息。
批量处理:
通过在输入上添加 batch 维度(用 [None, ] 进行包装),该函数可以一次性对多帧或多个运动序列进行处理,提升计算效率。
返回完整信息:
参数 return_full=True 表示返回的信息不仅仅包括最基础的关节位置(如 dof_pos),还可能包含其他运动参数或额外的中间结果,例如计算出的旋转矩阵、骨架节点层次结构等信息。这些数据对后续的动画、物理仿真或可视化都是十分有用的。
总结来说,mesh_parsers.fk_batch 的核心作用是利用骨骼模型对输入的关节旋转和平移数据进行正向运动学计算,从而得到骨骼系统的全局位姿数据,支持批量计算并返回全面的运动状态信息。
整体流程:
mesh_parsers.fk_batch 对选定的运动数据执行正向运动学计算,获得关节在全局空间中的位置及其他详细运动信息。mesh_parsers.fk_batch:
该方法利用输入的关节姿态(轴角表示)和平移数据,并结合预设的骨骼结构,计算出每个关节在全局空间内的最终位置和旋转,其批量处理的设计能够有效地处理多帧数据,是该函数实现正向运动学计算的核心组件。
这种设计常用于运动捕捉、动画生成、机器人运动控制等领域,将抽象的姿态数据转化成具有物理或几何意义的运动状态,从而为后续的可视化、碰撞检测或动力学仿真提供数据支持。
tensor([[ 2.0340e+01, 3.4694e+00, -1.7540e-02],
[ 2.0330e+01, 3.4663e+00, 1.0398e-01],
[ 2.0219e+01, 3.4660e+00, 1.1464e-01],
[ 2.0289e+01, 3.5751e+00, 4.6494e-01],
[ 2.0289e+01, 3.6878e+00, 4.6549e-01],
[ 2.0289e+01, 3.7806e+00, 4.7263e-01],
[ 2.0289e+01, 3.9498e+00, 4.6789e-01],
[ 2.0287e+01, 4.0600e+00, 4.7312e-01],
[ 2.0288e+01, 4.1288e+00, 4.7317e-01],
[ 2.0284e+01, 4.1885e+00, 4.7099e-01],
[ 2.0310e+01, 3.4638e+00, 5.7014e-01],
[ 2.0312e+01, 3.4687e+00, 6.4089e-01],
[ 2.0290e+01, 3.3495e+00, 4.6385e-01],
[ 2.0290e+01, 3.2408e+00, 4.6332e-01],
[ 2.0290e+01, 3.1480e+00, 4.6950e-01],
[ 2.0290e+01, 2.9788e+00, 4.6300e-01],
[ 2.0287e+01, 2.8686e+00, 4.6710e-01],
[ 2.0287e+01, 2.7998e+00, 4.6648e-01],
[ 2.0284e+01, 2.7401e+00, 4.6531e-01],
[ 2.0212e+01, 3.5796e+00, -1.1312e-02],
[ 2.0340e+01, 3.5701e+00, -1.4708e-02],
[ 2.0290e+01, 3.5795e+00, -1.1696e-01],
[ 2.0290e+01, 3.5798e+00, -4.4871e-01],
[ 2.0294e+01, 3.5801e+00, -8.6069e-01],
[ 2.0294e+01, 3.5801e+00, -8.6069e-01],
[ 2.0211e+01, 3.3596e+00, -1.6993e-02],
[ 2.0340e+01, 3.3687e+00, -2.0443e-02],
[ 2.0290e+01, 3.3588e+00, -1.2300e-01],
[ 2.0293e+01, 3.3566e+00, -4.5473e-01],
[ 2.0295e+01, 3.3537e+00, -8.6672e-01],
[ 2.0295e+01, 3.3537e+00, -8.6672e-01]], device='cuda:0') 这些值分别的机器人31个关节的xyz 详细分析哪个值是哪个关节的
这段 tensor 显示的是一个形状为 [31, 3] 的张量,意味着它包含了 31 个关节在全局空间中的 3D 坐标(x、y、z)。每一行对应一个关节的位置。不过,具体每一行代表哪个关节取决于你的机器人(或人体模型)的骨骼结构定义,也就是“skeleton”中规定的关节顺序。
关节顺序由骨骼树(Skeleton Tree)确定:
在大部分运动学或动画系统中,骨骼结构都会按照固定的顺序定义各个关节。例如,一个骨骼树可能从根关节(通常称为 pelvis 或 root)开始,依次排列身体各部分(如左右髋、脊柱、肩部、肘部、腕部,以及下肢和上肢的细分关节等)。
为什么无法直接断定每行对应哪个关节?
skeleton_trees 或类似名称)定义了关节名称与其索引之间的映射。只有知道这种映射关系,才能知道 tensor 的第 0 行、第 1 行等分别对应哪个部位。假设你的骨骼定义文件中指定 31 个关节,顺序可能类似如下(注意:下面仅为一种可能的定义,具体请查阅你项目中的配置):
注意: 以上只是一个示例。实际的映射关系必须参考你项目内定义的骨骼树配置。你可以在工程代码中查找类似
skeleton_trees、joint_names、joint_hierarchy或相关文档来确认每个索引对应的具体关节名称。
pythonjoint_names = [ "Pelvis", "Left_Hip", "Right_Hip", "Lower_Spine", "Middle_Spine", "Upper_Spine", "Neck", "Head", "Left_Shoulder", "Left_Elbow", "Left_Wrist", "Right_Shoulder", "Right_Elbow", "Right_Wrist", "Left_Knee", "Left_Ankle", "Left_Foot", "Right_Knee", "Right_Ankle", "Right_Foot", ... # 直到 31 个关节 ]
只有了解了这些细节,才能在使用或调试过程中正确解读每个数值代表的关节位置。