文章

三维高斯泼溅的实践与原理

三维高斯泼溅的实践与原理

前言

最近几天时间,我研究了一下近年大火的三维高斯泼溅(3D Gaussian Splatting),实际了解了一下它的数据原理、实用价值、优势与缺点,最后还将整个三维重建、渲染的流程使用目前还算不错并且主流的开源项目去测试实现了一下,最终的效果确实很惊艳,所以特来写一篇文章。至于这个具体的实现流程、包括用的哪些工具、执行步骤这些虽然我也会讲,但是它们实际上并不是特别重要,真正重要的是其原理,我们要知其所以然才能够彻底的理解这一项技术。

下面是我制作的一个Demo,随随便便拍的视频都可以做到这个效果。这可不是图片,是一个可以交互的三维场景,各位可以拖拽试试(如果你网络不好可能得多加载一会儿😅):

操作说明:

  • 鼠标:左键旋转、右键平移、滚轮缩放;
  • 触屏:单指旋转、双指平移/缩放;

原理:三维高斯是什么?

要讲原理我们必须明白什么是三维高斯,既然有三维高斯,那么肯定对应也有一维高斯、二维高斯,这里的高斯指的其实是每一个大学生都学习过的正态分布(也叫高斯分布)。是的没错三维的正态分布得到的就是一个椭球,这个椭球我们一般叫做高斯椭球。不同的高斯椭球本质上代表其实就是一个有限空间内某种点数据的分布情况,相信看到这里有的朋友已经反应过来了,是的你想的没有错,三维点云数据在局部空间的特征便可以形成一个三维高斯椭球。

如果你理解了什么是三维高斯之后自然而然你肯定明白这么做会带来一个好处,假设我们现在有十亿个点的点云,经过某种方式将其中部分有相同特征的点分布情况整合为一个高斯椭球,这么一来最终得到的高斯椭球数量肯定相较十亿数量便会大大降低,毕竟它表示的是点的集合,所以也就是意味着我们将原始的点云数据通过高斯分布的方式进行了压缩,最终的存储空间占用与渲染计算量就会极大的降低。

原理:深入三维高斯

我们现在知道了什么是三维高斯,也知道了它代表的是局部点云的特征,自然会出现几个问题:

  1. 高斯椭球哪来的凭什么可以代表这些点云数据?
  2. 这个代表本质上是一种简化,这个简化的代价是什么?
  3. 整个点云数据需要划分不同的局部点云,然后计算这个局部的高斯椭球,那么这个划分的依据是什么?

下面我们就非常硬核的来解释一下这些问题,事先声明,我个人的数学能力其实只能说是一般水平所以相较于大佬,我写的东西会更加啰嗦一点,高手见谅。

高斯椭球如何计算为什么可以代表点云?

这里首先需要明确的一个东西是我们如何在数学上表示这个高斯椭球,它具体是包含哪些数据,这些数据是什么东西,如何计算。从最终的渲染角度来看,高斯椭球至少包含中心点坐标、协方差矩阵和球谐函数,下面我们分布介绍一下这三个东西是什么。

1. 中心点坐标(均值)\({\mu}\)

我们记录这个局部点云的所有点记为集合 \(\{\mathbf{x}_i\}\) 其中 \(\mathbf{x}_i\)为每一个具体的点,最终我们计算的均值记为 \({\mu}\) 便有如下公式:

\[{\mu} = \frac{1}{N}\sum_{i=1}^N \mathbf{x}_i\]

所以均值这个数据就非常的好理解,相当于找到高斯椭球的中心,也就是对应的点平均值,它可以代表这个集合的中心点。

2. 协方差矩阵 \(\Sigma\)

最终我们将一个集合内部的所有点压缩为同一个高斯椭球,那么我们必须有一个方法来记录这些点的大致分布情况,这里使用到的数据工具叫做协方差矩阵,我们记作 \(\Sigma\),可以得到公式如下:

\[\Sigma = \frac{1}{N}\sum_{i=1}^N (\mathbf{x}_i - {\mu})(\mathbf{x}_i - {\mu})^\top + \epsilon I\]

