第一次在Apollo的DreamView里看到来自Carla的车辆定位点在地图上稳定移动,传感器数据一条条刷出来的时候,我才意识到这套联合仿真终于算是跑通了。从环境搭建到桥接成功,中间踩了不知道多少坑,光是protobuf版本不一致就折腾了两天。今天把这套Python + Carla + Apollo联合仿真的完整实践过程整理出来,从环境踩坑到桥接原理,一路到跑通闭环控制,尽量把手伸不到的细节都讲到。这篇东西适合已经有基础的自动驾驶开发者,也适合刚入门想在仿真环境里做自动驾驶测试的在校学生,只要照着做,大概率能少走不少弯路。
1. 为什么要用Carla和Apollo做联合仿真:它们各自缺的那块拼图
1.1 先搞清楚两个家伙的分工
Carla是一个开源仿真器,核心优势在场景渲染、传感器模型和物理引擎,它给你的是一个足够真实的虚拟世界。在这个世界里可以放红绿灯、布行人、设天气,还能用Python脚本随心所欲地控制路上的每一辆车。但Carla自身没有完整的自动驾驶算法栈,它的一些内置autopilot仅仅是简单的路径跟踪,连正儿八经的感知、预测、规划、控制闭环都谈不上。
Apollo则是百度开源的自动驾驶平台,从高精地图、定位、感知、预测、规划到控制,模块分得很细,算法栈非常完整。但它需要数据输入,在实车上验证风险大、成本高,在没有环境的情况下想要调试算法可以说是寸步难行。
这两个正好互补。Carla负责出题,Apollo负责解题,Python就是连接出题人和解题人的对话通道。
1.2 为什么用Python做胶水层而不是C++
Carla官方提供的API以Python为主,PythonAPI可以直接通过TCP/RPC和Carla服务端通信,获取车辆状态、传感器数据、控制车辆等等。Apollo虽然是C++写的,但Cyber RT框架提供了Python回调接口,可以直接创建Reader订阅话题消息。两端都有Python接口,那桥接层最自然的选择就是Python。
不要小看这一层Python桥,它要做的远不止转发数据。Carla的坐标系和Apollo的坐标系不一样,消息类型不一样,频率也不一样,单位甚至都可能存在差异。桥接层做得不好,轻则数据对不上,重则Apollo算法直接崩溃。后面我会把这些细节逐一拆开讲。
1.3 什么时候不该上联合仿真
写这篇文章之前我得先把话放在前面:联合仿真不是银弹,有些场景完全不适合用它。
如果你只是验证车道保持算法逻辑,在Carla里用自带API写个纯Python控制循环就够了,完全没有必要把Apollo整个搬进来。反过来,如果只是学习Apollo的算法架构和模块调度,随便找个Cyber RT的录播数据包就能离线看结果,也不需要Carla提供的连续反馈。
真正值得上联合仿真的场景,是当你要做端到端的集成测试,例如验证感知模块对传感器噪声的鲁棒性、规划模块在复杂场景下的响应、或者控制模块在物理反馈下的表现。这时候系统之间的消息流转本身就是被测对象,Carla和Apollo的联动才有意义。
2. 环境准备与版本对齐:八成故障都发生在这一步
2.1 我踩过的版本坑与推荐组合
版本一致性是整个环节里最容易出问题的地方。Carla、Apollo、Python、protobuf,任何一个版本对不上,都可能让你在编译或运行时遇到莫名其妙的错误。
我自己用的组合是这样的:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| 操作系统 | Ubuntu 20.04 | Apollo需要特定的系统支持,实测兼容性最好 |
| Carla | 0.9.13 | 0.9.x系列的Python API比较稳定 |
| Apollo | 6.0及以后版本 | 6.0之后Cyber RT框架的Python接口更完善 |
| Python | 3.8 | Carla 0.9.13和Apollo容器内的Python版本能对上 |
| protobuf | 3.6.1及配套runtime | 这是编译Carla-Apollo桥最容易出问题的点 |
ProTip:装Carla之前先确认一下显卡驱动和NVIDIA的依赖库,特别是libcarla相关的动态库。另外Carla对显存的要求比想象中高,官方城市地图场景下2K分辨率至少得6GB显存,否则一加载地图帧率就掉到个位数。
2.2 Carla的PythonAPI依赖安装
Carla的PythonAPI在安装包里有现成的,路径是PythonAPI/carla/dist/,里面有一个carla-0.9.13-py3.7-linux-x86_64.egg文件(具体文件名随版本略有差异)。你可以直接把这个egg文件加入到Python路径里,也可以更省事一点,用pip装:
pip install carla==0.9.13装完之后跑一下import carla,如果不报错就说明Python这块没问题。有一点特别提醒:Carla的RPC接口走的是TCP,默认端口是2000,如果本地端口被其他程序占了,记得在服务端启动时改端口,客户端也要对应修改。
2.3 Apollo环境中编译common_msgs:最关键的一步
这是新手最容易卡住的地方。Carla-Apollo桥需要把Carla的消息转换成Apollo的protobuf格式,但Apollo仓库里的proto定义文件一大堆,直接在Python层面用pip install apollo是不存在的。我们必须先在Apollo容器内拉取proto定义,然后用protoc编译成Python模块。
具体步骤是:
# 进入Apollo容器内 bash docker/scripts/dev_into.sh # 创建消息模块目录 mkdir -p /apollo/carla_bridge/common_msgs cd /apollo/carla_bridge # 从Apollo源码中拷贝proto文件 cp -r /apollo/modules/common_msgs modules/common_msgs # 编译proto文件为python模块 # 注意protoc版本要和Apollo容器内编译环境一致 protoc --python_out=. modules/common_msgs/localization/proto/localization.proto # 依次编译所有需要的proto文件这里有个隐藏的坑:Apollo容器内的protoc可能是自定义版本或有一定改动,如果你直接在宿主机上用系统自带的protoc编译,生成的文件很可能因为枚举类型冲突、descriptor_pool中重复注册等问题无法使用。
提示:编译完毕后,需要在
/apollo/carla_bridge下创建一个空的__init__.py,否则Python会把这些proto模块当作命名空间包而不是普通包处理,import时报错让你排查半天。
2.4 验证桥的依赖是否齐全
官方桥接脚本在carla-apollo目录下,依赖的Python库包括numpy、protobuf、transforms3d等。建议在容器里跑一遍pip install把需要的一次性装齐:
pip install numpy transforms3d shapely然后检查protobuf版本:
python -c "import google.protobuf; print(google.protobuf.__version__)"如果版本和Apollo内部的protobuf runtime不一致,桥启动时大概率会报TypeError: Descriptors cannot not be created directly之类的错误。这个问题在后续章节我会详细说,这里你只管把环境对齐就行。
3. 桥接机制深度拆解:数据到底是怎么从Carla流到Apollo的
3.1 消息通道拓扑一览
联合仿真的本质是消息流转。我用一个表格把主要的消息对应关系列出来,这样后续讲原理时你心里有底:
| Carla侧数据 | 桥接处理 | Apollo侧话题 |
|---|---|---|
| 车辆位置与姿态(carla.Transform) | 坐标系对齐、转四元数 | /apollo/localization/pose |
| 车辆运动学状态(速度、加速度) | 单位转换 | /apollo/canbus/chassis |
| 相机图像(carla.Image) | 编码转换 | /apollo/sensor/camera/front_6mm |
| 激光雷达点云(carla.LidarMeasurement) | 去除点云强度、坐标转换 | /apollo/sensor/lidar/velodyne128 |
| 障碍物边界框(carla.BoundingBox) | 类别映射 | /apollo/perception/obstacles |
| GNSS经纬度 | 地图坐标系偏移 | /apollo/localization/msf_gnss |
这里每一项的背后都有故事。我的实际经验是:越细小的字段越容易翻车。
3.2 坐标系转换:最容易被忽视又最容易出错的点
Carla用的是Unreal引擎坐标系,x轴向前,y轴向右,z轴向上——这在自动驾驶领域是比较标准的vehicle坐标系。但Apollo内部的地图坐标系是ENU(东-北-天),定位消息里的position字段是经纬度或者平面投影坐标。
官方桥处理坐标系的方式非常巧妙:它会在启动时记录车辆的初始位置,把初始位置当作相对原点,后续所有坐标都转换成相对这个原点的偏移量。这样Apollo就不需要厘米级高精地图了,只需要一个空地图,把车放在(0,0)点,然后输入局部坐标相对位置即可。
这里有一个极易踩坑的地方:Carla给的是roll/pitch/yaw欧拉角,Apollo定位消息里却是四元数。桥接脚本内部需要先做欧拉角到四元数的转换:
# 关键代码示意:欧拉角转四元数 from transforms3d.euler import euler2quat # carla_transform.rotation的pitch/yaw/roll对应心里要有数 q = euler2quat(roll, pitch, yaw)特别提醒:Carla的rotation属性里三个角度的顺序是pitch, yaw, roll,但很多人习惯按roll, pitch, yaw的顺序处理,一旦搞反,车辆在Apollo端显示的姿态就会出现“车头朝向错位90度”、“车辆在路面上翻滚”这种诡异现象。
3.3 消息生成的实现细节
有一个非常实用的小技巧,在桥接代码中大量应用了google.protobuf的消息合并机制。因为Apollo的protobuf消息结构非常深,逐字段赋值很容易漏掉嵌套消息,所以官方桥接方式通常是用CopyFrom或者MergeFrom来处理。
以生成定位消息为例:
localization.pose.position.x = x_offset localization.pose.position.y = y_offset localization.pose.position.z = z_offset localization.pose.orientation.qx = quat[1] localization.pose.orientation.qy = quat[2] localization.pose.orientation.qz = quat[3] localization.pose.orientation.qw = quat[0]其中x_offset和y_offset一定不能忘记减去初始位置的偏移,否则Apollo会觉得车辆在很远的地方,感知和规划模块对距离的估计全都会错乱。
另外,桥在发布消息时往往会做频率适配。Carla的仿真时钟可以高达上百赫兹,但Apollo的Localization模块通常只需要10Hz到20Hz,如果桥不做降频,Apollo的定时器任务队列会积压,导致延迟越来越高。这个时间段我建议用--timeout参数来控制阻塞时间,在固定时间窗口内抓取Carla数据并发布,而不是纯按Carla的步长来跑。
4. 手把手跑通数据链路:从启动到看到车辆位置
4.1 按正确顺序启动各个组件
启动顺序会直接影响你是否能一次跑通。我的经验是固定用这套顺序,不要乱序:
- 启动Carla服务端
- 启动Apollo容器和DreamView
- 启动Carla-Apollo桥
- 在DreamView中检查数据
第一步,启动Carla服务端:
cd /opt/carla-simulator ./CarlaUE4.sh -carla-rpc-port=2000 -quality-level=Low这里强制把画质调到Low,不是因为显卡不行,而是为了让仿真运行的CPU占用更低,给Apollo留出运算资源。画质对传感器数据的影响远小于你的预期,但渲染开销对整体性能的影响却是肉眼可见的。
第二步,进入Apollo容器并启动DreamView:
cd /apollo bash docker/scripts/dev_start.sh --local bash docker/scripts/dev_into.sh # 容器内执行 ./scripts/bootstrap.sh start启动后浏览器访问http://localhost:8888就能看到DreamView界面。注意如果Apollo开启了token验证,需要在界面下方设置token,否则API请求会被拒绝。
第三步,启动桥:
cd /apollo/carla_bridge python carla_apollo_bridge.py --sync --timeout=10 --role-name=hero--sync表示让桥和Carla的仿真步长同步,保证消息时序的确定性。--timeout是每次读取Carla数据时的超时时间,设成10秒比较稳妥,太小容易因为网络抖动丢数据。
4.2 DreamView里的验证手段
启动桥之后,如果一切正常,日志里应该能看到周期性打印的车辆位置信息。此时切到DreamView界面,在左侧任务栏把Localization模块模式改成Carla,Perception、Prediction、Routing、Planning、Control也都切换到Carla对应的选项。
实话说,我第一次跑通时的第一反应是去点开左侧菜单里的“车辆轨迹”,然后放大地图,看到自动驾驶的车在虚拟道路上跑起来。那一刻是真的有成就感的。
为了确认数据没有断流,你可以用Cyber Monitor在Apollo容器内实时查看话题消息频率:
cyber_monitor在界面上找到/apollo/localization/pose,检查它的频率是否为10Hz左右。只要这个数字稳定,说明桥的数据转发链路是通的。
4.3 写一个Python测试脚本确认车辆数据
光看到位置数据还不够,建议你写个小脚本进一步验证车辆底盘信息是否能正确送达Apollo:
import cyber from cyber.python.cyber_py3 import cyber from modules.canbus.proto.chassis_pb2 import Chassis def chassis_callback(msg): print(f"speed_mps: {msg.speed_mps:.2f}, " f"throttle: {msg.throttle_percentage:.1f}%, " f"brake: {msg.brake_percentage:.1f}%") if __name__ == "__main__": cyber.init() node = cyber.Node("chassis_listener") node.create_reader("/apollo/canbus/chassis", Chassis, chassis_callback) cyber.spin() cyber.shutdown()如果能在终端看到速度值随Carla里车辆的加速减速而变化,说明整个数据链路是完全打通的。到这一步,你已经成功了一大半。
5. 从数据展示到闭环控制:Apollo的规划结果如何控制Carla车辆
5.1 先想清楚一个概念:展示模式不等于闭环模式
很多人在这个阶段会有一个误区,以为DreamView里看到车辆在动,就是Apollo在控制车辆。这里必须把这个概念掰扯清楚:DreamView里显示车辆动,很可能只是Carla的autopilot在开车,Apollo只是“看着”数据在跑。
真正的闭环控制链路是这样的:
- Carla把车辆定位、底盘信息发给Apollo
- Apollo感知模块接收传感器数据并做障碍物检测
- 规划模块输出轨迹
- 控制模块把轨迹转换为油门、刹车、转向信号
- 这些控制指令发送到
/apollo/canbus/chassis_detail话题 - Carla桥订阅该话题,把控制量转成Carla车辆控制指令
- Carla对车辆施加控制,车辆状态改变,数据再次回流
在默认桥里,有些版本并不完整实现第6步,即桥只负责把Carla的状态上传给Apollo,但不接受Apollo的控制指令去驱动Carla。用这类桥,你只能做数据采集和感知算法调试,做不了完整的控制闭环。
5.2 如何判断你的桥支持闭环
一个快速判断的方法:在Carla里关闭车辆的autopilot,然后在Apollo端设置一条路由让车自动驾驶。如果车还能动,说明桥写回了控制指令,闭环成立;如果车纹丝不动,说明桥只做了单向数据转发。
我用的桥版本验证下来是支持闭环的,但网上流传的一些旧版桥或者社区魔改版本不一定有。如果你需要自己补上闭环这一块逻辑,代码核心其实很简单:
# 在桥接脚本中订阅Apollo控制指令 def chassis_detail_callback(chassis_detail): control = carla.VehicleControl() control.throttle = chassis_detail.throttle_percentage / 100.0 control.steer = chassis_detail.steering_percentage / 100.0 * -1 # 注意方向 control.brake = chassis_detail.brake_percentage / 100.0 vehicle.apply_control(control)这里我特意标注了steer的方向。Carla的转向和Apollo的转向方向定义恰好相反,如果不做取反,车辆会朝着你预期的反方向打轮。实测中这个bug极其隐蔽,你以为车没接收到指令,实际上是因为它在疯狂打反方向,轮胎发出刺耳声的同时车辆原地打转。
5.3 SimControl的作用不要忽略
在DreamView界面右下角有个Sim_Control按钮,很多人不知道它是干嘛的。开启SimControl后,Apollo不会把控制指令发给底盘执行器,而是在仿真器里直接按规划轨迹绘制车辆位置。
在Carla-Apollo桥接的场景里,如果你开启了SimControl,即使桥没有反向控制能力,车看起来也会根据规划轨迹“自动”行驶。这会掩盖闭环失效的问题。所以验证闭环时,一定要确保SimControl是关闭状态。
注意:如果你看到车辆动作和Apollo规划的轨迹完全一致,但底盘的
throttle、brake数据却没有任何变化,基本可以断定是SimControl在起作用,而不是真正的闭环控制。
6. 跑测期间的三个典型故障与完整排查链路
6.1 现象:车辆在Carla里动力响应迟钝、卡顿严重
这个问题在联合仿真里非常常见。Carla本身是重负载渲染程序,Apollo的感知和规划模块也吃CPU,两个大块头同时跑在一台机器上,资源竞争必然会发生。
我的排查链路是这样的:
- 先看CPU占用:
top或htop确认哪些进程在抢资源。实测中CarlaUE4很容易吃满8个核心,Apollo的模块加起来再吃4到6个核心。 - 再调参数:Carla画质降到Low,
-quality-level=Low是最直接的降载手段。Apollo容器内限制Cyber的CPU占用可以设置环境变量。 - 最后改用异步模式:把桥的
--sync去掉,让Carla按自己的节奏跑,桥只是周期性获取数据,而不是每一步都等待桥处理完才推进。
经过实测,异步模式在车辆控制和传感精度上会有一些时间戳偏差,但在资源有限的机器上确实是更现实的选择。如果对时序要求严格,建议升级CPU和多通道内存,这是绕不过去的硬件门槛。
6.2 现象:桥启动时报protobuf descriptor错误
这个问题我前面提到过,但因为它太经典了,这里专门展开讲一遍完整排查链路。报错往往长这样:
TypeError: Couldn't build proto file into descriptor pool: duplicate file name ...这个错误的原因几乎可以锁定为:Apollo容器内的protobuf运行时版本和你本地protoc编译出来的Python代码版本不匹配。Apollo 6.0基于protobuf 3.6.1封装的runtime做了不少定制,如果你用系统自带的protoc(比如3.8、3.11)去编译proto文件,生成的descriptor_pool注册表就会冲突。
解决步骤:
- 确认容器内protoc版本:
protoc --version,在Apollo 6.0容器里应该是3.6.1。 - 如果宿主机protoc版本不一致,用容器内的protoc重新编译所有proto文件。
- 重新编译后删除Python的
__pycache__目录,防止旧字节码干扰。 - 还不行的话,在容器内用
pip list | grep protobuf检查Python的protobuf库版本,必要时强制降级:
pip install protobuf==3.6.1这个问题我前后折腾了两天,最后就是降级Python的protobuf库解决的。这种问题没有捷径,只能一层层排查版本链。
6.3 现象:DreamView里看不到障碍物,或者障碍物位置严重错位
出现这个问题时,首先要明白Apollo的感知模块在默认配置下依赖高清地图、Radar和LiDAR的标定参数。Carla虽然是仿真器,但传感器数据一样需要标定。
我排查这个问题时的步骤是:
- 先在Carla一侧确认LiDAR数据是否正常发布:直接在Python脚本里打印点云的点数。
- 再看桥是否有障碍物直传模式。官方桥通常会把Carla的GroundTruth障碍物列表直接转成
/apollo/perception/obstacles,如果你用的是纯传感器数据经由Apollo感知模块处理的方式,那就要检查传感器标定参数和Carla里的传感器位置是否一致。 - 最后做一个“保守策略”:直接用Carla的GroundTruth模式启动桥,确认规划模块能正常看到障碍物并绕行,然后再切回纯感知模式做算法调试。
7. Python在联合仿真中的进阶用法:自动化测试与场景注入
7.1 用场景注入替代反复手工设置
聊完基础的桥接和闭环,我想再分享一些能让这套系统真正发挥价值的进阶经验。联合仿真最有价值的地方在于场景是可控的、可重复的,而Python正好是构建场景脚本的最佳工具。
比如我想测试Apollo在行人突然横穿马路时的反应,我只需要在Carla侧用Python写一个脚本控制行人行动:
import carla client = carla.Client('localhost', 2000) world = client.get_world() # 获取行人蓝图并生成 bp_lib = world.get_blueprint_library() ped_bp = bp_lib.filter('walker.pedestrian.0001')[0] walker = world.try_spawn_actor(ped_bp, carla.Transform(location=...)) # 控制行人行动 walker_controller_bp = bp_lib.filter('controller.ai.walker')[0] controller = world.try_spawn_actor(walker_controller_bp, carla.Transform(), attach_to=walker) controller.start() controller.go_to_location(carla.Location(...))这样Apollo的感知模块就能在车辆行驶过程中检测到突然出现的行人,我可以反复调整起始位置和运动轨迹,做各种边界条件的回归测试。
7.2 批量跑测时如何管理实验记录
每次仿真跑完,大量传感器数据、规划轨迹、控制指令分散在不同的频道里,如果不用工具聚合记录,事后复盘会非常痛苦。Cyber RT提供了录包工具:
cyber_recorder record -a -o carla_apollo_session录包之后,可以用Python离线分析数据,比如统计车辆在整个测试过程中的平均速度、最大横向加速度、与障碍物的最小距离等指标。我通常会写一个脚本,把bag文件里的消息读出来,转成pandas DataFrame再分析:
from cyber.python.cyber_py3 import cyber from cyber.python.cyber_py3 import record record_reader = record.RecordReader('carla_apollo_session.record') for msg in record_reader.read_messages(): topic = msg.topic_name # 根据topic解析不同的protobuf消息联合仿真的数据量非常可观,一条30秒的场景跑完,bag文件动辄好几个GB。建议录包前只选关键话题,不要用-a录所有话题,否则磁盘空间很快就会吃紧。
7.3 用Python实现一个简单的“智能体”来扩展场景边界
联合仿真跑得越多,越会感觉到内置场景的局限性。好在Carla给了很高的自由度,你完全可以用Python写一个“外部智能体”来扩展场景。
比如我想测试Apollo在跟车场景中面对前方车辆突然切入车道的反应,我就写一个脚本让前方车辆在特定距离时自动打转向灯并变道:
while True: ego_location = ego_vehicle.get_location() front_location = front_vehicle.get_location() distance = ego_location.distance(front_location) if distance < 20.0 and not lane_change_started: front_vehicle.apply_control(carla.VehicleControl( steer=-0.3, throttle=0.4 )) lane_change_started = True这类脚本在工程测试里很有用。你不需要修改Apollo的任何代码,就能创造出一个又一个面向特定功能验证的动态测试场景,这对回归测试的高频迭代来说价值极大。
最后再分享一个我的个人体会:联合仿真最有意思的部分,恰恰是那些“看似不需要折腾但就是不工作”的细节。坐标系方向对不上、欧拉角顺序错误、protobuf版本不一致、SimControl掩盖了闭环失效,这些问题每一个都足以让一个熟练的开发者耗费半天到两天时间。但只要你对数据流的走向足够敏感,排查链路清晰,这些问题其实都是有规律可循的。希望这篇文章能帮你把Carla和Apollo之间的那座桥搭得又快又稳。