Carla简单入门-2 同步,异步与交通管理器

Carla简单入门-2 同步,异步与交通管理器

本文写于2023年7月,文中所展示的版本为Ubuntu20.04以及Carla0.9.14,不同版本可能有一定的不同,欢迎各位伙伴们把遇到的问题和解决办法与其他人分享。

在上篇文档中,我们初步熟悉了如何使用PythonAPI去搭建一个基础的拟真交通环境以及实现一些我们自定义的需求。在最后我们遇到了主车的录像卡顿问题,这是由于Carla的同步异步设置导致的问题,这次我们就来了解一下同步与异步的区别和关系并且着手解决录像卡顿的问题。

1. 核心概念:同步与异步

同步与异步是极为重要的核心概念,对于同步与异步的正确设置与否将直接决定最终拟真时的结果的效率与正确率,官方文档中对此做了非常详尽的说明,英语好的伙伴们可以参阅官方文档: Synchrony and time-step - CARLA Simulator

a. 时间步长

  • 首先要清楚的是,Carla模拟世界中的时间和现实世界中的时间是不同的,模拟世界有着自己的时钟和时间,由服务器控制,两个临近的模拟时刻之间模拟世界经过的时间我们称之为时间步长。服务器计算两个模拟时刻需要一些现实时间,计算两个模拟时刻所需的现实时间我们称之为现实时间步长。现实时间步长可能会与模拟时间步长时长不同。现实步长由服务器的性能以及场景的复杂程度决定,而模拟时间步长可以由使用者自行设定。服务器可能需要几毫秒来计算两个模拟场景,而这两个模拟场景之间可能模拟世界已经经过了1秒,也就是说,模拟时间步长为1s,我们可以根据我们的需求来设置为固定模拟时间步长或者可变模拟时间步长。
  • 可变时间步长
    CARLA的默认模式。模拟步骤之间经过的时间将是服务器计算所需时间。
    settings = world.get_settings()
    settings.fixed_delta_seconds = None  # 设置可变时间步长
    world.apply_settings(settings)
    # 可以使用PythonAPI/util/config.py脚本来设置时间步长。将参数设置为零表示可变时间步长。
    cd PythonAPI/util && python3 config.py --delta-seconds 0
  • 固定时间步长
    每个模拟步骤之间经过的时间保持恒定。如果将其设置为0.5秒,每秒将有两个模拟帧。使用相同的时间增量对于从模拟中获取数据是最好的方式。物理和传感器数据将对应于模拟中易于理解的时刻。此外,如果服务器足够快,可以在更少的实际时间内模拟更长的时间段。
    可以在世界设置中设置固定的时间步长。要将模拟设置为固定时间步长为0.05秒,请应用以下设置。在这种情况下,模拟器将花费20个步骤(1/0.05)来重现模拟世界中的一秒。
    settings = world.get_settings()
    settings.fixed_delta_seconds = 0.05
    world.apply_settings(settings)
    # 也可以使用提供的脚本PythonAPI/util/config.py来设置固定时间步长。
    cd PythonAPI/util && python3 config.py --delta-seconds 0.05

b. 模拟记录(recorder)

  • Carla提供了recorder的特性,方便我们记录下模拟的状态并且重新进行回放模拟。需要注意的是,我们在不同的同步异步设定时回放模拟的精度可能会受到影响。
  • 固定模拟时间步长( fixed time-step) 当我们使用固定时间步长设定时进行回放模拟将很容易,将服务器的时间步长设定被记录模拟的时间步长即可。
  • 可变模拟时间步长( variable time-step) 当我们使用可变时间步长设定时,回放模拟就会变得复杂一些:
    如果服务器使用可变时间步长运行,那么时间步长将与原始模拟不同,对于缺失的步长的数据将使用记录的数据进行插值,这可能会导致回放模拟结果与原始记录结果不同。
    如果服务器被强制复现完全相同的时间步长,尽管回放模拟的时间步长是相同的,但是由于服务器的性能波动或者差异,真实世界时间将会不同,模拟将以奇怪的时间波动方式回放。
    同时,使用可变时间步长也会导致 浮点数算术误差,因为时间是连续变量,每个时间步长中间对于时间的浮点误差累积可能会导致最终结果误差甚至错误,以至于无法重现回放模拟。

c. 物理子步 (Physics substepping)

  • 为了精确计算物理效果,物理模拟必须在非常小的时间步内进行。当我们在模拟中每帧执行多次计算(例如传感器渲染,读取存储等)时,时间步长可能会成为一个问题。由于这个限制仅发生在物理模拟中,我们可以仅对物理计算应用子步。在默认情况下物理子步是打开的,并且被设定为每个时间步长最大10个物理子步没,每个物理子步最大为0.01秒。当然,我们可以通过API调整这些设定:
    settings = world.get_settings() # 获取当前设定
    settings.substepping = True #启用物理子步
    settings.max_substep_delta_time = 0.01 # 将物理子步最大步长设定为0.01秒
    settings.max_substeps = 10 # 设定每时间步长最多有10个物理子步
    world.apply_settings(settings) # 应用设定
  • 注意,当启用同步模式时,我们设定模拟时间步长时需要遵循以下条件来保证物理模拟的准确性:
    fixed_delta_seconds <= max_substep_delta_time * max_substeps
  • 注意,为了保证物理模拟的准确性,子步的时间间隔应该至少低于0.01666,理想情况下低于0.01。
  • 下图展示了在固定模拟时间步长为0.04秒的模拟中,速度随物理时间步长的变化而变化。我们可以看到,一旦物理时间步长超过0.01,速度的稳定性开始出现偏差,并且随着物理时间步长的增加而加剧。image

d. 客户端-服务器同步 (Client-server synchrony)

  • CARLA采用客户端-服务器架构。服务器运行模拟,客户端获取信息并对世界进行修改。本节涉及客户端和服务器之间的通信。
    默认情况下,CARLA以异步模式运行。服务器尽可能快地运行模拟,而不等待客户端。在同步模式下,服务器在更新到下一个模拟步骤之前会等待客户端发送的“ready to go”的消息。
  • 设置同步模式
    在同步和异步模式之间切换只需要改变 settings.synchronous_mode的值即可:
settings = world.get_settings()
settings.synchronous_mode = True  # 启用同步模式
world.apply_settings(settings)
  • 同时运行多个client时,只能有一个client开启同步模式,因为server会对每个收到的“ready to go”信息进行反应,多个client开启同步模式将会发送过多“ready to go”信息导致同步失败
  • 注意,如果启用了同步模式,并且正在运行Traffic Manager,则Traffic Manager也必须设置为同步模式。
    traffic_manager= client.get_trafficmanager() # 获得当前的traffic manager
    traffic_manager.set_synchronous_mode(True) #将traffic manager设为同步模式
  • 停用同步模式我们可以使用以下脚本,或者直接将变量设置为False即可:
    cd PythonAPI/util && python3 config.py --no-sync  # 禁用同步模式

