init cyy
This commit is contained in:
470
go1_unitree_rl_inference.py
Normal file
470
go1_unitree_rl_inference.py
Normal file
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Go1 RL Policy 推理 - 移植自 unitree_rl 项目
|
||||
基于 https://github.com/dstx123/unitree_rl
|
||||
|
||||
模型: body.jit + adapt.jit (GRU-based policy with history)
|
||||
训练环境: IsaacGym
|
||||
|
||||
使用方法:
|
||||
python3 /home/8x54zj-m/unitree_mujoco/go1_unitree_rl_inference.py
|
||||
|
||||
键盘控制:
|
||||
W/S: 前进/后退
|
||||
A/D: 左转/右转
|
||||
Q/E: 侧向左移/右移
|
||||
空格: 停止
|
||||
ESC: 退出
|
||||
"""
|
||||
import numpy as np
|
||||
import mujoco
|
||||
from mujoco import viewer
|
||||
import os
|
||||
import sys
|
||||
import tty
|
||||
import termios
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
import torch
|
||||
|
||||
# ============================================================
|
||||
# 全局退出标志
|
||||
# ============================================================
|
||||
g_exit_requested = False
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
global g_exit_requested
|
||||
g_exit_requested = True
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# ============================================================
|
||||
# 配置
|
||||
# ============================================================
|
||||
BODY_MODEL_PATH = "/tmp/unitree_rl/src/unitree_guide/unitree_guide/model/body.jit"
|
||||
ADAPT_MODEL_PATH = "/tmp/unitree_rl/src/unitree_guide/unitree_guide/model/adapt.jit"
|
||||
XML_PATH = "/home/8x54zj-m/unitree_mujoco/data/go1/xml/go1.xml"
|
||||
MESH_PATH = "/home/8x54zj-m/unitree_mujoco/data/go1/meshes"
|
||||
|
||||
# ============================================================
|
||||
# 模型参数 (来自 unitree_rl/State_RL.h)
|
||||
# ============================================================
|
||||
NUM_OBS = 45 # 观测维度
|
||||
NUM_OBS_HISTORY = 10 # 历史观测步数
|
||||
OBS_BUFFER_SIZE = NUM_OBS * NUM_OBS_HISTORY # 450
|
||||
|
||||
# 观测缩放
|
||||
SCALE_LIN_VEL = 2.0
|
||||
SCALE_ANG_VEL = 0.25
|
||||
SCALE_COMMANDS = np.array([SCALE_LIN_VEL, SCALE_LIN_VEL, SCALE_ANG_VEL])
|
||||
SCALE_DOF_POS = 1.0
|
||||
SCALE_DOF_VEL = 0.05
|
||||
|
||||
# 动作缩放
|
||||
ACTION_SCALE = 0.25
|
||||
HIP_SCALE_REDUCTION = 0.5 # hip关节额外缩放
|
||||
|
||||
# 限幅
|
||||
CLIP_OBSERVATIONS = 100.0
|
||||
CLIP_ACTIONS = 100.0
|
||||
|
||||
# 默认关节角度 (与 unitree_rl 一致)
|
||||
DEFAULT_DOF_POS = np.array([
|
||||
-0.1, 0.8, -1.5, # FR: hip, thigh, calf
|
||||
0.1, 0.8, -1.5, # FL: hip, thigh, calf
|
||||
-0.1, 1.0, -1.5, # RR: hip, thigh, calf
|
||||
0.1, 1.0, -1.5 # RL: hip, thigh, calf
|
||||
])
|
||||
|
||||
# Motor order in code: FR(3,4,5), FL(0,1,2), RR(9,10,11), RL(6,7,8)
|
||||
# Model order: FL(0,1,2), FR(3,4,5), RL(6,7,8), RR(9,10,11)
|
||||
# dof_mapping = {3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8}
|
||||
# Inverse mapping (model idx -> motor idx): 3->0, 4->1, 5->2, 0->3, 1->4, 2->5, 9->6, 10->7, 11->8, 6->9, 7->10, 8->11
|
||||
# This maps: model_output[0..11] -> motor_q[0..11]
|
||||
DOF_MAPPING = np.array([3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8])
|
||||
|
||||
# 速度命令范围
|
||||
MAX_LIN_VEL = 1.0
|
||||
MAX_ANG_VEL = 1.0
|
||||
|
||||
# PD控制参数 (来自 unitree_rl)
|
||||
KP = 7.0
|
||||
KD = 1.0
|
||||
|
||||
# MuJoCo 摩擦系数配置
|
||||
# friction = [滑动摩擦(X), 滑动摩擦(Y), 扭转摩擦]
|
||||
# - friction[0]: X方向滑动摩擦系数
|
||||
# - friction[1]: Y方向滑动摩擦系数 (各向异性材料用)
|
||||
# - friction[2]: 扭转摩擦系数 (绕法线的旋转阻力)
|
||||
#
|
||||
# 常见地面摩擦参数参考:
|
||||
# ┌──────────┬───────────────────┐
|
||||
# │ 地面类型 │ FLOOR_FRICTION │
|
||||
# ├──────────┼───────────────────┤
|
||||
# │ 冰面 │ [0.05, 0.02, 0.01]│
|
||||
# │ 木地板 │ [0.4, 0.3, 0.2 ]│
|
||||
# │ 瓷砖 │ [0.6, 0.4, 0.3 ]│
|
||||
# │ 橡胶垫 │ [1.5, 1.0, 0.5 ]│
|
||||
# │ 粗糙地面 │ [2.0, 1.5, 1.0 ]│
|
||||
# └──────────┴───────────────────┘
|
||||
# 机器人 geom 摩擦系数
|
||||
FRICTION = [0.6, 0.3, 0.3]
|
||||
# 地面摩擦系数 (单独设置)
|
||||
FLOOR_FRICTION = [2.0, 1.5, 1.0]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 键盘输入读取 (termios 非阻塞方式,无 root 依赖)
|
||||
# ============================================================
|
||||
import select
|
||||
|
||||
class KeyboardReader:
|
||||
def __init__(self):
|
||||
self.keys_pressed = set()
|
||||
self.last_key_time = 0
|
||||
self.timeout = 0.1 # 无按键超时时间(秒),超时后清除所有按键
|
||||
|
||||
def init(self):
|
||||
import termios
|
||||
self.old_settings = termios.tcgetattr(sys.stdin)
|
||||
# 非canonical模式,关闭回显
|
||||
new_settings = termios.tcgetattr(sys.stdin)
|
||||
new_settings[3] = new_settings[3] & ~termios.ICANON & ~termios.ECHO
|
||||
new_settings[6][termios.VMIN] = 0 # 非阻塞
|
||||
new_settings[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
||||
print("[INFO] Keyboard reader initialized (termios mode, no root required)")
|
||||
|
||||
def _read_key(self):
|
||||
"""尝试读取一个按键,返回按键字符或None"""
|
||||
try:
|
||||
if select.select([sys.stdin], [], [], 0)[0]:
|
||||
ch = sys.stdin.read(1)
|
||||
return ch
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""在主循环中调用,更新按键状态"""
|
||||
import time
|
||||
current_time = time.time()
|
||||
|
||||
# 尝试读取按键
|
||||
ch = self._read_key()
|
||||
if ch:
|
||||
if ch == '\x1b': # ESC
|
||||
self.keys_pressed.add('escape')
|
||||
elif ch == ' ':
|
||||
self.keys_pressed.add('space')
|
||||
elif ch == '\n' or ch == '\r':
|
||||
pass
|
||||
else:
|
||||
self.keys_pressed.add(ch.lower())
|
||||
self.last_key_time = current_time
|
||||
elif current_time - self.last_key_time > self.timeout:
|
||||
# 超时后清除所有按键(模拟按键释放)
|
||||
self.keys_pressed.clear()
|
||||
|
||||
def restore(self):
|
||||
import termios
|
||||
if hasattr(self, 'old_settings'):
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
|
||||
|
||||
def is_key_pressed(self, key):
|
||||
if key == '\x1b' or key == 'escape':
|
||||
return 'escape' in self.keys_pressed
|
||||
elif key == ' ':
|
||||
return 'space' in self.keys_pressed
|
||||
return key.lower() in self.keys_pressed
|
||||
|
||||
def clear(self):
|
||||
self.keys_pressed.clear()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数
|
||||
# ============================================================
|
||||
|
||||
def quaternion_to_rotation_matrix(q):
|
||||
"""四元数转旋转矩阵 (MuJoCo 格式: qx, qy, qz, qw)"""
|
||||
qx, qy, qz, qw = q
|
||||
norm = np.sqrt(qx**2 + qy**2 + qz**2 + qw**2)
|
||||
qx, qy, qz, qw = qx/norm, qy/norm, qz/norm, qw/norm
|
||||
return np.array([
|
||||
[1-2*(qy**2+qz**2), 2*(qx*qy-qz*qw), 2*(qx*qz+qy*qw)],
|
||||
[2*(qx*qy+qz*qw), 1-2*(qx**2+qz**2), 2*(qy*qz-qx*qw)],
|
||||
[2*(qx*qz-qy*qw), 2*(qy*qz+qx*qw), 1-2*(qx**2+qy**2)]
|
||||
])
|
||||
|
||||
|
||||
def quat_rotate_inverse(q, v):
|
||||
"""四元数逆旋转 (世界坐标系 -> 躯干坐标系)
|
||||
|
||||
对应 IsaacLab 的 quat_apply_inverse
|
||||
q: (w, x, y, z) 格式四元数
|
||||
v: (x, y, z) 向量
|
||||
"""
|
||||
q_w, q_x, q_y, q_z = q[3], q[0], q[1], q[2]
|
||||
return np.array([
|
||||
v[0] * (2*q_w**2 - 1) + 2*q_x*(q_y*v[2] - q_z*v[1]) + 2*q_w*(q_z*v[0] - q_x*v[2]),
|
||||
v[1] * (2*q_w**2 - 1) + 2*q_y*(q_z*v[0] - q_x*v[2]) + 2*q_w*(q_x*v[1] - q_y*v[0]),
|
||||
v[2] * (2*q_w**2 - 1) + 2*q_z*(q_x*v[1] - q_y*v[0]) + 2*q_w*(q_y*v[0] - q_x*v[1])
|
||||
])
|
||||
|
||||
|
||||
def compute_observations(data, prev_action, commands, default_dof_pos):
|
||||
"""
|
||||
计算 RL policy 观测向量 (45维)
|
||||
|
||||
对应 unitree_rl 的观测计算:
|
||||
[0:3] body_ang_vel - 躯干角速度 (body frame)
|
||||
[3:6] projected_gravity - 重力投影到躯干坐标系
|
||||
[6:9] commands - 速度命令 (已缩放)
|
||||
[9:21] dof_pos_rel - 相对关节位置
|
||||
[21:33] dof_vel - 关节速度
|
||||
[33:45] prev_action - 上一步动作
|
||||
"""
|
||||
obs = np.zeros(NUM_OBS, dtype=np.float32)
|
||||
|
||||
# 获取四元数 (MuJoCo 格式: x, y, z, w)
|
||||
quat_mujoco = data.qpos[3:7] # (x, y, z, w)
|
||||
# 转换为 unitree_rl 格式 (x, y, z, w) for quat_rotate_inverse
|
||||
|
||||
# 1. 躯干角速度 (body frame)
|
||||
# unitree_rl 直接使用 imu.gyroscope (已是在 body frame)
|
||||
# MuJoCo 的 qvel[3:6] 是世界坐标系,需要转换
|
||||
ang_vel_world = data.qvel[3:6]
|
||||
ang_vel_body = quat_rotate_inverse(np.concatenate([quat_mujoco[3:4], quat_mujoco[0:3]]), ang_vel_world)
|
||||
obs[0:3] = ang_vel_body * SCALE_ANG_VEL
|
||||
|
||||
# 2. 重力投影 (body frame)
|
||||
# gravity_vec = (0, 0, -1) in unitree_rl
|
||||
gravity_world = np.array([0.0, 0.0, -1.0])
|
||||
projected_gravity = quat_rotate_inverse(np.concatenate([quat_mujoco[3:4], quat_mujoco[0:3]]), gravity_world)
|
||||
obs[3:6] = projected_gravity
|
||||
|
||||
# 3. 速度命令 (已缩放)
|
||||
obs[6:9] = commands * SCALE_COMMANDS
|
||||
|
||||
# 4. 相对关节位置
|
||||
# Motor order: FR(3,4,5), FL(0,1,2), RR(9,10,11), RL(6,7,8)
|
||||
# Model order: FL(0,1,2), FR(3,4,5), RL(6,7,8), RR(9,10,11)
|
||||
motor_pos = data.qpos[7:19] # MuJoCo motor order
|
||||
# Map to model order
|
||||
model_order_pos = motor_pos[DOF_MAPPING] # (12,)
|
||||
dof_pos_rel = model_order_pos - default_dof_pos
|
||||
obs[9:21] = dof_pos_rel * SCALE_DOF_POS
|
||||
|
||||
# 5. 关节速度
|
||||
motor_vel = data.qvel[6:18]
|
||||
model_order_vel = motor_vel[DOF_MAPPING]
|
||||
obs[21:33] = model_order_vel * SCALE_DOF_VEL
|
||||
|
||||
# 6. 上一步动作
|
||||
obs[33:45] = prev_action
|
||||
|
||||
# 限幅
|
||||
obs = np.clip(obs, -CLIP_OBSERVATIONS, CLIP_OBSERVATIONS)
|
||||
|
||||
return obs
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主程序
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
# 1. 加载 MuJoCo 模型
|
||||
os.chdir(MESH_PATH)
|
||||
with open(XML_PATH, 'r') as f:
|
||||
xml_content = f.read()
|
||||
xml_content = xml_content.replace('meshdir="../meshes/"', f'meshdir="{MESH_PATH}"')
|
||||
xml_content = xml_content.replace('<default>\n=', '<default>\n')
|
||||
|
||||
model = mujoco.MjModel.from_xml_string(xml_content)
|
||||
data = mujoco.MjData(model)
|
||||
|
||||
print(f"[INFO] MuJoCo Model: {model.nbody} bodies, {model.nq} DoF, {model.nu} actuators")
|
||||
print(f"[INFO] 关节顺序: {[mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, i) for i in range(1, 13)]}")
|
||||
|
||||
# 2. 设置摩擦系数
|
||||
# friction = [滑动摩擦, 扭转摩擦, 滚动摩擦]
|
||||
# 设置地面摩擦系数 (通过 geom 名称查找)
|
||||
floor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "floor")
|
||||
if floor_id >= 0:
|
||||
model.geom_friction[floor_id] = FLOOR_FRICTION
|
||||
print(f"[INFO] 地面摩擦系数设置为: {FLOOR_FRICTION}")
|
||||
else:
|
||||
print(f"[WARNING] 未找到 floor geom,使用默认摩擦系数")
|
||||
|
||||
# 设置机器人 geom 的摩擦系数 (排除地面)
|
||||
for i in range(model.ngeom):
|
||||
if i != floor_id:
|
||||
model.geom_friction[i] = FRICTION
|
||||
print(f"[INFO] 机器人摩擦系数设置为: {FRICTION}")
|
||||
|
||||
# 3. 加载 RL 模型
|
||||
body_module = torch.jit.load(BODY_MODEL_PATH, map_location='cpu')
|
||||
adapt_module = torch.jit.load(ADAPT_MODEL_PATH, map_location='cpu')
|
||||
body_module.eval()
|
||||
adapt_module.eval()
|
||||
print(f"[INFO] Body model loaded from: {BODY_MODEL_PATH}")
|
||||
print(f"[INFO] Adapt model loaded from: {ADAPT_MODEL_PATH}")
|
||||
print(f"[INFO] 观测维度: {NUM_OBS}, 历史步数: {NUM_OBS_HISTORY}")
|
||||
|
||||
# 3. 初始化
|
||||
obs = np.zeros(NUM_OBS, dtype=np.float32)
|
||||
prev_action = np.zeros(12, dtype=np.float32)
|
||||
obs_buffer = np.zeros(OBS_BUFFER_SIZE, dtype=np.float32) # 10 * 45 = 450
|
||||
|
||||
# 初始化机器人姿态
|
||||
data.qpos[7:19] = DEFAULT_DOF_POS
|
||||
data.qvel[:] = 0.0
|
||||
data.qpos[2] = 0.35 # 抬高躯干
|
||||
mujoco.mj_step(model, data)
|
||||
|
||||
print("[INFO] 机器人初始化完成,开始 RL 策略推理...")
|
||||
|
||||
# 4. 初始化键盘控制
|
||||
keyboard_reader = KeyboardReader()
|
||||
keyboard_reader.init()
|
||||
print("[INFO] 键盘控制已启用!")
|
||||
print(" W/S: 前进/后退")
|
||||
print(" A/D: 左转/右转")
|
||||
print(" Q/E: 侧向左移/右移")
|
||||
print(" 空格: 停止")
|
||||
print(" ESC: 退出")
|
||||
|
||||
# 5. 启动交互式查看器
|
||||
import mujoco.viewer as mv
|
||||
view = mv.launch_passive(model, data)
|
||||
print("[INFO] 交互式查看器已启动!")
|
||||
|
||||
# 初始化观测 buffer (填充历史)
|
||||
for _ in range(NUM_OBS_HISTORY):
|
||||
obs = compute_observations(data, prev_action, np.array([0.0, 0.0, 0.0]), DEFAULT_DOF_POS)
|
||||
obs_buffer = np.concatenate([obs_buffer[NUM_OBS:], obs])
|
||||
|
||||
step_count = 0
|
||||
last_inference_time = time.time()
|
||||
inference_interval = 0.02 # 50Hz
|
||||
|
||||
command = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
joint_targets = DEFAULT_DOF_POS.copy() # 初始化关节目标
|
||||
|
||||
while view.is_running() and not g_exit_requested:
|
||||
current_time = time.time()
|
||||
|
||||
# 更新键盘状态
|
||||
keyboard_reader.update()
|
||||
|
||||
# 读取键盘输入 (切换模式: 按下切换状态)
|
||||
# 空格: 停止所有
|
||||
if keyboard_reader.is_key_pressed(' '):
|
||||
command[:] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
# W/S: 前进/后退 (vx)
|
||||
if keyboard_reader.is_key_pressed('w'):
|
||||
command[0] = MAX_LIN_VEL
|
||||
command[1] = 0.0 # 清除侧向
|
||||
command[2] = 0.0 # 清除转向
|
||||
keyboard_reader.clear()
|
||||
|
||||
if keyboard_reader.is_key_pressed('s'):
|
||||
command[0] = -MAX_LIN_VEL
|
||||
command[1] = 0.0
|
||||
command[2] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
# Q/E: 侧向移动 (vy)
|
||||
if keyboard_reader.is_key_pressed('q'):
|
||||
command[1] = MAX_LIN_VEL
|
||||
command[0] = 0.0
|
||||
command[2] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
if keyboard_reader.is_key_pressed('e'):
|
||||
command[1] = -MAX_LIN_VEL
|
||||
command[0] = 0.0
|
||||
command[2] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
# A/D: 左转/右转 (yaw_rate)
|
||||
if keyboard_reader.is_key_pressed('a'):
|
||||
command[2] = MAX_ANG_VEL
|
||||
command[0] = 0.0
|
||||
command[1] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
if keyboard_reader.is_key_pressed('d'):
|
||||
command[2] = -MAX_LIN_VEL
|
||||
command[0] = 0.0
|
||||
command[1] = 0.0
|
||||
keyboard_reader.clear()
|
||||
|
||||
if keyboard_reader.is_key_pressed('\x1b'):
|
||||
print("[INFO] ESC pressed, exiting...")
|
||||
break
|
||||
|
||||
# 50Hz 推理
|
||||
if current_time - last_inference_time >= inference_interval:
|
||||
# 计算观测
|
||||
obs = compute_observations(data, prev_action, command, DEFAULT_DOF_POS)
|
||||
|
||||
# 更新观测历史 buffer
|
||||
obs_buffer = np.concatenate([obs_buffer[NUM_OBS:], obs])
|
||||
|
||||
# 模型推理
|
||||
with torch.inference_mode():
|
||||
obs_buffer_tensor = torch.from_numpy(obs_buffer).float().unsqueeze(0) # (1, 450)
|
||||
obs_tensor = torch.from_numpy(obs).float().unsqueeze(0) # (1, 45)
|
||||
|
||||
# Adapt 模块: 编码历史观测为 latent
|
||||
latent = adapt_module(obs_buffer_tensor) # (1, 21)
|
||||
|
||||
# Body 模块: 结合当前观测和 latent 生成动作
|
||||
combined_input = torch.cat([obs_tensor, latent], dim=1) # (1, 66)
|
||||
action = body_module(combined_input).numpy().flatten() # (12,)
|
||||
|
||||
# 动作后处理
|
||||
action = np.clip(action, -CLIP_ACTIONS, CLIP_ACTIONS)
|
||||
actions_scaled = action * ACTION_SCALE
|
||||
|
||||
# Hip关节额外缩放
|
||||
hip_indices = [0, 3, 6, 9]
|
||||
for i in hip_indices:
|
||||
actions_scaled[i] *= HIP_SCALE_REDUCTION
|
||||
|
||||
# 转换为电机角度目标
|
||||
joint_targets = actions_scaled[DOF_MAPPING] + DEFAULT_DOF_POS
|
||||
|
||||
# 保存上一步动作
|
||||
prev_action = action.copy()
|
||||
last_inference_time = current_time
|
||||
|
||||
# PD 控制 (每步执行)
|
||||
current_pos = data.qpos[7:19]
|
||||
current_vel = data.qvel[6:18]
|
||||
torque = KP * (joint_targets - current_pos) - KD * current_vel
|
||||
|
||||
# 应用控制
|
||||
data.ctrl[:] = torque
|
||||
mujoco.mj_step(model, data)
|
||||
view.sync()
|
||||
|
||||
step_count += 1
|
||||
if step_count % 100 == 0:
|
||||
trunk_z = data.qpos[2]
|
||||
lin_vel = np.linalg.norm(data.qvel[0:3])
|
||||
print(f" Step {step_count}: cmd=[{command[0]:.2f}, {command[1]:.2f}, {command[2]:.2f}] | "
|
||||
f"trunk_z={trunk_z:.3f}m, vel={lin_vel:.3f}m/s")
|
||||
|
||||
keyboard_reader.restore()
|
||||
view.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user