4D Gaussian Splatting

1. GaussianModel:4D Gaussian 的参数

4DGS 参数示意

在代码里,GaussianModel 不是为每个 Gaussian 创建一个对象,而是把所有 Gaussian 的参数批量存成张量。

参数Shape含义
_xyz[N, 3]空间中心 x, y, z
_t[N, 1]时间中心 t
_scaling[N, 3]空间尺度,存的是 log-scale
_scaling_t[N, 1]时间尺度,存的是 log-scale
_rotation[N, 4]4D rotation 的左四元数;在 3D 模式下是 3D rotation 四元数
_rotation_r[N, 4]4D rotation 的右四元数,仅 rot_4d=True 时使用
_opacity[N, 1]不透明度,存的是 inverse-sigmoid 空间的值
_features_dc[N, 1, 3]SH / 4DSH 的 DC 颜色项
_features_rest[N, K-1, 3]其余 SH / 4DSH 系数

4DGS 相比 3DGS 多出来的核心参数是:

self._t
self._scaling_t
self._rotation_r

含义:

  • _t:这个 Gaussian 在时间轴上的中心。
  • _scaling_t:这个 Gaussian 在时间维度上的局部尺度。
  • _rotation_r:和 _rotation 一起构造完整 4D rotation,让 Gaussian 能在 (x, y, z, t) 空间中旋转,从而产生空间和时间的耦合。

左右四元数与 4D 协方差矩阵

3D rotation 可以用一个单位四元数表示;4D rotation 的自由度更多,代码使用一对单位四元数:

_rotation    # 左四元数
_rotation_r  # 右四元数

使用时会归一化为单位四元数。单位四元数表示纯旋转;如果不归一化,变换中会混入缩放,旋转矩阵就不再是正交矩阵。

需要注意:

  • 一个单位四元数有 3 个自由度。
  • 4D rotation 使用左右两个单位四元数,共 6 个旋转自由度。
  • 4D 对称协方差矩阵本身有 10 个独立元素;代码用 scaling_xyzt + 4D rotation 这种参数化方式来构造它。

4D 协方差构造链路:

scaling_xyzt = [sx, sy, sz, st]
R_4d = R(q_l, q_r)
Σ_4d = R_4d diag(sx², sy², sz², st²) R_4dᵀ

代码对应:

L = build_scaling_rotation_4d(scaling, rotation_l, rotation_r)
actual_covariance = L @ L.transpose(1, 2)

2. 4D Gaussian 如何切成当前帧 3D Gaussian

最重要的函数:

4d-gaussian-splatting/scene/gaussian_model.py
get_current_covariance_and_mean_offset()

核心变量:

dt = timestamp - self.get_t

dt 表示“当前渲染时间”和“这个 Gaussian 自己的中心时间”的差。

关键代码:

def build_covariance_from_scaling_rotation_4d(
    scaling,
    scaling_modifier,
    rotation_l,
    rotation_r,
    dt=0.0,
):
    L = build_scaling_rotation_4d(
        scaling_modifier * scaling,
        rotation_l,
        rotation_r,
    )
    actual_covariance = L @ L.transpose(1, 2)

    cov_11 = actual_covariance[:, :3, :3]   # Σ_xx:空间 xyz 的 covariance
    cov_12 = actual_covariance[:, 0:3, 3:4] # Σ_xt:空间和时间的耦合项
    cov_t = actual_covariance[:, 3:4, 3:4]  # Σ_tt:时间维度的 variance

    current_covariance = cov_11 - cov_12 @ cov_12.transpose(1, 2) / cov_t
    symm = strip_symmetric(current_covariance)

    if dt.shape[1] > 1:
        mean_offset = (cov_12.squeeze(-1) / cov_t.squeeze(-1))[:, None, :] * dt[..., None]
        mean_offset = mean_offset[..., None]
    else:
        mean_offset = cov_12.squeeze(-1) / cov_t.squeeze(-1) * dt

    return symm, mean_offset.squeeze(-1)

数学上,完整 4D Gaussian 可以分块写成:

