3、CARLA基础架构:服务器-客户端模型、世界与蓝图、Actor模型、传感器体系
各位同学,欢迎来到第三章。这一章我们聊聊CARLA的骨架——它的基础架构。
说实话,我第一次接触CARLA时,被它那一堆术语搞得有点懵。什么服务器、客户端、世界、蓝图、Actor……感觉像在学一门新语言。但后来我发现,只要你理解了它的设计哲学,这些东西其实很自然。
CARLA本质上是一个分布式仿真系统。它把仿真环境拆成了两个部分:一个负责跑物理引擎和渲染,另一个负责控制逻辑。这就是服务器-客户端模型的核心。
3.1 服务器-客户端模型
先说说服务器。CARLA服务器就是那个“跑仿真”的程序。它管理着整个虚拟世界——道路、建筑、车辆、行人,还有物理碰撞、光照、天气等等。你启动CARLA时,实际上就是在启动一个服务器进程。
客户端呢?它是你写的Python脚本。客户端向服务器发送指令:“给我创建一个车”、“让这辆车加速”、“装个摄像头上去”。服务器收到指令后执行,然后把结果返回给客户端。
我个人习惯把服务器想象成一个“游戏引擎”,客户端就是“游戏玩家”。玩家不能直接修改引擎内部的数据,只能通过API来操作。这种分离设计有个好处——你可以同时跑多个客户端,连接同一个服务器。比如一个客户端控制主车,另一个客户端监控全局状态。
关键点:服务器和客户端通过TCP/IP通信。默认端口是2000。你可以在一台机器上跑多个服务器,只要端口不冲突就行。
我曾经在调试多车协同场景时,遇到过客户端连接超时的问题。排查了半天,发现是服务器负载太高,处理不过来。后来我调整了仿真步长,把物理更新频率降下来,问题就解决了。嗯,这里要注意——服务器资源是有限的,别把客户端当“上帝”使唤。
3.2 世界(World)与蓝图(Blueprint)
世界是什么?说白了,它就是整个仿真环境的“容器”。道路、建筑、交通标志、车辆、行人……所有东西都装在世界里。你可以通过world = client.get_world()拿到当前世界的句柄。
世界有几个核心功能:
- 管理Actor:创建、销毁、查询所有仿真对象
- 控制环境:设置天气、时间、光照
- 提供地图:获取路网、车道线、交通信号灯信息
- 驱动仿真:控制仿真步进、暂停、恢复
蓝图呢?蓝图是CARLA里一个很巧妙的设计。你可以把它理解成“对象的模板”或“出厂配置”。每个Actor在创建之前,都需要先选一个蓝图。蓝图定义了Actor的类型、属性、默认参数。
举个例子:
# 获取所有可用的车辆蓝图
vehicle_blueprints = world.get_blueprint_library().filter('vehicle.*')
# 选一个具体的蓝图,比如奥迪A2
bp = world.get_blueprint_library().find('vehicle.audi.a2')
# 修改蓝图属性
bp.set_attribute('color', '255,0,0') # 红色
bp.set_attribute('role_name', 'hero') # 角色名
# 用蓝图创建Actor
vehicle = world.spawn_actor(bp, transform)
你想想看,如果没有蓝图,每次创建车辆都要手动指定所有参数——颜色、型号、轮胎类型、灯光配置……那得多麻烦。蓝图把这一切封装好了,你只需要选一个,改几个关键属性就行。
小技巧:蓝图库里的蓝图很多,但并不是所有蓝图都能在任意地图上使用。有些地图只支持特定类型的车辆。我建议你在创建之前,先检查一下蓝图是否有效。
3.3 Actor模型
Actor是CARLA里最核心的概念。所有能在仿真世界里“动”的东西,都是Actor。车辆、行人、传感器、交通标志、甚至路灯,都是Actor。
Actor模型有几个关键特性:
- 生命周期管理:Actor需要显式创建和销毁。创建用
spawn_actor,销毁用destroy。不用的Actor一定要销毁,否则会占用服务器资源。 - 状态查询:每个Actor都有位置、朝向、速度、加速度等属性。你可以通过
get_transform()、get_velocity()等方法获取。 - 行为控制:车辆可以设置油门、刹车、转向;行人可以设置行走路径;传感器可以设置采样频率。
- 父子关系:一个Actor可以附着在另一个Actor上。比如把摄像头装在车顶上,摄像头就是车的子Actor。
我记得有一次做V2X仿真,需要让多个车辆之间交换位置信息。我一开始用全局列表来管理所有车辆,结果代码越写越乱。后来我改用Actor的父子关系,把通信模块作为子Actor挂载到每辆车上,代码瞬间清晰了。这就是Actor模型的好处——它天然支持层次化组织。
注意:不要频繁创建和销毁Actor。每次spawn_actor和destroy都会触发服务器端的资源分配和释放,开销不小。我建议在场景初始化时一次性创建好所有Actor,仿真过程中尽量复用。
3.4 传感器(Sensor)体系
传感器是CARLA里最“值钱”的部分。没有传感器,你的自动驾驶算法就是“瞎子”。CARLA支持多种传感器,每种都有不同的用途。
常见的传感器类型:
| 传感器类型 | 输出数据 | 典型用途 |
|---|---|---|
| RGB相机 | 图像 (HxWx3) | 视觉感知、目标检测 |
| 深度相机 | 深度图 (HxW) | 距离估计、3D重建 |
| 激光雷达 | 点云 (Nx3) | 环境感知、障碍物检测 |
| 毫米波雷达 | 目标列表 | 速度测量、目标跟踪 |
| GNSS | 经纬度坐标 | 全局定位 |
| IMU | 加速度、角速度 | 惯性导航、姿态估计 |
| 语义分割相机 | 语义标签图 | 场景理解、语义分割 |
传感器的使用方式很统一:先选蓝图,再附着到Actor上,然后注册回调函数接收数据。
# 创建RGB相机蓝图
camera_bp = world.get_blueprint_library().find('sensor.camera.rgb')
camera_bp.set_attribute('image_size_x', '800')
camera_bp.set_attribute('image_size_y', '600')
camera_bp.set_attribute('fov', '90')
# 将相机附着到车辆上
camera = world.spawn_actor(camera_bp, transform, attach_to=vehicle)
# 注册回调函数,处理图像数据
def process_image(image):
# image是carla.Image对象
# 可以保存、显示或送入算法
array = np.frombuffer(image.raw_data, dtype=np.uint8)
array = array.reshape((image.height, image.width, 4))
# 注意:CARLA的图像是RGBA格式
rgb_array = array[:, :, :3]
camera.listen(process_image)
这里有个坑,我踩过好几次。传感器的listen回调是在独立线程里执行的。如果你的回调函数处理时间太长,会导致数据堆积,最终内存溢出。我曾经在回调里做了一次完整的YOLO推理,结果跑了不到一分钟程序就崩了。后来我把推理放到另一个线程,回调里只做数据拷贝,问题才解决。
最佳实践:传感器回调函数里只做轻量级操作,比如数据格式转换、保存到队列。复杂的处理逻辑放到主线程或单独的工作线程里。
另外,传感器的频率也很重要。默认情况下,传感器以仿真步长的频率输出数据。如果你把仿真步长设为0.05秒(20Hz),那么所有传感器都会以20Hz的频率输出。但有些传感器(比如激光雷达)可能只需要10Hz,这时候你可以通过蓝图属性sensor_tick来设置采样间隔。
# 设置激光雷达每0.1秒采样一次(10Hz)
lidar_bp.set_attribute('sensor_tick', '0.1')
为什么要控制频率?因为数据量太大了。一个64线激光雷达,每秒输出几十万个点。如果你不限制频率,网络传输和数据处理都会成为瓶颈。
3.5 知识体系总览
说了这么多,我们来画一张图,把这一章的核心逻辑串起来。
这张图把这一章的核心关系都画出来了。服务器和客户端通过TCP/IP通信;世界是容器,蓝图是模板;Actor是具体的仿真对象;传感器是Actor的一种特殊类型。理解了这些,你就掌握了CARLA的骨架。
好了,这一章就到这里。下一章我们会深入代码,看看怎么用Python操作这些概念。记住,理论是基础,但真正的理解来自动手实践。去写几行代码试试吧。
课后练习:启动CARLA服务器,写一个Python脚本,创建一个车辆并给它装上一个RGB相机。让车辆直线行驶,同时把相机图像保存到本地。试试看能不能做到。
可可精品资料库