接着上一篇博客的工作https://blog.csdn.net/2302_77159195/article/details/161718260?spm=1001.2014.3001.5502

1.使用mjlab里面的debugline,画一个棍棒,查看在mjlab的空间下,我做的动作被看成了什么东西

要进行的工作:

(1)提取相对坐标:使用pred_xyz_24_struct,将每一帧的24个点都减去第0个点骨盆的坐标

(2)修正轴向差异 :Hybrik输出的坐标是相机坐标系下的,而mjlab/mujocjo里面是世界坐标系

(3)定义关节点的连接关系

verify_upper_body.py:

问题:线段连接不正确

查看SMPL官方关节点定义

                       15 Head
                              |
                      12 Neck
                              |
    14 L_Collar — 9 Spine3 — 13 R_Collar
           /                  |               \
     17 L_Shoulder   |             16 R_Shoulder
         |                    |                |
     19 L_Elbow           6 Spine2         18 R_Elbow
         |                    |                |
     21 L_Wrist           3 Spine1         20 R_Wrist
         |                    |                    |
     23 L_Hand                        |             22 R_Hand
                          0 Pelvis 
                       /          \
                  1 L_Hip        2 R_Hip
                     |              |
                  4 L_Knee       5 R_Knee
                     |              |
                  7 L_Ankle      8 R_Ankle
                     |              |
                 10 L_Foot      11 R_Foot

代码里面连接逻辑没问题,是之前处理数据的时候,因为我们目的是上半身动作模仿,所以保留了上半身的关节,这样索引就更改了

更改之后的verify_upper_body.py

import os
import pickle
import numpy as np

# ==========================================
# 配置路径 (自动获取当前脚本所在目录,防止找不到文件)
# ==========================================
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PKL_PATH = os.path.join(CURRENT_DIR, "realsense_results", "res_realsense.pk")

# SMPL 24 关节点定义 (用于查找索引)
SMPL_JOINT_NAMES = [
    "pelvis", "left_hip", "right_hip", "spine1", "left_knee", "right_knee",
    "spine2", "left_ankle", "right_ankle", "spine3", "left_foot", "right_foot",
    "neck", "left_collar", "right_collar", "head", "left_shoulder", "right_shoulder",
    "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hand", "right_hand"
]

def process_upper_body():
    # 1. 加载数据
    if not os.path.exists(PKL_PATH):
        print(f"❌ 找不到数据文件: {PKL_PATH}")
        return
        
    with open(PKL_PATH, 'rb') as f:
        data = pickle.load(f)

    # 获取原始数据 (N, 24, 3)
    raw_joints = data['pred_xyz_24_struct']
    print(f"✅ 数据加载成功: {raw_joints.shape[0]} 帧")

    # 2. 坐标系转换 (修正倒立 + 适配 MuJoCo)
    mj_joints = np.zeros_like(raw_joints)
    mj_joints[:, :, 0] = raw_joints[:, :, 2]   # New X = Old Z
    mj_joints[:, :, 1] = raw_joints[:, :, 0]   # New Y = Old X
    mj_joints[:, :, 2] = -raw_joints[:, :, 1]  # New Z = -Old Y

    # 3. 归一化位置 (让骨盆回到原点)
    pelvis_pos = mj_joints[:, 0:1, :]  
    mj_joints_normalized = mj_joints - pelvis_pos

    # 4. 验证结果
    print("-" * 40)
    print("🔍 [最终检查 - 上半身模式]")

    frame_idx = 0
    pelvis_z = mj_joints_normalized[frame_idx, 0, 2]
    head_z = mj_joints_normalized[frame_idx, 15, 2] # Index 15 is Head
    neck_z = mj_joints_normalized[frame_idx, 12, 2] # Index 12 is Neck

    print(f"   Pelvis Z (应为0): {pelvis_z:.4f}")
    print(f"   Neck   Z (应为正): {neck_z:.4f}")
    print(f"   Head   Z (应为正且最高): {head_z:.4f}")

    if head_z > 0 and head_z > neck_z:
        print("   ✅ 姿态正确:人是正立的,且头比脖子高。")
    else:
        print("   ❌ 姿态依然异常,请检查数据源。")

    # 5. 【核心】按照指定的连线拓扑重新提取关键点 (共16个点)
    upper_body_indices = [
        # --- 中轴线 (0-3-6-9-12-15) ---
        0,   # 0:  pelvis
        3,   # 1:  spine1
        6,   # 2:  spine2
        9,   # 3:  spine3 (分支根节点)
        12,  # 4:  neck
        15,  # 5:  head
        
        # --- 左臂链 (9-14-17-19-21-23) ---
        14,  # 6:  left_collar
        16,  # 7:  left_shoulder
        18,  # 8:  left_elbow
        20,  # 9:  left_wrist
        22,  # 10: left_hand
        
        # --- 右臂链 (9-13-16-18-20-22) ---
        13,  # 11: right_collar
        17,  # 12: right_shoulder
        19,  # 13: right_elbow
        21,  # 14: right_wrist
        23,  # 15: right_hand
    ]

    upper_body_data = mj_joints_normalized[:, upper_body_indices, :]
    print(f"\n🚀 准备就绪: 已提取上半身数据,Shape: {upper_body_data.shape}")
    
    # 6. 【新增】将处理好的数据保存到 npz 文件中
    OUTPUT_PATH = os.path.join(CURRENT_DIR, "realsense_results", "upper_body_data.npz")
    np.savez(OUTPUT_PATH, joints=upper_body_data)
    print(f"💾 数据已成功保存至: {OUTPUT_PATH}")

