自动驾驶-M1-环境搭建-CARLA 仿真器部署

这是整个课程的数据源头和闭环评测场。 本节不只是"装上能跑",还要理解同步模式——这个概念不懂,后面采的数据全是错的。

一、进入环境

前边已经搭建好了L20 算力显卡的开发环境,这里直接进入:

cd ~carla/work

# 切换环境
conda activate ad

# 启动仿真器
./start_carla.sh

二、测试联通性

先用最小脚本确认客户端能连上服务端:test_connect.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CARLA 连通性测试 —— 确认服务端可用、看看有哪些资源"""

import carla
import sys

def main(host="localhost", port=2000):
    print(f"连接 {host}:{port} ...")
    client = carla.Client(host, port)
    client.set_timeout(15.0)   # 首次连接可能较慢,给足超时

    try:
        version = client.get_server_version()
    except RuntimeError as e:
        print(f"✗ 连接失败: {e}")
        print("  排查: 服务端起了吗?端口对吗?防火墙?")
        return 1

    print(f"✓ 服务端版本: {version}")
    print(f"✓ 客户端版本: {client.get_client_version()}")
    if version != client.get_client_version():
        print("  ⚠ 版本不一致,可能出现诡异问题,建议统一")

    world = client.get_world()
    m = world.get_map()
    print(f"\n当前地图: {m.name}")

    # 可用地图列表
    maps = client.get_available_maps()
    print(f"\n可用地图 ({len(maps)} 个):")
    for name in sorted(maps):
        print(f"  {name.split('/')[-1]}")

    # 生成点数量 —— 决定能放多少车
    spawn_points = m.get_spawn_points()
    print(f"\n生成点数量: {len(spawn_points)}")

    # 车辆蓝图
    bp_lib = world.get_blueprint_library()
    vehicles = bp_lib.filter("vehicle.*")
    print(f"车辆蓝图: {len(vehicles)} 种,例如:")
    for bp in list(vehicles)[:5]:
        print(f"  {bp.id}")

    # 传感器蓝图 —— 后面要用的
    sensors = bp_lib.filter("sensor.*")
    print(f"\n传感器蓝图: {len(sensors)} 种,关键的几个:")
    for key in ["sensor.camera.rgb", "sensor.lidar.ray_cast",
                "sensor.camera.depth", "sensor.other.imu"]:
        found = bp_lib.filter(key)
        mark = "✓" if len(found) > 0 else "✗"
        print(f"  {mark} {key}")

    # 当前仿真设置
    settings = world.get_settings()
    print(f"\n当前仿真设置:")
    print(f"  同步模式      : {settings.synchronous_mode}")
    print(f"  固定时间步长  : {settings.fixed_delta_seconds}")
    if not settings.synchronous_mode:
        print("  提示: 当前是异步模式,采数据前记得切同步")

    print("\n✓ 连通性测试通过\n")
    return 0

if __name__ == "__main__":
    h = sys.argv[1] if len(sys.argv) > 1 else "localhost"
    p = int(sys.argv[2]) if len(sys.argv) > 2 else 2000
    sys.exit(main(h, p))

执行结果:

cd  /home/leoking/carla/work/courses/M1
(ad) leoking@leoking-desktop:~/carla/work/courses/M1$ python test_connect.py 
连接 localhost:2000 ...
✓ 客户端版本: 0.9.15

当前地图: Carla/Maps/Town10HD_Opt

可用地图 (12 个):
  Town01
  Town01_Opt
  Town02
  Town02_Opt
  Town03
  Town03_Opt
  Town04
  Town04_Opt
  Town05
  Town05_Opt
  Town10HD
  Town10HD_Opt

生成点数量: 155
车辆蓝图: 41 种,例如:
  vehicle.audi.a2
  vehicle.citroen.c3
  vehicle.chevrolet.impala
  vehicle.dodge.charger_police_2020
  vehicle.micro.microlino

传感器蓝图: 16 种,关键的几个:
  ✓ sensor.camera.rgb
  ✓ sensor.lidar.ray_cast
  ✓ sensor.camera.depth
  ✓ sensor.other.imu

当前仿真设置:
  同步模式      : False
  固定时间步长  : None
  提示: 当前是异步模式,采数据前记得切同步

✓ 连通性测试通过

(ad) leoking@leoking-desktop:~/carla/work/courses/M1$ 

完整采集脚本

下面这个脚本是可以直接用于 M2 数据采集的雏形。它包含了所有正确做法: 同步模式、传感器队列、异常安全的资源清理。
▸ 三个关键设计,理解了受用整个课程
① 传感器队列:不用 listen(callback) 直接存盘,而是把数据放进队列, 主循环 tick 后统一取——这样才能保证"这一帧的所有传感器数据"配成一组。
② try/finally 清理:CARLA 的 actor 不会自动销毁。 脚本崩了不清理,仿真世界里会残留一堆幽灵车,越积越多直到卡死。
③ 恢复异步:退出时要把世界设回异步, 否则下次别人连上来会发现仿真"卡住不动"(因为在等谁调 tick)。

collect_demo.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CARLA 多相机同步采集 —— 最小可运行版本

这是 M2 数据采集的雏形,演示了三个必须掌握的正确做法:
  1. 同步模式:客户端驱动仿真步进,保证时间戳对齐
  2. 传感器队列:一帧内所有传感器数据配成一组
  3. 资源清理:异常安全,不留幽灵 actor

用法:
    python collect_demo.py --frames 100 --out ./data/run01
"""

