Rerun 组件批次(Component Batches)深入解析:从数据模型、实例连接语义到存储与查询
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
导读
本文围绕 Rerun 数据模型中最基础也最容易被忽视的概念——**组件批次(Component Batches)**展开。在 Rerun 中,任何组件在任一时刻的值本质上都是一个列表(batch),理解这一点是掌握点云、检测框、骨架关键点、机器人关节等批量数据高效落盘的前提。读完本文,你将掌握批次的不可变语义、实例连接(instance joining)与 clamping 规则、与 latest-at 查询的组合行为,以及批次在 Chunk 中以 Arrow List Array 存储并暴露在 DataFrame 查询中的完整链路。
什么是组件批次:组件值永远是一个列表
在 Rerun 的数据模型中,一个实体(entity)在某时刻的某个组件(component)的值,永远是一个列表——即一个批次(batch),而不是单个标量。这一设计贯穿整个数据模型,从日志 API 到存储层再到查询层都保持一致。
考虑下面这个最简单的例子:
rr.log("/data", rr.Points3D(positions=[0.0, 0.0, 0.0]))为了方便使用,rr.Points3D原语类型(archetype)接受单个位置,但实际发生的是:对应的Position3D组件被记录为长度为 1 的批次。因此,以下两种写法完全等价:
single_point = [0.0, 0.0, 0.0] rr.log("/data", rr.Points3D(positions=single_point)) rr.log("/data", rr.Points3D(positions=[single_point]))批次并不只限于单个元素,记录更大规模的批次当然也是可能的:
rr.log("/data", rr.Points3D(positions=[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]))以批次形式记录数据的场景非常普遍:
- 点云(如上面的例子);
- 目标检测得到的包围盒(bounding boxes);
- 骨架中跟踪的关键点(keypoints);
- 机器人手臂的各个关节值(joint values)。
这也是为什么在日志 API 中,大多数原语类型都以复数形式命名,例如上面的rr.Points3D、rr.Points2D、rr.Arrows3D等——它们天然表达"一个批次"。这种命名方式背后隐含的语义是:一个批次中的每一个独立的值被称为一个实例(instance)。
相关概念梳理:在 Entities and Components 中可以看到,实体(entity)是你用
rr.log()记录的"事物",由实体路径(entity path)标识;组件是附着在实体上的数据(位置、颜色、像素等);而原语类型(archetype)则是相关组件的语义组合,例如Points3D内部就是positions、colors、radii等组件的集合。
组件批次是不可变的(immutable)
当数据以批次的形式记录到某个时间点的某个组件上时,该批次就是不可变的:
- 不能向其中追加新的实例;
- 不能修改其中已有的实例。
如果需要改变,必须整体重新记录整个批次,新的批次会替换旧的批次。
一个值得注意的细节是:当同一个组件在同一时间点被多次记录时,最后一次记录的批次会被使用,但之前记录的批次仍然保留在存储(storage)中。也就是说,"覆盖"只是查询语义层面的结果,存储层并不会物理删除旧数据。这一点与 Chunk 的存储方式直接相关——多个时间点的数据会累积存放在 Chunk 的组件列中(见下文"存储"一节),Rerun 依赖 latest-at 查询语义来决定哪个批次生效。
实例连接语义(Instance joining semantics)
组件通常是作为原语类型的一部分被记录的。原语类型是相关组件的语义组合(参见 Entities and Components)。大多数原语类型具有实例连接语义(instance joining semantics),即:一个组件的第 n 个实例,与另一个组件的第 n 个实例相对应。
以rr.Points3D为例:其colors字段的第 n 个值,作用于positions字段的第 n 个值。也就是说,颜色与位置按索引一一对应。
从源码结构看,实例连接并不是在日志时完成的,而是在查询/可视化时通过迭代器实现。在crates/store/re_query/src/bin/clamped_zip.rs中可以找到clamped_zip_*系列迭代器组合子的生成逻辑,其文档注释明确指出:clamped zip 迭代器中元素的个数,与 required(主)迭代器中元素的个数一致——这正是 clamping 语义的代码级体现。
实例 clamping(Instance clamping)
具有实例连接语义的原语类型,通常会有一个必需的组件作为主组件(primary component)。主组件决定了该原语类型代表多少个"逻辑实例"。对于rr.Points3D而言,主组件是Position3D,其批次的长度决定了 Viewer 中会显示多少个点。
对于主组件以外的其他组件,规则如下:
- 如果某个组件有更多的实例,多余的实例会被 Viewer 忽略;
- 如果某个组件有更少的实例,最后一个实例会按需重复(repeat),补齐到与主组件相同的长度。
后者被称为clamping 语义,也可以理解为"以主组件为基准的左连接(left-join)"。
clamping 让很多"自然"的日志调用成为可能,例如:
rr.log( "/data", rr.Points3D( positions=[[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [2.0, 2.0, 2.0]], radii=0.5, ), )这里记录了一个 N=3 的位置批次,以及一个 N=1 的半径批次。这个唯一的半径值会被 clamping 到三个位置上,因此在 Viewer 中,三个点共用同一个半径显示。
从实现角度看,查询端通过clamped_zip迭代器把主组件批次与其余组件批次对齐:主组件决定迭代次数,其余组件"不足补最后一个、超出被截断"。这对应于文档中所说的"clamping 语义可以看作以主组件为基准的左连接"。
实例连接与 latest-at 语义
实例连接作用于 Viewer 中正在显示的各组件的当前值。需要牢记的是,latest-at 语义依然生效——也就是说,被连接的各个组件并不需要在同一时间点被记录。
举例说明:你可以在录制的开始阶段记录一个带有位置和颜色的点云,之后只记录更新的位置。Viewer 在渲染时,会按照 latest-at 语义去查找"最近一次"记录的颜色,并将其用于显示。这正是 latest-at.md 中"partial updates(部分更新)"理念的基石:由于实例连接发生在查询时,你完全可以在不同时间点只更新某个组件,而让其他组件沿用历史值。
实例连接并非普遍适用
需要强调:实例连接语义并不是通用的。有些原语类型完全不使用它,有些则部分使用。
以rr.Mesh3D为例:
vertex_positions是其必需组件,决定了网格中顶点的数量;- 部分组件与
vertex_positions具有实例连接语义,例如vertex_colors(顶点颜色)和vertex_texcoords(顶点纹理坐标); - 但另一些组件没有实例连接语义,例如
triangle_indices(三角形索引),它包含的是指向vertex_positions批次中顶点的三元组索引,用来定义要显示的三角形,其长度与顶点数没有一一对应关系。
因此在使用具体原语类型时,应查阅其字段说明(参见 Archetypes 参考),确认哪些组件与主组件连接、哪些独立定义结构。
存储:批次如何保存在 Chunk 中
在内部,组件数据以 **Arrow List 数组(Arrow List arrays)**的形式存储在 chunk 中:
- List 数组的每一行对应一个时间点;
- 每行中的值对应那个时间点的组件批次;
- 由于 List 数组的各行可以有不同的长度,因此 Rerun 天然支持每个时间点的组件批次长度不同。
一个 Chunk 是 Arrow 编码的、面向列的表结构,包含控制列(如行 ID)、时间/索引列(如log_tick、log_time、stable_time)以及若干组件列(如Points3D:colors、Points3D:positions、Points3D:radii)。组件列中每个单元格就是一个组件批次——组件批次是 Rerun 中数据的原子单元。
两种写入路径
- 行导向的
logAPI:每次调用生成一个行 ID、为内置时间线赋值,并把传入的数据打包成组件批次,交给 micro-batcher 累积进当前 chunk,达到一定大小或周期性阈值后发送。该路径在 SDK 端以小批量构建 chunk,用少量延迟与内存换取更高的传输与摄取效率(见 chunks.md 与 micro-batching 参考)。 - 列导向的
send_columnsAPI:一次性为多个索引列与组件列发送数据。它绕过时间上下文与 micro-batcher,只包含调用中显式给出的时间线,不自动添加log_time、log_tick或任何用户时间线(详见 send_columns 指南)。组件列中的每一行同样可以是一个批次——例如一次调用记录一个点云随时间演化的全部位置批次。
在crates/store/re_chunk/src/中,chunk.rs、latest_at.rs、earliest_at.rs、merge.rs等文件共同实现了 chunk 的构建、按时间查询与合并;slice.rs中则可以看到对 chunk 进行行/列切片时对 span 做 clamping 的处理逻辑(如span.clamped_to(self.num_rows())),与批次层面的 clamping 语义相呼应。
查询视角:DataFrame 中的 ListArray 列
批次以 List 数组存储的设计,在查询 Rerun 数据时体现得最为直观:返回的 DataFrame 中,组件列的数据类型永远是ListArray——即使底层列每行只有一个值,或者所有行(批次)长度相同。
例如 Dataframe queries 中展示的查询结果,其组件列类型标注为List[nullable f64](如Scalars:scalars列),每一行的单元格是包含若干数值的列表。这意味着:下游分析代码必须意识到组件列是"列表套标量"的结构,在处理时不能假设每行只有一个标量;同时也正是这种结构,让不同时间点上长度可变的批次可以整齐地放进同一张表。
此外,DataFrame 查询中行与批次的对应关系也值得注意:行由索引(时间线)的唯一值驱动生成,组件列在某一行的值就是该时间点上该组件的批次(缺失时为 null)。静态数据(static data)虽然"对所有时间有效",但它本身不能驱动生成行,只会出现在由其他时序数据生成的行中——因此在查询大量静态批次数据时,可能需要用filter_contents()将其单独过滤出来查询,以避免同一份大静态批次在每个行里被重复产出(详见 dataframe-queries.md 的 FAQ 部分)。
实践建议与进阶阅读
- 善用 clamping 减少数据量:当多个组件共享同一属性时(如整片点云使用统一半径、统一颜色),记录 N=1 的批次即可,无需为每个实例复制值,Viewer 会自动补全。
- 记住批次的不可变性与覆盖规则:要修改一个时间点上的批次,必须整体重记;同一时间点多次记录时以最后一次为准,但旧批次仍留在存储中。
- 结合 latest-at 做部分更新:实例连接发生在查询时,因此可以在不同时间分别更新不同组件(如先记颜色、后只更新位置),Viewer 会组合出当前状态。更多内容见 Query semantics & partial updates。
- 批量高效写入用
send_columns:当数据本身是列式结构(如已经按时间组织的数组)时,用send_columns一次性发送多个批次,比逐行log更高效;注意它绕过时间上下文与 micro-batcher,时间线需显式给出。 - 继续深入 Chunk 模型:批次是 Chunk 组件列中的单元格,理解 chunks.md 有助于把握数据如何被记录、注入、存储与查询的完整生命周期;源码层面可阅读
crates/store/re_chunk/src/chunk.rs与crates/store/re_query/src/bin/clamped_zip.rs中 clamping 迭代器的生成逻辑。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考