MuJoCo 地面两轮小车:射线感知与分级减速避障

对应课程任务:对地面载具进行物理仿真,通过传感器感知完成避障决策。 本模块在 MuJoCo 中串起一条完整的 感知 → 决策 → 控制 闭环: 射线扇面测距(mj_ray)→ 本车道判定 → 距离分级状态机 → 差速轮速。

本模块不含神经网络,定位是后续"神经网络感知 / 控制"课题的规则基线与对照。

代码位于 src/ground/mujoco_car_avoidance/。

1. 概述与目标

1.1 核心任务目标

  1. 物理仿真:车辆动力学由 MuJoCo 解算(速度伺服驱动两轮,前部万向支撑轮防倾覆)。
  2. 感知:25 束前视射线扇面(−60°…60°,5° 间隔)作为二维激光雷达,量程 20 m。
  3. 决策:按"本车道最近距离"分级——巡航 / 分级减速 / 急刹;感知失效时兜底刹停。
  4. 控制:目标速度经加减速限幅后由差速运动学换算为左右轮目标角速度。
  5. 可复现:全部仿真、测试与性能评价不需要图形界面与显卡,克隆即可运行。

1.2 与已有模块的区别

对比项 carla_perception_control carla_yolo_avoidance 本模块
引擎 CARLA 0.9.16 CARLA 0.9.16 MuJoCo
感知方式 RGB + 深度 + 雷达 → MLP YOLOv8 推理 + 单目测距 mj_ray 射线扇面
是否神经网络 是 是 否(规则基线)
运行前提 CARLA 服务端 + GPU + ROS CARLA + torch + ROS mujoco + numpy
无图形界面可用 是(离线取证模式) 否(需连服务端) 是(默认即无窗口)
决策判据 弯道自适应减速 距离阈值(20/8 m) 距离阈值 + 本车道判定 + 主动刹停

2. 计算原理

2.1 射线求交与扇面采样

每条射线由起点 (车头 site)与方向 定义,求与几何体的最近交点:

其中 为量程, 为场景几何体集合。 未命中时取 (等价于"该方向无障碍物")。

自车屏蔽:自车几何体全部位于 geom group 3,求交时传入组掩码 geomgroup = [1,1,1,0,1,1] 把 group 3 排除;同时用 bodyexclude 再兜一层。 只用 bodyexclude 是不够的——左右车轮是车体的子 body,侧向射线会打到自己的轮子, 产生"0.3 m 处有障碍物"的假报警并立刻刹停。

2.2 横向偏移与本车道判定

射线只有距离、没有角度信息,但射线角度是已知量,可由距离反算命中点的横向偏移:

只保留落在本车道内的命中参与决策:

为车道宽度(半宽 0.8 m)。 场景中的路侧干扰物放在 ,即使车头偏航 10°,其横向偏移仍在 1.3 m 以上, 会被射线打到但始终被判为非本车道目标,因此不会触发减速——这正是本判定的作用。

为什么门限收紧到 0.8 m、路侧物外移到 1.8 m

早期路侧物放在 、门限取 0.9 m。车擦着它开过去时, 45° 射线会以横向偏移 0.898 m 擦线命中(与门限只差 0.002), 本车道测距从 4.8 m 突降到 1.3 m,于是误触发刹停。 现在路侧物的最小横向偏移 >1.3 m,配合 0.8 m 门限,留出 0.5 m 以上余量。

2.3 分级速度剖面与主动刹停

设巡航速度 、开始减速距离 、停车目标距离 , 减速区内的目标速度为线性剖面:

判定规则(自上而下):

条件 状态 目标速度
无有效测距且超时 STALE 0(兜底刹停)
FULL_BRAKE 0(急刹)
FULL_BRAKE 0(主动刹停)
SLOW_DOWN
其余 CRUISE

为什么要主动刹停:线性剖面在 时速度趋于 0, 若不做截断,车辆会以几 cm/s 长时间蠕动(实测收敛需 21.9 s)。 引入蠕动阈值 后,车辆在 处果断停住,停车耗时降到 7.1 s。

2.4 加减速限幅

