
01. 为什么 2D Gaussian Splatting 值得关注
3D Gaussian Splatting 让实时神经渲染一下子“跑起来”了:它用大量可学习的 3D 高斯椭球表示场景,并通过 GPU rasterization 实现高质量、高速度的新视角合成。但它也有一个很现实的小烦恼:3D 高斯是体积状的,视觉效果很强,几何表面却不一定足够干净。
2D Gaussian Splatting,简称 2DGS,则把这个思路往几何方向推了一步。项目 README 对它的描述非常直白:
用一组有方向的 2D disks,也就是 surface elements,来表示场景,并通过 perspective-correct differentiable rasterization 进行渲染。
换句话说,2DGS 不再让高斯像小云团一样漂在空间里,而是让它们更像一片片贴在物体表面上的小“彩色贴片”。这对 几何重建、表面法线、深度一致性、mesh 提取 都更友好。
如果要用一句活泼但严谨的话概括它:
3DGS 擅长把场景“画得漂亮”,2DGS 进一步努力把场景“贴得像表面”。✨
02. 项目整体架构
本地项目的核心文件如下:
| 模块 | 作用 | 典型文件 |
|---|---|---|
| 2D 高斯模型 | 管理位置、颜色、透明度、二维尺度、旋转、稠密化和裁剪 | scene/gaussian_model.py |
| 可微渲染器 | 调用 diff_surfel_rasterization,输出 RGB、alpha、depth、normal、distortion |
gaussian_renderer/__init__.py |
| 训练主循环 | 图像重建损失、SSIM、法线一致性、深度畸变正则、稠密化 | train.py |
| 数据加载 | COLMAP / Blender 数据读取,相机与点云初始化 | scene/__init__.py, scene/dataset_readers.py |
| 渲染与 mesh 导出 | 渲染 train/test/path,bounded/unbounded mesh extraction | render.py, utils/mesh_utils.py |
| 参数配置 | 模型、管线、优化超参数 | arguments/__init__.py |
| 评测脚本 | MipNeRF360、DTU、Tanks and Temples | scripts/ |
整体流程可以简化为:

