Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
60 changes: 60 additions & 0 deletions src/box/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
user-in-the-box-simulator
基于Gymnasium和MuJoCo的仿真器,集成生物力学模型、感知模块与强化学习任务,支持分层强化学习与可视化渲染

一、环境准备
1.虚拟环境创建与激活
# 切换到项目根目录
cd C:\Users\86186\user-in-the-box

# 创建Python 3.9虚拟环境(兼容性最优)
python -m venv venv --python=3.9

# 激活虚拟环境(Windows PowerShell)
\venv\Scripts\Activate.ps1
2.依赖安装 使用国内镜像源加速安装:
# 核心依赖
pip install gymnasium==1.2.1 mujoco==2.3.5 stable-baselines3==2.2.1 pygame==2.5.2 opencv-python==4.9.0.80 -i https://pypi.tuna.tsinghua.edu.cn/simple

# 辅助依赖
pip install numpy==1.26.4 scipy==1.11.4 matplotlib==3.8.4 ruamel.yaml==0.18.6 certifi -i https://pypi.tuna.tsinghua.edu.cn/simple

二、核心文件说明

1.simulator.py(仿真器核心)
功能:继承 gym.Env,实现仿真环境的初始化、步骤推进(step)、环境重置(reset)和可视化渲染(render),集成生物力学模型、感知模块和任务逻辑。 运行方式:需通过调用脚本(如 test_simulator.py)运行,示例见 “三、运行步骤”。
2.main.py(辅助脚本) 功能:基于 certifi 查询 CA 证书信息(路径或内容),用于验证网络请求的安全性。
运行方式:
# 查看证书路径
python main.py

# 查看证书内容
python main.py -c
三、运行步骤

仿真器运行(simulator.py)
步骤 1:运行脚本 test_simulator.py

步骤 2:执行脚本

python test_simulator.py
此时会弹出 Pygame 窗口,展示仿真过程(如机械臂运动、感知模块渲染)。 2. 辅助脚本运行(main.py) 在终端执行以下命令,查看证书信息:

# 查看证书路径
python main.py

# 查看证书内容
python main.py -c

四、依赖清单
|库名称 |版本 |用途|
|------|-------|----|
|gymnasium| 1.2.1 |强化学习环境接口|
|mujoco|2.3.5|物理仿真引擎|
|stable-baselines3|2.2.1|强化学习算法库|
|pygame |2.5.2|可视化渲染|
|opencv-python|4.9.0.80|图像感知处理|
|numpy| 1.26.4| 数值计算|
|scipy| 1.11.4|科学计算|
|matplotlib|3.8.4|数据可视化|
|ruamel.yaml|0.18.6 |配置文件解析|
|certifi |2025.10.10 |CA 证书管理|
File renamed without changes.
Empty file.
15 changes: 15 additions & 0 deletions src/box/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import argparse
from certifi import where

parser = argparse.ArgumentParser()
parser.add_argument("-c", "--contents", action="store_true", help="查看证书文件内容")
args = parser.parse_args()

if args.contents:
# 读取 certifi 证书文件内容
cert_path = where()
with open(cert_path, "r", encoding="utf-8") as f:
print(f.read())
else:
# 打印证书文件路径
print(where())
5 changes: 5 additions & 0 deletions src/box/perception/_init_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import Perception
from .vision import VisionPerception
from .joint_state import JointStatePerception

__all__ = ["Perception", "VisionPerception", "JointStatePerception"]
109 changes: 109 additions & 0 deletions src/box/perception/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import numpy as np
import mujoco
from collections import defaultdict

class PerceptionModule:
"""感知子模块基类,所有具体感知模块需继承此类"""
def __init__(self, modality, model, data, **kwargs):
self.modality = modality # 感知模态名称(如vision/joint_state)
self.model = model # MuJoCo模型
self.data = data # MuJoCo数据
self.kwargs = kwargs # 模块参数
self.cameras = [] # 关联的相机(视觉类模块用)