决策给出的是目标速度,直接阶跃会导致急刹点头。实际指令由一阶限幅器给出:

其中 、、。 FULL_BRAKE 与 STALE 两个分支不过限幅器,直接给零速, 因此"分级减速"与"急停"在实现上是两条不同的路径,各自的减速度可被单独评价。

2.5 差速运动学

为轮距, 为轮半径,轮速上限 60 rad/s (对应 )。超出上限时置饱和标志并限幅。

2.6 起步静置与直行航向保持

起步静置与接触设置:车辆出生时轮胎与地面恰好相切(穿透量为 0),MuJoCo 会判定为 "未接触",实测此时 ncon 在 0/3/6 之间跳动、车轮空转,并由左右不对称摩擦引起约 10° 偏航。因此:

  • 出生高度取 (轮半径 ),即出生就带 2 mm 穿透;
  • 地面不设 margin:margin 会让接触在"尚未接触"时就产生力,车身会悬浮在弹性气垫上 (实测静置 ,比轮半径高约 7 mm),支撑又软又不稳,一给驱动指令就被弹飞;
  • 进入闭环前先零轮速推进 静置,等接触建立、车身稳定。

驱动轮出力限幅(决定稳定性):速度伺服的输出必须限幅,否则 的阶跃指令 会让它瞬间输出几百牛的力 —— 实测 内把 的车加速到 、车身冲到 并掀翻(upright = -1), 此后车轮悬空空转、滑移率 100%(看起来像"牵引力不足",实际是车已经倒扣着)。 XML 中给两个轮子设 forcerange = ±8 N,整车最大加速度约 ,足以跟住 的速度规划。

静定三点支撑(决定牵引力):前后支撑轮同时着地是一种静不定的四点支撑—— 求解器可能把整车重量几乎全部分配给支撑轮,驱动轮几乎没有法向力。实测表现为车轮空转 (滑移 75%)、指令 时车速只有 。因此:

  • 车体质心前移到驱动轮轴前方 ,静止时由"两驱动轮 + 前支撑轮"构成 静定三点支撑,驱动轮承担约 80% 重量;
  • 后支撑轮底面抬离地面 ,只作防后仰保护,不参与静止承重。

tools/check_scene.py 会打印质心位置与两个支撑轮的离地高度来校验这一点; tools/check_traction.py 则直接测量静置接触数、轮速与滑移率。

直行航向保持:左右牵引不可能完全对称,而偏航会让"本车道"跟着旋转, 把路侧目标错误地纳入避障决策(实测偏航 10° 即触发)。因此在自主模式下叠加一个比例修正:

其中 、, 取静置结束时的航向。

符号约定(写反会变成正反馈)

differential_drive 中正角速度表示左转(左轮慢、右轮快), 因此车头左偏()时必须输出负角速度。 早期实现多乘了一个负号,实测把偏航从 10° 推到了 29°, 进而让路侧干扰物进入本车道并触发提前刹停。test_heading_correction_reduces_yaw 专门锁住这个符号。

2.7 性能指标定义

指标 符号 含义
触发减速距离 首次进入 SLOW_DOWN 时的射线距离 (m)
触发刹停距离 首次进入 FULL_BRAKE / STALE 时的射线距离 (m)
射线最小距离 全程本车道射线距离最小值 (m)
停车间距 停车时刻车头到障碍物表面的真实间距 (m)
最小间距 全程真实间距最小值 (m),用于判定碰撞
峰值 / 平均速度 实际车速统计 (m/s)
最大减速度 全程减速度峰值 (m/s²)
停车耗时 起步到停稳的时间 (s)
实时率 仿真推进时长 / 墙钟耗时

口径说明:决策链路只用射线距离 ;真实间距 仅由 truth_clearance() 统计, 用于评价感知质量,不参与任何控制。命中点的横向偏移 是由射线角度反算的几何量, 不是场景真值。