03. 核心技术一:2D 高斯盘片,而不是 3D 小云团
在 scene/gaussian_model.py 中,2DGS 的高斯参数包括:
_xyz:高斯中心位置。
_features_dc与_features_rest:球谐颜色特征。
_opacity:透明度。
_scaling:二维尺度。
_rotation:空间朝向。
代码初始化如下:
class GaussianModel:
def __init__(self, sh_degree : int):
self.active_sh_degree = 0
self.max_sh_degree = sh_degree
self._xyz = torch.empty(0)
self._features_dc = torch.empty(0)
self._features_rest = torch.empty(0)
self._scaling = torch.empty(0)
self._rotation = torch.empty(0)
self._opacity = torch.empty(0)
self.max_radii2D = torch.empty(0)
self.xyz_gradient_accum = torch.empty(0)
self.denom = torch.empty(0)
self.optimizer = None
self.percent_dense = 0
self.spatial_lr_scale = 0
self.setup_functions()
重点在 setup_functions()。它构造 covariance 时只学习两个 scale,然后拼接一个固定的 1,形成面片式结构:
def build_covariance_from_scaling_rotation(center, scaling, scaling_modifier, rotation):
RS = build_scaling_rotation(
torch.cat([scaling * scaling_modifier, torch.ones_like(scaling)], dim=-1),
rotation
).permute(0,2,1)
trans = torch.zeros((center.shape[0], 4, 4), dtype=torch.float, device="cuda")
trans[:,:3,:3] = RS
trans[:, 3,:3] = center
trans[:, 3, 3] = 1
return trans
这就是 2DGS 的几何味道:它的 primitive 更接近 oriented disk / surfel,而不是完整 3D 体积高斯。结果是,场景表达更偏向表面,有利于深度和 mesh 质量。
04. 核心技术二:从 COLMAP 点云初始化可学习 Surfel
2DGS 并不是凭空长出场景。它通常从 COLMAP 的稀疏点云开始初始化:
def create_from_pcd(self, pcd : BasicPointCloud, spatial_lr_scale : float):
self.spatial_lr_scale = spatial_lr_scale
fused_point_cloud = torch.tensor(np.asarray(pcd.points)).float().cuda()
fused_color = RGB2SH(torch.tensor(np.asarray(pcd.colors)).float().cuda())
features = torch.zeros((fused_color.shape[0], 3, (self.max_sh_degree + 1) ** 2)).float().cuda()
features[:, :3, 0 ] = fused_color
features[:, 3:, 1:] = 0.0
print("Number of points at initialisation : ", fused_point_cloud.shape[0])
dist2 = torch.clamp_min(distCUDA2(torch.from_numpy(np.asarray(pcd.points)).float().cuda()), 0.0000001)
scales = torch.log(torch.sqrt(dist2))[...,None].repeat(1, 2)
rots = torch.rand((fused_point_cloud.shape[0], 4), device="cuda")
这段代码里有三个工程关键点:
- 初始点来自 SfM/COLMAP,说明相机位姿质量很重要。
- 颜色被转为球谐系数,支持视角相关外观。
- 初始 scale 根据点间距离估计,让高斯大小和稀疏点云密度匹配。
随后所有参数被注册为可优化的 nn.Parameter:
self._xyz = nn.Parameter(fused_point_cloud.requires_grad_(True))
self._features_dc = nn.Parameter(features[:,:,0:1].transpose(1, 2).contiguous().requires_grad_(True))
self._features_rest = nn.Parameter(features[:,:,1:].transpose(1, 2).contiguous().requires_grad_(True))
self._scaling = nn.Parameter(scales.requires_grad_(True))
self._rotation = nn.Parameter(rots.requires_grad_(True))
self._opacity = nn.Parameter(opacities.requires_grad_(True))
可以把这个过程理解为:先用 COLMAP 搭一个“脚手架”,再让 2D 高斯贴片在训练中自动挪位置、调颜色、改大小、变透明。
05. 核心技术三:Perspective-correct 可微 Surfel Rasterization
渲染入口在 gaussian_renderer/__init__.py。它调用 diff_surfel_rasterization 中的 GaussianRasterizer:
from diff_surfel_rasterization import GaussianRasterizationSettings, GaussianRasterizer
def render(viewpoint_camera, pc : GaussianModel, pipe, bg_color : torch.Tensor, scaling_modifier = 1.0, override_color = None):
"""
Render the scene.
Background tensor (bg_color) must be on GPU!
"""
渲染前会根据相机视场角和投影矩阵设置 rasterization 参数:
tanfovx = math.tan(viewpoint_camera.FoVx * 0.5)
tanfovy = math.tan(viewpoint_camera.FoVy * 0.5)
raster_settings = GaussianRasterizationSettings(
image_height=int(viewpoint_camera.image_height),
image_width=int(viewpoint_camera.image_width),
tanfovx=tanfovx,
tanfovy=tanfovy,
bg=bg_color,
scale_modifier=scaling_modifier,
viewmatrix=viewpoint_camera.world_view_transform,
projmatrix=viewpoint_camera.full_proj_transform,
sh_degree=pc.active_sh_degree,
campos=viewpoint_camera.camera_center,
prefiltered=False,
debug=False,
)
真正渲染时传入高斯中心、透明度、尺度、旋转和颜色特征:
rendered_image, radii, allmap = rasterizer(
means3D = means3D,
means2D = means2D,
shs = shs,
colors_precomp = colors_precomp,
opacities = opacity,
scales = scales,
rotations = rotations,
cov3D_precomp = cov3D_precomp
)
相比普通神经场的体渲染,splatting 的优势是速度非常舒服:它更像 GPU 图形管线里的 rasterization,而不是沿每条光线采样很多点。也正因为如此,Gaussian Splatting 系列特别适合实时预览和交互式三维内容生产。
06. 核心技术四:渲染器不只输出 RGB,还输出几何监督信号
2DGS 的渲染器输出不止一张彩色图。allmap 中还包含 alpha、normal、depth、distortion 等辅助图:
render_alpha = allmap[1:2]
render_normal = allmap[2:5]
render_normal = (render_normal.permute(1,2,0) @ (viewpoint_camera.world_view_transform[:3,:3].T)).permute(2,0,1)
render_depth_median = allmap[5:6]
render_depth_median = torch.nan_to_num(render_depth_median, 0, 0)
render_depth_expected = allmap[0:1]
render_depth_expected = (render_depth_expected / render_alpha)
render_depth_expected = torch.nan_to_num(render_depth_expected, 0, 0)
render_dist = allmap[6:7]
其中 depth_ratio 用来在 expected depth 与 median depth 之间插值:
surf_depth = render_depth_expected * (1-pipe.depth_ratio) + (pipe.depth_ratio) * render_depth_median
README 里也给了经验建议:
- bounded scene 通常可用 median depth,即
depth_ratio=1。
- unbounded / large scene 推荐 mean expected depth,即
depth_ratio=0,可减少 disk aliasing。
这很实用。简单说,depth_ratio 是一个调节“表面深度取法”的旋钮,影响几何质量和伪影表现。
07. 核心技术五:法线一致性与深度畸变正则
2DGS 要解决的不是“能不能渲染”,而是“渲染出来的东西是不是像真实表面”。因此训练里加入了几何正则。
在 train.py 中,基础图像损失是 L1 + SSIM:
gt_image = viewpoint_cam.original_image.cuda()
Ll1 = l1_loss(image, gt_image)
loss = (1.0 - opt.lambda_dssim) * Ll1 + opt.lambda_dssim * (1.0 - ssim(image, gt_image))
随后加入 depth distortion 和 normal consistency:
lambda_normal = opt.lambda_normal if iteration > 7000 else 0.0
lambda_dist = opt.lambda_dist if iteration > 3000 else 0.0
rend_dist = render_pkg["rend_dist"]
rend_normal = render_pkg['rend_normal']
surf_normal = render_pkg['surf_normal']
normal_error = (1 - (rend_normal * surf_normal).sum(dim=0))[None]
normal_loss = lambda_normal * (normal_error).mean()
dist_loss = lambda_dist * (rend_dist).mean()
total_loss = loss + dist_loss + normal_loss
这段设计很有节奏:
- 前 3000 步先别急着管深度畸变,让外观和大结构稳定下来。
- 7000 步后再加入法线一致性,让面片慢慢对齐真实表面。
这也提醒我们:2DGS 的训练不是一锅乱炖,而是一个阶段化优化过程。先“看起来像”,再“几何上更靠谱”。很像先打草稿,再上尺子 📐。
08. 核心技术六:高斯稠密化与裁剪,让模型自己长细节
Gaussian Splatting 系列的另一个灵魂,是训练过程中会动态增加和删除高斯。
训练循环里会累计可见高斯的屏幕空间梯度,并定期稠密化:
if iteration < opt.densify_until_iter:
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,
opt.opacity_cull,
scene.cameras_extent,
size_threshold
)
在 gaussian_model.py 中,稠密化分成 clone 和 split:
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)
直观理解:
- 梯度大,说明这里还没拟合好,需要更多高斯。
- 小高斯适合 clone,补局部细节。
- 大高斯适合 split,拆成多个更细的 surfel。
- 透明度太低或屏幕/世界尺度太大的点会被裁掉。
这让模型像一个会自我修剪的三维花园:哪里缺细节就长一点,哪里多余就剪掉。小小活泼一下,但机制很认真 🌱。
09. 数据入口:COLMAP 与 Blender,两条路都能进
scene/__init__.py 会自动判断数据格式:
if os.path.exists(os.path.join(args.source_path, "sparse")):
scene_info = sceneLoadTypeCallbacks["Colmap"](args.source_path, args.images, args.eval)
elif os.path.exists(os.path.join(args.source_path, "transforms_train.json")):
print("Found transforms_train.json file, assuming Blender data set!")
scene_info = sceneLoadTypeCallbacks["Blender"](args.source_path, args.white_background, args.eval)
else:
assert False, "Could not recognize scene type!"
COLMAP 相机读取时,项目只支持 SIMPLE_PINHOLE 和 PINHOLE:
if intr.model=="SIMPLE_PINHOLE":
focal_length_x = intr.params[0]
FovY = focal2fov(focal_length_x, height)
FovX = focal2fov(focal_length_x, width)
elif intr.model=="PINHOLE":
focal_length_x = intr.params[0]
focal_length_y = intr.params[1]
FovY = focal2fov(focal_length_y, height)
FovX = focal2fov(focal_length_x, width)
else:
assert False, "Colmap camera model not handled: only undistorted datasets (PINHOLE or SIMPLE_PINHOLE cameras) supported!"
这点对自采数据部署非常关键:如果 COLMAP 输出的是带畸变模型,比如 OPENCV、RADIAL,需要先做去畸变或转换成理想针孔模型。否则训练可能直接报错,或者更糟糕:看起来能跑,几何却悄悄变形。
10. Mesh 提取:从漂亮渲染走向可用几何
2DGS 的一大亮点,是它认真对待 mesh extraction。render.py 提供 bounded 和 unbounded 两种方式。
bounded 模式走 Open3D TSDF:
mesh = gaussExtractor.extract_mesh_bounded(
voxel_size=voxel_size,
sdf_trunc=sdf_trunc,
depth_trunc=depth_trunc
)
utils/mesh_utils.py 中实现如下:
volume = o3d.pipelines.integration.ScalableTSDFVolume(
voxel_length= voxel_size,
sdf_trunc=sdf_trunc,
color_type=o3d.pipelines.integration.TSDFVolumeColorType.RGB8
)
for i, cam_o3d in tqdm(enumerate(to_cam_open3d(self.viewpoint_stack)), desc="TSDF integration progress"):
rgb = self.rgbmaps[i]
depth = self.depthmaps[i]
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
o3d.geometry.Image(np.asarray(np.clip(rgb.permute(1,2,0).cpu().numpy(), 0.0, 1.0) * 255, order="C", dtype=np.uint8)),
o3d.geometry.Image(np.asarray(depth.permute(1,2,0).cpu().numpy(), order="C")),
depth_trunc = depth_trunc,
convert_rgb_to_intensity=False,
depth_scale = 1.0
)
volume.integrate(rgbd, intrinsic=cam_o3d.intrinsic, extrinsic=cam_o3d.extrinsic)
mesh = volume.extract_triangle_mesh()
对于无边界大场景,项目提供 unbounded mesh extraction,核心是空间收缩:
def contract(x):
mag = torch.linalg.norm(x, ord=2, dim=-1)[..., None]
return torch.where(mag < 1, x, (2 - (1 / mag)) * (x / mag))
def uncontract(y):
mag = torch.linalg.norm(y, ord=2, dim=-1)[..., None]
return torch.where(mag < 1, y, (1 / (2-mag) * (y/mag)))
再配合自适应 TSDF 截断:
sdf_trunc = 5 * voxel_size * torch.ones_like(samples[:, 0])
sdf_trunc[mask] *= 1/(2-torch.linalg.norm(samples, dim=-1)[mask].clamp(max=1.9))
这套设计很适合 MipNeRF360 一类室外/大范围场景:不用手工给一个有限盒子硬裁,也能尝试提取更完整的网格。
11. 训练与渲染快速上手
1. 环境安装
README 推荐直接使用 environment.yml:
git clone https://github.com/hbb1/2d-gaussian-splatting.git --recursive
conda env create --file environment.yml
conda activate surfel_splatting
环境里比较关键的依赖包括:
python=3.8.18
pytorch=2.0.0
open3d==0.18.0
trimesh==4.3.2
submodules/diff-surfel-rasterization
submodules/simple-knn
这里要特别注意:diff-surfel-rasterization 和 simple-knn 是 CUDA 扩展,部署时要保证 PyTorch、CUDA、编译器版本匹配。
2. 训练一个场景
python train.py -s <path_to_COLMAP_or_NeRF_synthetic_dataset> -m output/my_scene
常用正则参数:
python train.py -s <dataset> -m output/my_scene
--lambda_normal 0.05
--lambda_dist 0.0
--depth_ratio 0
3. 渲染与 mesh 导出
bounded mesh:
python render.py -s <dataset> -m output/my_scene --skip_train --skip_test --mesh_res 1024
unbounded mesh:
python render.py -s <dataset> -m output/my_scene --unbounded --skip_train --skip_test --mesh_res 1024
4. 训练过程可视化
项目支持 SIBR viewer / remote viewer。README 中给出的流程是:
# 先打开 viewer
<path_to_viewer>/bin/SIBR_remoteGaussian_app_rwdi
# 再训练或查看模型
python train.py -s <dataset>
python view.py -s <dataset> -m <trained_model>
这对调参特别友好,因为 Gaussian Splatting 的质量往往需要边训边看:哪里浮点多、哪里破面、哪里颜色漂,一眼就能发现。
12. 技术应用领域与前景 🌍

