3D Gaussian Splatting:渲染流程
1. 主程序入口 (if __name__ == "__main__")
程序从主入口开始执行:
pos = torch.load('trained_gaussians/kitchen/pos_7000.pt').cuda()
opacity_raw = torch.load('trained_gaussians/kitchen/opacity_raw_7000.pt').cuda()
f_dc = torch.load('trained_gaussians/kitchen/f_dc_7000.pt').cuda()
f_rest = torch.load('trained_gaussians/kitchen/f_rest_7000.pt').cuda()
scale_raw = torch.load('trained_gaussians/kitchen/scale_raw_7000.pt').cuda()
q_raw = torch.load('trained_gaussians/kitchen/q_rot_7000.pt').cuda()
cam_parameters = np.load('out_colmap/kitchen/cam_meta.npy', allow_pickle=True).item()
orbit_c2ws = torch.load('camera_trajectories/kitchen_orbit.pt').cuda()
如果不能用cuda则可以用cpu来实现:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
pos = torch.load('trained_gaussians/kitchen/pos_7000.pt', map_location=device)
opacity_raw = torch.load('trained_gaussians/kitchen/opacity_raw_7000.pt', map_location=device)
f_dc = torch.load('trained_gaussians/kitchen/f_dc_7000.pt', map_location=device)
f_rest = torch.load('trained_gaussians/kitchen/f_rest_7000.pt', map_location=device)
scale_raw = torch.load('trained_gaussians/kitchen/scale_raw_7000.pt', map_location=device)
q_raw = torch.load('trained_gaussians/kitchen/q_rot_7000.pt', map_location=device)
cam_parameters = np.load('out_colmap/kitchen/cam_meta.npy', allow_pickle=True).item()
orbit_c2ws = torch.load('camera_trajectories/kitchen_orbit.pt', map_location=device)
加载预训练的3D高斯参数和相机轨迹数据:
pos: 3D高斯点的位置坐标
opacity_raw: 原始透明度参数
f_dc: 球谐函数直流分量系数
f_rest: 球谐函数交流分量系数
scale_raw: 原始尺度参数
q_raw: 四元数旋转参数
cam_parameters: 相机内参
orbit_c2ws: 相机轨迹变换矩阵
sigma = build_sigma_from_params(scale_raw, q_raw)
根据尺度和旋转参数构建3D高斯的协方差矩阵。
2. 渲染循环
with torch.no_grad():
for i, c2w_i in tqdm(enumerate(orbit_c2ws)):
对每个相机位姿进行渲染,使用tqdm显示进度条,torch.no_grad()禁用梯度计算以提高性能。
3. 相机参数设置
c2w = c2w_i
H = cam_parameters['height'] // 2
W = cam_parameters['width'] // 2
H_src = cam_parameters['height']
W_src = cam_parameters['width']
fx, fy = cam_parameters['fx'], cam_parameters['fy']
cx, cy = W_src / 2, H_src / 2
fx, fy, cx, cy = scale_intrinsics(H, W, H_src, W_src, fx, fy, cx, cy)
设置当前帧的相机参数,包括图像尺寸和相机内参,并根据目标分辨率调整内参。
4. 颜色计算
color = evaluate_sh(f_dc, f_rest, pos, c2w)
调用evaluate_sh函数计算每个3D高斯点在当前相机视角下的颜色。这个函数根据观察方向计算球谐函数基函数,然后与系数相乘得到最终颜色。
球谐函数(Spherical Harmonics)在3DGS中的作用
1. 视点相关着色(View-dependent Shading)
在传统的点云渲染中,每个点通常只有一个固定颜色。但真实世界中的物体表面会根据观察角度呈现不同的颜色(比如镜面反射、法线变化等)。球谐函数允许我们表示这种与视点相关的着色效果。
2. 直流分量(DC Component)和交流分量(AC Components)
直流分量 (f_dc)
对应球谐函数的第0阶基函数(Y0)
表示与视角无关的基础颜色
是一个常数项,所有方向上的贡献相同
相当于传统渲染中的"漫反射颜色"
交流分量 (f_rest)
对应球谐函数的第1、2、3阶基函数(Y1-Y15)
表示与视角相关的颜色变化
捕捉光照和观察角度引起的颜色变化
总共15个系数(3个颜色通道 × 15个基函数 = 45个参数)
3. 球谐函数基函数
代码中的 evaluate_sh 函数计算了16个球谐基函数:
Y0 = torch.full_like(x, SH_C0) # 常数项(直流分量)
Y1 = - SH_C1_y * y # 1阶基函数
Y2 = SH_C1_z * z
Y3 = - SH_C1_x * x
Y4-Y15 = ... # 2阶和3阶基函数
4. 颜色计算过程
最终颜色通过以下方式计算:
# 构建球谐系数张量
sh[:, 0] = f_dc # 直流分量
sh[:, 1:, 0] = f_rest[:, :15] # R通道的交流分量
sh[:, 1:, 1] = f_rest[:, 15:30] # G通道的交流分量
sh[:, 1:, 2] = f_rest[:, 30:45] # B通道的交流分量
# 计算最终颜色
color = torch.sigmoid((sh * Y.unsqueeze(2)).sum(dim=1))
这相当于:
R = f_dc_r * Y0 + f_rest_r1 * Y1 + f_rest_r2 * Y2 + ... + f_rest_r15 * Y15
G = f_dc_g * Y0 + f_rest_g1 * Y1 + f_rest_g2 * Y2 + ... + f_rest_g15 * Y15
B = f_dc_b * Y0 + f_rest_b1 * Y1 + f_rest_b2 * Y2 + ... + f_rest_b15 * Y15
优势:
紧凑表示:用少量系数(48个)表示复杂的视点相关着色
旋转不变性:球谐函数在旋转时有良好的数学性质
高效计算:比存储每个方向的颜色值更高效
平滑过渡:自然地插值视角变化引起的效果
5. 核心渲染
img = render(pos, color, opacity_raw, sigma, c2w, H, W, fx, fy, cx, cy)
调用核心渲染函数,传入所有必要参数生成最终图像。
核心渲染函数详解
第一步:点投影和筛选
uv, x, y, z = project_points(pos, c2w, fx, fy, cx, cy)
in_guard = (uv[:, 0] > -pix_guard) & (uv[:, 0] < W + pix_guard) & (
uv[:, 1] > -pix_guard) & (uv[:, 1] < H + pix_guard) & (z > near) & (z < far)
将所有3D点投影到2D图像平面,并筛选在视锥体和图像边界内的点。
第二步:协方差矩阵投影
# Project the covariance
Rcw = c2w[:3, :3]
Rwc = Rcw.t()
invz = 1 / z.clamp_min(1e-6)
invz2 = invz * invz
J = torch.zeros((pos.shape[0], 2, 3), device=pos.device, dtype=pos.dtype)
J[:, 0, 0] = fx * invz
J[:, 1, 1] = fy * invz
J[:, 0, 2] = -fx * x * invz2
J[:, 1, 2] = -fy * y * invz2
tmp = Rwc.unsqueeze(0) @ sigma @ Rwc.t().unsqueeze(0) # Eq. 5
sigma_camera = J @ tmp @ J.transpose(1, 2)
将3D协方差矩阵投影到2D图像平面,得到2D高斯的协方差矩阵。这里使用了雅可比矩阵J进行变换。
第三步:正定性处理和排序
# Ensure positive definiteness
evals, evecs = torch.linalg.eigh(sigma_camera)
evals = torch.clamp(evals, min=1e-6, max=1e4)
sigma_camera = evecs @ torch.diag_embed(evals) @ evecs.transpose(1, 2)
# Global depth sorting
order = torch.argsort(z, descending=False)
确保投影后的协方差矩阵是正定的,并按深度对点进行排序(从近到远)。
第四步:瓦片划分(Tiling)
# Tiling
major_variance = evals[:, 1].clamp_min(1e-12).clamp_max(1e4) # [N]
radius = torch.ceil(3.0 * torch.sqrt(major_variance)).to(torch.int64)
umin = torch.floor(u - radius).to(torch.int64)
umax = torch.floor(u + radius).to(torch.int64)
vmin = torch.floor(v - radius).to(torch.int64)
vmax = torch.floor(v + radius).to(torch.int64)
计算每个2D高斯的影响半径,并确定其在图像上的轴对齐包围盒(AABB)。
# Tile index for each AABB
umin_tile = (umin // T).to(torch.int64) # [N]
umax_tile = (umax // T).to(torch.int64) # [N]
vmin_tile = (vmin // T).to(torch.int64) # [N]
vmax_tile = (vmax // T).to(torch.int64) # [N]
将图像划分为T×T大小的瓦片,并计算每个高斯影响的瓦片范围。
第五步:瓦片-高斯映射
nb_tiles_per_gaussian = n_u * n_v # [N]
gaussian_ids = torch.repeat_interleave(
torch.arange(nb_gaussians, device=pos.device, dtype=torch.int64),
nb_tiles_per_gaussian) # [0, 0, 0, 0, 1 ...]
建立瓦片和高斯点之间的映射关系,优化渲染性能。
第六步:瓦片排序
idx_z_order = torch.arange(nb_gaussians, device=pos.device, dtype=torch.int64)
M = nb_gaussians + 1
comp = flat_tile_id * M + idx_z_order[gaussian_ids]
comp_sorted, perm = torch.sort(comp)
gaussian_ids = gaussian_ids[perm]
tile_ids_1d = torch.div(comp_sorted, M, rounding_mode='floor')
按瓦片ID和深度对高斯点进行排序,确保正确的渲染顺序。
第七步:逐瓦片渲染
for tile_id, s0, s1 in zip(unique_tile_ids.tolist(), start.tolist(), end.tolist()):
遍历每个瓦片,只处理影响当前瓦片的高斯点。
du = px_u.unsqueeze(0) - gaussian_i_u.unsqueeze(-1) # [N, T * T]
dv = px_v.unsqueeze(0) - gaussian_i_v.unsqueeze(-1) # [N, T * T]
A11 = gaussian_i_inverse_covariance[:, 0, 0].unsqueeze(-1) # [N, 1]
A12 = gaussian_i_inverse_covariance[:, 0, 1].unsqueeze(-1)
A22 = gaussian_i_inverse_covariance[:, 1, 1].unsqueeze(-1)
q = A11 * du * du + 2 * A12 * du * dv + A22 * dv * dv # [N, T * T]
计算瓦片内每个像素到高斯中心的距离(二次型形式)。
inside = q <= chi_square_clip
g = torch.exp(-0.5 * torch.clamp(q, max=chi_square_clip)) # [N, T * T]
g = torch.where(inside, g, torch.zeros_like(g))
alpha_i = (gaussian_i_opacity.unsqueeze(-1) * g).clamp_max(alpha_max) # [N, T * T]
alpha_i = torch.where(alpha_i >= alpha_cutoff, alpha_i, torch.zeros_like(alpha_i))
计算每个像素受高斯影响的alpha值,使用指数衰减函数和截断。
T_i = torch.cumprod(one_minus_alpha_i, dim=0)
T_i = torch.concatenate([
torch.ones((1, alpha_i.shape[-1]), device=pos.device, dtype=pos.dtype),
T_i[:-1]], dim=0)
alive = (T_i > 1e-4).float()
w = alpha_i * T_i * alive # [N, T * T]
使用累积透射率公式计算最终权重,实现alpha混合。
final_image[pixel_idx_1d] = (w.unsqueeze(-1) * gaussian_i_color.unsqueeze(1)).sum(dim=0)
将加权颜色值累加到最终图像中。
第八步:输出结果
return final_image.reshape((H, W, 3)).clamp(0, 1)
将一维图像数组重塑为二维,并将值限制在[0,1]范围内。
Image.fromarray((img.cpu().detach().numpy() * 255).astype(np.uint8)).save(f'novel_views/frame_{i:04d}.png')
将渲染结果保存为PNG图像文件。
3DGS 训练过程产生的数据
这些 .pt 文件是通过 3D Gaussian Splatting 训练过程生成的,具体来源于:
1. 训练输入数据
训练 3DGS 模型需要以下输入:
图像集合: 同一场景的多视角照片
相机参数: 每张照片对应的相机位姿和内参
稀疏点云: 通过 Structure-from-Motion (SfM) 方法(如 COLMAP)生成的初始点云
2. 训练输出数据(即 .pt 文件内容)
位置数据 (pos_7000.pt)
# 来源于:
# 1. 初始SfM点云中的3D点位置
# 2. 训练过程中优化得到的最终位置
pos = torch.tensor([[x1, y1, z1], # 第1个高斯点的3D坐标
[x2, y2, z2], # 第2个高斯点的3D坐标
...,
[xn, yn, zn]]) # 第n个高斯点的3D坐标
颜色数据 (f_dc_7000.pt 和 f_rest_7000.pt)
# 来源于:
# 1. 从输入图像中采样初始颜色
# 2. 训练过程中优化得到的球谐函数系数
f_dc = torch.tensor([[r1, g1, b1], # 第1个点的直流颜色分量
[r2, g2, b2], # 第2个点的直流颜色分量
...])
f_rest = torch.tensor([[c1_1, c1_2, ..., c1_45], # 第1个点的交流分量(球谐系数)
[c2_1, c2_2, ..., c2_45], # 第2个点的交流分量
...])
透明度数据 (opacity_raw_7000.pt)
# 来源于:
# 1. 训练过程中学习到的每个点的不透明度
# 2. 控制点对最终图像的贡献程度
opacity_raw = torch.tensor([o1, o2, ..., on]) # 每个点的原始透明度参数
尺度和旋转数据 (scale_raw_7000.pt 和 q_rot_7000.pt)
# 来源于:
# 1. 初始点云的分布特征
# 2. 训练过程中优化得到的协方差矩阵参数
scale_raw = torch.tensor([[sx1, sy1, sz1], # 第1个点的尺度参数
[sx2, sy2, sz2], # 第2个点的尺度参数
...])
q_rot = torch.tensor([[qx1, qy1, qz1, qw1], # 第1个点的四元数旋转
[qx2, qy2, qz2, qw2], # 第2个点的四元数旋转
...])
具体场景示例
以代码中的 "kitchen" 场景为例:
输入: 厨房场景的多张照片(可能几十到几百张)
处理: 使用 COLMAP 生成稀疏重建
训练: 3DGS 训练算法迭代优化约 7000 步(文件名中的 7000 表示训练步数)
输出: 生成这 6 个 .pt 文件,完整描述厨房场景的 3D 高斯表示
本文参考知乎:https://zhuanlan.zhihu.com/p/1969141577483030927