两个容易出错的统计口径(都踩过坑):

  • 与 需要同时满足两个条件:先真正起步 ()、并且连续低速 ≥ 0.3 s。 只判"先起步"时,低速接近段偶发的接触抖动( 瞬间掉到 ) 会把 记成 ,而车实际停在 。
  • 只在 SLOW_DOWN / FULL_BRAKE / STALE 状态下统计, 否则巡航阶段接触抖动造成的瞬时跳变(实测出现过 )会被算进制动减速度。

3. 算法流程

--headless / --demo / ROS 2 节点(三者共用 runner.py 的同一闭环)
  ├─ 复位:mj_resetData + mj_forward
  ├─ 起步静置 0.6 s:零轮速推进,等轮胎接触建立、车身稳定后再进闭环
  │   (出生时轮胎带 2 mm 穿透;支撑接触不设 margin;驱动轮出力限幅 ±8 N)
  └─ 每个控制周期 (50 Hz):
       ① 射线扇面扫描 25 束                sensors.LidarFan.scan()
       ② 本车道筛选 → d_min                LidarFan.min_distance()
       ③ 分级决策 → 状态与目标速度          decision.decide()
       ④ 速度整形(限幅 / 急刹直通)        controller.SpeedRamp
       ⑤ 直行航向保持(仅自主模式)          runner.AvoidanceRunner._heading_correction
       ⑥ 差速运动学 → 左右轮目标角速度      controller.differential_drive
       ⑦ 写 ctrl 并推进一个控制周期         CarEnv.advance_control_period()
       ⑧ 读取状态与真实间距,记录指标        metrics.MetricsRecorder
  └─ 结束:导出 CSV 与曲线,打印指标表,按碰撞与否返回退出码

4. 源码解析

