资讯动态

Python+Carla+Apollo联合仿真从环境搭建到闭环控制全实践

发布时间:2026/10/2 1:31:22 来源:尧图企业网站定制
第一次在Apollo的DreamView里看到来自Carla的车辆定位点在地图上稳定移动传感器数据一条条刷出来的时候我才意识到这套联合仿真终于算是跑通了。从环境搭建到桥接成功中间踩了不知道多少坑光是protobuf版本不一致就折腾了两天。今天把这套Python Carla Apollo联合仿真的完整实践过程整理出来从环境踩坑到桥接原理一路到跑通闭环控制尽量把手伸不到的细节都讲到。这篇东西适合已经有基础的自动驾驶开发者也适合刚入门想在仿真环境里做自动驾驶测试的在校学生只要照着做大概率能少走不少弯路。1. 为什么要用Carla和Apollo做联合仿真它们各自缺的那块拼图1.1 先搞清楚两个家伙的分工Carla是一个开源仿真器核心优势在场景渲染、传感器模型和物理引擎它给你的是一个足够真实的虚拟世界。在这个世界里可以放红绿灯、布行人、设天气还能用Python脚本随心所欲地控制路上的每一辆车。但Carla自身没有完整的自动驾驶算法栈它的一些内置autopilot仅仅是简单的路径跟踪连正儿八经的感知、预测、规划、控制闭环都谈不上。Apollo则是百度开源的自动驾驶平台从高精地图、定位、感知、预测、规划到控制模块分得很细算法栈非常完整。但它需要数据输入在实车上验证风险大、成本高在没有环境的情况下想要调试算法可以说是寸步难行。这两个正好互补。Carla负责出题Apollo负责解题Python就是连接出题人和解题人的对话通道。1.2 为什么用Python做胶水层而不是CCarla官方提供的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.04Apollo需要特定的系统支持实测兼容性最好Carla0.9.130.9.x系列的Python API比较稳定Apollo6.0及以后版本6.0之后Cyber RT框架的Python接口更完善Python3.8Carla 0.9.13和Apollo容器内的Python版本能对上protobuf3.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 carla0.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/obstaclesGNSS经纬度地图坐标系偏移/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-port2000 -quality-levelLow这里强制把画质调到Low不是因为显卡不行而是为了让仿真运行的CPU占用更低给Apollo留出运算资源。画质对传感器数据的影响远小于你的预期但渲染开销对整体性能的影响却是肉眼可见的。第二步进入Apollo容器并启动DreamViewcd /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 --timeout10 --role-namehero--sync表示让桥和Carla的仿真步长同步保证消息时序的确定性。--timeout是每次读取Carla数据时的超时时间设成10秒比较稳妥太小容易因为网络抖动丢数据。4.2 DreamView里的验证手段启动桥之后如果一切正常日志里应该能看到周期性打印的车辆位置信息。此时切到DreamView界面在左侧任务栏把Localization模块模式改成CarlaPerception、Prediction、Routing、Planning、Control也都切换到Carla对应的选项。实话说我第一次跑通时的第一反应是去点开左侧菜单里的“车辆轨迹”然后放大地图看到自动驾驶的车在虚拟道路上跑起来。那一刻是真的有成就感的。为了确认数据没有断流你可以用Cyber Monitor在Apollo容器内实时查看话题消息频率cyber_monitor在界面上找到/apollo/localization/pose检查它的频率是否为10Hz左右。只要这个数字稳定说明桥的数据转发链路是通的。4.3 写一个Python测试脚本确认车辆数据光看到位置数据还不够建议你写个小脚本进一步验证车辆底盘信息是否能正确送达Apolloimport cyber from cyber.python.cyber_py3 import cyber from modules.canbus.proto.chassis_pb2 import Chassis def chassis_callback(msg): print(fspeed_mps: {msg.speed_mps:.2f}, fthrottle: {msg.throttle_percentage:.1f}%, fbrake: {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把车辆定位、底盘信息发给ApolloApollo感知模块接收传感器数据并做障碍物检测规划模块输出轨迹控制模块把轨迹转换为油门、刹车、转向信号这些控制指令发送到/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-levelLow是最直接的降载手段。Apollo容器内限制Cyber的CPU占用可以设置环境变量。最后改用异步模式把桥的--sync去掉让Carla按自己的节奏跑桥只是周期性获取数据而不是每一步都等待桥处理完才推进。经过实测异步模式在车辆控制和传感精度上会有一些时间戳偏差但在资源有限的机器上确实是更现实的选择。如果对时序要求严格建议升级CPU和多通道内存这是绕不过去的硬件门槛。6.2 现象桥启动时报protobuf descriptor错误这个问题我前面提到过但因为它太经典了这里专门展开讲一遍完整排查链路。报错往往长这样TypeError: Couldnt 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 protobuf3.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_towalker) 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, throttle0.4 )) lane_change_started True这类脚本在工程测试里很有用。你不需要修改Apollo的任何代码就能创造出一个又一个面向特定功能验证的动态测试场景这对回归测试的高频迭代来说价值极大。最后再分享一个我的个人体会联合仿真最有意思的部分恰恰是那些“看似不需要折腾但就是不工作”的细节。坐标系方向对不上、欧拉角顺序错误、protobuf版本不一致、SimControl掩盖了闭环失效这些问题每一个都足以让一个熟练的开发者耗费半天到两天时间。但只要你对数据流的走向足够敏感排查链路清晰这些问题其实都是有规律可循的。希望这篇文章能帮你把Carla和Apollo之间的那座桥搭得又快又稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