def reset(self, model, data):
"""重置感知模块状态"""
self.model = model
self.data = data

def update(self, model, data):
"""每步更新感知数据"""
pass

def get_observation(self, model, data, info=None):
"""获取该模块的观测数据(需子类实现)"""
raise NotImplementedError

def get_observation_space_params(self):
"""获取Gymnasium观测空间参数(需子类实现)"""
raise NotImplementedError

def get_renders(self):
"""获取可视化渲染结果(视觉类模块用)"""
return []

def close(self):
"""释放资源"""
pass


class Perception:
"""感知总控制器,管理所有感知子模块,适配仿真器架构"""
def __init__(self, model, data, bm_model, perception_modules, common_kwargs):
self.model = model
self.data = data
self.bm_model = bm_model # 关联生物力学模型
self.common_kwargs = common_kwargs # 通用参数(如dt、callbacks)

# 初始化感知子模块
self.perception_modules = []
self.cameras_dict = defaultdict(list) # 相机映射(适配渲染)
self.nu = 0 # 感知模块动作维度(无动作则为0,保持与仿真器兼容)

# 遍历配置的感知模块,初始化实例
for module_cls, module_kwargs in perception_modules.items():
module = module_cls(
model=model,
data=data,
**module_kwargs
)
self.perception_modules.append(module)
# 关联相机(用于渲染)
if hasattr(module, 'cameras') and module.cameras:
self.cameras_dict[module] = module.cameras

def reset(self, model, data):
"""重置所有感知模块"""
self.model = model
self.data = data
for module in self.perception_modules:
module.reset(model, data)

def update(self, model, data):
"""每步更新所有感知模块"""
self.model = model
self.data = data
for module in self.perception_modules:
module.update(model, data)

def get_observation(self, model, data, info=None):
"""收集所有感知模块的观测数据,返回字典(适配仿真器观测空间)"""
observation = {}
for module in self.perception_modules:
obs = module.get_observation(model, data, info)
if obs is not None:
observation[module.modality] = obs
return observation

def get_state(self, model, data):
"""获取感知模块状态(用于仿真器状态记录)"""
state = {}
for module in self.perception_modules:
state[f"perception_{module.modality}"] = module.get_observation(model, data)
return state

def get_renders(self):
"""获取所有视觉类模块的渲染结果(适配仿真器可视化)"""
renders = []
for module in self.perception_modules:
renders.extend(module.get_renders())
return renders

def set_ctrl(self, model, data, ctrl):
"""感知模块动作控制(无动作则空实现,保持与仿真器接口兼容)"""
pass

def close(self, **kwargs):
"""释放所有感知模块资源"""
for module in self.perception_modules:
module.close()
77 changes: 77 additions & 0 deletions src/box/perception/joint_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import numpy as np
import mujoco
from .base import PerceptionModule

class JointStatePerception(PerceptionModule):
"""关节状态感知模块,采集关节角度、速度、力矩"""
def __init__(self, model, data, joint_names=None, include_velocity=True,
include_torque=True, normalize=True, **kwargs):
super().__init__(modality="joint_state", model=model, data=data,** kwargs)

# 关节配置
self.joint_names = joint_names or self._get_all_joints() # 默认所有关节
self.include_velocity = include_velocity
self.include_torque = include_torque
self.normalize = normalize

# 获取关节ID和范围(用于归一化)
self.joint_ids = [mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, name)
for name in self.joint_names]
self.joint_ranges = model.jnt_range[self.joint_ids] # 关节角度范围(min, max)

def _get_all_joints(self):
"""获取模型中所有关节名称"""
joint_names = []
for i in range(self.model.njnt):
name = mujoco.mj_id2name(self.model, mujoco.mjtObj.mjOBJ_JOINT, i)
if name:
joint_names.append(name)
return joint_names

