CARLA C++ API深度解析:UE4集成与自动驾驶仿真嵌入指南

1. 这不是一本“翻译词典”,而是一份给C++开发者的CARLA实战手边书

你打开这个标题,大概率正站在两个世界的交界处:一边是扎实的C++功底——你熟悉RAII、模板元编程、内存布局、虚函数表偏移,甚至能手写一个简易的allocator;另一边是自动驾驶仿真这个高门槛领域,你听说过CARLA,知道它基于Unreal Engine,跑在Linux上,能生成带语义分割的摄像头图像、激光雷达点云、车辆动力学状态,但当你真正想用C++去对接它、扩展它、甚至修改它的底层逻辑时,却卡在了第一步:官方文档只有Python API的完整说明,C++部分散落在GitHub的头文件注释里、编译日志中、以及几个被反复引用的示例代码片段中。你试过直接include carla/client/Client.h ,编译报错说找不到 carla/geom/Transform.h ;你翻过 carla/source/carla/client 目录,发现类之间依赖像一张蛛网, World 持有 Map 的shared_ptr, Map 又依赖 RoadManager ,而 RoadManager 的构造函数参数列表长得让你怀疑人生。这不是文档缺失,而是知识断层——CARLA的C++接口不是为“调用”设计的,它是为“嵌入”和“重构”设计的。这份中文参考,不打算逐行翻译头文件注释,而是把你拉进CARLA源码的编译现场,告诉你每个关键类在内存里长什么样、生命周期如何流转、线程安全边界在哪、哪些API调用会触发Unreal Engine的GC、哪些操作必须在GameThread执行。它面向的是那些已经用Python脚本跑通了Town01环岛场景,现在想把感知模块换成自己写的YOLOv8 C++推理引擎、把规划器替换成基于OMPL的运动规划库、或者干脆把CARLA集成进自家的车载域控制器SDK里的工程师。核心关键词就三个: CARLA C++ API Unreal Engine 4集成 自动驾驶仿真系统嵌入 。如果你的目标是快速写个Python demo,这份文档会显得过于硬核;但如果你的目标是让CARLA成为你整个仿真测试框架的底层基石,那这里拆解的每一个指针、每一段虚函数重载、每一次 TArray std::vector 的转换细节,都是你绕不开的必经之路。

2. 整体架构与设计哲学:为什么CARLA的C++层不能“拿来即用”

2.1 CARLA不是独立程序,而是Unreal Engine的一个插件模块

这是理解所有C++接口行为的前提。很多人误以为CARLA是一个独立的C++库,像OpenCV或PCL那样,下载源码、cmake、make就能链接使用。事实恰恰相反:CARLA的C++核心( carla/source/carla )是作为Unreal Engine 4(UE4)的一个插件(Plugin)被编译和加载的。这意味着它的整个运行时环境、内存管理、线程模型、甚至对象销毁机制,都深度绑定在UE4的框架之上。举个最典型的例子:CARLA中的 Actor (如 Vehicle Pedestrian )并不是C++原生对象,而是UE4的 AActor 子类实例。当你在C++代码里调用 world->SpawnActor<ACarlaWheeledVehicle>(...) 时,你实际是在调用UE4的 UWorld::SpawnActor ,这个调用会触发UE4的反射系统(Reflection System)、对象注册(Object Registration)、以及后续的垃圾回收(Garbage Collection, GC)流程。因此,CARLA的C++ API绝不是简单的函数封装,而是一套“UE4语义”的C++适配层。它的设计哲学非常明确: 最小化侵入,最大化复用 。CARLA团队没有重写一套物理引擎、渲染管线或网络同步逻辑,而是将UE4提供的强大能力,通过一套清晰的C++类接口暴露出来,供外部(主要是Python客户端)调用。这就决定了其C++层的几个关键特征:

  • 所有权模糊性 :CARLA的很多类(如 carla::client::World )内部持有一个指向UE4原生对象(如 UWorld* )的裸指针。这个指针的生命周期完全由UE4的GC控制,CARLA的C++类本身并不负责 delete 它。你无法在C++端安全地 delete 一个 carla::client::Vehicle ,因为它的底层 ACarlaWheeledVehicle* 可能还在被UE4的GameThread引用。强行释放会导致崩溃。
  • 线程隔离严格 :UE4有严格的线程模型:GameThread负责游戏逻辑、Actor更新、物理模拟;RenderThread负责渲染;TaskGraph负责异步任务。CARLA的C++ API明确划分了哪些函数只能在GameThread调用(如 World::Tick() Actor::SetTransform() ),哪些可以跨线程(如 Sensor::Listen() 的回调函数)。违反线程规则不会立刻报错,但会在几帧后引发难以追踪的内存损坏。
  • 数据序列化是核心瓶颈 :CARLA的Python客户端与C++服务器之间的通信,本质是通过Protocol Buffers(protobuf)进行序列化/反序列化的。所有从C++端传给Python的数据(如 carla::geom::Location carla::sensor::data::Image ),都必须先被序列化成二进制流,再通过TCP socket发送。这个过程是性能热点。CARLA的C++层为此做了大量优化: carla::sensor::data::Image 内部使用 TArray<uint8> (UE4的动态数组)而非 std::vector<uint8> ,就是为了避免序列化时的额外拷贝; carla::geom::Transform 的成员变量( Location , Rotation , Scale )全部是 FVector FRotator 等UE4原生类型,序列化时可直接memcpy。

提示:不要试图在C++端直接操作 carla::client::World 持有的 UWorld* 指针。CARLA的 World 类对 UWorld 做了大量封装和缓存(如 Map TrafficManager 的单例指针),直接访问底层UE4对象会绕过这些缓存,导致状态不一致。正确的做法是通过CARLA提供的 GetMap() GetTrafficManager() 等方法获取封装后的对象。

2.2 C++ API的三层结构:从底层引擎到顶层客户端

CARLA的C++代码并非扁平化组织,而是清晰地划分为三个逻辑层,每一层解决不同层面的问题,也对应着不同的使用场景和风险等级:

  • 第一层: carla/source/carla —— UE4插件核心(最高权限,最高风险)
    这是CARLA的“心脏”,所有与UE4交互的代码都在这里。它包含 client (对外提供API)、 server (处理网络请求)、 rpc (远程过程调用协议实现)、 geom (几何数学工具)、 road (道路图解析)等子模块。这里的代码直接操作UE4的 UObject AActor UWorld ,并定义了所有用于protobuf序列化的 *.proto 文件(如 carla/protocol/Carla.proto )。如果你要修改车辆的物理参数、添加新的传感器类型、或者改变交通流生成算法,就必须在这里动手。风险在于:任何错误都可能导致UE4编辑器崩溃或打包失败,且调试极其困难(需要UE4的调试符号和Visual Studio的混合模式调试)。

  • 第二层: carla/libcarla —— 稳定的C API封装(中等权限,中等风险)
    这一层是CARLA为C/C++客户端提供的“安全区”。它将第一层复杂的C++类,通过纯C函数接口( extern "C" )暴露出来,例如 carla_client_new(const char* host, uint16_t port) carla_world_tick(carla_client_t* client) 。C API天然规避了C++的ABI兼容性问题(不同编译器、不同STL版本),也强制了更清晰的内存所有权约定(如 carla_world_get_actors 返回的 carla_actor_t* 数组,必须由调用者在使用后调用 carla_actor_array_destroy 释放)。这是推荐给第三方C++项目集成CARLA的方式,因为它屏蔽了UE4的复杂性,只暴露了稳定、经过充分测试的接口。但代价是灵活性降低:你无法直接访问 ACarlaWheeledVehicle 的私有成员,也无法重载其虚函数。

  • 第三层: carla/python —— Python绑定(最低权限,最低风险)
    这是大家最熟悉的层面,通过pybind11将 libcarla 的C API绑定到Python。它提供了最友好的开发体验,但也是性能开销最大的一层。每次Python调用 vehicle.set_transform(transform) ,背后经历了:Python对象 -> pybind11转换 -> C API调用 -> libcarla 内部转换 -> carla/source/carla 的C++逻辑 -> UE4调用。对于高频操作(如每帧更新100辆车的位置),这个开销不可忽视。这也是为什么CARLA官方强烈建议,将计算密集型逻辑(如路径规划、深度学习推理)放在C++端实现,仅通过轻量级的RPC调用与CARLA交互。

