第一次打开 CommonRoad 的场景文件时我盯着那一堆 XML 标签和 lanelet 的左右边界顶点序列看了快半小时也没搞明白它跟普通的车道线地图有什么区别。那时我刚把自己写的横向规划器在自制仿真里调通能跟着弯道走、能避让两个静止障碍物但当别人问“你这套东西放到公开场景上能跑成什么样”的时候我拿不出任何可对比的结果。后来才找到 CommonRoad——它给的正是这样一套东西一个统一的场景描述格式、一个公开的场景库以及一圈围绕“运动规划”搭起来的检查工具。它不渲染传感器、不模拟物理引擎只负责把道路结构、障碍物和规划任务讲清楚让你的规划器有标准的输入和输出。而安装就是第一道墙commonroad-io一行命令就装好了commonroad-drivability-checker却让我卡了整整两天。这篇就把两件事一次说透CommonRoad 是什么、每个包各自管什么以及从建环境、装包、排查编译错误到三步验证跑通的完整过程。做运动规划、控制算法、自动驾驶课程作业或者只是想要一个标准环境来检验自己 planner 的人都可以照着走一遍。1. CommonRoad 的定位它是一套场景格式加一圈检查工具很多人第一次听到 CommonRoad 会以为它是个仿真器装上以后发现既没有车辆模型的可视化油门刹车也没有激光雷达点云于是觉得“好像没什么用”。这是最容易走偏的第一步。CommonRoad 的核心其实是一份基于 lanelet 的 XML 场景描述规范道路被切成一段段 lanelet每段 lanelet 有左边界、右边界和中心线前后通过 predecessor / successor 串接左右通过 adjacent 表示相邻车道场景里再挂上静态障碍物、动态障碍物及其预测占用以及一个或多个“规划问题”——也就是初始状态加目标区域。规划器要做的事就是在这个标准输入上算出一条从初始状态满足车辆动力学约束、且不撞到任何障碍物的轨迹。1.1 为什么用 lanelet 而不是路点序列路点序列waypoint描述道路本质上是“一条折线 一些属性”它没法直接回答“我在第几条车道上”“我左边是不是还有一条车道”“前面这个路口有几条进口道”这类问题。lanelet 相当于把道路切成车道级的基本块块与块之间的拓扑关系是显式存储的所以你可以很自然地问出“从当前 lanelet 出发经过五次后继跳转能到达哪些 lanelet”也就是参考路线搜索的基础。我第一次用lanelet_network.find_lanelet_by_position()反查某点落在哪条车道时才真正意识到这种结构的好处位置查询、路线搜索、变道判断全都不用自己重新造轮子。1.2 整条工具链里每个包负责什么社区把功能拆成了若干独立包装的时候不用全装按需取用即可。下面这张表是我按“实际会用到”的顺序整理的标注了必需程度包名主要作用建议commonroad-io场景读写、lanelet 网络查询、几何运算、可视化必需commonroad-drivability-checker轨迹可行性车辆动力学与碰撞检查的 C 内核强烈建议commonroad-vehicle-models常见车辆动力学模型与参数KS、PM 等常用commonroad-route-planner从 lanelet 网络生成参考路线、计算曲线坐标常用commonroad-collision-checker面向规划问题的碰撞检查封装按需commonroad-scenario-designer场景编辑 GUI以及 SUMO / OpenDRIVE / Lanelet2 转换按需commonroad-reactive-planner官方参考实现的自反应式采样规划器学习首选commonroad-reach可达集与安全边界计算进阶commonroad-rl把场景封装成强化学习环境进阶需要说明的一点是这套工具链最有价值的不是某个单点算法而是“统一输入 可脚本化评价”。场景 ID 是唯一的初始状态、目标区域、障碍物预测占用都写在文件里所以同一个场景上不同人跑的轨迹可以直接用同一份检查脚本比较“是否可行”“是否碰撞”“舒适度如何”。这就是我当初缺的东西——不是算法不够好而是没有共同的尺子。2. 动手之前先定三件事解释器、依赖边界、要不要碰编译装 Python 生态的工具最怕的就是“边装边想”。CommonRoad 这条链上有一个 C 扩展包drivability-checker一旦踩到编译路径问题会从“pip 装包”升级成“配 C 构建环境”难度完全不同。所以我在动手前会先把三个决定做完后面几乎不会再返工。2.1 Python 版本尽量压在 3.8 到 3.10这不是守旧而是预编译轮子wheel的覆盖范围决定的。C 扩展包要针对每个 Python 小版本单独出轮子版本越新官方和社区的轮子越可能还没跟上pip 就会自动回退到“下载源码自己编译”。而源码编译要面对 CMake、Boost、Eigen 这些额外依赖失败率大幅上升。我的做法是新建环境时明确写死python3.10不去追最新的解释器版本。如果你已经在 3.12 上装之前先用pip debug --verbose看一下当前环境支持的 wheel 标签列表再决定要不要为了这个项目单独开一个 3.10 的环境——多开一个环境几乎零成本硬扛编译才是真的浪费一天。2.2 conda 建环境、pip 装包不要混着来纯 venv 也能用但一旦需要 Eigen、Boost 这类非 Python 依赖venv 就无能为力了只能靠系统包管理器。conda 的好处是这些依赖也能在同一个环境里装不会污染系统目录。我一般是这样分工的环境用 conda或 mamba创建Python 包一律用 pip 装。原因是 commonroad 系列包在 pip 上的版本更新更及时如果混用 conda 渠道安装很容易出现同一环境里两个渠道各装一半、依赖求解打架的情况。切记不要一会儿conda install commonroad-io、一会儿pip install -U commonroad-io这是最容易把环境搞坏的用法。2.3 三条安装路线按代价从低到高排路线一纯轮子安装。解释器版本合适、平台是主流 Linux 或 macOS几乎所有包都能直接装到预编译版本全程不需要编译器。这是最理想的路径也应该是你默认尝试的路径。路线二源码编译。只有当 pip 找不到匹配轮子、或者你需要改 C 内核的源码时才会走这条。代价是要补系统级依赖编译时间从几分钟到十几分钟不等内存吃紧的机器还可能被 OOM Killer 干掉。路线三换平台。如果你在 Windows 上C 扩展的编译体验通常最差。这时候与其跟编译器较劲不如换到 Linux 环境里做——无论是本地装一个 Linux、开一个虚拟机还是用容器起一个干净的 Linux 环境配好一次就能长期复用。我自己的经验是在这类以 C 内核为底的科研工具链上纠结 Windows 原生编译性价比极低。3. 从零到 import 成功完整命令流与项目骨架决定做完就可以动手了。这一节给的是可以直接复制执行的完整流程顺序不是随便排的——先核心包、后扩展包能让 pip 的依赖解析少很多麻烦因为扩展包内部依赖核心包的数据结构先装核心包相当于把它固定住后续装扩展包时就不会被牵着换版本。3.1 建环境并把基础工具链升到位conda create -n cr python3.10 -y conda activate cr python -m pip install -U pip setuptools wheel单独升pip / setuptools / wheel这一步很多人会跳过但装 C 扩展时构建后端版本过旧是常见的失败原因之一尤其是setuptools太老导致新的构建规范不被支持。另外注意用python -m pip而不是裸pip这样能确保 pip 一定是当前激活环境里的那个避免装到别处去。3.2 按顺序安装核心包# 核心场景读写、lanelet 网络、可视化 pip install commonroad-io # 动力学与碰撞的 C 内核这一步最容易出问题见第 4 节 pip install commonroad-drivability-checker # 常用配套 pip install commonroad-vehicle-models commonroad-route-planner # 学习阶段强烈建议装上可以直接看官方参考规划器怎么跑 pip install commonroad-reactive-planner装完立刻做一次最小验证别攒到最后一起试import commonroad print(commonroad-io:, commonroad.__version__) import commonroad_dc import commonroad_dc.pycrcc as pycrcc print(pycrcc loaded:, pycrcc.__name__) import commonroad_route_planner print(route planner ok)三个 import 都不报错说明最麻烦的部分已经过去了。如果commonroad_dc报的是undefined symbol或者 numpy 相关的 ABI 错误直接跳到第 4 节,那里有完整的排查链路。如果只是下载慢可以临时指定一个国内镜像源加速装完记得切回来避免长期用一个可能同步延迟的源。3.3 目录骨架与依赖冻结我习惯给每个 CommonRoad 相关项目建一个固定骨架用久了会省很多事cr-demo/ ├── data/ │ └── scenarios/ # 放下载好的 .xml 场景文件名就是场景 ID ├── scripts/ │ ├── inspect_scenario.py # 读场景、打印关键信息 │ └── batch_run.py # 批量跑规划实验 ├── outputs/ # 图片、日志、结果表 └── requirements.txt场景文件按“场景 ID 原样命名”这条规矩很重要因为 CommonRoad 的场景 ID 本身就编码了地图、编号等信息你一旦自己改成test1.xml、my_map_2.xml后面做批量统计时就得额外维护一张映射表非常容易出错。装完环境后立刻执行pip freeze requirements.txt同时把 conda 环境也导出成一份environment.yml半年后你想复现实验pip install -r能不能成功完全取决于今天有没有冻结这份清单。这是我踩过最多次的坑——同一份代码不同时间装出来的环境跑出不同结果找了两天才发现是依赖版本漂移。4. 装不上 drivability-checker 时我是怎么一层层排查的前面所有内容都顺利的话你可以直接跳到第 5 节。但把这篇写下来主要就是因为这一个包最容易出问题它不是一个纯 Python 包而是一个带 Python 绑定的 C 库装的过程同时牵涉“有没有匹配轮子”“有没有 C 编译器”“有没有数学库”“构建并行度会不会把内存打爆”这四件事。下面是我实际用过、按顺序执行的排查链路而不是一句笼统的“装一下依赖”。4.1 第一步永远是判断没轮子还是编译失败这两者的报错长得很像但处理方式完全不同。先用两条命令定位python -V pip debug --verbose | head -n 40第二条会输出当前解释器兼容的 wheel 标签形如cp310-cp310-manylinux_x86_64。然后尝试只下载不安装观察它的行为pip download commonroad-drivability-checker --no-deps -d /tmp/cr_probe如果下载到的是.whl文件说明有现成轮子问题可能出在安装环节的依赖冲突如果 pip 开始提示准备构建、拉取源码、执行cmake那就明确进入了源码编译路径需要按下面的步骤补依赖。这个判断很重要很多人一看到报错就去apt install一堆库结果本来只是版本冲突。4.2 补系统级依赖Linux 环境走编译路径时缺的是四样东西C 编译器、CMake、Eigen3、Boost。系统包管理器路线sudo apt-get update sudo apt-get install -y build-essential cmake libeigen3-dev libboost-all-dev python3-dev如果你的环境是 conda 主导的也可以在环境内装避免动系统目录conda install -c conda-forge cmake eigen boost-cpp pybind11 -y # 让 CMake 优先在这个环境里找依赖 export CMAKE_PREFIX_PATH$CONDA_PREFIX我倾向于第二种因为把依赖装在项目环境里卸载环境时一起清掉不会给系统留一堆编译用的库。补完之后重新执行pip install commonroad-drivability-checker观察 CMake 配置阶段是否顺利通过。4.3 编译期的三个高频故障与处理现象根因处理方式编译过程突然被杀死没有明确报错C 模板展开吃内存并行编译又开了多个进程export CMAKE_BUILD_PARALLEL_LEVEL2降并行度后重试找不到 Eigen3 / Boost 头文件CMake 搜索路径没覆盖到 conda 环境设置CMAKE_PREFIX_PATH或显式传 include 目录pybind11 相关报错绑定层依赖的 pybind11 缺失或版本过旧在同一环境里装一份 pybind11 再重试装完 import 报undefined symbol编译时链接的 numpy C-API 与运行时的 numpy 版本不一致不要再升 numpy重建环境并锁定版本最后一条我要多说一句因为它是隐性最强的一个坑。C 扩展在编译时就绑定了 numpy 的 C 接口如果装完 CommonRoad 之后你随手pip install -U numpy把版本从 1.x 升到 2.ximport 时就会直接失败而且报错信息完全看不出跟 numpy 有关。所以我的规矩是环境跑通之后这个环境里的 numpy 不再动。同理后来补装其他 commonroad 包时也要小心它可能顺手把 numpy 或 pybind11 一起升级装完最好执行一次pip check看有没有破坏依赖关系。4.4 实在编译不过的兜底方案科研和工程项目里我不建议为一个依赖卡三天。如果编译确实过不去完全可以只用commonroad-io然后自己做两件替代工作用运动学单轨模型做一遍轨迹积分检查加速度、前轮转角、曲率是否在物理范围内这相当于一个简化版的可行性检查碰撞检查则用 shapely 做多边形相交判断commonroad-io的几何对象本身就能转成 shapely 的形状实现成本很低。这套兜底方案精度不如 C 内核但足够支撑“跑通流程、做定性对比、写论文初稿”这些目标等环境问题解决了再换回官方检查器代码结构上只需要替换一个检查函数。5. 装完别急着写算法读、画、查三步验证环境装好之后最忌讳的就是直接开始写规划器。我见过太多次“算法调了两天最后发现场景根本没读对”。所以我的固定动作是三步先把场景读进内存并打印关键信息再画出来用眼睛确认最后拿一条简单轨迹走一遍检查流程。5.1 读进内存先看几个数字对不对from commonroad.common.file_reader import CommonRoadFileReader path data/scenarios/DEU_Example-1.xml scenario, planning_problem_set CommonRoadFileReader(path).open() print(dt(秒):, scenario.dt) print(lanelet 数量:, len(scenario.lanelet_network.lanelets)) print(静态障碍物:, len(scenario.static_obstacles)) print(动态障碍物:, len(scenario.dynamic_obstacles)) print(规划问题 ID:, list(planning_problem_set.planning_problem_dict.keys()))CommonRoadFileReader.open()返回的是两个对象场景本身和规划问题集合这两者要分开管理。这里最值得盯的是scenario.dtCommonRoad 用的是米和秒的国际单位常见时间步长是 0.1 秒也就是 10 赫兹但不同场景不一定一样。如果你把它当成固定的 0.1 秒来算速度遇到别的 dt 就会整体偏差而且偏差不会报错只会让结果悄悄不对。5.2 画出来看顺便确认坐标系方向import matplotlib matplotlib.use(Agg) # 无显示器环境必须设置否则可能卡住 import matplotlib.pyplot as plt from commonroad.visualization.mp_renderer import MPRenderer rnd MPRenderer(figsize(18, 9)) scenario.draw(rnd) planning_problem_set.draw(rnd) rnd.render() # 这一步不能省否则画布是空的 plt.savefig(outputs/scenario.png, dpi150, bbox_inchestight)如果你在服务器上跑不设置Agg后端matplotlib 可能去尝试连接显示设备轻则报错重则卡住这是无头环境里非常常见的一个坑。另外rnd.render()一定要调MPRenderer是延迟渲染的设计draw()只是把对象挂上去忘了 render 就会得到一张空白图。第一次画出来的图我建议仔细看两眼车道线是不是闭合的、左右边界有没有画反、目标区域是不是在你预期的位置。我确实遇到过因为左右边界顶点顺序搞反而画出镜像地图的情况肉眼一眼就能看出来。5.3 取一个规划问题走一遍检查流程pp_id list(planning_problem_set.planning_problem_dict.keys())[0] pp planning_problem_set.planning_problem_dict[pp_id] print(初始位置:, pp.initial_state.position) print(初始速度:, pp.initial_state.velocity) print(初始朝向:, pp.initial_state.orientation) print(目标区域类型:, pp.goal_region.shape_type)规划问题的结构很简单一个初始状态位置、朝向、速度、加速度、横摆角速度加一个目标区域。目标区域可能是圆形也可能是矩形所以别写死.radius先看shape_type再取对应属性。接下来做检查时如果你不确定 C 内核暴露出来的类名和参数名不同版本确实调整过用一句反射就能看到全部import commonroad_dc.feasibility.vehicle_dynamics as vd print([n for n in dir(vd) if n[0].isupper()])这比翻文档快得多也是我在版本不稳定期的常规动作。同理检查器本身的入口也可以这样确认把dir()的结果对着你的需求找通常就能对上。可行性检查的输入是一条带时间步的状态序列输出是“是否可行 不可行的原因”而碰撞检查的输入是轨迹加障碍物集合两者通常是配合使用的先确认轨迹物理上开得出来再确认开出去不会撞。提示初次跑检查时故意拿一条明显不合理的轨迹比如恒定速度直冲障碍物去跑一遍确认检查器真的能报出“不可行”和“碰撞”。只验证“正常情况返回通过”是没法证明检查器接对了的。6. 读懂场景文件lanelet 网络、规划问题与坐标系跑通流程之后真正决定你能不能用好 CommonRoad 的是对场景数据结构的理解。这块理解了后面写规划器、做指标统计、做场景筛选都会顺不理解的话就会一直在“怎么又取不到车道”的状态里打转。6.1 lanelet 网络的三种关系打开一个场景 XML你会看到laneletNetwork下面挂着 lanelet、交通标志、交通灯、停止线等。lanelet 本身是“左边界顶点序列 右边界顶点序列 中心线”而它们之间的关系有三类最关键前后关系predecessor / successor表示沿车道方向的前后接续、相邻关系adjacent left / right表示同一方向或对向的并排车道、包含关系哪些 lanelet 属于同一个路口区域。参考路线搜索就是在这张有向图上做路径规划做车道级决策时判断“能不能变道”也是基于相邻关系。我建议拿一个含路口的小场景手动遍历一遍这几类关系把打印结果和地图对照着看比读十页文档都管用。6.2 规划问题初始状态加目标区域一个场景可以带多个规划问题每个问题有自己的 ID 和障碍物子集这就是为什么同一个地图上能衍生出很多道“题”。做批量实验时正确的做法是外层遍历场景、内层遍历规划问题而不是一个场景只跑第一个问题。目标区域有时给的是“到达某个位置附近”有时给的是“进入某个车道”前者对应几何区域后者对应 lanelet 集合两种目标在规划器里的处理方式不一样写代码前先确认清楚你拿到的是哪一种。6.3 笛卡尔坐标与曲线坐标什么时候用哪个Cartesian 坐标适合画图、做定位匹配、处理全局路径但一旦进入纵向和横向的分解——比如判断跟车距离、算横向偏移、做变道轨迹生成——曲线坐标沿参考线的弧长 s 加上横向偏移 t要顺手得多。因为曲线坐标天然把“沿路走多远”和“偏离中心线多少”拆开了纵向 PID、横向五次多项式这类算法可以直接在 s-t 平面上工作不用每次都在全局坐标里做投影。参考线的选取也有讲究一般用参考路线的中心线参考线质量直接决定曲线坐标的稳定性如果参考线本身有折角或者跳变投影就会出现不连续进而导致横向偏移值剧烈抖动。这是我调试时花时间最多的地方比规划算法本身还费劲。6.4 时间索引与障碍物占用动态障碍物的预测通常以两种形式给一是按时间步的状态序列二是每个时间步的占用多边形集合。前者适合做轨迹级预测和意图分析后者适合做时空上的碰撞检查——因为它是“这个物体在这个时刻占据了这块区域”可以直接和你的候选轨迹做空间求交。这里有一个非常容易错的地方时间步索引从 0 开始第 t 个状态对应的时刻是 t 乘以 dt不是 t 秒。我有一次把索引当秒用结果所有的时间对齐检查都偏了 10 倍但因为轨迹本身还是形状正常的肉眼完全看不出来只有碰撞检查偶尔报出莫名的冲突才露了馅。注意如果你要自己解析场景 ID 字符串先看看库里有没有现成的解析类。场景 ID 的编码规则包含地图名、编号、规划问题标识等多段信息自己写字符串分割在遇到特殊命名的场景时很容易出错用官方提供的类去解析更稳妥。7. 把自己手上的地图转进来三条转换路径场景库里没有你想测的路口和路段结构是常态尤其是做工程落地的时候。这时候就需要把已有的地图数据转成 CommonRoad 格式常见的输入有三种难度和损耗各不相同。7.1 三种输入格式的取舍SUMO 的net文件转换通常最顺因为 SUMO 本身就是车道级的路网模型车道拓扑和连接关系都比较完整转换后 lanelet 的连接性一般不会缺。OpenDRIVE 的xodr是工业界常见格式道路几何描述精细但它的车道模型和 lanelet 的粒度不完全对等转换后需要重点检查车道连接和路口区域。Lanelet2 的 OSM 文件在概念上和 CommonRoad 最接近都是车道级单元转换损耗最小如果你手上有 Lanelet2 地图优先走这条路径。7.2 GUI 优先脚本其次场景设计器自带图形界面能导入上面几种格式并做可视化编辑转换后可以直接在界面上核对车道连接、删除多余元素、补上缺失的交通标志。我的建议是第一次转换走 GUI打开设计器通过导入菜单选择对应格式的文件转换完成后用它的编辑视图逐段检查。等确认转换流程稳定、需要批量处理几十个地图时再去脚本化调用map_conversion子包里的转换接口。这个子包的模块结构在不同版本间有过调整用之前先列一下模块名确认入口在哪儿比直接照抄旧教程靠谱。7.3 转换完必须做的三项校验转换工具不会替你保证数据合理下面三项是我每次转换后必查的连通性检查。从起点出发做一次前向搜索看能否到达目标区域所在的 lanelet。如果搜不到说明某些 lanelet 的前后关系断了通常是路口区域转换时丢连接。几何检查。抽查若干 lanelet 的左右边界顶点数量与长度看有没有零长度的车道、自交的边界、或者左右边界交叉的情况。这类问题在可视化时表现为车道线“拧成一团”。规划问题检查。转换出来的地图往往需要你自己补一个或多个规划问题初始状态加目标区域确认初始状态确实落在车道内、朝向沿着车道方向目标区域可达且不在障碍物内部。这三项做完你手上才有一个“可以拿来跑算法”的场景而不仅仅是一个“看起来能打开”的文件。我在这方面吃过亏地图能画出来、看着挺正常结果规划器跑出来总是绕远路查了半天发现是路口处两条 lanelet 的连接方向写反了只看图完全看不出来。8. 只有真跑过才会知道的几个细节最后这部分是纯粹的实操经验都是文档里不太会写、但每天用都会碰到的东西。解析一个场景通常要几秒到十几秒做参数扫描的时候这个开销会迅速累积到无法忍受。我的做法是把解析后的场景对象缓存下来做同一场景的多组参数实验时只解析一次后面的实验直接复用。绝大多数场景对象支持直接序列化存储如果遇到某个对象序列化失败退一步缓存原始文件路径加解析参数用一层字典做进程内缓存也能解决大半问题。配合这个技巧一个几十组参数的扫描从十几分钟压到一两分钟是很常见的。批量跑实验时用进程池而不是线程池。原因在于底层有 C 扩展线程模型下的 GIL 行为和释放时机不受你控制表面上写了多线程实际可能还是串行甚至因为资源竞争而更慢。改成多进程后每个进程独立持有一份场景数据和检查器实例效率提升非常明显。唯一要注意的是内存场景库大的时候进程数不要开太多进程数 物理核心数 - 1是我常用的起点并且每个进程里只缓存它自己真正要用的那几个场景不要图省事把整库读进内存。还有一个容易被忽略的点是绘图性能。默认的绘制参数会把车道线上的标注、交通标志细节全部画出来场景一大就慢得离谱甚至画到一半内存告急。做批量出图时我会显式传一个精简的绘制参数关掉标签和细碎元素只保留车道线、障碍物和轨迹。出图速度提升之后你才愿意多看几张图而多看几张图往往就能发现数值指标发现不了的问题。至于环境本身我现在的固定做法是环境一旦跑通立刻导出依赖清单并存进项目仓库同时在 README 里写清楚“这个项目验证过的解释器版本和关键包版本”。CommonRoad 生态更新比较活跃隔几个月再装一次装出来的东西大概率跟今天不一样能不能复现上次的实验取决于你有没有留下这份记录。这个习惯看起来跟算法毫无关系但它确实帮我省下过好几次“结果对不上、重跑一遍还是对不上”的痛苦排查。