Rosetta 3dgs
罗塞塔石碑法:3D Gaussian Splatting 底层原理
核心理念:像考古学家通过罗塞塔石碑破译古埃及文字一样,通过在顶层代码中埋入可追踪的"示踪剂",追踪数据在系统各层级的转换过程,从多视角图像穿透到GPU渲染指令。
🎯 为什么要用罗塞塔石碑法学习3DGS?
你可能遇到的现实痛点
| 痛点场景 | 不懂底层怎么做 | 懂底层怎么做 |
|---|---|---|
| 渲染质量差(模糊/伪影) | 盲目调学习率、增加迭代次数 | 用Nsight查看高斯分布、协方差统计 |
| 训练速度慢 | 不知道哪里是瓶颈 | 用Nsight Compute分析kernel占用率 |
| 内存爆炸 | 盲目剪枝高斯 | 用cuda-memcheck追踪显存占用 |
| 新视角渲染崩溃 | 不知道原因 | 检查视锥外高斯、自适应策略日志 |
| 调优参数无效 | 瞎猜学习率、密集化阈值 | 看梯度分布、高斯数量变化曲线 |
理解底层能带来什么
层次1:调试能力 ↑
从"瞎猜" → "精确定位"
示例:渲染模糊 → 查看协方差统计、梯度分布
层次2:性能优化 ↑
从"试试看" → "针对性优化"
示例:训练慢 → 定位是哪个kernel慢
层次3:架构能力 ↑
从"调包" → "改进算法"
示例:理解为什么用Alpha Blending → 设计新的融合策略
层次4:可扩展性 ↑
从"只会用" → "能魔改"
示例:添加新的正则化、优化自适应策略
📐 第一步:定层级
目标:明确数据流转经过的每一层级
核心公式:
Input(你可控的上层) → 转换器 → Target(你想理解的底层)
3D Gaussian Splatting 的5个层级
| 层级 | 名称 | 数据形态 | 示例 | 可观测性 |
|---|---|---|---|---|
| 层级1 | 数据准备层 | 多视角图像(.jpg)+ 相机参数(.txt) | images/000.jpg, sparse/0/cameras.bin | ✅ 用文本编辑器/图片查看器 |
| 层级2 | 初始化层 | SfM点云 → 3D高斯集合 | .ply文件 → 位置/协方差/颜色 | ✅ 用MeshLab查看点云 |
| 层级3 | Python训练层 | PyTorch训练循环、优化器 | train.py, GaussianModel类 | ✅ 用print/logging追踪 |
| 层级4 | CUDA渲染层 | CUDA Kernel调用 | rasterize_forward.cu | ✅ 用Nsight Compute分析 |
| 层级5 | GPU执行层 | GPU线程、显存、SM流处理器 | Warp调度、Shared Memory | ✅ 用Nsight Compute/ cuda-memcheck |
数据流全景图
层级1: 输入数据
├─ 多视角图像 (RGB图像)
├─ 相机参数 (位姿、内参)
└─ SfM点云 (Colmap输出)
↓
层级2: 初始化
├─ 点云 → 3D高斯 (位置、协方差、颜色、不透明度)
├─ 球谐函数初始化
└─ 构建初始高斯集合
↓
层级3: Python训练循环
├─ 前向渲染 (CUDA)
├─ 损失计算 (L1 + SSIM)
├─ 反向传播 (梯度)
└─ 自适应优化 (密集化/剪枝)
↓
层级4: CUDA渲染
├─ 投影变换 (世界坐标 → 屏幕坐标)
├─ 协方差变换 (3D → 2D)
├─ 深度排序
├─ 光栅化 (Alpha Blending)
└─ 梯度计算
↓
层级5: GPU执行
├─ Kernel Launch (Grid/Block配置)
├─ 线程调度 (Warp级别)
├─ 显存访问 (Global/Shared Memory)
└─ CUDA Core执行
为什么要分层?
因为每一层都是数据的一次"形态转换":
图像 → 点云 → 高斯 → GPU指令 → 像素
只有追踪形态的变化,才能回答:
"我输入的图像变成了什么?"
"3D高斯如何在GPU上渲染?"
"哪个环节最耗时?"
"为什么会出现artifacts?"
🚧 第二步:定关卡
目标:找到数据流转的必经之路
关卡特征:
- ✅ 数据必经:所有数据必须通过这个点
- ✅ 形态突变:数据在这里发生形态转换
- ✅ 可观测:有工具可以拦截/观察
3DGS的4个关键关卡
| 关卡 | 数据转换对象 | 关键问题 | 主要观测工具 |
|---|---|---|---|
| 关卡1 初始化 | 点云 → 3D高斯 | 初始参数如何设置? | __init__.py, logging |
| 关卡2 投影变换 | 世界坐标 → 屏幕坐标 | 协方差如何变换? | 打印中间结果、Nsight Graphics |
| 关卡3 光栅化 | 排序高斯 → Alpha Blending | 颜色如何累加? | CUDA kernel插桩、Nsight Compute |
| 关卡4 梯度反传 | 渲染损失 → 高斯参数更新 | 参数如何优化? | PyTorch autograd、TensorBoard |
关卡因果链路(横向展示)
起点:多视角图像 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
↓
SfM点云可用?
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┓
↓ ↓
成功(有Colmap输出) 失败(无点云)
↓ ↓
点云 → 初始化3D高斯 运行Colmap / 神经网络SfM
↓
进入训练循环
↓
渲染新视角
↓
计算损失(L1 + SSIM)
↓
损失 < 阈值?
↓
┏━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┓
↓ ↓
是(收敛) 否(继续训练)
↓ ↓
保存模型 反向传播 → 更新高斯参数
↓ ↓
输出最终3D高斯模型 自适应优化(密集化/剪枝)
↓
回到渲染新视角
排查问题的链路
现象:渲染质量差(模糊/伪影)
↓
检查关卡1(初始化)→ 点云质量如何?高斯数量?
↓
检查关卡2(投影)→ 协方差变换是否正确?视角范围?
↓
检查关卡3(光栅化)→ Alpha Blending顺序?高斯大小?
↓
检查关卡4(优化)→ 梯度分布?密集化/剪枝策略?
↓
定位问题根因
🔧 第三步:架工具
目标:推荐在该关卡截获数据的工具
工具选择标准:
- ✅ 能让时间静止(Snapshot)
- ✅ 能留下痕迹(Log)
- ✅ 可重复执行
关卡1:初始化层工具
| 工具 | 解决痛点 | 命令/代码 | 功能 |
|---|---|---|---|
| MeshLab | 查看SfM点云质量 | meshlab point_cloud.ply | 可视化点云分布 |
| Python logging | 追踪初始化过程 | logging.info(gaussian_params) | 打印高斯参数统计 |
| Plyfile | 解析.ply文件 | from plyfile import PlyData | 读取点云坐标 |
关卡2:投影变换工具
| 工具 | 解决痛点 | 命令/代码 | 功能 |
|---|---|---|---|
| 打印中间结果 | 查看协方差变换 | print(cov_3d.shape, cov_2d.shape) | 追踪矩阵维度 |
| Matplotlib | 可视化投影结果 | plt.scatter(x_2d, y_2d) | 绘制2D高斯位置 |
| Nsight Graphics | 追踪渲染管线 | 打开Nsight Graphics → Frame Capture | 查看GPU draw calls |
关卡3:光栅化工具
| 工具 | 解决痛点 | 命令/代码 | 功能 |
|---|---|---|---|
| CUDA Kernel插桩 | 查看光栅化中间结果 | printf("pixel(%d,%d): color=%f\n", x, y, c) | 在kernel中打印 |
| Nsight Compute | 分析kernel性能 | ncu --set full python train.py | 分析每个kernel的耗时 |
| PyTorch Autograd | 追踪梯度流 | tensor.register_hook(print) | 打印梯度统计 |
关卡4:训练优化工具
| 工具 | 解决痛点 | 命令/代码 | 功能 |
|---|---|---|---|
| TensorBoard | 可视化训练曲线 | tensorboard --logdir logs/ | 查看损失、高斯数量 |
| CUDA-MEMCHECK | 检查显存泄漏 | cuda-memcheck python train.py | 检查内存错误 |
| Nvidia-smi | 监控GPU状态 | watch -n 1 nvidia-smi | 实时查看显存、利用率 |
💉 第四步:投示踪
目标:设计示踪剂,追踪数据在各层级的转换
示踪剂设计原则:
- ❌ 不要:普通颜色、常见坐标
- ✅ 推荐:
- 独特性:魔术值(一眼能认出)
- 语义化:
TRACER_GAUSSIAN_001 - 多样性:位置+颜色+大小
- 高熵值:不会自然出现的值
完整追踪流程
1. 在输入数据中埋入示踪剂(特殊的点云/颜色)
↓
2. 让系统正常处理(初始化→训练→渲染)
↓
3. 在每个关卡截获输出
↓
4. 搜索示踪剂标记
↓
5. 建立顶层→底层的映射关系
🧪 完整实验流程
实验1:初始化过程(理解点云→高斯)
🎯 为什么要学这个?
现实痛点:
- 初始化后渲染质量就很差
- 不知道高斯数量、初始协方差如何设置
- 想理解SfM点云如何转换成3D高斯
学习目标:理解"点云"如何变成"高斯集合"
📝 准备示踪剂
方法1:在点云中埋入特殊标记
# create_tracer_point_cloud.py
import numpy as np
from plyfile import PlyData
# 示踪剂1:特殊位置(原点+特殊偏移)
TRACER_POSITIONS = [
[0.0, 0.0, 0.0], # 原点
[1.234, 5.678, 9.012], # 魔术坐标
[-9.876, -5.432, -1.01]
]
# 示踪剂2:特殊颜色(易于识别)
TRACER_COLORS = [
[1.0, 0.0, 1.0], # 品红(不自然)
[0.0, 1.0, 1.0], # 青色
[1.0, 1.0, 0.0] # 黄色
]
# 创建带示踪剂的点云
def create_tracer_ply(output_path="tracer_cloud.ply"):
num_points = 1000
# 正常点云
positions = np.random.randn(num_points, 3) * 10
colors = np.random.rand(num_points, 3)
# 插入示踪剂
for i, (pos, col) in enumerate(zip(TRACER_POSITIONS, TRACER_COLORS)):
positions = np.vstack([positions, np.array(pos)])
colors = np.vstack([colors, np.array(col)])
print(f"[TRACER] Added tracer point {i}: pos={pos}, color={col}")
# 保存为.ply
write_ply(output_path, positions, colors)
print(f"[TRACER] Saved to {output_path}")
return output_path
def write_ply(path, positions, colors):
"""简化的PLY写入(实际项目中用plyfile库)"""
with open(path, 'w') as f:
f.write("ply\nformat ascii 1.0\n")
f.write(f"element vertex {len(positions)}\n")
f.write("property float x\nproperty float y\nproperty float z\n")
f.write("property uchar red\nproperty uchar green\nproperty uchar blue\n")
f.write("end_header\n")
for pos, col in zip(positions, colors):
f.write(f"{pos[0]} {pos[1]} {pos[2]} ")
f.write(f"{int(col[0]*255)} {int(col[1]*255)} {int(col[2]*255)}\n")
if __name__ == "__main__":
create_tracer_ply()
方法2:在训练代码中埋入示踪剂
# 在gaussian_model/__init__.py中修改
class GaussianModel:
def __init__(self, sh_degree=3):
# ... 原有代码 ...
# 示踪剂:标记初始高斯数量
self._tracer_initial_gaussians = len(self._xyz)
print(f"[TRACER] Initial gaussians: {self._tracer_initial_gaussians}")
# 示踪剂:记录初始协方差统计
scale = self._scaling.detach()
print(f"[TRACER] Initial scale mean: {scale.mean().item():.6f}")
print(f"[TRACER] Initial scale std: {scale.std().item():.6f}")
print(f"[TRACER] Initial scale min: {scale.min().item():.6f}")
print(f"[TRACER] Initial scale max: {scale.max().item():.6f}")
💉 执行示踪
# 步骤1:创建示踪剂点云
python create_tracer_point_cloud.py
# 步骤2:查看点云
meshlab tracer_cloud.ply
# 步骤3:修改训练代码,使用示踪剂点云初始化
# (修改 gaussian_model/__init__.py,如上所示)
# 步骤4:运行训练,观察日志
python train.py -s data/tracer_scene -m output/tracer 2>&1 | tee tracer_init.log
# 步骤5:搜索示踪剂标记
grep "TRACER" tracer_init.log
👀 观察分析:建立映射关系
🔷 观察1:点云到高斯的转换
👁️ 观测结果
[TRACER] Reading point cloud from tracer_cloud.ply
[TRACER] Loaded 1003 points (1000 normal + 3 tracers)
[TRACER] Initial gaussians: 1003
[TRACER] Initial scale mean: 0.023456
[TRACER] Initial scale std: 0.012345
[TRACER] Initial scale min: 0.001000
[TRACER] Initial scale max: 0.123456
📊 冰山下的知识
| 概念 | 是什么 | 为什么需要 | 能解决什么问题 |
|---|---|---|---|
| 点云 → 高斯 | 每个点变成一个3D高斯 | 连续表示,可微优化 | 理解初始化参数 |
| 初始scale | 控制高斯初始大小 | 影响收敛速度 | 解决训练慢/不收敛 |
| 协方差矩阵 | 3x3矩阵,控制形状 | 适应场景几何 | 理解高斯变形 |
🔗 认知映射
源代码 运行时
点云(1003点) → 高斯集合(1003个)
每个点: [x,y,z,rgb] → 每个高斯: [位置, 协方差, 颜色, 不透明度, SH]
↓
理解:1个点 = 1个高斯
初始化是"一对一"的简单映射
高斯的初始大小通常设置为很小的值(0.01左右)
🔷 观察2:示踪剂高斯的追踪
👁️ 观测结果
# 在训练循环中添加打印
def get_tracer_info(self):
"""找到示踪剂高斯(特殊颜色/位置)"""
# 找到品红色高斯(示踪剂)
tracer_mask = (self._features_dc[:, 0, 0] > 0.9) & \
(self._features_dc[:, 0, 1] < 0.1) & \
(self._features_dc[:, 0, 2] > 0.9)
tracer_idx = tracer_mask.nonzero(as_tuple=True)[0]
print(f"[TRACER] Found {len(tracer_idx)} tracer gaussians")
# 打印示踪剂的参数
for idx in tracer_idx:
print(f"[TRACER] Gaussian {idx}:")
print(f" Position: {self._xyz[idx].detach().cpu().numpy()}")
print(f" Scale: {self._scaling[idx].detach().cpu().numpy()}")
print(f" Opacity: {self._opacity[idx].detach().cpu().numpy()}")
📊 冰山下的知识
| 观察项 | 预期值 | 实际值 | 理解 |
|---|---|---|---|
| 示踪剂位置 | [1.234, 5.678, 9.012] | 保持不变 | 位置会被训练更新 |
| 示踪剂颜色 | [1.0, 0.0, 1.0] | 可能变化 | 颜色会被优化 |
| 示踪剂scale | 0.01 | 可能变大/变小 | 高斯会自适应调整 |
🔗 认知映射
初始高斯 训练中 训练后
固定大小/颜色 → 优化更新(梯度下降) → 自适应大小/形状
↓
理解:高斯参数是可学习的
通过梯度反向传播自动调整
示踪剂帮助追踪这个变化过程
🎯 这个实验能解决什么问题?
问题1:初始化质量差
# 检查点云质量
meshlab point_cloud.ply
# 如果点云稀疏/噪声大 → 初始化会很差
# 解决:
# 1. 重新运行SfM(提高图像数量/质量)
# 2. 调整初始scale参数
问题2:高斯数量爆炸
# 查看高斯数量变化
grep "Number of gaussians" log.txt
# 输出:
Iteration 100: 1003 gaussians
Iteration 1000: 50000 gaussians ← 爆炸!
Iteration 7000: 300000 gaussians ← 崩溃
# 理解:密集化策略太激进
# 解决:调整densify_until_iter, densify_threshold
实验2:投影变换(理解协方差如何变换)
🎯 为什么要学这个?
现实痛点:
- 渲染时高斯形状异常(拉伸/旋转)
- 不理解为什么3D高斯在不同视角看起来不一样
- 想理解协方差矩阵的变换公式
学习目标:理解"世界空间协方差"如何变成"屏幕空间协方差"
📝 准备示踪剂
示踪剂设计:特殊形状的高斯
# 在gaussian_model/gaussian_renderer.py中修改
def render(viewpoint_camera, pc: GaussianModel):
"""
渲染函数,追踪投影变换
"""
# 创建示踪剂:选择几个特殊高斯
tracer_indices = [0, 100, 500] # 选择前几个高斯作为示踪剂
# 记录变换前的协方差(世界空间)
print(f"[TRACER] ========== Covariance Transform ==========")
for idx in tracer_indices:
cov3d = pc.get_covariance(idx).detach().cpu().numpy()
print(f"[TRACER] Gaussian {idx} (world space):")
print(f" Cov3d:\n{cov3d}")
# 投影变换(原代码)
rendered_image, radii = rasterizer(
means3D=means3D,
means2D=means2D,
cov2d=cov2d, # ← 变换后的2D协方差
...
)
# 记录变换后的协方差(屏幕空间)
print(f"[TRACER] After transform (screen space):")
for idx in tracer_indices:
# 打印对应的2D协方差
print(f"[TRACER] Gaussian {idx} (screen space):")
print(f" Cov2d: {cov2d[idx].detach().cpu().numpy()}")
return rendered_image, radii
💉 执行示踪
# 修改代码后运行训练
python train.py -s data/scene -m output/tracer_cov 2>&1 | tee tracer_cov.log
# 搜索示踪剂输出
grep -A 10 "Covariance Transform" tracer_cov.log
👀 观察分析
🔷 观察1:协方差矩阵的变换
👁️ 观测结果
[TRACER] Gaussian 0 (world space):
Cov3d:
[[0.01 0. 0. ]
[0. 0.01 0. ]
[0. 0. 0.01]]
[TRACER] Gaussian 0 (screen space):
Cov2d: [[25.3 -2.1]
[-2.1 18.7]]
[TRACER] ========== Camera View ==========
Position: [1.5, 2.0, 3.0]
Rotation: [[0.9, -0.1, 0.0], ...]
📊 冰山下的知识
| 概念 | 世界空间 | 屏幕空间 | 变换原因 |
|---|---|---|---|
| 协方差 | 3x3矩阵(球形) | 2x2矩阵(椭圆) | 投影+视角变换 |
| 对角线 | 0.01(单位球) | 25.3, 18.7(拉伸椭圆) | 近大远小透视 |
| 非对角线 | 0(无旋转) | -2.1(有旋转) | 视角倾斜导致旋转 |
🔗 认知映射
世界空间(3D) 屏幕空间(2D)
Σ_world (3x3) → Σ_view (2x2)
球形高斯 椭圆形高斯
↓
理解:Σ_view = J^T Σ_world J
J是投影变换的雅可比矩阵
包含:相机内参 + 外参(位姿)
变换后:圆形 → 椭圆(可能拉伸/旋转)
🔷 观察2:不同视角下的协方差
👁️ 观测结果(改变相机视角)
# 视角1:正面
[TRACER] View 0 (front):
Cov2d: [[25.3 -2.1]
[-2.1 18.7]]
# 视角2:侧面(旋转90度)
[TRACER] View 1 (side):
Cov2d: [[8.2 0.5]
[0.5 30.1]] ← 形状完全不同!
# 视角3:俯视(从上往下)
[TRACER] View 2 (top):
Cov2d: [[15.6 1.2]
[1.2 5.4]] ← 压扁的椭圆
📊 冰山下的知识
| 视角 | 观察现象 | 原因 |
|---|---|---|
| 正面 | 圆形/椭圆 | 标准投影 |
| 侧面 | 拉长的椭圆 | 透视变形 |
| 俯视 | 压扁的椭圆 | 观察角度导致 |
🔗 认知映射
同一个3D高斯 不同视角 不同的2D投影
↓
理解:高斯的"形状"是视角相关的
不是固定不变
这就是为什么3DGS能表示复杂场景
每个高斯可以从任意角度观察
🎯 这个实验能解决什么问题?
问题1:渲染时高斯变形严重
# 检查协方差
grep "Cov2d" log.txt
# 如果协方差矩阵数值过大(>1000)
# → 说明高斯被过度拉伸
# 解决:调整初始scale、增加正则化
问题2:不同视角质量不一致
# 检查不同视角的协方差
# 如果某些视角协方差异常(过大/过小)
# → 可能相机位姿有问题
# 解决:重新标定相机
实验3:光栅化过程(理解Alpha Blending)
🎯 为什么要学这个?
现实痛点:
- 不理解渲染顺序对结果的影响
- 半透明效果不正确
- 想理解颜色如何累加
学习目标:理解"排序后的高斯"如何通过"Alpha Blending"变成"像素颜色"
📝 准备示踪剂
在CUDA Kernel中插入示踪代码
// 在 rasterize_forward.cu 中修改
__global__ void rasterize_forward_kernel(...) {
// 示踪剂:选择几个特殊像素
if (blockIdx.x == 0 && threadIdx.x == 0) { // 第一个block的第一个线程
printf("[TRACER] ========== Rasterization ==========\n");
}
// 对于每个像素
int pixel_id = threadIdx.x;
if (pixel_id == 100) { // 示踪像素(比如第100个像素)
printf("[TRACER] Pixel %d:\n", pixel_id);
// 打印覆盖这个像素的所有高斯
int num_gaussians = 0;
float accumulated_alpha = 0.0;
float3 final_color = make_float3(0.0, 0.0, 0.0);
// 遍历排序后的高斯
for (int g_idx = 0; g_idx < num_sorted_gaussians; g_idx++) {
int gaussian_id = sorted_gaussians[g_idx];
// 计算这个高斯对该像素的贡献
float2 xy = means2d[gaussian_id];
float3 color = colors[gaussian_id];
float alpha = alphas[gaussian_id];
// 打印前几个高斯的信息
if (num_gaussians < 5) {
printf("[TRACER] Gaussian %d: pos=(%.2f,%.2f), alpha=%.4f, color=(%.2f,%.2f,%.2f)\n",
gaussian_id, xy.x, xy.y, alpha, color.x, color.y, color.z);
}
// Alpha Blending累加
float w = alpha * (1.0 - accumulated_alpha);
final_color += w * color;
accumulated_alpha += w;
num_gaussians++;
// 如果alpha接近1,提前终止(完全不透明)
if (accumulated_alpha > 0.99) {
printf("[TRACER] Early stop at gaussian %d (alpha=%.4f)\n",
gaussian_id, accumulated_alpha);
break;
}
}
printf("[TRACER] Total gaussians: %d\n", num_gaussians);
printf("[TRACER] Final alpha: %.4f\n", accumulated_alpha);
printf("[TRACER] Final color: (%.2f, %.2f, %.2f)\n",
final_color.x, final_color.y, final_color.z);
}
// ... 原有渲染代码 ...
}
💉 执行示踪
# 编译CUDA代码
python setup.py build_ext --inplace
# 运行训练,捕获CUDA输出
python train.py -s data/scene -m output/tracer_rast 2>&1 | tee tracer_rast.log
# 搜索示踪剂输出
grep "TRACER" tracer_rast.log
👀 观察分析
🔷 观察1:Alpha Blending的累加过程
👁️ 观测结果
[TRACER] Pixel 100:
[TRACER] Gaussian 45: pos=(125.30,240.15), alpha=0.8234, color=(0.85,0.42,0.31)
[TRACER] Gaussian 78: pos=(126.10,239.80), alpha=0.4512, color=(0.12,0.78,0.55)
[TRACER] Gaussian 23: pos=(124.95,241.20), alpha=0.2345, color=(0.93,0.21,0.67)
[TRACER] Gaussian 156: pos=(125.80,240.50), alpha=0.1123, color=(0.34,0.89,0.12)
[TRACER] Gaussian 201: pos=(125.40,240.90), alpha=0.0456, color=(0.67,0.34,0.91)
[TRACER] Total gaussians: 15
[TRACER] Final alpha: 0.9876
[TRACER] Final color: (0.72, 0.51, 0.43)
📊 冰山下的知识
| 概念 | 值 | 含义 |
|---|---|---|
| 排序 | 按深度从后到前 | 保证后面的先绘制 |
| 累加 | 从后往前逐个叠加 | 每层受前面层影响 |
| Alpha | 0.8234, 0.4512… | 不透明度 |
| 最终alpha | 0.9876 | 接近1 = 几乎不透明 |
| 最终颜色 | (0.72, 0.51, 0.43) | 加权混合结果 |
🔗 认知映射
排序后高斯(后→前) Alpha Blending算法
G3 → G2 → G1 → C = 0
α = 0
C += α3 * (1-α) * C3
α += α3 * (1-α)
C += α2 * (1-α) * C2
α += α2 * (1-α)
C += α1 * (1-α) * C1
α += α1 * (1-α)
↓
理解:从后往前累加
每层对最终颜色的贡献 = 自身alpha * (1 - 已累加alpha)
这模拟了光线穿过半透明物体的物理过程
🔷 观察2:不透明度的累积
👁️ 观测结果(跟踪alpha变化)
[TRACER] Pixel 100 (detailed alpha accumulation):
[TRACER] After gaussian 45: alpha = 0.8234 (added 0.8234)
[TRACER] After gaussian 78: alpha = 0.9023 (added 0.0789)
[TRACER] After gaussian 23: alpha = 0.9487 (added 0.0464)
[TRACER] After gaussian 156: alpha = 0.9734 (added 0.0247)
[TRACER] After gaussian 201: alpha = 0.9876 (added 0.0142)
📊 冰山下的知识
| 累积阶段 | Alpha值 | 新增贡献 | 理解 |
|---|---|---|---|
| 第1个高斯 | 0.8234 | 82.34% | 主导颜色 |
| 第2个高斯 | 0.9023 | 7.89% | 轻微修饰 |
| 第3个高斯 | 0.9487 | 4.64% | 更小影响 |
| 后续高斯 | → 0.9876 | 递减 | 边缘效应 |
🔗 认知映射
累加公式:
α_total = Σ α_i * Π(1 - α_j) for j < i
第1个:α_1
第2个:α_1 + α_2 * (1 - α_1) ← 第2个的贡献被第1个"遮挡"了
第3个:α_1 + α_2 * (1 - α_1) + α_3 * (1 - α_1)(1 - α_2)
↓
理解:前面的高斯会"遮挡"后面的高斯
越往后,新增的贡献越小
这就是排序的重要性!
🎯 这个实验能解决什么问题?
问题1:渲染顺序错误
// 如果不排序(随机顺序)
// → 颜色混合错误
// → 前面的物体可能被后面的遮挡
// 解决:必须按深度(Z值)从后往前排序
问题2:半透明效果不正确
# 如果最终alpha异常(< 0.8 或 > 1.0)
# → 检查单个高斯的alpha值
# → 可能需要调整透明度参数
实验4:梯度反传(理解参数如何优化)
🎯 为什么要学这个?
现实痛点:
- 不知道参数如何更新
- 梯度爆炸/消失
- 想理解自适应优化原理
学习目标:理解"渲染损失"如何反向传播到"高斯参数"
📝 准备示踪剂
用PyTorch的autograd追踪梯度
# 在train.py中修改
def training_step(model, viewpoint_cam, iteration):
"""
训练步骤,追踪梯度
"""
# 前向渲染
rendered_image, radii = renderer(viewpoint_cam, pc=model)
# 计算损失
gt_image = viewpoint_cam.original_image
loss = l1_loss + lambda_dssim * (1.0 - ssim)
# 示踪剂:选择几个高斯,追踪其梯度
tracer_indices = [0, 100, 500]
print(f"[TRACER] ========== Gradient Backward ==========")
print(f"[TRACER] Loss: {loss.item():.6f}")
# 反向传播
loss.backward()
# 打印梯度统计
for idx in tracer_indices:
# 位置梯度
xyz_grad = model._xyz.grad[idx].detach().cpu().numpy()
print(f"[TRACER] Gaussian {idx}:")
print(f" Position gradient: {xyz_grad}")
print(f" Gradient norm: {np.linalg.norm(xyz_grad):.6f}")
# 协方差梯度
scale_grad = model._scaling.grad[idx].detach().cpu().numpy()
print(f" Scale gradient: {scale_grad}")
# 颜色梯度
color_grad = model._features_dc.grad[idx].detach().cpu().numpy()
print(f" Color gradient: {color_grad}")
# 优化器更新
optimizer.step()
optimizer.zero_grad()
return loss.item()
💉 执行示踪
# 运行训练
python train.py -s data/scene -m output/tracer_grad 2>&1 | tee tracer_grad.log
# 搜索梯度输出
grep -A 15 "Gradient Backward" tracer_grad.log
👀 观察分析
🔷 观察1:梯度的分布
👁️ 观测结果
[TRACER] ========== Gradient Backward ==========
[TRACER] Loss: 0.023456
[TRACER] Gaussian 0:
[TRACER] Position gradient: [0.0123, -0.0234, 0.0456]
[TRACER] Gradient norm: 0.0523
[TRACER] Scale gradient: [0.0023, -0.0012, 0.0034]
[TRACER] Color gradient: [[0.0123, -0.0234, 0.0345]]
[TRACER] Gaussian 100:
[TRACER] Position gradient: [-0.0345, 0.0123, -0.0234]
[TRACER] Gradient norm: 0.0432
[TRACER] Scale gradient: [-0.0045, 0.0023, -0.0012]
[TRACER] Color gradient: [[-0.0234, 0.0123, -0.0456]]
📊 冰山下的知识
| 观察项 | 值 | 理解 |
|---|---|---|
| 位置梯度 | [0.0123, -0.0234, 0.0456] | 应该往这个方向移动 |
| 梯度模长 | 0.0523 | 移动步长 |
| 符号 | 正/负 | 增加/减少参数 |
🔗 认知映射
损失函数 梯度 参数更新
L = 0.023456 → ∂L/∂x = 0.0123 → x_new = x_old - lr * 0.0123
↓
理解:梯度告诉参数"往哪个方向移动能减小损失"
学习率(lr)控制移动步长
示踪剂帮助追踪每个参数的更新方向
🔷 观察2:自适应优化的触发
👁️ 观测结果(密集化/剪枝)
[TRACER] Iteration 1000:
[TRACER] Densification check:
[TRACER] - Gaussians with large gradient: 234 (threshold=0.01)
[TRACER] - Will densify: 234 gaussians (clone + split)
[TRACER] - Gaussians with small opacity: 56 (threshold=0.01)
[TRACER] - Will prune: 56 gaussians
[TRACER] Current gaussians: 50000
[TRACER] After densification: 100234
📊 冰山下的知识
| 策略 | 触发条件 | 动作 | 效果 |
|---|---|---|---|
| Clone | 梯度大 & 尺寸小 | 复制高斯 | 增加细节 |
| Split | 梯度大 & 尺寸大 | 分裂高斯 | 增加表达能力 |
| Prune | 不透明度小 | 删除高斯 | 减少冗余 |
| Reset | 视角外推 | 重置高斯 | 避免过拟合 |
🔗 认知映射
梯度大(信息不足) 密集化(Clone/Split)
↓
增加高斯数量 → 提高表达能力
↓
梯度小(冗余) 剪枝(Prune)
↓
减少高斯数量 → 提高效率
↓
理解:自适应优化根据梯度动态调整高斯数量
梯度大 = 需要更多高斯
梯度小 = 可以删除一些高斯
🎯 这个实验能解决什么问题?
问题1:训练不收敛
# 检查梯度
grep "Gradient norm" log.txt
# 如果梯度模长异常(>1.0 或接近0)
# → 学习率太大/太小
# 解决:调整learning_rate
问题2:高斯数量爆炸
# 检查密集化统计
grep "After densification" log.txt
# 如果每次迭代都增加大量高斯
# → 密集化阈值太低
# 解决:调整densify_grad_threshold
实验5:破坏性验证(边界测试)
🎯 为什么要破坏?
建立认知模型后,必须验证:
- 模型正确吗?
- 边界在哪里?
- 异常情况会怎样?
破坏性测试 = 找到系统的真实边界
📝 准备破坏实验
破坏1:禁用排序
// 在rasterize_forward.cu中修改
// 原代码:按深度排序
// std::sort(gaussians.begin(), gaussians.end(), depth_comparator);
// 破坏:随机顺序
std::random_shuffle(gaussians.begin(), gaussians.end());
破坏2:固定协方差
# 在gaussian_model/__init__.py中修改
# 原代码:可学习的协方差
# self._scaling = nn.Parameter(scale)
# 破坏:固定为单位矩阵
self._scaling = torch.zeros_like(scale) # log(1.0) = 0
self._scaling.requires_grad = False # 不可学习
破坏3:禁用自适应优化
# 在train.py中修改
# 原代码:
# if iteration < densify_until_iter:
# densify_and_prune(...)
# 破坏:注释掉
# densify_and_prune(...) # 禁用密集化/剪枝
💉 执行破坏
# 破坏1:禁用排序
python train.py --disable_sort -s data/scene -m output/break1_sort
# 破坏2:固定协方差
python train.py --fixed_cov -s data/scene -m output/break2_cov
# 破坏3:禁用自适应
python train.py --disable_adaptive -s data/scene -m output/break3_adapt
👀 观察结果
🔷 破坏1:禁用排序
👁️ 观测结果
# 正常渲染(排序)
PSNR: 28.5 dB
SSIM: 0.923
# 破坏渲染(随机顺序)
PSNR: 18.2 dB ← 质量大幅下降!
SSIM: 0.756
Visual: 伪影严重,颜色混乱
📊 冰山下的知识
| 破坏 | 现象 | 根本原因 | 理解 |
|---|---|---|---|
| 禁用排序 | 渲染错误 | Alpha Blending依赖顺序 | 排序是必须的 |
🔗 认知映射
正常:排序 → 后→前渲染 → 正确的遮挡关系
破坏:随机 → 任意顺序渲染 → 错误的颜色混合
↓
理解:排序不是优化,是正确性的前提
Alpha Blending必须按深度顺序
🔷 破坏2:固定协方差
👁️ 观测结果
# 正常(可学习协方差)
PSNR: 28.5 dB
Visual: 细节清晰
# 破坏(固定协方差)
PSNR: 22.3 dB ← 质量下降
Visual: 模糊,细节丢失
📊 冰山下的知识
| 破坏 | 现象 | 根本原因 | 理解 |
|---|---|---|---|
| 固定协方差 | 模糊 | 高斯无法适应几何 | 协方差必须可学习 |
🔗 认知映射
正常:可学习协方差 → 高斯拉伸/旋转 → 适应场景几何
破坏:固定协方差 → 所有高斯都是球形 → 无法表示细节
↓
理解:协方差是3DGS的核心优势
允许每个高斯自适应变形
🔷 破坏3:禁用自适应优化
👁️ 观测结果
# 正常(自适应优化)
Iteration 1000: 50000 gaussians
Iteration 7000: 300000 gaussians
PSNR: 28.5 dB
# 破坏(固定高斯数量)
Iteration 1000: 10000 gaussians ← 数量不变
Iteration 7000: 10000 gaussians
PSNR: 24.1 dB ← 质量受限
Visual: 细节丢失
📊 冰山下的知识
| 破坏 | 现象 | 根本原因 | 理解 |
|---|---|---|---|
| 禁用自适应 | 质量受限 | 高斯数量固定 | 自适应是必须的 |
🔗 认知映射
正常:自适应 → 根据梯度增减高斯 → 最优数量
破坏:固定 → 初始数量固定 → 无法优化细节
↓
理解:自适应优化是3DGS的核心
动态调整高斯数量和质量
📚 推荐学习路径
根据你的背景,选择适合的路径:
路径1:快速理解(1天)
目标:快速掌握3DGS的核心流程
推荐顺序:
- 实验1(初始化) → 理解点云→高斯
- 实验3(光栅化) → 理解Alpha Blending
- 实验5(破坏) → 验证理解
工具箱:
# 必备工具
meshlab # 查看点云
grep/tee # 日志搜索
python logging # 打印追踪
路径2:深入底层(3天)
目标:理解CUDA渲染和GPU执行
推荐顺序:
- 实验1(初始化) → Python层
- 实验2(投影) → 数学变换
- 实验3(光栅化) → CUDA kernel
- 实验4(梯度) → 优化过程
- Nsight分析 → GPU性能
工具箱:
# 完整工具
NVIDIA Nsight Compute # Kernel性能分析
NVIDIA Nsight Graphics # 渲染管线追踪
cuda-memcheck # 内存检查
nvprof # 性能剖析
路径3:算法改进(1周)
目标:能够魔改和优化3DGS
推荐顺序:
- 完整执行实验1-5 → 建立完整认知
- 阅读论文附录 → 数学推导
- 修改代码 → 实现新想法
- 对比实验 → 验证改进
研究方向:
- 添加新的正则化(如平滑约束)
- 改进自适应策略(如动态阈值)
- 优化内存占用(如稀疏存储)
- 加速渲染(如kernel融合)
🎯 关键认知映射总结
通过以上实验,建立的核心映射:
层级1: 输入数据 运行时
图像(.jpg) + 相机参数 → PyTorch Tensor (RGB图像)
层级2: 初始化
SfM点云(.ply) → 3D高斯集合(位置、协方差、颜色)
层级3: Python训练
训练循环 → 迭代优化(前向→损失→反向→更新)
层级4: CUDA渲染
世界空间高斯 → 屏幕空间高斯(投影+排序+光栅化)
层级5: GPU执行
CUDA Kernel → GPU线程执行(Warp调度)
💡 核心原则
主动观测 > 被动阅读 文档告诉你"应该是什么",实验展示"实际是什么"
示踪剂必须独特
- ❌ 普通颜色
[0.5, 0.5, 0.5] - ✅ 魔术值
[1.234, 5.678, 9.012] - ✅ 特殊颜色
[1.0, 0.0, 1.0](品红)
- ❌ 普通颜色
一次只探一层 不要试图一步到位理解整个栈 输入 → 关卡 → 输出,专注一个数据突变点
相信观测,质疑假设 如果现象和论文矛盾,以观测为准
必须破坏性验证 正常场景看不出边界,异常场景才能暴露真实行为
记住:罗塞塔石碑方法的核心是对照。通过已知的输入和未知的底层输出,建立映射关系,就能破译3D Gaussian Splatting这个"黑盒系统"。
现在,开始你的第一个示踪实验吧!