e. 同步模式的使用

  • 同步模式在客户端应用程序速度较慢以及需要传感器等不同元素之间的同步性时特别重要。如果客户端速度过慢,服务器不采用同步模式,信息将会溢出,导致信息丢失。例如当我们有多个传感器并且想把传感器画面存储到本地的情况,如果服务器不采用同步模式,那么我们传感器画面可能来不及存储就被覆盖,最后导致本地存储画面出现丢帧情况。
  • 下面是一个在同步模式下读取传感器信息的代码片段:
    settings = world.get_settings() # 获取当前模拟世界设定
    settings.synchronous_mode = True # 将同步模式设定为打开
    world.apply_settings(settings) # 应用设定
    
    camera = world.spawn_actor(blueprint, transform) # 生成传感器
    image_queue = queue.Queue() 
    camera.listen(image_queue.put) # 将传感器画面放入队列中
    
    while True:
        world.tick() # 更新模拟环境
        image = image_queue.get() # 从队列中读取传感器画面
  • 这段代码首先注意到的是 world.tick() 这个函数。它的作用是更新整个模拟世界。同时我们还会发现这里用了python自带的Queue, queue.get有一个功效,就是在它把列队里所有内容都提取出来之前,会阻止任何其他进程越过自己这一步,相当于一个blocker,在client读取完queue的数据之前,queue会将其他进程,例如更新整个模拟世界的进程阻挡住,这样我们就能做到client每读取一帧画面,server渲染下一帧。如果没有这个queue,我们会发现仿真虽然设置成了同步模式,但是server和client还是无法同步运行。
    所以可以这样理解,settings.synchronous_mode = True 让仿真的更新要通过这个client来唤醒,但这并不能保证它会等该client其他进程运行完,必须要再加一个queue来阻挡一下它,逼迫它等着该客户其他线程搞定。也就是说,启动同步模式,让你的server学会等待客户的必要条件有三个:
    1. settings.synchronous_mode = True
    2. world.tick()
    3. Thread Blocker(例如Queue)
  • 来自基于GPU的传感器(主要是相机)的数据通常会有几帧的延迟。在这种情况下,同步性至关重要。

f. 同步与异步的适用范围

  • 首先粘上官方文档的链接,里面有详细说明: Synchrony and time-step - CARLA Simulator
  • 同步模式 + 可变时间步长: 几乎可以确定这是一种不可取的状态。当时间步长大于0.1秒时,物理模拟将无法正常运行。尤其是当client较慢时(例如添加了较多传感器),这种情况很有可能发生,模拟时间和物理将无法同步,模拟将失效。
  • 异步模式 + 可变时间步长: 这是CARLA的默认状态。Server和client是异步的。模拟时间会自动根据现实时间调整。注意进行回放模拟时需要考虑浮点数算术误差和server之间可能存在的时间步长差异。
  • 异步模式 + 固定时间步长: Server将尽可能快地运行。检索到的信息将与模拟中的模拟时间被一一对应起来,所以回放模拟时不会产生浮点数算术误差。如果服务器性能足够快,这种配置可以在较少的实际时间内模拟较长的模拟时间段。
  • 同步模式 + 固定时间步长: Client将控制模拟进程。直到client发送信息后Server才会计算下一步。当同步性和精确性很重要时,这是最佳模式。特别是在client速度较慢或有多个传感器读取信息时。

2. 代码演示:同步与异步

对于同步与异步的演示,我会基于上篇文章 Carla简单入门-1 基本的API使用 中代码的基础上进行修改演示,感兴趣自己尝试的伙伴们可以在上篇文章文末找到完整代码。

a.首先是默认的异步模式 + 可变时间步长:

在这个模式下,我们并不需要对于模拟世界的设定或者traffic manager的设定做任何调整,我们只需要在进入while True循环之前设定好传感器,让它将读取到的数据存储到本地即可,代码如下:

    # 设定读取的数据的存储路径
    output_path = os.path.join("/home/ziyu/data/carla_pic", '%06d.png')
    # 令摄像头读取数据并存储
    camera.listen(lambda image: image.save_to_disk(output_path % image.frame))

下面是最终效果的参考图:
async_var_edited
我们可以看到,在默认的异步模式 + 可变时间步长下,因为server无需等待client的进程,模拟进行的非常快,让我们可以在同样现实时间内进行更长模拟时间的模拟(对比同步模式gif中车辆速度)同时这也造成了传感器记录画面的丢帧,卡顿。

b.其次是异步模式 + 固定时间步长:

在这个模式下,我们需要手动设定模拟世界的时间步长,注意我们需要一个很小的步长设定来使模拟正常运行,这里我们选择0.03秒作为步长,也即每33帧模拟世界经过1秒,如果我们的server足够快,例如每秒可以进行99帧的计算,那么现实中1秒我们模拟了模拟世界的3秒,异步模式 + 固定时间步长的设定代码如下:

    # 获得当前模拟世界的设定
    setting = world.get_settings()
    # 设定为异步模式
    setting.synchronous_mode = False
    # 将时间步长设定为固定的0.03秒
    setting.fixed_delta_seconds = 0.03
    # 应用设定
    world.apply_settings(setting)

    # 设定读取的数据的存储路径
    output_path = os.path.join("/home/ziyu/data/carla_pic", '%06d.png')
    # 令摄像头读取数据并存储
    camera.listen(lambda image: image.save_to_disk(output_path % image.frame))