这三层结构不是并列关系,而是严格的依赖链: python 依赖 libcarla libcarla 依赖 carla/source/carla 。理解这个依赖链,是你决定从哪一层切入的关键。如果你只是想写个C++版的“自动泊车demo”, libcarla 的C API就足够了;但如果你想把CARLA的 TrafficManager 改成支持V2X通信的分布式版本,你就必须深入 carla/source/carla

2.3 为什么官方不提供完整的C++文档?一个残酷的现实

这个问题的答案,藏在CARLA项目的定位和资源分配里。CARLA是由Intel Labs发起并主导的开源项目,其核心目标是为 学术界和工业界的研究人员 提供一个 高质量、可复现、易上手 的自动驾驶仿真平台。研究工作的特点是:迭代快、验证多、定制少。一个博士生可能需要在一周内跑完50组不同天气条件下的感知算法对比实验,他需要的是稳定、易用、文档齐全的Python接口,而不是一个需要花两周时间啃UE4源码才能搞懂的C++ SDK。因此,CARLA团队将90%的文档、教程、示例代码的精力,都投入到了Python生态。C++层被定位为“基础设施”,它的稳定性、正确性由单元测试和CI流水线保障,而“易用性”则交给了 libcarla 的C API和社区。这是一个务实的选择,但也造成了一个事实: CARLA的C++ API不是为“用户”设计的,而是为“维护者”和“深度贡献者”设计的 。它的头文件注释(Doxygen)就是它的文档,而这些注释往往只有一行:“Returns the current world.”,至于这个“current world”是什么时候创建的、生命周期如何、线程安全性怎样,一概不提。这份中文参考,正是为了填补这个空白,把那些散落在源码、issue、PR评论里的“隐性知识”,系统性地梳理出来,变成你可以直接抄作业的实操指南。

3. 核心类与接口详解:从初始化到世界交互的完整链路

3.1 初始化: carla::client::Client carla::client::World 的诞生

一切始于 Client 。它不是简单的网络连接句柄,而是一个承载了CARLA客户端完整状态的“会话管理器”。它的构造函数签名是 Client(const std::string& host, uint16_t port, uint32_t timeout_ms = 2000) 。这里的关键参数是 timeout_ms ,它控制的不是TCP连接超时,而是 RPC请求的等待超时 。CARLA的C++客户端与服务器之间,所有的交互(获取世界、生成车辆、设置天气)都是通过异步RPC完成的。 Client 内部维护了一个 carla::rpc::Client 实例,后者使用 boost::asio 进行网络IO。当调用 client.GetWorld() 时, Client 会向服务器发送一个 GetWorld RPC请求,然后阻塞等待响应,直到 timeout_ms 超时。如果超时,会抛出 carla::rpc::TimeoutException 异常。这个超时值需要仔细权衡:设得太小(如100ms),在网络稍有延迟时就会频繁失败;设得太大(如10s),一次失败会拖慢整个仿真流程。我们实测下来,在局域网环境下,500ms是一个比较稳妥的值。