Σ_4d =
[ Σ_xx  Σ_xt ]
[ Σ_tx  Σ_tt ]

给定当前时间 t 后,条件 3D Gaussian 为:

μ_x|t = μ_x + Σ_xt Σ_tt⁻¹ (t - μ_t)
Σ_x|t = Σ_xx - Σ_xt Σ_tt⁻¹ Σ_tx

代码中的:

current_covariance = Σ_x|t
mean_offset = Σ_xt Σ_tt⁻¹ (timestamp - self.get_t)

所以当前帧 3D 中心是:

当前帧 3D 中心 = _xyz + mean_offset

get_cov_t() 与时间可见性

get_cov_t() 用来计算某个 4D Gaussian 在世界时间轴上的方差:

cov_t = Var(t) = Σ_4d[3, 3]

它不是速度,也不是中心时间,而是控制这个 Gaussian 在时间维度上衰减速度的方差项。

时间可见性由 get_marginal_t() 计算:

sigma = self.get_cov_t()
torch.exp(-0.5 * (self.get_t - timestamp) ** 2 / sigma)

后续会和基础 opacity 相乘:

opacity_current = opacity_base × marginal_t(timestamp)

含义:

  • 当前时间越接近 Gaussian 的中心时间 _t,该 Gaussian 越可见。
  • 当前时间离 _t 越远,该 Gaussian 越透明。

3. 4DSH:颜色如何引入时间

i 个 Gaussian 的颜色同时依赖:

  • d:观察方向。
  • Δt:Gaussian 中心时间和当前渲染时间的差。

4DSH 的基函数为:

Z^m_{n,l}(t, θ, φ) = cos(2π n t / T) · Y^m_l(θ, φ)

其中:

  • Y^m_l(θ, φ) 处理观察方向。
  • cos(2π n t / T) 处理时间变化。

因此,4DSH 相比普通 SH 的核心变化是:基函数从只依赖方向的 Y_l^m(d),扩展为依赖方向和时间的 cos(...) · Y_l^m(d)。训练时学习的仍然是这些基函数前面的颜色系数。

代码对应:

utils/sh_utils.py
eval_shfs_4d()

默认 sh_degree=3sh_degree_t=2 时:

空间 SH basis 数 = (3 + 1)^2 = 16
时间 basis 数 = 3,即 n = 0, 1, 2
总通道数 = 16 × 3 = 48

颜色形式可以理解为:

RGB(d, Δt)
= C0(d)
+ cos(2π Δt / T) · C1(d)
+ cos(4π Δt / T) · C2(d)

4. 论文中的渲染公式

3DGS 中,一个 pixel 的最终颜色为:

C = Σ_i c_i α_i G_i^{2D}(x) Π_{j=1}^{i-1}(1 - α_j G_j^{2D}(x))

含义:

  • c_i:第 i 个 Gaussian 的颜色。
  • α_i:第 i 个 Gaussian 的基础 opacity。
  • G_i^{2D}(x):投影到屏幕后的 2D Gaussian 权重。
  • Π(...):前面 Gaussian 透过来的剩余透射率。

4DGS 将它扩展为依赖时间的渲染:

I(u, v, t)
= Σ_i p_i(u, v, t) α_i c_i(d, t)
  Π_{j=1}^{i-1}(1 - p_j(u, v, t) α_j)

其中 p_i(u, v, t) 是第 i 个 4D Gaussian 在当前像素和当前时间下的贡献。

进一步分解:

p_i(u, v, t) = p_i(t) · p_i(u, v | t)

也就是:

一个 4D Gaussian 在当前时间 t 的贡献
= 当前时间切片下的 3D Gaussian
× 这个 Gaussian 在时间 t 上的 marginal 权重

因此:

I(u, v, t)
= Σ_i p_i(t) p_i(u, v | t) α_i c_i(d, t)
  Π_{j=1}^{i-1}(1 - p_j(t) p_j(u, v | t) α_j)

5. 渲染过程

代码入口:

gaussian_renderer/__init__.py
render()

关键流程:

render()

