基于CALRA和DQN实现自动驾驶的仿真
基于carla与DQN实现自动驾驶的仿真
1. 安装
1.1 配置虚拟环境
在anaconda prompt逐条运行下面命令,单独配置虚拟环境。car是名字,可以自己取,但是python必须是3.7。创建好后使用pip命令下载配置所有的库(方便换源,conda换源不太方便)。
1 | conda create -n car python=3.7 |
1.2 配置carla模拟器
CARLA Simulator 点击进入官网,可以查看版本信息与官方文档。官方文档是我们学习的基础。下载模拟器时,应该进入官网查看最新版本,再前往carla的github主页进行下载,确保下载的是最新版本。因为只有最新版本的carla能通过pip命令安装依赖库,其他版本的安装非常繁琐(在下面第三大点有详细描述)。版本错误程序会直接崩溃。Releases · carla-simulator/carla (github.com) 根据版本下载对应的压缩包或使用git命令克隆下来。注意carla只支持Linux的乌班图系统和windows系统,不支持mac系统,mac用户使用虚拟机下载。
1.3 仿真环境初体验
解压下载好的压缩包,运行CarlaUE4.exe。windows可能会报“发布者未知”,这里点击信任,或直接关闭防火墙。carla的内核是虚幻4,需要较高的配置支持,首次启动请等待片刻。进去后是这样的画面:
carla有好几个地图,还可以下载地图拓展,这里是默认的一号地图,后面可以在代码中修改。使用wasd键控制前后左右,按住鼠标中键可以转换视角,细看城市中的内容。后面可以在代码中修改地图。
使用conda命令窗口,激活car环境,使用cd命令转到WindowsNoEditor\PythonAPI\examples,使用python manual_control.py命令运行这个代码,可以得到一辆车,自由操控,使用Back键还能换别的来玩。
即使是这样,城市也非常空旷。接下来我们来看看如何使用carla进行自动驾驶的仿真。
2. CARLA Simulator的核心概念
2.1 客户端
2.1.1客户端
客户端是 CARLA 架构中的主要元素之一。 它们连接到服务器、检索信息和命令更改。 这是通过脚本完成的。 客户端识别自己,并连接到世界,然后使用模拟进行操作。
除此之外,客户还可以访问高级 CARLA 模块、功能和应用命令批处理。 本节仅介绍命令批处理。 这些对于生成大量actors之类的基本事物很有用。 其余功能更为复杂,将在各自页面的高级步骤中进行介绍。
查看 Python API 参考中的 carla.Client 以了解该类的特定方法和变量。
2.1.2 客户端的创建
使用carla.Client方法创建。需要三个参数,标识它的 IP 地址,以及用于与服务器通信的两个 TCP 端口。可选的第三个参数设置工作线程的数量。 默认情况下,这设置为全部 (0)。默认情况下,CARLA 使用本地主机 IP 和端口 2000 进行连接,但这些可以随意更改。 在这种情况下,第二个端口将始终为 n+1, 2001。
1 | client = carla.Client('localhost', 2000) |
2.1.3 设置客户端超时时间
创建客户端后,使用set_timeout方法设置其超时时间。 这限制了所有网络操作,因此这些操作不会永远阻塞客户端。 如果连接失败,将返回错误。
可以连接多个客户端,因为一次运行多个脚本是很常见的。
超时时间以秒为单位。
1 | client.set_timeout(10.0) |
2.1.4 检查版本
客户端和服务器具有不同的 libcarla 模块。 如果版本不同,可能会出现问题。 这可以使用 get_client_version() 和 get_server_version() 方法进行检查。
1 | print(client.get_client_version()) |
2.1.5虚拟世界连接
客户端可以相当轻松地连接和检索当前世界。
1 | world = client.get_world() |
客户端还可以获取可用地图列表来更改当前地图。 这将摧毁当前的世界并创造一个新的世界。
1 | print(client.get_available_maps()) |
每个世界对象都有一个 ID 或形象地称作“情节”。 每次客户端调用 load_world() 或 reload_world() 时,前一个都会被销毁。 新的“情节”是从头开始创建的。 在此过程中不会重新启动虚幻引擎。
2.1.6 运行命令
命令是对一些最常见的 CARLA 方法的改编,可以批量应用。 例如,command.SetAutopilot 等价于 Vehicle.set_autopilot(),为车辆启用自动驾驶仪。 但是,使用 Client.apply_batch() 或 Client.apply_batch_sync() 方法,可以在一个模拟步骤中应用一系列命令。 这对于通常应用于甚至数百个元素的方法变得非常有用。
以下示例使用批处理一次性销毁车辆列表。
1 | client.apply_batch([carla.command.DestroyActor(x) for x in vehicles_list]) |
因此我们后面创建actors时,会开一个列表来存放方便销毁。
2.1.7 其他客户端实用程序
客户端对象的主要目的是获取或改变世界,并应用命令。 但是,它还提供对一些附加功能的访问。
- 交通管理模块:该模块负责每辆设置为自动驾驶的车辆,以重建城市交通。
- 录像模块:允许重新制定以前的模拟。 通过录像快照来总结每帧的模拟状态。
后面会重新提及。
2.2 虚拟世界
2.2.1 虚拟世界
模拟测试的主要部分。 它的实例应该由客户端检索。 它不包含世界本身的模型,它是 Map 类的一部分。 相反,可以从此类访问大部分信息和常规设置。
- 模拟中的参与者和旁观者
- 蓝图库
- 地图
- 模拟设置
- 快照
- 天气和灯光管理器
它的一些最重要的方法是 getter,精确地检索这些元素的信息或实例。 查看 carla.World 以了解更多信息。、
carla.World PythonAPI reference:
Instance Variables
- id (int):与这个世界关联的 episode 的 id
- debug (carla.DebugHelper):负责创建不同的 shapes for debugging
- id (int):与这个世界关联的 episode 的 id
Methods
- apply_settings:将 carla.WorldSettings 对象包含的设置应用于仿真,并返回实现他们的帧 id
- cast_ray:将射线从指定的 initial_location 投射到 final_location,检测与射线相交的所有几何体,并按顺序返回到 carla.LabelledPoint 列表
- enable_environment_objects:启用或禁用由 id 标识的一组 EnvironmentObject。这些对象将在关卡中显示或消失
- freeze_all_traffic_lights:冻结或解冻场景中所有的交通信号灯
- ground_projection:将指定的点向下 (0,0,-1) 投射到场景中,并返回 carla.Labelled 对象,这个对象是与射线相交的第一个几何体(通常是地面)
- load_map_layer:将选定的图层加载到关卡
- on_tick:此方法用于异步模式,从客户端启用传入的回调 callback
- project_point:将指定的点投影到场景中的所需方向
- remove_on_tick:停止从 on_tick 开始的 callback_id 的回调
- reset_all_traffic_lights:将地图中所有交通信号灯的周期重置为初始状态
- spawn_actor:在世界中创建 Actor
- tick:此方法用于同步模式,发从 tick 给 server,返回服务器计算好的新 id
- try_spawn_actor:与 spawn_actor,但失败使返回 None 而不是异常
- unload_map_layer:将选定的图层卸载
- wait_for_tick:此方法用于异步模式,使客户端等服务器 tick
- apply_settings:将 carla.WorldSettings 对象包含的设置应用于仿真,并返回实现他们的帧 id
Getters
- get_actor:通过 id 查找 Actor,如果没找到返回 None
- get_actors:检索 carla.Actor 列表,使用提供的 id 列表返回所有的 Actor
- get_blueprint_library:返回可以在世界中创建的 Actor 列表
- get_environment_objects:返回带有请求的语义的 EnvironmentObject 列表
- get_level_bbs:返回在世界空间中具有位置和渲染的边界框数组
- get_lightmanager:返回 carla.LightManager 的实例,该实例可用于处理场景中的灯光
- get_map:像服务器查询包含地图文件的 XDOR,将其解析为 carla.Map 并返回
- get_random_location_form_navigation:只能和行人一起使用。检索被用作目的地的随机位置 go_to_location 的 carla.WalkerAIController
- get_settings:返回一个包含仿真数据的对象,例如客户端和服务器之间同步或渲染模式
- get_snapshot:返回某个时刻的世界快照,包括有关参与者的所有信息
- get_spectator:返回观察者。观察者用来作为相机,并控制仿真窗口中的视图
- get_traffic_light:提供一个 landmark,返回他描述的交通灯对象
- get_traffic_sign:提供一个 landmark,返回他描述的交通标志对象
- get_vehicles_light_states:返回一个字典,他的 key 是 carla.Actor id,value 是 carla.VehicleLightState
- get_weather:检索一个对象,该对象包含当前仿真的天气参数,主要是 云、雨、风和太阳的位置
- get_actor:通过 id 查找 Actor,如果没找到返回 None
Setters
- set_weather:设定天气
Dunder methods
__str__
:解析并打印世界内容,作为其当前状态的简要报告
2.2.2 元素
世界有不同的方法,这与允许不同功能的“参与者”相关。
- 生成actors(但不破坏它们)
- 让每个元素都在现场,或者特别找一个
- 访问蓝图库
- 访问旁观者视角
- 检索适合生成元素的随机位置。
actors就是“演员”,代表着世界里可以移动的物体,包括汽车,传感器(因为传感器要安在车身上)以及行人。
2.2.3 天气
天气本身不是一个类,而是一组可从世界访问的参数。 参数化包括太阳方向、云量、风、雾等等。 辅助类 carla.WeatherParameters 用于定义自定义天气。
1 | weather = carla.WeatherParameters( |
有一些天气预设可以直接应用于世界。 这些在 carla.WeatherParameters 中列出,并可作为枚举访问。
1 | world.set_weather(carla.WeatherParameters.WetCloudySunset) |
天气也可以使用 CARLA 提供的两个脚本进行自定义。
environment.py
(inPythonAPI/util
) — 提供对天气和灯光参数的访问,以便实时更改这些参数。
environment.py中的可选参数:
1 | -h, --help show this help message and exit |
dynamic_weather.py
(inPythonAPI/examples
) — 启用开发人员为每个 CARLA 地图准备的特定天气周期。
dynamic_weather.py的可选参数:
1 | -h, --help show this help message and exit |
天气的变化不会影响世界中的车辆的物理性质。 它们只是相机传感器可以捕捉到的视觉效果。当 sun_altitude_angle < 0 时,夜间模式开始,这被认为是日落。 这是灯光变得特别重要的时候。世界中路灯会自动打开。
2.2.4 光线
路灯有这些相关类:
- carla.Light:地图开发者放置的 lights,通过 carla.Light 对象访问
- carla.LightState:颜色和强度等属性,在 light_state 中设置
- carla.LightGroup:使用 light_group 进行分类,例如 路灯、建筑物灯…
- carla.LightManager:可以在一个调用中被检索来处理一组灯光
当模拟进入夜间模式时,路灯会自动打开。 灯光由地图的开发人员放置,并可作为 carla.Light 对象访问。 颜色和强度等属性可以随意更改。 carla.LightState 类型的变量 light_state 允许在一次调用中设置所有这些。
路灯使用 carla.LightGroup 类型的属性 light_group 进行分类。 这允许将灯分类为路灯、建筑灯…… carla.LightManager 的一个实例可以被检索以在一次调用中处理灯组。
1 | # Get the light manager and lights |
- 车灯必须由用户打开/关闭。 每辆车都有一组在 carla.VehicleLightState 中列出的灯。 到目前为止,并非所有车辆都集成了灯光。 以下是撰写本文时可用的列表。
自行车:它们都有一个前后位置灯。
摩托车: 雅马哈和哈雷戴维森车型。
汽车:奥迪TT、雪佛兰、道奇(警车)、Etron、林肯、野马、特斯拉3S、大众T2以及来到CARLA的新模组。
可以使用 carla.Vehicle.get_light_state 和 carla.Vehicle.set_light_state 方法随时检索和更新车辆的灯光。 这些使用二进制操作来自定义灯光设置。
1 | # Turn on position lights |
也可以使用天气部分中描述的 environment.py 实时设置灯光。
2.2.5 调试
世界对象有一个 carla.DebugHelper 对象作为公共属性。 它允许在模拟过程中绘制不同的形状。 这些用于跟踪正在发生的事件。 以下示例将在演员的位置和旋转处绘制一个红色框。
1 | debug = world.debug |
此示例在 carla.DebugHelper 中的一个片段中进行了扩展,该片段显示了如何为世界快照中的每个角色绘制框。
2.2.6 快照
包含模拟中每个角色在单个帧中的状态。 一种带有时间参考的世界静止图像。 信息来自相同的模拟步骤,即使在异步模式下也是如此。
1 | # Retrieve a snapshot of the world at current frame. |
carla.WorldSnapshot 包含 carla.Timestamp 和 carla.ActorSnapshot 列表。 可以使用演员的 ID 搜索演员快照。 快照列出了其中出现的演员的 ID。
1 | timestamp = world_snapshot.timestamp # Get the time reference |
2.2.7 世界设置
世界可以访问一些用于模拟的高级配置。 这些决定了渲染条件、模拟时间步长以及客户端和服务器之间的同步。 它们可以从助手类 carla.WorldSettings 中访问。
目前,默认的 CARLA 以最佳图形质量、可变时间步长和异步运行。 要进一步了解此问题,请查看“高级步骤”部分。 有关同步和时间步长以及渲染选项的页面可能是一个很好的起点。
2.3 蓝图
CARLA 中的 Actor 是在模拟中执行动作的元素,它们可以影响其他 Actor。 CARLA 中的参与者包括车辆和步行者,还包括传感器、交通标志、交通灯和观众。 对如何操作它们有充分的了解是至关重要的。
本节将介绍生成、销毁、类型以及如何管理它们。
2.3.1 蓝图
这些布局允许用户将新演员顺利融入模拟。 它们是带有动画和一系列属性的已制作模型。 其中一些是可修改的,而另一些则不是。 这些属性包括车辆颜色、激光雷达传感器中的通道数量、步行者的速度等等。
可用的蓝图及其属性列在蓝图库中。 车辆和步行者蓝图有一个世代属性,指示它们是新的(第 2 代)还是旧的(第 1 代)资产。
2.3.2管理蓝图库
carla.BlueprintLibrary 类包含一个 carla.ActorBlueprint 元素列表。 世界对象可以提供对它的访问。
1 | blueprint_library = world.get_blueprint_library() |
蓝图有一个 ID 来识别它们以及由此产生的演员。 可以读取该库以查找特定 ID、随机选择蓝图或使用通配符模式过滤结果。
1 | # Find a specific blueprint. |
除此之外,每个 carla.ActorBlueprint 都有一系列 carla.ActorAttribute 可以获取和设置。
1 | is_bike = [vehicle.get_attribute('number_of_wheels') == 2] |
注意:有些属性无法修改,请在蓝图库中查看
属性有一个 carla.ActorAttributeType 变量。 它从枚举列表中声明其类型。 此外,可修改属性带有推荐值列表。
1 | for attr in blueprint: |
用户可以创建自己的交通工具。查看教程(资产)来学习。贡献者可以将他们的新内容添加到 CARLA。
2.4 演员的生命周期
本节提到了关于参与者的不同方法。PythonAPI 提供了在一个框架中应用最常见的批处理命令的命令。
2.4.1 生成
世界对象负责生成actors并跟踪它们。 生成只需要一个蓝图和一个 carla.Transform 来说明 Actor 的位置和旋转。
这个世界有两种不同的方法来产生Actor。
- spawn_actor() 生成失败会报错
- try_spawn_actor() 生成失败返回none
1 | transform = Transform(Location(x=230, y=195, z=40), Rotation(yaw=180)) |
注意:CARLA 使用虚拟引擎坐标系统。记住 carla.Rotation()构造函数定义为(俯仰,偏航,滚动) ,它不同于一般的虚拟引擎编辑器(滚动,俯仰,偏航)。xyz坐标的单位是m
这里的俯仰,偏航,滚动事实上是pitch, yaw, roll,在笛卡尔坐标系中,pitch是围绕X轴旋转,也叫做俯仰角,yaw是围绕Y轴旋转,也叫偏航角,roll是围绕Z轴旋转,也叫翻滚角。如下图所示
如果在指定位置发生碰撞,actor 将不会生成。 无论这发生在静态对象还是其他Actor身上。 可以尝试避免这些不希望的生成碰撞。
- map.get_spawn_points() 用于车辆。 返回推荐的生成点列表。
1 | spawn_points = world.get_map().get_spawn_points() |
- world.get_random_location() 用于行人。 返回人行道上的随机点。 同样的方法用于为步行者设置目标位置。
1 | spawn_point = carla.Transform() |
一个actor在生成时可以附加到另一个actor上。 演员跟随他们所依附的父类。 这对传感器特别有用。 附件可以是刚性的(适合检索精确数据),也可以根据其父项轻松移动。附加类型由carla.AttachmentType 定义。
注意:当产生附加到另一个actor上的actor时,提供的transformer必须相对于父actor。
下一个示例将摄像头固定在车辆上,因此它们的相对位置保持固定。
1 | # Attach Actor |
生成后,世界对象会将演员添加到列表中。 这可以很容易地搜索或迭代。
1 | actor_list = world.get_actors() |
上面的都是CARLA官方文档给我们写好的。实际上只有几个是常用的。如果我们想生成一个Actor, 必须要先定义它的蓝图(Blueprint),这就好比造房子前要先绘制设计图一样。
1 | # 拿到这个世界所有物体的蓝图 |
2.4.2管理
carla.Actor 主要由 get() 和 set() 方法组成,用于管理地图周围的演员。
1 | # Handle Actor |
可以禁用 actor 的物理,将其冻结在某位置
1 | actor.set_simulate_physics(False) |
除此之外,actor还有他们的蓝图提供的标签,这些标签主要用于语义分割传感器。
注意:大多数方法异步地向模拟器发送请求。模拟器在每次更新时解析它们的时间是有限的。用 set ()方法充斥模拟器将会积累一个显著的延迟。
2.4.3销毁
当 Python 脚本完成时,Actor 不会被销毁,需要明确地手动销毁他们。销毁会阻塞仿真,直到完成。
1 | destroyed_sucessfully = actor.destroy() # Returns True if successful |
2.5Actor类型
2.5.1 传感器
传感器是产生数据流的actor。在官方文档的第四部分和本文档接下来的部分会单独介绍。现在,让我们看看一个常见的传感器产生周期。此示例生成一个摄像机传感器,将其连接到车辆上,并告诉摄像机将生成的图像保存到磁盘。
1 | camera_bp = blueprint_library.find('sensor.camera.rgb') |
- 传感器也有蓝图,可以设置属性
- 大多数传感器将连接到车辆,收集有关其周围环境的信息
- 传感器监听数据。接收到数据后,他们调用 Lambda表达式 描述的函数
只设置了以上的回调,并不会保存传感器数据,需要调用 world.tick() 获取传感器数据:
1 | # Main loop |
运行脚本的控制台打印世界的当前帧数,rgb 图像会保存在磁盘里。默认情况下 sensor.camera.rgb 产生的图像分辨率是 800 x 600,这个可以设定。
2.5.2 旁观者
由虚幻引擎放置以提供游戏内视角。 它可以用来移动模拟器窗口的视图。 以下示例将移动旁观者演员,以将视线指向所需的车辆。
1 | spectator = world.get_spectator() |
2.5.3 交通标志和交通灯
到目前为止,在 CARLA 中,只有停止点、生成和红绿灯被认为是演员。 其余的 OpenDRIVE 标志可从 API 作为 carla.Landmark 访问。 使用这些实例可以访问它们的信息,但它们在模拟中不作为参与者存在。 在下方的“地图和导航”有对地标更详细的解释。
当模拟开始时,使用 OpenDRIVE 文件中的信息自动生成停止、生成和交通灯。这些都不能在蓝图库中找到,因此不能产生。
CARLA 地图在 OpenDRIVE 文件中没有交通标志和灯光,这些都是由开发人员手动放置的。
道路图本身并没有界定交通标志。相反,他们有一个 carla.BoundingBox边界框影响其中的车辆。
1 | #Get the traffic light affecting a vehicle |
交通信号灯出现在路口。 与任何actor一样,他们有自己的唯一 ID,但也有交汇点的组 ID。 为了识别同一组中的交通灯,使用了杆 ID。
同一组中的红绿灯遵循一个循环。 第一个设置为绿色,而其余的则保持为红色。 活跃的在绿色、黄色和红色上花费几秒钟,所以有一段时间所有的灯都是红色的。 然后,下一个红绿灯开始循环,前一个红绿灯与其他红绿灯一起冻结。
可以使用 API 设置交通灯的状态。 在每个状态上花费的秒数也是如此。 carla.TrafficLightState 将可能的状态描述为一系列枚举值。
交通信号灯的状态可以通过 API 设置:
1 | # Set traffic light state |
注意:只有当红灯亮时,车辆才会注意到红灯。
2.5.4 车辆
carla.Vehicle 是一种特殊类型的演员。 它包含模拟轮式车辆物理特性的特殊内部组件。 这是通过应用四种不同的控件来实现的:
- carla.VehicleControl 为油门、转向、刹车等驾驶命令提供输入。
1 | vehicle.apply_control(carla.VehicleControl(throttle=1.0, steer=-1.0)) |
- carla.VehiclePhysicsControl 定义了车辆的物理属性并包含另外两个控制器:
- carla.GearPhysicsControl 控制齿轮。
- carla.WheelPhysicsControl 提供对每个车轮的特定控制。
1 | vehicle.apply_physics_control(carla.VehiclePhysicsControl(max_rpm = 5000.0, center_of_mass = carla.Vector3D(0.0, 0.0, 0.0), torque_curve=[[0,400],[5000,400]])) |
车辆有一个碰撞体积 carla.BoundingBox 。 此边界框允许将物理应用到车辆并检测碰撞。
1 | box = vehicle.bounding_box |
通过启用扫描轮碰撞参数可以改善车轮的物理特性。 默认的车轮物理场对每个车轮使用从轴到地板的单射线投射,但是当启用扫描车轮碰撞时,会检查车轮的整个体积以防止碰撞。 它可以这样启用:
1 | physics_control = vehicle.get_physics_control() |
车辆包括其他独有的功能:
- 自动驾驶模式将为车辆订阅交通管理器以模拟真实的城市状况。 这个模块是硬编码的,不是基于机器学习的。
1 | vehicle.set_autopilot(True) |
- 车灯必须由用户打开和关闭。 每辆车都有一组在 carla.VehicleLightState 中列出的灯。 并非所有车辆都集成了照明灯。 在撰写本文时,集成车灯的车辆如下:
自行车:所有自行车都有前后位置灯。
摩托车:雅马哈和哈雷戴维森车型。
汽车:奥迪 TT、雪佛兰 Impala、道奇警车、道奇 Charger、奥迪 e-tron、林肯 2017 和 2020、野马、特斯拉 Model 3、特斯拉 Cybertruck、大众 T2 和梅赛德斯 C 级。
可以使用方法 carla.Vehicle.get_light_state 和 carla.Vehicle.set_light_state 随时检索和更新车辆的灯光。 这些使用二进制操作来自定义灯光设置。
1 | # Turn on position lights |
根据部分博主的内容实验以及本机实验,二进制操作灯光并不稳定(在很多情况下失败),因此建议把上方的第一第二行代码改为current_lights = carla.VehicleLightState.Position直接设置。
2.5.5行人
carla.Walker 的工作方式与车辆类似。 对它们的控制由controllers提供。
- carla.WalkerControl 以一定的方向和速度移动行人。 它还允许他们跳跃。
- carla.WalkerBoneControl 提供对 3D 骨架的控制。
步行者可以由 AI controller 控制。 他们没有自动驾驶模式。 carla.WalkerAIController Actor 围绕它所连接的 Actor 移动。
1 | walker_controller_bp = world.get_blueprint_library().find('controller.ai.walker') |
每个 AI controller都需要初始化、目标和速度(可选)。 停止控制器的工作方式相同。
1 | ai_controller.start() |
注意:AI controller是没有身体的,没有物理。它不会出现在现场。而且,相对于其父节点的位置(0,0,0)不会引起冲突。
当行人到达目标位置时,他们会自动步行到另一个随机点。 如果无法到达目标点,步行者将前往离他们当前位置最近的点。
注意:如果要删除AI行人,需要停止AI controller,并且清除所有的actor和controller。
carla.Client 中的一个片段使用批量生成大量步行者并让他们四处游荡。
2.6 地图
地图包括城镇的 3D 模型及其道路定义。 地图的道路定义基于 OpenDRIVE 文件,这是一种标准化、带注释的道路定义格式。 OpenDRIVE 标准 1.4 定义道路、车道、交叉口等的方式决定了 Python API 的功能以及决策背后的推理。
Python API 充当高级查询系统来导航这些道路。 它不断发展以提供更广泛的工具集。后面会单独提到python API
2.6.1 改变地图
要改变地图,世界也必须改变。 模拟将从头开始重新创建。 您可以在新世界中使用同一张地图重新开始,也可以同时更改地图和世界:
- reload_world() 在同一张地图里创建一个新的实例
- load_world() 改变当前地图,并创建一个新的世界
每张地图都有一个与当前加载的城市名称相匹配的name
属性,例如 Town01,可以用client.get_available_maps()来获取可用地图列表。
1 | # 加载地图 |
2.6.2 地标
OpenDRIVE 种定义的交通标志将转换为 CARLA 的 landmark 对象,使用 API 查询:
- carla.Landmark 对象代表 OpenDRIVE 信号。 此类的属性和方法描述了地标及其影响范围。
- carla.LandmarkOrientation 说明地标相对于道路几何定义的方向。
- carla.LandmarkType 包含常见的地标类型,以便于转换为 OpenDRIVE 类型。
- carla.Waypoint 可以获得位于其前方一定距离的地标。 可以指定要获取的地标类型。
- carla.Map 检索地标集。 它可以返回地图中的所有地标,或者具有共同 ID、类型或组的地标。
- carla.World 充当地标与在模拟中代表它们的 carla.TrafficSign 和 carla.TrafficLight 之间的中介。
1 | # 获取地标 |
2.6.3航点
carla.Waypoint 是 CARLA 世界中的 3D 定向点,对应于 OpenDRIVE 车道。 与航点相关的一切都发生在客户端; 只需与服务器通信一次即可获取包含航点信息的地图对象。
每个航路点都包含一个 carla.Transform,它说明了它在地图上的位置以及包含它的车道的方向。 变量 road_id、section_id、lane_id 和 s 对应于 OpenDRIVE 道路。 路点的 id 由这四个值的哈希组合构成。
航路点保存有关包含它的车道的信息。 此信息包括车道的左右车道标记、确定车道是否在交叉路口内的布尔值、车道类型、宽度和车道变更权限。
注意:同一条道路上距离小于2厘米的路标共用相同的id。
航点还包含他所在的车道信息。
1 | # Access lane information from a waypoint |
2.6.4 车道
OpenDRIVE 标准 1.4 定义的车道类型作为一系列枚举值转换为 carla.LaneType 中的 API。
车道周围的车道标记可通过 carla.LaneMarking 访问。 车道标记由一系列变量定义:
- 颜色:carla.LaneMarkingColor 是定义标记颜色的枚举值。
- 变道:carla.LaneChange 说明车道是否允许左转、右转、两者都允许或不允许。
- 类型:carla.LaneMarkingType 是根据 OpenDRIVE 标准定义标记类型的枚举值。
- 宽度:定义标记的厚度。
下面的示例显示了获取有关特定航路点的车道类型、车道标记和车道变更权限的信息:
1 | # Get the lane type where the waypoint is |
2.6.5 路口
carla.Junction 表示 OpenDRIVE 连接点。 此类包含一个带有边界框的交叉路口,以识别其中的车道或车辆。
carla.Junction 类包含 get_waypoints 方法,该方法为路口内的每条车道返回一对航路点。 每对都位于交汇点边界的起点和终点。
1 | # 获取路口 |
2.6.6 环境对象
CARLA 地图上的每个对象都有一组相关变量,可在此处找到。 详见 carla.EnvironmentObject。这些变量中包含一个唯一 ID,可用于切换该对象在地图上的可见性。 您可以使用 Python API 根据语义标签获取每个环境对象的 ID:
1 | # Get the building in the world |
2.7 CARLA中的导航
CARLA 中的导航是通过 Waypoint API 管理的,它是 carla.Waypoint 和 carla.Map 方法的组合。
客户端必须首先与服务器通信以检索包含航点信息的地图对象。 这只需要一次,所有后续查询都在客户端执行。
2.7.1 通过航点导航
Waypoint API 公开了一些方法,这些方法允许路点相互连接并沿着道路构建路径供车辆导航:
- next(d) 在车道方向上创建近似距离 d 内的航路点列表。 该列表包含每个可能偏差的一个航路点。
- previous(d) 创建一个航路点列表,航路点在车道相反方向的近似距离 d 内。 该列表包含每个可能偏差的一个航路点。
- next_until_lane_end(d) 和 previous_until_lane_start(d) 返回相距 d 的航路点列表。 这些列表分别从当前航路点到其车道的终点和起点。
- get_right_lane() 和 get_left_lane() 返回相邻车道中的等效航路点(如果存在)。 可以通过在其右/左车道上找到下一个航路点并移动到该航路点来进行变道操作。
1 | # Find next waypoint 2 meters ahead. |
以下代码,生成车辆,获取航路点。关闭车辆的物理模拟,在 loop 中获取下一个航路点,将车辆 transform 设置到航路点位置,同时将 spectator 设置到当前车辆位置上方俯视观察:
1 | def set_spectator_transform(in_transform): |
有一种纯数学方法的航点导航法,似乎已经无法使用,具体原因我水平太低经过无数测试也没找出来。
2.7.2 生成地图导航
客户端需要向服务器发出请求以获取 .xodr 映射文件并将其解析为 carla.Map 对象。 这只需要执行一次。
这里补充一点OpenDRIVE的知识:
OpenDRIVE格式是以可扩展标记语言(XML)为基础,文件后缀为xodr格式的描述道路及道路网的通用标准。存储在OpenDRIVE文件中的数据描述了道路的几何形状以及沿线的特征并且定义了可以影响交通逻辑的交通标志以及道路基础设施,例如车道和信号灯。
路网是OpenDRIVE文件中描述的道路信息,其既是基于经验建造的,也可以是依据真实道路数据生成的。OpenDRIVE的主要目的是提供一种可用于仿真模拟的道路网络描述,并且可以使得这些道路以及道路网的描述可以在仿真平台或仿真软件中被自定义或改变。
OpenDRIVE根据XML的格式以节点和元素描述道路中各类信息。这样的通用格式有助于虚拟仿真测试的高度专业化,并且可以保持不同国家之间数据交换所需的相互操作性。
获取地图对象:
1 | map = world.get_map() |
地图对象包含用于创建车辆的推荐生成点。 您可以使用以下方法获取这些生成点的列表,每个生成点都包含一个 carla.Transform。 请记住,生成点可能已经被占用,导致由于碰撞而无法创建车辆。
1 | spawn_points = world.get_map().get_spawn_points() |
您可以通过获取最接近特定位置或地图 OpenDRIVE 定义中的特定 road_id、lane_id 和 s 值的航点来开始使用航点:
1 | # Nearest waypoint in the center of a Driving or Sidewalk lane. |
下面的示例显示了如何生成航点集合以可视化城市车道。 这将在地图上为每条道路和车道创建航点。 它们都将相距约 2 米:
1 | waypoint_list = map.generate_waypoints(2.0) |
要生成道路拓扑的最小图,请使用以下示例。 这将返回航点对(元组)的列表。 每对中的第一个元素与第二个元素连接,并且都定义了地图中每个车道的起点和终点。 有关此方法的更多信息,请参阅 PythonAPI。
1 | waypoint_tuple_list = map.get_topology() |
下面的示例以 carla.GeoLocation 的形式将 carla.Transform 转换为地理纬度和经度坐标:
1 | my_geolocation = map.transform_to_geolocation(vehicle.transform) |
使用以下示例将 OpenDRIVE 格式的道路信息保存到磁盘:
1 | info_map = map.to_opendrive() |
2.8 CARLA中的地图
CARLA 有8个城镇,每个城镇有2种地图,即非分层地图和分层地图(后缀_Opt)。图层包含这些分组:
- NONE 无
- Buildings 建筑
- Decals 贴花
- Foliage 植被
- Ground 地面
- ParkedVehicles 停靠的车辆
- Particles 粒子
- Props 杂物
- StreetLights 路灯
- Walls 墙体
- All 所有
2.8.1 非分层地图
非分层地图如下表所示(单击城镇名称可查看布局的俯视图)。 所有图层始终存在,并且无法在这些地图中打开或关闭。 在 CARLA 0.9.11 之前,这些是唯一可用的地图类型。
- Town01:基本城镇,T型路口
- Town02:类似Town01,更小
- Town03:复杂城镇,5车道路口,环路,坡道,隧道
- Town04:高速路和小镇的循环道路
- Town05:带有交叉路口和桥的格子小镇。每个方向有多条车道,适合验证变道
- Town06:长高速路,出入匝道
- Town07:乡村环境,道路狭窄,少信号灯
- Town10:高清城市环境
2.8.2 分层地图
分层地图的布局与非分层地图的布局相同,但可以关闭和打开地图的图层。 有一个不能关闭的最小布局,由道路、人行道、交通信号灯和交通标志组成。 分层地图可以通过后缀 _Opt 来标识,例如 Town01_Opt。 使用这些地图,可以通过 Python API 加载和卸载图层:
1 | # Load layerred map for Town01 with minimum layout plus buildings and parked vehicles |
2.8.3 自定义地图
CARLA 旨在为专业应用程序提供可扩展性和高度可定制性。 因此,除了 CARLA 中已经开箱即用的许多地图和资产外,还可以创建和导入新的地图、道路网络和资产,以在 CARLA 模拟中填充定制环境。官方文档中有详细的介绍。而大多数情况下暂时不需要用到,因此这里不多加赘述,可以到CARKA官网中查看详细教程。
2.9 传感器与数据
传感器是从周围环境中检索数据的actor,“参与者”。
carla.Sensor 类定义了一种特殊类型的actor,能够测量和流式传输数据。
- 这是什么数据? 根据传感器的类型,它变化很大。 所有类型的数据都继承自通用 carla.SensorData。
- 他们什么时候检索数据? 在每个模拟步骤或注册某个事件时。 取决于传感器的类型。
- 他们如何检索数据? 每个传感器都有一个listen() 方法来接收和管理数据。
尽管存在差异,但所有传感器都以相似的方式使用。
2.9.1 设置
与其他所有参与者一样,找到蓝图并设置特定属性。 这在处理传感器时至关重要。 它们的属性将决定获得的结果。
以下示例设置仪表板高清摄像头。
1 | # Find the blueprint of the sensor. |
2.9.2 生成
attach_to 和attachment_type 至关重要。 传感器应该连接到父参与者(通常是车辆)上,以跟随它并收集信息。 附件类型将确定其位置关于所述车辆的更新方式。
- 刚性附加。 运动对其父位置非常严格。 这是从模拟中检索数据的正确附件。
- 弹性附加。 运动很平稳,加速和减速很少。 此附件仅推荐用于录制模拟视频。 移动是平滑的,并且在更新摄像机位置时避免了“跳跃”。
1 | transform = carla.Transform(carla.Location(x=0.8, z=1.7)) |
2.9.3 监听
每个传感器都有一个listen() 方法。 每次传感器检索数据时都会调用它。
参数回调是一个 lambda 函数。 它描述了传感器在检索数据时应该做什么。 这必须将检索的数据作为参数。
1 | # do_something() will be call each time a new image is generated by the camera |
2.9.4 数据
大多数传感器数据对象都具有将信息保存到磁盘的功能。 这将允许它在其他环境中使用。
传感器类型之间的传感器数据差异很大。 但是,它们总是带有一些基本信息的标记。
传感器数据属性 | 类型 | 描述 |
---|---|---|
frame |
int | 进行测量时的帧数。 |
timestamp |
double | 自剧集开始以来以模拟秒为单位的测量时间戳。 |
transform |
carla.Transform | 测量时传感器的世界参考。 |
注意:is_listening 是传感器属性,用于启用和关闭数据侦听。sensor_tick 是蓝图属性,用于设置接收数据之间的仿真时间
将以上的代码完成一下,就实现了 设置 rgb 相机参数 -> 生成相机演员附加在车辆上 -> 设置相机回调方法,将图像保存在磁盘上 -> 调用 world.tick() 接收服务器数据:
1 | # Find the blueprint of the sensor |
2.10 传感器的类型
2.10.1 摄像头
从相机的角度拍摄世界。 对于返回 carla.Image 的相机,您可以使用帮助类 carla.ColorConverter 来修改图像类型以表示不同的信息。
- 检索每个模拟步骤的数据。
传感器 | 输出 | 概述 |
---|---|---|
Depth | carla.Image | 在灰度图中渲染视场中元素的深度。 |
RGB | carla.Image | 提供对周围环境的清晰视野。 看起来像一张普通的现场照片。 |
Optical Flow | carla.Image | 渲染来自相机的每个像素的运动。 |
Semantic segmentation | carla.Image | 根据标签以特定颜色渲染视野中的元素。 |
Instance segmentation | carla.Image | 根据标签和唯一的对象 ID 以特定颜色渲染视野中的元素。 |
DVS | carla.DVSEventArray | 作为事件流异步测量亮度强度的变化。 |
2.10.2 探测器
探测器,当传感器附加的物体触发特定事件时,检索数据:
传感器 | 输出 | 概述 |
---|---|---|
Collision | carla.CollisionEvent | 检索其父actor和其他actor之间的碰撞。 |
Lane invasion | carla.LaneInvasionEvent | 在其父项越过车道标记时注册。 |
Obstacle | carla.ObstacleDetectionEvent | 检测到其父级之前可能存在的障碍。 |
2.10.3 其它
不同的功能,例如导航、物理属性测量和场景的 2D/3D 点图。
传感器 | 输出 | 概述 |
---|---|---|
GNSS | carla.GNSSMeasurement | 检索传感器的地理位置。 |
IMU | carla.IMUMeasurement | 包括加速度计、陀螺仪和指南针。 |
LIDAR | carla.LidarMeasurement | 旋转激光雷达。 生成一个 4D 点云,每个点的坐标和强度对周围环境进行建模。 |
Radar | carla.RadarMeasurement | 2D 点图建模元素在视线内及其与传感器有关的运动。 |
RSS | carla.RssResponse | 根据安全检查修改应用于车辆的控制器。 此传感器的工作方式与其他传感器不同,并且有专门的 RSS 文档。 |
Semantic LIDAR | carla.SemanticLidarMeasurement | 旋转激光雷达。 生成具有有关实例和语义分割的额外信息的 3D 点云。 |
- Camera构建
与汽车类似,我们先创建蓝图,再定义位置,然后再选择我们想要的汽车安装上去。不过,这里的位置都是相对汽车中心点的位置(以米计量)。
1 | camera_bp = blueprint_library.find('sensor.camera.rgb') |
我们还要对相机定义它的callback function,定义每次仿真世界里传感器数据传回来后,我们要对它进行什么样的处理。在这个教程里我们只需要简单地将文件存在硬盘里。
1 | camera.listen(lambda image: image.save_to_disk(os.path.join(output_path, '%06d.png' % image.frame))) |
- Lidar构建
Lidar可以设置的参数比较多,对Lidar模型不熟也没有关系,我在后面会另开文章详细介绍激光雷达模型,现在就知道我们设置了一些常用参数就好。
1 | lidar_bp = blueprint_library.find('sensor.lidar.ray_cast') |
接着把lidar放置在奔驰上, 定义它的callback function.
1 | lidar_location = carla.Location(0, 0, 2) |
3. PythonAPI的使用
3.1 配置默认库
我们之前虽然已经make好了Carla的PythonAPI, 但是并没有将它的库安装到我们默认的python3里,如果你查看carla自带的example, 会发现都要先进行以下操作:
1 | import glob |
在我们的代码中,也要这样做。当然有一劳永逸的方法。将Carla Library安装到你的python3.7里。这是我个人研究的方法,不一定都能成功。
最新版本的CARLA模拟器,可以直接pip install carla解决。
如果安装了旧版,需要按照以下步骤进行:
使用anaconda激活car环境,cd命令进入PythonAPI/carla/dist。运行:
1 | unzip carla-0.9.13-py3.7-win-amd64.egg -d carla-0.9.13-py3.7-win-amd64 |
在同目录下建立一个setup.py,复制进入:
1 | from distutils.core import setup |
最后通过pip命令安装进去。
1 | pip3 install -e carla-0.9.13-py3.7-win-amd64 |
3.2
我原本准备详细写写他提供的pythonAPI,但是他提供的实在是非常多,后期只能用到什么查什么,写什么。这里放个官方文档地址。Python API 参考 - CARLA 模拟器 中文文档 (zlhou-carla-doc-cn.readthedocs.io)
4. 基于DQN的自动驾驶
4.1 DQN
DQN(Deep Q-Learning)可谓是深度强化学习(Deep Reinforcement Learning,DRL)的开山之作,是将深度学习与强化学习结合起来从而实现从感知(Perception)到动作( Action )的端对端(End-to-end)学习的一种全新的算法。由DeepMind在NIPS 2013上发表,后又在Nature 2015上提出改进版本。
DQN是DRL的其中一种算法,它要做的就是将卷积神经网络(CNN)和Q-Learning结合起来,CNN的输入是原始图像数据(作为状态State),输出则是每个动作Action对应的价值评估Value Function(Q值)。
创新点:
基于Q-Learning构造Loss Function(不算很新,过往使用线性和非线性函数拟合Q-Table时就是这样做)。
通过experience replay(经验池)解决相关性及非静态分布问题;
使用TargetNet解决稳定性问题。
优点:
算法通用性,可玩不同游戏;
End-to-End 训练方式;
可生产大量样本供监督学习。
缺点:
无法应用于连续动作控制;
只能处理只需短时记忆问题,无法处理需长时记忆问题(后续研究提出了使用LSTM等改进方法);
CNN不一定收敛,需精良调参。
4.2 DQN自动驾驶
这里我们用pytroch+gym实现一下来感受深度强化学习。pytorch的环境配置之前已经说过,这里不多加赘述。
4.2.1 配置环境
gym是用于开发和比较强化学习算法的工具包,在python中安装gym库和其中子场景都较为简便。
安装gym:pip install gym
安装自动驾驶模块,这里使用Edouard Leurent发布在github上的包highway-env(原链接):pip install –user git+https://github.com/eleurent/highway-env
其中包含6个场景:
高速公路——“highway-v0”
汇入——“merge-v0”
环岛——“roundabout-v0”
泊车——“parking-v0”
十字路口——“intersection-v0”
赛车道——“racetrack-v0”
4.2.2 实验环境
安装好后即可在代码中进行实验(以高速公路场景为例):
1 | import gym |
会出现这样的画面:
可以看到这是上帝视角。绿色的就是我们可以用代码操控的车。这样的游戏相对简单,数据比较好处理,大家也比较容易理解。
env类有很多参数可以配置,具体可以参考在github上的原文档。
4.2.3 数据处理
(1)state
highway-env包中没有定义传感器,车辆所有的state (observations) 都从底层代码读取,节省了许多前期的工作量。根据文档介绍,state (ovservations) 有三种输出方式:Kinematics,Grayscale Image和Occupancy grid。
Kinematics
输出V*F的矩阵,V代表需要观测的车辆数量(包括ego vehicle本身),F代表需要统计的特征数量。
例:
Vehicle x y v_x v_y
ego-vehicle 5.0 4.0 15.0 0
vehicle1 -10.0 4.0 12.0 0
vehicle2 13.0 8.0 13.5 0
数据生成时会默认归一化,取值范围:[100, 100, 20, 20],也可以设置ego vehicle以外的车辆属性是地图的绝对坐标还是对ego vehicle的相对坐标。
在定义环境时需要对特征的参数进行设定:
1 | config = \ |
Grayscale Image
生成一张W*H的灰度图像,W代表图像宽度,H代表图像高度
Occupancy grid
生成一个WHF的三维矩阵,用W*H的表格表示ego vehicle周围的车辆情况,每个格子包含F个特征。
(2) action
highway-env包中的action分为连续和离散两种。连续型action可以直接定义throttle和steering angle的值,离散型包含5个meta actions:
1 | ACTIONS_ALL = { |
(3) reward
highway-env包中除了泊车场景外都采用同一个reward function:
这个function只能在其源码中更改,在外层只能调整权重。
4.2.4 搭建模型
这里采用第一种state表示方式——Kinematics进行示范。
由于state数据量较小(5辆车*7个特征),可以不考虑使用CNN,直接把二维数据的size[5,7]转成[1,35]即可,模型的输入就是35,输出是离散action数量,共5个。
1 | import torch |
- 模型结构
这里对原作者的网络结构进行了更改。在example里设置的模型只有一个隐藏层,neuron数量和输入层相同,即模型结构为[35,35,5],把隐藏层的数量和neuron数都增大,这里设为了[35,256,256,5],模型效果有所提升。
- reward定义
highway-env环境内部对于reward的定义比较固定,不支持自由更改,如果想高度自定义reward,建议在环境外部自己写function,不要使用环境反馈的reward。
在highway-env的官方文档中又有说明,奖励必须是有界的,所以无论如何在config里设置奖励或惩罚的数值,都会被归一化,使其介于[0,1]。如果因为认为模型碰撞发生率过高,为了让它更注重避障,在config里把碰撞的惩罚设置成一个很小的负数,环境对碰撞的奖励输出还是0,但是加速奖励因为归一化的下界变化变得更接近1了,这样相当于没有减小碰撞奖励,反而增大了速度奖励,适得其反,模型变得更激进了。碰撞惩罚越大,发生碰撞的概率越高。
- 网络价值分析
假设当前DQN已经训练到最优,即对每个局面下的每个动作价值输出都是准确的,并且策略会在每个状态下做出最优选择。由于env定义的reward介于[0,1],可以使用等比数列求和公式计算出最理想情况下每个动作值的上界。
$$
Qmax = 1+γ∗1+γ∗(γ∗1)+γ∗(γ∗(γ∗1))+…=1/(1-γ)
$$
如果期望的收益是有限的,那么γ一定小于1,如果设为0.8,模型能输出的最大Q值是5,如果设为0.9,模型能输出的最大Q值是10,以此类推。如果模型输出的某个动作Q值超过了这个数,就可以认为模型没有正确学习到价值,学习过程有bug,需要检查代码。
4.2.5 开始训练
初始化环境,加入DQN的类
1 | import gym |
训练:
1 | import gym |
小车进行了规避的动作,以避免发生碰撞。
在终端运行tensorboard –logdir=./log可以查看可视化界面。
可以看出平均碰撞发生率会随训练次数增多逐渐降低,每个epoch持续的时间会逐渐延长(如果发生碰撞epoch会立刻结束)
看到原作者的效果对比,可以看出模型更快达到高reward,高time,碰撞率也下降得更快。(原作者中是每40次记录一次,相当于八百次)。
没有加上模型保存代码。这个模型也不是非常好,大家看着玩就行,调参的时间成本非常高,我没继续调下去了。
4.3 使用CARLA模拟器实现DQN自动驾驶
由于图像数据的结构复杂,数据量大,考虑到用没有超强算力的电脑运行程序的时候,为了简化模型结构,对数据进行压缩,摄像头传来的图像先设置为80*60。
为了让模型能学到正确的参数,需要对智能体的action和reward进行定义,汽车控制的主要3个参数可以量化成油门力度([0,1]),刹车力度([0,1]),方向盘角度([-1,1]),是否倒档(True/False)。但是根据一般的开车习惯,这些变量并不是相互独立的,比如油门和刹车一般不会同时踩下(除了漂移),定义DQN的输出时,为了计算对应action的Q值,先对action量化为几个类别:
1.直行加速:throttle=1, brake=0, steer=0, reverse=False
2.左转(满舵):throttle=0.5, brake=0, steer=-1, reverse=False
3.右转(满舵):throttle=0.5, brake=0, steer=1, reverse=False
4.直行减速:throttle=0, brake=0.5, steer=0, reverse=False
5.直行倒车:throttle=1, brake=0, steer=0, reverse=True
之后需要定义汽车行驶的reward,我们可以随机在地图上另选一点,将其坐标作为驾驶的终点,每一帧刷新时,如下定义reward:
1.若发生碰撞,reward=-200
2.若下一帧和当前帧相比,汽车到终点的距离更近,reward=1
3.若下一帧和当前帧相比,汽车到终点的距离更远,reward=-1
定义好之后我们需要将上述功能封装进step()函数并加入环境class,修改后环境class代码如下:
1 | import abc |
定义好环境后我们就可以开始定义DQN网络了,选择pytorch框架。在训练之前,还要开辟一个存储空间,用来保存小车每次和环境交互的数据(push_memory()函数),每次训练都从buffer中随机抽取batch_size的样本(get_sample()函数)。
1 | import torch |
之后添加主函数,模型便可以开始训练。每次和环境交互时选择action,一定概率是模型的输出结果,一定概率是随机选择,可以通过阈值设定(EPSILON)。
1 | if __name__ == '__main__': |
运行主函数后,我们就可以看到小车在道路中反复做出各种action以便探索环境。
但是现在的模型还很基础,神经网络对驾驶的控制也远没达到智能,需要经过成千上万次的训练,或者增加传感器或摄像头数据的丰富度,才有可能训练出达到驾驶要求的DQN网络。愿意做下去的同学上github找好的模型或好的paper复现。千万不要硬跑这个模型,我们的电脑跑起来是没有止境的。
那么这个小教程就到这里了。还有很多不是很完善的地方,以后我会慢慢完善或出视频。感谢看到这里,希望对大家有所帮助。
参考文献: