LingBot-Depth:把 RGB-D 相机的“缺测”变成可用的空间先验

0. 简介

关于UCloud(优刻得)旗下的compshare算力共享平台

UCloud(优刻得)是中国知名的中立云计算服务商,科创板上市,中国云计算第一股。

Compshare GPU算力平台隶属于UCloud,专注于提供高性价4090算力资源,配备独立IP,支持按时、按天、按月灵活计费,支持github、huggingface访问加速。

使用下方链接注册可获得20元算力金,免费体验10小时4090云算力

https://www.compshare.cn/?ytag=GPU_lovelyyoshino_Lcsdn_csdn_display

最近受到优刻得的使用邀请,正好解决了我在大模型和自动驾驶行业对GPU的使用需求。UCloud云计算旗下的Compshare的GPU算力云平台。他们提供高性价比的5090 GPU,按时收费每卡2.5元,并附带50G的免费磁盘空间。暂时已经满足我的使用需求了,同时支持访问加速,独立IP等功能,能够更快的完成项目搭建。

LingBot-Depth 面向机器人操作、三维重建和动态场景理解中的 RGB-D 深度补全任务,处理的是消费级深度相机在玻璃、反光、弱纹理和复杂光照下出现的大面积缺测与噪声。项目把完整 RGB 图像、残缺深度和相机内参送入带深度感知注意力的 ViT-L/14 编码器,再由 ConvStack 解码出度量深度和相机空间点云。技术报告给出的训练配置使用 128 张 GPU、全局 batch size 1024,当前部署实测在 RTX 4090 上完成 640×480 单帧推理,Web 快速档峰值显存约 2.35 GiB,说明 单卡服务与大规模训练 是两种完全不同的资源问题。

工程上真正难的地方不在于把模型文件下载下来,而在于把输入单位、内参坐标系、变长 RGB-D token、显存并发和浏览器输出格式接在一起。项目官方示例要求深度图按毫米读取后换算成米,内参还要按图像宽高归一化;如果遗漏任何一个步骤,画面可能仍然显示,但点云尺度会错误,或者在推理时触发 CPU 与 CUDA 张量混用。目前相关内容已经在官网中可以直接安装使用了LingBot-Depth自动生成项目中了。

1. 深度相机的失败

1.1 深度相机的失败不是随机噪声

RGB-D 相机的优势是同时提供像素对齐和度量尺度,但它的失败模式也很具体:透明表面没有稳定纹理,镜面会把匹配线索反射到别处,弱纹理平面缺少可区分的局部特征,遮挡区域则根本没有有效回波。传统流程往往把这些像素标为无效并交给后处理,结果是下游定位、抓取和跟踪都失去同一套几何坐标。LingBot-Depth 的出发点是把缺测区域当成传感器暴露出来的几何歧义,把 RGB 视作完整条件,把剩余有效深度视作稀疏锚点。

1.2 它位于哪条路线的交叉点

现有路线可以分成三类:多视图几何强调相机运动和跨帧约束,单目深度估计强调视觉语义但通常缺少绝对尺度,主动深度传感器拥有实时度量信息却会在材料和成像条件上失效。LingBot-Depth 没有把三类路线简单拼在一起,而是用自然缺测训练一个 RGB-D 表征,再让同一套编码器同时服务深度补全、单目先验、视频深度、三维跟踪和灵巧抓取。这里要厘清的是,它首先是一个 RGB-D 深度细化模型,能否用于其他任务取决于下游系统如何消费它的 latent representation。

2. 整体框架:RGB 条件与有效深度共同编码

2.1 输入输出接口与 token 数量

模型输入包括 RGB 图像 I \mathbf{I} I、原始深度 D \mathbf{D} D 和可选相机内参 K \mathbf{K} K。RGB 在代码中转成 [B, 3, H, W][0,1] 浮点张量,深度转成 [B, H, W] 的米制浮点张量,内参则把 f x , c x f_x,c_x fx,cx 除以宽度、把 f y , c y f_y,c_y fy,cy 除以高度。输入经过 patch size 为 14 的两路 embedding 后,RGB token 和 depth token 在二维网格上对齐;输出字典包含细化深度、有效区域 mask,以及在给定内参时生成的 [B, H, W, 3] 相机空间点云。