if __name__ == "__main__":
    process_upper_body()

debug_visualize3.py(建立在mjlab的项目下):

import os
import numpy as np
import mujoco
import mujoco.viewer
import time

# ==========================================
# 1. 配置与路径
# ==========================================
DATA_PATH = "/home/labuser/unitree_ws/src/HybrIK/realsense_results/upper_body_data.npz"
MODEL_PATH = "/home/labuser/unitree_ws/src/unitree_rl_mjlab/mjlab/asset_zoo/robots/unitree_g1/xmls/g1_invisible.xml"

HEIGHT_OFFSET = 0.8  # 抬升高度,防止陷进地里

# ==========================================
# 2. 【核心修改】严格按照 16 点数据的连接顺序
# ==========================================
SKELETON_EDGES_CUSTOM = [
    # --- 躯干与头部链 (0-1-2-3-4-5) ---
    (0, 1), (1, 2), (2, 3), (3, 4), (4, 5),
    
    # --- 左侧分支 (3-6-7-8-9-10) ---
    (3, 6), (6, 7), (7, 8), (8, 9), (9, 10),
    
    # --- 右侧分支 (3-11-12-13-14-15) ---
    (3, 11), (11, 12), (12, 13), (13, 14), (14, 15),
]


def main():
    if not os.path.exists(DATA_PATH):
        print(f"❌ 找不到数据文件: {DATA_PATH}")
        return

    data_dict = np.load(DATA_PATH)
    joints = data_dict['joints']
    num_frames, num_points, _ = joints.shape
    print(f"✅ 数据加载成功: {num_frames} 帧, {num_points} 个点")

    if not os.path.exists(MODEL_PATH):
        print(f"❌ 找不到模型文件: {MODEL_PATH}")
        return
        
    model = mujoco.MjModel.from_xml_path(MODEL_PATH)
    sim_data = mujoco.MjData(model)
    print("✅ G1 隐形模型加载成功!")

    with mujoco.viewer.launch_passive(model, sim_data) as viewer:
        frame_idx = 0
        
        while viewer.is_running() and frame_idx < num_frames:
            step_start = time.time()
            current_skeleton = joints[frame_idx].copy()

            # === 坐标抬升 ===
            current_skeleton[:, 2] += HEIGHT_OFFSET

            # === 强制相机看向骨架中心 (解决看不见的问题) ===
            center = np.mean(current_skeleton, axis=0)
            with viewer.lock():
                viewer.cam.lookat[:] = center 

            # 物理步进
            mujoco.mj_step(model, sim_data)

            # === 绘制 Debug 几何体 ===
            viewer.user_scn.ngeom = 0

            # A. 绘制所有关节点 (红球)
            for i in range(num_points):
                add_sphere(viewer, current_skeleton[i], radius=0.02, rgba=[1, 0, 0, 1])

            # B. 绘制自定义连线 (蓝线)
            for start_idx, end_idx in SKELETON_EDGES_CUSTOM:
                # 安全检查:确保索引不越界
                if start_idx < num_points and end_idx < num_points:
                    add_line(viewer, current_skeleton[start_idx], current_skeleton[end_idx], width=0.015, rgba=[0, 0, 1, 1])

            viewer.sync()

            elapsed = time.time() - step_start
            sleep_time = max(0, 1.0 / 30.0 - elapsed)
            time.sleep(sleep_time)

            frame_idx += 1
            if frame_idx % 10 == 0:
                print(f"Playing frame: {frame_idx}/{num_frames} | Center: {center}")


