在上一篇笔记中,我们对相机的帧率进行了探讨,并通过修改SDGPipeline中的Isaac Simulation Gate节点实现了对于不同相机的帧率控制。在本篇笔记中,我们继续探讨帧率问题,但是IMU。这也就是上篇博客开头的第二个问题:IMU能否手动设置帧率。首先给出答案:是肯定的。不仅可以实现对于IMU帧率的设置,还可以同时实现对于相机和IMU帧率的同时控制。但要完成这个目标是有“代价”的,那就是,我们没有办法再用以前熟悉的GUI方式通过构造Action Graph的方式了,而是要通过Standalone编写代码的方式实现。在一开始的笔记中我们提到,Isaac Sim中主要有三种工作流,分别是GUI、Extensions、Standalone Python。之前我们主要接触的都是GUI,但到这篇博客,或者说为了实现上面提到的目标,就不得不用Standalone方式了,感兴趣可以阅读这个和这个网页。简单来说就是GUI方式已经到了它的天花板。所以本篇博客,我们首先会对Standalone模式进行进一步介绍,然后再说明如何使用它控制IMU帧率。最后,结合我们之前这篇笔记中搭建出来的机器人小车,实现对于相机和IMU帧率的同时控制与数据采集。
1.Standalone Python工作流初识
官方文档中对于Standalone Python工作流的介绍以及与其它两种方式的对比如下。 简单来说,Standalone Python有两个主要特点,一是他可以手动精确地控制每一步渲染需要做什么(比如通过if语句更精确地控制一些逻辑),二是他可以以headless模式(没有GUI界面)运行。
1.1 Standalone Python环境介绍
当你安装好Isaac Sim以后,它就会自带一个Python环境。所有的Standalone程序就都是通过这个Python环境运行的。这个Python环境位于Omniverse安装根目录/Libs/Isaac Sim安装目录下,比如我电脑上就是/home/xuhui/Softwares/Omniverse/Libs/isaac_sim-2022.1.1/python.sh
。另外需要注意的是,在Windows下,Python环境的名字叫做python.bat
。运行这个Python环境如下。
可以看到,就是正常的Python。但不同之处在于,其内置了Isaac Sim的各种Python API接口,基于它们可以开发想要的功能。这些包和API在其它Python环境里没有。
1.2 官方示例
在Isaac Sim安装目录下打开终端,并输入如下命令,即可打开官方示例。比如我的路径是/home/xuhui/Softwares/Omniverse/Libs/isaac_sim-2022.1.1/
。
./python.sh standalone_examples/api/omni.isaac.franka/follow_target_with_rmpflow.py
这是一个机械臂跟随的例子。我们移动这个TargetCube,机械臂会自动跟随。当跟随完成后,TargetCube颜色变成绿色,否则为红色,如下。
2.Standalone Python基础功能代码
Isaac Sim官方在这个网页中给出了一些常用功能的代码片段。我参考其它资料进行了进一步整理,归纳出以下内容。
2.1 Isaac Sim的启动与关闭
在Standalone Python模式中,我们通过如下代码开启或关闭Isaac Sim。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 关闭程序
simulation_app.close()
利用Isaac Sim的Python环境运行上述代码,你应该会看到Isaac Sim被启动,然后又自动退出,如下所示。 可以看到,以输出的语句为界限,前面是启动,后面是退出。当然,其实你可以不在代码中关闭Isaac Sim,这样的效果就是,程序运行以后,Isaac Sim被启动。如果要结束仿真,那就在终端中按Ctrl+C强行结束即可(如果按一次没反应,多按几次就好了)。根据自己的需要自行判断是否需要关闭。根据目前的经验,其实大部分场景可能都不需要在代码中主动关闭Isaac Sim,把关闭程序的权力交给用户即可,除非是有特殊需求,比如渲染指定帧数或时间之后关闭等。
2.2 添加世界
上面启动Isaac Sim以后什么都没有,我们至少要添加一个World。在一个Isaac Sim仿真环境中,有且仅有一个World,代码如下。
from omni.isaac.core import World
world = World()
当然了,World()
函数里有一些参数可以设置,比如世界的单位等等。
2.3 添加地面
在Isaac Sim中添加地面的代码如下。
world.scene.add_default_ground_plane()
通过world.scene
提供的函数即可添加一个默认的地面,如下所示。
2.4 开启渲染
前面也说了,Standalone模式的一个特点就是可以手动控制渲染。截至目前,我们编写的代码如下所示。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
如果直接运行的话,会报下面的错误。
可以看到,程序启动成功,但是就退出了。这是因为没有渲染。具体而言,可以通过world.step()
实现,所以我们需要修改代码,如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 开启渲染
while True:
world.step(render=True)
这里我们写了个死循环,这样程序就会一直渲染,直到手动Ctrl+C结束。当然你也可以更精准地控制,比如渲染100帧,然后退出,代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 渲染100帧
for i in range(100):
print(i+1)
world.step(render=True)
# 关闭程序
simulation_app.close()
运行效果如下。 可以看到,程序渲染了100次以后自动退出了。这里有个稍微需要注意的地方,如果渲染了100帧以后没有关闭程序,就会报和上面类似的错误(因为没有渲染了)。所以如果要手动控制渲染,渲染循环结束就要紧跟关闭程序的命令。
2.5 添加物体
和GUI方式类似的,我们也可以通过代码向场景中添加各种物体。以立方体为例,代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 添加一个立方体
from omni.isaac.core.objects import DynamicCuboid
import numpy as np
my_cube = world.scene.add(
DynamicCuboid(
prim_path = "/World/my_cube",
name = "my_cube",
position = np.array([0,0,0.3]),
scale = np.array([5,5,5]),
color = np.array([0.7,0.5,1.0])))
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
简单解释一下代码,为了添加立方体,我们需要引入DynamicCuboid
这个包。然后通过world.scene.add()
函数添加物体,并且手动设置如prim_path
、位置、尺度、颜色等属性。启动脚本,就可以看到一个浅紫色的立方体了,效果如下。
除了立方体,还有Capsule
、Cone
、Cylinder
、Sphere
。Dynamic
开头表示物体受到物理引擎作用,而Visual
开头则表示物体仅用于外观,不受物理引擎约束。比如下面的代码,我们添加两个物体,一个受物理引擎约束,一个不受。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 添加两个立方体,一个受物理引擎约束,一个不受
from omni.isaac.core.objects import DynamicCuboid, VisualCuboid
import numpy as np
my_cube = world.scene.add(
DynamicCuboid(
prim_path = "/World/my_cube",
name = "my_cube",
position = np.array([0,-0.1,0.3]),
scale = np.array([5,5,5]),
color = np.array([0.7,0.5,1.0])))
my_cube2 = world.scene.add(
VisualCuboid(
prim_path = "/World/my_cube2",
name = "my_cube2",
position = np.array([0,0.4,0.3]),
scale = np.array([5,5,5]),
color = np.array([0.8,0.1,0.3])))
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
启动后,点击播放按钮开启仿真,可以看到,一个落到地面上,一个漂浮在空中。
2.6 获得物体的属性
在Isaac Sim中,提供了一些预置好的函数,可以方便获取物体的位姿、速度等属性,如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 添加一个立方体
from omni.isaac.core.objects import DynamicCuboid
import numpy as np
my_cube = world.scene.add(
DynamicCuboid(
prim_path = "/World/my_cube",
name = "my_cube",
position = np.array([0,0,0.3]),
scale = np.array([5,5,5]),
color = np.array([0.7,0.5,1.0])))
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
# 获取位姿、速度属性
position, oritentation = my_cube.get_world_pose()
linear_velocity = my_cube.get_linear_velocity()
angular_velocity = my_cube.get_angular_velocity()
print("Position is:", position)
print("Orientation is:", oritentation)
print("Linear velocity is:", linear_velocity)
print("Angular velocity is:", angular_velocity)
print("\n")
world.step(render=True)
运行脚本后,如下所示,就可以看到输出的相关属性了。当然,既然是Python的代码,除了终端输出,自然也可以保存到文件,这里不再赘述。
2.7 加载已有场景
前面我们介绍了如何通过代码新建一个场景并向其中插入物体。但很多时候,我们可能会有现成的场景需要加载。Isaac Sim为我们提供了相关函数,如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 加载已有场景
from omni.isaac.core.utils.stage import open_stage
file_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd"
open_stage(usd_path=file_path)
# 添加世界
from omni.isaac.core import World
world = World()
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
执行上述代码,程序就会自动加载指定的.usd
文件,如下图所示。这里的our_carV6.usd
见这个Github项目。
这里唯一需要注意的是,加载场景必须要在新建世界之前,不然就会报错。关于这一点的讨论可以参考这个网页。
2.8 查找指定Prim
在某些情况下,我们加载了一些已有的场景,然后想通过代码获取到一些已有的Prim,就需要用get_prim_at_path()
函数。比如在我们的our_carV6.usd
中,IMU的Prim Path是/our_car/camera_body/Imu_Sensor
,如下所示。
获取这个Prim的代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 加载已有场景
from omni.isaac.core.utils.stage import open_stage
file_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd"
open_stage(usd_path=file_path)
# 按路径获取指定Prim
from omni.isaac.core.utils.prims import get_prim_at_path
prim = get_prim_at_path("/our_car/camera_body/Imu_Sensor")
if not prim.IsValid():
print("Invalid prim")
else:
print("Valid prim")
# 添加世界
from omni.isaac.core import World
world = World()
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
运行效果如下。可以看到,在终端中输出了“Valid prim”,表示获取对象成功。
2.9 获取Prim的指定属性值
在查找到我们想要的Prim之后,自然想要获取到它相关属性的值。在Isaac Sim中提供了两种方式,一种是基于Prim类的成员函数GetAttribute()
,另一种是直接调用函数get_prim_property()
。以我们刚刚获取的IMU为例,尝试读取它的sensorPeriod
属性,代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 加载已有场景
from omni.isaac.core.utils.stage import open_stage
file_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd"
open_stage(usd_path=file_path)
# 按路径获取指定Prim
from omni.isaac.core.utils.prims import get_prim_at_path
prim = get_prim_at_path("/our_car/camera_body/Imu_Sensor")
if not prim.IsValid():
print("Invalid prim")
else:
print("Valid prim")
# 获取Prim的特定属性值
# 方法1: 调用GetAttribute()函数
frequency = prim.GetAttribute("sensorPeriod").Get()
print("IMU freq.(GetAttribute):", frequency)
# 方法2: 调用get_prim_property()函数,第一个参数是Prim路径,第二个参数是属性名称
from omni.isaac.core.utils.prims import get_prim_property
frequency = get_prim_property("/our_car/camera_body/Imu_Sensor", "sensorPeriod")
print("IMU freq.(get_prim_property):", frequency)
# 添加世界
from omni.isaac.core import World
world = World()
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
运行结果如下,可以看到,两种方式获得的属性值是相同的。
2.10 设置Prim的指定属性值
前面我们获取了想要查看的属性值,但比如当前的值我不满意,需要重新设置,Isaac Sim也提供了相应的函数。在官方文档中也展示了两种方式。一种是调用USD API的Usd.Attribute.Set()
函数,另一种则是通过ChangeProperty
命令实现,这两种方式效果是一样的。还是以前面的IMU对象为例,设置它的sensorPeriod
属性,代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 编写代码,做想做的事情
print("simulation started")
# 加载已有场景
from omni.isaac.core.utils.stage import open_stage
file_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd"
open_stage(usd_path=file_path)
# 按路径获取指定Prim
from omni.isaac.core.utils.prims import get_prim_at_path
prim = get_prim_at_path("/our_car/camera_body/Imu_Sensor")
if not prim.IsValid():
print("Invalid prim")
else:
print("Valid prim")
# 打印原始属性值
frequency = prim.GetAttribute("sensorPeriod").Get()
print("IMU freq. ori:", frequency)
# 设置Prim的特定属性值
# 方法1: 调用USD API Usd.Attribute.Set()
prim.GetAttribute("sensorPeriod").Set(1/400)
# 打印第一次设置后的属性值
frequency = prim.GetAttribute("sensorPeriod").Get()
print("IMU freq. new1:", frequency)
# 方法2: 调用ChangeProperty命令
import omni.kit.commands
omni.kit.commands.execute("ChangeProperty",
prop_path="/our_car/camera_body/Imu_Sensor.sensorPeriod",
value=1/1000,
prev=frequency)
# 打印第二次设置后的属性值
frequency = prim.GetAttribute("sensorPeriod").Get()
print("IMU freq. new2:", frequency)
# 添加世界
from omni.isaac.core import World
world = World()
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 开启渲染
while True:
world.step(render=True)
运行结果如下。可以看到原始数值是-1,第一次设为400Hz,对应0.025s,第二次设置为1000Hz,对应0.001s。
2.11 启用Extension
在前面的这篇笔记中我们提到,要想实现对于ROS消息的发布,需要先启动ros_bridge这个扩展,在Standalone模式中也是相同的。启用方式也很简单,如下。
# 启用ros_bridge扩展
from omni.isaac.core.utils import extensions
extensions.enable_extension("omni.isaac.ros_bridge")
我们可以在启动后的Isaac Sim中看到ros_bridge被开启了。
2.12 创建Action Graph
之前,我们都是在GUI中通过拖动的方式手动创建Action Graph,但Isaac Sim也支持代码的方式创建Action Graph。比如如下代码。如果发布的是相机渲染数据,那么需要增加"pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND
属性。比如,这里我们以左相机为例发布Topic,代码如下。
# 一些参数
prim_cam_left_path = "/our_car/body/Camera_left" # 左相机的Prim路径
topic_name_left_image = "/rgb_left" # 发布的左影像Topic名称
# ------------------------------------------------------------
# standalone模式规定代码
from omni.isaac.kit import SimulationApp
simulation_app = SimulationApp({"headless": False})
# 启用ROS Bridge扩展,否则Isaac Sim无法发布ROS Topic
from omni.isaac.core.utils import extensions
extensions.enable_extension("omni.isaac.ros_bridge")
# 加载环境与机器人(要在新建世界之前,不然会报错)
from omni.isaac.core.utils.stage import open_stage
open_stage(usd_path="/home/xuhui/Omniverse-Projects/our_carV6.usd")
# 新建世界
from omni.isaac.core import World
world = World()
# 构造Action Graph,发布相机数据
import omni.graph.core as og
keys = og.Controller.Keys
(ros_camera_graph, _, _, _) = og.Controller.edit(
{
"graph_path": "/publish_camera", # 注意Graph的名称必须以/开头
"evaluator_name": "push",
"pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND,
},
{
keys.CREATE_NODES: [
("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"),
("createViewportLeft", "omni.isaac.core_nodes.IsaacCreateViewport"),
("setActiveCameraLeft", "omni.graph.ui.SetActiveViewportCamera"),
("cameraHelperLeft", "omni.isaac.ros_bridge.ROS1CameraHelper"),
],
keys.CONNECT: [
("OnPlaybackTick.outputs:tick", "createViewportLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "setActiveCameraLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "cameraHelperLeft.inputs:viewport"),
("setActiveCameraLeft.outputs:execOut", "cameraHelperLeft.inputs:execIn"),
],
keys.SET_VALUES: [
("createViewportLeft.inputs:viewportId", 1),
("setActiveCameraLeft.inputs:primPath", prim_cam_left_path),
("cameraHelperLeft.inputs:topicName", topic_name_left_image),
("cameraHelperLeft.inputs:type", "rgb"),
],
},
)
# 运行一次构造的Graph,生成SDGPipeline
og.Controller.evaluate_sync(ros_camera_graph)
simulation_app.update()
print("start to simulate")
world.reset()
# 开始循环
while True:
world.step(render=True)
首先打开一个新的终端,启动roscore,然后运行脚本。可以看到,Isaac Sim发布了左相机的Topic。
3.对IMU帧率的修改
基于前面提到的各种基础知识,我们在这里就可以对IMU的帧率进行修改了。
3.1 手动插入IMU并对帧率进行修改
先介绍第一种相对简单的情况,就是我们在代码中手动新建并插入一个IMU传感器,在新建的时候就对帧率进行设置,代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
# 添加世界
from omni.isaac.core import World
world = World()
# 添加地面
world.scene.add_default_ground_plane()
# 添加一个立方体
from omni.isaac.core.objects import DynamicCuboid
import numpy as np
my_cube = world.scene.add(
DynamicCuboid(
prim_path = "/World/my_cube",
name = "my_cube",
position = np.array([0,0,0.3]),
scale = np.array([5,5,5]),
color = np.array([0.7,0.5,1.0])))
# 新建一个IMU传感器并绑定到cube上
import omni.kit.commands
from pxr import Gf
result, sensor = omni.kit.commands.execute(
"IsaacSensorCreateImuSensor",
path="/imu_sensor",
parent="/World/my_cube",
sensor_period=1 / 1000,
translation=Gf.Vec3d(0, 0, 0),
orientation=Gf.Quatd(1, 0, 0, 0),
visualize=True,
)
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 构造接口,获取IMU数据
from omni.isaac.isaac_sensor import _isaac_sensor
imu_data_loader = _isaac_sensor.acquire_imu_sensor_interface()
# 新建文件用于输出
fout = open("/home/xuhui/imu_data.txt", "w")
# 开始仿真
while True:
# 读取IMU传感器数据
imu_reading = imu_data_loader.get_sensor_readings("/World/my_cube/imu_sensor")
# 解析IMU传感器数据
if(len(imu_reading) != 0):
for i in range(len(imu_reading)):
tmp_imu_reading = imu_reading[i]
data_timestamp = tmp_imu_reading[0]
data_acc_x = tmp_imu_reading[1]
data_acc_y = tmp_imu_reading[2]
data_acc_z = tmp_imu_reading[3]
data_ang_x = tmp_imu_reading[4]
data_ang_y = tmp_imu_reading[5]
data_ang_z = tmp_imu_reading[6]
data_ori_x = tmp_imu_reading[7][0]
data_ori_y = tmp_imu_reading[7][1]
data_ori_z = tmp_imu_reading[7][2]
data_ori_w = tmp_imu_reading[7][3]
print(data_timestamp, data_acc_x, data_acc_y, data_acc_z, data_ang_x, data_ang_y, data_ang_z, data_ori_x, data_ori_y, data_ori_z, data_ori_w)
fout.write(str(data_timestamp) + " " + str(data_acc_x) + " " + str(data_acc_y) + " " + str(data_acc_z) + " " + str(data_ang_x) + " " + str(data_ang_y) + " " + str(data_ang_z) + " " + str(data_ori_x) + " " + str(data_ori_y) + " " + str(data_ori_z) + " " + str(data_ori_w) + "\n")
world.step(render=True)
在代码中,我们首先通过IsaacSensorCreateImuSensor
命令新建了一个IMU传感器,并且在新建的时候就设置好了它的帧率。然后通过acquire_imu_sensor_interface()
接口提供的get_sensor_readings()
函数获取IMU数据。最后,将IMU数据保存到文件同时打印到终端。运行效果如下。
可以看到,每两个IMU观测之间的时间间隔为0.001秒,也就是1ms,对应1000Hz。
3.2 加载已有IMU并对帧率进行修改
另一种情况则是,IMU传感器已经创建好了,我们只需要加载相关场景以后,读取IMU的sensor_period
属性并修改即可,这主要用到2.9和2.10部分的知识。代码如下。
# 启动Isaac Sim,对于Standalone方式而言是必须的
from omni.isaac.kit import SimulationApp
# 可以选择是否以headless模式运行
simulation_app = SimulationApp({"headless": False})
scene_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd" # 场景+小车所在的usd文件路径
imu_outpath = "/home/xuhui/omni-record/imu_data.txt" # IMU数据输出文件路径
frequency_imu = 500 # IMU的观测频率
# 加载环境与机器人(要在新建世界之前,不然会报错)
from omni.isaac.core.utils.stage import open_stage
open_stage(usd_path=scene_path)
# 添加世界
from omni.isaac.core import World
world = World()
# 根据参数设置IMU频率
from omni.isaac.core.utils.prims import get_prim_at_path
prim_imu = get_prim_at_path("/our_car/camera_body/Imu_Sensor")
prim_imu.GetAttribute("sensorPeriod").Set(1/frequency_imu)
# 根据官方文档建议,添加完物体之后,最好重置刷新一下世界
world.reset()
# 构造接口,获取IMU数据
from omni.isaac.isaac_sensor import _isaac_sensor
imu_data_loader = _isaac_sensor.acquire_imu_sensor_interface()
# 新建文件用于输出
fout = open("/home/xuhui/imu_data.txt", "w")
# 开始仿真
while True:
# 读取IMU传感器数据
imu_reading = imu_data_loader.get_sensor_readings("/our_car/camera_body/Imu_Sensor")
# 解析IMU传感器数据
if(len(imu_reading) != 0):
for i in range(len(imu_reading)):
tmp_imu_reading = imu_reading[i]
data_timestamp = tmp_imu_reading[0]
data_acc_x = tmp_imu_reading[1]
data_acc_y = tmp_imu_reading[2]
data_acc_z = tmp_imu_reading[3]
data_ang_x = tmp_imu_reading[4]
data_ang_y = tmp_imu_reading[5]
data_ang_z = tmp_imu_reading[6]
data_ori_x = tmp_imu_reading[7][0]
data_ori_y = tmp_imu_reading[7][1]
data_ori_z = tmp_imu_reading[7][2]
data_ori_w = tmp_imu_reading[7][3]
print(data_timestamp, data_acc_x, data_acc_y, data_acc_z, data_ang_x, data_ang_y, data_ang_z, data_ori_x, data_ori_y, data_ori_z, data_ori_w)
fout.write(str(data_timestamp) + " " + str(data_acc_x) + " " + str(data_acc_y) + " " + str(data_acc_z) + " " + str(data_ang_x) + " " + str(data_ang_y) + " " + str(data_ang_z) + " " + str(data_ori_x) + " " + str(data_ori_y) + " " + str(data_ori_z) + " " + str(data_ori_w) + "\n")
world.step(render=True)
运行以后,效果如下。 相邻观测的时间间隔为0.002秒,也即500Hz。
3.3 小结
至此,我们便完成了对于IMU数据的帧率设置以及对数据的保存,也就顺利回答了上篇博客的第二个问题:IMU能否设置帧率。对于IMU数据的保存,当然你可能会有疑问,为什么这里对于IMU的数据不通过ROS发布。其实我试过,但是或多或少会有问题。第一种方案是,在usd文件中写好IMU数据发布的Action Graph,然后在脚本中修改IMU帧率。这种做法经过实验,发现帧率修改没有作用。第二种方案是,在代码中构造Action Graph,然后再发布,效果和方案一一样,帧率没有改变。第三种方案是在代码中获取IMU数据以后,直接利用ROS的Python接口进行发布。这种方案理论上是可行的。Isaac Sim官方给出的VINS示例也是这样做的。但这种方案在我电脑上有个bug(可能是因为我的环境没配置好导致的),那就是运行脚本是在Isaac Sim提供的Python环境里,但这个环境里并没有ROS的Pyhton接口,比如rospy、rospkg等包,没办法直接发布ROS Topic。所以才有了第四种方案,也就是读取IMU数据以后,直接保存成文本文件,再经过后期脚本,变成ROS Bag。所以小结一下就是:对于IMU数据,不建议使用Action Graph方式发布(无论是GUI方式构建的Graph还是Standalone编写代码构建的),这种方式发布的数据最多只能达到渲染上限,而且没办法改变帧率,即使通过代码修改了IMU的sensorPeriod
属性。所以如果要保存或者发布IMU数据,要么通过ROS的Python接口,要么直接保存为文件,后期再处理。
4.相机和IMU不同帧率的设置
接着,让我们回答上篇博客的第三个问题,不同传感器如何设置不同帧率。对于多个相机设置不同帧率的问题,在上篇博客已经探讨过了。而如何同时设置相机和IMU的帧率呢?这也就是这一部分的内容。
4.1 Standalone方式设置相机帧率
尽管在上篇笔记,我们通过修改SDGPipeline的Isaac Simulation Gate的Step属性完成了对相机帧率的修改,但这是在GUI方式下完成的。正如前面说的,如果要对IMU帧率进行修改,只能通过Standalone的方式。所以这里我们首先就先要了解,如何在Standalone模式下构造Action Graph并修改相机帧率。从本质上来说,思路是相同的,只是把我们之前手动的方式用代码实现。代码如下。
# 已知bug:在启动仿真环境后,需要手动修改Viewport:
# Viewport1 -> Overall_camera
# Viewport2 -> Camera_left
# Viewport3 -> Camera_right
# 代码主要实现对于双目数据的帧率设置与发布
# 一些参数
scene_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd" # 场景+小车所在的usd文件路径
prim_cam_left_path = "/our_car/body/Camera_left" # 左相机的Prim路径
prim_cam_right_path = "/our_car/body/Camera_right" # 右相机的Prim路径
rgb_interval = 10 # 每隔N个渲染帧发布一帧
topic_name_left_image = "/rgb_left" # 发布的左影像Topic名称
topic_name_right_image = "/rgb_right" # 发布的右影像Topic名称
# ------------------------------------------------------------
# standalone模式规定代码
from omni.isaac.kit import SimulationApp
simulation_app = SimulationApp({"headless": False})
# 启用ROS Bridge扩展,否则Isaac Sim无法发布ROS Topic
from omni.isaac.core.utils import extensions
extensions.enable_extension("omni.isaac.ros_bridge")
# 加载环境与机器人(要在新建世界之前,不然会报错)
from omni.isaac.core.utils.stage import open_stage
open_stage(usd_path=scene_path)
# 新建世界
from omni.isaac.core import World
world = World()
# 构造Action Graph,发布相机数据
import omni.graph.core as og
keys = og.Controller.Keys
(ros_camera_graph, _, _, _) = og.Controller.edit(
{
"graph_path": "/publish_camera", # 注意Graph的名称必须以/开头
"evaluator_name": "push",
"pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND,
},
{
keys.CREATE_NODES: [
("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"),
("createViewportLeft", "omni.isaac.core_nodes.IsaacCreateViewport"),
("createViewportRight", "omni.isaac.core_nodes.IsaacCreateViewport"),
("setActiveCameraLeft", "omni.graph.ui.SetActiveViewportCamera"),
("setActiveCameraRight", "omni.graph.ui.SetActiveViewportCamera"),
("cameraHelperLeft", "omni.isaac.ros_bridge.ROS1CameraHelper"),
("cameraHelperRight", "omni.isaac.ros_bridge.ROS1CameraHelper"),
],
keys.CONNECT: [
("OnPlaybackTick.outputs:tick", "createViewportLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "setActiveCameraLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "cameraHelperLeft.inputs:viewport"),
("setActiveCameraLeft.outputs:execOut", "cameraHelperLeft.inputs:execIn"),
("OnPlaybackTick.outputs:tick", "createViewportRight.inputs:execIn"),
("createViewportRight.outputs:viewport", "setActiveCameraRight.inputs:viewport"),
("createViewportRight.outputs:viewport", "cameraHelperRight.inputs:viewport"),
("setActiveCameraRight.outputs:execOut", "cameraHelperRight.inputs:execIn"),
],
keys.SET_VALUES: [
("createViewportLeft.inputs:viewportId", 1),
("createViewportRight.inputs:viewportId", 2),
("setActiveCameraLeft.inputs:primPath", prim_cam_left_path),
("setActiveCameraRight.inputs:primPath", prim_cam_right_path),
("cameraHelperLeft.inputs:topicName", topic_name_left_image),
("cameraHelperLeft.inputs:type", "rgb"),
("cameraHelperRight.inputs:topicName", topic_name_right_image),
("cameraHelperRight.inputs:type", "rgb"),
],
},
)
# 运行一次构造的Graph,生成SDGPipeline
og.Controller.evaluate_sync(ros_camera_graph)
simulation_app.update()
# 修改SDGPipeline对应的节点,调整帧率
left_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_2_LdrColorSDIsaacSimulationGate"
right_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_3_LdrColorSDIsaacSimulationGate"
import omni.graph.core as og
og.Controller.attribute(left_camera_path + ".inputs:step").set(rgb_interval)
og.Controller.attribute(right_camera_path + ".inputs:step").set(rgb_interval)
print("start to simulate")
world.reset()
# 开始循环渲染
while True:
world.step(render=True)
运行上述代码,如下。 可以看到,相机帧率按照我们的设定降下来了。在我电脑上跑满渲染帧率大约在60FPS左右,每隔10帧发布一次,就是大约6FPS。当然,正如同代码一开始写的,目前还有个潜在的bug,就是运行脚本之后,Viewport的内容需要手动设置一下,不然不太对。目前不知道如何解决,欢迎大佬指正。官方也提供了相应的代码,感兴趣可以看看,思路是一样的。
4.2 相机与IMU联合
在实现了利用代码控制帧率并发布相机数据以后,接着就可以利用3.2部分的知识对IMU帧率进行控制。代码如下。
# 已知bug:在启动仿真环境后,需要手动修改Viewport:
# Viewport1 -> Overall_camera
# Viewport2 -> Camera_left
# Viewport3 -> Camera_right
# 代码主要实现对于双目数据的帧率设置与发布、IMU的帧率设置与发布
# 一些参数
scene_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd" # 场景+小车所在的usd文件路径
imu_outpath = "/home/xuhui/omni-record/imu_data.txt" # IMU数据输出文件路径
prim_imu_path = "/our_car/camera_body/Imu_Sensor" # IMU的Prim路径
prim_cam_left_path = "/our_car/body/Camera_left" # 左相机的Prim路径
prim_cam_right_path = "/our_car/body/Camera_right" # 右相机的Prim路径
frequency_imu = 500 # IMU的观测频率
rgb_interval = 10 # 每隔N个渲染帧发布一帧
topic_name_left_image = "/rgb_left" # 发布的左影像Topic名称
topic_name_right_image = "/rgb_right" # 发布的右影像Topic名称
# ------------------------------------------------------------
# standalone模式规定代码
from omni.isaac.kit import SimulationApp
simulation_app = SimulationApp({"headless": False})
# 启用ROS Bridge扩展,否则Isaac Sim无法发布ROS Topic
from omni.isaac.core.utils import extensions
extensions.enable_extension("omni.isaac.ros_bridge")
# 加载环境与机器人(要在新建世界之前,不然会报错)
from omni.isaac.core.utils.stage import open_stage
open_stage(usd_path=scene_path)
# 新建世界
from omni.isaac.core import World
world = World()
# 根据参数设置IMU频率
from omni.isaac.core.utils.prims import get_prim_at_path
prim_imu = get_prim_at_path(prim_imu_path)
prim_imu.GetAttribute("sensorPeriod").Set(1/frequency_imu)
# 构造Action Graph,发布相机数据
import omni.graph.core as og
keys = og.Controller.Keys
(ros_camera_graph, _, _, _) = og.Controller.edit(
{
"graph_path": "/publish_camera", # 注意Graph的名称必须以/开头
"evaluator_name": "push",
"pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND,
},
{
keys.CREATE_NODES: [
("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"),
("createViewportLeft", "omni.isaac.core_nodes.IsaacCreateViewport"),
("createViewportRight", "omni.isaac.core_nodes.IsaacCreateViewport"),
("setActiveCameraLeft", "omni.graph.ui.SetActiveViewportCamera"),
("setActiveCameraRight", "omni.graph.ui.SetActiveViewportCamera"),
("cameraHelperLeft", "omni.isaac.ros_bridge.ROS1CameraHelper"),
("cameraHelperRight", "omni.isaac.ros_bridge.ROS1CameraHelper"),
("computeOdometry","omni.isaac.core_nodes.IsaacComputeOdometry"),
("getTime","omni.isaac.core_nodes.IsaacReadSimulationTime"),
("publishOdometry","omni.isaac.ros_bridge.ROS1PublishOdometry"),
],
keys.CONNECT: [
("OnPlaybackTick.outputs:tick", "createViewportLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "setActiveCameraLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "cameraHelperLeft.inputs:viewport"),
("setActiveCameraLeft.outputs:execOut", "cameraHelperLeft.inputs:execIn"),
("OnPlaybackTick.outputs:tick", "createViewportRight.inputs:execIn"),
("createViewportRight.outputs:viewport", "setActiveCameraRight.inputs:viewport"),
("createViewportRight.outputs:viewport", "cameraHelperRight.inputs:viewport"),
("setActiveCameraRight.outputs:execOut", "cameraHelperRight.inputs:execIn"),
],
keys.SET_VALUES: [
("createViewportLeft.inputs:viewportId", 1),
("createViewportRight.inputs:viewportId", 2),
("setActiveCameraLeft.inputs:primPath", prim_cam_left_path),
("setActiveCameraRight.inputs:primPath", prim_cam_right_path),
("cameraHelperLeft.inputs:topicName", topic_name_left_image),
("cameraHelperLeft.inputs:type", "rgb"),
("cameraHelperRight.inputs:topicName", topic_name_right_image),
("cameraHelperRight.inputs:type", "rgb"),
],
},
)
# 运行一次构造的Graph,生成SDGPipeline
og.Controller.evaluate_sync(ros_camera_graph)
simulation_app.update()
# 修改SDGPipeline对应的节点,调整帧率
left_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_2_LdrColorSDIsaacSimulationGate"
right_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_3_LdrColorSDIsaacSimulationGate"
import omni.graph.core as og
og.Controller.attribute(left_camera_path + ".inputs:step").set(rgb_interval)
og.Controller.attribute(right_camera_path + ".inputs:step").set(rgb_interval)
# 获取IMU传感器对象
from omni.isaac.isaac_sensor import _isaac_sensor
_is = _isaac_sensor.acquire_imu_sensor_interface()
print("start to simulate")
frame_counter = 0
imu_out = open(imu_outpath,"w")
world.reset()
# 开始循环,在循环过程中采集IMU数据并输出至指定文件
while True:
# 读取IMU传感器数据
imu_reading = _is.get_sensor_readings(prim_imu_path)
# 解析IMU传感器数据
if(len(imu_reading) != 0):
for i in range(len(imu_reading)):
tmp_imu_reading = imu_reading[i]
data_timestamp = tmp_imu_reading[0]
data_acc_x = tmp_imu_reading[1]
data_acc_y = tmp_imu_reading[2]
data_acc_z = tmp_imu_reading[3]
data_ang_x = tmp_imu_reading[4]
data_ang_y = tmp_imu_reading[5]
data_ang_z = tmp_imu_reading[6]
data_ori_x = tmp_imu_reading[7][0]
data_ori_y = tmp_imu_reading[7][1]
data_ori_z = tmp_imu_reading[7][2]
data_ori_w = tmp_imu_reading[7][3]
imu_out.write(str(data_timestamp)+"\t"+
str(data_acc_x)+"\t"+str(data_acc_y)+"\t"+str(data_acc_z)+"\t"+
str(data_ang_x)+"\t"+str(data_ang_y)+"\t"+str(data_ang_z)+"\t"+
str(data_ori_x)+"\t"+str(data_ori_y)+"\t"+str(data_ori_z)+"\t"+str(data_ori_w)+"\n")
# print(data_timestamp, data_acc_x, data_acc_y, data_acc_z, data_ang_x, data_ang_y, data_ang_z, data_ori_x, data_ori_y, data_ori_z, data_ori_w)
world.step(render=True)
if frame_counter % 5000 == 0:
print("IMU Recoding in progress")
frame_counter += 1
运行上述脚本,首先可以看到,相机帧率被修改了,如下。 然后,打开保存的IMU数据,如下。 可以看到,IMU帧率也和设置的是相同的。
4.3 小结
至此,我们真正意义上回答了上篇博客的第三个问题。通过Standalone方式,同时修改了相机和IMU的帧率。
5.实际展示
最后一部分,我们展示一下利用Isaac Sim进行数据采集的全过程,包括环境和机器人的搭建、Standalone脚本编写、数据采集、数据后处理步骤。为了方便,把每一步骤的相关文件都上传到了Github,点击查看,欢迎Star或Fork。
5.1 环境和机器人的搭建
机器人和环境的搭建在之前这篇博客中就已经完成了。但我们需要对其稍作修改。
- 对于小车的控制,以GUI方式搭建Action Graph完成
- 对于小车相机数据发布和帧率修改,以Standalone方式构建Action Graph并修改SDGPipeline
- 对于小车IMU数据的发布与帧率修改,以Standalone方式修改,结果保存到文件中
- 对于小车位姿真值发布,以GUI方式搭建Action Graph完成
所以,我们需要把之前的那个很大的Aciton Graph删除一部分,主要是双目相机的流程和IMU数据发布的流程,只保留位姿真值发布流程。删除完以后如下所示。
修改后的场景文件见Github项目中的ourCarV6.usd
文件。
5.2 Standalone脚本编写
我们脚本的核心目的是构建Action Graph发布双目数据、修改SDGPipeline以改变帧率、修改IMU传感器的sensor_period
以修改IMU帧率、将IMU数据保存到文件。完整代码如下。
# 已知bug:在启动仿真环境后,需要手动修改Viewport:
# Viewport1 -> Overall_camera
# Viewport2 -> Camera_left
# Viewport3 -> Camera_right
# 小车的控制、位姿真值的发布由GUI Actin Graph完成(真值Topic名称为/odom)
# 代码主要实现对于双目数据的帧率设置与发布、IMU的帧率设置与发布
# 一些参数
scene_path = "/home/xuhui/Omniverse-Projects/our_carV6.usd" # 场景+小车所在的usd文件路径
imu_outpath = "/home/xuhui/omni-record/imu_data.txt" # IMU数据输出文件路径
prim_imu_path = "/our_car/camera_body/Imu_Sensor" # IMU的Prim路径
prim_cam_left_path = "/our_car/body/Camera_left" # 左相机的Prim路径
prim_cam_right_path = "/our_car/body/Camera_right" # 右相机的Prim路径
frequency_imu = 500 # IMU的观测频率
rgb_interval = 10 # 每隔N个渲染帧发布一帧
topic_name_left_image = "/rgb_left" # 发布的左影像Topic名称
topic_name_right_image = "/rgb_right" # 发布的右影像Topic名称
# ------------------------------------------------------------
# standalone模式规定代码
from omni.isaac.kit import SimulationApp
simulation_app = SimulationApp({"headless": False})
# 启用ROS Bridge扩展,否则Isaac Sim无法发布ROS Topic
from omni.isaac.core.utils import extensions
extensions.enable_extension("omni.isaac.ros_bridge")
# 加载环境与机器人(要在新建世界之前,不然会报错)
from omni.isaac.core.utils.stage import open_stage
open_stage(usd_path=scene_path)
# 新建世界
from omni.isaac.core import World
world = World()
# 根据参数设置IMU频率
from omni.isaac.core.utils.prims import get_prim_at_path
prim_imu = get_prim_at_path(prim_imu_path)
prim_imu.GetAttribute("sensorPeriod").Set(1/frequency_imu)
# 构造Action Graph,发布相机数据
import omni.graph.core as og
keys = og.Controller.Keys
(ros_camera_graph, _, _, _) = og.Controller.edit(
{
"graph_path": "/publish_camera", # 注意Graph的名称必须以/开头
"evaluator_name": "push",
"pipeline_stage": og.GraphPipelineStage.GRAPH_PIPELINE_STAGE_ONDEMAND,
},
{
keys.CREATE_NODES: [
("OnPlaybackTick", "omni.graph.action.OnPlaybackTick"),
("createViewportLeft", "omni.isaac.core_nodes.IsaacCreateViewport"),
("createViewportRight", "omni.isaac.core_nodes.IsaacCreateViewport"),
("setActiveCameraLeft", "omni.graph.ui.SetActiveViewportCamera"),
("setActiveCameraRight", "omni.graph.ui.SetActiveViewportCamera"),
("cameraHelperLeft", "omni.isaac.ros_bridge.ROS1CameraHelper"),
("cameraHelperRight", "omni.isaac.ros_bridge.ROS1CameraHelper"),
],
keys.CONNECT: [
("OnPlaybackTick.outputs:tick", "createViewportLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "setActiveCameraLeft.inputs:execIn"),
("createViewportLeft.outputs:viewport", "cameraHelperLeft.inputs:viewport"),
("setActiveCameraLeft.outputs:execOut", "cameraHelperLeft.inputs:execIn"),
("OnPlaybackTick.outputs:tick", "createViewportRight.inputs:execIn"),
("createViewportRight.outputs:viewport", "setActiveCameraRight.inputs:viewport"),
("createViewportRight.outputs:viewport", "cameraHelperRight.inputs:viewport"),
("setActiveCameraRight.outputs:execOut", "cameraHelperRight.inputs:execIn"),
],
keys.SET_VALUES: [
("createViewportLeft.inputs:viewportId", 1),
("createViewportRight.inputs:viewportId", 2),
("setActiveCameraLeft.inputs:primPath", prim_cam_left_path),
("setActiveCameraRight.inputs:primPath", prim_cam_right_path),
("cameraHelperLeft.inputs:topicName", topic_name_left_image),
("cameraHelperLeft.inputs:type", "rgb"),
("cameraHelperRight.inputs:topicName", topic_name_right_image),
("cameraHelperRight.inputs:type", "rgb"),
],
},
)
# 运行一次构造的Graph,生成SDGPipeline
og.Controller.evaluate_sync(ros_camera_graph)
simulation_app.update()
# 修改SDGPipeline对应的节点,调整帧率
left_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_2_LdrColorSDIsaacSimulationGate"
right_camera_path = "/Render/PostProcess/SDGPipeline/RenderProduct_Viewport_3_LdrColorSDIsaacSimulationGate"
import omni.graph.core as og
og.Controller.attribute(left_camera_path + ".inputs:step").set(rgb_interval)
og.Controller.attribute(right_camera_path + ".inputs:step").set(rgb_interval)
# 获取IMU传感器对象
from omni.isaac.isaac_sensor import _isaac_sensor
_is = _isaac_sensor.acquire_imu_sensor_interface()
print("start to simulate")
frame_counter = 0
imu_out = open(imu_outpath,"w")
world.reset()
# 开始循环,在循环过程中采集IMU数据并输出至指定文件
while True:
# 读取IMU传感器数据
imu_reading = _is.get_sensor_readings(prim_imu_path)
# 解析IMU传感器数据
if(len(imu_reading) != 0):
for i in range(len(imu_reading)):
tmp_imu_reading = imu_reading[i]
data_timestamp = tmp_imu_reading[0]
data_acc_x = tmp_imu_reading[1]
data_acc_y = tmp_imu_reading[2]
data_acc_z = tmp_imu_reading[3]
data_ang_x = tmp_imu_reading[4]
data_ang_y = tmp_imu_reading[5]
data_ang_z = tmp_imu_reading[6]
data_ori_x = tmp_imu_reading[7][0]
data_ori_y = tmp_imu_reading[7][1]
data_ori_z = tmp_imu_reading[7][2]
data_ori_w = tmp_imu_reading[7][3]
imu_out.write(str(data_timestamp)+"\t"+
str(data_acc_x)+"\t"+str(data_acc_y)+"\t"+str(data_acc_z)+"\t"+
str(data_ang_x)+"\t"+str(data_ang_y)+"\t"+str(data_ang_z)+"\t"+
str(data_ori_x)+"\t"+str(data_ori_y)+"\t"+str(data_ori_z)+"\t"+str(data_ori_w)+"\n")
# print(data_timestamp, data_acc_x, data_acc_y, data_acc_z, data_ang_x, data_ang_y, data_ang_z, data_ori_x, data_ori_y, data_ori_z, data_ori_w)
world.step(render=True)
if frame_counter % 5000 == 0:
print("IMU Recoding in progress")
frame_counter += 1
运行脚本之后,在Isaac Sim中就可以通过键盘控制小车。同时打开rqt监控工具,可以看到同时发布了相关Topic,如下。
这里需要说明的是1.Isaac Sim支持同时运行多个Action Graph,这也就是为什么可以利用键盘控制小车,并且发布数据的原因。2.对于IMU数据的录制从启动程序的那一刻就开始了,初始时间戳为0。相机和位姿真值同样是从启动就会发布Topic。但如果运行脚本以后,点击了结束按钮后,再重新启动仿真环境。相机和位姿真值的时间戳仍然会继续累加,但IMU时间戳会从零重新开始。完整的代码文件见Github项目中的runSimulation.py
文件。
5.3 数据录制
对于IMU数据,无需操心,程序自动将其记录在文本文件中,如下。 而对于位姿真值、双目相机数据,则需要通过ROS工具进行录制。打开一个新终端,输入如下内容即可开启录制。
rosbag record /odom /rgb_left /rgb_right
这里稍微需要注意的是,因为我们前面编写的代码文件有个需要手动切换Viewport内容的bug。所以这里并不建议一运行脚本就立刻开启录制。而是等手动切换完成,确保没什么问题了以后再开启录制。录制完成后的Bag信息如下。 下一步要做的就是,把文本文件形式的IMU数据和现有的ROS Bag文件整合起来。
5.4 数据后处理
将文本文件形式保存的IMU数据转换到ROS Bag文件,其实在之前的这篇笔记中已经介绍了。这里直接基于它进行简单修改即可。核心思路就是,对于已有的Bag文件,利用函数依次读取里面的数据,然后直接“转存”到新的Bag文件中即可。对于IMU数据,则按照ROS要求,构建ROS的IMU数据格式,最后将它们写入新的Bag文件。完整代码如下。
# coding=utf-8
from ctypes import c_void_p
from re import I
import rosbag
import subprocess, yaml
import sys
import os
import numpy as np
import cv2
from sensor_msgs.msg import Image, Imu
from cv_bridge import CvBridge
import rospy
from geometry_msgs.msg import Vector3
from nav_msgs.msg import Odometry
import subprocess, yaml
def loadTopicImgs(bag_path, topic_name):
imgs = []
timestamps = []
counter = 0
with rosbag.Bag(bag_path, 'r') as bag:
for topic, msg, t in bag.read_messages():
if topic == topic_name:
try:
cur_time = msg.header.stamp.to_sec()
print(cur_time)
timestamps.append(msg.header.stamp)
imgs.append(msg)
counter += 1
print(topic_name,counter)
except CvBridgeError as e:
print(e)
return imgs, timestamps
def loadOdometry(bag_path, topic_name):
poses = []
timestamps = []
counter = 0
with rosbag.Bag(bag_path, 'r') as bag:
for topic, msg, t in bag.read_messages():
if topic == topic_name:
cur_time = msg.header.stamp.to_sec()
print(cur_time)
timestamps.append(msg.header.stamp)
poses.append(msg)
counter += 1
print(topic_name,counter)
return poses, timestamps
def loadIMU(imu_path):
imu_msg = Imu()
angular_v = Vector3()
linear_a = Vector3()
imu_msgs = []
timestamps = []
counter = 0
fin = open(imu_path, 'r')
fin.readline()
line = fin.readline().strip()
while line:
parts = line.split("\t")
ts = float(parts[0])
ax = float(parts[1])
ay = float(parts[2])
az = float(parts[3])
wx = float(parts[4])
wy = float(parts[5])
wz = float(parts[6])
imu_ts_ros = rospy.rostime.Time.from_sec(ts)
timestamps.append(imu_ts_ros)
imu_msg.header.stamp = imu_ts_ros
angular_v.x = wx
angular_v.y = wy
angular_v.z = wz
linear_a.x = ax
linear_a.y = ay
linear_a.z = az
imu_msg.angular_velocity = angular_v
imu_msg.linear_acceleration = linear_a
imu_msgs.append(imu_msg)
print("imu",counter)
counter += 1
line = fin.readline().strip()
fin.close()
return imu_msgs, timestamps
def getSummaryInfo(bag_path):
info_strs = []
info_dict = yaml.load(
subprocess.Popen(['rosbag', 'info', '--yaml', bag_path], stdout=subprocess.PIPE).communicate()[0])
end_timestamp = float(info_dict['end'])
duration = float(info_dict['duration'])
start_timestamp = end_timestamp - duration
start_time_str = "Start timestamp:" + str(start_timestamp) + " s"
end_time_str = "End timestamp:" + str(end_timestamp) + " s"
duration_str = "Duration:" + str(duration) + " s"
print("-" * 100)
print("Summary Info:")
print(start_time_str)
print(end_time_str)
print(duration_str)
info_strs.append("-" * 100 + "\n")
info_strs.append("Summary Info:\n")
info_strs.append(start_time_str + "\n")
info_strs.append(end_time_str + "\n")
info_strs.append(duration_str + "\n")
return start_timestamp
if __name__ == '__main__':
imu_path = "/home/xuhui/omni-record/imu_data.txt"
bag_path = "/home/xuhui/omni-record/2023-02-19-23-16-29.bag"
out_path = "/home/xuhui/omni-record/2023-02-19-23-16-29-out.bag"
left_img_name = "/rgb_left"
right_img_name = "/rgb_right"
pose_name = "/odom"
imu_name = "/imu"
left_imgs, left_timestamps = loadTopicImgs(bag_path, left_img_name)
right_imgs, right_timestamps = loadTopicImgs(bag_path, right_img_name)
poses, pose_timestamps = loadOdometry(bag_path, pose_name)
imu_msgs, imu_timestamps = loadIMU(imu_path)
print(len(imu_msgs), len(imu_timestamps))
bag_out = rosbag.Bag(out_path,'w')
for i in range(len(imu_msgs)):
bag_out.write(imu_name,imu_msgs[i], imu_timestamps[i])
print('save imu:',i+1,'/',len(imu_msgs))
for i in range(len(left_imgs)):
bag_out.write(left_img_name, left_imgs[i], left_timestamps[i])
bag_out.write(right_img_name, right_imgs[i], right_timestamps[i])
print("save img",i+1,"/",len(left_imgs))
for i in range(len(poses)):
bag_out.write(pose_name, poses[i], pose_timestamps[i])
print("save pose gt",i+1,"/",len(poses))
bag_out.close()
运行结束以后,会生成新的ROS Bag文件,查看相关信息如下。
利用rqt_bag工具查看如下。
可以看到,已经成功将这几个需要的Topic放到了同一个ROS Bag里面。完整的代码文件见Github项目中的joinBag.py
文件。
5.5 数据可视化
如下所示,我们播放整合好的ROS Bag文件,利用RViz可视化位姿真值、影像数据,如下。 可以看到,一切正常。以上所有代码均在Isaac Sim 2022.1.1版本测试通过。至此,我们便算是真正意义上回答了上篇博客中的所有三个问题。并且把利用Isaac Sim进行数据采集的流程全部走了一遍,依靠这个流程采集的数据终于达到“真正可用”的状态,而不再只是看起来能用的数据,暂时没发现新的问题。这期间解决了很多问题,也确实遇到了太多太多的坑和挫折,不过也学到了很多。Isaac Sim的相关笔记就暂时告一段落了。之后如果遇到了新的需求、新的问题会再做记录。
6.参考资料
- [1] https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/tutorial_intro_workflows.html#isaac-sim-app-tutorial-intro-workflows
- [2] https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/tutorial_core_hello_world.html#isaac-sim-app-tutorial-core-hello-world
- [3] https://zhuanlan.zhihu.com/p/546773212
- [4] https://forums.developer.nvidia.com/t/loading-usd-scene-in-standalone-application/203052
- [5] https://docs.omniverse.nvidia.com/prod_usd/prod_usd/python-snippets/properties/get-attribute-value.html
- [6] https://docs.omniverse.nvidia.com/prod_usd/prod_usd/python-snippets/properties/set-attribute.html
- [7] https://forums.developer.nvidia.com/t/can-i-set-imu-frequency-in-isaac-sim/226055
- [8] https://forums.developer.nvidia.com/t/imu-sensor-hz-setting-problem-using-ros2-in-isaac-simulator/237944
- [9] https://forums.developer.nvidia.com/t/imu-sensor-readings-false/214533
- [10] https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/reference_python_snippets.html
- [11] https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/tutorial_ros_python.html#isaac-sim-app-tutorial-ros-python
- [12] https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/tutorial_ros_quadruped_vio.html
本文作者原创,未经许可不得转载,谢谢配合