如何理解3d gaussian splatting 3D高斯溅射 和splatam(论文和代码)

前置知识:
线性代数: 理解向量、矩阵、矩阵乘法。
微积分: 理解“梯度”(gradient)的概念。
基本概念: 知道什么是“渲染”,什么是“优化”,什么是“损失函数”。

第一部分:核心哲学 —— 为什么是高斯溅射3d gaussian splatting?

在3DGS出现之前,最火的技术是NeRF(神经辐射场)。
NeRF的哲学: 世界是连续的、隐式的。它像一个“魔法黑盒”(一个巨大的神经网络),你问它空间中任意一个点(x,y,z)从某个方向看是什么颜色,它会通过复杂的计算告诉你。这很强大,但缺点是慢(查询一次就要跑一次神经网络),而且不可编辑(你无法“抓住”场景里的一个物体)。
3DGS的哲学: 世界是离散的、显式的。它认为,我们不需要一个神秘的黑盒,我们可以像“印象派画家”一样,用数百万个半透明的、有颜色的、有形状的“颜料块”(即3D高斯基元),来“画”出整个三维世界。
3DGS的核心优势: 因为这些“颜料块”是明确存在的实体,所以我们可以极快地把它们“甩”到画布上(实时渲染),并且我们还能选中、移动、删除任何一个颜料块(可编辑性)。

第二部分:世界的“原子”——解构一个3D高斯基元

整个3DGS场景,就是由数百万个这样的3D高斯基元构成的。每一个3D高斯基元(3D Gaussian Primitive),都由以下四个核心属性定义。

1. 位置 Position, \(μ\)

3D高斯基元在三维世界中的中心点坐标 \((x, y, z)\)。数据类型为一个3维向量。

2. 形状与姿态 Shape & Orientation, \(Σ\)

最关键也最难理解的部分。它描述了3D高斯基元是什么形状以及朝向哪里。
它是一个胖胖的球体?一个扁平的“煎饼”(适合表示地面)?还是一根细长的“针”(适合表示电线杆)?
在数学上,这由一个3x3的协方差矩阵 (Covariance Matrix) \(Σ\) 来描述。
为了方便优化,论文作者没有直接存储这个矩阵,而是把它分解成了两个更容易理解的部分:
三维缩放向量 \(S = (sx, sy, sz)\) 代表这个“原子”在自己的坐标系下,三个轴向的“大小”或“胖瘦”。
四元数\(R\)代表这个“原子”在空间中的“旋转姿态”。
可以把每个高斯“原子”想象成一个可以自由旋转和缩放的、半透明的椭球体。

3. 颜色 Color, \(c\)

3D高斯基元是什么颜色?不是一个简单的RGB值。为了表示一些有光泽、有反射的、从不同角度看颜色会轻微变化的物体(View-dependent Effects),作者使用了球谐函数 (Spherical Harmonics, SH) 来表示颜色。
暂时不用深究球谐函数的数学。只需要记住球谐函数是一种聪明的数学工具,它让一个高斯“原子”的颜色,能够根据你看向它的方向(视角)而发生轻微的变化,从而模拟出现实世界中的高光等效果。

4. 透明度 Opacity, α

3D高斯基元有多“实”。α=1代表完全不透明,α=0代表完全透明,数据类型是一个标量。
整个3DGS世界,就是由数百万个“拥有位置、形状、视角相关颜色和透明度的半透明椭球体”所构成的。

第三部分:渲染引擎 —— 从3D高斯到2D图片

现在,我们有了3D高斯基元,如何把它们变成我们能看到的2D图片呢?这个过程就是光栅化(Rasterization)。

Step 1: 投影 (Projection)

对于一个给定的相机位姿,我们需要将所有这些三维的椭球体,投影到相机的二维成像平面上,变成一个个二维的椭圆。
实现上述任务是一个标准的计算机图形学过程。用相机矩阵,可以把三维中心点μ投影成二维中心点。用一个更复杂的数学公式(涉及到雅可比矩阵),可以把3D协方差矩阵Σ投影成一个2D协-方差矩阵。

Step 2: 切片/瓦片化光栅化 (Tiled Rasterization)

高效地找出每个像素被哪些2D高斯椭圆覆盖,需要tile(瓦片)技术。
屏幕被划分为一个个16x16的瓦片。
GPU为每个瓦片,首先快速地筛选出所有与这个瓦片区域重叠的2D高斯椭圆。
然后,将这些相关的高斯椭圆信息加载到高速共享内存中。
最后,才在这个小区域内,进行精细的、逐像素的计算。