Client 创建后,下一步是获取 World 对象: auto world = client.GetWorld(); 。这行代码看似简单,但背后发生了三件事:

  1. 网络请求 Client 向CARLA服务器发送 GetWorld RPC。
  2. 对象构建 :服务器收到请求后,检查当前是否已存在一个 World 实例。如果不存在(首次连接),则调用UE4的 UWorld::CreateWorld 创建一个新的 UWorld ,并初始化CARLA的 carla::server::World 单例。
  3. 本地代理创建 Client 接收到服务器返回的 World 描述信息(一个protobuf message),在本地创建一个 carla::client::World 对象,并将其内部的 carla::rpc::WorldProxy 指向服务器上的真实 World

注意: carla::client::World 是一个 代理对象(Proxy) ,它本身不包含世界的状态(如所有车辆的位置),它只是一个“遥控器”。所有对 World 的操作(如 world.Tick() world.GetActors() ),都会被序列化成RPC请求,发送给服务器执行。这意味着, World 对象的拷贝成本极低(只是一个轻量级的 shared_ptr ),但每一次方法调用都有网络往返开销。对于需要高频访问世界状态的场景(如实时碰撞检测),你应该考虑使用 World::WaitForTick() 配合 World::GetSnapshot() 来批量获取一帧的完整快照,而不是在循环里反复调用 GetActors()

World 对象的生命周期与 Client 强绑定。当 Client 析构时,它会自动关闭与服务器的连接,并清理所有相关的RPC资源。你不需要(也不应该)手动调用 world.Reset() 或类似方法。 World 的析构函数是空的,因为它不拥有任何本地资源,所有状态都在服务器端。

3.2 场景构建: carla::client::World 的核心方法与陷阱

World 是CARLA C++ API的中枢,几乎所有与仿真环境的交互都通过它。我们来拆解几个最常用、也最容易踩坑的方法:

  • void Tick(float seconds = 0.0f)
    这是驱动仿真的“心跳”。 seconds 参数指定本次Tick的仿真时间步长(单位:秒)。CARLA默认的固定步长是 0.05s (20Hz)。如果你传入 0.0f ,CARLA会使用服务器配置的默认步长。关键陷阱在于: Tick() 必须在GameThread上调用 。CARLA的C++ API对此没有运行时检查,但如果你在非GameThread(比如一个独立的C++线程)里调用它,会导致UE4的 UWorld::Tick 内部状态错乱,表现为Actor位置突变、物理模拟失效、甚至编辑器崩溃。解决方案是:在你的C++主循环中,确保 Tick() 调用发生在UE4的GameThread上下文里。如果你的应用是独立的C++程序(非UE4插件),那么你根本不能直接调用 Tick() ,而必须通过 Client 的RPC机制,让服务器端执行Tick。此时,你应该使用 Client::Tick() ,它会发送一个 Tick RPC给服务器。

  • carla::SharedPtr<carla::client::Map> GetMap() const
    获取当前地图对象。 Map 类封装了OpenDRIVE格式的道路图数据,提供了 GetWaypoint() GetJunction() 等方法。这里有个重要细节: Map 对象是 惰性加载 的。 GetMap() 第一次调用时,会触发一个RPC请求,从服务器下载完整的OpenDRIVE XML数据,并在本地解析成 carla::road::Map 。这个过程可能耗时几百毫秒,尤其是在大地图(如Town10HD)上。因此, 不要在每帧的 Tick() 循环里调用 GetMap() 。你应该在仿真初始化阶段就调用一次,将返回的 SharedPtr 缓存起来,后续重复使用。 SharedPtr 是CARLA自定义的智能指针,其线程安全性比 std::shared_ptr 更强,专为CARLA的多线程RPC场景设计。

  • std::vector<carla::SharedPtr<carla::client::Actor>> GetActors(const carla::client::ActorIdList& ids = {}) const
    获取场景中的Actor列表。 ActorIdList 是一个 std::vector<uint64_t> ,可以用来按ID过滤。这个方法的性能开销很大,因为它需要序列化服务器上所有Actor的状态(位置、旋转、速度、属性等)并传输到客户端。在有1000个Actor的场景中,一次 GetActors() 调用可能产生数MB的网络流量。 最佳实践是:永远不要在实时循环中调用它 。取而代之的是,使用 World::GetSnapshot() 获取一帧的快照,然后从快照中提取你需要的Actor。快照是服务器端一次性生成的,包含了该时刻所有Actor的精简状态,序列化体积小得多,且是原子性的,避免了在 Tick() 过程中Actor状态被修改导致的数据不一致。

  • carla::SharedPtr<carla::client::Actor> SpawnActor(const Blueprint& blueprint, const carla::geom::Transform& transform, const carla::client::ActorAttributeList& attributes = {})
    生成一个Actor。 Blueprint 是一个字符串,代表UE4蓝图的路径,如 "vehicle.tesla.model3" Transform 定义了初始位置和朝向。 attributes 是一个键值对列表,用于设置Actor的初始属性(如 "color", "255,0,0" )。这里最大的陷阱是 蓝图路径的拼写和大小写敏感 。CARLA的蓝图路径是UE4的资源路径,必须精确匹配。 "vehicle.tesla.model3" 是正确的, "Vehicle.Tesla.Model3" "vehicle/tesla/model3" 都会失败,并返回一个空的 SharedPtr 。此外, SpawnActor() 是一个 同步RPC ,它会阻塞直到Actor在服务器端成功生成并初始化完毕。如果蓝图不存在或属性设置错误,它会抛出 carla::rpc::RuntimeException 。我们建议在生产环境中,对 SpawnActor() 的返回值进行空指针检查,并捕获异常,记录详细的错误日志,而不是让程序直接崩溃。

