3DGS 算法架构与实现:初始化、密度控制与训练策略
在理解了 3DGS 的核心数学原理后,本文将深入探讨其完整的算法架构与实现细节。3DGS 的成功不仅依赖于优雅的数学表示,更依赖于精心设计的训练策略。
1. 引言
1.1 算法总体框架
3DGS 完整流程包括 SfM 点云初始化、高斯初始化、训练循环(可微分渲染、计算损失、反向传播、参数更新)、密度控制(添加/移除高斯)及最终输出高斯场景。
主要步骤:
- SfM 点云初始化:从多视角图像获取稀疏点云和相机位姿。
- 高斯初始化:基于 SfM 点生成初始高斯集合。
- 训练循环:随机采样视角,前向渲染,计算损失,反向传播。
- 密度控制:根据梯度累积情况克隆、分裂或剪枝高斯。
2. 初始化
2.1 SfM 点云获取
3DGS 使用 Structure from Motion (SfM) 获取初始点云和相机位姿。
常用 SfM 工具:
- COLMAP(推荐)
- OpenMVG
- VisualSFM
SfM 流程:
- 特征提取 (SIFT)
- 特征匹配
- Bundle Adjustment
- 输出稀疏点云与相机位姿
COLMAP 输出格式:
| 文件 | 内容 |
|---|---|
| cameras.bin/txt | 相机内参 (fx, fy, cx, cy) |
| images.bin/txt | 图像位姿 (四元数 + 平移) |
| points3D.bin/txt | 稀疏点云 (位置 + 颜色) |
2.2 高斯初始化策略
每个 SfM 点初始化为一个高斯:
# 初始化参数
for point in sfm_points:
# 位置:直接使用 SfM 点坐标
mu = point.xyz # (3,)
# 缩放:基于邻近点距离
distances = compute_knn_distances(point, k=3)
scale = mean(distances)/2 # (3,) 各向同性
# 旋转:单位四元数(无旋转)
rotation = [1,0,0,0] # (4,)
# 不透明度:初始为 0.1,存储 logit 值
opacity = inverse_sigmoid(0.1)
# 颜色:从 SfM 点颜色初始化 SH 0 阶
sh_dc = RGB2SH(point.color) # (3,)
sh_rest = zeros(45) # 高阶 SH 初始化为 0
2.3 参数存储格式
| 参数 | 存储形式 | 激活函数 | 原因 |
|---|---|---|---|
| 位置 μ | 原始值 | 无 | 无约束 |
| 缩放 s | log 值 | exp | 保证正值 |
| 旋转 q | 原始值 | 归一化 | 保证单位四元数 |
| 不透明度 α | logit 值 | sigmoid | 约束到 [0,1] |
| SH 系数 | 原始值 | 无 | 无约束 |
2.4 随机初始化(可选)
当没有 SfM 点云时,可以使用随机初始化:
# 在场景边界框内随机采样
points = uniform_sample(bbox, num_points=100000)
colors = random_colors(num_points)
但随机初始化通常需要更多迭代才能收敛。
3. 自适应密度控制
3.1 为什么需要密度控制?
SfM 点云的问题:
- 稀疏:无法覆盖所有表面
- 分布不均:纹理丰富区域点密集,平滑区域稀疏
- 噪声:存在离群点
密度控制目标:
- 欠重建区域:克隆/分裂增加高斯
- 过度重建:剪枝减少高斯
- 高斯过大:分裂细化
- 透明高斯:剪枝移除
3.2 梯度累积
密度控制的关键依据是位置梯度的累积:
$$\bar{g}i = \frac{1}{T}\sum{t=1}^{T} \left|\frac{\partial \mathcal{L}}{\partial \mu'_i}\right|$$
其中 $\mu'_i$ 是高斯中心在图像平面的投影位置。
直觉解释:
- 梯度大 → 高斯位置"想要移动"很多 → 当前覆盖不好
- 梯度小 → 高斯位置稳定 → 当前覆盖良好
3.3 克隆(Clone)
触发条件:
- 位置梯度累积 $\bar{g}i > \tau{grad}$(默认 0.0002)
- 高斯尺度 $s_i < \tau_{scale}$(较小的高斯)
操作:复制高斯,沿梯度方向移动
适用场景:欠重建区域,需要更多高斯覆盖
3.4 分裂(Split)
触发条件:
- 位置梯度累积 $\bar{g}i > \tau{grad}$
- 高斯尺度 $s_i > \tau_{scale}$(较大的高斯)
操作:将大高斯分裂为多个小高斯
分裂策略:
def split_gaussian(gaussian, n_split=2):
# 新高斯的位置:沿主轴方向采样
samples = sample_pdf(gaussian.covariance, n_split)
new_positions = gaussian.mu + samples
# 新高斯的缩放:除以分裂系数
new_scale = gaussian.scale / (0.8 * n_split)
# 其他参数:继承原高斯
new_rotation = gaussian.rotation
new_opacity = gaussian.opacity
new_sh = gaussian.sh
return new_gaussians
3.5 剪枝(Pruning)
剪枝策略:
| 条件 | 操作 | 目的 |
|---|---|---|
| $\alpha_i < \epsilon_\alpha$(如 0.005) | 移除 | 删除近乎透明的高斯 |
| 过大的高斯 | 移除 | 防止"飞溅"伪影 |
| 世界空间中过大 | 移除 | 删除异常高斯 |
def prune_gaussians(gaussians, opacity_threshold=0.005):
# 透明度剪枝
mask = gaussians.opacity > opacity_threshold
# 尺度剪枝
max_scale = compute_scene_extent() * 0.1
mask &= gaussians.scale.max(dim=-1) < max_scale
return gaussians[mask]
3.6 密度控制完整流程
- 累积梯度
- 每 N 次迭代计算平均梯度
- 判断梯度是否大于阈值
- 是:判断高斯尺度
- 小:克隆
- 大:分裂
- 否:保留
- 是:判断高斯尺度
- 判断透明度是否低
- 是:移除
- 否:保留
- 重置梯度累积
- 继续训练
3.7 密度控制参数
| 参数 | 默认值 | 含义 |
|---|---|---|
| densify_from_iter | 500 | 开始密度控制的迭代 |
| densify_until_iter | 15000 | 停止密度控制的迭代 |
| densify_grad_threshold | 0.0002 | 梯度阈值 |
| densification_interval | 100 | 密度控制间隔 |
| opacity_reset_interval | 3000 | 不透明度重置间隔 |
| percent_dense | 0.01 | 场景范围比例阈值 |
4. 优化策略
4.1 优化器配置
3DGS 使用 Adam 优化器,不同参数使用不同学习率:
| 参数 | 学习率 | 说明 |
|---|---|---|
| 位置 μ | 0.00016 × extent | 与场景大小相关 |
| 缩放 s | 0.005 | 对数空间 |
| 旋转 q | 0.001 | 四元数空间 |
| 不透明度 α | 0.05 | logit 空间 |
| SH DC(0 阶) | 0.0025 | 基础颜色 |
| SH rest(高阶) | 0.0025 / 20 | 视角相关颜色 |
4.2 学习率调度
位置学习率指数衰减:
$$lr_t = lr_0 \cdot \left(\frac{lr_{final}}{lr_0}\right)^{t/T}$$
4.3 球谐函数渐进激活
为避免过早学习高阶 SH 导致的过拟合,3DGS 采用渐进激活策略:
# 球谐函数激活迭代
sh_degree_schedule = {
0: 0, # 从迭代 0 开始使用 0 阶 SH
1000: 1, # 从迭代 1000 开始使用 1 阶 SH
2000: 2, # 从迭代 2000 开始使用 2 阶 SH
3000: 3 # 从迭代 3000 开始使用 3 阶 SH
}
4.4 不透明度重置
为避免高斯变得过于透明或训练初期的不稳定,周期性重置不透明度:
def reset_opacity(gaussians, target=0.01):
# 将所有高斯的不透明度重置到较低值
new_opacity = min(gaussians.opacity, target)
gaussians.opacity = inverse_sigmoid(new_opacity)
重置时机:每隔 opacity_reset_interval(默认 3000)次迭代
5. 训练流程详解
5.1 完整训练算法
算法:3D Gaussian Splatting 训练
输入:多视角图像集合 {I_i}, 相机参数 {K_i, R_i, t_i}
输出:优化后的高斯集合 G
1. 初始化
- 运行 COLMAP 获取稀疏点云 P 和相机位姿
- 从 P 初始化高斯集合 G
- 初始化优化器
2. 训练循环 (iter = 1 to max_iter)
2.1 随机选择视角
- 随机选取训练图像 I_i
2.2 前向渲染
- 计算协方差矩阵 Σ = RSS^TR^T
- 投影高斯到 2D Σ' = JWΣWᵀJᵀ
- Tile-based 排序
- Alpha 混合得到渲染图像 Î_i
2.3 损失计算
- L = (1-λ)L1(I_i, Î_i) + λ·L_SSIM(I_i, Î_i)
2.4 反向传播
- 计算 ∂L/∂θ
- 累积位置梯度 g_μ
2.5 参数更新
- Adam 优化器更新参数
- 更新学习率
2.6 密度控制 (每 densification_interval 迭代)
- if iter < densify_until_iter:
- 克隆小高斯(高梯度)
- 分裂大高斯(高梯度)
- 剪枝透明高斯
- 重置梯度累积
2.7 不透明度重置 (每 opacity_reset_interval 迭代)
- 重置所有高斯不透明度
3. 返回优化后的高斯集合 G
5.2 训练时间线
- 0: SfM 重建,高斯初始化
- 500: 密度控制激活
- 1000: 1 阶 SH 激活
- 2000: 2 阶 SH 激活
- 3000: 3 阶 SH 激活,不透明度重置
- 30000+: 完成训练
5.3 典型训练配置
training:
iterations: 30000
# 学习率
position_lr_init: 0.00016
position_lr_final: 0.0000016
position_lr_delay_mult: 0.01
position_lr_max_steps: 30000
feature_lr: 0.0025
opacity_lr: 0.05
scaling_lr: 0.005
rotation_lr: 0.001
# 损失权重
lambda_dssim: 0.2
# 密度控制
densify_from_iter: 500
densify_until_iter: 15000
densify_grad_threshold: 0.0002
densification_interval: 100
# 不透明度重置
opacity_reset_interval: 3000
# SH 渐进
sh_degree: 3
6. 关键实现细节
6.1 协方差计算的数值稳定性
为避免协方差矩阵奇异,添加小的对角正则化:
def build_covariance(scale, rotation):
S = torch.diag(scale)
R = quaternion_to_matrix(rotation)
# 协方差矩阵
L = R @ S
cov = L @ L.T
# 数值稳定性
cov = cov + 1e-6 * torch.eye(3)
return cov
6.2 深度排序优化
为减少排序开销,使用基数排序(Radix Sort):
// CUDA 基数排序(简化版)
// 对 (key, value) 对按 key 排序
// key: 深度值的整数表示
// value: 高斯索引
cub::DeviceRadixSort::SortPairs(
d_temp_storage, temp_storage_bytes,
d_keys_in, d_keys_out,
d_values_in, d_values_out,
num_items
);
6.3 Tile 分配
高斯到 Tile 的分配使用保守光栅化:
def get_tile_rect(gaussian_2d, image_size, tile_size=16):
# 计算 2D 高斯的边界框(3σ范围)
radius = 3 * sqrt(max(eigenvalues(cov_2d)))
# 转换为 Tile 坐标
min_tile = floor((center - radius) / tile_size)
max_tile = ceil((center + radius) / tile_size)
return min_tile, max_tile
6.4 内存优化
梯度累积的高效实现:
# 使用原子操作累积梯度
# 避免为每个高斯存储完整梯度历史
class GradientAccumulator:
def __init__(self, num_gaussians):
self.grad_sum = torch.zeros(num_gaussians)
self.count = torch.zeros(num_gaussians)
def accumulate(self, grad, indices):
# 原子加法
self.grad_sum.index_add_(0, indices, grad.norm(dim=-1))
self.count.index_add_(0, indices, torch.ones_like(indices))
def get_mean(self):
return self.grad_sum / (self.count + 1e-6)
7. 常见问题与解决方案
7.1 训练问题诊断
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 高斯数量爆炸 | 密度阈值过低 | 增大 densify_grad_threshold |
| 渲染模糊 | SH 阶数不足 | 增加 SH 阶数或训练更多迭代 |
| 飞溅伪影 | 存在异常大高斯 | 降低缩放学习率,增加剪枝 |
| 训练不收敛 | 学习率过高/过低 | 调整学习率 |
| 孔洞 | 初始点云稀疏 | 使用更密集的 SfM 或增加密度控制强度 |
7.2 性能优化建议
- 减少高斯数量:更激进的剪枝
- 加速渲染:更大的 Tile 尺寸
- 减少内存:FP16 混合精度
- 加速训练:梯度累积批处理
7.3 质量优化建议
| 优化目标 | 方法 |
|---|---|
| 细节保持 | 增加训练迭代,降低密度控制阈值 |
| 视角一致性 | 确保 SH 阶数≥2 |
| 边缘清晰 | 增加 SSIM 权重 |
| 减少伪影 | 增加不透明度重置频率 |
8. 算法变体与改进
8.1 Mip-Splatting
解决锯齿和模糊问题:
- 多尺度渲染问题
- 3D 平滑滤波器
- 抗锯齿 + 保持清晰
8.2 Compact-3DGS
通过向量量化压缩高斯参数:
| 组件 | 原始大小 | 压缩后 | 压缩比 |
|---|---|---|---|
| 位置 | 12B | 6B | 2x |
| 协方差 | 24B | 8B | 3x |
| 颜色 | 48B | 8B | 6x |
| 总计 | ~236B | ~40B | ~6x |
8.3 Mini-Splatting
通过自适应采样减少高斯数量:
def importance_sampling(gaussians, target_count):
# 基于渲染贡献度采样
importance = compute_render_contribution(gaussians)
selected = multinomial_sample(importance, target_count)
return gaussians[selected]
9. 总结
本文详细介绍了 3DGS 的完整算法架构:
9.1 核心组件
| 组件 | 作用 | 关键设计 |
|---|---|---|
| SfM 初始化 | 提供初始点云和相机 | COLMAP |
| 自适应密度控制 | 动态调整高斯数量 | 克隆 + 分裂 + 剪枝 |
| 优化策略 | 参数学习 | Adam+ 学习率调度+SH 渐进 |
| 损失函数 | 监督信号 | L1+SSIM |
9.2 关键超参数
| 类别 | 参数 | 推荐值 |
|---|---|---|
| 训练 | 迭代次数 | 30000 |
| 密度 | 梯度阈值 | 0.0002 |
| 损失 | SSIM 权重 | 0.2 |
| 学习率 | 位置初始 | 0.00016 |
9.3 算法特点
- 自适应:根据场景复杂度动态调整高斯数量
- 高效:分钟级训练,实时渲染
- 稳定:多种策略保证训练收敛
