
做自动驾驶仿真的人一半的时间其实不是花在调算法上而是花在和环境较劲上。尤其是当你发现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导出xodrCARLA直接无法解析。后来统一更新到较新版本并且导出时确认选的是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到CARLA3.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-port2000 -quality-levelLow --mapMyTown如果不出意外引擎会加载场景并且在终端里输出地图中道路、交叉口的统计信息。这里我要特别强调一下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(fTotal roads: {len(topology)}) waypoint_list map.generate_waypoints(2.0) print(fWaypoints: {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安装目录里替换后启动加--mapMyTown就能同步打开。这样可以大大减少团队的协作成本。另外要提一句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逻辑层和三维模型物理层的对应关系吃透。很多人在这个环节被卡住往往不是因为某个技术难到学不会而是对这两层数据的组织方式理解不到位导致出了问题不知道去哪排查。希望这篇分享能帮你把这个思路理清楚后面做二次开发的时候你会发现自己能折腾的玩法一下子多了很多。