def _normalize_qpos(self, qpos):
"""归一化关节角度到[-1, 1]"""
norm_qpos = (qpos - self.joint_ranges[:, 0]) / (self.joint_ranges[:, 1] - self.joint_ranges[:, 0] + 1e-8)
return (norm_qpos - 0.5) * 2 # 映射到[-1,1]

def get_observation(self, model, data, info=None):
"""获取关节状态观测(角度+速度+力矩)"""
# 关节角度
qpos = data.qpos[[model.jnt_qposadr[jid] for jid in self.joint_ids]]
if self.normalize:
qpos = self._normalize_qpos(qpos)

# 拼接观测
obs_list = [qpos.astype(np.float32)]

# 关节速度
if self.include_velocity:
qvel = data.qvel[[model.jnt_dofadr[jid] for jid in self.joint_ids]]
# 速度归一化到[-1,1](基于经验范围)
if self.normalize:
qvel = np.clip(qvel / 10.0, -1.0, 1.0)
obs_list.append(qvel.astype(np.float32))

# 关节力矩
if self.include_torque:
torque = data.qfrc_actuator[[model.jnt_dofadr[jid] for jid in self.joint_ids]]
# 力矩归一化到[-1,1]
if self.normalize:
torque = np.clip(torque / 50.0, -1.0, 1.0)
obs_list.append(torque.astype(np.float32))

return np.concatenate(obs_list)

def get_observation_space_params(self):
"""定义关节状态观测空间参数"""
# 计算观测维度
dim = len(self.joint_ids)
if self.include_velocity:
dim += len(self.joint_ids)
if self.include_torque:
dim += len(self.joint_ids)

return {
"low": -1.0 if self.normalize else -np.inf,
"high": 1.0 if self.normalize else np.inf,
"shape": (dim,)
}
62 changes: 62 additions & 0 deletions src/box/perception/vision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import numpy as np
import mujoco
from .base import PerceptionModule
from src.box.utils.rendering import Camera # 复用仿真器的Camera类

class VisionPerception(PerceptionModule):
"""视觉感知模块,支持RGB/深度图采集"""
def __init__(self, model, data, camera_id="rgb_camera", resolution=(640, 480),
capture_depth=False, normalize=True, **kwargs):
super().__init__(modality="vision", model=model, data=data, **kwargs)

# 相机配置
self.camera_id = camera_id
self.resolution = resolution
self.capture_depth = capture_depth
self.normalize = normalize

# 初始化相机(复用仿真器的Camera类)
self.camera = Camera(
context=self.kwargs.get("rendering_context"),
model=model,
data=data,
camera_id=camera_id,
dt=self.kwargs.get("dt", 0.01)
)
self.cameras = [self.camera] # 关联到模块相机列表

def get_observation(self, model, data, info=None):
"""获取视觉观测数据(RGB/深度图)"""
# 渲染相机画面
rgb_img, depth_img = self.camera.render()

# 拼接观测(RGB为主,可选深度)
if self.capture_depth:
# 归一化深度图
depth_img = (depth_img - depth_img.min()) / (depth_img.max() - depth_img.min() + 1e-8)
obs = np.concatenate([rgb_img, depth_img[..., np.newaxis]], axis=-1)
else:
obs = rgb_img

# 归一化到[0,1](可选)
if self.normalize:
obs = obs.astype(np.float32) / 255.0

# 展平为一维向量(适配RL观测空间)
return obs.flatten()

def get_observation_space_params(self):
"""定义视觉观测空间参数(适配Gymnasium)"""
# 计算观测维度:RGB(3通道) + 深度(可选1通道)
channels = 3 + (1 if self.capture_depth else 0)
shape = (self.resolution[0] * self.resolution[1] * channels,)
return {
"low": 0.0,
"high": 1.0,
"shape": shape
}

def get_renders(self):
"""返回原始RGB图像(用于仿真器渲染)"""
rgb_img, _ = self.camera.render()
return [rgb_img]
Loading
Loading