LGSVL Simulator python API 整理总结
引言
在之前的文章中我们基于 LGSVL simulator 和 Autoware 实现了一些自动驾驶功能。那里我们是将 LG 仿真环境中的车辆作为实车对待,接受仿真车发送的传感器数据,经过 Autoware 处理,得到路径跟踪需要的控制信息,再传回 LG 驱动仿真车自动行驶。
除了与 ROS / Autoware 互动,LG simulator 还提供了功能强大的 python 接口,让我们可以通过 python 程序与其交互。
本文将总结 python API 中常用的类和函数以及与 LG simulator 交互的基本步骤。
参考文献:
- https://www.lgsvlsimulator.com/docs/python-api/
- https://www.lgsvlsimulator.com/docs/api-quickstart-descriptions/
安装 python API
支持 python 3.5 及以上版本。在下载的 LGSVL simulator 中有 Api 文件夹,在该文件夹中,用如下命令安装
pip3 install --user -e .
基本用法和步骤
调用 python package lgsvl
与 LG simulator 交互的类和函数是由 python package lgsvl
提供的,因此在 python 程序中首先要调用 lgsvl
:
import lgsvl
与 simulator 建立连接
python 程序需要知道跟哪个 simulator 互动,这里就需要建立 python 程序和 simulator 的连接。这本质上是在 python 程序中实体化 Simulator 类的一个对象,送入两个参数:simulator 所在的 IP 地址和程序占用的端口,程序如下:
sim = lgsvl.Simulator("localhost", 8181) # 本机
或者
sim = lgsvl.Simulator("IP_ADDRESS", 8181) # 其中 IP_ADDRESS 替换为远程主机的 IP
其中 8181 是 simulator 默认使用的端口。
注意,在执行这一步之前,要确保本机或者远程的 simulator 已经启动,并且进入如下的界面:
lgsvl_main.png
simulator 只允许一个 python client 接入。这一点不如 Carla。Carla 允许多个 client 同时接入,不同的 client 可以操控不同的车在同一个仿真环境中行驶。不过这并不影响我们现阶段的研究工作。
加载仿真环境
通过 Simulator 类的 load() 函数实现,例如加载 “SanFrancisco” 环境:
sim.load("SanFrancisco")
目前可用的仿真环境如下:
- SanFrancisco
- SimpleMap
- SimpleRoom
- SimpleLoop
- Duckietown
- DuckieDowntown
在仿真运行过程中,有时并不是冷启动加载环境,而是已经在环境中,需要重置到初始状态,即只保留仿真环境,清空所有加载的车辆和行人,这时可以用 reset() 函数,速度比 load() 更快
if sim.current_scene == "SanFrancisco": # 判断是否已经在仿真环境中了
sim.reset()
else:
sim.load("SanFrancisco")
添加本车 (Ego vehicle),其他车辆 (NPC )和行人
向环境中添加本车(只能添加一辆)、其他车辆(可以多辆)、行人(可以多人)都是通过 Simulator 类的 add_agent() 函数实现,例如
a = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO)
第一个参数是添加个体的名称,第二个参数是该个体的类型。
个体类型包括:
- AgentType.EGO(本车)
- AgentType.NPC(NPC车辆,NPC=Non-Player Character)
- AgentType.PEDESTRIAN (行人)
其中 EGO 类型的车辆名称如下:
- XE_Rigged-apollo
- XE_Rigged-apollo_3_5
- XE_Rigged-autoware
- Tugbot
- duckiebot-duckietown-ros1
- duckiebot-duckietown-ros2
这里选择车辆的时候要跟仿真环境配合起来,正常大小的车辆要放在正常大小的城市道路中,模型车要放在模型城市中。如果把 XE_Rigged-apollo 添加到 DuckieDowntown 就会是下面的效果:
apollo_in_duckie.png仿真环境与车辆的对应关系如下:
- SanFrancisco,SimpleMap:正常城市街道,可用 XE_Rigged-apollo,XE_Rigged-apollo_3_5,XE_Rigged-autoware
- SimpleRoom:小房间,可用 Tugbot robot
- SimpleLoop,Duckietown,DuckieDowntown:模型车道和小镇,可用 Duckiebot robot
除了 EGO 类型的车辆,还有 AgentType.NPC 类型的车辆如下:
- Sedan
- SUV
- Jeep
- HatchBack
- SchoolBus
- DeliveryTruck
每种车都有与其名称对应的外观
另外,AgentType.PEDESTRIAN 行人如下:
- Bob
- Entrepreneur
- Howard
- Johnny
- Pamela
- Presley
- Robin
- Stephen
- Zoe
每个人的外表也有区别。
设定添加个体的位置和朝向
在前述 add_agent() 函数中,默认将添加的个体放在仿真环境的坐标原点。我们可以添加一个 AgentState 类型的参数,在其中指定添加个体的位置、角度等信息,例如:
state = lgsvl.AgentState()
state.transform.position = lgsvl.Vector(210, 10, 200) # 可以将 x,y,z 三个坐标包装成 Vector 类型数据赋值
state.transform.rotation.x = 0
state.transform.rotation.y = 270
state.transform.rotation.z = 0 # 也可以单独设置各个分量
a = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO, state)
仿真环境中 x,y,z 三个坐标构成左手坐标系,其中 y 轴始终垂直指向上方,若 x 轴指向车后方,则 z 轴指向车右侧。rotation 中三个角度对应绕 x,y,z 轴顺时针旋转的角度。
在选择车辆放置位置时可能会遇到一些问题,我们很可能事先并不知道 (x,y) 坐标在一个场景中具体处于哪个位置,因此很难设定恰当的放置位置。对于这个问题,实际上仿真环境内置了适合放置车辆的位置信息,可以通过如下命令获取:
spawns = sim.get_spawn()
例如在 SanFrancisco 仿真环境中,通过上述命令可以获得两个位置
Transform(position=Vector(210.809997558594, 10.1000003814697, 197.850006103516), rotation=Vector(0.0159243624657393, 269.949066162109, 3.56300406565424e-05))
Transform(position=Vector(214.600006103516, 10.1000003814697, 201.800003051758), rotation=Vector(0.0159243624657393, 269.949066162109, 3.56300406565424e-05))
如果希望将车放在上述第一个位置,可以将该位置赋值给 state,如下:
spawns = sim.get_spawn()
state = lgsvl.AgentState()
state.transform = spawns[0]
a = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO, state)
除了将个体放置在某个指定点处,我们还可以通过 map_point_on_lane() 函数找到离指定点最近的车道,将车放入其中,命令如下:
point = lgsvl.Vector(x, y, z)
state = lgsvl.AgentState()
state.transform = sim.map_point_on_lane(point) # 由 point 找到最近的车道中的点
sim.add_agent("Sedan", lgsvl.AgentType.NPC, state) # 对 Ego 和 NPC 都适用
AgentState() 类型的数据中不仅有 transform (即 position + rotation) 部分,还有速度 velocity 和角速度 angular_velocity 部分,完整结构如下:
{
'velocity': Vector(x, y, z),
'angular_velocity': Vector(x, y, z),
'transform': Transform(position=Vector(x, y, z), rotation=Vector(x, y, z))
}
其中位置/距离的单位是 meter,角度单位是 degree,速度单位是 meter per second
设置速度的命令如下:
state.velocity = lgsvl.Vector(10, 0, 0) # 沿 x 轴正方向以 10m/s 的速度行驶
不过这里的速度设置只是初始速度,在仿真环境中,由于摩擦力的作用,速度会逐渐减小到零。要驱动小车持续前进,还是需要通过油门、方向盘等与车辆互动。
与 Ego vehicle 的交互
可以通过 lgsvl.VehicleControl() 类设定本车的各类控制量(只适用于本车控制,其他社会车辆和行人不可用),包括
{
'turn_signal_left': None, # 后边为默认值
'headlights': None,
'windshield_wipers': None,
'throttle': 0.0, # 取值范围 (0 ... 1)
'braking': 0.0, # 取值范围 (0 ... 1)
'turn_signal_right': None,
'steering': 0.0, # 取值范围 (-1 ... 1),左转为负,右转为正
'reverse': False,
'handbrake': False
}
设定好了控制量,再通过 apply_control() 函数将控制量施加在本车上。例如,要设置油门为 30%,车轮角度向左打到底 ,可以用如下命令
c = lgsvl.VehicleControl()
c.throttle = 0.3
c.steering = -1.0
a.apply_control(c, True) # True 表示持续作用
当设置好了仿真环境之后,需要用 run() 函数启动仿真:
input("Press Enter to run") # 暂停一下,等待用户敲回车才开始仿真
sim.run() # 开始仿真
run() 可以加参数,设定仿真运行的时间,默认是无限时间。下面的命令设定仿真为 5 秒:
sim.run(time_limit = 5.0) # 或 sim.run(5.0)
所有的个体,包括本车,NPC 车辆和行人,都可以设定 callback 函数 on_collision(),如果个体发生碰撞,则调用该函数,例如:
def collision_occur(agent1, agent2, contact): # 参数是两个碰撞个体的名字以及碰撞发生的地点
name1 = "STATIC OBSTACLE" if agent1 is None else agent1.name
name2 = "STATIC OBSTACLE" if agent2 is None else agent2.name
print("{} collided with {} at {}".format(name1, name2, contact))
ego.on_collision(collision_occur) # 当 ego 与其他物体发生碰撞时,执行事先定义的 colllision_occur 函数
与 NPC 车辆的交互
对 NPC 车辆常用的设定是令其沿某条车道或者一系列给定的路径点 (waypoints) 行驶。
例如,follow_closest_lane() 命令让 NPC 沿当前车道行驶,如果当前车辆横跨在两条车道之间,则驶入最近的车道
npc.follow_closest_lane(True, 10) # 若为 False,则车辆停止。 10 为最大速度
当遇到交通路口时,车辆会随机选择直行或转向。
follow() 命令可以让 NPC 沿该定路径点行驶:
npc_x = npc_state.transform.position.x
npc_y = npc_state.transform.position.y
npc_z = npc_state.transform.position.z
delta_x = 5
delta_z = 3
waypoints = [
lgsvl.DriveWaypoint(lgsvl.Vector(npc_x - delta_x, npc_y, npc_z - delta_z), 3), # 包括路径点坐标和期望速度
lgsvl.DriveWaypoint(lgsvl.Vector(npc_x - 2*delta_x, npc_y, npc_z + delta_z), 3),
lgsvl.DriveWaypoint(lgsvl.Vector(npc_x - 3*delta_x, npc_y, npc_z - delta_z), 3),
lgsvl.DriveWaypoint(lgsvl.Vector(npc_x - 4*delta_x, npc_y, npc_z + delta_z), 3),
lgsvl.DriveWaypoint(lgsvl.Vector(npc_x - 5*delta_x, npc_y, npc_z - delta_z), 3)
]
npc.follow(waypoints, loop=True) # True 表示车辆到达最后一个 waypoint 之后,再回头从第一个 waypoint 开始循环
在车辆沿 waypoints 行驶时会忽略所有的交通规则,而且不避碰。
此处还可以设置事件触发的 callback 函数 on_waypoint_reached(),即当车辆到达一个 waypoint 就调用函数一次,函数内容可以任意设置,例如
def on_waypoint(agent, index):
print("waypoint {} reached".format(index))
npc.on_waypoint_reached(on_waypoint)
上述程序的效果是每到达一个 waypoint 就在屏幕上显示出来。
类似的 callback 函数还有 on_stop_line() 和 on_lane_change(),例如:
# This will be called when an NPC reaches a stop line
def on_stop_line(agent):
print(agent.name, "reached stop line")
# This will be called when an NPC begins to change lanes
def on_lane_change(agent):
print(agent.name, "is changing lanes")
npc.on_lane_change(on_lane_change)
npc.on_stop_line(on_stop_line)
与行人的交互
可以设置行人随机行走,也可以设置 follow waypoints。但是这种两种行为都只能在步行道上进行,即使将 waypoint 设定在车行道上,行人也走不过去。
如下程序设置行人随机行走:
ped_state = lgsvl.AgentState()
ped_state.transform = spawns[0]
ped_state.transform.position.x -= 10
ped_state.transform.position.z += 5 # 要让行人离步行道足够近,他/她才会开始随机行走。
ped = sim.add_agent("Bob", lgsvl.AgentType.PEDESTRIAN, ped_state)
ped.walk_randomly(True) # False 则停止不动
行人沿路径点行走的程序与 NPC 类似,例如:
ped = sim.add_agent("Bob", lgsvl.AgentType.PEDESTRIAN, ped_state)
px = ped_state.transform.position.x
py = ped_state.transform.position.y
pz = ped_state.transform.position.z
delta_z = 4
waypoints = [
lgsvl.WalkWaypoint(lgsvl.Vector(px,py,pz + delta_z), 2), # 与 NPC 车辆不同,这里最后的数字是行人在 waypoint 逗留的时间
lgsvl.WalkWaypoint(lgsvl.Vector(px,py,pz), 4),
lgsvl.WalkWaypoint(lgsvl.Vector(px,py,pz + delta_z), 3),
]
ped.follow(waypoints, loop=True)
行人也可以设置 callback 函数 on_waypoint_reached(),用法与 NPC 车辆完全一样。
运行过程中实时互动
除了在初始阶段设置个体位置、速度等参数,在程序运行过程中,随时都可以读取个体信息,然后进行修改,例如
s = ego.state
s.velocity.x = -50
ego.state = s
Ego vehicle 传感器数据采集
本车 (以 XE_Rigged-apollo 为例) 安装了如下传感器:
类别 | 名称 |
---|---|
LidarSensor | velodyne |
GpsSensor | GPS |
CameraSensor | Telephoto Camera,Main Camera,Segmentation Camera,Left Camera,Right Camera,Depth Camera |
ImuSensor | IMU |
RadarSensor | RADAR |
CanBusSensor | CANBUS |
上述传感器的名字可以通过如下命令获取:
ego = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO)
for sensor in ego.get_sensors():
print(sensor.name)
开启和关闭传感器
所有的传感器都可以通过 sensor.enabled 查看开启状态,也可以用 sensor.enabled=True/False 开启或关闭,命令如下:
for sensor in ego.get_sensors():
print(sensor.enabled) # 显示开启状态,默认情况下只有 CANBUS 是开启的
sensor.enabled = True # 开启每一个 sensor
传感器开启之后可以收集数据发送给 ROS,但前提是已经建立了与 ROS 的通讯,默认情况下是没有通讯的,可以通过 ego.bridge_connected 查看。要想建立通讯,可以用 ego.connect_bridge("IP_ADDRESS", PORT) 函数。
用 python API 收集数据并不会受到传感器开启或关闭状态的影响。
velodyne
Velodyne Lidar 的点云数据可以通过如下命令保存成 pcd 格式的文件:
ego = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO)
for sensor in ego.get_sensors():
if sensor.name = "velodyne":
sensor.save("lidar.pcd")
camera
如下命令可以保存 Camera 的图像数据:
ego = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO)
for sensor in ego.get_sensors():
if sensor.name = "Main Camera":
sensor.save("main-camera.png", compression=0)
# 或者
sensor.save("main-camera.jpg", quality=75)
这里 save() 函数的第一个参数是保存图片的路径,是相对于 simulator 的路径,不是相对于 python 程序的路径。如果保存成 png 格式,参数为 compression 取值 (0...9) ,如果保存成 jpg 格式,参数为 quality 取值 (0..100).
GPS
GPS 可以提供经纬度信息,也可以提供在本地图中的坐标信息。例如在 SanFrancisco 这个场景中,第一个车辆放置点 (即 sim.get_spawn() 得到的第一个位置) 的坐标如下:
spawns = sim.get_spawn()
print(spawns[0])
返回结果
Transform(position=Vector(210.809997558594, 10.1000003814697, 197.850006103516), rotation=Vector(0.0159243624657393, 269.949066162109, 3.56300406565424e-05))
通过 sim.map_to_gps() 函数可以将其转化成 GPS 经纬度信息:
gps = sim.map_to_gps(spawns[0])
print(gps)
返回结果
GpsData(latitude=37.7908081474212, longitude=-122.399389820989, northing=4182775.01028442, easting=52881.6509428024, altitude=10.1000003814697, orientation=-224.649066925049)
查找一下,这个经纬度地址确实位于 San Francisco 。
sanFrancisco.png通过 sim.map_from_gps() 可以将经纬度信息转换回坐标信息,这里如果要获得原本的 x,y,z 和 rotation 信息,则不仅需要送入经纬度信息,还需要海拔和角度信息:
coor = sim.map_from_gps(latitude = gps.latitude, longitude = gps.longitude, altitude = gps.altitude, orientation = gps.orientation)
print(coor)
返回结果
Transform(position=Vector(210.811340332031, 10.1000003814697, 197.848648071289), rotation=Vector(0, 269.949066162109, 0))
既然仿真环境 SanFrancisco 对应的经纬度就是实际中 San Francisco 城市的经纬度,那么 simpleMap 对应哪儿呢?可以用同样的方法搜索了一下,定位到了 LG Silicon Valley Lab 。。。
simpleMap.png传感器安装位置与坐标转换
每个传感器都有一个 transform 参数,表征了该传感器相对于车身的位置,例如:
a = sim.add_agent("XE_Rigged-apollo", lgsvl.AgentType.EGO, state)
sensors = a.get_sensors()
for sensor in sensors:
if sensor.name == "velodyne":
print("lidar: ", sensor.transform)
if sensor.name == "Main Camera":
print("Main Camera: ", sensor.transform)
if sensor.name == "Right Camera":
print("Right Camera: ", sensor.transform)
if sensor.name == "Left Camera":
print("Left Camera: ", sensor.transform)
if sensor.name == "RADAR":
print("RADAR: ", sensor.transform)
返回结果:
lidar: Transform(position=Vector(0, 2.31200003623962, -0.367920100688934), rotation=Vector(0, 0, 0))
Main Camera: Transform(position=Vector(0, 1.70000004768372, -0.200000002980232), rotation=Vector(0, 0, 0))
RADAR: Transform(position=Vector(0, 0.689000010490417, 2.2720000743866), rotation=Vector(0, 0, 0))
Left Camera: Transform(position=Vector(-0.699999988079071, 1.70000052452087, -0.199996501207352), rotation=Vector(0, 0, 0))
Right Camera: Transform(position=Vector(0.699999988079071, 1.70000052452087, -0.199996501207352), rotation=Vector(0, 0, 0))
从上述各传感器的相对位置大概可以推测出来车身坐标系的三个轴的方向:
- x 轴:正方向为车身右侧
- y 轴:正方向为垂直向上
- z 轴:正方向为车身前方,三个轴构成左手坐标系
- 车身坐标系的原点大约在车身底盘的几何中心位置。
设置天气与时间
天气的参数有三个,rain, fog, wetness,每个取值都在 [0,1] 之间。
设置命令如下:
sim.weather = lgsvl.WeatherState(rain=0, fog=0.5, wetness=0)
可以将仿真环境中的时间设置为 0~24 某个时刻,还可以令时间固定在某个时刻。
命令如下:
sim.time_of_day # 获取当前仿真时间
sim.set_time_of_day(10, fixed=True) # 将仿真时间固定在上午 10点