当前位置:   article > 正文

3D Gaussian Splatting 和 NeRF_经辐射场在三维空间渲染对应的物理方程

经辐射场在三维空间渲染对应的物理方程

任务

三维重建,再从不同视角渲染成2D图像。

三维重建技术对比

  • 传统Mesh:在三维空间中的纯离散的显式表达。
  • NeRF (Neural Radiance Fields):基于深度学习的方法。在可微空间内的纯连续的隐式表达。通过使用多层感知机MLP映射3D空间坐标到场景的颜色和密度,从而实现对复杂场景的连续高质量渲染。也可以只通过图像和位姿内参生成新视角的图像,不需要三维重建。
    • 优点:流程简单,使用神经网络直接优化连续的体积场景函数,生成较为逼真的三维模型。

    体积场景函数:一个连续的5D函数,用于描述一个场景中每个点的颜色和密度
    输入:三维空间位置,二维视角方向。
    输出:该位置的体积密度和从该视角看到的颜色。
    函数表示: f ( X , θ , ϕ ) = ( c , σ ) f(X, \theta, \phi) = (c, \sigma) f(X,θ,ϕ)=(c,σ),其中, X = ( x , y , z ) X=(x,y,z) X=(x,y,z)为三维坐标; θ , ϕ \theta,\phi θ,ϕ为观察该点的方向; c = ( r , g , b ) c=(r,g,b) c=(r,g,b)是从给定视角观察该点的颜色; σ \sigma σ 是该点的体积密度,影响光线通过该点的衰减。
    该函数用深度神经网络来近似。

    • 缺点:需要大量计算资源和时间;大模型和复杂光照难以处理;模型质量和视角数量成正比。
  • 3D Gaussian Splatting 是在高斯球内是连续可微,在整个空间中高斯球之间又是离散的。
    • 优点:能达到30 FPS的实时渲染;更逼真更精细,在纹理细节表现更优异;显存需求小。
    • 缺点:在边缘设备上仍有进步空间。

动机

  • 之前的技术在随机采样过程中产生的噪声影响渲染质量,且渲染速度和渲染质量不可兼得。

贡献

  • 使用3D Gaussian作为3D表示
  • 3D Gaussian优化算法:Clone 和 剪枝
  • 快速可微渲染器

专业词汇解释

  • Splatting:传统的3D到2D的光栅化渲染方式。文章中与此有所区别。
  • Radiance Field 辐射场:一种函数表达,用于表达三维空间中光的分布和光强的一个模型,具体点就是,描述从各个方向穿过空间各点的光的量。辐射场用函数 L L L表示,将空间中一个点 ( x , y , z ) (x,y,z) (x,y,z)和一个方向(球坐标表示 ( θ , ϕ ) (\theta, \phi) (θ,ϕ))映射到一个非负的辐射值。可用公式表示:
    L ( x , y , z , θ , ϕ ) = ∑ i G ( x , y , z , μ i , Σ i ) ⋅ c i ( θ , ϕ ) L(x,y,z,\theta,\phi)=\sum_{i}G(x,y,z,\mu_i,\Sigma_{i})\cdot c_i(\theta,\phi) L(x,y,z,θ,ϕ)=iG(x,y,z,μi,Σi)ci(θ,ϕ)

神经辐射场在三维空间渲染的物理方程:
L o ( x , d ) = L e ( x , d ) + ∫ Ω f r ( x , d , ω i ) L i ( x , ω i ) cos ⁡ θ d ω i L_o(x,d)=L_e(x,d)+\int_{\Omega}f_r(x,d,\omega_i)L_i(x,\omega_i)\cos{\theta}d\omega_i Lo(x,d)=Le(x,d)+Ωfr(x,d,ωi)Li(x,ωi)cosθdωi