N patch = H enc W enc 14 2 , N rgbd = N patch + N visible-depth + 1 N_{\text{patch}}=\frac{H_{\text{enc}}W_{\text{enc}}}{14^2},\qquad N_{\text{rgbd}}=N_{\text{patch}}+N_{\text{visible-depth}}+1 Npatch=142HencWenc,Nrgbd=Npatch+Nvisible-depth+1

其中 H enc H_{\text{enc}} Henc 和 W enc W_{\text{enc}} Wenc 是编码器内部按 num_tokens 调整后的尺寸, N visible-depth N_{\text{visible-depth}} Nvisible-depth 是没有被深度 mask 掉的深度 token 数,最后的 1 1 1 对应全局 [cls] token。这个公式解释了为什么 Web 端不能盲目按原始 4K 分辨率跑满:高分辨率会增加预处理和输出成本,真正进入 Transformer 的 token 数还受 resolution_level 控制,但有效深度比例会影响变长序列和显存峰值。

2.2 编码器、解码器与点云输出

编码器是 24 层 ViT-Large/14,RGB token 保持完整,深度 token 按 patch 内有效像素统计进行筛选。进入解码器前,latent depth token 被舍弃,只保留带有全局 [cls] 信息的上下文特征;ConvStack 解码器 逐级上采样并注入 UV 坐标,最后输出深度回归和有效区域。若调用者提供归一化内参,模型再把深度反投影为相机坐标系点云。这里的关键是,深度补全和点云导出共享同一张预测深度图,Web 端只需要把尺度和内参处理正确,就不必重复实现一套几何后处理。

难点提示:变长 token 像一间只保留有用座位的教室,RGB 区域全部入场,缺测深度的空座位先移除。这样既节省注意力计算,又让每个样本保留不同数量的有效深度证据,难点是后续注意力实现必须支持这种不规则批次。

3. 第一条核心机制:自然缺测就是训练信号

3.1 从随机遮挡转向传感器语义

普通 MAE 常用随机遮挡构造预训练任务,遮挡位置与真实传感器失败原因没有直接关系。LingBot-Depth 则先观察深度图中每个 patch 的有效像素比例和有效像素数量:完全缺测的 patch 必须 mask,部分缺测的 patch 以较高概率 mask,如果仍未达到目标比例,再从完全有效的 patch 中随机补齐。技术报告给出的深度 mask 比例大致在 60% 到 90% 之间,训练目标是让模型利用未被遮挡的 RGB 和少量深度锚点恢复完整度量结构。

3.2 mask_utils.py 中的实际实现

下面的代码来自 mdm/model/dinov2_rgbd/models/mask_utils.py。它没有把深度图先压成一个全局缺测率,而是把深度 reshape 成 patch,再按 patch 统计有效比例和有效数量,最终为 batch 中每个样本返回一组可见 token。这个实现直接对应论文中的自然 mask 设计,也是 Web 输入必须保留零值缺测语义的原因。

python 复制代码
# mdm/model/dinov2_rgbd/models/mask_utils.py
depth_reshaped = depth_values.view(B, 1, H_patch, patch_h, W_patch, patch_w)
depth_reshaped = depth_reshaped.permute(
    0, 2, 4, 1, 3, 5
).reshape(B, N, -1)

valid_depth = (
    (depth_reshaped >= min_depth)
    & (depth_reshaped <= max_depth)
)
valid_depth_ratio = valid_depth.float().mean(dim=-1)
valid_depth_num = valid_depth.float().sum(dim=-1)

if threshold_ratio is not None:
    invalid_mask |= valid_depth_ratio < threshold_ratio
if threshold_num is not None:
    invalid_mask |= valid_depth_num < threshold_num

这段代码做的不是普通的图像裁剪,而是在 token 化之前建立"这个 patch 是否有足够几何证据"的判定。换句话说,自然 mask 本身携带了传感器和材质信息,模型在训练时面对的困难与真实部署场景相似。工程上要避免把缺测像素改成随机非零值,也不要把无效值误当成毫米单位下的近距离,否则 mask 判定会被污染,模型会学到错误的深度先验,进而影响 Web 端的有效区域统计与点云质量。

