Rerun DepthImage 原语详解:深度图像的记录、单位换算与 3D 点云反投影
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
DepthImage 是 Rerun 中用于记录深度相机数据的核心原语(archetype):每个像素对应一个由DepthMeter指定单位的深度值。本文以 depth_image.md 参考文档为骨架,结合仓库内 SDK 生成源码、Spatial 视图可视化器实现以及 Python / C++ / Rust 三端示例,系统讲解 DepthImage 的字段语义、默认行为、2D 着色与 3D 点云反投影原理。读完本文,你将能够正确构造 DepthImage、理解meter与depth_range对渲染结果的影响,并把带针孔相机标定的深度图一键变成 3D 点云。
DepthImage 是什么
DepthImage 表示由深度相机(depth camera)捕获的深度图像。其官方定义位于 depth_image.def.rs,文档分类为 "Image & tensor"(图像与张量),状态为stable(稳定),并由DepthImage可视化器(visualizer)负责渲染:
A depth image, i.e. as captured by a depth camera. Each pixel corresponds to a depth value in units specified by
DepthMeter.
这段话说明了 DepthImage 的两个关键特征:
- 语义上不是普通图像:像素值是标量深度(距离),而不是颜色。因此渲染时必须先经过 colormap(颜色映射)才能看到可视化结果。
- 单位是可配置的:像素值所代表的物理长度由
DepthMeter组件决定(例如 uint16 像素值 1000 表示 1 米,即毫米精度)。
在 SDK 中,DepthImage是一个包含 8 个组件的 archetype(2 个必填、0 个推荐、6 个可选),Rust 侧的完整定义见 depth_image.rs,其类型声明为"rerun.archetypes.DepthImage",显示名为 "Depth image"。
字段详解:必填与可选组件
必填字段(2 个)
| 字段 | 组件类型 | 说明 |
|---|---|---|
buffer | ImageBuffer | 原始深度图像数据,如 uint8 / uint16 / float32 的连续数组 |
format | ImageFormat | 图像的格式(宽、高、像素类型、色彩模型等) |
这两个字段均标记为#[rerun(no_ui_edit)](定义见 depth_image.def.rs),即它们属于结构性数据,不允许在 Viewer 的 UI 中直接编辑,只能通过重新记录数据来更新。Rust SDK 中DepthImage::new(buffer, format)的构造器签名也印证了这一点(depth_image.rs)。
从 C++ 使用角度,由于ImageBuffer底层使用rerun::Collection,原始指针、std::vector、std::array或 C 数组可以直接零拷贝传入,无需先拷贝到中间容器;如需扩展,还可以自定义CollectionAdapter(见 depth_image.def.rs)。
可选字段(6 个)
| 字段 | 组件类型 | 类型 | 默认行为 |
|---|---|---|---|
meter | DepthMeter | Float32 | 浮点深度格式默认1.0;整数格式默认1000.0(毫米) |
colormap | Colormap | 枚举 | 未设置时使用 Turbo 颜色映射 |
depth_range | ValueRange | Range1D | 未设置时由 Viewer 根据数据自动估计 |
point_fill_ratio | FillRatio | Float32 | 默认1.0,仅影响 3D 视图 |
draw_order | DrawOrder | Float32 | 默认-20.0,仅影响 2D 视图 |
magnification_filter | MagnificationFilter | 枚举 | 2D 放大时的纹理过滤方式 |
对应组件在源码中的类型包装可参见 depth_meter.rs、fill_ratio.rs 与 value_range.rs。
meter:深度单位换算
meter是一个浮点值,表示「1 米等于多少个原生深度单位」。定义中的注释给出了一个直观的例子(depth_image.def.rs):
with uint16, perhaps
meter=1000which would mean you have millimeter precision and a range of up to ~65 meters (2^16 / 1000).
即:uint16 最大像素值 65535,若meter=1000(每米 1000 个原生单位,毫米精度),则最大可表示的深度约 65 米。meter在 2D 与 3D 视图中的作用不同:
- 2D 视图:只影响鼠标悬停时显示的物理深度值;
- 3D 视图:直接决定反投影点云中各点沿相机光轴方向的坐标位置。
若省略该字段,Viewer 对浮点深度格式默认1.0,对整数格式默认1000.0(即毫米)。
colormap:着色方式
深度值是标量,必须映射为颜色才能显示。默认使用Turbo颜色映射;可选值包括Grayscale、Inferno、Magma、Plasma、Turbo、Viridis等(示例中即使用viridis)。该逻辑在可视化器中体现为:所有深度图像都必须有一个 colormap,未设置时通过 fallback 机制补齐(depth_images.rs)。
depth_range:颜色映射范围
depth_range指定期望的深度值范围,通常对应有效值范围:
- 超出该范围的值在颜色映射时会被 clamp 到范围内;
- 但由该图生成的点云仍会显示全部点,不受范围裁剪影响。
若未指定,Viewer 会从数据自动估计,且可能估计出比数据实际最小/最大值更宽的范围。例如:若所有值均为正、部分大于 1.0 且全部小于 255.0,Viewer 会猜测数据来自 8bit 图像,从而假设范围为 0–255(depth_image.def.rs)。在可视化器实现中,未设置value_range时通过ImageStatsCache计算图像统计量,再调用ColormapWithRange::default_range_for_depth_images得到默认范围(depth_images.rs)。
point_fill_ratio:点云点半径
该值缩放由深度图生成的 3D 点云中每个点的半径:
1.0(默认):每个点大到与同深度相邻点的中心相接触,不留空隙;0.5:每个点恰好与同深度相邻点的边缘相接触。
注意源码中的TODO(#6744)标注:该参数当前仅作用于 3D 视图(depth_image.def.rs)。
draw_order:2D 绘制顺序
仅当深度图以 2D 图像形式展示时生效,值越大越靠上层绘制,默认-20.0。
magnification_filter:2D 放大过滤
2D 视图下当纹素被放大(texel 大于屏幕像素)时使用的过滤方式。过滤作用于标量值在经 colormap 映射为颜色之前,因此过滤的是深度数值而非颜色;对 3D 视图无影响(depth_image.def.rs)。
可在哪些视图中展示
参考文档明确列出 DepthImage 支持的三类视图:
- Spatial2DView:以经过 colormap 着色的 2D 图像(textured rect)展示;
- Spatial3DView:在实体位于针孔相机(pinhole)投影之下时,自动反投影为 3D 深度点云(depth cloud);
- DataframeView:以表格形式查看组件数据。
从源码看,SpatialView3D下深度图不再显示为纹理矩形,而是需要变换树中存在 pinhole 相机才生成点云;而即使最终只展示点云,纹理矩形仍会被构建用于 UI 交互(悬停取色等),见 depth_images.rs。
示例一:记录一张简单的深度图像
参考文档的 "Simple example" 在仓库中对应三份代码:depth_image_simple.py、depth_image_simple.cpp、depth_image_simple.rs。它们生成一张 200×300 的 uint16 合成深度图,背景 65535,两个矩形区域分别填 20000 与 45000。
Python:
import numpy as np import rerun as rr depth_image = 65535 * np.ones((200, 300), dtype=np.uint16) depth_image[50:150, 50:150] = 20000 depth_image[130:180, 100:280] = 45000 rr.init("rerun_example_depth_image_simple", spawn=True) # Log the tensor, assigning names to each dimension rr.log("depth", rr.DepthImage(depth_image, meter=10_000.0))C++:
#include <rerun.hpp> #include <algorithm> // fill_n #include <vector> int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_depth_image_simple"); rec.spawn().exit_on_failure(); // create a synthetic depth image. const uint32_t HEIGHT = 200; const uint32_t WIDTH = 300; std::vector<uint16_t> pixels(WIDTH * HEIGHT, 65535); for (uint32_t y = 50; y < 150; ++y) { std::fill_n( pixels.begin() + y * WIDTH + 50, 100, static_cast<uint16_t>(20000) ); } for (uint32_t y = 130; y < 180; ++y) { std::fill_n( pixels.begin() + y * WIDTH + 100, 180, static_cast<uint16_t>(45000) ); } rec.log( "depth", rerun::DepthImage(pixels.data(), {WIDTH, HEIGHT}).with_meter(10000.0) ); }Rust:
//! Create and log a depth image. use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box<dyn std::error::Error>> { let rec = rerun::RecordingStreamBuilder::new("rerun_example_depth_image_simple") .spawn()?; let mut image = Array::<u16, _>::from_elem((200, 300).f(), 65535); image.slice_mut(s![50..150, 50..150]).fill(20000); image.slice_mut(s![130..180, 100..280]).fill(45000); let depth_image = rerun::DepthImage::try_from(image)?.with_meter(10_000.0); rec.log("depth", &depth_image)?; Ok(()) }几个值得注意的实操要点:
- 维度顺序:NumPy 中数组形状为
(height, width)(行优先,200 行 × 300 列);Rust 侧使用 Fortran 顺序(200, 300).f()与之对应。 meter=10_000.0的含义:这里是 1 米 = 10000 个原生单位,即 0.1 毫米精度(参考文档示例即采用此值)。DepthImage::try_from:Rust SDK 支持直接从ndarray数组构造,自动推导出ImageBuffer与ImageFormat两个必填字段,无需手动指定尺寸。
示例二:深度图反投影为 3D 点云
参考文档的 "Depth to 3D example"(depth_image_3d.py、depth_image_3d.cpp、depth_image_3d.rs)展示了 DepthImage 最有价值的场景:在实体路径上先记录一个 Pinhole(针孔相机)模型,深度图会被自动反投影为 3D 点云。
Python:
import numpy as np import rerun as rr depth_image = 65535 * np.ones((200, 300), dtype=np.uint16) depth_image[50:150, 50:150] = 20000 depth_image[130:180, 100:280] = 45000 rr.init("rerun_example_depth_image_3d", spawn=True) # If we log a pinhole camera model, the depth gets automatically # back-projected to 3D rr.log( "world/camera", rr.Pinhole( width=depth_image.shape[1], height=depth_image.shape[0], focal_length=200, ), ) # Log the tensor. rr.log( "world/camera/depth", rr.DepthImage(depth_image, meter=10_000.0, colormap="viridis"), )C++:
#include <rerun.hpp> #include <algorithm> // fill_n #include <vector> int main(int argc, char* argv[]) { const auto rec = rerun::RecordingStream("rerun_example_depth_image_3d"); rec.spawn().exit_on_failure(); // Create a synthetic depth image. const int HEIGHT = 200; const int WIDTH = 300; std::vector<uint16_t> data(WIDTH * HEIGHT, 65535); for (auto y = 50; y < 150; ++y) { std::fill_n( data.begin() + y * WIDTH + 50, 100, static_cast<uint16_t>(20000) ); } for (auto y = 130; y < 180; ++y) { std::fill_n( data.begin() + y * WIDTH + 100, 180, static_cast<uint16_t>(45000) ); } // If we log a pinhole camera model, the depth gets automatically back-projected to 3D rec.log( "world/camera", rerun::Pinhole::from_focal_length_and_resolution( 200.0f, {static_cast<float>(WIDTH), static_cast<float>(HEIGHT)} ) ); rec.log( "world/camera/depth", rerun::DepthImage(data.data(), {WIDTH, HEIGHT}) .with_meter(10000.0) .with_colormap(rerun::Colormap::Viridis) ); }Rust:
use ndarray::{Array, ShapeBuilder as _, s}; fn main() -> Result<(), Box<dyn std::error::Error>> { let rec = rerun::RecordingStreamBuilder::new("rerun_example_depth_image_3d") .spawn()?; let width = 300; let height = 200; let mut image = Array::<u16, _>::from_elem((height, width).f(), 65535); image.slice_mut(s![50..150, 50..150]).fill(20000); image.slice_mut(s![130..180, 100..280]).fill(45000); let depth_image = rerun::DepthImage::try_from(image)? .with_meter(10000.0) .with_colormap(rerun::components::Colormap::Viridis); // If we log a pinhole camera model, the depth gets automatically back-projected to 3D rec.log( "world/camera", &rerun::Pinhole::from_focal_length_and_resolution( [200.0, 200.0], [width as f32, height as f32], ), )?; rec.log("world/camera/depth", &depth_image)?; Ok(()) }该示例的关键结构是层级实体路径:
world/camera:记录Pinhole相机模型(焦距 200,分辨率与深度图一致 300×200);world/camera/depth:记录DepthImage。
在 3D 视图中,可视化器会沿实体路径向上查找 pinhole 变换,一旦找到,就依据针孔模型把每个像素反投影为 3D 点,形成深度点云;meter与colormap在此场景下分别决定点云在光轴方向的物理位置与点云着色。这正是机器人多模态数据可视化(如 RGB-D 相机、LiDAR 投影、SLAM 回放)中最常用的套路:相机内参 + 深度帧 = 即时的 3D 观测。
渲染与可视化的底层原理
深度图在 Spatial 视图中的处理链路集中在 depth_images.rs,核心流程如下:
- 组装组件数据:从数据存储中取出
ImageInfo、DepthMeter、FillRatio、Colormap、ValueRange、MagnificationFilter,其中缺失的可选字段通过 fallback 机制补齐(depth_images.rs)。 - 生成着色纹理:用
ColormapWithRange{colormap, value_range}将深度标量映射为颜色,构建textured_rect(depth_images.rs)。 - 按视图分流:
- 2D 视图:直接展示着色后的纹理矩形;
- 3D 视图:先判断变换树中是否存在 pinhole,存在则生成
DepthCloud点云并丢弃纹理矩形(depth_images.rs 之后的部分)。
这段实现从侧面印证了参考文档的三条要点:colormap 缺省为 Turbo(fallback 逻辑兜底)、depth_range 缺省时由统计缓存自动估计、3D 点云生成以针孔投影为前提。同时,即使只显示点云,纹理矩形仍被保留用于 UI 交互(如悬停时查看对应像素的深度值),这也是文档中"2D 视图中 meter 影响悬停显示的物理深度值"这一行为的实现基础。
使用建议与注意事项
综合参考文档与源码实现,实际使用 DepthImage 时有几点值得留意:
- 必须同时提供 buffer 与 format:二者是必填组件;Rust 的
try_from与 Python 的rr.DepthImage(array, ...)会自动从数组推断格式,C++ 则通过{WIDTH, HEIGHT}显式给出尺寸。 - 整数深度务必设置
meter:整数格式默认按毫米(meter=1000.0)解释,若传感器原始单位不同(如 0.1mm 精度),不设置会导致 3D 点云距离与真实物理尺寸不符。 depth_range影响的是着色而非点云:clamp 只作用于 colormap 映射;若希望剔除无效深度点,需要在记录前自行过滤数据。point_fill_ratio当前仅对 3D 点云生效(源码中存在 TODO(#6744)),2D 显示不受影响。- 向 3D 反投影的前提是存在 pinhole:记得在深度图实体路径的祖先位置记录
Pinhole,且其分辨率(width/height)与深度图一致。 - DataframeView 同样支持该原语:可用表格方式逐行检查各组件值,便于调试数据问题。
参考文档本身还附有指向 C++ / Python / Rust 三端 API 文档的链接(ref.rerun.io与docs.rs),需要查阅完整构造器与重载方法时可访问对应语言的 API 参考。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考