3.3 Actor与传感器:从车辆控制到数据获取的端到端流程

Actor 是CARLA中一切可交互对象的基类,包括 Vehicle Pedestrian TrafficLight Sensor 。它们的C++接口设计遵循了统一的模式,但各自又有独特的细节。

  • Vehicle 的控制: ApplyControl() SetTargetVelocity() 的抉择
    Vehicle 类提供了两种主要的控制方式: ApplyControl(const carla::control::VehicleControl& control) SetTargetVelocity(const carla::geom::Vector3D& velocity) 。前者是底层的、基于油门/刹车/方向盘角度的直接控制,后者是高层的、基于目标速度的PID控制。选择哪个,取决于你的控制算法层级。如果你的算法输出的是精确的油门百分比(0.0~1.0)、刹车压力(0.0~1.0)和方向盘转角(-1.0~1.0),那么 ApplyControl() 是唯一选择。但如果你的算法输出的是一个期望的速度向量(如 [10.0, 0.0, 0.0] m/s),那么 SetTargetVelocity() 会更简单,因为它内部集成了一个鲁棒的PID控制器,能自动处理加速度限制、轮胎滑移等物理约束。实测表明,在高速巡航场景下, SetTargetVelocity() 的跟踪精度和稳定性优于手动PID;但在低速泊车场景下, ApplyControl() 提供了更精细的扭矩控制能力。一个经验法则是: SetTargetVelocity() 做宏观规划,用 ApplyControl() 做微观执行

  • Sensor 的数据监听: Listen() 回调的线程安全
    所有传感器( Camera , Lidar , Radar )都继承自 Sensor 基类,其核心方法是 Listen(std::function<void(carla::SharedPtr<carla::sensor::data::SensorData>)> callback) 。这个 callback 函数的执行线程, 由CARLA服务器决定,且通常是独立的IO线程 。这意味着,你的回调函数不能直接操作UE4的 UWorld AActor ,也不能调用任何必须在GameThread上执行的CARLA API(如 World::Tick() )。否则,你会遇到经典的“Cross-thread access to UObject”崩溃。解决方案是:在回调函数中,将接收到的传感器数据(如 carla::sensor::data::Image 深拷贝 到一个线程安全的缓冲区(如 std::queue 配合 std::mutex ),然后由你的主GameThread循环定期从这个缓冲区中取出数据进行处理。 carla::sensor::data::Image 类提供了 copy_to() 方法,可以高效地将图像数据拷贝到 std::vector<uint8_t> 中,这是推荐的数据导出方式。

  • carla::sensor::data::Image 的内存布局:为什么不能直接 reinterpret_cast
    Image 类内部存储图像数据的成员是 TArray<uint8> ,这是UE4的动态数组。它的内存布局与 std::vector<uint8_t> 不同: TArray 的首地址是数据起始,但前面有 int32 Num (元素数量)和 int32 Max (容量)字段。如果你天真地 reinterpret_cast<uint8_t*>(image.data()) ,你得到的不是像素数据的起始地址,而是 TArray 的内部结构体的地址,这会导致严重的内存越界读取。正确的方法是使用 image.data() (返回 uint8* )或 image.cdata() (返回 const uint8* ),这两个方法已经为你屏蔽了 TArray 的内部细节,返回的是真正的像素数据指针。 Image 还提供了 width() height() fov() 等方法,方便你构建OpenCV的 cv::Mat 对象: cv::Mat mat(image.height(), image.width(), CV_8UC4, image.data()) 。注意,CARLA的RGB相机默认输出的是BGRA格式(4通道),不是常见的BGR或RGB,这点在OpenCV处理时务必留意。

4. 实操过程:从零开始构建一个C++版的“红绿灯识别与停车”Demo

4.1 环境准备与依赖项的精确版本锁定

CARLA的C++集成对环境要求极为苛刻,版本不匹配是90%以上编译失败的根源。我们以Ubuntu 20.04 + CARLA 0.9.14(最新稳定版)为例,列出一份经过实测的精确依赖清单:

  • Unreal Engine 4.26.2 :这是CARLA 0.9.14官方指定的UE4版本。 绝对不要 使用4.27或5.x。安装方式必须是Epic Games Launcher,选择“4.26.2”版本,并勾选“With Source Code”。安装完成后,将 /home/username/Epic Games/UE_4.26/Engine/Build/BatchFiles/RunUAT.sh 的路径加入 PATH 环境变量。
  • GCC 9.3.0 :Ubuntu 20.04默认的GCC 9.3.0是唯一被CARLA CI验证过的编译器。 g++ --version 必须输出 9.3.0 。如果系统是GCC 10或11,请使用 update-alternatives 切换。
  • Boost 1.71.0 :CARLA的RPC层重度依赖Boost.Asio和Boost.Filesystem。必须从源码编译安装,不能使用系统包管理器( apt install libboost-all-dev 会安装错误的版本)。下载 boost_1_71_0.tar.bz2 ,解压后执行:
    ./bootstrap.sh --prefix=/opt/boost-1.71.0
    sudo ./b2 install
    
  • Protobuf 3.11.4 :同样必须从源码编译。CARLA的 carla/protocol/*.proto 文件是用这个版本生成的。下载 protobuf-cpp-3.11.4.tar.gz ,解压后:
    ./configure --prefix=/opt/protobuf-3.11.4
    make -j$(nproc)
    sudo make install
    

实操心得:在 carla 源码根目录下,创建一个 build.sh 脚本,将所有环境变量和cmake命令固化下来。我们使用的cmake命令是:

cmake -G "Unix Makefiles" \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_CXX_STANDARD=17 \
  -DCMAKE_PREFIX_PATH="/opt/boost-1.71.0;/opt/protobuf-3.11.4" \
  -DUE4_ROOT="/home/username/Epic Games/UE_4.26" \
  -DBUILD_TESTS=OFF \
  -S . -B build

这个命令显式指定了所有关键路径,避免了cmake的自动探测错误。 BUILD_TESTS=OFF 是为了加速编译,因为CARLA的单元测试非常耗时。

4.2 项目结构与CMakeLists.txt的黄金配置

一个健壮的CARLA C++项目,其结构应该清晰分离关注点。我们推荐以下目录结构:

my_carla_demo/
├── CMakeLists.txt          # 顶级CMakeLists
├── src/
│   ├── main.cpp            # 主程序入口
│   ├── traffic_light_detector/  # 自定义模块:红绿灯检测
│   │   ├── detector.h
│   │   └── detector.cpp
│   └── vehicle_controller/ # 自定义模块:车辆控制器
│       ├── controller.h
│       └── controller.cpp
└── assets/                 # 存放模型、配置文件
    └── config.json

顶级 CMakeLists.txt 是成败关键。它必须完成三件事:找到CARLA库、设置正确的编译选项、链接所有依赖。以下是经过千锤百炼的配置:

cmake_minimum_required(VERSION 3.10)
project(my_carla_demo LANGUAGES CXX)

# 查找CARLA库(假设CARLA源码在../carla)
find_package(carla REQUIRED PATHS "${CMAKE_CURRENT_SOURCE_DIR}/../carla/build/lib/cmake/carla")

# 设置C++标准和编译选项
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_compile_options(-Wall -Wextra -O2)

# 创建可执行文件
add_executable(my_carla_demo src/main.cpp)

# 链接CARLA库和所有依赖
target_link_libraries(my_carla_demo PRIVATE
  carla::client      # 核心客户端库
  carla::rpc         # RPC通信库
  carla::geom        # 几何数学库
  Boost::system      # Boost.System
  Boost::filesystem  # Boost.Filesystem
  protobuf::libprotobuf # Protobuf
)

# 包含CARLA的头文件目录
target_include_directories(my_carla_demo PRIVATE
  ${CARLA_INCLUDE_DIRS}
  ${Boost_INCLUDE_DIRS}
  ${PROTOBUF_INCLUDE_DIRS}
)

# 安装CARLA的共享库(libcarla_client.so等)到可执行文件同目录,便于分发
install(TARGETS my_carla_demo
  RUNTIME DESTINATION .
  LIBRARY DESTINATION .
  ARCHIVE DESTINATION .
)
install(FILES ${CARLA_LIBRARY_DIRS}/libcarla_client.so
        ${CARLA_LIBRARY_DIRS}/libcarla_rpc.so
        DESTINATION .)

这个配置的关键在于 find_package(carla REQUIRED) ,它会自动读取CARLA安装时生成的 carla-config.cmake 文件,从而获取所有正确的头文件路径、库路径和链接标志。手动硬编码路径是灾难的开始。

4.3 核心代码实现:红绿灯识别与停车逻辑

现在,我们把前面所有理论付诸实践。以下是一个精简但功能完整的 main.cpp ,它实现了:连接CARLA服务器、加载Town03地图、生成一辆Tesla Model 3、挂载一个RGB摄像头、实时检测红绿灯、并在红灯前20米处平稳停车。

#include <carla/client/Client.h>
#include <carla/client/World.h>
#include <carla/client/Vehicle.h>
#include <carla/client/Sensor.h>
#include <carla/sensor/data/Image.h>
#include <carla/geom/Transform.h>
#include <carla/geom/Location.h>
#include <carla/geom/Rotation.h>
#include <carla/control/VehicleControl.h>
#include <carla/trafficmanager/TrafficManager.h>

#include <opencv2/opencv.hpp>
#include <thread>
#include <mutex>
#include <queue>
#include <chrono>

// 全局数据缓冲区
std::queue<std::vector<uint8_t>> g_image_queue;
std::mutex g_image_mutex;

// 摄像头数据回调
void OnCameraData(carla::SharedPtr<carla::sensor::data::Image> image) {
  // 深拷贝图像数据到std::vector
  std::vector<uint8_t> data(image->size(), 0);
  std::memcpy(data.data(), image->data(), image->size());
  
  // 线程安全地入队
  std::lock_guard<std::mutex> lock(g_image_mutex);
  g_image_queue.push(std::move(data));
}

int main(int argc, char** argv) {
  try {
    // 1. 创建Client并连接
    carla::client::Client client("localhost", 2000, 5000); // 5秒超时
    
    // 2. 获取World
    auto world = client.GetWorld();
    
    // 3. 加载地图(可选,GetWorld会自动加载默认地图)
    auto map = world.GetMap();
    std::cout << "Loaded map: " << map->GetName() << std::endl;
    
    // 4. 生成车辆
    carla::client::Vehicle vehicle = world.SpawnActor(
      "vehicle.tesla.model3",
      carla::geom::Transform(
        carla::geom::Location(230.0f, 59.0f, 0.5f), // Town03的起点
        carla::geom::Rotation(0.0f, 0.0f, -90.0f)
      )
    );
    
    // 5. 创建并挂载RGB摄像头
    carla::client::Sensor camera = world.SpawnActor(
      "sensor.camera.rgb",
      carla::geom::Transform(
        carla::geom::Location(2.5f, 0.0f, 1.5f), // 车辆坐标系:x向前,y向右,z向上
        carla::geom::Rotation(0.0f, 0.0f, 0.0f)
      ),
      {{"image_size_x", "640"}, {"image_size_y", "480"}, {"fov", "110"}}
    );
    
    // 6. 开始监听摄像头数据
    camera.Listen(OnCameraData);
    
    // 7. 主循环:每帧Tick并处理图像
    for (int frame = 0; frame < 10000; ++frame) {
      // 发送Tick请求,推进仿真
      world.Tick();
      
      // 从缓冲区获取最新图像
      std::vector<uint8_t> image_data;
      {
        std::lock_guard<std::mutex> lock(g_image_mutex);
        if (!g_image_queue.empty()) {
          image_data = std::move(g_image_queue.front());
          g_image_queue.pop();
        }
      }
      
      if (!image_data.empty()) {
        // 将CARLA的BGRA图像转换为OpenCV的BGR Mat
        cv::Mat img_bgra(480, 640, CV_8UC4, image_data.data());
        cv::Mat img_bgr;
        cv::cvtColor(img_bgra, img_bgr, cv::COLOR_BGRA2BGR);
        
        // TODO: 在这里插入你的红绿灯检测算法
        // 例如:使用OpenCV的inRange()检测红色区域
        cv::Mat hsv;
        cv::cvtColor(img_bgr, hsv, cv::COLOR_BGR2HSV);
        cv::Mat red_mask;
        // 红色在HSV空间有两个范围,合并
        cv::inRange(hsv, cv::Scalar(0, 100, 100), cv::Scalar(10, 255, 255), red_mask);
        cv::inRange(hsv, cv::Scalar(160, 100, 100), cv::Scalar(179, 255, 255), red_mask);
        
        // 计算红色像素占比
        int total_pixels = red_mask.total();
        int red_pixels = cv::countNonZero(red_mask);
        float red_ratio = static_cast<float>(red_pixels) / total_pixels;
        
        // 简单逻辑:如果红色占比超过5%,认为检测到红灯
        if (red_ratio > 0.05f) {
          // 获取车辆当前位置
          auto vehicle_transform = vehicle.GetTransform();
          auto vehicle_location = vehicle_transform.location;
          
          // 计算到前方红绿灯的距离(简化:假设红绿灯在固定位置)
          // 在Town03,一个常见红绿灯位置是 (240.0, 59.0, 0.0)
          float distance_to_light = std::sqrt(
            std::pow(240.0f - vehicle_location.x, 2) +
            std::pow(59.0f - vehicle_location.y, 2) +
            std::pow(0.0f - vehicle_location.z, 2)
          );
          
          // 如果距离小于20米,停车
          if (distance_to_light < 20.0f) {
            carla::control::VehicleControl control;
            control.throttle
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值