1. 基础信息
- 标题: 3D Gaussian Splatting for Real-Time Radiance Field Rendering
- 作者与机构: Inria
- 出处与年份: SIGGRAPH 2023
- 项目主页:3D Gaussian Splatting for Real-Time Radiance Field Rendering
- 主要参考:
- 三维高斯泼溅的实践与原理 | 唐玥璨
- bilibili up主 SY_007系列视频 【较真系列】讲人话-3d gaussian splatting全解(原理+代码+公式)【1】 捏雪球_哔哩哔哩_bilibili
2. 问题定义与动机
核心问题:
1.NeRF方法想要获得高视觉质量,需要训练和渲染成本高昂的神经网络;想要速度更快,就需要牺牲质量。
2.目前还没有方法能够实现无界场景的完整重建和1080P分辨率的渲染研究背景与重要性:
现有方法及不足:
方法一: [Plenoxels: Radiance Fields without Neural Networks]
方法二: [Point-nerf: Point-based neural radiance fields]
这些方法都是通过对存储在离散表示上的值进行插值(比如point-nerf用距离权重插值 feature,得到ray上采样点的特征)不足:渲染所需的随机采样成本很高,并且可能会导致噪声
我理解是NeRF本身的这种采样策略就不够快,一种天然的劣势
3. 方法与创新点
核心思想(来自摘要):
1️⃣ 3DGS 用连续的各向异性 3D 高斯作为显式几何原语表示场景,从稀疏点初始化,避免了传统体渲染在空空间中的大量无效计算。
2️⃣ 通过对高斯的位置、透明度和各向异性协方差进行交错优化与自适应增密/裁剪,场景几何和外观可以高效、稳定地逐步精化。
3️⃣ 基于可见性感知的光栅化与 α-blending,而非逐射线积分,3DGS 实现了高质量、可微渲染,从而在 1080p 分辨率下实现实时新视角合成。
⭐三句话,讲清楚3dgs,但是模块随之也引出了一些问题
先拿语言介绍一下3DGS的流程
给定一组输入图像,利用SfM方法获得相机位姿信息,同时获得了副产物稀疏点云。将稀疏点云进行初始化,获得3D Gaussians(这个3D Gaussian包含三类参数:
- 几何参数:均值μ \muμ(位置)和 协方差∑ \sum∑(形状、尺度、各向异性)
- 外观参数:SH颜色系数(3阶SH函数,能够较为丰富地描述颜色随视角变化的辐射特性,反映一定的光场变换)
- 不透明度:α \alphaα
(其实第一次初始化得到的并不是椭球,而是各向同性的球,球的半径是通过与该点最近的3个点的距离的均值来计算出来的,周围的点越密集,这个均值就越小,球的半径就越小,生成的高斯椭球就会越密集,反之更稀疏)。
此时的3d gs(Gaussians的缩写,下同)是在世界坐标系下的,并不在相机坐标系中。
世界坐标 → 相机坐标(SE(3),线性)
相机坐标 → 像素坐标(透视投影,非线性)
通过旋转和平移能够将其移动到相机坐标系中,但是椭球在光锥投影到平面是非线性的,所以要先将光锥压扁成立方体大小,这就是所谓的透视投影。
投影变换的过程是一个非线性的过程,均值(中心)是一个点,非线性变换并不会造成扭曲,但是协方差在透视投影下不能被精确地线性传播,要想办法把这个过程转换成线性过程。用到的方法就是雅各比矩阵J在局部进行一阶近似。用J来表示近似仿射变换矩阵。
∑ 2 D ≈ J ∑ 3 D J T \sum _ { 2 D } \approx J \sum _ { 3 D } J ^ { T }2D∑≈J3D∑JT
协方差必须满足半正定的性质,在优化过程中可能出现无意义的协方差,所以不能直接优化协方差。因为高斯椭球可以看作是从一个圆,经过缩放s和旋转R获得的,那么就可以把这个问题转换为优化圆的R和S,来表示椭球。论文中提到,R是一个四元数,S是表示缩放的3D向量。
Σ = R S S T R T \Sigma = R S S ^ { T } R ^ { T }Σ=RSSTRT
紧接着就是光栅化(3D->2D)。
- 将屏幕划分为16×16 的 tile
- 对每个 tile:
- 根据 tile 对应的视锥
- 剔除不可见的 Gaussians
- 只保留 99% 置信区间与视锥相交的 Gaussians
- 额外剔除:
- 非常靠近 near plane 的 Gaussians(Jacobian 不稳定)
- 极远处的 Gaussians(对图像无贡献)
为了获得更快的渲染速度,算法将图像分成16*16的tile(我把这个翻译成“块”),每个块中不止一个Pixel。根据相机和块形成的视锥,来选择3dgs,(因为有些gs并不存在于某些像素中,需要剔除),有些gs同时存在多个视锥中(可能是在同一个相机的多个块中,也可能是在不同相机的视锥中),因此作者有一个判断标准,如果99%,就认为该gs在当前视锥中。
Specifically, we only keep Gaussians with a 99% confidence interval intersecting the view frustum.
原文是这样说的
同时,必须拒绝旋转那些离near plane很近或者在视锥很远的gs,为什么呢?因为靠近near plane,z就会很小,在雅各比中存在z/1的项,这样就会造成某一项趋于无穷,不利于计算;在视锥很远的gs实际上可以认为是离群点,对渲染没啥作用。
排序、分块、并行渲染
- 由于一个 Gaussian 可能同时影响多个 tile:
- 作者为每个 tile实例化 Gaussian 引用
(不是复制参数,而是复制索引)- 然后:
- 按 Gaussian 在相机坐标系下的深度 z排序
- 得到一段连续的 GPU 内存
- 为每个 tile 记录一个
[start, end)区间- 渲染时:
- 每个 tile → 一个 GPU block
- block 中的每个 thread → 一个 pixel
- 对 tile 中的 Gaussian:
- 从近到远做 α-blending
- 当透明度累积到 1 时提前终止
做完这些之后,我就得到了256个块,并且每个块有和它对应的能看见的gs,其中必然是有重复的,比如g1既在t i l e 1 , 5 tile_{1,5}tile1,5中,也在t i l e 2 , 8 tile_{2,8}tile2,8中,所以作者将gs实例化,如果出现重复的gs,就生成多个引用实例(说白了就是复制?)那现在每个块都有了属于自己的gs,就按照深度z对所有的gs由近到远进行排序,这个z其实是经过变换的μ \muμ。
这个排序的过程是在GPU上进行的,所以排序结束之后得到了一段连续的内存,存储了一个相机对应所有可见的gs的深度排序,每个块包含的gs不同,并不是每个块都能用得上所有的gs(即整个内存),所以,作者又创建了一个list,存储每个块,用到的gs的起点和重点[start,end),其实就是某一段连续的内存,相当于告诉GPU的线程,你要从哪里开始,到哪里结束,GPU只管吞吐就完事儿了。所以接下来就是将每一个块放到GPU的block上,每个block有多个thread,每个线程就针对一个像素做渲染,直到[start,end)结束,或者透明度达到1,这个线程就停止工作。所有所有所有的线程,就好像并行运算的工人,一起完成图像的渲染。渲染的结果和GT做比较,损失函数是L 1 \mathcal{L}_{1}L1和L D − S S I M \mathcal{L}_{D-SSIM}LD−SSIM,反向传播回高斯的透明度α \alphaα,实现优化
L = ( 1 − λ ) L 1 + λ L D − S S I M L 1 = 1 N ∑ x ∣ I r e r a d e r ( x ) − I g t ( x ) ∣ L D − S S I M = 1 − S S I M 2 \mathcal{L}= ( 1 - \lambda ) \mathcal{L} _ { 1 } + \lambda \mathcal{L} _ { D - S S I M } \\ \mathcal{L} _ { 1 } = \frac { 1 } { N } \sum _ { x } | I _ { r e r a d e r } ( x ) - I _ { g t } ( x ) | \\ \mathcal{L}_{D-SSIM} = \frac { 1 - S S I M } { 2 }L=(1−λ)L1+λLD−SSIML1=N1x∑∣Irerader(x)−Igt(x)∣LD−SSIM=21−SSIM
梯度会反向传播到:
- Gaussian 的位置 μ
- 协方差参数 R, S
- 不透明度 α
- SH 颜色系数
虽然损失函数没有显式监督 RS,但由于 RS 决定了 splat 的形状和覆盖范围,它们会通过颜色误差被间接优化。
其实到这里就介绍完了3dgs的训练,但是发现了一个问题,如果初始点不准确怎么办,如果gs优化过程中不准确怎么办,它们还会参与到排序、渲染部分,先不说大量的冗余计算,不准确的gs一定会影响结果的精度。为解决这个问题,作者参考了point-nerf的剪枝和生长,对高斯进行了自适应控制。
这里说的用论文的话讲是:3D到2D是不适定的,高斯可能会放错位置,所以需要定期新增可靠的高斯,删除/透明/缩小/挪走不可靠/不重要的高斯
✔创建几何
- 新增 Gaussian(densification)
- 细节不够就多加点
✔销毁 / 修正几何
- 不重要的高斯 → 透明掉 / 缩小
- 放错位置的高斯 → 挪走
- 被遮挡、没用的 → “自然死亡”
首先每100次迭代,进行一次“grow and purn”(类比point-nerf)
α < ϵ α \alpha <\epsilon_\alphaα<ϵα
这一步是删去本质上是透明的高斯
此外,需要填补空白区域,主要聚焦于两类情况:重建不足和过度重建,这两种情况都会导致“视图空间位置梯度升高”
m e a n ∣ ∣ ∂ L ∂ p v i e w ∣ ∣ > ϵ p o s e mean||\frac { \partial \mathcal{L} } { \partial p _ { view } } ||>\epsilon_{pose}mean∣∣∂pview∂L∣∣>ϵpose
如果我在相机坐标系里轻微移动这个高斯中心,图像损失会怎么变化
- 小 → “你动不动它都无所谓”
- 大 → “你现在的位置很不对!”
情况一:重建不足
解释:本该有表面的地方没有高斯,梯度会把附近的高斯拉过来,使其像正确的位置移动,所以视图空间梯度会升高
解决:克隆一个高斯出来,用两个高斯扭曲填满几何区域
情况二:过度重建
解释:一个高斯覆盖了很大的区域,没有办法描述该区域的细节(光照、颜色变化),在多个视角下相互拉扯,都想让这个高斯满足几何描述,但是朝一个方向移动之后会破坏另一个方向的梯度
解决:拆分,把原来的大高斯拆分成小高斯。
具体怎么做?(其实只要知道两个值就好了,一个是大小,一个是位置)
大小:原本的高斯半径/1.6 这个1.6是根据实验得来的,算是经验吧(初始化是球的时候可以/1.6,迭代之后已经变成椭球了,怎么/1.6,难道是缩放尺度s的处理吗?)
是的,处理的是缩放的S,S在哪里用得到?是在∑ R S S T R T \sum RSS^TR^T∑RSSTRT这里用到了S)
位置:位置是在现有的高斯上根据PDF进行采样,PDF采样的好处是中心落在密集的地方的概率更大
但是这种自适应调整也会出现问题,在靠近摄像机的区域,优化效果可能变弱,就会导致高斯数量的急剧增加。为什么会有这种情况,不是已经通过视角梯度约束了高斯了吗?(原文:our optimization can get stuck with floaters close to the input cameras;in our case this may result in an unjustified increase in the Gaussian density)
为什么靠近相机的区域会stuck呢?
越靠近相机(near plane),z的值就越小,极小的3D位移可能会导致很大的像素变化,所以越靠近相机,像素误差越敏感,进而影响“视图空间位置梯度”,而且靠近相机的高斯多视角一致性约束比较弱
针对这个问题,作者提出了一个方法:每隔N=3000代,将α \alphaα的值设置为接近0,设置完之后,处在正确位置的高斯:协方差正确、位置正确、颜色正确,一旦透明度不正确,损失函数会立马增大,所以会立马修正透明度,而位置不正确的高斯则被慢慢透明化。这样实现了高斯数量的控制
4. 实验与验证
- 数据集:
- 名称: synthetic Blender dataset、Tanks&Temples
- 评价指标: SSIM PSNR LPIP
- 实验结果:
- 主实验(与SOTA对比): SOTA方法是Mip-NeRF 360,GS的速度更快;与NGP方法比质量
- 消融实验(证明各模块有效性): No SH、No Clone、No Split,这是在验证剪枝与生长、球谐函数的作用
- 定性结果(可视化效果): 见文章