Rerun Lenses 详解:用派生与变异镜头提取、重塑并重路由多模态数据

Rerun Lenses 详解:用派生与变异镜头提取、重塑并重路由多模态数据 Rerun Lenses 详解用派生与变异镜头提取、重塑并重路由多模态数据【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun本篇围绕 Rerun 官方文档 Lenses 展开Lenses 是 Rerun 数据管线中“在飞行中on the fly提取、重塑并重路由组件数据”的机制能把 Rerun 未定义语义的用户自定义类型例如经 schema reflection 导入的 Protobuf MCAP 消息转换成标准的 Rerun 组件列、实体路径与时间线。读完本文你将能够使用 Rust 的Lens/LensesSink或 Python 的DeriveLens/MutateLens构建镜头书写 jq 风格的Selector查询含pack()、!、?与管道组合理解三种输出模式ForwardUnmatched/ForwardAll/DropUnmatched的数据转发语义并了解 Rerun 内部如何用镜头实现 MCAP 等数据导入器。1. 动机让任意形状的数据获得 Rerun 语义Rerun 的设计目标是处理各种类型与形状的用户数据。除了 Rerun 自身定义的编码也可以把用户自定义类型的数据加载进查看器和.rrd文件——例如通过 schema reflection 导入任意基于 Protobuf 的 MCAP 消息。但这类数据本身没有 Rerun 的可视化/查询语义。Lenses 正是为此提供的表达性 API它允许你把组件重路由reroute到不同的实体为任意数据附加 Rerun 语义把提取结果挂到Scalars、Position3D等已知组件描述符上重塑单个组件中存储的值简化、缩放、单位换算等。入口方面Rust SDK 提供LensesSink或直接在Chunk上通过ChunkExttrait 应用Python 中则可以直接把镜头应用到 chunk或作为ChunkStreamAPI 的管线步骤。从源码结构看核心定义集中在 re_lenses_core 这个 crate镜头类型、构建器与可组合的 Arrow 数组变换并经由 re_lenses 对 SDK 暴露。值得注意的是Rerun 自身在内部就用镜头实现了大量数据导入器MCAP 导入器就是典型例子参见 re_importer。也就是说Lenses 既是对外的用户 API也是 Rerun 导入管线的内部基础设施。2. 示例数据带结构体组件的输入 Chunk官方文档的所有示例都操作于同一个输入 chunk记录在实体/sensor/imu上使用frame作为时间线包含两个组件列Imu:accel结构体类型与Imu:status字符串。Rust 端构建输入 chunk 的完整代码来自 lenses.rs 示例// 构建带结构体类型组件的 chunk let imu_struct StructArray::from(vec![ ( Field::new(x, DataType::Float64, false).into(), Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0])) as _, ), ( Field::new(y, DataType::Float64, false).into(), Arc::new(Float64Array::from(vec![4.0, 5.0, 6.0])) as _, ), ( Field::new(elapsed, DataType::Int64, false).into(), Arc::new(Int64Array::from(vec![0, 10_000_000, 20_000_000])), ), ]); let status StringArray::from(vec![ok, ok, warn]); let chunk Chunk::from_columns( /sensor/imu, [TimeColumn::new_sequence(frame, [0, 1, 2])], [ ( rerun::ComponentDescriptor::partial(Imu:accel) .with_archetype(Imu.into()), imu_list, // ListArray 包装 imu_struct ), ( rerun::ComponentDescriptor::partial(Imu:status) .with_archetype(Imu.into()), status_list, ), ], )?;Python 端等价代码来自 lenses.py 示例import pyarrow as pa import rerun as rr from rerun.chunk import Chunk, DeriveLens, LazyChunkStream, MutateLens, Selector imu_data pa.StructArray.from_arrays( [ pa.array([1.0, 2.0, 3.0], typepa.float64()), pa.array([4.0, 5.0, 6.0], typepa.float64()), pa.array([0, 10_000_000, 20_000_000], typepa.int64()), ], names[x, y, elapsed], ) status_data pa.array([ok, ok, warn], typepa.utf8()) chunk Chunk.from_columns( /sensor/imu, indexes[rr.TimeColumn(frame, sequence[0, 1, 2])], columnsrr.DynamicArchetype.columns( archetypeImu, components{accel: imu_data, status: status_data} ), )得到的数据如下表Imu:accel每行是一个{x, y, elapsed}结构体列表frameImu:accelImu:status0[{x: 1.0, y: 4.0, elapsed: 0}][ok]1[{x: 2.0, y: 5.0, elapsed: 10000000}][ok]2[{x: 3.0, y: 6.0, elapsed: 20000000}][warn]3. 派生镜头Derive Lens从输入列生成新列派生镜头从一个输入组件创建新的组件列它选择输入列用Selector提取数据并把结果写成新列可选地写到不同实体、附加新的时间线。以下镜头从Imu:accel中提取.y字段作为Scalar组件把.elapsed提取为新的时间线并都写到新实体/new_entity/accel_y// 提取 y 字段到不同实体并把 elapsed 字段提取为新时间线 let extract_y Lens::derive(Imu:accel) .output_entity(/new_entity/accel_y) .to_component( rerun::Scalars::descriptor_scalars(), Selector::parse(.y)?, ) .to_timeline( sensor_elapsed, TimeType::DurationNs, Selector::parse(.elapsed)?, ) .build()?;extract_y ( DeriveLens(Imu:accel, output_entity/new_entity/accel_y) .to_component(rr.Scalars.descriptor_scalars(), .y) .to_timeline(sensor_elapsed, duration_ns, .elapsed) )应用extract_y后得到两组结果 chunk在/sensor/imu上未被该镜头消费的Imu:status列原样保留frameImu:status0[ok]1[ok]2[warn]在/new_entity/accel_y上得到提取出的Scalar列和新的sensor_elapsed时间线framesensor_elapsedScalars:scalars00[4.0]110000000[5.0]220000000[6.0]注意原始frame时间线在所有实体上都存在且取值正确——镜头在重路由数据时不会丢失原有时间列。3.1 构建器 API 的完整能力源码佐证从 DeriveLensBuilder 的实现看Rust 端派生镜头的构建器比示例展示的更丰富Lens::derive(input)1:1 行映射的派生镜头默认Rows::OneToOneLens::scatter(input)1:N 行映射scatter/explode每个输入行可产生多个输出行即构建器上的scatter_rows()output_entity(...)设置目标实体路径输出写到该实体而非输入实体to_component(descriptor, selector)追加一个组件输出列原样发出to_component_with_cast(descriptor, selector, cast)追加组件输出列并做类型转换cast支持CastTo::Auto通过组件描述符反射查找目标组件的规范 Arrow 类型或CastTo::Type(DataType)显式指定元素类型to_timeline(name, time_type, selector)从输入列提取数据生成新的时间列。build()有两个约束值得注意见 builder.rs 的build方法必须至少有一个组件输出否则报MissingOutputComponent错误时间线输出不能单独存在若未指定目标实体输出组件不能与输入组件同名InputEqualsOutput避免同实体下原地覆盖语义的歧义——原地修改请改用 mutate 镜头。镜头的内部表示ast.rs按输出位置分成三类Mutate、DeriveSameEntity1:1 且无时间线输出时可合并进前缀 chunk见is_merge_candidate与DeriveSeparateEntity。这个分类发生在镜头加入Lenses集合时使得逐 chunk 执行时免去重复的分类开销。4. 变异镜头Mutate Lens原地改写组件列变异镜头通过选择器修改一个已存在的组件列与派生镜头不同它不创建新列输入列就地变换并停留在原实体。以下镜头把Imu:accel结构体简化为只保留.x字段// 把 accel 结构体就地简化为其 x 字段 let simplify_accel Lens::mutate(Imu:accel, Selector::parse(.x)?).build();simplify_accel MutateLens(Imu:accel, .x)应用simplify_accel后/sensor/imu变为frameImu:accelImu:status0[1.0][ok]1[2.0][ok]2[3.0][warn]结构体被提取出的 float 值替换而Imu:status保持不变。从 MutateLensBuilder 看变异镜头还支持keep_row_ids()——在输出 chunk 中保留原始RowIds默认会生成新的行 ID。5. 输出模式流式转发时哪些组件通过当把数据流通过镜头时输出模式OutputMode控制哪些组件被转发模式语义ForwardUnmatched转发未被任何镜头消费的原始组件外加所有镜头产生的输出ForwardAll转发全部原始组件外加镜头输出。会造成数据重复但便于调试DropUnmatched只转发镜头产生的输出丢弃其余组件这是跨整个镜头集合的全局设置Lenses集合上的mode字段可通过set_output_mode修改。例如 Python 端在LazyChunkStream.lenses(...)中显式传入output_modeforward_unmatched。在 ChunkExt trait 中可以看到默认行为Chunk::apply_lenses(lenses, runtime)内部以OutputMode::ForwardUnmatched创建Lenses集合再执行若没有任何镜头匹配该 chunk包括传入空切片原 chunk 原样返回。此外 trait 还提供了apply_selector对单个组件就地应用选择器并保留原描述符——源码注释建议性能敏感场景优先用Lens::mutateapply_lenses因为后者可以在单次遍历中处理多个变换。6. Selectorsjq 风格的声明式列式查询列内部的内容与值变换由Selectors表达简洁、声明式、受jq启发的表达式。由于许多用户自定义类型是层次化嵌套的消息定义选择器提供了描述提取过程的自然方式。文档列出的基本语法元素.— 恒等选择当前值.field— 访问具名字段如.my.nested.struct.field.sequence[]— 遍历序列中的所有元素.sequence[].x— 访问序列每个元素上的字段.optional_field?— 访问可选字段跳过缺失值pack(.x, .y, .z)— 把多个同类型字段打包成固定长度列表见下文。这些元素可以通过管道|组合。从 selector 模块 的实现文档看该语法是 jq 的一个子集语义是**列式columnar**的——遵循 Apache Arrow 数据模型而非行式对象模型语法含义示例.field访问结构体中的具名字段.location[]遍历列表中每个元素.poses[][N]按位置索引列表.[0]?错误抑制 / 可选操作符.field?!断言非空把全空行提升为外层 null.field!\|把一个表达式的输出传给下一个.foo \| .barpack(…)把 1:1 路径打包为FixedSizeListpack(.x, .y, .z)段segment可以在没有显式管道的情况下链式书写.poses[].x等价于.poses[] | .x。与jq的关键差异列式而非行式——操作作用于整个 Arrow 列而非单个 JSON 值没有过滤器和算术运算——仅支持路径导航、迭代和内置函数数值运算请通过管道接宿主语言函数见下节不支持引号字段名或字符串插值——字段名必须是裸标识符字母数字、-、_。解析实现上Selector::parse或字符串FromStr经过 lexer.rs 词法分析、parser.rs 语法分析得到Expr树再由 eval.rs 对 Arrow 数组求值。6.1 面向 Protobuf 的?与!空值处理?和!两个操作符主要服务于从 Protobuf 消息生成的 Arrow 列。Proto3 的optional字段带存在性追踪presence tracking字段未设置时对应 Arrow 列里是null而非类型默认值。因此导航进含可选子字段的 struct可能得到内部值全为 null 的列表例如[null]而非顶层null?在字段完全不存在于 schema时如 schema 演进、可选列被省略抑制错误!把“所有内部值均为 null”的行提升为外层 null即折叠[null] → null让下游消费者看到干净的 null 语义。6.2 管道从查询语法到宿主函数|操作符把前一个表达式的输出送入下一个类似 Unix 管道。在查询字符串中它用于串联多步以提升可读性.poses[] | .x。更强大的是Selector.pipe()还可以链到宿主语言的任意函数上用于超出路径导航之外的值变换如单位换算或算术。例如以下镜头提取.x字段并乘以9.81缩放// 提取 .x 后用管道应用自定义变换 let scale_x Lens::derive(Imu:accel) .output_entity(/new_entity/accel_scaled_x) .to_component( rerun::Scalars::descriptor_scalars(), Selector::parse(.x)?.pipe(|arr: ArrayRef| { let scaled: Float64Array compute::unary(arr.as_primitive::Float64Type(), |v| { v * 9.81 }); Ok(Some(Arc::new(scaled) as _)) }), ) .build()?;extract_scaled_x DeriveLens( Imu:accel, output_entity/new_entity/accel_scaled_x ).to_component( rr.Scalars.descriptor_scalars(), Selector(.x).pipe(lambda arr: pa.compute.multiply(arr, 9.81)), )源码层面见 selector/mod.rs 中pipe与IntoDynExpr的实现pipe接收任何可转为DynExpr的东西——另一个Selector或满足Fn(ArrayRef) - ResultOptionArrayRef Send Sync的匿名函数返回的是SelectorDynExpr可与普通选择器一样执行。这就是 Rust 端“动态表达式”的能力来源路径导航与任意 Arrow 变换可以任意交错组合。6.3 用pack(...)构建固定长度列表组件许多 Rerun 组件基于 Arrow 的 FixedSizeList。例如Position3D就是FixedSizeListf32[3]。pack(...)把若干条解析到同一数据类型的路径组装成固定长度列表例如pack(.x, .y, .z)。选择器模块 的实现文档进一步给出了精确约束每条路径对pack的输入求值后必须每行恰好一个值、数据类型相同可空性可以不同路径按行 zip结果行数与输入一致可空性遵循条目级 AND模型只要某条路径在某一行为 null整个条目即为 null某个组件缺失会让整个条目变 null。因为一条路径的 null 会遮蔽兄弟字段的合法值这种遮蔽必须被显式承认非空路径无需标注可空路径必须用!标注如pack(.x, .y!, .z)否则pack直接报错。这一要求是类型驱动的——取决于 schema 中该路径是否可空而不是当前批次是否真的含 null——所以同一个pack要么总能通过校验、要么总是报错与数据内容无关结果FixedSizeList及其元素字段的可空性为“任一可空路径为则可空”元素级 null 只出现在 null 条目之下。7. 端到端应用从 Chunk 到结果流把四个镜头extract_x、extract_y、simplify_accel、scale_x应用到同一个 chunk 的完整流程Rust 端为let results chunk .apply_lenses( [extract_x, extract_y, simplify_accel, scale_x], rerun::lenses::default_runtime(), ) .map_err(|partial| { let errors: Vec_ partial.errors().map(|e| e.to_string()).collect(); format!(Lens errors: {}, errors.join(, )) })?; rec.send_chunks(results);Python 端则把镜头作为ChunkStream的管线步骤stream LazyChunkStream.from_iter([chunk]) results stream.lenses( [extract_x, extract_y, simplify_accel, extract_scaled_x], output_modeforward_unmatched, ) rr.send_chunks(results)执行入口对应 execute.rs 中的Lenses::apply每个镜头按输入组件匹配若多个镜头对同一输出组件标识符产生冲突则第一个镜头获胜、重复的被跳过并给出警告。8. 小结与延伸阅读镜头分两类derive新列可换实体、可加时间线1:1 或 scatter 1:N与 mutate原地改写输出模式决定未匹配组件的去向选择器是列式 jq 子集路径导航 迭代 ?/!空值控制 pack定长列表配合pipe可以接任意宿主函数完成数值变换完整的可运行示例Rust 版 docs/snippets/all/concepts/lenses.rs、Python 版 docs/snippets/all/concepts/lenses.py核心实现镜头 AST 与输出模式 crates/store/re_lenses_core/src/ast.rs、构建器 crates/store/re_lenses_core/src/builder.rs、选择器解析与求值 crates/store/re_lenses_core/src/selector/mod.rs、chunk 扩展 crates/store/re_lenses_core/src/chunk.rs相关概念数据目录对象模型、DataFusion 查询以及作为内部消费者的 MCAP 消息格式与 schema reflection。【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考