import argparse
import os
import queue
import random
import json

import carla
import numpy as np

# ---------------- 相机布局 ----------------
# 仿照 nuScenes 的 6 路环视布局
# x 向前, y 向右, z 向上;yaw 单位为度,正为顺时针(向右)
CAMERA_RIG = {
    "CAM_FRONT":       {"x":  1.5, "y":  0.0, "z": 2.4, "yaw":    0, "fov": 70},
    "CAM_FRONT_LEFT":  {"x":  1.3, "y": -0.5, "z": 2.4, "yaw":  -55, "fov": 70},
    "CAM_FRONT_RIGHT": {"x":  1.3, "y":  0.5, "z": 2.4, "yaw":   55, "fov": 70},
    "CAM_BACK":        {"x": -1.5, "y":  0.0, "z": 2.4, "yaw":  180, "fov": 110},
    "CAM_BACK_LEFT":   {"x": -1.0, "y": -0.5, "z": 2.4, "yaw": -110, "fov": 70},
    "CAM_BACK_RIGHT":  {"x": -1.0, "y":  0.5, "z": 2.4, "yaw":  110, "fov": 70},
}

IMG_W, IMG_H = 800, 450   # 先用小分辨率快速验证,正式采集再调大

class SyncCollector:
    """同步采集器:管理世界设置、actor 生命周期、传感器队列"""

    def __init__(self, host="localhost", port=2000, fps=20):
        self.client = carla.Client(host, port)
        self.client.set_timeout(15.0)
        self.world = self.client.get_world()
        self.fps = fps

        self.actors = []          # 所有需要销毁的 actor
        self.sensors = {}         # name -> sensor actor
        self.queues = {}          # name -> queue,存传感器输出
        self._orig_settings = None
        self.vehicle = None

    # ---------- 世界设置 ----------
    def setup_sync(self):
        """切换到同步模式,并记住原设置以便恢复"""
        self._orig_settings = self.world.get_settings()

        settings = self.world.get_settings()
        settings.synchronous_mode = True
        settings.fixed_delta_seconds = 1.0 / self.fps
        # 关键:让子步长稳定,物理仿真更可靠
        settings.substepping = True
        settings.max_substep_delta_time = 0.01
        settings.max_substeps = 10
        self.world.apply_settings(settings)

        # Traffic Manager 也必须切同步,否则 NPC 车行为异常
        self.tm = self.client.get_trafficmanager(8000)
        self.tm.set_synchronous_mode(True)

        print(f"[setup] 同步模式已开启, {self.fps} FPS "
              f"(每帧 {1.0/self.fps:.3f}s)")

    def restore(self):
        """恢复原始设置 —— 退出时必须调用,否则影响他人使用"""
        if self._orig_settings is not None:
            self.world.apply_settings(self._orig_settings)
            print("[cleanup] 已恢复原始仿真设置")
        try:
            self.tm.set_synchronous_mode(False)
        except Exception:
            pass

    # ---------- 生成主车 ----------
    def spawn_vehicle(self, model="vehicle.tesla.model3"):
        bp_lib = self.world.get_blueprint_library()
        bp = bp_lib.filter(model)[0]
        bp.set_attribute("role_name", "ego")

        spawn_points = self.world.get_map().get_spawn_points()
        if not spawn_points:
            raise RuntimeError("地图没有可用生成点")

        # 尝试多个生成点,避免"被占用"直接失败
        random.shuffle(spawn_points)
        for sp in spawn_points:
            vehicle = self.world.try_spawn_actor(bp, sp)
            if vehicle is not None:
                self.vehicle = vehicle
                self.actors.append(vehicle)
                print(f"[spawn] 主车已生成: {model}")
                return vehicle
        raise RuntimeError("所有生成点都被占用,无法生成主车")

    # ---------- 挂载相机 ----------
    def attach_cameras(self, rig=None):
        """按 rig 配置挂载多路相机,每路配一个队列"""
        rig = rig or CAMERA_RIG
        bp_lib = self.world.get_blueprint_library()

        for name, cfg in rig.items():
            bp = bp_lib.find("sensor.camera.rgb")
            bp.set_attribute("image_size_x", str(IMG_W))
            bp.set_attribute("image_size_y", str(IMG_H))
            bp.set_attribute("fov", str(cfg["fov"]))
            # 传感器输出间隔设为 0 = 每帧都输出(同步模式下由 tick 驱动)
            bp.set_attribute("sensor_tick", "0.0")

            transform = carla.Transform(
                carla.Location(x=cfg["x"], y=cfg["y"], z=cfg["z"]),
                carla.Rotation(yaw=cfg["yaw"]),
            )
            cam = self.world.spawn_actor(bp, transform, attach_to=self.vehicle)

            # 关键:用队列而不是直接存盘
            q = queue.Queue()
            cam.listen(q.put)

            self.sensors[name] = cam
            self.queues[name] = q
            self.actors.append(cam)

        print(f"[sensor] 已挂载 {len(self.sensors)} 路相机")

    # ---------- 交通流 ----------
    def spawn_traffic(self, n_vehicles=30):
        """生成 NPC 车辆,让场景不那么空"""
        bp_lib = self.world.get_blueprint_library()
        blueprints = bp_lib.filter("vehicle.*")
        spawn_points = self.world.get_map().get_spawn_points()
        random.shuffle(spawn_points)

        count = 0
        for sp in spawn_points:
            if count >= n_vehicles:
                break
            bp = random.choice(blueprints)
            npc = self.world.try_spawn_actor(bp, sp)
            if npc is not None:
                npc.set_autopilot(True, self.tm.get_port())
                self.actors.append(npc)
                count += 1
        print(f"[traffic] 已生成 {count} 辆 NPC")

    # ---------- 主循环 ----------
    def run(self, n_frames, out_dir, warmup=20):
        os.makedirs(out_dir, exist_ok=True)
        for name in self.sensors:
            os.makedirs(os.path.join(out_dir, name), exist_ok=True)

        # 主车开自动驾驶(用 CARLA 内置 TM,先保证能跑起来)
        self.vehicle.set_autopilot(True, self.tm.get_port())

        # 预热:让物理稳定、传感器进入状态,这些帧不保存
        print(f"[run] 预热 {warmup} 帧...")
        for _ in range(warmup):
            self.world.tick()
            for q in self.queues.values():
                q.get(timeout=5.0)   # 丢弃预热数据

        print(f"[run] 开始采集 {n_frames} 帧 → {out_dir}")
        meta_records = []

        for i in range(n_frames):
            # 1) 推进仿真一帧,拿到本帧编号
            frame_id = self.world.tick()

            # 2) 从每个队列取出本帧数据
            #    正确性关键:校验 image.frame 与 frame_id 一致
            frame_data = {}
            for name, q in self.queues.items():
                while True:
                    img = q.get(timeout=5.0)
                    if img.frame == frame_id:
                        frame_data[name] = img
                        break
                    # 帧号落后就继续取,直到追上(正常情况不会发生)
                    if img.frame > frame_id:
                        raise RuntimeError(
                            f"传感器 {name} 帧号超前,同步逻辑有误")

            # 3) 存盘
            for name, img in frame_data.items():
                path = os.path.join(out_dir, name, f"{i:06d}.jpg")
                img.save_to_disk(path)

            # 4) 记录自车位姿(后面做 BEV 时必需)
            tf = self.vehicle.get_transform()
            vel = self.vehicle.get_velocity()
            meta_records.append({
                "index": i,
                "carla_frame": frame_id,
                "timestamp": i / self.fps,
                "location": [tf.location.x, tf.location.y, tf.location.z],
                "rotation": [tf.rotation.pitch, tf.rotation.yaw, tf.rotation.roll],
                "velocity": [vel.x, vel.y, vel.z],
                "speed_kmh": 3.6 * np.sqrt(vel.x**2 + vel.y**2 + vel.z**2),
            })

            if (i + 1) % 20 == 0:
                print(f"  已采集 {i+1}/{n_frames} 帧")

        # 5) 保存元数据 + 相机外参(BEV 投影要用)
        meta = {
            "fps": self.fps,
            "image_size": [IMG_W, IMG_H],
            "camera_rig": CAMERA_RIG,
            "frames": meta_records,
        }
        with open(os.path.join(out_dir, "meta.json"), "w") as f:
            json.dump(meta, f, indent=2)
        print(f"[run] 完成,元数据已写入 meta.json")

    # ---------- 清理 ----------
    def cleanup(self):
        """销毁所有 actor —— 顺序很重要:先传感器后车辆"""
        for name, sensor in self.sensors.items():
            try:
                sensor.stop()
            except Exception:
                pass

        # 批量销毁比逐个快很多
        if self.actors:
            self.client.apply_batch(
                [carla.command.DestroyActor(a) for a in self.actors])
            print(f"[cleanup] 已销毁 {len(self.actors)} 个 actor")
        self.actors.clear()

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--host", default="localhost")
    ap.add_argument("--port", type=int, default=2000)
    ap.add_argument("--frames", type=int, default=100)
    ap.add_argument("--fps", type=int, default=20)
    ap.add_argument("--traffic", type=int, default=30)
    ap.add_argument("--out", default="./data/run01")
    args = ap.parse_args()

    collector = SyncCollector(args.host, args.port, args.fps)
    try:
        collector.setup_sync()
        collector.spawn_vehicle()
        collector.attach_cameras()
        collector.spawn_traffic(args.traffic)
        collector.run(args.frames, args.out)
    except KeyboardInterrupt:
        print("\n用户中断")
    finally:
        # finally 保证异常时也能清理,这一点非常重要
        collector.cleanup()
        collector.restore()

if __name__ == "__main__":
    main()

采集执行:

(ad) leoking@leoking-desktop:~/carla/work/courses/M1$ python collect_demo_v2.py
[setup] 同步模式已开启, 20 FPS (每帧 0.050s)
[spawn] 主车已生成: vehicle.tesla.model3
[sensor] 已挂载 6 路相机
[traffic] 已生成 30 辆 NPC
[run] 预热 20 帧...
[run] 开始采集 100 帧 → ./data/run01
  已采集 20/100 帧
  已采集 40/100 帧
  已采集 60/100 帧
  已采集 80/100 帧
  已采集 100/100 帧
[run] 完成,元数据已写入 meta.json
[cleanup] 已销毁 37 个 actor
[cleanup] 已恢复原始仿真设置
(ad) leoking@leoking-desktop:~/carla/work/courses/M1$ 

file

为者常成,行者常至