3DGS(三维高斯泼溅)在重建与实时渲染里表现惊艳,但实际工程里经常会遇到渲染结果布满颗粒、黑点或者边缘闪烁的问题。这些噪声主要来自高斯数量过多导致的过拟合、稀疏视角下几何歧义,以及光栅化过程中透明度累加的数值不稳定。要去噪不能只靠后期美颜,得从表示方法和渲染链路两个层面同时入手。

为什么3DGS渲染会出现噪声
要解决问题先得弄清噪声从哪里来。标准3DGS用显式的高斯椭球表达场景,每个高斯带位置、协方差、不透明度和球谐系数。训练时如果视角少,很多高斯的深度和法线本来就不唯一,优化器为了压低损失会把某些高斯推到错误位置,形成悬浮颗粒。这类噪声在自由视角漫游时尤其明显,因为训练时没见过的角度会让错误几何暴露出来。
另一类噪声来自光栅化的数值过程。前端用基于瓦片的光栅化,把屏幕分成小块,再按深度排序做前后混合。当高斯极度细长或者重叠区域密布,排序近似和梯度截断会让不透明度估计抖动。结果就是同一帧里相邻像素颜色跳变,视频里表现为闪烁。理解这两点后,去噪技术就能针对性地分为训练期约束和渲染期滤波两条路线。
还有容易被忽略的一点:球谐系数阶数过高也会带来高频噪声。很多项目默认开到3阶,在纹理平坦区域反而学会了用高频项去拟合随机误差。适当降阶或加平滑先验,往往比后期滤波更治本。下面几节会分别展开具体手段。
训练期正则与几何约束滤波
在优化阶段介入是最根本的去噪思路。最常用的做法是给不透明度和高斯尺度加惩罚项,逼着冗余高斯消失。比如对不透明度低于阈值的高斯做周期性剪枝,能直接砍掉大量产生颗粒的小椭球。另一种有效策略是几何一致性损失:用渲染深度图反投影到邻近视角,要求重投影误差小,这样错误悬浮点会因为通不过多视角检验而被抑制。
代码上可以在原有损失里挂一个轻量正则。下面这段伪代码展示如何在每轮迭代后做尺度惩罚和剪枝:
import torch
def regularize_and_prune(gaussians, opacity_thresh=0.05, scale_w=0.01):
# gaussians: 包含 opacity, scale 等属性的对象
# 不透明度过低的直接标记为可剪枝
mask = gaussians.opacity < opacity_thresh
# 尺度惩罚:鼓励高斯不要过度细长
scale_penalty = scale_w * torch.sum(torch.exp(gaussians.scale))
loss_reg = scale_penalty + mask.float().mean()
# 实际剪枝在优化器外部调用,这里只返回附加损失
return loss_reg, mask
for step, batch in enumerate(loader):
loss_main = render_and_compare(batch)
reg_loss, prune_mask = regularize_and_prune(g)
total = loss_main + reg_loss
total.backward()
optimizer.step()
if step % 500 == 0:
g.opacity[prune_mask] = 0.0 # 简单置零等效移除
这种方法的优点是去噪和重建一体完成,渲染时无需额外开销。缺点是调参麻烦,正则权重大了会糊,小了没效果。实践中建议先用默认参数跑通,再逐步加约束观察验证集峰值信噪比变化。
除了手工正则,近期也有工作用神经网络预测每个高斯的可靠性分数,训练时动态降权不可靠高斯。这类方法显存占用高,但能更细腻地分离信号与噪声,适合离线高质量重建。
渲染后图像域滤波技术
如果模型已经训完、不想重训,那么渲染后的图像滤波就是最实用的降噪方式。传统计算机视觉里成熟的技术基本都能搬过来。双边滤波在保边的同时平滑平坦区噪声,对静态图很有效;非局部均值利用图像自相似块,能去掉结构附近的颗粒而不牺牲细节。它们都不依赖3DGS内部表示,接入成本极低。
当输出是视频或连续漫游,单帧滤波会造成时间抖动。此时需要临时一致性滤波,比如把前后几帧送进光流网络对齐再做中值或高斯混合。下面给出一个基于OpenCV的双边加简单时域平均的示例:
import cv2
import numpy as np
def denoise_frame(cur, prev_list, d=5, sigma_color=30, sigma_space=30):
# 空间域双边滤波
spatial = cv2.bilateralFilter(cur, d, sigma_color, sigma_space)
if len(prev_list) == 0:
return spatial
# 时域一致性:与历史帧按权重混合
stack = np.stack(prev_list[-3:] + [spatial], axis=0)
temporal = np.median(stack, axis=0).astype(np.uint8)
return temporal
prev_frames = []
for raw in render_stream():
out = denoise_frame(raw, prev_frames)
prev_frames.append(out)
这类方案的短板是可能抹掉锐利文字或细小结构,而且光流计算有额外延迟。如果追求实时,可以改用更轻的递归滤波,只保留上一帧状态,用指数滑动平均来抑制闪烁,代价是运动快时有点拖影。
更激进的做法是训一个小CNN或Transformer做3DGS专属去噪,输入带噪声渲染图输出干净图。它需要配对数据,但推理只要几毫秒,适合产品化。选择哪条路取决于你能否接受重训、以及对延迟和保真度的偏好。
方案对比与落地建议
把前面几条路线放到一张表里比较会更直观。训练期正则零渲染开销但迭代慢;图像滤波即插即用但可能损细节;时域滤波解决视频闪烁但依赖对齐精度;学习型去噪质量高但需数据和显卡。
| 方案 | 额外显存 | 单帧耗时 | 保真度 | 适用场景 |
|---|---|---|---|---|
| 训练期剪枝正则 | 无 | 无 | 高 | 离线重建可重训 |
| 双边/非局部均值 | 低 | 数毫秒 | 中 | 静态图快速出图 |
| 时域一致性滤波 | 中 | 十余毫秒 | 中高 | 漫游视频 |
| 轻量CNN去噪 | 高 | 几毫秒 | 高 | 线上实时产品 |
落地时我的一般建议是:先确认噪声是模型问题还是光栅化问题。把场景用已知好视角渲染,如果噪声仍在,多半是表示或训练问题,优先加剪枝和几何损失重训;如果只在陌生视角出现,后期滤波就能救场。小团队没算力重训,就用双边加时域中位数的组合,参数调温和一点,用户感知会好很多。
最后提醒,无论哪种滤波都不要盲目叠层。先测峰值信噪比和结构相似性,再让人眼看一下边缘,避免为了去噪把该有的几何也吃掉了。3DGS去噪本质是在噪声和信号间找平衡,清楚自己的瓶颈在哪,才能选对技术。