我们具体来拆解一下这个公式:

  1. 偏移向量 \(\mathbf{x}_i - {\mu}\)

    • 表示第 \(i\) 个点相对于群体中心的“偏离”。
  2. 外积 \((\mathbf{x}_i - {\mu})(\mathbf{x}_i - {\mu})^\top\)

    • 这是一个 \(3\times3\) 矩阵:

      \[\begin{bmatrix} (x_i - \mu_x)^2 & (x_i - \mu_x)(y_i - \mu_y) & (x_i - \mu_x)(z_i - \mu_z) \\ (y_i - \mu_y)(x_i - \mu_x) & (y_i - \mu_y)^2 & (y_i - \mu_y)(z_i - \mu_z) \\ (z_i - \mu_z)(x_i - \mu_x) & (z_i - \mu_z)(y_i - \mu_y) & (z_i - \mu_z)^2 \end{bmatrix}\]
    • 对角项:反映点在 \(x\)、\(y\)、\(z\) 方向的伸展程度。
    • 非对角项:反映不同轴向偏移的关联程度——正值说明同向变化,负值说明反向变化。
  3. 求平均 \(\tfrac{1}{N}\sum\)

    • 把所有点的外积矩阵求和并取平均,即得到整个局部点云的总体散布矩阵。
    • 这样可以去掉样本数量的影响,使得不同点数的簇具有可比的分布度量。
  4. 正则化项 \(\epsilon I\)

    • 在实际计算中,所有点共面等情况导致某个方向方差为零,所以加入很小的 \(\epsilon\)(如 \(10^{-6}\))乘以单位矩阵保证协方差矩阵有效。

有的朋友肯定会问了,这个协方差矩阵凭什么能代表点云分布?由于我们的主要目标最终是在渲染上能够一定程度保持与原始点云的一致性,所以我们关注的重点也应该是渲染相关,由于协方差矩阵本身就是正态分布的核心参数,本质上它实际在描述相关点云在各方向的集中或者离散的特性。

  • 形状与方向:对角元素表示了伸展程度,所以就能够形成一个按这三条主轴方向拉伸或压缩的椭球。
  • 分布概率:由于高斯就是正态分布,所以点在中心位置的概率肯定是最高的,着色上就可以中心边缘使用不同的透明度表示密集程度。

于是我们在最终渲染的时候就可以用椭球表示局部点云,既能保持整体形状与方向特性,又能在渲染时进行解析的光线积分和模糊处理,比离散点更平滑、连续。如果你理解了这一层再回头看这个矩阵是不是觉得太合理了。

3. 球谐函数

我们知道了高斯椭球的位置、形状与点分布情况,接下来进一步渲染肯定还差一个东西对吧?没错那就是颜色,而且很显然这个颜色不可能是一个高斯椭球同一个颜色,我们需要做到不同角度观察到的颜色应该有区别,只有这样对应高斯椭球渲染才能提供足够的拟真度。各位有没有想起来什么?这不就和环境光照的实现效果一模一样吗?没错这个信息就是通过球谐函数进行记录的,一切都串起来了!

还是相对详细的说一下,球谐函数 \(Y_l^m(\mathbf{v})\) 是定义在单位球面上的一组正交基函数,可以用来逼近从任意方向 \(\mathbf{v}\) 入射的颜色、光照等方向性函数。每个高斯椭球存储一组球谐系数\(\{c_{l,m}\}\),通常是每个 RGB 通道各一组,一般我们使用二阶球谐所以其实一共就九个系数。最终在渲染时我们只需要计算当前观察方向 \(\mathbf{v}\),用它去执行该高斯的球谐函数:

\[\text{Color}_\text{view} = \sum_{l=0}^{L}\sum_{m=-l}^{l} c_{l,m} Y_l^m(\mathbf{v})\]

然后我们就得到了该高斯椭球在该观察方向上的颜色,这个模式计算的成本实际上很低,最NB的是这样一来就可以实现一些视觉效果,比如说向一个方向更亮、向另一个方向更暗或者模拟带高光或阴影效果的反射。这也就是为什么三维高斯椭球渲染对那种透明材质的表现力比网格高了不止一个级别。

综上:从渲染角度我们已经完整论证了使用高斯椭球代表一个局部点云集合的渲染表示是极其合理的且有效的。

高斯椭球代表点云,这种简化的代价是什么?

通过上面的数据推论之后,我相信大家都或多或少都明白了,我把我能够想到的几个点大概说一下:

  1. 细节丢失:整合点云数据到高斯椭球当然丧失了一定程度的细节;
  2. 存储冗余:点云抽稀实际上也是丧失了细节,但是高斯椭球同时还存储了协方差矩阵和球谐函数,自然存储了部分冗余数据;
  3. 渲染复杂度:点云的渲染在着色器里面直接给片源输出即可,没有什么计算,但是高斯椭球渲染需要计算协方差矩阵和球谐函数,渲染计算量加大了;