其中, x x x为当前待分析的三维空间坐标; d d d为光线照射方向; L e ( x , d ) L_e(x,d) Le(x,d)为光源点 x x x d d d方向上的辐射量;后面的部分为该点光源照射到其他表面后,折射(沿着入射方向)在 d d d方向的辐射, f r ( x , d , ω i ) f_r(x,d,\omega_i) fr(x,d,ωi)为散射函数, L i ( x , ω i ) L_i(x,\omega_i) Li(x,ωi)为从 ω i \omega_i ωi方向接收到的辐射, θ \theta θ ω i \omega_i ωi d d d的夹角。

为了好理解:光源点 x x x d d d方向上散射的光强度变化(不完全不散射(0)----完全散射(1)),乘上 ω i \omega_i ωi方向上的辐射值在 d d d方向上的值,从而得到 ω i \omega_i ωi方向对 d d d方向上的贡献的辐射值。

  • 基于点的渲染:基于点的渲染是一种使用而非传统多边形来可视化3D场景的技术。该方法特别适用于渲染复杂、非结构化或稀疏的几何数据。点可以通过添加额外属性,如可学习的神经描述符来进行增强,并且可以高效地进行渲染,但这种方法可能会出现渲染中的空洞或混叠效应等问题。只考虑点的表面属性,不考虑光线在物体内部的传播。
  • 体渲染:使用体素Voxel建模,并考虑光线在物体内部的传播效果,适用于渲染半透明材质。NeRF 采用体渲染。

NeRF 渲染
体渲染物理公式:
C ( r ) = ∫ t n t f T ( t ) σ ( r ( t ) ) c ( r ( t ) , d ) d t C(r)=\int_{t_n}^{t_f}T(t)\sigma(r(t))c(r(t),d)dt C(r)=tntfT(t)σ(r(t))c(r(t),d)dt

其中, t n , t f t_n,t_f tn,tf表示起点和终点; T ( t ) = exp ⁡ ( − ∫ t n t σ ( r ( s ) ) d s ) T(t)=\exp{(-\int_{t_n}^{t}\sigma(r(s))ds)} T(t)=exp(tntσ(r(s))ds)为光线累积量,光线累积量是一个随着光线的路径长度增加,而不断对体素密度积分的量,光线到达地方深度和其大小成反比(越远说明透明度在下降),简单来说即光线到达这里光还剩多少 σ ( t ) \sigma(t) σ(t)为体素密度; c ( t ) c(t) c(t)为颜色。
这个像素的颜色,由这个点的颜色的权重 T ( t ) σ ( r ( t ) ) T(t)\sigma(r(t)) T(t)σ(r(t))控制

NeRF 分层体素渲染
为了平衡计算消耗,需要控制采样起始点,选择合适的起始、终止点,能大幅提高采样效率和质量。

NeRF 采用两个网络同时训练(coarse和fine网络),coarse网络输入的点是对光线均匀采样得到的;根据coarse网络预测的体密度值,对光线的分布进行估计,再根据估计出的分布进行第二次采样;然后把所有采样的点输入到fine网络中进行预测。均匀(粗)采样 & 细采样
分层采样流程:

  • 先使用粗采样得到 N c N_c Nc个点(把射线平均分成N个小区间,每个区间随机采样一个点),采样通过coarse的渲染方程计算,对颜色加权权重 ω i = T i ( 1 − exp ⁡ ( − σ i δ i ) ) \omega_i=T_i(1-\exp{(-\sigma_i\delta_i)}) ωi=Ti(1exp(σiδi))作为对应区间采样的概率:
    C ^ ( r ) = ∑ i = 1 N T i ( 1 − exp ⁡ ( − σ i δ i ) ) c i \hat{C}(r)=\sum_{i=1}^{N}T_i(1-\exp{(-\sigma_i\delta_i)})c_i C^(r)=i=1NTi(1exp(σiδi))ci

    其中, δ i = t i + 1 − t i \delta_i=t_{i+1}-t_{i} δi=ti+1ti ω i = T i ( 1 − exp ⁡ ( − σ i δ i ) ) \omega_i=T_i(1-\exp{(-\sigma_i\delta_i)}) ωi=Ti(1exp(σiδi))为权重,归一化之后成为概率值; T i = exp ⁡ ( − ∑ j = 1 i − 1 σ j δ j ) T_i=\exp{(-\sum_{j=1}^{i-1}\sigma_j\delta_j)} Ti=exp(j=1i1σjδj)
    ω ^ i = ω i ∑ j = 1 N c ω j \hat{\omega}_i=\frac{\omega_i}{\sum_{j=1}^{N_c}\omega_j} ω^i=j=1Ncωjωi
    ω ^ i \hat{\omega}_i ω^i为概率分布采样 N f N_f Nf个点,用 N c + N f N_c+N_f Nc+Nf个点训练精细网络。

  • 3D Gaussian Splatting 通过使用各向异性高斯进行更连贯的场景表达。可以理解为3D Gaussian splatting 是一种改进的点渲染方式。

