简介:基于Python与CARLA的高性能分布式自动驾驶仿真平台源码包,主要面向人工智能、自动化、电子信息、物联网等专业学生,适用于毕业设计、课程设计、科研演示及初学者进阶学习。资源围绕分布式协同仿真、车辆控制、传感器配置与数据同步等关键场景,提供可运行的主体代码和完整设计文档,便于快速搭建一套CARLA自动驾驶仿真环境。压缩包共17个文件,整体约96KB,核心内容为13个Python源文件,分别覆盖仿真入口、同步控制、传感器手动控制、客户端视图、日志记录与常量配置等功能;另外附带2个Markdown说明文档、1个YAML配置文件和1份Word版设计报告,用于解释目录结构、服务端参数配置与整体设计方案。目前已有94人学习/下载,代码经过严格测试且资料齐全,可直接作为课设或毕设作品使用,也适合在此基础上继续扩展多传感器融合、多车协同以及分布式计算等实验方向。
1. 拆开这套Python+CARLA仿真平台的第一眼印象
当时拿到这套源码,我先扫了一遍文件清单,最吸引我的不是carlaSimulation.py,而是synchronization.py、configureModule.py、clientview.py这几个模块。CARLA本身已经提供了client和server的交互,但要把多个视角、多个传感器、多台车的仿真步调对齐,默认的异步模式根本做不到。这套平台实际上是一个轻量级的分布式仿真框架:一台CARLA服务器,多个Python客户端进程,统一走同步模式,把world.on_tick、传感器数据和车辆控制指令收敛到同一个仿真帧里。适合谁用?做课程设计和毕业设计的学生,可以拿它当多智能体仿真或数据采集底座的起点;工作三五年以上的自动驾驶从业者,也能从它拆出来的同步逻辑里看到工程化数据链路的常规做法。这篇文章不吹骨架,就看真实代码怎么组织、参数怎么传、坑在哪里。
2. CARLA仿真架构与分布式同步原理
2.1 理解CARLA客户端/服务器模型
CARLA最基础的结构是一个服务器进程负责渲染和物理运算,客户端通过Python API连接服务器,创建车辆、放置传感器、发送控制指令。每一帧,服务器推进仿真世界,客户端可以读取世界状态、传感器数据。这套平台里的carlaSimulation.py就是客户端入口,它做的事情看起来只是client.get_world(),实际上背后已经按分布式思路拆了好几个独立模块。
常见做法是每个客户端进程只负责一小块任务:有的控制主车,有的做旁观视角,有的采集传感器数据。进程之间不直接通信,统一由CARLA服务器转发世界状态。这个方案的优点是隔离性好,一个客户端崩溃不会把整台服务器带崩;缺点是帧同步变得非常麻烦,所以必须引入synchronizeModule.py这类模块来协调步调。
# 典型的CARLA客户端连接方式 import carla client = carla.Client("localhost", 2000) # 服务器地址和端口 client.set_timeout(10.0) # 网络超时时间 world = client.get_world() # 获取当前世界这段代码里set_timeout很关键。分布式环境下服务器如果掉线,默认客户端会一直阻塞,超时时间设为10秒能让你快速看到错误,而不是整个进程挂死。端口2000是CARLA服务器默认的RPC端口,后面的流数据端口是2001到2002,多个客户端同时连进来的情况下,端口管理要提前规划。
2.2 同步模式与异步模式:仿真步进的控制权
CARLA有两种运行模式。异步模式下,服务器按照自己的节奏跑,客户端发一条指令过去,指令在哪一帧被执行不确定;同步模式下,服务器必须等客户端调用world.tick()才会推进一帧。对分布式仿真平台来说,同步模式几乎是必须选的,因为你要把多个客户端的传感器回调、控制指令和世界状态对齐到同一个时间点。
这套平台的synchronization.py文件就是干这个事的。它的核心逻辑不是简单地循环tick,而是维护一个帧计数器,让每个客户端都在同一帧下工作。注意CARLA的同步模式有个特点:world.on_tick()返回的frame_id是递增的,你要根据这个ID来判断数据是否来自同一帧。
# 同步循环的基本骨架(参考 synchronizeModule.py 的常见写法) while True: snapshot = world.wait_for_tick() frame_id = snapshot.frame # 每个客户端都拿到同一个 frame_id if frame_id > last_frame_id: sensors_data = retrieve_sensor_data(frame_id) control_vehicles(frame_id) last_frame_id = frame_idwait_for_tick()会阻塞当前线程直到CARLA服务器推进了一帧并返回世界快照,比反复调world.get_snapshot()更节省CPU。同步模式下,控制逻辑必须在拿到新快照后再下发,否则你发送的油门和转向会在下一帧才生效,数据对齐就会出现一帧的偏差。这个偏差在数据采集场景下是致命的,因为相机图像比LiDAR点云晚了一帧,后续标定和融合算法直接错位。
2.3 configureModule.py里的配置加载与参数映射
分布式平台最怕配置写死在代码里。这套项目的configureModule.py承担了统一的配置入口,它的典型做法是从carlaservers.yaml读取服务器列表、车辆型号、传感器参数和同步策略,然后在Python里转换成配置对象。YAML的好处是层级清晰,注释可以直接写在里面,适合课程设计和团队协作。
下面是一个简化的配置数据结构,常见的字段包括:
# carlaservers.yaml 片段 server: host: "localhost" port: 2000 timeout: 10.0 sync: mode: "synchronous" fps: 20 enable_tick: True vehicles: ego: model: "vehicle.tesla.model3" spawn_point: [36.0, 64.0, 0.6] sensors: camera_front: type: "sensor.camera.rgb" width: 800 height: 600 fov: 90configureModule.py里会有一个load_config(path)函数,用yaml.safe_load()读取文件,然后逐字段赋值到对应的数据结构里。使用safe_load而不是load是为了避免YAML反序列化漏洞,虽然CARLA配置是可信文件,但工程习惯上不应该偷懒。
# configureModule.py 中的加载逻辑 import yaml import types def load_config(path="carlaservers.yaml"): with open(path, "r", encoding="utf-8") as f: raw = yaml.safe_load(f) server_cfg = raw["server"] sync_cfg = raw["sync"] # 用 types.SimpleNamespace 把 dict 转成属性访问 server = types.SimpleNamespace(**server_cfg) sync = types.SimpleNamespace(**sync_cfg) return server, syncattributes动态挂载在Python里很灵活,但debug的时候不好看。更严谨的做法是用dataclass定义schema,字段缺失时直接抛错。如果你要在这个平台基础上做二次开发,我建议至少加上字段校验,否则别人改YAML时漏写一个fps,程序会在某个深层报KeyError,很难追。
2.4 同步模块的代码骨架与关键参数表
synchronizeModule.py在平台上相当于节拍器。它的接口通常暴露几个方法:start()、step()、stop()。step()里面把world.tick()和传感器数据检索封装在一起,保证调用方不会漏掉关键步骤。
| 参数或方法 | 作用 | 推荐取值或说明 |
|---|---|---|
mode | 同步或异步 | 分布式场景固定为"synchronous" |
fps | 目标仿真帧率 | 20到30,太高会导致CPU/GPU跑不满物理引擎 |
timeout | 等待服务器响应最大时间 | 10到15秒,网络抖动时不至于无限阻塞 |
step() | 推进一帧并返回快照 | 内部调用world.tick() |
get_frame() | 获取当前帧号 | 用于数据对齐和日志记录 |
这里特别提醒,fps不是越高越好。CARLA的物理计算在低帧率下能保持一致性,但如果你使用深度学习模型实时推理,传感器数据量会成倍增长,CPU和显卡的显存压力很大。我这个平台测试时通常用20fps,既保证画面流畅,又不至于让队列积压。
# synchronizeModule.py 的步进逻辑 def step(self): snapshot = self.world.tick() frame_id = snapshot.frame # 把当前帧号交给日志模块做记录 self.log.debug(f"frame {frame_id} advanced") for sensor in self.sensors: sensor.retrieve_data(frame_id)retrieve_data在CARLA Python API里一般配合队列使用。每个传感器注册回调函数,把数据放入一个queue.Queue,等到对应帧号出现后取出。如果这个队列不及时清理,内存占用会一路飙高,尤其在高分辨率相机下,单帧RGB图像就有数MB,几百帧积压下来直接内存溢出。
3. 多视角客户端与传感器数据通路
3.1 多客户端的连接管理与world.on_tick
clientview.py这个模块,从命名看就是独立视觉客户端,负责把场景里的画面拉出来供人观察或录像。和主控制客户端不同,它的职责很纯粹:连上CARLA服务器,监听世界状态,然后渲染到窗口。分布式平台里,多个客户端可以同时连接同一台服务器,只要端口不冲突。但要注意,服务器同一时刻只允许有限数量的RPC连接,默认情况下几百个连接没问题,但之前遇到超过配额后被拒绝的情况,排查了半天才发现是连接没有正常释放。
world.on_tick(callback)是注册一个持续回调,每当服务器推进一帧,回调就会被执行。这个机制非常适合做相机图像显示,因为画面必须跟随每一帧刷新,不能漏也不能重复。下面是多客户端视角模块里常见的监听代码。
# clientview.py 中的视角回调 def _on_tick(self, snapshot): # snapshot 包含世界时间戳和帧号 self.current_frame = snapshot.frame if not self.renderer.is_open(): return # 根据帧号取出最新图像 image = self.camera_queue.get() self.renderer.draw(image)camera_queue.get()默认会阻塞,如果on_tick一帧内回调没及时取数据,下一帧来了又会把新的图像放进队列,可能导致队列积压。实践中需要用queue.get_nowait()捕获Empty异常,保证不再同步回调里阻塞。
3.2 摄像头/激光雷达回调的数据格式
传感器的回调是所有数据通路的第一站。CARLA里相机的回调参数是一个carla.Image对象,它本质上是Raw数组,RGB值按BGRA排列;LiDAR回调则是carla.LidarMeasurement,包含点云坐标和强度。处理的时候要先做数据类型转换,不然直接拼接成数组会出现颜色通道错位。
# manual_control_sensor.py 中的传感器回调示例 def on_camera_image(self, image): # 转换未处理的原生数据:BGRA转RGB并压成numpy数组 import numpy as np array = np.frombuffer(image.raw_data, dtype=np.uint8) array = array.reshape((image.height, image.width, 4)) rgb = array[:, :, :3][:, :, ::-1] # BGRA -> RGB self.image_queue.put(rgb)参数说明:image.raw_data是字节流,np.frombuffer不会复制底层数据,性能好;reshape的(height, width, 4)中第4个通道是Alpha,相机传感器固定为255;取[..., :3]后做逆序切片把BGR换成RGB。如果你直接把原始数据丢给OpenCV显示,颜色偏蓝偏红的问题十有八九在这里。
3.3 manual_control_sensor.py的传感器配置
manual_control_sensor.py里的传感器往往是通过world.spawn_actor(blueprint, transform)动态创建的。blueprint需要从CARLA的蓝图库中拿到,然后设置属性。比如相机的分辨率、FOV、位置和朝向,都是有讲究的。
# 配置一个前置相机传感器 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_transform = carla.Transform( carla.Location(x=-0.5, z=1.7), carla.Rotation(pitch=0.0, yaw=0.0) ) camera = world.spawn_actor(camera_bp, camera_transform) camera.listen(lambda image: self.on_camera_image(image))位置x=-0.5意味着传感器装在车体中心略靠前的位置,z=1.7模拟驾驶室高度。listen()注册的回调会在每个数据帧到达时被调用,无论你是否消费。如果你只想要每隔几帧取一张照片,千万别在回调里做抽帧逻辑,而应该在消费端根据frame_id过滤。
3.4 数据对齐:时间戳与frame_id
分布式平台里最容易出问题的就是时间同步。CARLA传感器数据自带时间戳,但不同传感器的时间戳可能对应不同帧。正确的做法是用snapshot.frame作为全局唯一主键,把相机、LiDAR和控制指令都打上当前帧号。clientview.py里一般会维护一个frames字典,键是frame_id,值是当前帧存放数据的列表。
# 数据对齐逻辑 import threading data_lock = threading.Lock() frame_buffers = {} def collect_sensor_data(frame_id, name, data): with data_lock: if frame_id not in frame_buffers: frame_buffers[frame_id] = {} frame_buffers[frame_id][name] = data这个字典本质上是小窗口的缓存。你需要在消费完某一帧数据后主动删除该帧,否则字典越来越大。使用threading.Lock是因为传感器回调运行在独立的线程,主线程读取数据时不加锁会有竞争风险。实际平台中这类问题通常在长时间跑数据采集时才暴露,运行十几分钟没有问题,跑一个小时后内存疯涨,多半就是某个帧没有被清理。
4. 分布式仿真平台落地:配置、启动与踩坑排查
4.1 carlaservers.yaml的服务器编排
分布式平台不只有一台服务器。你可以在多台机器上分别启动不同地图的CARLA实例,然后由YAML配置统一管理。carlaservers.yaml这种命名方式说明它不只是单机配置,而是服务器集群的清单。每个条目可以包含主机名、端口对、地图名称、是否启用GPU等。
典型的多服务器配置:
servers: - name: "Town01" host: "192.168.1.101" port: 2000 map: "Town01" gpu: 0 - name: "Town02" host: "192.168.1.102" port: 2000 map: "Town02" gpu: 1这里的port默认都是2000,因为每台机器上只有一个CARLA服务器进程。如果同一台机器上跑两个实例,第二个实例要分配2001端口,并且设置参数-carla-rpc-port=2001。配置读取后,客户端进程按name选择要连接哪一台服务器,而不是写死localhost。这个设计对于毕业设计演示特别有说服力,一台机器显示画面,另一台机器跑计算,直观展示分布式架构。
4.2 启动脚本与命令行参数
这套平台包含多个Python文件,启动前必须保证Python环境依赖齐全。CARLA客户端依赖的包一般包括numpy、Pygame、Pillow和yaml。安装方式直接看requirements.txt是否有,没有的话可以自己补。
# 初始化Python虚拟环境 python -m venv carla_env source carla_env/bin/activate # Linux / macOS # carla_env\Scripts\activate # Windows pip install numpy pygame pillow pyyaml启动顺序一般是先启动CARLA服务器,再启动配置模块,然后启动主控制和视角客户端。服务器启动时可以通过环境变量PYTHONPATH指向CARLA Python API的目录,例如PYTHONPATH=/opt/carla/PythonAPI/carla。多个终端窗口分别启动不同的模块,是标准的调试方式。
# 启动主仿真客户端 python carlaSimulation.py --config carlaservers.yaml --role ego # 启动视觉客户端 python clientview.py --config carlaservers.yaml --role viewer这里--role是不同客户端的角色标识,ego负责车辆控制,viewer只旁观。这样同一个carlaSimulation.py可以通过命令行参数切换到不同职责,避免代码重复。
4.3 常见问题:同步卡死、客户端崩溃、视角不同步
同步模式下最常见的问题是服务器还在异步跑,或者某个客户端忘记调tick,导致其他客户端永久等待。判断方法很朴素:看日志里frame_id是否还在增长。框架的logModule.py会把每帧心跳写入日志,如果心跳停了,优先检查是不是同步循环被某个异常打断了。
另一个高发问题是崩溃时simulator进程挂起。在Python脚本结束前一定要清理actors:
# 清理传感器和车辆 def cleanup(): for actor in active_actors: actor.destroy() client.apply_batch([carla.command.DestroyActor(a) for a in target_actors])不清理actor直接退出Python脚本,服务器上会残留一堆幽灵车辆,占显存和物理资源。尤其做长时间数据采集时,每次中途kill进程都会造成服务器内存泄漏,连续跑几次后服务器帧率暴跌。
视角不同步往往不是时间戳的问题,而是相机Transform设置不一致。比如主车上的相机和外部视角相机的朝向差了几度,看起来就“不同步”。建议把所有相机的传感器参数集中放在YAML里,统一管理,而不是在每个脚本里单独写数字。
4.4 通过logModule.py定位同步帧偏移
这个平台的logModule.py很有价值。它提供了分级日志和帧号跟踪能力。当一个传感器数据比另一个传感器晚了一帧,日志上就能看到两行相邻帧号不齐。用法是初始化Logger的时候传入当前frame_id,然后每个模块打印自己的帧号:
# logModule.py 的典型接口 from logModule import get_logger logger = get_logger("sync", log_file="trace.log") logger.info(f"camera frame={frame_id} ts={timestamp}") blackbox = get_blackbox(level="sync") blackbox.record(f"lidar frame={frame_id} ts={timestamp}")排查方案:打开日志后,检索两个传感器连续10帧的时间戳,如果差值保持恒定则数据对齐正常;如果差值在1和2之间跳变,说明传感器回调没有绑定到帧推进上,要改回调逻辑,在帧开始时清空队列而不是数据到了再塞。
5. 在这个平台基础上做一套多车数据采集工具
最后的进阶玩法是把这套架子改造成离线数据采集工具。多车协同场景需要给每辆车分配不同的起点和目标点,并让每个车辆客户端独立运行。这里有一个常用技巧:每个车辆客户端在接收到frame_id后,把自己所在位置写入共享内存或Redis,而采集主进程每隔若干帧做一次快照对齐。
实际实现时,可以让每个客户端同时采集多路相机和LiDAR,并把数据写入HDF5文件而不是直接存图片,这样帧号、时间戳、车辆位姿和传感器数据一次性写入,避免文件系统频繁创建小文件。
# 数据保存到HDF5 import h5py with h5py.File("episode_001.h5", "a") as f: # 使用frame_id作为groupId grp = f.create_group(f"frame_{frame_id}") grp.create_dataset("rgb", data=rgb_array, compression="gzip") grp.create_dataset("lidar_xyz", data=lidar_xyz, compression="gzip") grp.attrs["timestamp"] = timestamp使用compression="gzip"能压缩点云数据,但CPU开销不小,如果采集频率很高建议改为无压缩。另一个更轻量的方案是直接保存numpy数组的.npz,每一帧写一个文件,后期在读取时按帧号合并。验证方式也很直接:把viewer功能改成回放器,按帧号读取HDF5中对应数据,通过CARLA的无头模式渲染出来,就能确认采集到的数据是否连续、是否丢帧。这种“采集后立即回放”的闭环验证方法,也是我在做平台验收时必做的一项测试。
最后的技巧是:在回放时不要依赖CARLA服务器是否可以复现原场景,而是对保存的相机图像加一个简单的可视化播放窗口,用OpenCV循环显示。这样既能检查数据有没有问题,也能在答辩现场做演示,不需要演示时真的启动CARLA服务器,省去一大坨环境配置时间。
本文还有配套的精品资源,点击获取