19fd67c294c4c.png

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/

整体流程可以简化为:

19fd67f926279.png

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")

这段代码里有三个工程关键点:

  1. 初始点来自 SfM/COLMAP,说明相机位姿质量很重要。
  1. 颜色被转为球谐系数,支持视角相关外观。
  1. 初始 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. 技术应用领域与前景 🌍

19fd6810eef0b.png

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. 质量评估与常见问题

19fd6827e5028.png
问题 可能原因 建议
训练不收敛 相机内参/主点不匹配、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 就是在开心地补上一句:我们还可以让它更像真实表面。又快、又亮、还更讲几何,这条路线很值得继续跟进。🌈

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注