# ==========================================
# 辅助绘图函数
# ==========================================
def add_sphere(viewer, pos, radius=0.05, rgba=[1, 0, 0, 1]):
    MAX_GEOM_LIMIT = 10000
    if viewer.user_scn.ngeom >= MAX_GEOM_LIMIT: return
    geom = viewer.user_scn.geoms[viewer.user_scn.ngeom]
    mujoco.mjv_initGeom(geom, type=mujoco.mjtGeom.mjGEOM_SPHERE, size=np.array([radius, 0, 0]), pos=np.array(pos, dtype=np.float64), mat=np.eye(3).flatten(), rgba=np.array(rgba))
    geom.category = mujoco.mjtCatBit.mjCAT_DECOR
    viewer.user_scn.ngeom += 1

def add_line(viewer, p1, p2, width=0.01, rgba=[0, 0, 1, 1]):
    MAX_GEOM_LIMIT = 10000
    if viewer.user_scn.ngeom >= MAX_GEOM_LIMIT: return
    p1, p2 = np.array(p1, dtype=np.float64), np.array(p2, dtype=np.float64)
    mid = (p1 + p2) * 0.5; vec = p2 - p1; length = np.linalg.norm(vec)
    if length < 1e-6: return
    z_axis = vec / length
    up = np.array([0, 0, 1]) if abs(z_axis[2]) < 0.9 else np.array([1, 0, 0])
    x_axis = np.cross(up, z_axis); norm_x = np.linalg.norm(x_axis)
    if norm_x < 1e-6: return
    x_axis /= norm_x; y_axis = np.cross(z_axis, x_axis)
    rot_mat = np.column_stack([x_axis, y_axis, z_axis]).flatten()
    geom = viewer.user_scn.geoms[viewer.user_scn.ngeom]
    mujoco.mjv_initGeom(geom, type=mujoco.mjtGeom.mjGEOM_CAPSULE, size=np.array([width, length * 0.5, 0]), pos=mid, mat=rot_mat, rgba=np.array(rgba))
    geom.category = mujoco.mjtCatBit.mjCAT_DECOR
    viewer.user_scn.ngeom += 1

if __name__ == "__main__":
    main()

运行结果:

2.逆运动学转换脚本以及训练前的可视化验证

编写两个脚本,这两个脚本扮演了“翻译官”和“审核员”的核心角色。它们是能够成功启动 RL 训练的前提条件。简单来说,一个是把人动作变机器动作,一个是验证变完之后机器动作是否合理。

(1)motion_retargeting2.py (Translator)

这个脚本的核心任务是“跨物种动作映射”。它负责把人类动作数据转化为 G1 机器人能执行的动力学指令。

    输入:来自 HybrIK 的原始人体动作数据 (.pk 文件,包含 24 个 SMPL 关节的 3D 坐标)。

    核心工作流程:

        解析与对齐:读取人体关节坐标,将其从“人类尺度”归一化到“G1 机器人尺度”。

        运动学解算 (IK):由于 G1 的骨架结构(29 个 DOF)与 SMPL(24 个关节点)并不完全一一对应,脚本通过逆运动学(Inverse Kinematics, minimize 函数)计算出:“如果人手的目标位置在这里,G1 的这 4 个手臂关节应该转多少角度?”

        物理状态模拟 (FK):通过 mujoco.mj_forward 计算出在这些角度下,机器人身体各部位的物理状态(速度、线速度、角速度)。

        格式打包 (Export):将上述计算结果打包成 g1_mjlab_tracking.npz。这里就是我们之前反复调整维度的关键点,确保生成的矩阵形状严格符合 mjlab 环境的 (N, 29) 要求。

#!/usr/bin/env python3
import os
import pickle
import numpy as np
import mujoco
from scipy.optimize import minimize

# ==============================================================================
# 1. 配置路径
# ==============================================================================
XML_PATH = '/home/labuser/unitree_ws/src/unitree_rl_mjlab/mjlab/asset_zoo/robots/unitree_g1/xmls/g1.xml' 
PKL_PATH = '/home/labuser/unitree_ws/src/HybrIK/realsense_results/res_realsense.pk'     
OUTPUT_PATH = '/home/labuser/unitree_ws/src/unitree_rl_mjlab/data/reference_motions/g1_mjlab_tracking.npz'

