SimToReal 概述

AKA-Sim2Real 是一个基于前视视角的自动驾驶模拟到真实(Sim2Real)系统,旨在通过模拟器采集人类驾驶数据,训练 ACT(Action Chunking Transformer)模型,并将训练好的策略迁移到真实小车上运行。


系统架构

┌─────────────────────────────────────────────────────────┐
│                     AKA-Sim2Real                        │
│                                                         │
│  ┌─────────────┐    ┌──────────────┐    ┌───────────┐  │
│  │  模拟器/真车  │───▶│  数据采集模块  │───▶│  数据集   │  │
│  │ (Sim/Real)  │    │  Episode API │    │ output/  │  │
│  └─────────────┘    └──────────────┘    │ dataset/ │  │
│         │                               └────┬─────┘  │
│         │                                    │        │
│         │           ┌──────────────┐    ┌────▼─────┐  │
│         │           │  ACT 模型推理  │◀───│ 模型训练  │  │
│         └──────────▶│  Inference   │    │Training  │  │
│                     │   Runtime    │    └──────────┘  │
│                     └──────────────┘                  │
└─────────────────────────────────────────────────────────┘

系统由三个核心部分组成:

模块说明
模拟器 / 真车接口提供前视视角仿真环境,支持键盘手动控制与自动推理两种模式
数据采集记录图像帧 + 车辆状态 + 动作,导出为结构化数据集
ACT 训练与推理基于 Action Chunking Transformer 进行模仿学习,支持 CVAE 与时序集成

技术栈

层级技术
后端框架FastAPI + Socket.IO (Python)
深度学习PyTorch + ResNet18 + Transformer
前端React + TypeScript + Socket.IO Client

文档目录

SimToReal 概述

AKA-Sim2Real 是一个基于前视视角的自动驾驶模拟到真实(Sim2Real)系统,旨在通过模拟器采集人类驾驶数据,训练 ACT(Action Chunking Transformer)模型,并将训练好的策略迁移到真实小车上运行。


系统架构

┌─────────────────────────────────────────────────────────┐
│                     AKA-Sim2Real                        │
│                                                         │
│  ┌─────────────┐    ┌──────────────┐    ┌───────────┐  │
│  │  模拟器/真车  │───▶│  数据采集模块  │───▶│  数据集   │  │
│  │ (Sim/Real)  │    │  Episode API │    │ output/  │  │
│  └─────────────┘    └──────────────┘    │ dataset/ │  │
│         │                               └────┬─────┘  │
│         │                                    │        │
│         │           ┌──────────────┐    ┌────▼─────┐  │
│         │           │  ACT 模型推理  │◀───│ 模型训练  │  │
│         └──────────▶│  Inference   │    │Training  │  │
│                     │   Runtime    │    └──────────┘  │
│                     └──────────────┘                  │
└─────────────────────────────────────────────────────────┘

系统由三个核心部分组成:

模块说明
模拟器 / 真车接口提供前视视角仿真环境,支持键盘手动控制与自动推理两种模式
数据采集记录图像帧 + 车辆状态 + 动作,导出为结构化数据集
ACT 训练与推理基于 Action Chunking Transformer 进行模仿学习,支持 CVAE 与时序集成

技术栈

层级技术
后端框架FastAPI + Socket.IO (Python)
深度学习PyTorch + ResNet18 + Transformer
前端React + TypeScript + Socket.IO Client

文档目录

快速开始

本节介绍如何在本地搭建并运行 AKA-Sim2Real 系统。


环境要求

依赖版本建议
Python3.10+
Node.js18+
PyTorch2.0+(支持 CUDA/MPS/CPU)

1. 克隆仓库

git clone <仓库地址>
cd AKA-Sim2Real

2. 安装后端依赖

cd backend
pip install -r requirements.txt

提示:推荐使用 conda 或 venv 创建独立 Python 环境,避免依赖冲突。


3. 安装前端依赖

cd ui
npm install

4. 启动后端服务

cd backend
python main.py

后端将运行在 http://localhost:8000

启动成功后,终端会输出类似以下信息:

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

注意:后端启动时会尝试自动加载 output/train/model.pt,如果模型文件不存在,则以无模型状态运行(推理功能不可用,但数据采集和训练功能正常)。


5. 启动前端服务

新开一个终端窗口:

cd ui
npm run dev

前端将运行在 http://localhost:5173


6. 访问界面

在浏览器中打开 http://localhost:5173,可以看到:

  • Sim 页面:模拟器视角,用于数据采集与模拟推理
  • Real 页面:真实小车接口视图

目录结构说明

AKA-Sim2Real/
├── backend/          # Python 后端(FastAPI + Socket.IO)
│   ├── main.py       # 服务入口
│   ├── api/          # REST API 路由
│   ├── sio_handlers/ # Socket.IO 事件处理
│   └── services/     # 核心业务逻辑(ACT、训练、采集)
├── ui/               # React 前端
├── policies/         # ACT 模型定义与训练脚本
├── output/           # 运行时输出目录
│   ├── dataset/      # 采集的数据集
│   └── train/        # 训练输出(model.pt)
└── tests/act/        # ACT 最小回归测试

验证安装

运行 ACT 单元测试,验证核心链路正常:

python3 backend/run_act_checks.py

全部通过则说明 ACT 主链路(模型定义、推理、数据导出)运行正常。


下一步

数据采集

数据采集是 Sim2Real 流程的第一步。通过在模拟器中手动驾驶小车,系统会同步记录摄像头图像车辆状态控制动作,最终导出为结构化数据集供 ACT 模型训练使用。


采集的数据格式

每个时间步采集以下数据:

字段类型说明
imageRGB 图像帧模拟器前视摄像头画面
state[vel_left, vel_right]左右轮速度(当前车辆状态)
action[vel_left, vel_right]左右轮控制指令

状态维度和动作维度均为 2,分别对应左轮速度和右轮速度。


数据采集步骤

1. 启动系统

确保后端和前端均已启动(参见快速开始),在浏览器中打开 Sim 页面。

2. 控制小车

使用键盘控制小车行驶:

按键动作
W / 前进
S / 后退
A / 左转
D / 右转

3. 开始录制

点击界面上的 "开始采集" 按钮,系统进入录制模式,此时每个控制帧的图像、状态和动作均会被记录。

4. 停止录制

完成一段演示后,点击 "停止采集" 按钮。数据会自动导出到:

output/dataset/

数据集结构

导出后的数据集组织结构如下:

output/dataset/
├── episode_0/
│   ├── images/        # 每帧图像(PNG 格式)
│   ├── states.npy     # 状态序列 [N, 2]
│   └── actions.npy    # 动作序列 [N, 2]
├── episode_1/
│   └── ...
└── stats.json         # 数据集统计信息(均值、标准差)

stats.json 保存了状态和动作的归一化统计量,推理时也需要用到:

{
  "state_mean": [0.0, 0.0],
  "state_std":  [1.0, 1.0],
  "action_mean": [0.0, 0.0],
  "action_std":  [1.0, 1.0]
}

采集建议

  • 数量:建议至少采集 100 个样本(帧)才能开始训练,样本越多模型越稳定
  • 多样性:涵盖直行、左转、右转等多种驾驶场景,提升模型泛化能力
  • 一致性:每段演示应保持合理的驾驶行为,避免急停急转等极端操作

REST API(高级用法)

数据采集功能也通过 REST API 暴露,可用于自动化脚本:

方法路径说明
POST/api/episode/start开始采集
POST/api/episode/stop停止采集并导出
GET/api/episode/list列出所有 episode
DELETE/api/episode/{id}删除指定 episode

下一步

数据采集完成后,继续进行 模型训练

模型训练

采集到足够的演示数据后,即可训练 ACT(Action Chunking Transformer)模型。训练过程由后端的训练编排器(Training Orchestrator)自动完成,无需手动干预。


训练前提

  • 已完成数据采集,数据保存在 output/dataset/
  • 数据集中至少有 100 个样本

启动训练

在前端界面点击 "开始训练" 按钮,或通过 REST API 触发:

curl -X POST http://localhost:8000/api/training/start

训练流程详解

训练器(backend/services/training/orchestrator.py)执行以下步骤:

数据集加载
    │
    ▼
构建 ACT 配置
    │
    ▼
初始化模型(ACTModel)
    │
    ▼
训练循环(L1 Loss + KL Loss)
    │
    ▼
保存模型检查点
output/train/model.pt(含 CVAE 潜变量统计)

损失函数

ACT 使用两个损失项联合训练:

损失公式说明
重建损失L1(predicted_actions, gt_actions)动作预测的主要监督信号
KL 散度KL(q(z|obs,action) ∥ p(z|obs))CVAE 正则项,权重为 kl_weight=0.1

总损失:Loss = L1_loss + kl_weight × KL_loss


默认超参数