文件 类型 职责
main.py 入口 五种模式(demo/interactive/headless/test/launch),退出码约定
mujoco_car_avoidance/config.py 配置 路径解析与全部可调参数,业务代码不写魔数
mujoco_car_avoidance/decision.py 纯函数 decide():距离与新鲜度 → 状态与目标速度
mujoco_car_avoidance/controller.py 纯函数 SpeedRamp 与 differential_drive()
mujoco_car_avoidance/metrics.py 纯逻辑 指标累积、汇总、CSV 与曲线导出
mujoco_car_avoidance/sensors.py 感知 mj_ray 扇面求交、横向偏移、本车道掩码
mujoco_car_avoidance/car_env.py 仿真 场景加载、步进、车辆状态、真实间距、碰撞判定
mujoco_car_avoidance/runner.py 闭环 起步静置、串联感知→决策→控制→指标、直行航向保持,四种入口复用
mujoco_car_avoidance/teleop.py 输入 键盘遥控(termios / msvcrt),手动速度受避障限速封顶
mujoco_car_avoidance/ros_node.py ROS 2 发布 ~/scan、~/odom、~/decision,订阅 ~/cmd_vel
models/car_road_scene.xml 场景 自建 MJCF:直道 + 自车 + 本车道障碍 + 路侧干扰物
tools/check_scene.py 工具 场景静态检查(名称引用、geom group、射线可见性、接触与出生高度、支撑与质心),不需要 MuJoCo
tools/check_traction.py 工具 接触与牵引力诊断(静置接触数、轮速、滑移率、车身高度与姿态),需要 MuJoCo
test/*.py 测试 决策 / 控制 / 闭环逻辑 / 桩环境闭环 / 场景与传感器

设计要点:决策、控制、指标三个模块不导入 MuJoCo, 因此可以在没有安装 MuJoCo 的机器上直接跑单测; test_runner_stub.py 用桩环境驱动真实的 runner.py, 验证的是"实际跑着的那段接线",而不是另写一份简化逻辑。

5. 仿真运行步骤

5.1 支持与测试环境

组件 版本 / 说明
操作系统 Ubuntu 22.04(虚拟机);20.04 见下
Python 3.10
MuJoCo 3.12.0(作业要求版本)
实测环境 Ubuntu 22.04 + Python 3.10.12 + MuJoCo 3.15.0 + numpy 1.26.4
依赖 mujoco、numpy;绘图可选 matplotlib
ROS ROS 2 Humble(仅 ROS 节点与 --launch)
显卡 不需要;--headless 全程无渲染

作业书要求的引擎版本是 MuJoCo 3.12.0;本模块只使用 MjModel / MjData / mj_step / mj_ray / viewer.launch_passive 这一小组稳定接口,3.12 ~ 3.15 均可运行。

Ubuntu 20.04 + Humble 的 Python 版本

20.04 上 ROS 2 Humble 的 _rclpy_pybind11 是针对系统 Python 3.8 编译的, 因此 ROS 节点必须用 3.8 运行;若 mujoco 3.12.0 没有 cp38 wheel, 可用 3.10 的解释器单独运行 main.py(仿真与 ROS 节点无进程内耦合)。

5.2 安装

sudo apt update && sudo apt install -y python3-pip patchelf libgl1 libglfw3
python3 -m pip install -r src/ground/mujoco_car_avoidance/requirements.txt

5.3 运行

cd src/ground/mujoco_car_avoidance

python3 main.py --version        # 先确认代码版本(当前 1.11.0),避免对着旧文件排查
python3 tools/check_scene.py     # 场景静态检查(不需要 MuJoCo):接触、射线分组、支撑质心、出力限幅
python3 tools/check_traction.py  # 接触与牵引力诊断(需要 MuJoCo):接触数、轮速、滑移率、车身姿态
python3 main.py --test           # 自检:纯逻辑 + 场景冒烟测试(当前 62 项)
python3 main.py --headless       # 无窗口跑完整闭环,导出结果
python3 main.py --demo           # 视窗自动演示(录屏用)
python3 main.py --interactive    # 视窗 + WASD 手动驾驶

本模块的修复过程(每一版对应一次真机复现)记录在 src/ground/mujoco_car_avoidance/README.md 的"版本记录"一节。

5.4 ROS 2

cd ~/ros2_ws && colcon build --packages-select mujoco_car_avoidance --symlink-install
source install/setup.bash
ros2 launch mujoco_car_avoidance main.launch.py
# 另开终端观察
ros2 topic echo /mujoco_car_avoidance/scan --once
ros2 topic echo /mujoco_car_avoidance/decision
ros2 topic pub /mujoco_car_avoidance/cmd_vel geometry_msgs/Twist \
  "{linear: {x: 0.5}, angular: {z: 0.0}}" -r 10    # 手动接管,超时后自动交还

6. 运行效果

自动避障过程

自车以 2 m/s 巡航,进入 6 m 减速区后分级降速,在障碍物前约 2 m 停住

场景视窗:自车与前方障碍物

MuJoCo 场景:黄色自车与红色障碍物在直道上

射线距离、车速与真实间距曲线

上:射线测距;中:车速;下:真实间距(--headless 导出)

7. 性能评价

7.1 逻辑级闭环实测(一维理想速度跟踪被控对象,不含 MuJoCo 接触)

由 test/test_closed_loop_logic.py 直接输出,控制频率 50 Hz,初始间距 7.50 m:

指标 数值
进入减速时刻 / 距离 /
峰值速度
停车间距
全程最小间距 (未碰撞)
停车耗时

7.2 MuJoCo 实测(Ubuntu 22.04 + Python 3.10.12 + MuJoCo 3.15.0)

指标 数值
触发减速距离
触发刹停距离
射线最小距离
停车间距
最小间距 (未碰撞)
峰值速度
平均速度 (含长距离低速接近段)
最大减速度 (见下方说明)
停车耗时
实时率 100.0
是否碰撞 否

关于最大减速度 5.93 m/s²:常规减速段按 限幅; 峰值出现在最后的"主动刹停"瞬间——该路径不走速度斜坡、直接把速度指令清零, 因此实测略超常规限幅。统计上用 0.1 s 滑动窗口估计,避免单帧差分被接触抖动放大 (单帧差分曾给出 15.1 m/s² 的假值)。

7.3 结论

  • 决策阈值自洽:逻辑级闭环与 MuJoCo 实测都能在不碰撞的前提下停在障碍物前约 2 m, 且减速段指令速度 单调下降,不出现二次加速(实际车速 因轮胎-地面粘滑会有小幅抖动)。
  • 感知与真值口径一致:射线起点位于前保险杠,实测 与 完全一致,说明射线始终打在目标障碍物上,没有误检。
  • 本车道判定有效:路侧干扰物放在 ,贴着它开过去(约 4.5 m 行程) 全程未被判进本车道,也没有触发误减速。
  • 低速主动刹停有效:把停车耗时从无截断时的 21.9 s 降到约 7~10 s,避免长时间蠕动。
  • 物理层三条硬约束:出生留 2 mm 穿透、支撑接触不设 margin、驱动轮出力限幅 ±8 N。 缺任何一条都会出现"车轮空转 / 车身被弹飞 / 倒扣"这类看起来像算法故障的假故障。

8. 常见问题

现象 原因与处理
ModuleNotFoundError: No module named 'mujoco' 未装依赖,或 pip 与解释器不一致;用 python3 -m pip install
--demo 打不开窗口 无图形界面/未开 3D 加速;改用 --headless,或加 MESA_GL_VERSION_OVERRIDE=3.3
静止时射线测到 0.3 m 的"障碍物" 自车几何体没有进 geom group 3,射线打到了自己的轮胎;运行 tools/check_scene.py 复核
车在障碍物前反复加减速 邻车道目标落进了车道半宽内;调小 LANE_HALF_WIDTH_M 或加大减速区
路侧物体一开始就触发减速 本车道判定未生效(检查 lateral_offsets() 的角度符号与量程掩码)
停车后位置不理想 调整 stop_target_m 与 creep_speed;注意 emergency_m < stop_target_m < slow_start_m
ncon 频繁为 0、车轮空转但车不走 出生时轮胎与地面恰好相切(零穿透)→ 接触断续。检查 car 出生高度与地面 margin,并确认进闭环前调用了 runner.settle()
车不走直线、慢慢偏出去 左右牵引不对称。确认 --heading-gain 不为 0(默认 0.8);仍偏时用探针打印 yaw 曲线定位偏航源
闭环没开到位就停(例如停在 4.8 m 处) 多半是"本车道判定"把路侧目标计入了。打印刹停瞬间的射线表:若命中的是 ±10° 束且横向偏移接近车道半宽,说明车已偏航,应按上一行处理
车速远低于指令(如指令 2 m/s 只跑 0.5 m/s) 先跑 python3 tools/check_traction.py:若"车身高度/姿态"两项 FAIL,说明驱动力没限幅、车被弹飞掀翻了(检查 XML 中 actuator 的 forcerange);若只是滑移率高,再按提示查质心位置与后支撑轮离地高度
不确定虚拟机里是不是最新代码 python3 main.py --version(当前 1.11.0),并用 python3 main.py --test 核对测试数(当前 62 项)
指标表里 c_停车 远大于 c_最小、或 t_停车 很小 停车判定被"起步静止段"或"接近段瞬时抖动"污染(1.8.0 / 1.9.0 已修正:要求先真正起步、且连续低速 ≥0.3 s)
车速远低于指令(如指令 2 m/s 只跑 0.5 m/s) 驱动轮法向力不足(四点支撑静不定)。先跑 python3 tools/check_traction.py,看静置接触数是否 ≥3、滑移率是否 <25%;再跑 check_scene.py 看质心是否在轮轴前方、后支撑轮是否离地
擦着路侧物开过去时突然减速 本车道门限过宽或路侧物离车道太近。检查 LANE_HALF_WIDTH_M(0.8 m)与 roadside_object 的 y(1.8 m),可用探针打印刹停瞬间的射线表确认命中束与横向偏移

9. 参考

  • MuJoCo 文档:射线求交 mj_ray、几何体分组 geom group
  • 课程仓库已有作业:src/water/rov_mujoco(MuJoCo + ROS 2 的水下机器人示例)
  • 本模块对照作业:docs/ground/carla_yolo_avoidance.md(CARLA + YOLOv8 目标检测与减速避障)

辅助工具说明

编写过程中使用 DeepSeek 辅助代码与文档起草,全部内容已在 Ubuntu 22.04 + MuJoCo 环境中实测验证,提交者对内容负责。