直觉理解:这像驾校练习在湿滑路面上刹车,随机抽掉一块路面并不能模拟真实风险;传感器自然产生的空洞才是"哪种路况最容易失控"的提示。模型被迫看完整 RGB,再结合周围少量深度线索完成补全。

4. 第二条核心机制:分离 embedding 与跨模态注意力

4.1 RGB 和 depth 为什么要分两路

RGB 与深度共享空间位置,却不共享统计分布。RGB 是三通道外观信号,深度是单通道、带单位且存在大量零值的几何信号,如果把它们直接沿通道拼接,第一层卷积或 patch embedding 就要同时承担模态区分、尺度归一化和缺测处理。项目采用 分离 patch embedding,并加入共享空间位置编码和模态编码,让同一位置的 RGB token 与 depth token 既能对齐,又不会在表示层面混成同一种数据。

4.2 vision_transformer.py 如何拼接可见 token

下面的代码来自 mdm/model/dinov2_rgbd/models/vision_transformer.py。它先为 RGB 和 depth 分别生成 patch token,再把模态身份加到位置编码上,随后只对 depth token 做 mask,最后把 [cls]、完整 RGB token 和可见 depth token 拼成一个样本序列。这里的 BlockDiagonalMask 由变长序列路径使用,正是前面 Web 端不能强行切换到原生 SDPA 的兼容边界。

python 复制代码
# mdm/model/dinov2_rgbd/models/vision_transformer.py
x_img = self.patch_embed(x_img)
x_depth = self.depth_patch_embed(x_depth)

img_pose_enc = 1 + self.interpolate_pos_encoding_without_cls(
    x_img, h_img, w_img, self.pos_embed[:, 1:]
).repeat(B, 1, 1)
depth_pose_enc = 2 + self.interpolate_pos_encoding_without_cls(
    x_depth, h_depth, w_depth, self.pos_embed[:, 1:]
).repeat(B, 1, 1)

x_img = x_img + img_pose_enc
x_depth = x_depth + depth_pose_enc
x_depth_masked, depth_mask_info = depth_masking(
    x_depth, depth_patch_num_h, depth_patch_num_w,
    depth_values=x_depth_raw,
    depth_mask_threshold_num=[1] * B,
    valid_depth_range=(-9.5, 200.0),
)

这段实现的关键不在于"把两种 token 放到一个列表里",而在于给同一空间位置的两种模态不同的身份标记。数字 12 是简单但明确的 modality embedding,之后的 self-attention 才能学习"这个深度查询应该去哪里找 RGB 证据"。服务器端不需要修改模型源码,只要按官方输入格式提供 RGB、原始深度和内参即可保留这一机制。

工程价值:这像把地图上的道路和海拔分别用两种图层标记,而不是把所有颜色压进一张图片。查询某个深度 patch 时,模型知道哪些 token 是视觉线索、哪些 token 是几何线索,跨模态匹配才有稳定的语义边界。

5. ConvStack:让 token 重新长成稠密几何

5.1 [cls] 全局上下文与 UV 位置

ViT 的输出天然是序列,深度预测却需要每个像素都有数值。LingBot-Depth 的做法是把最终层上下文 token 恢复成二维特征图,再把 [cls] token broadcast 到每个空间位置,交给共享 neck 和多级解码头。解码器每一级通过残差卷积和上采样扩大空间分辨率,同时拼接归一化视平面 UV,使不同宽高比的输入仍然保留稳定的视野布局。核心问题在于,稠密深度边界既需要全局场景判断,也需要局部卷积的空间连续性,单一的浅层线性头很难同时满足两者。

5.2 modules_decoder.py 中的层级复用

下面的代码来自 mdm/model/modules_decoder.pyConvStack.forward() 对多个尺度重复使用输入 block、残差 block 和 resampler,并把每一级输出交给 task-specific head。这样做的好处是 encoder 负责高层上下文,decoder 负责空间恢复,各输出头可以共享中间几何特征,避免把 ViT 的全部 token 直接展开到原始分辨率,也让深度边界和局部空间连续性更容易在较低成本下恢复。

