做自动驾驶仿真的人,一半的时间其实不是花在调算法上,而是花在和环境较劲上。尤其是当你发现CARLA自带的Town01到Town10不够用,想复现一个特定的园区、停车场或者高速匝道场景时,就得自己上手改地图、造地图。这个系列我准备把CARLA二次开发的一些实战经验整理出来,第一篇先聊最基础也最绕不开的一件事——自定义创建地图。
这篇文章会完整走一遍从选型、备料、建模、导入到验证的自定义地图全流程,重点讲清楚CARLA地图的文件结构、OpenDRIVE路网和三维模型是怎么配合工作的,以及新手最容易翻车的几个点。如果你正准备在CARLA里复现自家测试场的布局,或者想让仿真场景更贴合实际业务逻辑,这篇文章应该能帮你少走不少弯路。
1. 自定义地图开发的整体思路
1.1 先搞清楚CARLA地图的本质
自定义地图之前,必须先理解CARLA里一张地图到底由什么组成。很多人一开始会以为地图就是一张贴图或者一个3D模型文件,放进场景就能用,这个理解是错的。
CARLA的地图是典型的“逻辑路网+物理世界”双层结构。逻辑层是OpenDRIVE格式的路网描述文件(xodr后缀),里面定义了道路的几何形状、车道数量、车道宽度、车道类型、限速信息、交叉口连接关系、信号灯位置等一系列自动驾驶逻辑需要的数据。物理层则是用3D建模软件生成的视觉和碰撞模型,包含路面Mesh、建筑物、树木、路灯、路牌、护栏以及对应的碰撞体和材质。
这两层是分离但又必须严格对齐的。车辆在仿真空旷场景里跑的时候,规划控制模块读取的是OpenDRIVE里的车道路径点,而不是看着3D模型跑。所以你可以建一个视觉上非常漂亮的路面,但如果xodr里没有这条路,车辆就无法在上面行驶。反过来,xodr里有路,但物理层没做路面Mesh,车辆就会悬空掉下去。
这个逻辑想通之后,后面所有的问题都好理解了。所谓的自定义创建地图,本质上就是两件事:第一,产出正确的OpenDRIVE路网文件;第二,产出和路网严格对齐的三维模型文件。两个文件需要经过CARLA提供的工具处理,才能变成引擎能识别的地图包。
1.2 四条主流建图路线怎么选
明确了双层结构,接下来就是干活路径的问题。我试过几种常见方式,各有优劣,直接给你结论。
第一条路是直接用RoadRunner建图。这是目前最主流、最推荐的方式。RoadRunner是MathWorks家的产品,能够比较方便地画道路、设车道、建交叉口,而且能直接导出OpenDRIVE和FBX模型,连材质和碰撞体都帮你生成好。CARLA官方文档里推荐的工作流就是RoadRunner导出再导入。
第二条路是用Blender手动建模,然后用代码或者插件生成路网。这种方式自由度最高,能做出完全自定义的场景,比如复杂的立交桥、创意园区、室内停车场。代价是工作量大到离谱,光是把几百米的路面网格贴合到xodr路网对齐,就够你调一两个星期。
第三条路是直接用代码批量生成路网。CARLA的Python API里其实可以动态创建路网,用carla.Map的API来构造道路和车道。这种方式适合快速生成简单的测试路网,比如一条笔直的高速路做AEB测试,但对复杂交叉口的支持比较弱,很多细节控制不了。
第四条路是改造自带地图或者社区共享地图。直接拿Town05的xodr文件,改路网拓扑,再重新导入。这种方式看起来省事,但改OpenDRIVE文件的手工工作量极大,而且坐标对齐问题会很折磨人。
我个人的建议是:如果预算和条件允许,直接上RoadRunner;如果只是想做简单的逻辑测试不想动3D,就用Python动态生成路网;非要复现复杂的定制场景,再考虑Blender手工建模这条路。
2. 工具选型与环境准备
2.1 需要的组件清单
开工之前,先把工具链备齐,免得做到一半发现缺少关键组件又停下来查环境。我按重要程度列一个清单。
第一个是CARLA本体,建议直接用最新稳定版本,我这边用的是0.9.15版本,更高版本也是向下兼容的。第二个是RoadRunner,这个工具不是免费的,但官方有试用期,对付一两个项目问题不大。第三个是RoadRunner的CARLA插件,在MathWorks官网的File Exchange里可以下载,是用来让RoadRunner导出CARLA专用格式的。第四个是Blender,主要用于处理RoadRunner导出的FBX文件,做一些材质补充和模型调整。第五个是地图导入工具,也就是CARLA自带的carla-maps相关脚本,一般在/PythonAPI/util目录下。
这几个组件的版本兼容性要注意一下。RoadRunner的版本不能太老,否则导出的OpenDRIVE版本可能与CARLA不匹配。我踩过一次坑,用旧版RoadRunner导出xodr,CARLA直接无法解析。后来统一更新到较新版本,并且导出时确认选的是OpenDRIVE 1.4版本,才解决。
2.2 开发环境配置细节
环境配置这块,我踩过一些坑,给你几个关键点。
Python这块,CARLA 0.9.x版本对Python版本要求比较严格,0.9.15版本支持Python 3.8,新版0.9.16用Python 3.10也行。建议用Anaconda建一个独立环境,然后把CARLA的Python API路径加进去,也就是/PythonAPI/carla/dist/目录下的.egg文件,用pip install即可。特别是Windows系统下,这个路径问题很多人搞不定,我建议直接把egg文件路径加到.pth文件里,避免每次都在代码开头去手动添加环境变量。
存放路径方面,这里有个很重要的点:CARLA对非ASCII字符支持很差。我曾经把项目放在D:\自动驾驶\目录下,结果RoadRunner导出的地图导入CARLA时疯狂报错,字符编码乱掉,排查了很久才想到是路径中文的问题。后来把所有工具和项目切换到纯英文路径,一切正常。做CARLA开发,所有路径都建议用纯英文、无空格。
另外,显卡驱动和DirectX版本也要确认一下,CARLA基于Unreal Engine 4,对显卡要求不低。如果启动CARLA时出现渲染异常或者直接崩溃,多半不是代码问题,而是显卡驱动太老。我建议先把显卡驱动更新到最新再开始干活。
3. 实操过程:从RoadRunner到CARLA
3.1 在RoadRunner中搭建基础路网
装好环境以后,我们正式进入建图流程。打开RoadRunner,新建一个Scene,然后开始画路网。
RoadRunner的操作逻辑和画图软件类似。左侧工具栏里有道路工具,直接在场景上点击拖拽就能拉出道路。拉到差不多的长度和走向后,右侧属性面板里可以精确设置道路的曲率、宽度和车道配置。这个阶段不用太纠结模型的精细度,先把道路拓扑和车道逻辑定下来是关键。
我以一个简单的双向四车道路段加十字交叉口为例。先画一条水平道路,再画一条垂直道路,使用交叉口工具在交汇处创建Intersection。RoadRunner会自动生成交叉口内部的连接车道和冲突区域,这个功能非常实用。
画交叉口的时候有几点要注意。第一,交叉口的每条连接道路的车道数最好保持一致,否则RoadRunner生成的连接逻辑容易错乱。第二,交叉口内部的转向车道(左转、右转、直行)如果不能自动生成,得手动检查连接关系,确保每条车道都有合理的起点和终点。第三,车道宽度要根据实际场景来,城市道路一般3.5米到3.75米,人行横道、停止线这些细节可以在RoadRunner里标记,CARLA导入后会自动识别成车道标记。
拉完道路后,建议去右侧World面板里仔细检查一下整个道路网络的联通性。RoadRunner有Validate功能,会在有断头路或者车道连接错误时提示。做自动驾驶仿真,路网拓扑的闭合性非常重要,一根断头路会让车辆在路径规划时直接卡死或者找不到路由。
3.2 导出前必须检查的几件事
RoadRunner建模完成后,导出是一个关键节点,导出前的检查项直接决定了后面导入CARLA的成败。
首先是坐标原点。RoadRunner场景的默认原点是世界坐标(0,0,0),但如果你在建模过程中移动过场景,或者导入过别的参考底图,原点可能已经偏移。导出时在Export面板里设置Target Scale和Origin都很重要。CARLA内部使用的坐标单位是厘米,而OpenDRIVE的标准单位是米。RoadRunner导出CARLA格式时,会有一个单位换算选项,在这个环节要确认选择的是CARLA专用配置,否则导进去的地图尺寸全错。
其次是车道连接关系。导出之前把每条道路都点一遍,检查车道宽度、车道方向是否正确。双向车道如果方向反了,车开进去就会逆行,这个问题特别隐蔽,因为视觉上看起来道路是正常的。
然后是信号灯。RoadRunner里可以通过Traffic Signal工具在交叉口放置信号灯组,每个信号灯组对应一组红绿灯状态。如果交叉口比较密集,建议把信号灯组的相位逻辑设置好。CARLA导入后虽然能识别信号灯的位置和类型,但是信号灯的相位配置(红灯、绿灯、黄灯的时序)可能需要在后续的Python脚本里细调。
最后,还有一个容易忽略的点——路缘石和车道边界的碰撞体。RoadRunner导出的时候会自动生成路面的碰撞体,但路缘石的碰撞体有时候不会自动带上。如果在仿真里车辆能直接碾着路边压过去,那就是这个碰撞体缺失了。检查方法是在RoadRunner的3D视图中看路面边缘的物理边界是否可见。
3.3 导入CARLA的完整步骤
RoadRunner里规划好一切之后,点击Export按钮,选择CARLA插件导出。正常导出后会得到一个文件夹,里面包含若干个关键文件:Xodr格式的OpenDRIVE文件、一组FBX格式的三维模型文件(通常按区域或地块拆分成多个)、一个package.json的元信息文件。
拿到这些文件后,我们需要把它们放到CARLA指定位置。CARLA的安装目录下有一个Content/Carla/Maps文件夹,所有自定义地图都会把文件放这里。我把导出的完整文件夹拷进去,命名为MyTown,然后做几项关键配置。
首先是地图的注册文件AdditionalMaps.json。在CARLA根目录的Configuration文件夹下可以找到这个文件。用记事本打开,里面是一个JSON数组,每一条对应一张可以通过CARLA启动参数加载的地图配置。我添加了一条:
{ "name": "MyTown", "path": "Carla/Maps/MyTown", "supported\_map\_themes": ["Countryside"] }这里name是启动CARLA后要传入的地图名称,path是相对于Content目录的路径。
然后是启动CARLA,直接用--map参数指定:在命令行里进入CARLA根目录,运行:
./CarlaUE4.sh -carla-server -carla-rpc-port=2000 -quality-level=Low --map=MyTown如果不出意外,引擎会加载场景,并且在终端里输出地图中道路、交叉口的统计信息。
这里我要特别强调一下,CARLA对FBX文件命名有严格约定。RoadRunner导出的模型通常以map_dynamic、map_static等前缀命名,这是引擎内部用来区分可交互对象和静态对象的。如果改了名字,引擎很可能把这些模型当成普通静态Actor处理,信号灯、交通标志等动态元素就会全部失效。
3.4 用Python脚本验证地图是否可用
地图导入后,你需要用Python API去验证这张地图能不能被正确识别和驾驶。我常用的验证步骤有这么几步。
第一步,确认地图能正常切换。在Python环境里运行:
import carla client = carla.Client('localhost', 2000) world = client.load_world('MyTown') print(world.get_map().name)如果输出MyTown,说明地图注册成功。
第二步,检查路网数据是否完整。加载地图后,遍历车道和道路信息,统计一下道路数量、车道数量和交叉口数量:
map = world.get_map() topology = map.get_topology() print(f"Total roads: {len(topology)}") waypoint_list = map.generate_waypoints(2.0) print(f"Waypoints: {len(waypoint_list)}")如果这里输出的waypoint为零,大概率是xodr文件没有解析成功或者路网数据异常。
第三步,做一次实车冒烟测试。生成一辆车放在地图上,让自动驾驶模式开启自适应巡航(真不行就手动控制开一段),看看车辆能不能正常沿着车道行驶。这一步能直观的暴露车道连接不符合预期、车道宽度太窄车辆跑偏、交叉口逻辑错乱等问题。
第四步,检查信号灯和路牌是否能被识别。在地图上生成几个关键位置的Actor,用world.get_actor().get_traffic_light()查看信号灯状态是否正常切换。
这几步走完,你的自定义地图就算基本能用了。接下来就是漫长的细节调整阶段。我在实际项目中就发现,RoadRunner里看着非常规整的道路交叉口,导入到CARLA后会因为碰撞体、道路连接线的微小误差,导致车辆通过时会有明显的顿挫感。这种问题没有捷径,只能跟着数据走,逐段检查车道连接和道路几何。
4. 我踩过的坑:排查与避坑
4.1 地图加载黑屏或直接崩溃
这是新手最常遇到的情况,画了好几个小时路,结果导入CARLA后启动直接黑屏,或者干脆报错退出。排查优先级按下面这个顺序来。
第一,检查AdditionalMaps.json里面的路径是否写对了。路径要相对于Content目录,且文件夹名的大小写要和实际一致。我见过很多次因为路径大小写不一致导致找不到地图包,白白浪费半天时间。
第二,检查FBX文件是否完整。RoadRunner导出时有时候会漏文件,特别是当你改过场景但没重新导出全部内容时。确认FBX、xodr、package.json三个核心文件都在,且非零字节。
第三,检查是否有中文或特殊字符路径。这个我之前反复说过了,CARLA对路径的宽容度极低,务必用纯英文路径。
如果以上都确认无误,还是黑屏,试试把-quality-level降到Low。有时候是你的显卡扛不住高分辨率材质加载,但降级之后能正常加载说明地图数据本身没有大问题。
4.2 车辆掉出路面或悬浮在半空
这个问题的本质是物理层和逻辑层没有对齐。如果RoadRunner生成的Mesh和xodr路网没有严格贴合,车辆在仿真中按照xodr里的车道路径行驶,但视觉上或者物理上已经不在路面上。
典型的场景是:道路在RoadRunner里是单纯的平面,没有做路缘石下沉或者路肩处理,导致车道模型悬空。解决方法是返回RoadRunner,检查路面是否贴在地形上,路缘石区域是否和路面有重叠。
还有一种情况,是碰撞体设置错误。RoadRunner导出时,每个模型都会带一个碰撞体。如果碰撞体比路面小了那么几十厘米,车辆就会在特定位置穿过路面。这种问题应对方法是在RoadRunner里缩小碰撞体精度,或者手工在Blender里对Mesh重新生成碰撞体。这个过程有点繁琐,但做完之后整个场景会稳很多。
4.3 信号灯不工作或相位错乱
RoadRunner导出的信号灯,CARLA识别出来之后,初始状态可能是全红或者全绿。很多人在这一步会以为信号灯没导入成功,去反复重导模型。实际上大部分情况下是相位逻辑没有配置。
CARLA读取信号灯的分组和相位,主要靠OpenDRIVE文件里的group和phase信息。RoadRunner如果你只是放了信号灯,没有设置红灯、绿灯、黄灯的时序,CARLA导入后给的默认状态可能是空转。
遇到这种情况,简单粗暴的办法是写一个Python脚本,去手动控制信号灯的状态和切换逻辑。API里可以通过traffic_light.set_state(carla.TrafficLightState.Green)来强制设置信号灯状态。想做一个简单的定时切换,就写一个协程,定时设置红绿灯。这个方法虽然没有RoadRunner里精确到秒的相位控制那么精细,但90%的测试场景是够用的。
4.4 建筑物和树木位置偏移
RoadRunner通常会把来自GIS或者参考底图的数据转换成模型,但不同来源数据的坐标参考系经常对不齐。你有很大的概率会在导入CARLA后发现,房子在路中间,树挂在天上。
这个问题主要原因有两个。第一,RoadRunner场景里面插入静态模型时,模型的原点没有放在地面上,导致模型被举高或下沉。第二,FBX导出时模型的单位设置错误,导致整个静态模型组相对道路Mesh有一个系统性的偏移。
排查方法是在Blender里导入RoadRunner导出的FBX,和xodr路网文件做一次对比,看模型组和道路的坐标差异。如果差异是偏移,直接选中模型组整体平移。如果是缩放问题,检查单位设置(1单位=1厘米还是1单位=1米)。这个工作一定要在做实车测试之前完成,否则后期改起来会非常痛苦。
5. 进阶扩展思路
自定义地图的基础流程走通之后,你多半会开始想做更多定制化的事情。根据我自己的实践,有以下几个常见的方向。
动态路网是一个大方向。目前我们做的是静态路网,道路拓扑是固定死的。但在一些场景下,比如临时封路、道路施工、园区内部物流路线调整,你希望路网是动态变化的。CARLA目前对动态路网的支持有限,但可以通过在运行时加载多个地图包,然后切换来实现伪动态效果。我用这种方式在仿真中实现过一个多楼层停车场的电梯切换场景,效果尚可。
多地图切换也是实际项目中常遇到的。一套仿真系统往往需要覆盖多种测试场景,比如城区、高速、乡村,每张地图对应不同的物理环境。我的做法是在配置文件中维护一张地图列表,测试开始时根据测试用例动态load_world。但要注意,切换地图是有开销的,实测Town05切换到自定义地图,耗时约10到20秒,用例设计时要提前预留这个时间。
无限地图或者超大地图是另一些人的需求。CARLA的引擎底层对地图大小有一定限制,场景过大的时候Streaming Loading会跟不上导致模型闪现。Phase I的话,先用RoadRunner把道路拓扑画好,用简单模型替代复杂建筑,小范围验证完逻辑,再逐步增加地图面积和模型精度。不要一开始就想着做一个几百平方公里的全城地图,渲染和物理计算都会直接拖垮机器。
还有很多人关心的是如何把自定义地图分享给团队其他人使用。我的经验是,不要去分发整个CARLA安装包,只需要把Content/Carla/Maps/MyTown目录和AdditionalMaps.json的修改记录发给同事。他们在自己的CARLA安装目录里替换后,启动加--map=MyTown就能同步打开。这样可以大大减少团队的协作成本。
另外要提一句,CARLA的地图导入工具链一直在更新,我用的这套流程基于0.9.x版本,到了0.10或者后续版本,AdditionalMaps.json的配置方式可能会变化。建议在动手之前先看一眼官方文档,看看格式有没有调整。
最后再分享一个小技巧。RoadRunner里画路的时候,尽量给每段道路起一个稳定且唯一的名字。CARLA的Python API支持按Road ID和Lane ID查询路点,如果你在RoadRunner里就把路名规划好,比如Main_Road_01、Parking_Lot_A_02,后期写测试脚本时查找路点会高效得多。千万别用默认的未命名道路,等到脚本里对着坐标范围一个个找路点时,你会怀疑人生的。
我自己的体会是,自定义地图这事没有太多玄学,核心就是把OpenDRIVE逻辑层和三维模型物理层的对应关系吃透。很多人在这个环节被卡住,往往不是因为某个技术难到学不会,而是对这两层数据的组织方式理解不到位,导致出了问题不知道去哪排查。希望这篇分享能帮你把这个思路理清楚,后面做二次开发的时候,你会发现自己能折腾的玩法一下子多了很多。