下面是最终效果的参考图:
async_fixed_edited

我们可以看到,相比同步模式,异步模式 + 固定时间步长可以在同样现实时间内进行更长模拟时间的模拟(对比同步模式gif中车辆速度),但是还是无法避免传感器读取信息时由于异步模式造成的掉帧

c.然后是同步模式 + 可变时间步长:

这是一种几乎不可取的模式,这里就只简单做一下演示,不放代码了:
sync_var_edited
我们可以看到,在同步模式 + 可变时间步长设定下,虽然丢帧问题有所改善,但是行人的模拟行为出现了严重问题,这将导致我们的模拟无法正常进行,所以这种模式是基本不可取的模式。

d.然后是同步模式 + 固定时间步长:

这是我们想要进行精确模拟的最佳设定,这种设定下整个模拟的进程将有client端掌控,传感器记录的数据也会被完整读取并保存,是很多情况下的最优选设定,代码如下:

    # 获得当前模拟世界的设定
    setting = world.get_settings()
    # 设定为异步模式
    setting.synchronous_mode = True
    # 将时间步长设定为固定的0.03秒
    setting.fixed_delta_seconds = 0.05
    # 应用设定
    world.apply_settings(setting)
    # 获得当前客户端的交通管理器
    traffic_manager = client.get_trafficmanager()
    # 将交通管理器设置为同步模式
    traffic_manager.synchronous_mode = True
    # 设定存储摄像头数据的队列
    image_queue = queue.Queue()
    # 设定传感器每读取一帧数据后存储到队列中
    camera.listen(image_queue.put)
    # 设定数据的存储路径
    output_path = os.path.join("/home/ziyu/data/carla_pic", '%06d.png')

在我们设置好各种设定之后,就需要在whlie loop中通过Queue来阻挡更新世界的进程来达到同步的目的:

    while True:
        # 从world中获取观察者视角,并将观察者视角的方位信息设置为相机的对应方位信息
        world.get_spectator().set_transform(camera.get_transform())

        # 如果为同步模式设定
        if traffic_manager.synchronous_mode:
            # 更新模拟世界
            world.tick()
            # 从队列中读取传感器图像
            image = image_queue.get()
            # 将图像存储到本地路径
            image.save_to_disk(output_path % image.frame)
        # 如果为异步模式设定
        else:
            # 更新模拟世界
            world.wait_for_tick()

将图像存储到本地会消耗大量时间,而server因为需要等待client存储结束后才能计算下一帧所以模拟的速度会变得很慢。注意,存储图像并非开启同步模式的必须项,如果发现模拟进行太慢可以注释掉这行代码。

最后在我们的模拟运行结束后一定要记得将所有设定调回原来的模式,也即异步模式,因为同步模式下server会等待client的信息来计算下一帧,如果保持同步模式关闭client,server将因为接收不到client的信息而卡死:

    finally:
        # 停止并销毁所有controller
        for controller in world.get_actors().filter('*controller*'):
            controller.stop()
        # 销毁所有车辆
        for vehicle in world.get_actors().filter('*vehicle*'):
            vehicle.destroy()
        # 销毁所有行人
        for walker in world.get_actors().filter('*walker*'):
            walker.destroy()
  
        # 获得当前模拟世界设定
        settings = world.get_settings()
        # 设定为异步模式
        settings.synchronous_mode = False
        # 设定为可变时间步长
        settings.fixed_delta_seconds = None
        # 应用设定
        world.apply_settings(settings)

最终效果图如下:
sync_fixed_edited
可以看到,现在整个模拟中无论是行人还是车辆都是很顺畅的,没有丢帧的情况出现,但是相比异步模式,同样现实时间所模拟的模拟时长会短很多,在大多数我们需要模拟精确运行的时候这个设定都是最佳设定。

