做 3D Gaussian Splating(以下简称 3DGS)的同学经常会遇到一个需求:模型训练好之后,除了渲染漂亮的 RGB 图像,还希望拿到每个像素的深度值,用于下游的 SLAM、三维重建、场景补全或者前景分割任务。但官方实现默认只输出彩色图,深度信息虽然在光栅化过程中被计算过,却没有被保存下来。这篇文章就来详细聊聊怎么从 3DGS 的渲染结果中提取深度图,包括原理层面的分析和具体代码层面的修改方法。

深度是怎么在渲染管线里产生的
要理解深度提取,得先看 3DGS 的渲染公式。3DGS 把场景表示成一组带颜色的三维高斯椭球,渲染某个像素时,把所有投影到该像素上的高斯按深度从近到远排序,然后依次做 alpha 混合。最终的像素颜色可以写成累加形式:
C = sum_i(c_i * alpha_i * T_i) T_i = prod_j<i (1 - alpha_j)
其中 c_i 是第 i 个高斯的球谐系数计算出的颜色,alpha_i 是这个高斯投影到当前像素后的二维不透明度,T_i 是前面所有高斯的透过率。这个公式大家都很熟悉,而深度提取的核心洞察其实非常简单:既然颜色可以加权累积,深度当然也可以。把公式里的颜色换成分割线到像素的距离,就得到了期望深度:
D = sum_i(d_i * alpha_i * T_i)
这里的 d_i 是第 i 个高斯中心到相机平面的深度值,也就是 view space 下的 z 坐标。渲染完一个像素的所有高斯后,累加得到的 D 就是该像素的深度,最后再除以总不透明度做归一化,保证半透明边缘区域的深度也合理。这个深度通常被称为 expected depth 或者 mean depth,它反映的是该像素视线方向上所有高斯的加权平均位置。
需要特别注意的是,深度提取必须在 CUDA 光栅化内核里完成,而不是事后从 RGB 图反推。因为排序信息和高斯的逐个贡献量只在渲染过程中存在,渲染结束后就丢掉了。这也是为什么几乎所有深度提取方案都要改渲染器代码的原因。
修改官方 CUDA 渲染器输出深度
如果你用的是原版 3DGS 代码,深度修改集中在 submodules/diff-gaussian-rasterization 这个子模块里。核心文件是 render_cuda.cu 和 config.h。第一步是在 config.h 中把渲染通道数从 3 改成 4:
#define NUM_CHANNELS 4 // 原本是3,增加一个深度通道 // 同时在 renderCUDA 内核中: float accumulated_depth = 0; // 每处理一个高斯时: accumulated_depth += (depth_in_view * alpha * con_o.w); contrib += con_o.w; // 像素遍历结束后: out[collected_id] 即深度值 = accumulated_depth / max(contrib, 1e-4);
其中 con_o.w 是 alpha 乘以透过率 T 的合成贡献权重,depth_in_view 可以在投影阶段从 t_view 矩阵变换结果里取 z 分量。由于相机朝向不同坐标系约定可能不同,注意 z 取值方向,必要时取相反数,保证深度为正。
第二步是修改 python 端的 rasterize_gaussians 调用,为输出分配四通道的 tensor,渲染完成后再把前三通道拆成 RGB、第四通道拆成深度。深度图一般会用伪彩色可视化,比如用 matplotlib 的 viridis 或者 turbo 色图,先做 min-max 归一化再映射,视觉效果会比较直观。修改完成后重新编译子模块:
cd submodules/diff-gaussian-rasterization pip install .
这个方案的好处是你完全掌控渲染逻辑,可以在累积深度的同时把 final_T、中值深度、最小深度等辅助信息一并输出,灵活性最高。缺点是每次改代码都要重新编译,而且和官方仓库的更新会产生冲突,维护成本略高。
使用 gsplat 等第三方库的现成支持
如果不想自己动 CUDA 代码,可以借助 nerfstudio 团队维护的 gsplat 库。这个库对深度渲染做了原生支持,调用 rasterization 函数时直接提供了几个相关参数:render_mode 可以设为 RGB+D 模式,一次渲染同时输出颜色和深度;另外还支持 depth_mode 参数,可选 expected 模式和 median 模式。典型的调用方式如下:
from gsplat import rasterization
render_colors, render_alphas, meta = rasterization(
means=gaussians.means,
quats=gaussians.quats,
scales=gaussians.scales,
opacities=gaussians.opacities,
colors=sh_degree_0_colors,
viewmats=view_matrix,
Ks=intrinsics,
width=W, height=H,
render_mode="RGB+D", # 输出四通道,前3为RGB,第4为深度
)gsplat 的安装也简单,pip install gsplat 即可,它会自动编译对应 CUDA 架构的内核。如果你在训练阶段就需要深度监督,比如结合单目深度估计模型做正则化,gsplat 的可微分深度输出可以直接接进 loss,训练和推理阶段都不需要额外的代码改动。
除了 gsplat,还有一些社区维护的 3DGS 深度变体仓库,比如支持 COLMAP 深度监督的 Depth-Regularized GS 项目,它在训练时用单目深度网络估计的深度图约束渲染深度,能明显改善弱纹理区域的几何质量。如果你提取深度的目的就是为了让重建更准,可以参考这类项目的训练策略,而不只是事后提取。
期望深度与中值深度的差异及常见问题
同样是深度图,expected depth 和 median depth 的含义并不一样。期望深度是所有高斯贡献的加权平均,在半透明区域会被前景和背景拉扯,出现夹在中间的过渡值;中值深度则是累积不透明度首次超过 0.5 时对应的那个高斯的深度,更接近深度图的传统定义,边缘更锐利。做表面重建或和 COLMAP 稀疏深度对齐时,中值深度通常更稳;做场景融合或视角插值时,期望深度更平滑自然。gsplat 两种都支持,自己改 CUDA 的话建议两个都输出,按需选用。
实际使用中还有几个常见坑值得提醒。一是坐标系问题,3DGS 默认在 world space 训练,深度提取必须在 view space 做,忘乘 view matrix 得到的深度图会完全错乱;二是背景区域没有高斯覆盖时,深度值可能为零或 NaN,需要用 alpha 通道做 mask,把无效像素过滤掉再用于下游任务;三是浮动伪影会污染深度图,训练不充分或白点噪声没清理时,深度图上会出现贴着相机的异常小值,建议训练完成后先用 3DGS-MCMC 或者简单的密度阈值清理一遍高斯再提深度。
最后提一下深度质量的评估。如果有 LiDAR 或者 COLMAP 稀疏点云作为参考,可以在有效像素上计算尺度对齐后的 L1 误差和相对误差;没有参考数据时,也可以做多视角一致性检查:同一区域从不同视角提取的深度反投影回三维空间,理想情况下点云应该重合,偏差大的区域往往就是几何质量差的地方。通过这些检查手段,你就能确认提取出的深度图是否真正可用,而不是只看起来好看。
总结一下,3DGS 的深度提取本质上是把 alpha 混合公式从颜色扩展到深度的过程,要么自己修改 CUDA 渲染器增加一个输出通道,要么直接用 gsplat 这类支持深度渲染的库。理解了期望深度和中值深度的区别,处理好坐标系和无效像素问题,就能在自己的项目里稳定拿到高质量的深度图了。