python 复制代码
# mdm/model/modules_decoder.py
def forward(self, in_features: List[torch.Tensor]):
    out_features = []
    for i in range(len(self.res_blocks)):
        feature = self.input_blocks[i](in_features[i])
        if i == 0:
            x = feature
        elif feature is not None:
            x = x + feature
        x = self.res_blocks[i](x)
        out_features.append(self.output_blocks[i](x))
        if i < len(self.res_blocks) - 1:
            x = self.resamplers[i](x)
    return out_features

这段代码看起来短,但它定义了部署时的一个重要取舍:高质量档位主要增加 encoder token 计算,输出分辨率仍然要回到用户输入尺寸;如果用户上传 4K 图像,Web 端就会承担更大的图像编码、点云导出和浏览器传输成本。当前服务增加了最长边上限,默认 1280 像素,并在缩放 RGB 和 depth 的同时同步缩放内参,确保输出几何尺度不因前处理而漂移,避免页面交互和显存峰值同时失控。

6. 推理路径:从官方示例到单卡 Web 服务

6.1 v2.py 的 infer() 过程

下面的代码来自 mdm/model/v2.pyinfer()。模型会根据 resolution_level 在 1200 到 3600 的 token 范围内选择内部规模,使用 BF16 autocast 完成 forward,随后把深度和 mask 转成 FP32,再按内参生成点云并执行有效区域过滤。这里的 apply_mask=True 会把无效区域写成 inf,这能让点云导出时直接过滤,但可视化函数必须先检查有限值,不能直接对全图做最小值和最大值统计。

python 复制代码
# mdm/model/v2.py
if num_tokens is None:
    min_tokens, max_tokens = self.num_tokens_range
    num_tokens = int(
        min_tokens
        + (resolution_level / 9) * (max_tokens - min_tokens)
    )

with torch.autocast(
    device_type=self.device.type,
    dtype=torch.bfloat16,
    enabled=use_fp16 and self.dtype != torch.bfloat16,
):
    output = self.forward(
        image, num_tokens=num_tokens, depth=depth_in, **kwargs
    )

depth_reg, mask = (output.get(k, None) for k in ["depth_reg", "mask"])
depth_reg, mask = map(
    lambda x: x.float() if isinstance(x, torch.Tensor) else x,
    [depth_reg, mask],
)

实测中,官方示例第一次加载模型约 57.38 秒,随后 640×480 单帧推理约 0.411 秒;Web API 的快速档因为包含上传封装、伪彩生成和 PLY/GLB 写盘,端到端约 0.628 秒,峰值显存约 2.35 GiB。这个差值说明 模型本体延迟 并不是完整用户体验,部署评估还要把文件处理、点云采样和浏览器传输一起纳入。

6.2 example.py 的输入准备

下面的片段来自 example.py,它展示了项目规定的深度单位和内参归一化方式。Web 端实现沿用了同一语义,并额外支持 .npy 深度;默认 depth_scale=1000.0 适合以毫米存储的 16 位 PNG,如果设备导出的单位本来就是米,则应把缩放系数改成 1.0,这项设置必须和相机 SDK 的实际输出单位保持一致。

python 复制代码
# example.py
depth_np = load_depth_map(str(depth_path))
depth_tensor = torch.tensor(
    depth_np, dtype=torch.float32, device=device
)

h, w = image_np.shape[:2]
intrinsics = load_intrinsics(str(intrinsics_path), w, h)
intrinsics_tensor = torch.tensor(
    intrinsics, dtype=torch.float32, device=device
).unsqueeze(0)

output = model.infer(
    image_tensor,
    depth_in=depth_tensor,
    apply_mask=not args.no_mask,
    intrinsics=intrinsics_tensor,
)

这里要厘清的是,内参不是一个只用于绘图的附加字段,它参与 depth_to_pointcloud() 的反投影。如果 RGB、depth 和内参文件尺寸不一致,Web 服务会用最近邻对齐 depth,并按 RGB 当前尺寸重新归一化内参;这种降级适合调试,不适合生产采集,生产流程最好从相机 SDK 直接保证三者像素对齐。

7. 训练目标与资源边界

7.1 深度重建的监督目标