# SMPL 关节点定义
HUMAN_R_SHOULDER, HUMAN_R_ELBOW = 17, 19
RIGHT_ARM_JOINTS = ["right_shoulder_pitch_joint", "right_shoulder_roll_joint", "right_shoulder_yaw_joint", "right_elbow_joint"]

# ==============================================================================
# 2. 核心 IK 求解器
# ==============================================================================
def solve_arm_ik(model, data, target_wrist_pos, target_elbow_pos, joint_names, site_id, elbow_body_id, default_arm_qpos):
    qpos_indices = [model.jnt_qposadr[mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, name)] for name in joint_names]
    bounds = [model.jnt_range[mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, name)] for name in joint_names]
    init_q = data.qpos[qpos_indices].copy()
    
    def loss_func(q_arm):
        data.qpos[qpos_indices] = q_arm
        mujoco.mj_forward(model, data)
        wrist_loss = np.sum((data.site_xpos[site_id] - target_wrist_pos) ** 2)
        elbow_loss = np.sum((data.xpos[elbow_body_id] - target_elbow_pos) ** 2)
        reg_loss = 0.005 * np.sum((q_arm - default_arm_qpos) ** 2)
        return wrist_loss + 1.2 * elbow_loss + reg_loss
    
    res = minimize(loss_func, init_q, method='SLSQP', bounds=bounds, options={'maxiter': 50})
    data.qpos[qpos_indices] = res.x
    mujoco.mj_forward(model, data)
    return res.x

# ==============================================================================
# 3. 主逻辑
# ==============================================================================
def main():
    model = mujoco.MjModel.from_xml_path(XML_PATH)
    data = mujoco.MjData(model)
    
    with open(PKL_PATH, 'rb') as f:
        hybrik_data = pickle.load(f)
        
    pred_xyz = hybrik_data['pred_xyz_24_struct']
    world_transl = hybrik_data['transl']
    num_frames = pred_xyz.shape[0]
    
    robot_qpos_trajectory = np.zeros((num_frames, model.nq))
    mujoco.mj_resetData(model, data)
    default_qpos = data.qpos.copy()
    
    r_shoulder_bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "right_shoulder_pitch_link")
    r_palm_sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "right_palm")
    r_elbow_jid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, "right_elbow_joint")
    r_elbow_bid = model.jnt_bodyid[r_elbow_jid]
    r_jnt_idx = [model.jnt_qposadr[mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, n)] for n in RIGHT_ARM_JOINTS]
    
    # 容器
    body_pos_w, body_quat_w, body_lin_vel_w, body_ang_vel_w = [], [], [], []
    
    print("开始生成 mjlab 参考动作数据...")
    for frame_idx in range(num_frames):
        # 1. 躯干位置 (假设前 3 位是位置)
        disp = world_transl[frame_idx] - world_transl[0]
        data.qpos[0:3] = default_qpos[0:3] + np.array([-disp[2], disp[0], -disp[1]])
        
        # 2. IK 求解
        human_xyz = pred_xyz[frame_idx]
        r_up_vec = np.array([-(human_xyz[HUMAN_R_ELBOW]-human_xyz[HUMAN_R_SHOULDER])[2], 0, 0])
        solve_arm_ik(model, data, data.xpos[r_shoulder_bid] + r_up_vec*1.2, data.xpos[r_shoulder_bid] + r_up_vec, RIGHT_ARM_JOINTS, r_palm_sid, r_elbow_bid, default_qpos[r_jnt_idx])
        
        robot_qpos_trajectory[frame_idx] = data.qpos.copy()
        
        # 3. FK 数据提取
        mujoco.mj_forward(model, data)
        body_pos_w.append(data.xpos.copy())
        body_quat_w.append(data.xquat.copy())
        body_lin_vel_w.append(data.cvel[:, 3:].copy())
        body_ang_vel_w.append(data.cvel[:, :3].copy())
        
    # 4. 严格维度切片 (7: 对应浮动基座 7 自由度)
    final_joint_pos = robot_qpos_trajectory[:, 7:] 
    
    robot_qvel = np.zeros_like(robot_qpos_trajectory)
    robot_qvel[1:] = (robot_qpos_trajectory[1:] - robot_qpos_trajectory[:-1]) / (1.0/30.0)
    final_joint_vel = robot_qvel[:, 7:] 

    np.savez(OUTPUT_PATH, 
             joint_pos=final_joint_pos,
             joint_vel=final_joint_vel,
             body_pos_w=np.stack(body_pos_w),
             body_quat_w=np.stack(body_quat_w),
             body_lin_vel_w=np.stack(body_lin_vel_w),
             body_ang_vel_w=np.stack(body_ang_vel_w))
             
    print(f"成功保存,输出文件: {OUTPUT_PATH}")
    print(f"最终 joint_pos 维度: {final_joint_pos.shape}, joint_vel 维度: {final_joint_vel.shape}")