e.效果图演示:

分别为:
左上 异步 + 可变步长 右上 异步 + 固定步长
左下 同步 + 可变步长 右下 同步 + 固定步长
async_var_editedasync_fixed_edited
sync_var_editedsync_fixed_edited

3. 交通管理器

交通管理器(traffic manager)的概念我们之前也有简单提到过,在上一期使用基本API搭建简单拟真环境时我们也有使用到,现在我们来深入了解学习这个概念,官方文档链接:Traffic Manager - CARLA Simulator

a.traffic manager框架

Traffic manager会控制模拟环境中的所有处于“autopilot”模式下的vehicle来模拟一个真实城市的交通流,通过traffic manager我们可以自定义一些交通流的行为,比如说变道,跟车距离等来构造我们所需要的拟真环境。
Traffic manager 是基于client端建立的,这也是为什么我们需要在client端加入while loop 来使client端持续运行去控制autopilot的车辆。
Traffic manager的控制流程被分为了几个阶段,每个阶段都有自己独立的目标和控制内容,并且在各自不同的线程中运行以达到更高的效率。

上图是Traffic manager的内部架构图。每个组件的C++代码可以在LibCarla/source/carla/trafficmanager中找到,有兴趣的伙伴们可以去看看源代码。大致的运行逻辑概述如下:

第一步: 存储并更新模拟的当前状态。

Agent Lifecycle & State Management (ALSM)扫描世界,跟踪所有车辆和行人,并清理那些被销毁的条目。所有数据都是从server中获取的,ALSM是唯一调用服务器的组件。 车辆注册表包含一个包含autopilot模式车辆(由TM控制)的数组,以及一个不由TM控制的行人和车辆的列表。并且通过缓存( simulation state)存储模拟环境中所有车辆和行人的位置、速度和其他信息。

第二步: 计算每辆自动驾驶车辆的移动。

TM会根据存储的车辆位置,速度等信息来分别为每个车辆计算生成可执行的命令。这些计算被划分为不同的阶段,控制循环(control loop)通过在阶段之间创建同步屏障,确保所有计算在各个阶段之间保持一致。在当前阶段的所有车辆的计算完成之前,同步屏障会阻止进入下一个阶段。以下是每一辆车需要经过的所有阶段:

2.1 - 定位阶段(Localization stage)。

使用从内存地图(In-Memory Map)收集的附近路径点(waypoint)列表动态创建路径。在内存地图中,模拟地图被简化为一个路径点网格。在交叉路口,自动驾驶车辆会从左转与右转方向中随机选择。每辆车的路径由路径缓冲与车辆跟踪(Path Buffers & Vehicle Tracking,PBVT)组件存储和维护,以便在以后的阶段中轻松访问和修改。

2.2 - 碰撞阶段(Collision Stage)。

这一阶段TM会根据每个管辖车辆的周边环境,他们的路线规划以及车辆的边界框(bounding box)延伸来识别和规避潜在的碰撞危险。

2.3 - 红绿灯阶段(Traffic light Stage)。

与碰撞阶段类似,识别由红绿灯影响、停车标志和交叉口优先权引起的潜在路径危险。

2.4 - 运动规划阶段(Motion planner Stage)。
将上个两阶段的信息整合到一起,制定每一辆所管辖的车下一步的举动(比如变道、急刹或者正常行驶),设立下一个目标点,将该点传给PID控制器,通过PID控制器得到方向盘应该转动的角度和踩油门的力度、刹车的力度等CARLA命令,以在下一步应用。

2.5 - 车灯阶段(Vehicle light Stage)。

车辆灯光根据环境因素(例如光照条件、雾或雨)和车辆行为(例如,如果车辆在下一个交叉口将要左转/右转,则打开转向灯;或者如果正在刹车,则打开刹车灯)动态开关。

第三步: 将命令应用于模拟世界。
前面几步生成的命令会存储在 Command Array并以批的形式传递给Carla服务器。

