记录数据#

记录器(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 的记录器选项对象,即如何处理数据:文件写入器或绘图器。

此后,管理器会替你驱动记录器:

  1. 在 scene.build() 时,管理器构建并启动每个已注册的记录器:打开文件,弹出绘图窗口。

  2. 在每个 scene.step() 之前,管理器按配置的速率调用数据函数,并把结果交给记录器。因此,一个样本就是该步开始时的状态,其中包含该步所消费的控制输入——无论这些输入是由 setter 还是由步进前回调写入的。

  3. 在 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 导出。

文件写入器把数据持久化到磁盘:

记录器

写入格式

说明

NPZFile

.npz

缓冲全部数据,在停止时一次性写入。支持数组和由数组组成的字典。

CSVFile

.csv

每个样本一行。传入 header 命名各列;save_every_write=True 可连续刷新。

VideoFile

.mp4

通过 PyAV 把帧直接流式写入文件。数据必须是 [H, W] 或 [H, W, 3] 的 uint8 图像。

TrajectoryFile

.gstraj

场景自身的状态,每步一帧,供 gs.Scene.load_trajectory 定位和回放。因为数据源就是场景,所以通过 scene.start_recording 注册。

绘图器实时可视化数据,也可通过 save_to_filename 保存动画:

记录器

显示内容

数据形状

PyQtLinePlot

实时折线图(PyQtGraph)

标量、元组或由它们组成的字典

MPLLinePlot

实时折线图(matplotlib)

标量、元组或由它们组成的字典

MPLImagePlot

实时图像

(H, W)、(H, W, 1/3/4)

MPLVectorFieldPlot

投影到平面的三维矢量,按模长着色

在固定 positions 上的 (N, 3)

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

另见#

  • 记录 API 参考:RecorderManager、Recorder 以及所有记录器选项类。

  • 传感器:可从中记录的接触、触觉、表面距离、IMU 和温度传感器。

  • 相机传感器:RGB 帧,可与 VideoFile 和 MPLImagePlot 搭配。