训练时,模型从残缺 RGB-D 输入中预测完整深度,技术报告说明深度图使用有效 ground truth 像素上的 L1 损失。它没有要求输入深度和监督深度在所有像素都有效,因为传感器缺测恰好是训练信号的一部分;只在有效 ground truth 上计算损失,可以避免把未知区域的任意填充值当成错误答案。

$$

\mathcal{L}_{\text{depth}}

\frac{1}{\sum_{p}\mathbf{1}D\^{\\ast}*{p}\\ \\text{valid}}
\sum* {p}

\mathbf{1}D\^{\\ast}*{p}\\ \\text{valid}
\left|D* {p}-D^{\ast}_{p}\right|

$$

其中 D p D_p Dp 是模型在像素 p p p 上的深度预测, D p ∗ D^{\ast}_p Dp∗ 是重建目标,指示函数只保留有效 ground truth 像素。这个损失形式把重点放在绝对度量误差上,和单目深度常见的尺度不变目标不同,因此输出可以被相机内参直接反投影到米制相机空间,并能继续交给跟踪或抓取模块使用。

7.2 论文训练配置不能照搬到单卡

技术报告给出的预训练规模是 24 层 ViT-Large、128 张 GPU、全局 batch size 1024、每卡 batch size 8、250,000 iterations、BF16,完整训练约 7.5 天。当前服务器只有一张 RTX 4090,适合模型推理、下游小规模实验和 Web 服务,不适合复现从零预训练。若要做微调,建议先冻结 encoder、缩小输入 token 或使用梯度累积,并把数据放到额外数据盘,避免 3M RGB-D 数据与系统环境竞争 79GB 根盘。

GPU-hours train ≈ 128 × 24 × 7.5 = 23040 GPU-hours \text{GPU-hours}_{\text{train}} \approx 128\times 24\times 7.5 =23040\ \text{GPU-hours} GPU-hourstrain≈128×24×7.5=23040 GPU-hours

这个粗略换算只表达论文训练配置的量级,其中 24 是每天小时数,7.5 是报告中的训练天数,未把验证、失败重跑和数据准备计入。它不能直接推出单卡需要 23040 小时,因为硬件、通信、吞吐和 batch 形态都会变化,但足以说明"部署一个 checkpoint"和"重新训练一个 ViT-L"是两个预算级别。

难点提示:训练资源像修建一条高速公路,128 张 GPU 是施工队;推理像开一辆已经造好的车,单张 4090 就能上路。把施工队规模套到线上推理,或者把线上显存余量误当成训练余量,都会得到完全错误的预算。

8. 数据与下游应用

8.1 3M RGB-D 数据为什么重要

项目 README 列出的公开数据包括约 140 万条 RobbyReal、580,960 条 RobbyVla、999,264 条 RobbySim 和 38,976 条 RobbySimVal,合计 3,019,200 条 RGB-D 样本;技术报告进一步说明预训练还混合了公开 RGB-D 数据,总训练样本规模约 10M。这里的关键是,模型需要真实传感器自然产生的缺测分布,纯净渲染深度只能提供几何监督,不能完整覆盖反光、透明和弱纹理带来的失败模式。

8.2 从深度细化到跟踪和抓取

技术报告展示了视频深度补全、在线三维点跟踪和灵巧抓取三个方向。视频实验使用 640×480、30 FPS 的采集序列,模型按帧处理但表现出较稳定的空间结构;跟踪系统把细化深度交给 SpatialTrackerV2,抓取策略则消费点云和几何 latent。换句话说,LingBot-Depth 的下游价值不只在于把黑洞填成连续图案,而在于让机器人后面的位姿估计、运动理解和接触规划拿到更可靠的度量坐标。

9. 服务器安装与 Web 使用

9.1 Conda 环境与依赖安装

本次部署没有改动已有 py312 环境,而是创建了独立的 lingbot-depth 环境。服务器使用 Python 3.10.21、PyTorch 2.6.0+cu124、TorchVision 0.21.0、xFormers 0.0.29.post2 和 Gradio 6.26.0;由于无桌面镜像缺少 libGL.so.1,OpenCV 使用 opencv-python-headless。下面的命令表达完整安装路径,若后续换机器,优先确认 NVIDIA 驱动能够支持 CUDA runtime,再执行项目 editable install。