参数默认值说明
state_dim2状态维度(左右轮速)
action_dim2动作维度(左右轮指令)
action_chunk_size8每次预测的动作步数
hidden_dim512Transformer 隐层维度
num_attention_heads8多头注意力头数
num_encoder_layers4Transformer 编码器层数
num_decoder_layers4Transformer 解码器层数
dim_feedforward3200前馈网络中间维度
kl_weight0.1KL 散度损失权重
latent_dim32CVAE 潜变量维度
use_cvaeTrue是否启用 CVAE

训练输出

训练完成后,模型保存在:

output/train/
├── model.pt           # 完整模型检查点(含 CVAE 潜变量统计)
└── final_model.pt     # 最终模型(如启用了 CVAE 则包含 latent stats)

检查点内容:

{
    "model_state_dict": ...,   # 模型权重
    "config": { ... },         # ACT 配置字典
    "latent_mean": ...,        # CVAE 潜变量均值(用于推理时采样)
    "latent_std":  ...,        # CVAE 潜变量标准差
}

查看训练状态

通过 REST API 查询训练进度:

# 获取训练状态
curl http://localhost:8000/api/training/status

# 停止训练
curl -X POST http://localhost:8000/api/training/stop

单独运行训练脚本(高级)

如需在命令行直接运行训练(不通过 Web 界面):

python policies/models/act/train_act.py \
    --dataset_dir output/dataset \
    --output_dir output/train

下一步

训练完成后,继续进行 模型推理,让小车自主行驶。

模型推理

模型推理阶段,系统使用训练好的 ACT 模型替代人工操控,实现小车自主驾驶。系统同时支持**模拟器(Sim)真实小车(Real)**两种模式。


推理前提

  • 已完成模型训练output/train/model.pt 存在
  • 后端和前端均已启动

加载模型

方式一:自动加载(推荐)

后端启动时会自动尝试加载 output/train/model.pt,无需手动操作。可通过以下接口确认模型状态:

curl http://localhost:8000/health

返回示例:

{
  "model_loaded": true,
  "device": "mps"
}

方式二:前端手动加载

在界面上点击 "加载模型" 按钮,系统会加载最新的模型检查点。

方式三:REST API

curl -X POST http://localhost:8000/api/inference/load

启动自动推理

前端操作

点击 "自动推理" 按钮,系统进入自动驾驶模式:

  1. 摄像头每帧捕获当前图像
  2. 图像与车辆状态输入 ACT 模型
  3. 模型输出动作块(action chunk,8 个时间步)
  4. 取第一个动作执行,循环往复

Socket.IO 事件

推理通过 Socket.IO 实时传输控制指令:

事件方向说明
start_inference客户端 → 服务端启动自动推理
stop_inference客户端 → 服务端停止自动推理
inference_action服务端 → 客户端下发控制动作

推理流程详解

摄像头图像 ──┐
             ├──▶ ACTInferenceRuntime.infer()
车辆状态   ──┘         │
                       ├── 图像预处理(ResNet18 归一化)
                       ├── 状态归一化
                       ├── Transformer 编码解码
                       ├── 输出 action chunk [8, 2]
                       ├── (可选)时序集成(Temporal Ensembling)
                       └── 动作反归一化 → 发送给小车

图像预处理:图像统一缩放并按 ImageNet 均值/方差归一化后送入 ResNet18 视觉编码器。

状态归一化:使用数据集中保存的 state_meanstate_std 进行 Z-score 归一化。

动作反归一化:输出的动作使用 action_meanaction_std 还原为真实控制量。


时序集成(Temporal Ensembling)

时序集成是 ACT 论文中提出的平滑技术,通过对多个历史 action chunk 的加权融合来减少控制抖动。

启用方式(设置环境变量):

export ACT_TEMPORAL_ENSEMBLING=1
python backend/main.py

衰减权重temporal_ensembling_weight 配置参数控制(默认 0.5),值越小融合越平滑。

注意:当前默认关闭时序集成,适合大多数场景。如控制输出存在抖动,可尝试开启。


Sim 与 Real 模式

系统同时支持两个 Socket.IO 命名空间:

命名空间用途
/sim模拟器推理,控制虚拟小车
/real真实小车推理,通过硬件接口控制实体小车

两个命名空间使用相同的推理逻辑,仅底层执行器(模拟器控制器 vs 真实硬件驱动)不同。


推理 REST API

方法路径说明
GET/health查询模型加载状态
POST/api/inference/load加载模型
POST/api/inference/reset重置推理上下文(清空时序集成缓存)

推理调试

查看后端日志中的推理输出,确认模型正常运行:

INFO: 加载 ACT 模型...
INFO: 状态归一化: mean=[0.0, 0.0], std=[1.0, 1.0]
INFO: 动作归一化: mean=[0.0, 0.0], std=[1.0, 1.0]
INFO: ACT 模型加载完成,使用设备: mps

若模型未加载时触发推理,系统会返回零动作 [0.0, 0.0] 并打印警告日志,而非抛出异常,保证系统鲁棒性。

ACT 模型架构

ACT(Action Chunking Transformer)是一种基于 Transformer 的模仿学习策略,通过预测**动作块(action chunk)**而非单步动作来实现平滑、稳定的控制输出。


整体架构图

                     ┌──────────────────────────────────────┐
                     │           ACTModel                   │
                     │                                      │
  图像输入            │  ┌──────────────┐                   │
  (H×W×3) ─────────▶│  │  RGBEncoder   │──▶ 视觉特征 (D)   │
                     │  │  (ResNet18)   │                   │
                     │  └──────────────┘         │          │
                     │                           │          │
  状态输入            │  ┌──────────────┐         │          │
  [vel_l, vel_r] ──▶│  │ StateEncoder  │──▶ 状态特征 (D)   │
                     │  │    (MLP)      │         │          │
                     │  └──────────────┘         │          │
                     │                           ▼          │
  ┌──────────┐       │               ┌────────────────┐     │
  │   CVAE   │──────▶│               │ Transformer    │     │
  │ (训练时)  │  z   │               │ Encoder-Decoder│     │
  └──────────┘       │               └────────┬───────┘     │
                     │                        │             │
                     │                        ▼             │
                     │               ┌────────────────┐     │
                     │               │  动作预测头     │     │
                     │               │  (Linear)      │     │
                     │               └────────────────┘     │
                     │                        │             │
                     └────────────────────────┼─────────────┘
                                              │
                                              ▼
                              action_chunk [chunk_size, action_dim]
                              (默认 8 步 × 2 维)

核心组件

1. RGBEncoder(视觉编码器)

  • 骨干网络:ResNet18(ImageNet 预训练)
  • 输入:归一化后的 RGB 图像
  • 输出:展平的视觉特征向量,投影到 hidden_dim(默认 512)
  • 图像预处理:resize → ImageNet 均值/方差归一化

2. StateEncoder(状态编码器)

  • 结构:简单 MLP(多层感知机)
  • 输入:归一化后的车辆状态 [vel_left, vel_right](维度 2)
  • 输出:状态特征向量(维度 hidden_dim

3. CVAE(条件变分自编码器)

CVAE 在训练阶段引入潜变量 z,为动作预测提供多模态表达能力。

训练时:
  encoder: (观测, 动作序列) → (μ, σ) → z ~ N(μ, σ²)

推理时:
  z ~ N(latent_mean, latent_std)  # 从训练分布中采样
  或 z = 0                        # 使用零向量(确定性推理)

KL 散度损失约束潜变量分布接近标准正态分布:

KL Loss = KL(N(μ, σ²) ∥ N(0, I))

4. Transformer Encoder-Decoder

参数默认值
hidden_dim512
num_attention_heads8
num_encoder_layers4
num_decoder_layers4
dim_feedforward3200
  • 编码器:将视觉特征 + 状态特征 + CVAE 潜变量拼接,经多层自注意力编码为上下文表示
  • 解码器:以可学习的动作查询(action queries)为输入,通过交叉注意力从编码器上下文中提取信息,输出 action_chunk_size 个动作向量

5. 动作预测头

线性层将 Transformer 解码器的输出投影到动作空间(维度 action_dim=2),输出归一化后的动作块。


动作块(Action Chunking)

ACT 的核心创新之一是不预测单步动作,而是一次性预测 action_chunk_size 步的动作序列(默认 8 步)。

优势

  • 缓解复合误差(compound error)
  • 提升长序列动作的时间一致性
  • 减少控制器与环境之间的高频交互需求

执行策略:推理时每次只执行 action chunk 的第一步动作(或使用时序集成),下一帧重新预测。


时序集成(Temporal Ensembling)

时序集成通过加权融合多个历史 action chunk 的预测,进一步平滑控制输出。

当前动作 = α × 当前 chunk[0] + (1-α) × 历史融合动作

权重衰减系数 αtemporal_ensembling_weight,默认 0.5)控制历史信息的保留程度:

  • 值越大:跟随最新预测,响应速度快
  • 值越小:保留更多历史,控制更平滑

代码层次结构

