3DGS在重建质量和渲染速度上都表现出色,但训练完成后动辄数十万乃至上百万个高斯点,是很多使用者共同的困扰。点云过密带来的直接后果是渲染开销线性增长、显存占用居高不下、导出的模型文件体积庞大,在移动端或Web端部署时尤其难受。其实点云数量失控并非算法本身的缺陷,而是致密化策略与修剪阈值共同作用的结果,只要理解其内在机制并做针对性调优,完全可以在几乎不损失画质的前提下把点数压缩到原来的三分之一甚至更低。

为什么点云会越训越密:致密化机制剖析
3DGS的训练过程每隔一定迭代步数(默认每100步)会执行一次densify_and_split和densify_and_clone操作。克隆操作针对的是视角梯度大但尺度小的高斯点,分裂操作针对的是视角梯度大且尺度大的高斯点。这两类操作的本意是补充欠重建区域,但问题在于判断依据只有平均梯度这一个信号,缺乏对整体冗余度的评估。
具体来说,位置梯度过高的点会被不断克隆,而克隆出来的新点如果继续处于高梯度区域,下一轮致密化又会被再次克隆,形成滚雪球效应。在纹理复杂、光照变化剧烈的区域,比如树叶、毛发、反光表面,这种叠加尤其明显。最终这些区域会堆积大量相互重叠的高斯点,每个点对最终像素颜色的贡献都微乎其微,却实实在在地消耗着渲染资源。
另一个不可忽视的膨胀来源是随机初始化。如果初始点云来自SfM的稀疏点,程序会额外随机生成若干点填充空洞,这些点的透明度往往需要很长时间才能收敛,而默认的修剪机制如果阈值设得太宽松,它们会一直存活到训练结束。
修剪阈值调优:三个关键参数的取舍
修剪操作的核心逻辑是定期删除贡献过小的高斯点,最直接的手段是透明度阈值。原版实现中每3000步执行一次densify_postfix里的过滤,将opacity低于0.005的点剔除。这个默认值对于追求轻量模型的场景来说过于宽松,实践中可以把该值提升到0.01甚至0.02,配合更早的启动时机(比如从迭代500步就开始修剪),可以在训练早期就压制无效点的存活。
除了透明度,还可以引入基于体积的修剪。高斯点的世界空间尺度如果超过场景包围盒的一定比例,通常说明这个点在用巨大的椭球糊弄渲染,删除它对画质影响很小。下面是一个组合了透明度和尺度阈值的自定义修剪实现:
def prune_gaussians(gaussians, opacity_thresh=0.01, max_radius_ratio=0.1):
# 场景包围盒对角线长度,用于归一化高斯尺度
scene_extent = gaussians.get_scaling.max(dim=0).values.norm()
with torch.no_grad():
# 条件一:透明度过低,对像素贡献可忽略
low_opacity = gaussians.get_opacity.squeeze() < opacity_thresh
# 条件二:最大半径超过场景尺度的比例阈值,属于糊弄型大椭球
too_large = gaussians.get_scaling.max(dim=1).values > scene_extent * max_radius_ratio
prune_mask = low_opacity | too_large
gaussians.prune_points(prune_mask)
print(f"pruned {prune_mask.sum().item()} gaussians, remaining {gaussius_count(gaussians)}")第三个值得关注的阈值是梯度阈值本身。densify_grad_threshold默认为0.0002,这个值决定了哪些点会被判定为欠重建。如果观察到点数增长过快,可以将该阈值提高到0.0005左右,只有梯度真正异常的点才会触发致密化,能有效抑制纹理区域的过度增殖。反过来如果场景存在大量细节丢失,则应适当降低该值,因此梯度阈值与修剪阈值需要配合调试,单方面调大修剪强度容易造成细节区域出现空洞。
生长策略优化:从机制层面控制点数增长
阈值修剪属于事后补救,更彻底的思路是改造生长策略本身。首先是限制致密化的时间窗口,原版实现在迭代到15000步之前一直执行致密化,实践中将致密化区间收紧到3000至9000步,既能让模型在早期快速成型,又避免了后期高梯度区域的持续堆点。同时把致密化间隔从100步拉长到200步,给优化器留出收敛时间,很多点在下一轮致密化到来之前梯度已经回落,自然就不会被克隆。
其次是给分裂操作加上数量上限或概率衰减。可以在每次致密化前统计当前点数,一旦超过预设上限(例如30万),就按比例降低致密化执行概率:
def adaptive_densify_prob(current_count, hard_limit=300000):
# 点数越接近上限,致密化概率越低,超过上限则完全停止
if current_count >= hard_limit:
return 0.0
ratio = current_count / hard_limit
# 线性衰减,可根据需要换成指数衰减
return max(0.0, 1.0 - ratio * 0.8)
# 训练循环中调用
if iteration % densify_interval == 0:
prob = adaptive_densify_prob(len(gaussians._xyz))
if random.random() < prob:
gaussians.densify_and_prune(args, optimizer)再者,分裂后的尺度收缩系数也值得调整。原版分裂时把父高斯的尺度乘以0.8作为两个子点的初始尺度,这保证了父子点覆盖体积之和小于原点。如果将这个系数压到0.6左右,子点会更小更精细,配合后续修剪可以更快地淘汰掉冗余的那一个。此外,对于欠重建区域优先采用克隆而非分裂、对过重建区域执行点融合(合并重叠度高且颜色相近的高斯),都是学术界已经验证有效的压缩方向,相关的融合策略在多个压缩版3DGS工作中都有实现可以参考。
效果验证与参数推荐组合
调参不能凭感觉,建议固定一个测试视角集合,在每轮实验后记录三个指标:最终高斯点数、测试视角的PSNR、以及单帧渲染耗时。经验上,透明度阈值0.01、梯度阈值0.0004、致密化窗口收窄到前60%训练步数,这一组合在多数室外场景下能把点数压到原版的40%左右,PSNR下降控制在0.2dB以内,肉眼几乎无法察觉差异。
室内场景纹理相对简单,可以更激进一些,透明度阈值上调到0.02,同时启用尺度修剪。需要注意的是,修剪强度与训练学习率存在耦合关系:大量删点后剩余点的梯度分布会变化,如果发现删点后画质明显劣化,可以适当降低致密化阶段的学习率,给幸存点更多时间重新收敛。调优本质上是在点数、质量、训练时长三者之间寻找平衡,先从默认参数出发做单变量实验,逐步收紧阈值并观察指标变化,比一次性大改参数要稳妥得多。