LGSVL Simulator python API 整理总结

引言

之前的文章中我们基于 LGSVL simulator 和 Autoware 实现了一些自动驾驶功能。那里我们是将 LG 仿真环境中的车辆作为实车对待,接受仿真车发送的传感器数据,经过 Autoware 处理,得到路径跟踪需要的控制信息,再传回 LG 驱动仿真车自动行驶。

除了与 ROS / Autoware 互动,LG simulator 还提供了功能强大的 python 接口,让我们可以通过 python 程序与其交互。

本文将总结 python API 中常用的类和函数以及与 LG simulator 交互的基本步骤。

参考文献:

  1. https://www.lgsvlsimulator.com/docs/python-api/
  2. 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点
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容