Rerun 数据记录与摄入指南:用 DNA 算盘示例打通 Log and Ingest 全流程
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
导读
本文基于 Rerun 官方入门教程《Log and Ingest》(data-in.md),以「DNA 算盘」(DNA abacus)这一经典示例为主线,从初始化 SDK、记录首批点云,到引入时间轴实现动画、叠加层级空间变换,逐步带你掌握 Rerun 的核心数据模型(Archetypes、Components、Entity Paths、Component Batches)与「Latest-at」查询语义。读完本文,你将能够在 Python、Rust、C++ 三种语言中完成从"白纸一张"到"动态交互式 3D 场景"的完整数据记录流程,并掌握通过网络(gRPC)、RRD 文件等途径投喂数据的多种方式。
前置准备:安装 SDK 与建立项目
在开始记录数据之前,需要完成两件事:
- 安装 SDK:按官方指引安装对应语言的 Rerun SDK(Python 使用
pip install rerun-sdk,Rust/C++ 分别通过 Cargo 与 CMake 接入),详见 install-rerun.md。 - 建立项目:为你的语言创建项目骨架,详见 project-setup.md。
本教程的完整代码位于仓库中对应语言的示例目录,可直接对照运行:
- Python:examples/python/dna/dna.py
- Rust:examples/rust/dna/src/main.rs
- C++:examples/cpp/dna/main.cpp
此外,教程中逐步讲解用到的分片代码也以可复用片段的形式存放在 docs/snippets/all/tutorials/dna.py(以及同目录的dna.rs、dna.cpp)中,便于单独抽取验证。
初始化 SDK:命名记录并唤起 Viewer
创建一个新文件(或项目),导入 SDK 相关工具,并初始化一条 recording。初始化会为这条记录命名一个稳定的ApplicationId,然后拉起一个 Rerun Viewer 并把记录连接到它:
Python
import rerun as rr rr.init("rerun_example_dna_abacus", spawn=True)Rust
let rec = rerun::RecordingStreamBuilder::new("rerun_example_dna_abacus").spawn()?;C++
const auto rec = rerun::RecordingStream("rerun_example_dna_abacus"); rec.spawn().exit_on_failure();稳定的ApplicationId会让 Viewer 在多次运行间为该数据集保留 UI 状态(例如蓝图布局),在迭代调试时非常省事。关于 Application ID 的更多规则(非空 ASCII 字符串、最长 180 字符、仅允许字母数字与_、-、.、空格、括号、冒号等字符,非法字符会被归一化为连字符),可参考 recordings.md。
默认情况下,spawn会在另一个进程中启动 Viewer,并把数据自动通过管道送进去。作为对比,还有多种把数据送往 Viewer 的方式(本教程末尾会讲到),但实验阶段spawn默认行为已经足够。
说明:
spawn每次会随机生成一个新的 recording ID;如果需要把多个进程/多台机器产生的数据合并成同一条逻辑记录(分布式记录),可以在初始化时覆盖 recording ID,详见 recordings.md。
记录第一批点云:两个螺旋
DNA 形状的核心结构,可以很方便地用两团螺旋状点云来描述:
Python
import numpy as np from math import tau from rerun.utilities import bounce_lerp, build_color_spiral NUM_POINTS = 100 points1, colors1 = build_color_spiral(NUM_POINTS) points2, colors2 = build_color_spiral(NUM_POINTS, angular_offset=tau * 0.5) rr.log("dna/structure/left", rr.Points3D(points1, colors=colors1, radii=0.08)) rr.log("dna/structure/right", rr.Points3D(points2, colors=colors2, radii=0.08))Rust
const NUM_POINTS: usize = 100; let (points1, colors1) = color_spiral(NUM_POINTS, 2.0, 0.02, 0.0, 0.1); let (points2, colors2) = color_spiral(NUM_POINTS, 2.0, 0.02, TAU * 0.5, 0.1); rec.log_static( "dna/structure/left", &rerun::Points3D::new(points1.iter().copied()) .with_colors(colors1) .with_radii([0.08]), )?; rec.log_static( "dna/structure/right", &rerun::Points3D::new(points2.iter().copied()) .with_colors(colors2) .with_radii([0.08]), )?;C++
std::vector<rerun::Position3D> points1, points2; std::vector<rerun::Color> colors1, colors2; color_spiral(NUM_POINTS, 2.0f, 0.02f, 0.0f, 0.1f, points1, colors1); color_spiral(NUM_POINTS, 2.0f, 0.02f, TAU * 0.5f, 0.1f, points2, colors2); rec.log_static( "dna/structure/left", rerun::Points3D(points1).with_colors(colors1).with_radii({0.08f}) ); rec.log_static( "dna/structure/right", rerun::Points3D(points2).with_colors(colors2).with_radii({0.08f}) );运行程序后,Viewer 中即可看到这两个螺旋。注意:如果 Viewer 已经在运行,Rerun 会直接连接到现有会话,并把数据替换成这条新的recording(逻辑记录按 recording ID 与 application ID 归并,多个物理来源可合并为同一条逻辑记录)。
在继续之前,建议先在 Viewer 中动手交互一下场景、熟悉各个菜单,可参考 navigating-the-viewer.md 与 viewer 参考 的完整讲解。
幕后原理:这几行代码到底做了什么?
这两行简单的调用背后蕴含了大量 Rerun 的核心概念。逐一拆解如下。
Archetypes(原型)
记录几何图元最省事的方式,就是用 SDK 的log方法配合内置的原型类(这里就是Points3D)。Archetype 负责把一组组件(components)打包成能被 Viewer 正确识别与渲染的批次。Rerun 中大多数原型类都以复数形式命名(如rr.Points3D、rr.LineStrips3D),原因正是它们天然面向组件批次,参见 batches.md。
Components(组件)
在底层,Rerun SDK 记录的其实是独立的组件,比如位置(Position3D)、颜色(Color)、半径(Radius)等。Archetype 只是构建这类组件集合的高层便捷方式。例如rr.log("my_point", rr.Points2D([32.7, 45.9], colors=[255, 0, 0]))内部会构建Points2D:positions(类型Position2D)与Points2D:colors(类型Color)两个组件并与"my_point"实体关联,Viewer 再通过组件元数据反查对应原型并决定如何渲染,详见 entity-component.md。
对于高级用法,你既可以往 Archetype 上附加自定义组件(Python 中可用rr.AnyValues辅助对象),也可以完全绕过 Archetype,直接记录自定义的组件集合,参见 custom-data.md。
Entities & hierarchies(实体与层级)
注意我们传入的两个字符串:"dna/structure/left"与"dna/structure/right"。这些是entity paths(实体路径),唯一标识场景中的每个实体。每个实体由一条路径加一个或多个组件构成。实体路径天然形成层级结构——例如本例中dna/structure之下挂着left、right、scaffolding、scaffolding/beads——这种层级在数据如何被可视化与如何施加变换(transform)上起着关键作用,后续的旋转动画正是依赖这一点。
Component batches(组件批次)
最后注意:我们是一下子把一整批点与一整批颜色一次性记录进去的。组件批次在 Rerun 中是一等公民,带有大量性能收益与专属特性。你现在看到的正是其中一个特性:我们只为所有点记录了一个半径radii=0.08,它却同时作用于全部 100 个点——这被称为clamping(夹取)。
其底层规则是:Points3D以Position3D为主组件,主组件的批次长度决定了场景中可见的实例数;对于其他组件,实例数多了会被 Viewer 忽略,少了则最后一个实例会被重复补齐。同一时间点对同一组件重复记录时,最后一次记录的批次生效(此前批次仍留在存储中),详见 batches.md。
小结:这两次函数调用包含的信息量远不止表面——消化完 Archetype、Component、Entity Path、Component Batch 这四个概念后,记录其他任何实体都只是"依葫芦画瓢"。下面我们就把场景里的其余元素全部补上。
补全场景:脚手架与珠子
用 3D 线段条表示脚手架
DNA 的骨架可以用一批 3D 线带(line strips)来表示——每条线带连接左右螺旋的对应点:
Python
rr.log( "dna/structure/scaffolding", rr.LineStrips3D( np.stack((points1, points2), axis=1), colors=[128, 128, 128] ), )Rust
let lines: Vec<[glam::Vec3; 2]> = std::iter::zip(&points1, &points2) .map(|(&p1, &p2)| (p1, p2).into()) .collect_vec(); rec.log_static( "dna/structure/scaffolding", &rerun::LineStrips3D::new(lines.iter().cloned()) .with_colors([rerun::Color::from_rgb(128, 128, 128)]), )?;C++
std::vector<rerun::LineStrip3D> lines; for (size_t i = 0; i < points1.size(); ++i) { lines.emplace_back(rerun::LineStrip3D({points1[i].xyz, points2[i].xyz})); } rec.log_static( "dna/structure/scaffolding", rerun::LineStrips3D(lines).with_colors(rerun::Color(128, 128, 128)) );记录珠子(第一版,静态位置)
剩下的就是珠子。先把珠子放在两条螺旋之间的某处(基于每个点对应的随机偏移量做一次插值),颜色随偏移量从暗到亮渐变:
Python
offsets = np.random.rand(NUM_POINTS) beads = [ bounce_lerp(points1[n], points2[n], offsets[n]) for n in range(NUM_POINTS) ] colors = [ [int(bounce_lerp(80, 230, offsets[n] * 2))] for n in range(NUM_POINTS) ] rr.log( "dna/structure/scaffolding/beads", rr.Points3D(beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1)), )这里虽然数组操作变复杂了,但本质没有新东西:仍然是往 Archetype 里填数据、喂给 Rerun API。bounce_lerp是一个在两点间往返插值的辅助函数(仓库中 Python 侧位于rerun.utilities,Rust 侧位于rerun::demo_util,见 dna.py 与 main.rs)。
让珠子动起来:引入时间轴
时间与时间线(Timelines)
到目前为止,我们完全搁置了 Rerun 的核心概念之一:Time and Timelines。即便如此,打开 Viewer 底部的时间面板(Timeline View,参考 timeline.md),你会发现 Rerun 已经默默替我们按每次 log 调用的发生时刻记录了时间——这条自动时间线名为log_time(另一个自动时间线log_tick是序号时间线,默认关闭,可通过RERUN_LOG_TICK环境变量或set_log_tick_enabled开启;log_time则可用RERUN_LOG_TIME或set_log_time_enabled关闭)。
但记录时间在这里帮不上忙:我们不能让珠子按"记录时刻"运动,否则珠子移动速度会受记录进程性能影响而忽快忽慢!为此需要引入一条自定义时间线,使用一个由我们自己掌控的确定性时钟。
Rerun 对时间的支持很丰富:并发或互不重叠的多条时间线、乱序插入、甚至完全"活在时间线之外"的数据(静态数据)都行。具体做法是:把记录珠子的部分替换成一个在不同时间戳下循环记录珠子的循环:
Python
time_offsets = np.random.rand(NUM_POINTS) for i in range(400): time = i * 0.01 rr.set_time("stable_time", duration=time) times = np.repeat(time, NUM_POINTS) + time_offsets beads = [ bounce_lerp(points1[n], points2[n], times[n]) for n in range(NUM_POINTS) ] colors = [ [int(bounce_lerp(80, 230, times[n] * 2))] for n in range(NUM_POINTS) ] rr.log( "dna/structure/scaffolding/beads", rr.Points3D(beads, radii=0.06, colors=np.repeat(colors, 3, axis=-1)), )Rust
let mut rng = rand::rng(); let offsets = (0..NUM_POINTS).map(|_| rng.random::<f32>()).collect_vec(); for i in 0..400 { let time = i as f32 * 0.01; rec.set_duration_secs("stable_time", time as f64); let times = offsets.iter().map(|offset| time + offset).collect_vec(); let beads = std::iter::zip(&lines, ×) .map(|(&[p1, p2], &time)| bounce_lerp(p1, p2, time)) .collect_vec(); let colors = times .iter() .map(|time| bounce_lerp(80.0, 230.0, time * 2.0) as u8) .map(|c| rerun::Color::from_rgb(c, c, c)) .collect_vec(); rec.log( "dna/structure/scaffolding/beads", &rerun::Points3D::new(beads) .with_colors(colors) .with_radii([0.06]), )?; }C++
std::default_random_engine gen; std::uniform_real_distribution<float> dist(0.0f, 1.0f); std::vector<float> offsets(NUM_POINTS); std::generate(offsets.begin(), offsets.end(), [&] { return dist(gen); }); std::vector<rerun::Position3D> beads_positions(lines.size()); std::vector<rerun::Color> beads_colors(lines.size()); for (int t = 0; t < 400; t++) { auto time = std::chrono::duration<float>(t) * 0.01f; rec.set_time_duration("stable_time", time); for (size_t i = 0; i < lines.size(); ++i) { float time_offset = time.count() + offsets[i]; auto c = static_cast<uint8_t>(bounce_lerp(80.0f, 230.0f, time_offset * 2.0f)); beads_positions[i] = rerun::Position3D( bounce_lerp(lines[i].points[0].x(), lines[i].points[1].x(), time_offset), bounce_lerp(lines[i].points[0].y(), lines[i].points[1].y(), time_offset), bounce_lerp(lines[i].points[0].z(), lines[i].points[1].z(), time_offset) ); beads_colors[i] = rerun::Color(c, c, c); } rec.log( "dna/structure/scaffolding/beads", rerun::Points3D(beads_positions).with_colors(beads_colors).with_radii({0.06f}) ); }调用set_time(Python)/set_duration_secs(Rust)/set_time_duration(C++)会创建一条名为stable_time的新时间线,并确保其后所有 log 调用都被打上该时间戳。记录数据时你可以随意添加任意多条时间线与时间戳。
Rerun 支持三种时间索引类型(都以i64编码):序号(sequential)、时间戳(自 Unix 纪元起的纳秒)、时长(纳秒)。Python 端对应的set_time_sequence/set_time_nanos/set_time_secs可查阅 timelines.md;若你手里已有一段随时间变化的传感器数据,更高效的做法是用send_columns一次性批量发送,参见 send-columns.md。
⚠️ 警告:直接运行上面的代码,结果会很"意外"——珠子确实在动画了,但此前记录的所有东西都消失了!
原因在于:Viewer 默认切换到了显示你的自定义时间线stable_time,而原始数据(螺旋、脚手架)此前只记录在默认时间线log_time上。
理解 "Latest-at" 语义
修复:把结构数据记录在时间零点
解决办法是在记录原始结构之前,先把自定义时间线设为时间零点:
Python
rr.set_time("stable_time", duration=0) # ...然后记录 left / right / scaffoldingRust
rec.set_duration_secs("stable_time", 0f64); // ...然后记录 left / right / scaffolding(示例中用 log_static 记录)C++
rec.set_time_duration("stable_time", 0s); // ...然后记录 left / right / scaffolding(示例中用 log_static 记录)这一修复背后的原理
这个修复实际上引出了 Rerun 中另一个非常重要的概念:"latest-at"(取最新)语义。注意"dna/structure/left"与"dna/structure/right"只在时间零点被记录过一次,但当我们在远超零点的时刻查询场景时,它们依然可见——因为:
Rerun 总是以"最新"数据的口径来推理:对于给定实体,在给定时刻,它会取该实体在该时刻的所有最新组件。
换句话说,查询某时刻某实体的状态,得到的是"该时刻或之前最后一次记录的组件值",数据会一直延续到被更新的记录覆盖为止。这也解释了为什么把静态结构放在stable_time=0就能与动态珠子共存。顺便一提,如果某些数据希望"属于所有时间线(包括尚未创建的)",可以直接用static=True(Rust/C++ 中为log_static)记录为静态数据——本示例最终版本即采用这种方式,例如 dna.py 中的rr.log("helix/structure/left", ..., static=True)。静态数据会遮蔽同一实体上同类型的时序数据,参见 timelines.md。
变换空间:让算盘转起来
现在只剩最后一步:原场景中算盘会沿其主轴旋转。与时间一样,Rerun 把(层级)空间变换也作为一等公民对待——transforms.md 中有完整讲解。现在我们只需要把两者结合起来:在每个时间戳为脚手架记录一个旋转变换即可。
既可以扩充上面的循环来顺带记录变换,也可以像下面这样单独加一个循环:
Python
for i in range(400): time = i * 0.01 rr.set_time("stable_time", duration=time) rr.log( "dna/structure", rr.Transform3D( rotation=rr.RotationAxisAngle( axis=[0, 0, 1], radians=time / 4.0 * tau ) ), )Rust
for i in 0..400 { let time = i as f32 * 0.01; rec.set_duration_secs("stable_time", time as f64); rec.log( "dna/structure", &rerun::archetypes::Transform3D::from_rotation(rerun::RotationAxisAngle::new( glam::Vec3::Z, rerun::Angle::from_radians(time / 4.0 * TAU), )), )?; }C++
for (int t = 0; t < 400; t++) { auto time = std::chrono::duration<float>(t) * 0.01f; rec.set_time_duration("stable_time", time); rec.log( "dna/structure", rerun::archetypes::Transform3D(rerun::RotationAxisAngle( {0.0f, 0.0f, 1.0f}, rerun::Angle::radians(time.count() / 4.0f * TAU) )) ); }注意这里把Transform3D记录在父实体"dna/structure"上——因为实体路径的层级结构,作用于父实体的变换会自动级联应用到其所有子实体(left、right、scaffolding乃至scaffolding/beads)。至此,一个完整动画的 DNA 算盘就诞生了。
记录与可视化数据的其他方式
spawn非常适合单机实验,但如果记录过程发生在一台无头(headless)计算机上呢?Rerun 为此提供了多种方案。
通过网络记录数据(gRPC)
任何时候你都可以直接运行rerun命令启动一个 Viewer——这个 Viewer 本身就是一个服务器,随时准备通过 gRPC 接收数据(默认监听0.0.0.0:9876)。
在记录端,把前面的spawn调用替换成connect_grpc调用,即可把数据发送到任意 gRPC 地址:
Python
import rerun as rr rr.init("rerun_example_dna_abacus") rr.connect_grpc() # 连接到默认 URL 上正在运行的 viewer # …其余数据记录逻辑与 spawn 版一致…对应完整片段见 docs/snippets/all/tutorials/dna_connect_grpc.py(同目录还有dna_connect_grpc.rs与dna_connect_grpc.cpp)。运行rerun --help可查看更多启动选项。
保存 / 加载 RRD 文件
有时走网络不可行——比如想分享数据、把它附在 bug 报告里。Rerun 对此也有内置支持:每个 SDK 都提供save方法(Python 的rr.save、Rust 与 C++ 的RecordingStream::save),把所有已记录数据流式写入磁盘;随后用rerun path/to/recording.rrd即可查看生成的文件。此外,你也可以在可视化过程中直接从 Viewer 里保存当前记录(或其中一部分)。
⚠️ 关于 RRD 向后兼容:Rerun 0.23 及之后保存的 RRD 文件可以用更新的 Rerun 版本打开,但目前只保证相邻次版本之间的兼容(例如 Rerun 0.24 可以打开 0.23 的 RRD)。更详细内容参见发布说明文档 CHANGELOG.md。
Rust 独有:进程内嵌 Viewer
Rust SDK 可以通过rerun::native_viewer::show把 Viewer 直接宿主在你的应用进程内——它期望的是内存中的一条完整 recording,而不是实时数据流。使用它需要在Cargo.toml中启用native_viewerfeature;该 Viewer 会阻塞主线程直到被关闭,具体见 Rust API 文档。
直接打开各类文件
Viewer 本身也支持直接打开已有文件(RRD、MCAP、图片、视频、点云等),共有三种加载方式:CLI 参数(如rerun myfile.jpeg)、拖拽、以及 Viewer 内的打开对话框,详见 open-any-file.md。内置支持的文件类型包括原生rrd、3D 模型(gltf/glb/obj/stl)、常见图片格式(png/jpeg/gif/tiff/webp等)、点云ply、文本md/txt,以及 LeRobot 数据集目录。若要在 SDK 内把文件内容作为数据记录进去,可调用log_file_from_path/log_file_from_contents方法——注意此时文件由运行 SDK 的进程加载,而非 Viewer 进程。
总结与下一步
至此,我们已经完成了一次 Rerun 记录(Log and Ingest)的全流程速览:从初始化 SDK、记录点云与线带,到引入自定义时间线实现动画、借助层级实体路径施加空间变换,再到掌握 Latest-at 语义以及 gRPC / RRD / 内嵌 Viewer 等多种数据通路。这还只是冰山一角,但它应该给了你充足的出发点去动手实验。
接下来可以:
- 浏览 reference/types.md 了解更多编码(encodings)的简单用法;
- 深入学习如何查询已记录的数据;
- 或者在仓库中示例,例如 examples/python、examples/rust、examples/cpp 下的其他真实用例。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考