记录数据#
记录器(recorder)按计划从你的仿真中采样数据,并替你处理这些数据(写入文件或在实时图表中绘制),无需你在步进循环里穿插日志代码。只需描述要记录什么以及如何记录,然后照常步进场景。
默认情况下,记录在后台线程中运行,因此对仿真本身几乎不增加开销。
本页完整可运行的示例是 examples/sensors/imu_franka.py,它把 IMU 传感器记录到 .npz 文件并实时绘图:
scene.add_recorder(
data_func=lambda: imu.read()._asdict(),
rec_options=gs.recorders.NPZFile(filename="out/imu_data.npz"),
)
这一次调用就会在每一步捕获 IMU 读数,并在记录停止时把它们写入 out/imu_data.npz。
记录的工作方式#
每个场景都拥有一个 RecorderManager。每次调用 add_recorder 都会向该管理器注册一个记录器,把两样东西配对:
数据函数: 一个无参数的可调用对象,返回要捕获的数据(标量、数组或由它们组成的
dict)。来自
gs.recorders的记录器选项对象,即如何处理数据:文件写入器或绘图器。
此后,管理器会替你驱动记录器:
在
scene.build()时,管理器构建并启动每个已注册的记录器:打开文件,弹出绘图窗口。在每个
scene.step()之前,管理器按配置的速率调用数据函数,并把结果交给记录器。因此,一个样本就是该步开始时的状态,其中包含该步所消费的控制输入——无论这些输入是由 setter 还是由步进前回调写入的。在
scene.stop_recording()时(或场景被销毁时),管理器记录本次运行停止时的状态(无论采样率如何),然后每个记录器完成刷新并正常关闭。整个场景的重置也会以同样方式记录重置后留下的状态。
由于管理器自己读取数据函数,你永远不需要在循环中调用它。在构建之前一次性描述好要记录的内容,然后正常步进即可。
Warning
所有记录设置都必须在 scene.build() 之前完成。scene.add_recorder、scene.start_recording 和 sensor.start_recording 都会断言场景尚未构建,否则抛出异常,因为记录器要在构建期间分配文件句柄和窗口。
可视化相机有自己的视频 API camera.start_recording(),它在构建之后调用,并且完全绕过记录器管理器;参见 录制视频。scene.start_recording 记录的是场景本身——其仿真在每一步的每个数组——写入一个可用于回放的文件;参见 检查点与仿真状态。
记录传感器数据#
对于 传感器,sensor.start_recording 是最短路径:它把传感器自身的 read() 用作数据函数,因此你只需传入记录器选项。
imu = scene.add_sensor(gs.sensors.IMU(entity_idx=franka.idx))
imu.start_recording(gs.recorders.NPZFile(filename="out/imu_data.npz"))
记录任意数据#
要记录其他内容,或者要合并、预处理传感器输出,请使用 scene.add_recorder 并传入自己的数据函数。它先接收可调用对象,再接收记录器选项:
def data_func():
data = imu.read()
true_data = imu.read_ground_truth()
return {
"lin_acc": data.lin_acc, # measured, with noise
"true_lin_acc": true_data.lin_acc, # ground truth, for comparison
"ang_vel": data.ang_vel,
"true_ang_vel": true_data.ang_vel,
}
scene.add_recorder(
data_func,
gs.recorders.MPLLinePlot(
title="IMU Data",
labels={
"lin_acc": ("x", "y", "z"),
"true_lin_acc": ("x", "y", "z"),
"ang_vel": ("x", "y", "z"),
"true_ang_vel": ("x", "y", "z"),
},
),
)
返回值若是 dict,每个键会成为一个带标签的子图。结果是一张随场景步进而实时更新的图表:
可用的记录器#
把下列任意一个作为记录器选项传给 add_recorder。它们都从 gs.recorders 导出。
文件写入器把数据持久化到磁盘:
记录器 |
写入格式 |
说明 |
|---|---|---|
|
缓冲全部数据,在停止时一次性写入。支持数组和由数组组成的字典。 |
|
|
每个样本一行。传入 |
|
|
通过 PyAV 把帧直接流式写入文件。数据必须是 |
|
|
场景自身的状态,每步一帧,供 |
绘图器实时可视化数据,也可通过 save_to_filename 保存动画:
记录器 |
显示内容 |
数据形状 |
|---|---|---|
实时折线图(PyQtGraph) |
标量、元组或由它们组成的字典 |
|
实时折线图(matplotlib) |
标量、元组或由它们组成的字典 |
|
实时图像 |
|
|
投影到平面的三维矢量,按模长着色 |
在固定 |
PyQtGraph 和 matplotlib 是可选依赖。示例优先使用 PyQtLinePlot,在缺少 PyQtGraph 时回退到 MPLLinePlot,两者都未安装时跳过实时绘图;它从 genesis.recorders.plotters 读取 IS_PYQTGRAPH_AVAILABLE 和 IS_MATPLOTLIB_AVAILABLE 来决定。
更多用法:相机视频与图像记录见 examples/manipulation/grasp_env.py,关节力矩绘图见 examples/sensors/joint_torque_franka.py,触觉矢量场见 examples/sensors/tactile_franka.py。
采样率与缓冲#
每个记录器选项对象都接受几个共享设置:
hz:采样频率,单位为每秒样本数。若省略,数据函数每一步都会运行。Genesis World 会把hz对齐到时间步的最近整数倍,并在不得不调整时发出警告。buffer_size和buffer_full_wait_time:限定离开线程记录时使用的后台队列。当队列满载时间超过buffer_full_wait_time时,最旧的样本会被丢弃。
scene.add_recorder(
data_func=lambda: franka.get_qpos(),
rec_options=gs.recorders.NPZFile(filename="qpos.npz", hz=50), # 50 samples/second
)
记录器、采样或写入失败(例如磁盘写满)会在步进线程上抛出:若记录器就在该线程上运行,会立即从 scene.step() 抛出;若它在后台线程上运行,则在下一步或调用 sync() 时抛出。失败的记录器不再记录,场景重置会重启其余记录器,而 scene.stop_recording() 仍会刷新它已写入的内容。场景重置绝不会新建文件:记录会在同一个文件中继续,状态被重新初始化。
停止记录#
场景销毁时记录会自动停止,因此短脚本无需显式清理。调用 scene.stop_recording() 可提前停止并刷新所有记录器,例如在程序继续之前把文件定稿:
scene.stop_recording() # flushes files, closes plot windows