bash 复制代码
# 服务器安装命令
source /usr/local/miniconda3/etc/profile.d/conda.sh
conda create -n lingbot-depth python=3.10 -y
conda activate lingbot-depth
python -m pip install --index-url https://download.pytorch.org/whl/cu124 \
    torch==2.6.0 torchvision==0.21.0
python -m pip install --no-deps xformers==0.0.29.post2
python -m pip install click opencv-python-headless scipy matplotlib \
    trimesh pillow huggingface_hub gradio
cd /root/lingbot-depth
python -m pip install -e . --no-deps
python example.py --example 0 --output result

安装阶段要保留模型自动下载的 Hugging Face 缓存,首次启动会把 model.pt 放到缓存目录,之后服务重启不再重复下载。若服务器无法访问 Hugging Face,可以先在有网络的机器下载 checkpoint,再把本地模型路径传给 --model;但本项目的 from_pretrained() 仍然要求 checkpoint 内含 model_configmodel 字段,不能拿一个普通 DINOv2 权重替换。

9.2 Web 输入与输出

浏览器端需要上传 RGB 图像、16 位 raw depth 和相机内参文件,也可以把 3x3 内参矩阵直接粘贴到文本框。深度 PNG 默认按毫米处理,米制 NPY 则把缩放系数改为 1.0;"快速、平衡、高质量"分别映射到较低、中等和最高 token 预算,最长边默认限制为 1280。输出区域会展示输入深度伪彩、细化深度伪彩、模型有效区域、输入/输出对比、深度统计、运行统计和点云预览,并提供 PLY 下载。

这里的关键是,PLY 是下载格式,GLB 是浏览器 3D 预览格式,两者都来自同一批相机空间点。服务把点数限制为最多 250,000 个,避免高分辨率点云让浏览器页面失去响应;如果用户只关心深度质量,可以不提供内参,模型仍能输出深度和 mask,但不会生成有度量意义的点云。当前公网 7860 端口受外层网络策略限制,服务器本机访问正常,已通过 Jupyter Server Proxy 将其挂载到 8888 的 /proxy/7860/ 路径。

10. 总结

LingBot-Depth 的部署重点不是堆更大的服务器,而是守住深度单位、内参归一化、变长注意力和单并发显存边界。 对实际使用者来说,先用仓库示例验证输入闭环,再逐步提高质量档位和最长边上限,比直接把所有请求交给最高质量配置更容易定位问题,也更符合当前单卡服务的资源边界。

相关推荐
YOLO数据集集合15 小时前
基准标记目标检测数据集 |基准标记 视觉定位 相机标定 目标检测 YOLO格式 深度学习数据集 计算机9038期
数码相机·yolo·目标检测·视觉定位·基准标定
浪里镖客2 天前
RM65B睿尔曼机械臂与realsense d4354i相机眼在手外的手眼标定
数码相机
长江后浪博客2 天前
陶瓷盘UV喷墨印刷中的Z轴高度测量与防碰撞技术方案
数码相机·uv·陶瓷盘uv打印·uv喷墨印刷·z轴测高·喷头防碰撞·激光位移传感器
挽安学长4 天前
浅谈一下,伟财工程水印相机的特点及优势!
数码相机
蓝宝石的傻话5 天前
因为MiBeeNVR,决定接 onvif-go 自己管理并重构
数码相机·重构·golang
格林威5 天前
C# 相机Burst模式图像采集:使用相机内存配合OpenCvSharp和Halcon实现短时间的高速采集的方法
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
长江后浪博客5 天前
黑色滴灌带三角结构视觉检测光源设计方案——200mm高角度环光 + 低角度条形暗场补光
人工智能·数码相机·视觉检测·机器视觉·光源设计·暗场补光·环形光
长江后浪博客6 天前
陶瓷浮雕盘印刷视觉定位方案:8K线扫相机 + 暗场光源 + 旋转平台
人工智能·数码相机·机器视觉·视觉定位·线扫相机·暗场光源·陶瓷印刷
爱分享的康康6 天前
破解座舱数采五大痛点:DMS/OMS多路相机数采工程方案
数码相机·自动驾驶