1. 三维内容生产
2DGS 可以从多视角图片生成高质量新视角渲染,并进一步导出 mesh。对文旅数字化、游戏资产、影视预演、电商 3D 展示来说,它是一条很有吸引力的快速建模路线。
2. 数字孪生与场景建模
相比只追求视觉效果的神经渲染,2DGS 更关注几何准确性和表面重建,因此适合做室内空间、建筑立面、园区局部区域的可视化建模。
3. AR / VR 与空间计算
AR 需要稳定的几何边界和遮挡关系。2DGS 的深度、法线和 mesh 输出可以帮助虚拟物体更自然地嵌入真实场景。
4. 机器人与具身智能
机器人不仅需要漂亮图片,还需要可规划、可碰撞检测的几何结构。2DGS 的 mesh extraction 能为离线地图构建、仿真环境生成、导航可视化提供素材。
5. 研究与评测
项目提供 MipNeRF360、DTU、Tanks and Temples 等评测脚本,既能评估新视角合成,也能评估几何重建。对做 NeRF/3DGS/mesh extraction 的团队来说,它是一个很好的研究基线。
13. 部署应用建议
1. 数据采集建议
2DGS 对相机位姿和图像覆盖度很敏感。建议:
- 每个物体或场景至少保证足够视角重叠。
- 避免大面积运动模糊和曝光跳变。
- 尽量使用固定焦距,减少自动变焦。
- 对反光、玻璃、透明物体要降低预期。
- 自采数据先跑 COLMAP,并确认
sparse/0中相机和点云质量。
2. COLMAP 预处理建议
由于代码只支持 PINHOLE 和 SIMPLE_PINHOLE,建议使用 COLMAP undistort 后的数据:
dataset/
images/
sparse/0/
cameras.bin
images.bin
points3D.bin
如果相机主点明显偏离图像中心,README FAQ 提醒可能出现 convergence issues。遇到训练不收敛时,优先检查:
- 相机模型是否已经去畸变。
- 图像尺寸与相机内参是否一致。
- COLMAP 位姿是否有明显漂移。
- 稀疏点云是否覆盖主体区域。
3. 参数选择建议
| 场景 | 推荐设置 | 说明 |
|---|---|---|
| 室内/物体级 bounded scene | --depth_ratio 1 可尝试 |
median depth 通常更贴近明确表面 |
| 室外/大场景 unbounded scene | --depth_ratio 0 |
README 建议 mean depth 可减少 disk aliasing |
| mesh 破碎 | 调整 depth_trunc, voxel_size, sdf_trunc |
bounded TSDF 对这些参数敏感 |
| mesh 范围很大 | 使用 --unbounded --mesh_res 1024 |
更少手工调参,但计算更重 |
| 训练外观好但几何差 | 提高 lambda_normal 或开启 lambda_dist |
需要小心过强正则导致外观下降 |
4. 工程服务化建议
建议把系统拆成五段:
数据接入:上传图片/视频,抽帧,质量筛选
位姿恢复:COLMAP/SfM,生成 sparse/0 和初始点云
2DGS 训练:train.py,记录参数、日志、checkpoint
产物导出:render.py,输出 RGB、depth、mesh、point_cloud.ply
质量审核:PSNR、LPIPS、点云/mesh 连通性、人工预览
生产环境建议:
- 使用任务队列,不要把训练放在同步 HTTP 请求里。
- GPU 节点预装 CUDA 扩展,避免运行时编译。
- 将模型输出目录结构标准化,便于后处理和前端查看。
- 保留
cfg_args、训练日志、checkpoint,方便复现。
- 对每个场景保存采集元数据:相机、焦距、图像数量、COLMAP 版本、训练参数。
5. Viewer 与交付格式
2DGS 可保存 point_cloud.ply,也能导出 mesh:
output/my_scene/
point_cloud/iteration_30000/point_cloud.ply
train/ours_30000/fuse.ply
train/ours_30000/fuse_post.ply
交付建议:
- 内部技术评审:保留高斯
.ply、渲染图、深度图、mesh。
- Web 展示:使用支持 Gaussian Splatting 的 viewer 或转成 mesh/GLB。
- 三维软件编辑:优先导出 mesh,经 post-process 去浮点。
- 算法后处理:保留 depth
.tiff和相机参数。
14. 质量评估与常见问题