拿当前 viewpoint_camera.timestamp

如果 compute_cov3D_python=True:
    pc.get_current_covariance_and_mean_offset()
    pc.get_marginal_t()
    在 Python 里得到当前 3D Gaussian

否则:
    把 4D 参数 ts / scales_t / rotation_r 连同 timestamp 交给 CUDA
    CUDA 内部得到当前 3D Gaussian

rasterizer 把当前 3D Gaussian 投影成 2D splat

alpha blending 得到 rendered image

默认配置一般是:

compute_cov3D_python: False
convert_SHs_python: False

因此,默认训练时:

  • 4D → 3D 的条件化主要在 CUDA 中完成。
  • 4DSH → RGB 的颜色求值主要在 CUDA 中完成。

6. 训练优化

6.1 参数优化

论文中优化主要依赖渲染损失。代码里的主 loss 是:

loss = (1 - λ_dssim) · L1 + λ_dssim · (1 - SSIM)

训练时每个样本包含:

  • gt_image
  • viewpoint_cam
  • viewpoint_cam.timestamp

训练流程:

DataLoader 抽取 batch

每个样本根据 timestamp 做 4D 切片

render 得到预测图像

和 GT image 计算 loss

反向传播更新 4D Gaussian 参数

论文提到 batch sampling 可以缓解动态场景中的 temporal flickering / jitter。代码实现上,它就是一次 iteration 中采 batch_size 张训练图一起算 loss;这些样本通常对应多个相机位置和多个 timestamp。

6.2 密度控制

普通 3DGS 主要根据屏幕空间位置梯度判断哪里重建不足,需要增密。4DGS 论文中还提到引入时间位置 t 的平均梯度作为 density control 指标。

需要注意代码实现细节:

  • 代码确实计算并累计了 _t.grad

    _t.grad → batch_t_grad → t_gradient_accum → grads_t
  • 但当前 densify_and_clone()densify_and_split() 的选择条件主要仍然使用屏幕空间梯度 grads,没有真正使用 grads_t 来决定哪些 Gaussian 被增密。

当前代码中真正体现时间维度增密的是 time split

7. Gaussian 增长与减少逻辑

7.1 什么时候触发

训练循环中:

densify_from_iter 之后开始
每 densification_interval 个 iter 执行一次
densify_until_iter 之后停止增长

默认大致是:

500 iter 后开始
每 100 iter 执行一次
15000 iter 后停止 densification

7.2 Clone:小 Gaussian 直接复制

如果一个 Gaussian:

屏幕空间梯度大
并且自身尺度较小

代码会执行 clone:复制一个几乎相同的 Gaussian,让后续优化把它们拉开。

7.3 Split:大 Gaussian 分裂

如果一个 Gaussian:

屏幕空间梯度大
并且自身尺度较大

代码会执行 split:从该 Gaussian 的分布中采样多个新 Gaussian,并删除旧 Gaussian。

4DGS 的关键是 split 在完整 xyzt 空间中进行:

new_xyzt = R_4d @ sample_xyzt + old_xyzt

所以新 Gaussian 的中心不仅 x, y, z 会变化,t 也会变化。

这就是 time split

7.4 Prune:删除无效 Gaussian

删除条件主要包括:

  • opacity 太低。
  • 屏幕空间半径太大。
  • 世界空间尺度太大。

删除时,代码会同步删除对应的:

xyz / t / scaling / scaling_t / rotation / rotation_r / opacity / feature

以及 Adam optimizer 中的动量状态。

8. 评估与消融实验

消融实验结果

论文中的主要消融项:

  • No-4DRot:不使用完整 4D rotation,空间和时间不充分耦合,运动表达能力下降。
  • No-4DSH:不使用时间相关颜色建模,外观随时间变化的表达能力下降。
  • No-Time split:split 时不在时间维度采样,时间维度上的表示密度不足。

核心结论:

4D rotation 负责让 Gaussian 表达运动;
4DSH 负责让颜色随视角和时间变化;
time split 负责让 Gaussian 在时间维度上也能增密。