policies/models/act/
├── modeling_act.py         # ACTModel、RGBEncoder、StateEncoder、CVAE、Transformer 层
├── configuration_act.py    # ACTConfig 数据类
├── defaults.py             # DEFAULT_ACT_CONFIG 与 build_act_config()
└── train_act.py            # 独立训练脚本

backend/services/inference/
├── checkpoint.py           # 检查点加载、模型实例化、统计量读取
├── preprocess.py           # 图像预处理、状态/动作归一化与反归一化
├── execution.py            # TemporalEnsemblingPolicy(时序集成)
└── runtime.py              # ACTInferenceRuntime(推理运行时装配)

训练损失汇总

损失项权重说明
L1 重建损失1.0预测动作块与真实动作的 L1 误差
KL 散度0.1(kl_weightCVAE 潜变量正则化

总损失 = L1_loss + 0.1 × KL_loss


参考资料

UI 文档

本节介绍 AKA-Sim2Real 前端的架构、页面和组件。


页面概览

页面路由用途
SimPage/模拟器视角,用于数据采集与模拟推理
RealPage/real真实小车控制接口

技术栈

  • 框架: React 19 + TypeScript
  • 路由: React Router DOM 7
  • 状态管理: Zustand
  • 样式: Tailwind CSS 4
  • HTTP 客户端: Ky
  • 实时通信: Socket.IO Client
  • 构建工具: Vite 7

目录结构

ui/src/
├── main.tsx              # 应用入口
├── App.tsx               # 根组件,路由配置
├── index.css             # Tailwind 入口
├── api/
│   ├── api.ts            # REST API 客户端
│   ├── socket.ts         # Socket.IO 客户端工厂
│   └── realCar.ts        # 真实小车 HTTP API
├── models/
│   └── types.ts          # 共享 TypeScript 类型
├── stores/
│   └── simCarStore.ts    # Zustand 状态管理
└── pages/
    ├── SimPage/          # 模拟器页面
    ├── RealPage/         # 真实小车页面
    └── NotFound.tsx      # 404 页面

核心概念

页面详解


SimPage

路由: /

模拟器主页面,用于数据采集与模型推理测试。

布局结构

┌─────────────────────────────────────────────────────────────┐
│                      SimPage                                │
├───────────────────────────────┬─────────────────────────────┤
│                               │       RightPanel             │
│        TopDownView            │  ┌─────────────────────┐    │
│       (俯视画布)              │  │   FirstPersonView   │    │
│                               │  │   (第一视角)          │    │
│   [小车站 + 障碍物 + 地图]    │  └─────────────────────┘    │
│                               │  ┌─────────────────────┐    │
│                               │  │    LogConsole       │    │
│                               │  │   (日志控制台)       │    │
├───────────────────────────────┴─────────────────────────────┤
│                   TrainingControl + InferenceControl         │
└─────────────────────────────────────────────────────────────┘

核心功能

  1. 键盘控制: WASD/方向键控制小车运动
  2. 数据采集: 录制驾驶演示数据
  3. 训练控制: 启动/停止模型训练
  4. 推理测试: 加载模型进行模拟推理
  5. 实时日志: 显示后端运行日志

Socket.IO 事件

事件名方向说明
actionemit发送动作指令
car_state_updatelisten接收小车状态更新
collect_dataemit发送采集数据
training_progresslisten接收训练进度
act_infer_resultlisten接收推理结果

RealPage

路由: /real

真实小车控制页面,用于连接和控制物理机器人。

布局结构

┌─────────────────────────────────────────────────────────────┐
│                      RealPage                                │
├─────────────────────────────────┬─────────────────────────────┤
│                                 │        RealRightPanel       │
│       RealCameraView            │  ┌─────────────────────┐    │
│      (浏览器摄像头)              │  │     Car IP 配置      │    │
│                                 │  │   连接状态显示        │    │
│                                 │  │   电机状态显示        │    │
├─────────────────────────────────┴─────────────────────────────┤
│                      控制面板                                  │
│     [连接] [前进] [后退] [左转] [右转] [停止]                   │
└─────────────────────────────────────────────────────────────┘

核心功能

  1. 摄像头访问: 使用浏览器 API 获取小车摄像头画面
  2. IP 配置: 设置小车 IP 地址
  3. 电机控制: HTTP 请求控制小车电机
  4. 状态监控: 显示心跳和电机状态

API 通信

真实小车通过 HTTP API 直接通信:

// 发送心跳
carHeartbeat(carIP)

// 获取电机状态
motorStatusAt(carIP)

// 直接控制电机
motorDirect(carIP, leftVel, rightVel)

// 小车整体控制
carControl(carIP, action)

组件详解


SimPage 组件

TopDownView

俯视画布组件,展示小车、障碍物和地图。

文件: pages/SimPage/TopDownView.tsx

功能:

  • Canvas 绑定的 2D 地图渲染
  • 键盘事件监听(WASD/方向键)
  • 小车位置和角度显示
  • 障碍物绘制

状态依赖: 监听 simCarStore 获取小车状态


RightPanel

右侧面板容器,包含第一视角视图和日志控制台。

文件: pages/SimPage/RightPanel.tsx

子组件:

  • FirstPersonView(第一视角渲染)
  • LogConsole(日志显示)

InferenceControl

推理控制面板。

文件: pages/SimPage/InferenceControl.tsx

功能:

  • 加载训练好的模型
  • 单步推理测试
  • 自动推理模式

TrainingControl

训练和数据采集控制面板。

文件: pages/SimPage/TrainingControl.tsx

功能:

  • Episode 管理(开始/结束/保存)
  • 数据采集开关
  • FPS 配置
  • 训练启动/停止

LogConsole

实时日志显示组件。

文件: pages/SimPage/LogConsole.tsx

功能:

  • Socket.IO log_message 事件监听
  • 自动滚动到底部
  • 日志级别过滤(可选)

RealPage 组件

RealCameraView

浏览器摄像头访问组件。

文件: pages/RealPage/RealCameraView.tsx

功能:

  • navigator.mediaDevices.getUserMedia 获取视频流
  • 设备选择下拉框
  • 视频预览显示

RealRightPanel

真实小车右侧状态面板。

文件: pages/RealPage/RealRightPanel.tsx

功能:

  • 小车 IP 输入
  • 连接状态指示
  • 电机状态显示

共享组件

actionMapping

键盘按键到动作的映射工具。

文件: pages/SimPage/actionMapping.ts

映射关系:

按键动作
W / ↑forward
S / ↓backward
A / ←turn_left
D / →turn_right
Qforward_left
Eforward_right
Zbackward_left
Cbackward_right
Spacestop

状态管理


Zustand Store

使用 Zustand 进行轻量级状态管理。

simCarStore

文件: stores/simCarStore.ts

管理模拟器小车状态。

interface CarState {
  x: number;        // 小车 X 坐标
  y: number;        // 小车 Y 坐标
  angle: number;   // 小车角度 (弧度)
  vel_left: number;  // 左轮速度
  vel_right: number; // 右轮速度
}

const initialState: CarState = {
  x: 400,
  y: 300,
  angle: -Math.PI / 2,
  vel_left: 0,
  vel_right: 0,
};

Actions

Action说明
setCarState(state)更新小车状态
resetCarState()重置为初始状态

页面级 State

除全局 Store 外,各页面使用 useState 管理本地状态。

SimPage

State类型说明
isRecordingboolean是否正在录制
episodeCountnumberEpisode 数量
trainingStatusstring训练状态
inferenceResultobject推理结果

RealPage

State类型说明
carIPstring小车 IP 地址
isConnectedboolean连接状态
cameraDevicesMediaDeviceInfo[]摄像头设备列表
motorStatusobject电机状态

API 通信


REST API

使用 Ky HTTP 客户端与后端通信。

基础配置: api/api.ts

const api = ky.create({
  prefixUrl: '/api',
  headers: { 'Content-Type': 'application/json' },
});

端点列表

方法路径说明
POSTdataset/collect采集训练图片数据
POSTtrain启动模型训练
POSTtrain/stop停止训练
POSTact/load_trained加载训练好的模型

Socket.IO

实时双向通信,用于模拟器状态同步和日志推送。

Socket 工厂

文件: api/socket.ts

createSocket(namespace: string): Socket

创建命名空间的 Socket 实例:

实例命名空间用途
simSocket/sim模拟器状态同步
realSocket/real真实小车控制

事件列表

Emit 事件 (前端 → 后端)

事件数据格式说明
action{ action: string }发送动作指令
reset_car_state-重置小车状态
get_car_state-请求当前状态
collect_data{ image: string, state: object }采集数据
start_episode{ episodeId: number }开始 Episode
end_episode{ episodeId: number }结束 Episode
finalize_episode{ episodeId: number }保存 Episode
act_infer{ image: string, state: object }推理请求

Listen 事件 (后端 → 前端)

事件数据格式说明
connected-连接成功
car_state_updateCarState小车状态更新
collection_count{ count: number }采集数量
episode_infoEpisodeInfoEpisode 信息
training_progress{ epoch: number, loss: number }训练进度
act_infer_result{ action: string }推理结果
log_message{ level: string, message: string }日志消息

真实小车 HTTP API

文件: api/realCar.ts

直接向小车 IP 发送 HTTP 请求。

接口列表

函数HTTP 方法路径说明
carHeartbeatGET/api/heartbeat发送心跳
motorStatusAtGET/api/motor/status获取电机状态
motorDirectPOST/api/motor/direct直接控制电机
carControlPOST/api/car/control小车整体控制
carTimeSyncGET/api/time/sync时间同步

使用示例

import { carControl, motorDirect } from '@/api/realCar';

// 控制小车前进
await carControl(carIP, 'forward');

// 直接控制电机速度
await motorDirect(carIP, 100, 100);

MuJoCo

MuJoCo(Multi-Joint dynamics with Contact)是一个高效的物理仿真引擎,广泛用于机器人学和强化学习研究。

学习资源

文档

安装指南

本指南旨在帮助不同系统的开发者快速搭建 MuJoCo 虚拟仿真环境,重点针对 Linux (Debian/WSL) 进行了优化。

如果使用强化学习,推荐安装如下pip包

# 提供强化学习(RL)的标准化环境接口
pip install gymnasium

# 录制视频或制作 GIF 动图
pip install imageio

# 处理庞大的矩阵和数组数学运算
pip install numpy

# 深度学习框架
pip install torch  

Linux 安装 (Debian/WSL)

  1. 安装miniconda

打开 Linux 系统终端,依次运行以下命令进行静默安装:

# 1. 创建 miniconda 安装文件夹
mkdir -p ~/miniconda3

# 2. 从官方源下载最新的 Linux 安装包 (大概 140MB,稍微等进度条跑完)
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh

# 3. 执行静默安装 (-b 代表后台静默,-u 代表更新,-p 指定绝对路径)
bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3

# 4. 安装完后,把刚才下载的安装包删掉,节约空间!(可选)
rm ~/miniconda3/miniconda.sh

# 5. 初始化 conda,让你的终端认识 conda 命令
~/miniconda3/bin/conda init bash
  1. 激活并初始化 Conda

为了让刚才的配置立刻生效,需要刷新当前终端

source ~/.bashrc

验证成功标志:终端命令行最左边出现 (base) 前缀!

  1. 接受协议与创建Python环境

重要提示:由于新版 Conda 的合规要求,第一次下载包前需要接受服务条款,否则会触发报错。

miniconda_bug

请执行以下命令接受 Anaconda 官方服务条款

conda tos accept

miniconda_bug

创建一个独立的 Python 3.10 环境:

# 创建一个名叫 mujoco_env 的环境,并指定 python 版本为 3.10
conda create -n mujoco_env python=3.10 -y
  1. 激活环境
# 激活进入专属环境
conda activate mujoco_env
  1. 安装 MuJoCo
# 安装 MuJoCo 引擎
pip install mujoco
  1. 代码测试

创建 test_mujoco.py 文件

import mujoco
import mujoco.viewer

model = mujoco.MjModel.from_xml_string("""
<mujoco>
  <worldbody>
    <body>
      <geom type="box" size=".1 .1 .1" rgba="1 0 0 1"/>
    </body>
  </worldbody>
</mujoco>
""")

data = mujoco.MjData(model)

with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running():
        mujoco.mj_step(model, data)
        viewer.sync()
  1. 通过python运行
python test_mujoco.py
  1. 若弹出包含红色方块的 3D 软件窗口,则表示安装成功

linux_init

温馨提示:建议第4步下载速度慢,可配置清华镜像源:

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

Macos 安装

  1. 安装miniconda

要安装miniconda,请按照官方安装指南

  1. 创建干净的 Python 环境
conda create -n mujoco python=3.10
conda activate mujoco
  1. 安装 MuJoCo
pip install mujoco
  1. 用代码测试

创建 test_mujoco.py 文件

import mujoco
import mujoco.viewer

model = mujoco.MjModel.from_xml_string("""
<mujoco>
  <worldbody>
    <body>
      <geom type="sphere" size="0.1"/>
    </body>
  </worldbody>
</mujoco>
""")

data = mujoco.MjData(model)

with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running():
        mujoco.mj_step(model, data)
        viewer.sync()
  1. 通过mjpython运行
mjpython test_mujoco.py
  1. 弹出软件窗口为安装成功

mac_init

Windows 安装

Windows 用户有两种选择:快速查看或 Python 开发环境(推荐两者都做)

A. 快速查看 (免配置)

  1. 下载并解压 MuJoCo点击下载MuJoCo 3.5.0 Windows 压缩包

sha256校验:mujoco-3.5.0-windows-x86_64.zip.sha256 877d0dfbceac3de90a874c41e0f20c568d104e8ca19de955c0482e3b63832519

  1. 解压后双击 bin/simulate.exe

windows_init

  1. 双击上图的simulate.exe后,会同时打开两个窗口
    注意:这里的shell窗口不要关闭(最小化即可)

windows_init1

  1. 鼠标拖拽,mujoco-3.5.0-windows-x86_64/model/humanoid/humanoid.xml ,下的文件到MuJoCo窗口,即可查看效果

windows_init2


B. 开发环境 (推荐使用PyCharm + Miniconda使用)

  1. 安装环境

安装 Miniconda:前往 官方下载

安装 PyCharm:前往 官方下载

  1. 创建环境

在Anaconda Powershell Prompt 或 Anaconda Prompt 中执行以下命令:

conda create -n mujoco_env python=3.10 -y
conda activate mujoco_env
  1. 安装库
pip install mujoco
pip install gymnasium
pip install imageio
  1. 在PyCharm环境下运行代码测试

创建 test_mujoco.py 文件

import mujoco
import mujoco.viewer

model = mujoco.MjModel.from_xml_string("""
<mujoco>
  <worldbody>
    <body>
      <geom type="box" size=".1 .1 .1" rgba="1 1 1 1"/>
    </body>
  </worldbody>
</mujoco>
""")

data = mujoco.MjData(model)

with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running():
        mujoco.mj_step(model, data)
        viewer.sync()
  1. 在PyCharm环境下运行 python test_mujoco.py,弹出软件窗口为安装成功

windows_init3

No.1 第一个 MuJoCo 仿真

本节通过一个最小示例,帮助你快速上手 MuJoCo XML 建模与 Python 仿真脚本。


文件说明

本节的示例文件位于 mujoco/No_1/ 目录下:

mujoco/No_1/
├── hello.xml   # MuJoCo XML 场景描述文件
└── no_1.py     # Python 仿真脚本

hello.xml 详解

完整内容

<mujoco>
    <!-- 全局仿真选项:重力加速度 -->
    <option gravity="0 0 -9.81"/>

    <!-- 编译器设置:角度单位使用弧度 -->
    <compiler angle="radian"/>

    <!-- 视觉渲染设置 -->
    <visual>
        <headlight ambient="0.1 0.1 0.1"/>
    </visual>

    <!-- 资产定义:可复用的材质 -->
    <asset>
        <material name="white" rgba="1 1 1 1"/>
    </asset>

    <!-- worldbody: 物理世界中的所有物体 -->
    <worldbody>
        <!-- 场景光源 -->
        <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>

        <!-- 地面(红色平面) -->
        <geom type="plane" size="1 1 0.5" rgba=".9 0 0 1"/>

        <!-- 物体 1:白色盒子,z=1 -->
        <body pos="0 0 1" euler="0 0 0">
            <joint type="free"/>
            <inertial pos="0 0 0" mass="1" diaginertia="0.01 0.01 0.01"/>
            <geom type="box" size=".1 .2 .3" material="white"/>
        </body>

        <!-- 物体 2:青色盒子,z=2,pitch=90°(侧立) -->
        <body pos="0 0 2" euler="0 90 0">
            <joint type="free"/>
            <inertial pos="0 0 0" mass="1" diaginertia="0.01 0.01 0.01"/>
            <geom type="box" size=".1 .2 .3" rgba="0 .9 .9 1"/>
        </body>

        <!-- 物体 3:灰色球体,z=3 -->
        <body pos="0 0 3" euler="0 0 0">
            <joint type="free"/>
            <inertial pos="0 0 0" mass="1" diaginertia="0.01 0.01 0.01"/>
            <geom type="sphere" size=".1" rgba=".5 .5 .5 1"/>
        </body>
    </worldbody>
</mujoco>

元素说明

<option>

全局仿真选项。

  • gravity="0 0 -9.81":标准重力加速度 9.81 m/s²(向下)

<compiler>

编译器设置。

  • angle="radian":角度单位使用弧度(默认是角度),这样 euler 值可以直接写数值如 90 表示 90°

<visual>

渲染视觉设置。

  • headlight:场景主光源的亮度

<asset>

可复用的资产定义。

  • material:定义可复用的材质(name="white",rgba 白色),在 geom 中通过 material="white" 引用

<worldbody>

物理世界的根容器,包含所有物体。

<light>

场景光源。

  • diffuse:漫反射颜色(灰白色)
  • pos:光源位置(上方 3 米)
  • dir:光照方向(沿 -z,即向下)

<geom> 地面

  • type="plane":平面
  • size="1 1 0.5":半尺寸(x=1, y=1, z=0.5 → 全尺寸 2×2×1 米)
  • rgba=".9 0 0 1":红色

<body>

刚体,可内嵌关节、惯性、几何体。

属性说明
pos初始位置(世界坐标系)
euler初始欧拉角(roll pitch yaw,弧度)

三个刚体的配置:

物体poseulergeom 类型颜色
物体 1z=10 0 0(水平)box白色(material)
物体 2z=20 90 0(侧立)box青色
物体 3z=30 0 0sphere灰色

<joint>

关节,连接 body 与父级(或世界)。

  • type="free":自由关节,物体不受任何约束,可在 6 个自由度上自由运动(平移 + 旋转)

<inertial>

刚体的质量分布特性。

属性说明
pos质心在 body 本地坐标系中的位置
mass质量(kg)
diaginertia惯性张量的三个主对角元素(Ixx, Iyy, Izz)

提示:如果不手动指定 inertial,MuJoCo 会根据 geom 的形状和默认密度自动计算。

<geom> 物体几何体

  • type:形状类型,支持 boxspherecylindercapsuleplaneellipsoid
  • size:半尺寸向量
    • box:size="0.1 0.2 0.3" → 全尺寸 0.2×0.4×0.6 米
    • sphere:size="0.1" → 半径 0.1 米
  • rgba:颜色 + 透明度(RGBA,各通道 0~1)
  • material:引用 <asset> 中定义的材质(优先于 rgba)

no_1.py 详解

import mujoco
import mujoco.viewer

# 1. 从 XML 文件加载模型
model = mujoco.MjModel.from_xml_path('hello.xml')

# 2. 创建仿真数据容器
data = mujoco.MjData(model)

# 3. 启动交互式查看器
with mujoco.viewer.launch_passive(model, data) as viewer:
    # 4. 主循环:每帧推进一次仿真
    while viewer.is_running():
        mujoco.mj_step(model, data)   # 执行一步仿真(默认 dt=0.002s)
        viewer.sync()                  # 同步查看器显示
步骤说明
MjModel.from_xml_path()解析 XML 构建物理模型
MjData存储仿真运行时数据(位置、速度、力等)
mj_step()推进一个仿真时间步
viewer.sync()将仿真状态同步到可视化窗口

运行方法

mujoco/No_1/ 目录下执行:

mjpython no_1.py

运行效果:三个物体同时从不同高度自由下落,落到红色地面上后弹起/静止。


常见错误

1. ValueError: XML Error: Schema violation: unrecognized element

原因:XML 中有拼写错误的标签名,如 <gemo><intertial><muhoco>

解决:检查并修正标签拼写,确认是 <geom><inertial><mujoco>

2. 物体没有出现或直接穿透地面

原因inertial 未指定且 geom 没有定义时,MuJoCo 可能使用了零质量。

解决:确保每个 body 下都有 <inertial mass="..."/> 或让 geom 的密度足够大。

3. mjpython: command not found

原因mujoco 包未正确安装,或 mjpython 不在 PATH 中。

解决

pip install mujoco
# 或直接用 python 运行
python no_1.py

No.2 交互式仿真与鼠标控制

本节介绍如何使用 GLFW 窗口创建交互式 3D 仿真,包括鼠标视角控制和控制器回调。


文件说明

本节的示例文件位于 mujoco/No_2/temp_mjcpy/ 目录下:

mujoco/No_2/temp_mjcpy/
├── ball.xml              # MuJoCo XML 场景描述文件
├── projectile.py         # 基础交互式仿真脚本
└── template_mujoco.py    # 带详细注释的模板脚本

ball.xml 详解

一个简单的弹球场景:

<mujoco>
  <worldbody>
    <!-- 场景光源 -->
    <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>

    <!-- 红色平面地面 -->
    <geom type="plane" size="10 1 0.1" rgba=".9 0 0 1"/>

    <!-- 绿色小球,初始位置 z=1 -->
    <body pos="0 0 1">
      <joint type="free"/>
      <geom type="sphere" size=".1" rgba="0 .9 0 1"/>
    </body>
  </worldbody>
</mujoco>
元素说明
plane平面地面,半尺寸 10×1×0.1
sphere绿色小球,半径 0.1

template_mujoco.py 详解

核心结构

import mujoco as mj
from mujoco.glfw import glfw
import numpy as np
import os

# 1. 模型加载
model = mj.MjModel.from_xml_path(xml_path)
data = mj.MjData(model)
cam = mj.MjvCamera()      # 视角相机
opt = mj.MjvOption()      # 可视化选项

# 2. GLFW 窗口初始化
glfw.init()
window = glfw.create_window(1200, 900, "Demo", None, None)
glfw.make_context_current(window)
glfw.swap_interval(1)

# 3. 可视化数据结构
scene = mj.MjvScene(model, maxgeom=10000)
context = mj.MjrContext(model, mj.mjtFontScale.mjFONTSCALE_150.value)

# 4. 注册回调函数
glfw.set_key_callback(window, keyboard)
glfw.set_cursor_pos_callback(window, mouse_move)
glfw.set_mouse_button_callback(window, mouse_button)
glfw.set_scroll_callback(window, scroll)
mj.set_mjcb_control(controller)

# 5. 主循环
while not glfw.window_should_close(window):
    mj.mj_step(model, data)
    # 渲染...
    glfw.poll_events()

glfw.terminate()

回调函数

controller

每步仿真前调用的控制回调,可写入外力:

def controller(model, data):
    """控制回调,每 mj_step 前自动调用"""
    pass

projectile.py 中的示例实现了空气阻力:

def controller(model, data):
    vx, vy, vz = data.qvel[0], data.qvel[1], data.qvel[2]
    v = np.sqrt(vx**2 + vy**2 + vz**2)
    c = 1.0  # 阻力系数
    data.qfrc_applied[0] = -c * v * vx
    data.qfrc_applied[1] = -c * v * vy
    data.qfrc_applied[2] = -c * v * vz

keyboard

键盘事件处理:

def keyboard(window, key, scancode, act, mods):
    if act == glfw.PRESS and key == glfw.KEY_BACKSPACE:
        mj.mj_resetData(model, data)
        mj.mj_forward(model, data)
按键动作
Backspace重置仿真到初始状态

mouse_move

鼠标拖动改变视角:

组合动作
左键拖动旋转视角
右键拖动移动视角
中键拖动缩放
Shift + 拖动切换交互模式

scroll

滚轮缩放视角。


交互操作

视角控制

操作功能
左键拖动旋转视角(水平/垂直)
右键拖动移动视角
滚轮缩放
Shift + 拖动切换模式

键盘

按键功能
Backspace重置仿真

初始条件设置

# 设置小球初始位置
data.qpos[2] = 0.1

# 设置小球初速度 (vx=2, vy=0, vz=5)
data.qvel[0] = 2.0
data.qvel[2] = 5.0

# 设置相机视角
cam.azimuth = 90.0
cam.distance = 8.0
cam.elevation = -45.0

运行方法

mujoco/No_2/temp_mjcpy/ 目录下执行:

python projectile.py
# 或带注释模板
python template_mujoco.py

运行效果:绿色弹球以初速度 (2, 0, 5) 抛出,受重力下落并受空气阻力影响。


与 No.1 的区别

特性No.1 (Passive)No.2 (GLFW)
窗口mujoco.viewer 被动窗口GLFW 主动创建窗口
视角控制受限鼠标自由控制
控制回调mj.set_mjcb_control
渲染控制自动同步手动调用 mjr_render
帧率控制自动手动控制循环频率

No.3 单摆控制仿真

本节介绍倒立摆(Inverted Pendulum)的 MuJoCo 建模与闭环控制,包括关节电机、传感器配置,以及两种控制模式(力矩模式 vs 伺服模式)。


文件说明

本节的示例文件位于 mujoco/No_3/ 目录下:

mujoco/No_3/
├── no_3.py              # 最小主脚本(使用 viewer.launch_passive)
├── control_pendulum.py   # 完整交互脚本(使用 GLFW)
└── pendulum.xml         # MuJoCo XML 模型文件

一、pendulum.xml 详解(对比 No.2 的 ball.xml)

No.3 pendulum.xml 完整代码

<mujoco>
    <!-- 全局仿真选项:重力加速度 -->
    <option gravity="0 0 -9.81">
    </option>

    <worldbody>
        <!-- 场景光源 -->
        <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>

        <!-- 红色平面地面 -->
        <geom type="plane" size="1 1 0.1" rgba=".9 0 0 1"/>

        <!-- 摆杆 body:固定在 (0, 0, 2),绕 y 轴旋转 -->
        <body pos="0 0 2" euler="0 180 0">
            <!--
                hinge 关节:约束为只能绕一个轴旋转
                对比 No.2:No.2 使用 free 关节(6 自由度)
                          No.3 使用 hinge 关节(1 自由度)
            -->
            <joint name="pin" type="hinge" axis="0 -1 0" pos="0 0 0.5"/>
            <!-- 绿色圆柱几何体,半径 0.05,长度 0.5,质量 1 -->
            <geom type="cylinder" size=".05 .5" rgba="0 .9 0 1" mass="1"/>
        </body>
    </worldbody>

    <!--
        actuator 驱动器:【No.3 新增】
        对比 No.2:No.2 没有 actuator,只有纯物理仿真
                  No.3 新增了 3 种驱动器用于闭环控制
    -->
    <actuator>
        <!-- 力矩电机:直接控制关节力矩 -->
        <motor joint="pin" name="torque" gear="1" ctrllimited="true" ctrlrange="-100 100"/>
        <!-- 位置伺服:使用 PD 控制跟踪目标位置 -->
        <position name="position_servo" joint="pin" kp="10"/>
        <!-- 速度伺服:使用 PD 控制跟踪目标速度 -->
        <velocity name="velocity_servo" joint="pin" kv="0"/>
    </actuator>

    <!--
        sensor 传感器:【No.3 新增】
        对比 No.2:No.2 没有 sensor
                  No.3 新增了位置和速度传感器用于闭环反馈
    -->
    <sensor>
        <!-- 关节位置传感器,带噪声 -->
        <jointpos joint="pin" noise="0.2"/>
        <!-- 关节速度传感器,带噪声 -->
        <jointvel joint="pin" noise="1"/>
    </sensor>
</mujoco>

No.2 ball.xml 完整代码(对比参考)

<mujoco>
    <worldbody>
        <!-- 场景光源 -->
        <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>

        <!-- 红色平面地面 -->
        <geom type="plane" size="10 1 0.1" rgba=".9 0 0 1"/>

        <!-- 绿色小球,初始位置 z=1 -->
        <body pos="0 0 1">
            <!-- free 关节:6 自由度,可自由平移和旋转 -->
            <joint type="free"/>
            <geom type="sphere" size=".1" rgba="0 .9 0 1"/>
        </body>
    </worldbody>
    <!-- No.2 没有 actuator 和 sensor -->
</mujoco>

XML 配置对比表

配置项No.2 (ball.xml)No.3 (pendulum.xml)
关节类型free(6 自由度)hinge(1 自由度)
关节名称name="pin"
几何体sphere(球)cylinder(圆柱)
actuator3 个(motor、position、velocity)
sensor2 个(jointpos、jointvel)
mass 定义无(自动计算)mass="1" 显式指定

关节类型详解

关节类型自由度适用场景No.2No.3
free6自由运动物体(球、无人机)
hinge1旋转关节(摆、门)
slide1滑动关节(活塞)--
ball3球形关节(机械臂)--

二、no_3.py 详解(最小脚本)

完整代码

import mujoco
import mujoco.viewer
import time

# 加载 XML 模型
model = mujoco.MjModel.from_xml_path('pendulum.xml')
data = mujoco.MjData(model)

# 使用 viewer.launch_passive 启动被动查看器
# 对比 No.2:No.2 的 template_mujoco.py 使用完整 GLFW 窗口(170 行)
#           No.3 的 no_3.py 使用简化 viewer(仅 12 行核心代码)
with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running():
        mujoco.mj_step(model, data)
        viewer.sync()
        time.sleep(1/500)  # ~60 Hz real-time,约 500ms 休眠实现实时仿真

核心 API 对比

APINo.1 (no_1.py)No.3 (no_3.py)
模型加载MjModel.from_xml_path()MjModel.from_xml_path()
可视化viewer.launch_passive()viewer.launch_passive()
仿真步进mj_step()mj_step()
同步方式viewer.sync()viewer.sync()
主循环条件viewer.is_running()viewer.is_running()
帧率控制time.sleep(1/500)

三、control_pendulum.py 详解(完整脚本)

完整代码

import mujoco as mj
from mujoco.glfw import glfw
import numpy as np
import os

# XML 模型文件路径
xml_path = 'pendulum.xml'
# 仿真结束时间(秒),simend=5 表示仿真运行 5 秒后自动停止
simend = 5

# ============================================================
# 鼠标状态变量(用于鼠标拖动交互)
# 【与 No.2 完全相同】
# ============================================================
button_left = False   # 鼠标左键是否按下
button_middle = False # 鼠标中键是否按下
button_right = False  # 鼠标右键是否按下
lastx = 0             # 上一次鼠标 x 位置
lasty = 0             # 上一次鼠标 y 位置

# ============================================================
# controller 回调:每一步仿真前调用,可在此写入控制指令
# 【核心差异】No.2 的 controller 为空 pass,No.3 实现 PD 控制
# ============================================================
def controller(model, data):
    """
    PD 控制器实现

    对比 No.2:
    - No.2 的 controller 为空 pass,无任何控制逻辑
    - No.3 的 controller 根据 actuator_type 实现两种控制模式
    """
    if actuator_type == "torque":
        # 力矩模式:直接写入控制量
        # 对比 No.2:No.2 没有 actuator_gainprm 配置
        model.actuator_gainprm[0, 0] = 1
        # PD 控制:ctrl = -kp * pos_error - kv * vel_error
        data.ctrl[0] = -10 * \
            (data.sensordata[0] - 0.0) - \
            1 * (data.sensordata[1] - 0.0)
    elif actuator_type == "servo":
        # 伺服模式:配置增益参数
        kp = 10.0
        model.actuator_gainprm[1, 0] = kp
        model.actuator_biasprm[1, 1] = -kp
        data.ctrl[1] = -0.5

        kv = 1.0
        model.actuator_gainprm[2, 0] = kv
        model.actuator_biasprm[2, 2] = -kv
        data.ctrl[2] = 0.0

# ============================================================
# keyboard 回调:键盘事件处理
# 【与 No.2 完全相同】
# ============================================================
def keyboard(window, key, scancode, act, mods):
    if act == glfw.PRESS and key == glfw.KEY_BACKSPACE:
        mj.mj_resetData(model, data)
        mj.mj_forward(model, data)

# ============================================================
# mouse_button 回调:记录鼠标按键状态
# 【与 No.2 完全相同】
# ============================================================
def mouse_button(window, button, act, mods):
    global button_left, button_middle, button_right
    button_left = (glfw.get_mouse_button(window, glfw.MOUSE_BUTTON_LEFT) == glfw.PRESS)
    button_middle = (glfw.get_mouse_button(window, glfw.MOUSE_BUTTON_MIDDLE) == glfw.PRESS)
    button_right = (glfw.get_mouse_button(window, glfw.MOUSE_BUTTON_RIGHT) == glfw.PRESS)

# ============================================================
# mouse_move 回调:鼠标拖动改变视角
# 【与 No.2 完全相同】
# ============================================================
def mouse_move(window, xpos, ypos):
    global lastx, lasty, button_left, button_middle, button_right
    dx = xpos - lastx
    dy = ypos - lasty
    lastx = xpos
    lasty = ypos

    if not button_left and not button_middle and not button_right:
        return

    width, height = glfw.get_window_size(window)
    mod_shift = (glfw.get_key(window, glfw.KEY_LEFT_SHIFT) == glfw.PRESS or
                 glfw.get_key(window, glfw.KEY_RIGHT_SHIFT) == glfw.PRESS)

    if button_right:
        action = mj.mjtMouse.mjMOUSE_MOVE_H if mod_shift else mj.mjtMouse.mjMOUSE_MOVE_V
    elif button_left:
        action = mj.mjtMouse.mjMOUSE_ROTATE_H if mod_shift else mj.mjtMouse.mjMOUSE_ROTATE_V
    else:
        action = mj.mjtMouse.mjMOUSE_ZOOM

    mj.mjv_moveCamera(model, action, dx/height, dy/height, scene, cam)

# ============================================================
# scroll 回调:滚轮缩放视角
# 【与 No.2 完全相同】
# ============================================================
def scroll(window, xoffset, yoffset):
    mj.mjv_moveCamera(model, mj.mjtMouse.mjMOUSE_ZOOM, 0.0, -0.05*yoffset, scene, cam)

# ============================================================
# 模型加载
# 【与 No.2 完全相同】
# ============================================================
dirname = os.path.dirname(os.path.abspath(__file__))
abspath = os.path.join(dirname, xml_path)

model = mj.MjModel.from_xml_path(abspath)
data = mj.MjData(model)
cam = mj.MjvCamera()
opt = mj.MjvOption()

# ============================================================
# GLFW 初始化和窗口创建
# 【与 No.2 完全相同】
# ============================================================
glfw.init()
window = glfw.create_window(1200, 900, "Demo", None, None)
glfw.make_context_current(window)
glfw.swap_interval(1)

# ============================================================
# MuJoCo 可视化数据结构初始化
# 【与 No.2 完全相同】
# ============================================================
mj.mjv_defaultCamera(cam)
mj.mjv_defaultOption(opt)
scene = mj.MjvScene(model, maxgeom=10000)
context = mj.MjrContext(model, mj.mjtFontScale.mjFONTSCALE_150.value)

# ============================================================
# 注册 GLFW 回调和 MuJoCo 控制回调
# 【与 No.2 完全相同】
# ============================================================
glfw.set_key_callback(window, keyboard)
glfw.set_cursor_pos_callback(window, mouse_move)
glfw.set_mouse_button_callback(window, mouse_button)
glfw.set_scroll_callback(window, scroll)
mj.set_mjcb_control(controller)

# ============================================================
# 初始条件设置
# 【核心差异】
# No.2:data.qvel[0] = 2.0(设置初速度)
# No.3:data.qpos[0] = np.pi/2(设置初始角度)
# ============================================================
data.qpos[0] = np.pi/2  # 摆杆初始角度 90°(朝上位置)

# 设置相机视角
cam.azimuth = 90.0
cam.distance = 5.0
cam.elevation = -5
cam.lookat = np.array([0.012768, -0.000000, 1.254336])

# ============================================================
# 主仿真循环
# 【与 No.2 完全相同】
# ============================================================
while not glfw.window_should_close(window):
    simstart = data.time

    while (data.time - simstart < 1.0/60.0):
        mj.mj_step(model, data)

    if (data.time >= simend):
        break

    # 获取窗口帧缓冲区大小
    viewport_width, viewport_height = glfw.get_framebuffer_size(window)
    viewport = mj.MjrRect(0, 0, viewport_width, viewport_height)

    # 更新场景并渲染
    mj.mjv_updateScene(model, data, opt, None, cam,
                       mj.mjtCatBit.mjCAT_ALL.value, scene)
    mj.mjr_render(viewport, scene, context)

    # 交换 OpenGL 缓冲区
    glfw.swap_buffers(window)
    # 处理 GUI 事件
    glfw.poll_events()

glfw.terminate()

控制模式详解

模式 1:力矩模式(torque)

model.actuator_gainprm[0, 0] = 1
data.ctrl[0] = -10 * (data.sensordata[0] - 0.0) - 1 * (data.sensordata[1] - 0.0)
参数说明来源
sensordata[0]关节位置(rad)<sensor><jointpos>
sensordata[1]关节速度(rad/s)<sensor><jointvel>
kp=10位置增益手动设置
kv=1速度增益手动设置
目标位置0.0手动设置

模式 2:伺服模式(servo)

kp = 10.0
model.actuator_gainprm[1, 0] = kp
model.actuator_biasprm[1, 1] = -kp
data.ctrl[1] = -0.5  # 目标位置

使用 XML 中定义的 position_servo,通过配置增益参数实现 PD 控制。


四、No.2 与 No.3 完整代码对比

代码结构对比

模块No.2 (template_mujoco.py)No.3 (control_pendulum.py)
import
全局变量(鼠标状态)
controller 回调pass(空)PD 控制器
keyboard 回调
mouse_button 回调
mouse_move 回调
scroll 回调
模型加载
GLFW 初始化
可视化结构
回调注册
初始条件qvel 设置速度qpos 设置角度
主循环
glfw.terminate

关键差异代码片段

1. 初始条件设置

No.2(template_mujoco.py):

# No initial velocity in template, but in projectile.py:
data.qvel[0] = 2.0  # vx
data.qvel[2] = 5.0  # vz

No.3(control_pendulum.py):

data.qpos[0] = np.pi/2  # 摆杆初始角度 90°(朝上)

2. 控制回调

No.2(template_mujoco.py):

def controller(model, data):
    """控制回调,每 mj_step 前自动调用"""
    pass  # 无任何控制逻辑

No.3(control_pendulum.py):

actuator_type = "torque"  # 或 "servo"

def controller(model, data):
    if actuator_type == "torque":
        model.actuator_gainprm[0, 0] = 1
        data.ctrl[0] = -10 * (data.sensordata[0] - 0.0) - 1 * (data.sensordata[1] - 0.0)
    elif actuator_type == "servo":
        kp = 10.0
        model.actuator_gainprm[1, 0] = kp
        model.actuator_biasprm[1, 1] = -kp
        data.ctrl[1] = -0.5

五、运行方法

mujoco/No_3/ 目录下执行:

# 最小脚本(仅可视化,无控制)
python no_3.py

# 完整脚本(带控制)
python control_pendulum.py

运行效果:

  • no_3.py:摆杆从 90° 位置自由摆动(无控制),仅做可视化
  • control_pendulum.py:摆杆在 PD 控制下稳定在目标位置(0 rad)

六、与 No.2 的整体对比总结

功能特性对比

特性No.2 (弹球)No.3 (倒立摆)
模型类型弹球 + 地面单摆 + 地面
关节类型free(6 自由度)hinge(1 自由度)
驱动器3 个(motor、position、velocity)
传感器2 个(jointpos、jointvel)
控制方式无(被动仿真)力矩控制 + 伺服控制
初始状态设置data.qvel(速度)data.qpos(角度)
主脚本行数170 行(GLFW)12 行(viewer)/ 171 行(GLFW)
控制器复杂度PD 控制实现

学习路径

No.1: 基础建模 + viewer 可视化(被动窗口)
  ↓
No.2: GLFW 窗口 + 鼠标交互 + 回调机制 + 弹球物理
  ↓
No.3: 关节控制 + 传感器读取 + 闭环控制 + 倒立摆

代码复用情况

代码模块No.2 → No.3
鼠标状态变量完全相同
keyboard 回调完全相同
mouse_button 回调完全相同
mouse_move 回调完全相同
scroll 回调完全相同
GLFW 初始化完全相同
可视化结构完全相同
回调注册完全相同
主循环完全相同
controller 回调完全不同(空 → PD)
初始条件完全不同(速度 → 角度)
XML 模型完全不同(弹球 → 倒立摆)

七、常见问题

1. 摆杆直接穿透地面

原因:初始位置或关节配置错误。

解决:检查 euler="0 180 0" 确保摆杆朝下,检查 joint axis 方向。

2. 控制不稳定

原因

  • 比例增益(kp)过大
  • 缺少重力补偿
  • 传感器噪声过大

解决:调整 kpkv 参数,或启用重力补偿。

3. AttributeError: 'NoneType' object has no attribute '...'

原因:GLFW 窗口未正确创建。

解决glfw.create_window() 返回 None 时,检查显示器配置或降低分辨率。

No.4 双摆控制仿真

本节介绍双摆(Double Pendulum)的 MuJoCo 建模与反馈线性化控制,包括多体系统层级结构、RK4 积分器、以及基于动力学模型的控制器设计。


文件说明

本节的示例文件位于 mujoco/No_4/ 目录下:

mujoco/No_4/
├── no_4.py              # 最小主脚本(使用 viewer.launch_passive)
├── double_pendulum.py   # 完整交互脚本(使用 GLFW)
├── doublependulum.xml   # MuJoCo XML 模型文件
└── template_mujoco.py   # GLFW 模板(参考)

一、doublependulum.xml 详解(对比 No.3 的 pendulum.xml)

No.4 doublependulum.xml 完整代码

<mujoco>
    <!--
        全局仿真选项:【No.4 新增】
        - timestep:仿真步长 0.0001s(比默认 0.002 更精细)
        - integrator:使用 RK4(4阶龙格-库塔)积分器,精度更高
        对比 No.3:No.3 使用默认积分器(Euler), timestep 0.002
    -->
    <option timestep="0.0001" integrator="RK4">
    </option>

    <worldbody>
        <!-- 场景光源 -->
        <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>

        <!-- 红色平面地面 -->
        <geom type="plane" size="1 1 0.1" rgba=".9 0 0 1"/>

        <!--
            第一连杆 body:固定在 (0, 0, 2.5),绕 y 轴旋转
            对比 No.3:No.3 只有单个摆杆,No.4 有两个级联的摆杆
        -->
        <body pos="0 0 2.5" euler="0 0 0">
            <!--
                hinge 关节:约束为只能绕一个轴旋转
                注意:pos="0 0 -0.5" 表示关节在连杆的下端
            -->
            <joint name="pin" type="hinge" axis="0 -1 0" pos="0 0 -0.5"/>
            <!-- 绿色圆柱几何体,半径 0.05,长度 0.5,质量 1 -->
            <geom type="cylinder" size="0.05 0.5" rgba="0 .9 0 1" mass="1"/>

            <!--
                第二连杆 body:【No.4 新增】
                嵌套在第一连杆内部,形成层级结构
                位置 pos="0 0.1 1" 相对于父 body
            -->
            <body pos="0 0.1 1" euler="0 0 0">
                <!-- 第二关节 -->
                <joint name="pin2" type="hinge" axis="0 -1 0" pos="0 0 -0.5"/>
                <!-- 蓝色圆柱几何体 -->
                <geom type="cylinder" size="0.05 0.5" rgba="0 0 0.9 1" mass="1"/>
            </body>
        </body>
    </worldbody>
    <!-- No.4 没有 actuator 和 sensor,使用 qfrc_applied 直接施加力 -->
</mujoco>

No.3 pendulum.xml 完整代码(对比参考)

<mujoco>
    <option gravity="0 0 -9.81">
    </option>

    <worldbody>
        <light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
        <geom type="plane" size="1 1 0.1" rgba=".9 0 0 1"/>

        <!-- 摆杆 body -->
        <body pos="0 0 2" euler="0 180 0">
            <joint name="pin" type="hinge" axis="0 -1 0" pos="0 0 0.5"/>
            <geom type="cylinder" size=".05 .5" rgba="0 .9 0 1" mass="1"/>
        </body>
    </worldbody>

    <!-- actuator 驱动器 -->
    <actuator>
        <motor joint="pin" name="torque" gear="1" ctrllimited="true" ctrlrange="-100 100"/>
        <position name="position_servo" joint="pin" kp="10"/>
        <velocity name="velocity_servo" joint="pin" kv="0"/>
    </actuator>

    <!-- sensor 传感器 -->
    <sensor>
        <jointpos joint="pin" noise="0.2"/>
        <jointvel joint="pin" noise="1"/>
    </sensor>
</mujoco>

XML 配置对比表

配置项No.3 (pendulum.xml)No.4 (doublependulum.xml)
关节类型hinge(1 个)hinge(2 个,层级嵌套)
关节名称name="pin"name="pin", name="pin2"
几何体cylinder(单色)cylinder(双色)
body 层级单层双层嵌套
actuator3 个(motor、position、velocity)无(使用 qfrc_applied)
sensor2 个(jointpos、jointvel)
积分器默认(Euler)RK4(4阶龙格-库塔)
timestep0.0020.0001

积分器类型详解

积分器精度计算成本适用场景No.3No.4
Euler最低快速预览
RK44 倍 Euler高精度仿真
implicit刚体系统--

二、no_4.py 详解(最小脚本)

完整代码

import mujoco
import mujoco.viewer
import time

model = mujoco.MjModel.from_xml_path('ball.xml')  # 注:实际加载 ball.xml
data = mujoco.MjData(model)

with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running():
        mujoco.mj_step(model, data)
        viewer.sync()
        time.sleep(1/500)  # ~60 Hz real-time

核心 API 对比

APINo.3 (no_3.py)No.4 (no_4.py)
模型加载MjModel.from_xml_path()MjModel.from_xml_path()
可视化viewer.launch_passive()viewer.launch_passive()
仿真步进mj_step()mj_step()
同步方式viewer.sync()viewer.sync()
主循环条件viewer.is_running()viewer.is_running()
帧率控制time.sleep(1/500)time.sleep(1/500)

注意no_4.py 实际加载的是 ball.xml(单球模型),而非 doublependulum.xml。如需查看双摆,请使用 double_pendulum.py


三、double_pendulum.py 详解(完整脚本)

完整代码

import mujoco as mj
from mujoco.glfw import glfw
import numpy as np
import os

xml_path = 'doublependulum.xml'
simend = 50  # 仿真时间 50 秒

# 鼠标状态变量
button_left = False
button_middle = False
button_right = False
lastx = 0
lasty = 0

def controller(model, data):
    """
    反馈线性化控制器(Feedback Linearization)

    对比 No.3:
    - No.3 使用简单的 PD 控制:ctrl = -kp * pos_error - kv * vel_error
    - No.4 使用反馈线性化,基于系统动力学模型
    """
    # 计算位置相关能量(势能)
    mj.mj_energyPos(model, data)

    # 计算速度相关能量(动能)
    mj.mj_energyVel(model, data)

    # 将稀疏惯性矩阵 M 转换为满矩阵
    M = np.zeros((2, 2))
    mj.mj_fullM(model, M, data.qM)

    # PD 增益和参考角度
    Kp = 100 * np.eye(2)  # 比例增益
    Kd = 10 * np.eye(2)   # 微分增益
    qref = np.array([[-0.5], [-1.6]])  # 目标角度(弧度)

    # f 补偿科里奥利力和重力
    f = data.qfrc_bias[:, np.newaxis]

    # τ = M * ddqref(反馈线性化公式)
    ddqref = Kp @ (qref - data.qpos[:2][:, np.newaxis]) + \
        Kd @ (0 - data.qvel[:2][:, np.newaxis])
    tau = M @ ddqref

    # 直接施加力到系统
    data.qfrc_applied = (tau + f)[:, 0]

反馈线性化控制器详解

动力学模型

双摆系统的动力学方程:

M(q) * qdd + C(q, qd) + g(q) = τ

其中:

  • M(q):惯性矩阵(2x2)
  • C(q, qd):科里奥利力和离心力
  • g(q):重力
  • τ:控制力矩

控制律

ddqref = Kp @ (qref - q) + Kd @ (0 - qd)  # 伪加速度
tau = M @ ddqref                           # 力矩 = 惯性矩阵 × 加速度
data.qfrc_applied = tau + f                # 加上重力/科里奥利补偿
参数说明来源
qref目标角度 [-0.5, -1.6] rad手动设置
Kp位置增益 100 * I手动设置
Kd速度增益 10 * I手动设置
M惯性矩阵mj_fullM()data.qM 计算
f偏置力(重力+科里奥利)data.qfrc_bias

与 No.3 PD 控制的对比

方面No.3 PD 控制No.4 反馈线性化
控制量计算ctrl = -kp * e - kv * edτ = M @ (Kp*e + Kd*ed)
模型知识无需需要惯性矩阵 M
重力补偿通过 qfrc_bias 补偿
科里奥利补偿通过 qfrc_bias 补偿
控制精度中等高(理论上精确跟踪)

四、no_4.py 与 double_pendulum.py 对比

代码结构对比

模块no_4.pydouble_pendulum.py
import
全局变量(鼠标状态)
controller 回调✅(反馈线性化)
keyboard 回调
mouse_button 回调
mouse_move 回调
scroll 回调
模型加载ball.xmldoublependulum.xml
GLFW 窗口
主循环viewer 被动模式GLFW 主循环

五、运行方法

mujoco/No_4/ 目录下执行:

# 最小脚本(仅可视化,实际加载的是 ball.xml)
python no_4.py

# 完整脚本(带反馈线性化控制)
python double_pendulum.py

运行效果:

  • double_pendulum.py:双摆在反馈线性化控制下稳定到目标角度 [-0.5, -1.6] rad

六、与 No.3 的整体对比总结

功能特性对比

特性No.3 (单摆)No.4 (双摆)
模型类型单摆 + 地面双摆 + 地面
关节数量12
body 层级单层双层嵌套
积分器EulerRK4
timestep0.0020.0001
驱动器actuator(motor、servo)qfrc_applied
传感器jointpos、jointvel
控制方式PD 控制反馈线性化
初始条件qpos[0] = np.pi/2qpos[0] = 0.1

学习路径

No.1: 基础建模 + viewer 可视化(被动窗口)
  ↓
No.2: GLFW 窗口 + 鼠标交互 + 回调机制 + 弹球物理
  ↓
No.3: 单摆关节控制 + 传感器读取 + PD 闭环控制
  ↓
No.4: 双摆层级结构 + RK4 积分器 + 反馈线性化控制

代码复用情况

代码模块No.3 → No.4
鼠标状态变量完全相同
keyboard 回调完全相同
mouse_button 回调完全相同
mouse_move 回调完全相同
scroll 回调完全相同
GLFW 初始化完全相同
可视化结构完全相同
回调注册完全相同
主循环完全相同
controller 回调完全不同(PD → 反馈线性化)
初始条件完全不同(单自由度 → 双自由度)
XML 模型完全不同(单摆 → 双摆)

七、常见问题

1. 双摆运动不稳定

原因

  • timestep 过大(RK4 需要更小的步长)
  • 增益参数不合适

解决:No.4 已使用 timestep="0.0001" 和 RK4 积分器,如仍不稳定可进一步减小 timestep。

2. 初始位置不正确

原因qpos[0] 只设置了第一个关节的角度。

解决:如需设置两个关节的初始角度:

data.qpos[0] = 0.1      # 第一关节
data.qpos[1] = 0.2      # 第二关节

3. 反馈线性化控制器不工作

原因qfrc_bias 包含重力但控制器没有正确补偿。

解决

data.qfrc_applied = (tau + f)[:, 0]  # 确保加上偏置力

4. no_4.py 显示的不是双摆

原因no_4.py 加载的是 ball.xml 而非 doublependulum.xml

解决:使用 double_pendulum.py 查看双摆模型。