| 问题 | 可能原因 | 建议 |
|---|---|---|
| 训练不收敛 | 相机内参/主点不匹配、COLMAP 位姿差 | 重新去畸变,检查 PINHOLE 模型 |
| 浮点多 | 透明度低但未裁剪、稠密化过度 | 提高 opacity_cull,后处理保留最大连通簇 |
| mesh 破碎 | TSDF 参数不合适,depth_trunc 太小或太大 | 调整 voxel_size/depth_trunc/sdf_trunc |
| 大场景边缘伪影 | bounded volume 不适合 | 使用 --unbounded 与 depth_ratio=0 |
| 渲染清晰但几何差 | 外观损失主导,表面约束不足 | 调整 lambda_normal 和 lambda_dist |
| 显存不足 | 分辨率高、高斯数量多、mesh_res 太大 | 降低图像分辨率或 mesh 分辨率,分阶段训练 |
15. 与 NeRF / 3DGS 的关系
| 技术 | 优点 | 局限 | 适合场景 |
|---|---|---|---|
| NeRF | 表达连续,理论优雅 | 训练/渲染通常较慢 | 高质量离线渲染研究 |
| 3D Gaussian Splatting | 实时渲染强,训练快 | 几何表面可能不够干净 | 实时新视角合成、交互展示 |
| 2D Gaussian Splatting | 更贴合表面,mesh 更友好 | 对位姿、相机模型、参数仍敏感 | 几何重建 + 高质量渲染 |
从趋势看,2DGS 不是简单替代 3DGS,而是把 Gaussian Splatting 往“几何准确辐射场”方向推进。它既继承了 splatting 的速度优势,也更认真地拥抱深度、法线和表面重建。
16. 合规与使用边界
项目 LICENSE 说明该软件用于 non-commercial, research and evaluation use。因此:
- 内部研究、技术预研、教学演示较适合。
- 商业产品、客户项目、在线 SaaS 或硬件集成前,应进行许可证审查。
- 子模块
diff-surfel-rasterization、simple-knn、Open3D、LPIPS 等依赖也需要单独确认许可。
- 生成 mesh 不应默认视为测绘级或工业检测级结果,高风险应用需要额外标定和人工复核。
17. 小结:2DGS 的真正价值 ✨
2D Gaussian Splatting 的意义,不只是“又一个 Gaussian Splatting 变体”。它抓住了一个非常实际的问题:三维内容不仅要看起来像,还要尽量长在正确的表面上。
它把高斯从体积云团变成有方向的表面盘片,再用深度畸变、法线一致性和 TSDF/空间收缩 mesh 提取,把实时神经渲染和几何重建之间的距离拉近了不少。
如果说 3DGS 是让三维重建终于“飞快好看”,那么 2DGS 就是在开心地补上一句:我们还可以让它更像真实表面。又快、又亮、还更讲几何,这条路线很值得继续跟进。🌈