b.traffic manager控制车辆行为的API

这里是官方文档中traffic manager所有API的内容: Python API - CARLA Simulator,可以在里面查找需要的方法,下面也有官方文档简单总结的traffic manager的API效果:

项目 描述
常规 - 创建连接到端口的 TM 实例
- 检索连接 TM 的端口。
安全条件 - 设置最小跟车距离(停车时)
- 设定速度百分比(相对于限速,例如限速60,百分比设置为70,车速就为42)
- 重置交通信号灯
碰撞管理 - 启用/禁用特定车辆之间的碰撞。
- 使车辆忽略所有其他车辆。
- 使车辆忽略所有行人。
- 使车辆忽略所有交通信号灯。
变道 - 强制变道,忽略可能的碰撞。
- 启用/禁用特定车辆的变道行为。
混合物理模式     - 启用/禁用混合物理模式。
- 更改启用物理运算的半径。

c.traffic manager的混合物理模式介绍

  • 混合物理模式允许用户对于所有车辆或者距离主车(属性为“hero”的车辆)大于一定距离的车辆禁用绝大部分物理计算来提高计算效率和运行帧数 Hybrid physics mode的优点是可以在保持较高物理精度的同时提高计算效率 ,使得用户可以更快速地迭代和测试自动驾驶算法。
  • 混合模式启用物理半径演示:
    下面用官方文档所提供的动态图来演示混合物理模式的工作模式。红色标签的是主车,绿色标签是启动混合物理模式的车辆(主车触发范围内),蓝色标签是禁用物理模式的车辆(主车触发范围外),通过设置混合物理模式的半径可以设定主车的触发范围

    注意只有在存在主车(即有车辆的属性被设置为hero时)混合物理模式才能生效
  • 设置代码:
    # 设置主车属性
    blueprint.set_attribute('role_name', 'hero' )
    # 生成主车
    world.spawn_actor(blueprint, spawn_point)
    # 开启混合物理模式
    traffic_manager.set_hybrid_physics_mode(True)
    # 设置混合物理模式的物理生效半径
    set_hybrid_physics_radius(50.0)

d.大地图下的traffic manager

  • 为了提升Carla模拟的性能,在大地图下Carla并不会一次性加载全部的地图,它会将地图划分为最大2km x 2km的片区来分区计算和渲染。Carla会渲染距离主车(ego_vehicle)一定距离内的地图片区,随着主车的移动,渲染的片区也会改变。
  • 这种特性也被应用到了traffic manager上,当被设定为autopilot模式由TM控制的车辆不处于渲染的片区或者距离主车距离大于一个值时,车辆会进入休眠状态,车辆运动将会像混合物理模式一样,不做绝大多数的物理运算,并且不会被渲染。
  • 当车辆进入到一个设定的与主车的距离内,车辆会被唤醒,所有物理计算将会正常化,并且会被正常渲染,我们可以通过API去定义这个唤醒车辆的距离值:
    # 获得世界模拟环境设定
    settings = world.get_settings()
    # 设定在距离主车距离小于2km时唤醒车辆
    settings.actor_active_distance = 2000
    # 应用设定
    world.apply_settings(settings)
  • 我们还可以通过另一种方法来节省其他交通流车辆所消耗的性能来提升模拟的效率,这种方法下我们可以将进入休眠模式的车辆重新生成在主车附近,这样我们就避免了让其他交通流做无用功,可以通过少量的车辆来模拟出一个复杂的交通环境,代码如下:
    # 开启休眠车辆重新生成模式
    traffic_manager.set_respawn_dormant_vehicles(True)
    # 设定重新生成车辆范围,为距离主车25 - 700 米
    traffic_manager.set_boundaries_respawn_dormant_vehicles(25,700)

4.后话:

a. 完整代码分享:

import math
import os
import queue
import random
import sys

import carla