if __name__ == '__main__':
    main()

(2) play_motion.py (The Viewer)

这个脚本的角色是“动作可视化验证”。在投入宝贵的 GPU 资源进行 RL 训练之前,必须先看一看生成出来的轨迹是不是“物理合理”的。

    核心工作流程:

        加载模型与轨迹:读取 g1.xml 场景和生成的 .npz 参考动作文件。

        实时渲染:调用 mujoco.viewer 启动一个仿真窗口,按照时间序列把 .npz 里的 joint_pos 一帧一帧写给机器人。

        视觉校准:你通过眼睛观察机器人执行动作时,是否存在:

            自我碰撞 (Self-collision):比如胳膊插进了身体。

            关节扭曲:动作是否生硬,或者某些关节是否被强行拉扯到了极限值。

            漂移:机器人是不是在按照你预期的轨迹走。

        调试决策:如果看起来很诡异,需要回到 motion_retargeting.py 修改 IK 求解的权重(比如 reg_loss 或 elbow_loss),直到动作看起来自然为止。

#!/usr/bin/env python3
import os
import time
import numpy as np
import mujoco
import mujoco.viewer

# ==============================================================================
# 1. 路径配置(绝对路径,确保万无一失)
# ==============================================================================
XML_PATH = '/home/labuser/unitree_ws/src/unitree_rl_mjlab/mjlab/asset_zoo/robots/unitree_g1/xmls/g1.xml' 
NPZ_PATH = '/home/labuser/unitree_ws/src/unitree_rl_mjlab/data/reference_motions/g1_upper_body.npz'

def main():
    # 检查文件是否存在
    if not os.path.exists(XML_PATH):
        print(f"错误: 未找到 G1 模型文件 {XML_PATH}")
        return
    if not os.path.exists(NPZ_PATH):
        print(f"错误: 未找到动作轨迹文件 {NPZ_PATH},请先运行 IK 脚本生成它。")
        return

    # 2. 加载模型与数据
    print("正在加载 MuJoCo 模型...")
    model = mujoco.MjModel.from_xml_path(XML_PATH)
    data = mujoco.MjData(model)

    print("正在读取动作轨迹...")
    motion_data = np.load(NPZ_PATH)
    qpos_trajectory = motion_data['qpos']
    
    num_frames = qpos_trajectory.shape[0]
    print(f"成功加载动作为:{num_frames} 帧,每帧包含 {qpos_trajectory.shape[1]} 个自由度。")
    print("正在启动 MuJoCo 可视化窗口(可在窗口内通过鼠标旋转/缩放视角)...")

    # 3. 启动被动渲染器并循环播放
    with mujoco.viewer.launch_passive(model, data) as viewer:
        # 开启完美的默认视点(让机器人居中)
        viewer.cam.distance = 2.0
        viewer.cam.lookat = [0, 0, 0.7]
        viewer.cam.elevation = -15
        viewer.cam.azimuth = 135

        print("\n>>> 开始循环播放动作。在终端按 Ctrl+C 或直接关闭渲染窗口退出。")
        
        frame_idx = 0
        fps = 30
        frame_dt = 1.0 / fps

        while viewer.is_running():
            step_start = time.time()

            # 将当前帧的关节角度写入机器人
            data.qpos[:] = qpos_trajectory[frame_idx]
            
            # 前向运动学计算更新骨骼位置
            mujoco.mj_forward(model, data)
            
            # 刷新渲染窗口
            viewer.sync()

            # 推进到下一帧(实现循环播放)
            frame_idx = (frame_idx + 1) % num_frames

            # 精准控制 30 FPS 帧率
            elapsed = time.time() - step_start
            if elapsed < frame_dt:
                time.sleep(frame_dt - elapsed)

if __name__ == '__main__':
    main()

运行结果:

下一步就是开始训练了,先尝试不更改奖励函数,看看训练效果

输出的动作包:/home/labuser/unitree_ws/src/unitree_rl_mjlab/data/reference_motions/g1_mjlab_tracking.npz

python3 scripts/train.py Mjlab-Tracking-Flat-Unitree-G1 --motion-file=./data/reference_motions/g1_mjlab_tracking.npz --env.scene.num_envs=64

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