上述这些代价固然是存在的,但是相较于最终渲染效果与性能表现而言,讲道理,这个代价我们完全可以承受。相对于最终结果,我甚至会说这个代价太少了完全可以忽略不计😎😎😎。

点云数据的分类划分依据

我们现在已经知道了高斯椭球如何计算,最后一个需要回答的问题也就呼之欲出了:我们如何对已有的整体点云进行分类?因为我们一旦有了局部点云后直接计算他们的高斯椭球即可,这个分类的算法实际上完全决定了最后的渲染效果。嘿嘿,你猜的没错,这个算法其实就是三维高斯泼溅!最终在实践上我们可以用的分类算法就非常多了,不过当前技术领域的最佳实践是依靠一种参数优化算法实现的,它并且也不是先分类再计算高斯椭球,我使用分类这个词仅仅是为了方便理解,实际上执行的方案叫做可微分渲染,它并不属于传统的分类算法,执行步骤如下:

  1. 点云计算:利用 COLMAP、Open3D 等 SfM/多视几何库或深度相机,生成高质量原始点云;
  2. 高斯椭球初始化:通过常规算法对点云做一次聚类,然后用这个聚类在场景中初始化一组高斯椭球参数\(\{\mu_i,\Sigma_i,\alpha_i,c_i\}\)计算高斯椭球;
  3. 可微分渲染:将这些高斯椭球投影到像平面,渲染计算每个像素的合成颜色;
  4. 反向传播与参数优化:用渲染结果与真实多视角照片计算像素级损失,通过梯度下降算法同时更新所有\(\{\mu_i,\Sigma_i,\alpha_i,c_i\}\)参数;
  5. 稀疏化剪枝:设定某种阈值方法,让不重要的高斯椭球自动衰减至零并剔除,得到最终紧凑的高斯椭球集合;

这里的核心是可微分渲染以及自适应的参数优化与调整。整个过程有点类似神经网络的训练,通过不断迭代使得三维高斯集合能够越来越好地重建输入图像。我知道单这么讲可能大家会觉得非常神奇,这是如何做到的?我其实刚开始也是这样完全想不到是如何实现可微分渲染,直到我看了数学表示后也仅仅是理解每一个步骤具体要怎么做,至于说到底最早采用这个方法的人是怎么想到的这个方法,不好意思,我完全无法想象😜😜😜😜。

原理:三维高斯泼溅的实现

上文已经将基础理解全部阐述的差不多了,这里便是本文最硬核的部分,如果仅仅是对三维高斯泼溅达到认知上的理解其实不需要看这段,如果你想刨根到底这里就是你需要重点理解的部分,下面我们就开始。首先点云计算这个步骤相对比较常规也不是本文重点,我就偷懒略过了,我们就直接从高斯椭球初始化开始。

高斯椭球初始化

这一步是先要计算出\(\{\mu_i,\Sigma_i,\alpha_i,c_i\}\)

  • 位置(\(\mu_i\)):先对点云做一次快速的 K‑means、均值漂移或体素网格滤波等聚类,把每个簇的质心当作 \(\mu_i\);
  • 协方差矩阵(\(\Sigma_i\)):在每个聚类簇里计算点云的样本协方差,用它来初始化\(\Sigma_i\),令高斯椭球初始形状贴近该簇的局部几何;
  • 不透明度权重(\(\alpha_i\)):这个参数实际上没有特别大的意义一般都设为一个小的常数,这样每一个高斯椭球都会参与最终像素颜色的渲染,如果较大的话则相互遮挡不利于后续收敛;
  • 颜色系数(\(\mathbf{c}_i\)):可以设置为这个区域的平均颜色,但是实际我们在渲染上一般使用球谐函数计算颜色;

可微分渲染

三维高斯椭球的渲染实际上就是三维→二维高斯投影,这个投影就像把高斯椭球泼溅到二维平面一样,所以算法名称叫做高斯泼溅。对于三维高斯椭球\(\{\mu_i,\Sigma_i,\alpha_i,c_i\}\),我们首先将其投影到像平面:

\[\Sigma_i' = J_W\,\Sigma_i\,J_W^\top\]

其中\(J_W\)是从世界坐标到像平面的仿射近似雅可比。去掉投影协方差矩阵的第三行三列,即得\(2×2\)协方差 \(\Sigma_{i,2D}\),用于描述在像平面上的椭圆形泼溅。

然后我们需要将不同的投影得到的像素进行合成,每个像素的颜色\(C\)由所有重叠的高斯泼溅按深度排序后做Alpha混合得到:

\[C = \sum_{i\in \mathcal S} T_i\,\alpha_i\,c_i,\quad T_i = \prod_{j<i}(1-\alpha_j), \quad \alpha_i = 1 - \exp(-\sigma_i\,\delta_i),\]

其中\(\sigma_i\)与高斯峰值相关,\(\delta_i\)为相邻样本间距。由于我们显式存储了\(\alpha_i\),可直接执行无噪声的可微分Alpha混合。

实际上这里还有一个优化的套路,可以加速计算可微分渲染管线:

  1. 裁剪与分块:将屏幕划分为 \(16\times16\) 小块(tile),对每块提前剔除视野外或足够透明的高斯。
  2. 快速排序:对每个 tile 内的泼溅按深度做 GPU Radix Sort,一次性完成全局排序。
  3. 前向渲染:每个 tile 并行遍历排序后列表,累积 \(T_i\) 与 \(C\),遇到 \(T_i\simeq0\)(即像素饱和)即停止。

该流程既保留了精确的可微分性质,又大幅减少了 per-pixel 排序开销,实现实时渲染,只能说实在是太聪明了。

反向传播与参数优化

首先是需要根据原始图片对比我们渲染结果确定一个损失度:

\[\mathcal L = (1-\lambda)\,\|C_{\rm 渲染图}-C_{\rm 原始图}\|_1 \;+\;\lambda\,\mathcal L_{\rm D\text{-}SSIM},\quad \lambda=0.2\]

这里面的\(L_{\rm D\text{-}SSIM}\)是结构相似度(SSIM)的平均值,对于渲染图\(x=C_{\rm 渲染图}\)和真实图\(y=C_{\rm 原始图}\),在每个像素点(或小窗口)处先计算局部统计量:

\[\begin{aligned} \mu_x &= G * x, &\mu_y &= G * y,\\ \sigma_x^2 &= G * x^2 - \mu_x^2, &\sigma_y^2 &= G * y^2 - \mu_y^2,\\ \sigma_{xy} &= G * (x\,y) - \mu_x\,\mu_y, \end{aligned}\]

其中 \(G\) 是高斯平滑核(卷积),“\(*\)” 表示卷积运算。然后定义每个位置的 SSIM:

\[\mathrm{SSIM}(x,y) = \frac{(2\mu_x\mu_y + C_1)\,(2\sigma_{xy} + C_2)} {(\mu_x^2 + \mu_y^2 + C_1)\,(\sigma_x^2 + \sigma_y^2 + C_2)},\]

常数 \(C_1,C_2\) 用于数值稳定(如 \(C_1=(k_1 L)^2,\,C_2=(k_2 L)^2\),\(L\) 为像素动态范围,典型取 \(k_1=0.01,k_2=0.03\))。

最后把所有像素(或窗口)上的 SSIM 取平均,并用它来构造可微分损失:

\[\mathcal L_{\rm D\text{-}SSIM} = \frac{1}{N}\sum_{p}\bigl[\,1 - \mathrm{SSIM}_p(x,y)\bigr],\]

其中\(N\)是像素(或窗口)总数,\(\mathrm{SSIM}_p\)表示在位置\(p\)处的局部SSIM值。这样\(\mathcal L_{\rm D\text{-}SSIM}\)就完全由当前渲染结果和真实图像通过一系列可微分操作得到,每次迭代都会重新评估并参与梯度计算。

然后我们就可以基于得到的损失度来更新参数了,步骤如下:

  • 位置\(\mu_i\)与颜色系数\(c_i\):直接对梯度做SGD或Adam更新;
  • 协方差缩放\(s_i\):对数空间优化,保证正定后转换\(\Sigma_i=R_i\,{\rm diag}(s_i^2)\,R_i^\top\);
  • 不透明度\(\alpha_i\):sigmoid激活约束\([0,1)\),平滑梯度;
  • 球谐系数:分阶段引入频带,先只优化零阶,再逐步开放至2阶或3阶(实际上2阶已经足够了),以防欠观测区域发散。

PS:这里也有一个优化的套路,我们可以前500次迭代在低分辨率(原图四分之一)下训练,分别在第250、500次后提升到二分之一与原始分辨率,这样就加速收敛并抑制早期噪声。

稀疏化与自适应剪枝

  1. 透明度阈值剔除:每\(N=3000\)步,将所有\(\alpha_i<\epsilon_\alpha\)(如\(10^{-3}\))的高斯椭球置为“瘦身”并移除,保持模型紧凑。

  2. 梯度驱动的密度控制:计算每个高斯在视域空间位置梯度\(\|\nabla_{\mu_i}\mathcal L\|\),会有两种情况
    • 若梯度过大(阈值\(\tau_{\rm pos}\approx2\times10^{-4}\)),且当前体规模小,则克隆:复制并沿梯度方向轻微偏移,增加表示能力;
    • 若梯度过大且高斯椭球积大,则分裂:按比例\(\phi=1.6\)缩小、随机偏移,精细化表达,保持总体体积。
  3. 周期性体积与透明度调节
    • 对过大或投影占比过高的高斯,周期性施以衰减操作,避免“跳变”或“漂浮”;
    • 同步更新\(\alpha_i\)和缩放因子,使重要高斯逐渐增强、不重要者自动衰减为0。

通过上述剪枝与重分配,高斯集在表示精度与体量之间获得动态平衡,最终得到一组紧凑且有效的三维高斯椭球。

小结

至此实际上你已经掌握了整个算法的核心逻辑与数学实现,后续在程序的上的实现无法也仅仅是将这个算法实现,这个过程不仅仅是上述数学逻辑,里面还会涉及到一些优化的

实践:三维高斯泼溅重建模型

我们现在已经对整个三维高斯泼溅的逻辑、数学实现及其背后的原理有了完整的认知,接下来剩余的事情就很简单了,相反也不怎么重要了,我们只需要找到目前已经实现上述功能的开源项目将三维重建 + 渲染整个过程都实现一次就行了。置于我对开源项目的选项依据其实主要是看社区热度,很明显只要热度越高对应的项目的实现肯定也更加好。

首先第一步是获取原始的素材,我随手用手机拍了一下家里面的餐桌的视频,然后通过ffmpeg将这个视频转为序列帧图片,由于不需要每一帧的图片我是隔几帧取一张:

然后就是通过图片数据去重建计算点云数据,我用的是COLMAP进行重建,并且在使用上我几乎没有做任何参数上的优化,直接通过随手拍的视频去重建点云,整体结果效果还是可以接受:

然后就是通过点云去生产传说中的高斯椭球的,我这里选择的项目是OpenSplat,同样我没有使用任何参数去优化结果,直接讲点云与图片扔进去就行了,这里建议各位使用docker版本,社区有很多版本我用的这个:ripleyaffect/runpod-opensplat

最后就是如何渲染了,通过上述原理我们可以明确的知道,虽然三维高斯椭球不再是mesh的渲染管线了,并且和现代的mesh完全不相干,所以必须要自行实现渲染计算。但是无论怎么搞最后还是脱离不了图形API对吧,所以在web端你还得是靠webgl或者webgpu,最后也没有什么变革,无非就是实现了一个特殊的着色器而已。同样的,我随便找了一个开源项目:GaussianSplats3D,最终实现的渲染效果就是我文章一开始那个DEMO😁😁😁。

总结

三维高斯泼溅确实算的上对传统三维模型与重建算法的一次冲击,我们知晓其原理之后可以非常轻易推导出它诸多优点或者说特点,比如说:

  1. 对微小三维结构得表现效果远远好于传统的网格模型;
  2. 对反射透明效果表现也远远好于传统网格,特别是重建场景下,传统网格根本无法保持这些信息;
  3. 由于高斯椭球记录了球谐函数,基于它们可以更方便地模拟次表面散射、烟雾、雾气等体积光效应,同样,传统网格压根记录不了这些信息;
  4. 完美的LOD(level of detail)方案,我们可以只渲染部分高斯椭球,从而实现从远景到近景的无缝过渡,传统网格则需要单独创建低模;
  5. 对于动态场景,高斯椭球更加是降维打击,因为我们只需要记录高斯椭球的生命周期就可以描述动态场景,极其便利;

但是我们必须承认三维高斯也远远算不上所谓的银弹,不仅仅不能算银弹,从它的生产过程和原理,我们也可以清晰的认识到,绝大部分三维应用实际上都没法应用它。话虽如此,我还是期待该技术的未来,其他不讲,单说假设以后任意边缘设备的性能都可以实现实时的采集、重建、渲染,那么各位可以想象一下会怎么样😉😉😉。

参考文章:3D Gaussian Splatting for Real-Time Radiance Field Rendering(PS:能写出这个论文的绝对是个天才)

本文由 唐玥璨 版权所有