def main():
    try:
        # setup client并且加载我们所需要的地图
        client = carla.Client('localhost', 2000)
        client.set_timeout(120.0)
        client.load_world('Town01')
        num_walkers = 20
        num_vehicle = 20
        # 设置跑步的行人比例
        percentage_pedestrians_running = 0.25
        # 设置横穿马路的行人比例
        percentage_pedestrians_crossing = 0.15

        # 获取我们client所对应的world
        world = client.get_world()
        # 获得这个world中的观察者
        spectator = world.get_spectator()

        # 获得当前客户端的交通管理器
        traffic_manager = client.get_trafficmanager()

        # 获得观察者的方位信息
        transform = spectator.get_transform()

        # 根据观察者默认方位设置新的方位
        location = transform.location + carla.Location(x=-30, z=20)
        rotation = carla.Rotation(pitch=-20, yaw=-20, roll=0)
        new_transform = carla.Transform(location, rotation)

        # 将观察者设置到新方位上
        spectator.set_transform(new_transform)

        # 获得整个的blueprint库并从中筛选出车辆
        vehicle_blueprints = world.get_blueprint_library().filter('*vehicle*')

        # 获得整个的blueprint库并从中筛选出行人
        ped_blueprints = world.get_blueprint_library().filter('*pedestrian*')

        # 通过world获得map并获得所有可以生成车辆的地点
        vehicle_spawn_points = world.get_map().get_spawn_points()

        # 通过world获得所有可以生成行人的地点并存储
        ped_spawn_points = []
        for i in range(num_walkers):
            spawn_point = carla.Transform()
            loc = world.get_random_location_from_navigation()
            if loc is not None:
                spawn_point.location = loc
                ped_spawn_points.append(spawn_point)

        # 在地图上随机生成num_vehicle辆车,每辆车为车辆蓝图库中的随机车辆
        for i in range(0, num_vehicle):
            world.try_spawn_actor(random.choice(vehicle_blueprints),
                                  random.choice(vehicle_spawn_points))

        # 创建用来存储行人,行人速度设置和行人控制器的list
        walker_batch = []
        walker_speed = []
        walker_ai_batch = []

        # 在地图上随机生成num_walkers个行人,每个行人为行人蓝图库中的随机行人,并设定行人的移动速度
        for j in range(num_walkers):
            walker_bp = random.choice(ped_blueprints)

            # 取消行人无敌状态
            if walker_bp.has_attribute('is_invincible'):
                walker_bp.set_attribute('is_invincible', 'false')

            # 设置行人的移动速度
            if walker_bp.has_attribute('speed'):
                if random.random() > percentage_pedestrians_running:
                    # 将对应行人速度设置为走路速度
                    walker_speed.append(walker_bp.get_attribute('speed').recommended_values[1])
                else:
                    # 将对应行人速度设置为跑步速度
                    walker_speed.append(walker_bp.get_attribute('speed').recommended_values[2])
            # 从可生成行人的生成点中随机选择并生成随机行人,之后将生成的行人添加到同一批中
            walker_batch.append(world.try_spawn_actor(walker_bp, random.choice(ped_spawn_points)))

        # 从蓝图库中寻找控制行人行动逻辑的控制器
        walker_ai_blueprint = world.get_blueprint_library().find('controller.ai.walker')

        # 为整批行人各自生成对应的控制器,并把控制器添加到代表批量控制器的列表中
        for walker in world.get_actors().filter('*pedestrian*'):
            walker_ai_batch.append(world.spawn_actor(walker_ai_blueprint, carla.Transform(), walker))

        # 批量启动行人控制器,并设置控制器参数
        for i in range(len(walker_ai_batch)):
            # 启动控制器
            walker_ai_batch[i].start()
            # 通过控制器设置行人的目标点
            walker_ai_batch[i].go_to_location(world.get_random_location_from_navigation())
            # 通过控制器设置行人的行走速度
            walker_ai_batch[i].set_max_speed(float(walker_speed[i]))

        # 设置行人横穿马路的参数
        world.set_pedestrians_cross_factor(percentage_pedestrians_crossing)

        # 从world中获取车辆,并将每辆车的autopilot模式设置为打开
        for vehicle in world.get_actors().filter('*vehicle*'):
            vehicle.set_autopilot()

        # 设定主车生成位置
        ego_spawn_point = vehicle_spawn_points[0]
        # 从蓝图库中挑选我们需要的主车蓝图
        ego_bp = world.get_blueprint_library().find('vehicle.mini.cooper_s_2021')
        # 设置主车蓝图的属性中的角色名
        ego_bp.set_attribute('role_name', 'hero')
        # 生成主车
        ego_vehicle = world.spawn_actor(ego_bp, ego_spawn_point)
        # 将主车设置为自动驾驶模式
        ego_vehicle.set_autopilot()

        # 从蓝图库中寻找rgb相机
        camera_bp = world.get_blueprint_library().find('sensor.camera.rgb')
        # 设置rgb相机的方位信息
        camera_transform = carla.Transform(carla.Location(x=-8, y=0, z=5),
                                           carla.Rotation(pitch=10, yaw=0, roll=0))
        # 生成rgb相机并用SpringArmGhost的方式绑定到主车上
        camera = world.spawn_actor(camera_bp, camera_transform, attach_to=ego_vehicle,
                                   attachment_type=carla.libcarla.AttachmentType.SpringArmGhost)

        # 获得当前模拟世界的设定
        setting = world.get_settings()
        # 设定为异步模式
        setting.synchronous_mode = True
        # 将时间步长设定为固定的0.03秒
        setting.fixed_delta_seconds = 0.05
        # 应用设定
        world.apply_settings(setting)

        # 将交通管理器设置为同步模式
        traffic_manager.synchronous_mode = True
        # 通过交通管理器设置所有车辆相对于限速的差值,这里为负即为所有车辆都会i超速行驶
        traffic_manager.global_percentage_speed_difference(-30)
        # 设定存储摄像头数据的队列
        image_queue = queue.Queue()
        # 设定传感器每读取一帧数据后存储到队列中(同步模式)
        camera.listen(image_queue.put)
        # 设定数据的存储路径
        output_path = os.path.join("/home/ziyu/data/carla_pic", '%06d.png')

        # 令摄像头读取数据并存储(异步模式)
        #camera.listen(lambda image: image.save_to_disk(output_path % image.frame))

        while True:
            # 从world中获取观察者视角,并将观察者视角的方位信息设置为相机的对应方位信息
            world.get_spectator().set_transform(camera.get_transform())

            # 如果为同步模式设定
            if traffic_manager.synchronous_mode:
                # 更新模拟世界
                world.tick()
                # 从队列中读取传感器图像
                image = image_queue.get()
                # 将图像存储到本地路径(同步模式)
                # image.save_to_disk(output_path % image.frame)
            # 如果为异步模式设定
            else:
                # 更新模拟世界
                world.wait_for_tick()

    finally:
        # 停止并销毁所有controller
        for controller in world.get_actors().filter('*controller*'):
            controller.stop()
        # 销毁所有车辆
        for vehicle in world.get_actors().filter('*vehicle*'):
            vehicle.destroy()
        # 销毁所有行人
        for walker in world.get_actors().filter('*walker*'):
            walker.destroy()

        # 获得当前模拟世界设定
        settings = world.get_settings()
        # 设定为异步模式
        settings.synchronous_mode = False
        # 设定为可变时间步长
        settings.fixed_delta_seconds = None
        # 应用设定
        world.apply_settings(settings)

if __name__ == '__main__':

    try:
        main()
    except KeyboardInterrupt:
        pass
    finally:
        print('\ndone.')

b. 参考文档:

CARLA Simulator

Carla的官方文档写的非常详细和完善,英语能力强的伙伴们一定要活用Cltr+f,很多问题都能在其中找到答案。另外,笔者也在积极学习Carla当中,如果文章有错处希望大家指出,有好的观点以及Carla的经验也都十分欢迎分享,让我们一起共同学习进步,共勉!

下期预告:

解决了同步问题,我们就可以自由的添加传感器来读取数据了,下一篇文档,我们会详细了解Carla里所提供的传感器类型,自定义方法等等细节。