Step 3: 排序与混合 (Sorting & Blending)

对于屏幕上的任何一个像素,现在我们已经知道它被哪些高斯椭圆覆盖了。
我们需要按照从前到后的顺序,将它们的颜色混合起来,得到这个像素的最终颜色。
首先,所有覆盖这个像素的高斯椭圆,需要按照它们的深度(离相机的距离)进行排序,从近到远。
然后,从最近的那个高斯开始,依次向后“混合”颜色。最终的颜色C由以下公式决定:
\(C = Σ c_i * α'_i * Π(1 - α'_j)_{j = 1 … i-1}\)
\(c_i\)当前第i个高斯(按深度排序后)的颜色。
\(α'_i\)第i个高斯对这个像素点的贡献透明度。它等于这个高斯本身的透明度α,乘以它在这个像素位置的“浓度”(高斯分布的概率密度)。
\(Π(1 - α'_j)\)是透光率 (Transmittance)。它代表了所有在第i个高斯前面的j个高斯,允许多少光线通过。
\(1 - α'\)代表一个高斯的不透明度,将它们全部乘起来,就得到了光线能“幸存”到第i层的比例。
一个像素的最终颜色,是所有覆盖它的、半透明的彩色颜料块,按照从前到后的顺序,一层一层“叠加”出来的结果。

第四部分:学习过程 —— 如何让高斯“活”起来

我们一开始并不知道这些高斯“原子”应该放在哪、应该是什么形状和颜色。
1 优化Optimization
从一系列已知相机位姿的真实照片中,自动地学习出所有这些参数。
让渲染器(第三部分)渲染出的图片,和真实的训练图片,长得一模一样。
2 损失函数Loss Function
用一个数学函数来衡量长得像不像。
通常是L1损失(逐像素颜色差异)和D-SSIM(结构相似性损失)的结合。
3 自动微分Autograd
3DGS最神奇的地方在于,从参数(位置、形状、颜色、透明度)到最终像素颜色的整个渲染流程,都是可微分的。
我们可以利用PyTorch等深度学习框架的autograd功能,自动计算出损失函数对于每一个高斯“原子”的每一个参数的梯度。
4 优化器Optimizer
梯度告诉我们“如何调整参数,才能让渲染结果和真实图片更像一点点”。
我们将这些梯度,喂给一个Adam优化器。优化器就会自动地、迭代地去微调那数百万个高斯“原子”的所有参数。
5 自适应控制Adaptive Control
在训练过程中,系统还会智能地进行“分裂”和“克隆”操作。
如果某个地方的误差很大(颜色不对),系统就会自动地将那里的一个大高斯点分裂成几个小高斯点,以增加细节。
这就是所谓的“致密化” (Densification)。

第五部分:完整流程串讲

输入: 一堆带有精确相机位姿的图片(通常由COLMAP工具生成)。
初始化: 用COLMAP生成的稀疏点云,来初始化第一批非常粗糙的3D高斯“原子”。
进入训练循环:
a. 随机选一张训练图片和它的位姿。
b. 使用渲染引擎(第三部分),从当前的高斯模型中,渲染出这张图片。
c. 将渲染图与真实图进行比较,计算损失(第四部分)。
d. 通过自动微分,计算损失对所有高斯参数的梯度。
e. 用优化器更新所有高斯参数。
f. 根据需要,进行自适应的致密化或剪枝。
重复上一步数万次。
完成! 最终得到了一组优化好的、能够完美复现整个场景的、数百万个3D高斯基元的参数集合。

下一步做什么?

下一步,就是将这些理论与代码对应起来,达到“庖丁解牛”的境界:
scene/gaussian_model.py: 去看GaussianModel这个类,您会找到我们第二部分讨论的所有属性(位置_xyz,缩放_scaling,旋转_rotation,颜色_features_dc/_features_rest,透明度_opacity)。
gaussian_renderer/init.py: 这是第三部分“渲染引擎”的C++/CUDA实现接口。
train.py: 这是第四部分“学习过程”的完整体现,您可以在其中找到损失函数、优化器和致密化逻辑的调用。

tile在CUDA代码中的角色和意义

在3DGS的CUDA光栅化代码中,tile是实现极致性能的核心策略,我们称之为Tiled-based Rasterization
理解了tile(瓦片)的概念,就等于理解了3D Gaussian Splatting之所以能实现惊人速度的底层原因之一。
简单来说,tile(瓦片)是GPU为了高效并行处理图像渲染,而将最终屏幕画面分割成的一个个小的、独立的矩形计算单元。
让我们用一个比喻,然后深入到CUDA代码的实现细节中,来彻底搞懂它。
一个生动的比喻:贴马赛克瓷砖
想象一下,您的任务是用无数片五颜六色的、半透明的马赛克小瓷片(这就是“高斯点”),铺满一整面巨大的墙壁(这就是“最终的屏幕画面”)。
一个低效的工人可能会:
拿起一片红色瓷片,跑到墙的左上角贴一下。
再拿起一片蓝色瓷片,跑到墙的右下角贴一下。
再拿起一片绿色瓷片,跑到墙的中间贴一下。
...
他会把大量时间浪费在来回奔跑上,效率极低。
一个聪明的工人(GPU)则会这样做:
1 划分网格: 他先把整面墙,用粉笔划成一个个16x16厘米的方格。每一个方格,就是一个tile(瓦片)。
2 团队协作: 他叫来了一群工友(一个CUDA Thread Block),每个人负责一个方格。
3 批量备料: 在开始贴自己负责的那个方格之前,每个工友会先把所有可能会覆盖到这个方格的马赛克瓷片,从远处的大仓库(全局显存 VRAM)里,一次性地全部搬到自己手边的一个小推车里(片上共享内存 Shared Memory)。
4 专心施工: 接下来,工友就只在自己这一小块方格里工作。他需要的所有材料都在手边的小推车里,取用速度极快。他在这里完成排序(哪片在前)、混合(半透明颜色叠加)等所有复杂工序。
5 并行完成: 因为所有工友都是同时在自己的小方格里施工,整面墙很快就铺好了。
6 技术拆解:tile在CUDA代码中的角色和意义

在3DGS的CUDA光栅化代码中,tile是实现极致性能的核心策略,我们称之为“Tiled-based Rasterization”

1. tile是什么?

它是一个软件层面的概念。渲染程序会将1920x1080的屏幕,在逻辑上划分为一个由120x68个tile组成的网格(假设每个tile是16x16像素)。GPU会启动120 * 68 = 8160个线程块(Thread Blocks),每一个线程块(例如包含256个线程),就负责完全渲染好其中一个16x16的tile。

2. 为什么必须用tile?—— 不可或缺性

使用tile能带来三大核心优势,解决了GPU渲染中最关键的性能瓶颈:

优势一:最大化并行度 (Parallelization)

渲染几百万个高斯点是一个巨大的任务,但渲染一个只被几十个高斯点覆盖的16x16tile是一个小任务。通过将大任务分解成数千个独立的小任务,可以把GPU上成千上万个核心的计算能力“喂饱”,实现最大程度的并行处理。

优势二:利用片上高速缓存 (Cache/Shared Memory Efficiency)

这是最最关键的一点。 GPU的全局显存(VRAM)速度“很慢”,而线程块内部的共享内存(Shared Memory)速度“极快”,堪比CPU的L1缓存。
工作流程
1 筛选 (Culling): 在处理一个tile之前,程序会先进行一次粗略的计算,找出所有投影后会与这个16x16像素区域有重叠的3D高斯点。
2 加载 (Loading): 程序将这些“相关的”高斯点的全部信息(位置、协方差、颜色、透明度),一次性地从慢速的全局显存,加载到该线程块专属的、快速的共享内存中。
3 处理 (Processing): 接下来,这个线程块内的256个线程,在计算这个tile内所有像素的颜色时,需要的所有高斯点数据,都直接从共享内存中读取。这避免了成千上万次对慢速全局显存的访问,带来了数量级的性能提升。

优势三:实现高效的局部排序 (Efficient Local Sorting)

3DGS的核心是需要将覆盖同一个像素的所有高斯点,按照深度从前到后进行排序,然后依次混合颜色。
对整个场景的几百万个高斯点进行全局排序是极其昂贵的。
而在tile的框架下,每个线程块只需要对自己共享内存里的那几十到几百个相关的“高斯点”进行排序。这是一个规模小得多的问题,可以在片上高速内存中非常快速地完成。

CUDA代码中的执行流程

所以,当您看到gaussian-splatting的CUDA渲染核心代码时,它的逻辑可以被理解为(以一个线程块/一个tile的视角):

  1. 启动: 我是第N号线程块,被分配去渲染屏幕上(x, y)位置的那个16x16的tile。
  2. 筛选: 我快速过一遍场景中所有高斯点的包围盒,发现有150个高斯点可能会覆盖我的tile。
  3. 加载: 我和我的255个“兄弟”线程一起,把这150个高斯点的详细数据,从全局显存搬到我们共享的“小推车”(Shared Memory)里。
  4. 排序: 我们一起,用一个并行的排序算法(如Bitonic Sort),快速地把这150个高斯点按照深度排好序。
  5. 渲染: 我们256个线程,每个人分几个像素。我负责计算(x+3, y+5)这个像素的最终颜色。我将按照排好的顺序,依次读取共享内存中的150个高斯点数据,计算它们对我这个像素的颜色贡献,并按顺序混合,得到最终颜色。
  6. 写回: 我们256个线程,把自己负责的像素的最终颜色,写回到全局显存的最终图像缓冲区中。
  7. 收工。

这个过程,同时在GPU上的数千个线程块中发生着,最终在一瞬间(小于16毫秒)完成了整个屏幕的渲染。

高效理解一个开源研究项目的四步法(以splatam为例)

Step1 理解“是什么” - 精读论文 (High-Level Concept)

  • 目标: 忘掉代码,先从最高层理解作者的设计思想。
  • 做法:
  1. 阅读SplaTAM的论文。第一遍先看摘要、引言、方法章节的框图和结论。你要能用自己的话回答:
  • 这个系统解决了什么问题?(用3DGS做实时单目SLAM)
  • 它的输入和输出分别是什么?(输入:单目RGB视频流;输出:3DGS地图+相机位姿)
  • 它由哪几个核心模块组成?(通常是追踪Tracking, 建图Mapping, 优化Backend等)
  1. 第二遍再稍微细致地读一下方法(Methodology)章节,对照着流程图,搞清楚数据在这些模块之间是如何流动的。暂时不用深究每一个数学公式。

Step2 验证“能运行” - 复现官方Demo (Baseline Replication)

  • 目标: 确认你的环境配置无误,并且亲眼看到这个“轮子”能正常转动。
  • 做法:
  1. 严格遵循官方 README.md 的指引,安装所有依赖。
  2. 下载他们提供的标准测试数据集(比如Replica)。
  3. 运行官方的demo脚本,确保你能够复现出和他们展示的视频或图片里差不多的结果。
  • 这一步非常重要。如果官方demo都跑不起来,那说明你的环境有问题,需要先解决。如果跑通了,它会给你巨大的信心,并为你后续的修改提供一个可供对比的“黄金标准”。

Step3 探索“在哪里” - 代码与论文的对应 (Code Walkthrough)

  • 目标: 将论文中的抽象模块和代码库里的具体文件对应起来。
  • 做法:
  1. 打开代码编辑器(如VS Code),把SplaTAM项目作为工作区打开。
  2. 根据论文里的模块名和关键词,去寻找对应的 .py 文件。
  • 论文里的“Tracking”模块,可能对应着 tracker.py 或 frontend.py。
  • “Mapping”模块,可能对应着 mapper.py 或 backend.py。
  • “Gaussian Representation”可能对应着一个叫 gaussian_model.py 的文件,里面定义了高斯点的数据结构。
  1. 找到主程序入口(通常是 run.py 或 main.py),看看它是如何初始化并调用这些模块的。试着在关键位置加上一些 print("I am here") 语句,然后重新运行demo,看看这些信息打印的顺序,以验证你对代码流程的理解。

Step4 明确“怎么用” - 剖析核心API (API Dissection)

  • 目标: 彻底搞清楚我们写“桥梁”脚本所需要调用的那几个核心函数的“接口”长什么样。
  • 做法:
  1. 重点关注初始化: 找到SplaTAM主类的 init 方法。看看它需要哪些参数?是不是需要一个配置文件路径?这个配置文件里有哪些关键参数(比如图像分辨率、相机内参)?
  2. 重点关注数据处理函数: 找到那个处理每一帧数据的主函数,比如 slam.track(frame) 或 slam.process_frame(image, pose)。你需要精确地知道:
  • 这个函数需要几个参数?
  • 每个参数的数据类型是什么?(是NumPy数组还是PyTorch Tensor?)
  • 图像的格式是什么?(RGB还是BGR?像素值范围是0-255还是0-1?)
  • 深度图的单位是什么?(米还是毫米?)
  • 相机位姿是用什么格式表示的?(是4x4的变换矩阵吗?它的坐标系定义是什么?)
posted @ 2025-08-14 22:35  asandstar  阅读(309)  评论(0)    收藏  举报