3D Gaussian Splatting

3D Gaussian 表示

3D Gaussian分布即为椭球,用 中心位置 μ \mu μ、不透明度 α \alpha α、三维协方差矩阵 Σ \Sigma Σ(表示缩放程度)和颜色 c c c 表示。 c c c球谐函数表示,以呈现视角依赖的外观。所有属性都可学习。

球谐函数 Spherical Harmonics:3D空间中的傅里叶级数,可以用来表示定义在球面上的函数。球谐函数原本公式表示为 r = f ( θ , ϕ ) r=f(\theta,\phi) r=f(θ,ϕ) θ \theta θ表示与 z z z轴得夹角,即极角 ϕ \phi ϕ表示其在 x y xy xy平面上的投影与 x x x轴的夹角,即方位角 r r r为半径。
在图形学中,将半径 r r r换为颜色的值,三个颜色通道就要三个球谐函数。
Q:为什么要用球谐函数表示颜色?
A:为了让该点的颜色不是显示单一的颜色,需要满足从不同角度看,该点展现不同的颜色或者不同饱和度。
球谐函数 Y l m Y^m_l Ylm表达式:
Y l m ( θ , ϕ ) = { 2 K l m cos ⁡ ( m ϕ ) P l m ( cos ⁡ θ ) ( m > 0 ) 2 K l m sin ⁡ ( − m ϕ ) P l − m ( cos ⁡ θ ) ( m < 0 ) K l 0 P l 0 ( cos ⁡ θ ) ( m = 0 ) Y^m_l(\theta,\phi)=

{2Klmcos(mϕ)Plm(cosθ)(m>0)2Klmsin(mϕ)Plm(cosθ)(m<0)Kl0Pl0(cosθ)(m=0)
Ylm(θ,ϕ)= 2 Klmcos(mϕ)Plm(cosθ)(m>0)2 Klmsin(mϕ)Plm(cosθ)(m<0)Kl0Pl0(cosθ)(m=0)

3D高斯分布的概率密度函数:
f ( x ∣ μ , Σ ) = 1 ( 2 π ) 3 ∣ Σ ∣ exp ⁡ ( − 1 2 ( ( x ) − μ ) T Σ − 1 ( x − μ ) ) f(\bf{x}|\mu,\Sigma)=\frac{1}{\sqrt{(2\pi)^3}|\Sigma|}\exp(-\frac{1}{2}(\bf(x)-\mu)^T\Sigma^{-1}(\bf{x}-\mu)) f(xμ,Σ)=(2π)3 ∣Σ∣1exp(21((x)μ)TΣ1(xμ))

其中, x \bf{x} x表示三维点坐标。

标准化+模型中心放置原点(方便缩放旋转),放入世界空间时再加上平移,得到了论文中的3D高斯表示:
G ( x ) = e − 1 2 x T Σ − 1 ( x ) G(x)=e^{-\frac{1}{2}x^T\Sigma^{-1}(x)} G(x)=e21xTΣ1(x)

三维协方差计算:
Σ = R S S T R T \Sigma=RSS^TR^T Σ=RSSTRT

其中, S S S是缩放变换,由于缩放变换是沿着坐标轴,所以只需要3D向量 s s s表示缩放变换; R R R是旋转变换,用四元数 q q q表示。

  • 方差:描述单个随机变量变化程度的量。
  • 协方差:描述两个随机变量如何一起变化的量。
  • 协方差矩阵: 3 × 3 3\times 3 3×3的对称矩阵,用于表示变量之间的线性关系
    Σ = [ σ x x σ x y σ x z σ y x σ y y σ y z σ z x σ z y σ z z ] \Sigma=
    [σxxσxyσxzσyxσyyσyzσzxσzyσzz]
    Σ= σxxσyxσzxσxyσyyσzyσxzσyzσzz

    其中, σ x x , y y , z z \sigma_{xx,yy,zz} σxx,yy,zz分别是 x , y , z x,y,z x,y,z轴上的方差, σ x y \sigma_{xy} σxy x x x y y y之间的协方差。
  • 方差的影响:
    • 缩放:方差值越大,表示在该轴上分布越分散,即在该轴上的更广。
    • 旋转:协方差矩阵的非对角线元素(协方差)如果不为零,则表示两个轴之间存在相关性。这种相关性使得分布呈现出一定的倾斜或旋转,而这种倾斜正是由于两个轴之间的线性关系造成的。

Splatting

把椭球投影到2D成像平面,即Splatting渲染方法。
给定视图变换 W W W和三维协方差矩阵 Σ \Sigma Σ,计算投影的二维协方差矩阵 Σ ′ \Sigma' Σ公式如下:
Σ ′ = J W Σ W T J T \Sigma'=JW\Sigma W^TJ^T Σ=JWΣWTJT

其中, J J J是投影变换的仿射近似的雅可比矩阵,将3D向量转换为2D向量的矩阵。

  • 可直接得到的参数:2D高斯中心位置和颜色
  • 需要计算得到的参数:2D高斯的不透明度 α ′ \alpha ' α.
    α ′ = 1 − exp ⁡ ( − α det ⁡ ( Σ ) ) \alpha '=1-\exp(-\frac{\alpha}{\sqrt{\det(\Sigma)}}) α=1exp(det(Σ) α)

渲染

使用Tiled-based Rasterizer:

  • 预处理:在这一步骤中,3D 高斯的参数(协方差、位置、不透明度、球谐函数)被优化,以适应给定的图像序列和视角。使用梯度下降或其他优化算法来实现,目标是最小化重建误差。使用自适应密度控制(ADC)来调整 3D 高斯的数量和分布,以达到最佳的渲染效果。

自适应密度控制 ADC

  • 初始化:3D GS从SfM产生的稀疏点云初始化或随机初始化高斯。
  • 密集化:自适应增加高斯密度,以更好地捕捉场景细节。在规定迭代次数后执行,针对在视图空间中具有较大位置梯度(超过规定阈值)的高斯进行密集化操作。在未充分重建区域克隆小高斯,或分裂过度重建区域的大高斯。
def densify_and_clone(self, grads, grad_threshold, scene_extent):
     """ 克隆 """
     # Extract points that satisfy the gradient condition
     selected_pts_mask = torch.where(torch.norm(grads, dim=-1) >= grad_threshold, True, False)
     selected_pts_mask = torch.logical_and(selected_pts_mask,
                                           torch.max(self.get_scaling, dim=1).values <= self.percent_dense*scene_extent)
     
     # 直接复制,并添加到存储空间中
     new_xyz = self._xyz[selected_pts_mask]
     new_features_dc = self._features_dc[selected_pts_mask]
     new_features_rest = self._features_rest[selected_pts_mask]
     new_opacities = self._opacity[selected_pts_mask]
     new_scaling = self._scaling[selected_pts_mask]
     new_rotation = self._rotation[selected_pts_mask]

     self.densification_postfix(new_xyz, new_features_dc, new_features_rest, new_opacities, new_scaling, new_rotation)


def densify_and_split(self, grads, grad_threshold, scene_extent, N=2):
        """ 分裂,对满足梯度条件的点进行相应操作 """
        n_init_points = self.get_xyz.shape[0]
        # Extract points that satisfy the gradient condition
        padded_grad = torch.zeros((n_init_points), device="cuda")
        padded_grad[:grads.shape[0]] = grads.squeeze()
        # 标记大于梯度阈值的点
        selected_pts_mask = torch.where(padded_grad >= grad_threshold, True, False)
        selected_pts_mask = torch.logical_and(selected_pts_mask,
                                              torch.max(self.get_scaling, dim=1).values > self.percent_dense*scene_extent)

        # 使用repeat操作,实现分裂
        stds = self.get_scaling[selected_pts_mask].repeat(N,1)
        means =torch.zeros((stds.size(0), 3),device="cuda")
        samples = torch.normal(mean=means, std=stds)	# 采样
        rots = build_rotation(self._rotation[selected_pts_mask]).repeat(N,1,1)
        # 新的点=高斯噪声+重复的xyz,即做了初始位置调整
        new_xyz = torch.bmm(rots, samples.unsqueeze(-1)).squeeze(-1) + self.get_xyz[selected_pts_mask].repeat(N, 1)
        # 缩放
        new_scaling = self.scaling_inverse_activation(self.get_scaling[selected_pts_mask].repeat(N,1) / (0.8*N))
        new_rotation = self._rotation[selected_pts_mask].repeat(N,1)
        new_features_dc = self._features_dc[selected_pts_mask].repeat(N,1,1)
        new_features_rest = self._features_rest[selected_pts_mask].repeat(N,1,1)
        new_opacity = self._opacity[selected_pts_mask].repeat(N,1)
			 
		# 存储新的点
        self.densification_postfix(new_xyz, new_features_dc, new_features_rest, new_opacity, new_scaling, new_rotation)
		# 根据梯度阈值mask创建剪枝过滤器,因为新克隆一个点,所以是N+1维
        prune_filter = torch.cat((selected_pts_mask, torch.zeros(N * selected_pts_mask.sum(), device="cuda", dtype=bool)))
        # 剪枝
        self.prune_points(prune_filter)

def densification_postfix(self, new_xyz, new_features_dc, new_features_rest, new_opacities, new_scaling, new_rotation):
        """ 添加密集化点 """
        d = {"xyz": new_xyz,
        "f_dc": new_features_dc,
        "f_rest": new_features_rest,
        "opacity": new_opacities,
        "scaling" : new_scaling,
        "rotation" : new_rotation}
			
		# 添加新的点
        optimizable_tensors = self.cat_tensors_to_optimizer(d)
        self._xyz = optimizable_tensors["xyz"]
        self._features_dc = optimizable_tensors["f_dc"]
        self._features_rest = optimizable_tensors["f_rest"]
        self._opacity = optimizable_tensors["opacity"]
        self._scaling = optimizable_tensors["scaling"]
        self._rotation = optimizable_tensors["rotation"]

        self.xyz_gradient_accum = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")
        self.denom = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")
        self.max_radii2D = torch.zeros((self.get_xyz.shape[0]), device="cuda")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
'
运行
  • 剪枝:移除冗余或影响较小的高斯。消除几乎透明的高斯(不透明度 α \alpha α低于阈值)和过大的高斯。为了防止输入相机附近的高斯密度不合理地增加,这些高斯会在固定迭代次数后,将 α \alpha α设置为接近0的值。在保证高斯精度和有效性的情况下,节约资源。
def prune_points(self, mask):
   valid_points_mask = ~mask 
   # 使用mask剪枝
   optimizable_tensors = self._prune_optimizer(valid_points_mask)

   self._xyz = optimizable_tensors["xyz"]
   self._features_dc = optimizable_tensors["f_dc"]
   self._features_rest = optimizable_tensors["f_rest"]
   self._opacity = optimizable_tensors["opacity"]
   self._scaling = optimizable_tensors["scaling"]
   self._rotation = optimizable_tensors["rotation"]

   self.xyz_gradient_accum = self.xyz_gradient_accum[valid_points_mask]

   self.denom = self.denom[valid_points_mask]
   self.max_radii2D = self.max_radii2D[valid_points_mask]


def densify_and_prune(self, max_grad, min_opacity, extent, max_screen_size):
	"""剪枝"""
    grads = self.xyz_gradient_accum / self.denom
    grads[grads.isnan()] = 0.0

    self.densify_and_clone(grads, max_grad, extent)
    self.densify_and_split(grads, max_grad, extent)
	
	# 根据透明度筛选
    prune_mask = (self.get_opacity < min_opacity).squeeze()
    if max_screen_size:
        big_points_vs = self.max_radii2D > max_screen_size
        big_points_ws = self.get_scaling.max(dim=1).values > 0.1 * extent
        prune_mask = torch.logical_or(torch.logical_or(prune_mask, big_points_vs), big_points_ws)
    
    # 剪枝
    self.prune_points(prune_mask)

    torch.cuda.empty_cache()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
# 在计算loss之后,反向传播设置为false,开始密集化
# Densification
if iteration < opt.densify_until_iter:
    # Keep track of max radii in image-space for pruning
    gaussians.max_radii2D[visibility_filter] = torch.max(gaussians.max_radii2D[visibility_filter], radii[visibility_filter])
    gaussians.add_densification_stats(viewspace_point_tensor, visibility_filter)

    if iteration > opt.densify_from_iter and iteration % opt.densification_interval == 0:
        size_threshold = 20 if iteration > opt.opacity_reset_interval else None
        gaussians.densify_and_prune(opt.densify_grad_threshold, 0.005, scene.cameras_extent, size_threshold)
    
    if iteration % opt.opacity_reset_interval == 0 or (dataset.white_background and iteration == opt.densify_from_iter):
        gaussians.reset_opacity()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 投影:3D高斯椭球投影到2D椭圆。给定视图变换 W W W和3D协方差矩阵 Σ \Sigma Σ,计算投影的2D协方差矩阵。
  • 排序:投影的2D高斯被按照其深度值进行排序,以便在后续的渲染过程中正确地处理遮挡关系。
  • 渲染

分块

为了减少对每个像素进行高斯运算,将图像划分成多个不重叠的块(Tile)。

  • 接下来,识别与指定高斯投影相交的Tile。
  • 为了解决一个高斯和多个Tile相交的问题,复制这个高斯,并为每个分配唯一的标识符,即与之相交的Tile的ID。

排序

像素位置 x x x通过视图变换 W W W,可以计算与所有重叠高斯体的距离,即深度。再根据深度排序得到深度列表 N N N。最后使用Alpha Blending(混合alpha合成)计算整个图像的最终颜色 C C C
C = ∑ i ∈ N c i α i ′ ∏ j = 1 i − 1 ( 1 − α j ′ ) C=\sum_{i\in N}c_i\alpha'_i\prod^{i-1}_{j=1}(1-\alpha'_j) C=iNciαij=1i1(1αj)

其中, c i c_i ci是学到的颜色; α ′ \alpha' α是最终的透明度,通过学到的透明度 a a a与高亮的亮度:
α i ′ = α i × exp ⁡ ( − 1 2 ( x ′ − μ i ′ ) T ∑ i ′ − 1 ( x ′ − μ i ′ ) ) \alpha'_i=\alpha_i\times \exp(-\frac{1}{2}(x'-\mu'_i)^T\sum'^{-1}_i(x'-\mu'_i)) αi=αi×exp(21(xμi)Ti1(xμi))

其中, x ′ , u i ′ x',u'_i x,ui是投影到同一坐标系。

3D → \rightarrow 2D

  • 将3D高斯通过仿射变换转换为新的3D高斯分布,并与画布像素对齐。
  • 沿第三维积分,可计算出椭球在特定像素上的着色。
  • 声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/833106
推荐阅读
相关标签