Rerun 数据清除(Clear/Tombstone)完全指南:清除语义、递归清除与最佳实践
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
导读:Rerun Viewer 在回放流式数据时,会在当前时间点上为每个可见实体展示“最新值”,但有些数据在录制全程中并非始终有效——目标丢失、检测失效、临时状态过期等场景都需要主动告知 Rerun“某个实体不再显示”。本篇指南以 docs/content/howto/logging-and-ingestion/clears.md 为核心,结合仓库源码深入讲解 Rerun 的
Clear归档类型(tombstone 机制)的语义、recursive参数、底层查询行为,以及三种典型的清除与数据组织方案,帮助你在目标跟踪、多频率数据对齐、短生命周期数据等场景中正确使用清除能力。
为什么需要“清除数据”?
Rerun 的数据模型是流式的:开发者不断向实体路径(Entity Path)写入数据,而 Viewer 默认采用latest-at(最新时刻)查询语义——即在当前时间点上,为每个可见实体展示其最近一次写入的组件值。这套机制天然适合构建连贯的时序视图:只要某个实体在更早的时间点写入过数据,它在之后的时间点上就会一直可见,直到有更新的数据覆盖它。
问题随之而来:某些数据只在一段时间内有效,之后既没有新值写入,也不应该继续显示。例如:
- 目标跟踪中,某个目标被判定为丢失,旧的包围框不该继续显示;
- 每 10 帧才运行一次的检测结果,在中间 9 帧里显示的仍是旧检测框;
- 某个临时传感器读数只在一秒内有效,之后应消失。
Rerun 为此提供了 tombstone(墓碑)机制:通过向实体路径写入一个特殊的Clear归档类型,明确标记“该实体在此时间点之后不再可见”。这也正是本指南标题中“Clears”的含义——它记录的是“清除动作发生的时间点”,而不是“删除过去的数据”。
方法一:显式记录实体已被清除(rr.Clear)
最直接的方式是向任意实体路径显式记录一个Clear。其语义是:Clear被记录的时间点,就是该实体在此之后不再在视图中显示的时间点。
… for frame in sensors.read(): # Associate the following logs with `frame == frame.id` rr.set_time("frame", sequence=frame.id) # Do the actual tracking update tracker.update(frame) if tracker.is_lost: # Clear everything on or below `tracked/{tracker.id}` # and that happened on or before `frame == frame.id` rr.log(f"tracked/{tracker.id}", rr.Clear(recursive=True)) else: # Log data to the main entity and a child entity rr.log(f"tracked/{tracker.id}", rr.Rect2D(tracker.bounds)) rr.log(f"tracked/{tracker.id}/cm", rr.Point2D(tracker.cm))这是典型的目标跟踪应用场景:每一帧先rr.set_time("frame", sequence=frame.id)关联时间点,再做跟踪更新;一旦tracker.is_lost为真,就向tracked/{tracker.id}写入rr.Clear(recursive=True),把该目标及其子树下的全部数据一次性清掉;否则正常写入主实体(包围框)和子实体(质心点)。通过把Clear和普通日志放在同一个frame时间点上,保证了清除动作与数据更新在时间轴上完全对齐。
flat 与 recursive:两种清除粒度
Clear构造函数的唯一参数是recursive(Python SDK 中为命名参数,见 clear_ext.py)。仓库同时提供了两个语义清晰的静态构造方法:
| 构造方式 | 清除范围 | 适用场景 |
|---|---|---|
rr.Clear(recursive=False)/rr.Clear.flat() | 只清空该实体本身在该时间点的全部组件,子实体不受影响 | 单个实体失效,但子树仍需保留 |
rr.Clear(recursive=True)/rr.Clear.recursive() | 清空该实体本身,以及其所有递归子实体的全部组件 | 整个实体子树同时失效(如目标丢失) |
源码中的语义注释进一步说明了二者的行为差异:
flat():“This will empty all components of the associated entity at the logged timepoint. Children will be left untouched.”(清空关联实体在该时间点的全部组件,子实体保持不动)recursive():“This will empty all components of the associated entity at the logged timepoint, as well as all components of all its recursive children.”(同时清空所有递归子实体的全部组件)
Python 侧的实现在 clear_ext.py,Rust 侧的等价实现位于 crates/store/re_types_core/src/archetypes/clear_ext.rs,二者保持一致,均委托给底层Clear::new(is_recursive)构造器(crates/store/re_types_core/src/archetypes/clear.rs)。
底层原理:一个只有 is_recursive 组件的归档类型
从源码看,Clear是整个 Rerun 中最“轻量”的归档类型之一:它只包含一个必填组件ClearIsRecursive,没有推荐组件、没有可选组件(REQUIRED_COMPONENTS = 1,见 clear.rs)。其类型定义来自crates/build/re_type_definitions/rerun/archetypes/clear.def.rs,并被代码生成器同步产出 Python、Rust 等各语言绑定。
用一句话概括它在数据层的行为(也是 clear.py 与 clear.rs 中共同的 docstring):
Clear 的存在意味着:对该路径做 latest-at 查询时,不会再返回在 Clear 之前写入该路径的任何组件;而 Clear 之后写入的组件不受影响。
这意味着两点关键推论:
- 过去的数据并未被物理删除:如果做 range(范围)查询并覆盖 Clear 之前的时刻,仍能取到那些组件。因此实践中,当你在 Viewer 中使用“可见时间范围”(visible time ranges)时,Clear 是无效的——它只作用于 latest-at 语义。
- 标量曲线(Scalar plots)是例外:时间序列视图会显式跟踪 Clear,并用它来表示数据中的“空洞”(即不连续的断线),而不是简单地把曲线截断。
从源码验证查询语义:entity_db 测试与视图层
仓库用两个层面的代码验证并消费上述语义:
数据层测试:crates/store_app/re_entity_db/tests/clear.rs 是一个完整的 “clear & pending clear” 测试套件(636 行),构造了parent、parent/child1、parent/deep/deep/down/child2、parent/child1/grandchild等多级实体路径,在frame时间轴上写入点、颜色等组件后再写入Clear,然后用latest_at查询验证:清除之后该路径不再返回清除前写入的组件、清除后新写入的数据正常返回、递归清除会覆盖全部后代路径等行为。
视图渲染层:crates/viewer_support/re_view/src/clears.rs 中的collect_recursive_clears函数展示了实际消费逻辑:它收集在给定时间范围内、对entity_path生效的每一个Clear的(time, row_id)。一个Clear生效的条件是——记录在该路径自身,或记录在其任意祖先且is_recursive = true;同时还会用latest_at在可见窗口起始点做一次“引导查询”(bootstrap),把恰好落在可见窗口之前、但仍在生效期内的 Clear 也拾取进来。这些时间点最终被喂给需要渲染“间断”的视觉化器(如折线图跨重置断开、状态时间线结束等),这也解释了为什么标量曲线能表现出断线效果。
方法二:澄清数据含义——重新设计日志方式
在某些情况下,与其用Clear事后清除,不如重新思考数据的组织方式,让数据本身更准确地表达“它是什么”。文档给出了一个更新频率不匹配的典型反例:
… for frame in sensors.read(): # Associate the following logs with `frame = frame.id` rr.set_time("frame", sequence=frame.id) # Log every image that comes in rr.log("input/image", rr.Image(frame.image)) if frame.id % 10 == 0: # Run detection every 10 frames detection = detector.detect(frame) # Woops! These detections will not update at the # same frequency as the input data and thus look strange rr.log("input/detections", rr.Rect2D(detection.bounds))这里每帧都记录input/image,但检测结果每 10 帧才更新一次,导致input/detections的包围框长期“僵持”在旧值上,观感上像是检测失效。虽然可以用rr.Clear修复,但更本质的解法是:把检测结果连同它实际使用的输入一起,记录到另一个命名空间下,让 Viewer 始终能看到“当前检测结果对应的输入帧是哪一帧”。
修复示例:
class Detector: … def detect(self, frame): downscaled = self.downscale(frame.image) # Log the downscaled image rr.log("detections/source", rr.Image(downscaled)) result = self.model(downscaled) detection = self.post_process(result) # Log the detections together with the downscaled image # Image and detections will update at the same frequency rr.log("downscaled/detections", rr.Rect2D(detection.bounds)) return detection … for frame in sensors.read(): # Associate the following logs with `frame = frame.id` rr.set_time("frame", sequence=frame.id) # Log every image that comes in rr.log("input/image", rr.Image(frame.image)) if frame.id % 10 == 0: # Run detection every 10 frames # Logging of detections now happens inside the detector detected = detector.detect(frame)这个方案在 Viewer 中会形成第二个视图:downscaled/detections下的降采样图像与检测框以完全相同的频率一起更新,二者永远保持时间一致,杜绝了“检测框配错图”的歧义。核心思想是:当两组数据天然存在依赖关系(检测依赖输入)时,把它们放进同一时间节奏的实体路径下,比事后清除更能表达真实语义。
方法三:时间跨段(spans)语义的现状与手动清除替代方案
有些场景下,你在记录数据时就已经知道这条数据的有效期(例如一个只持续 1 秒的传感器读数)。理想情况下,你希望直接记录(from_timepoint, to_timepoint)或(timepoint, time-to-live)这样的时间跨段(span),让 Rerun 在有效期结束后自动隐藏数据。
需要明确的是:Rerun 目前尚不支持为日志数据关联 span 语义(该能力在项目中是已知的未实现特性,社区对此有持续的讨论与跟踪)。因此在当前版本中,唯一可靠的替代方案是——在数据失效的时间点上,手动记录一个非递归的Clear:
# Associate the following data with `start_time` on the `time` timeline rr.set_time("time", duration=start_time) # Log the data as usual rr.log("short_lived", rr.Tensor(one_second_tensor)) # Associate the following clear with `start_time + 1.0` on the `time` timeline rr.set_time("time", duration=start_time + 1.0) rr.log("short_lived", rr.Clear(recursive=False)) # or `rr.Clear.flat()` # Set the time back so other data isn't accidentally logged in the future. rr.set_time("time", duration=start_time)这段代码有三个值得注意的细节:
- 把 Clear 记录在
start_time + 1.0这个时间点,即数据恰好失效的那一刻。由于 latest-at 语义,任何晚于该时间点的查询都不会再看到short_lived实体上的旧张量。 - 使用
recursive=False(或rr.Clear.flat()):因为这里只想清除short_lived这一个实体,而不应波及其子树。 - 记录完 Clear 后把时间设回
start_time:rr.set_time设置的是“当前时间上下文”,如果不清除它,后续代码中未显式设置时间的日志都会被错误地关联到start_time + 1.0,导致数据被记录到“未来”。这是多时间点编程中极易踩坑、也极易被忽略的一行。
实践要点与小结
综合文档与源码,把“清除数据”的正确姿势总结为一张决策清单:
| 场景 | 推荐方案 | 关键参数 |
|---|---|---|
| 实体(含子树)永久失效,如目标丢失 | rr.Clear(recursive=True)记录在失效时刻 | recursive=True |
| 单个实体失效、子树保留 | rr.Clear(recursive=False)或rr.Clear.flat() | recursive=False |
| 数据频率不匹配导致观感异常 | 重构日志命名空间,让依赖数据同步更新 | 无需 Clear |
| 已知有效期(short-lived 数据) | 在失效时刻手动记录非递归 Clear | 记录后把时间设回 |
核心结论:
Clear是 tombstone 而非删除:它只影响latest-at 查询,过去的数据在 range 查询中仍然可读;使用可见时间范围时 Clear 无效,标量曲线除外(表现为断线)。- 递归清除通过
is_recursive组件实现,并在视图层由collect_recursive_clears收集生效的(time, row_id)用于渲染间断(crates/viewer_support/re_view/src/clears.rs)。 - 数据层行为有完整测试背书(crates/store_app/re_entity_db/tests/clear.rs),可放心依赖该语义。
- 在 span 语义落地前,“在失效时间点手动 Clear + 用后即恢复时间上下文”是处理短生命周期数据的标准工作流。
若想深入验证,建议阅读本指南的原始文档 docs/content/howto/logging-and-ingestion/clears.md、Python 侧构造器 clear_ext.py 与 Rust 侧实现 clear.rs,并结合re_entity_db/tests/clear.rs中的测试用例逐条对照理解。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考