- 论文概述:
- 这篇论文做的是:从同步多视角视频里,重建一个会随时间变化的人体 3D Gaussian 表示,并且把它压缩到足够小,好在 VR/移动端实时播放。 论文把这种动态的、随时间变化的 3DGS 资产称为 human-centric volumetric video / 4D assets。
- 论文提出的核心是 DualGS(双高斯表示):不用一套高斯同时负责“怎么动”和“长什么样”,而是拆成两套——joint Gaussians 负责运动,skin Gaussians 负责外观细节。然后再用一个粗到细的优化流程先把大动作追准,再补细节,最后再用一套专门的压缩方案把整段序列压到很小。论文自己总结的关键点就是:把 motion 和 appearance 显式解耦、用 coarse-to-fine 做稳健跟踪、再对 motion/appearance 分开压缩,最终做到高保真和高压缩并存。
- 它不是每一帧都重新建一个全新的 3DGS;它是先在第 1 帧建好一套“会被驱动的高斯系统”,后面每一帧都在更新这同一套高斯的时刻 ttt 属性,让它随时间动起来。
0. 基础
0.1 4D Gaussian
- 原始静态3DGS:
- 这些属性在静态场景里基本不随时间变。
- 4DGS增加了一个时间下标:
0.2 输入
- 使用81个相机,每个时间点相当于有81个多视角图
1 Introduction
原始 3DGS 在动态人体上为什么不够好?
- 因为静态场景和动态人体差很多:
- 静态场景:高斯位置基本不变,只优化外观和几何就行
- 动态人体:人每一帧都在动,手、衣服、头发都在变
- 如果你还是让“同一批高斯”既负责运动又负责外观,就容易出问题:
- 优化会偷懒:论文明确提到,他们观察到高斯容易去“改 appearance”,而不是把 position 真正移动到该去的位置。也就是:明明应该靠移动位置去跟踪动作,它却靠改颜色/透明度来糊弄 photometric loss。
- 快速动作难跟踪:比如甩双截棍、弹乐器、跳舞,位移很快,单一高斯系统容易漂。
- 时序不稳:每一帧单独拟合的话,会有闪烁、抖动。
- 存储巨大:动态 4D 资产每帧都要存大量属性,不压缩根本没法上 VR 头显。
- 所以这篇论文的核心不是“把 3DGS 再堆复杂一点”,而是先从表示层面改造:别让所有高斯干一样的活。
2 overview
- 本文将高斯属性分成两类:
- motion-aware 参数:位置 p、旋转 q
- appearance-aware 参数:SH 系数、opacity 、scaling
- 分为两类高斯:
- joint Gaussians:少量、专门负责运动(约 15,000)
- skin Gaussians:大量、专门负责外观细节(约 180,000)
- 为什么这么分:
- 因为人体运动本身有层级结构,比如抬手这个动作,不需要让衣服上的每个纹理点各自学一个完全独立的运动。更合理的方式是:
- 先用少量“运动控制点”表达大的身体运动
- 再让细节点跟着这些控制点一起动
- 这就很像“骨骼驱动皮肤”的思想。论文也明确说灵感来自 SMPL:皮肤顶点可由少量关节插值得到。只不过这里不是传统网格+骨骼,而是高斯版的 joint / skin 二层结构。
- 因为人体运动本身有层级结构,比如抬手这个动作,不需要让衣服上的每个纹理点各自学一个完全独立的运动。更合理的方式是:
- 这么做的好处:
- skin Gaussians 的相对局部位置更稳定:它们持续被同一组 joint 驱动,所以时空一致性更好。
- 需要描述运动的参数更少:后续压缩更方便。
3 Method
3.1 双高斯初始化
3.1.1 初始化 joint Gaussians
1. 高斯初始化
- 类似3dgs,在第一帧里从随机点云出发训练一组高斯
- 前15000 iter 做 densification 和 pruning
- 把数量压到大约 15000 个
- 后面固定数量,只优化数值
这里不是从colmap初始化,而是在第一帧上随机初始化,因为这里不是精细纹理的重建,随机初始化即可。
2. isotropic loss:防止高斯太“针状”
- 最大轴和最小轴的差别别太大,major/minor axis 的比例不要超过阈值 r
- joint Gaussians 是要拿来表达“身体某局部如何整体移动”的。如果它们长得太怪,就更像是在钻 photometric loss 的空子,而不是在学稳定运动结构。
3 size loss:防止高斯太大
- 如果某个 Gaussian 的 scale 超过平均值乘某个系数
,就罚它 - 防止少数大高斯把很多局部区域胡乱包进去,joint 初始化阶段并不只是“训练一堆高斯”,而是在刻意把它们塑造成适合做运动锚点的一组高斯。
3.1.2 初始化 skin Gaussians
1. 高斯初始化
- 用joint高斯作为初始,继续训练,经过复制、densify之后得到180k的skin高斯
2. 建立joint-skin 绑定:KNN anchoring
- 对于每个 skin Gaussian,论文会找它附近的 k 个最近 joint Gaussians 作为 anchor,论文中 k=8
- 然后定义一个权重:
-
指的是高斯位置 -
指的是影响半径,论文取0.001 -
指的是这个joint对某个skin的影响程度 - skin 离某个 joint 越近,这个 joint 对它影响越大
-
到这里,第一帧你已经拿到了:
- 一组运动锚点:joint Gaussians
- 一组外观高斯:skin Gaussians
- 每个 skin 对应的 8 个邻近 joint 和它们的权重
3.2 双高斯优化
- 初始化只解决第一帧。后面真正难的是:整个视频序列怎么一帧一帧稳稳跟踪下去。论文的答案是:coarse-to-fine。
- 论文观察到如果直接一起优化,Gaussians 往往更愿意改 appearance,而不是去更新位置来拟合图像。
- 从这里开始,需要注意训练方式
- 训练不是随机采样,假设视频时常T=10,训练方式是,先训练t=1(已经初始化过了),训练完之后训练t=2,按照时间顺序训练下去,训练到t=10后训练结束。
- 这里的训练方式本质上相当于对每个时间t训练一个3dgs场景,只不过各个场景之间是共享拓扑关系和高斯id的。
- 优化顺序:
- t=0时刻,初始化joint;初始化skin,初始化skin、joint的knn
- t=1时刻,粗优化10000 iter:
- Motion Prediction预测初始化joint
- 仅优化joint的运动参数,渲染也只用joint
- 相对运动约束这里会用joint之间的knn
- t=1时刻,细优化10000 iter:
- 初始化skin,已经有了t=0的joint和skin的knn关系,初始化skin时,颜色参数保留,位置参数根据knn的joint插值(该过程会用到第一帧的skin参考)得到
- 将skin和joint都做渲染,算loss,反向传播更新skin和joint(fine 阶段主要用当前帧 skin 作为高保真渲染层;joint 不是主要视觉表达层,而是通过可微的 skin motion 插值间接影响渲染结果)
- 计算skin,skin的颜色参数得到了优化,但是运动参数没变,还是根据上一步的计算公式,根据knn的joint得到skin的位置参数。这就实现了运动与颜色的分离,joint带动skin。
- t=2时刻粗优化......
3.2.1 Coarse Alignment:先把大动作追准
- 在粗对齐阶段:
- 固定颜色、透明度、尺度
- 只优化 joint Gaussians 的运动(位置、旋转两个参数)
- 为什么只优化运动参数?
- 因为论文观察到:高斯会更倾向于修改 appearance,而不是把 position 真正移动到目标位置去拟合 photometric loss。比如t=1时刻,高斯i表示的是大拇指,高斯j表示的是食指;到了t=2时刻,大拇指移动了位置,到了之前食指的位置。如果你不固定颜色参数,那他可能通过修改高斯j的颜色参数让高斯j渲染出来成了大拇指,这样高斯的移动就难以学习。
- 注意该阶段渲染时只用joint高斯,也就是说在这个阶段没有skin高斯的事。
1. ARAP 平滑约束
- 相邻的 joint Gaussians 在前后两帧的相对结构,不要突然乱掉。比如上一帧里两个邻近 joint 的相对位置关系是这样的,到了下一帧,你可以让它们整体平移、整体旋转,但不要突然互相撕裂、拉爆、折叠。
- 比如上一帧里两个邻居 joint:
- joint A 在 (0,0)
- joint B 在 (1,0)
- 它们的相对向量是 (1,0)
- 如果下一帧人体抬手了,这个局部块整体转了 30 度,那么合理结果可能是:
- A 到 (2,1)
- B 到 (2+cos30∘,1+sin30∘)
- 比如上一帧里两个邻居 joint:
2. 颜色损失
- 粗阶段的颜色损失仍然是 3DGS 风格的 photometric loss:
- 虽然 appearance 参数被冻结了,但 joint rasterization 后仍能得到图像,所以还是能和 GT 比颜色差。只是这时候优化的自由度主要在 motion 上。
3. 粗阶段完整目标
- 一边让渲染结果接近真实图像,一边让 joint 的局部运动别乱来
3.3 Motion Prediction:先猜一帧,再优化一帧
- 为了应对快速运动,论文还加了一个 motion prediction 模块:
- 给每个 Gaussian 维护一个 velocity
- 用最近两帧的位置变化,推测下一帧应该在哪
- 再把这个预测结果作为 coarse alignment 的初始化
3.4 Fine-grained Optimization:补细节
- 粗阶段结束后,你已经有了比较靠谱的 joint motion。接下来进入细阶段,这一阶段会同时考虑:
- joint motion
- skin appearance
- skin 的更细位置/外观细节
- temporal consistency
3.4.1 初始的skin 怎么从 joint 运动里算出来
- 论文用初始化阶段得到的 KNN 图和 blending weights,把 joint 的运动插值到 skin 上:
- 每个 skin 不是自己瞎动,它是由那 8 个 anchor joint 加权决定的
- skin rotation 是多个 joint rotation 的加权和
- skin position 结合skin与相邻joint的相对旋转+joint的位置,加权求和
3.4.2 反向传播是怎么走的
- skin 的位置和旋转是由 joint 通过计算图算出来的,所以 skin 上的梯度会继续传回 joint。最后的图像损失会反过来告诉 joint:你带着 skin 动得对不对。而skin的颜色参数是自己的,因此skin可以通过颜色参数的修改来迫使渲染图接近。
3.4.3 temporal regularization
- 这里约束的是:
- sh系数
- 不透明度
- scale
- 因为这里是一个时间步一个时间步训练的,所以可以使用上一个时间步得到的结果
- 相邻两帧,同一个 Gaussian 的外观属性不要突然变化太大。这会视觉更稳定,减少闪烁
3.4.4 细阶段完整目标
- 可以理解成三部分:
- 继续让运动别乱
- 让外观时间上平滑
- 让渲染结果贴近真实图像
3.5 压缩模块
- 关键思想是 motion 和 appearance 分开压缩
- 压缩模块只用于存储,前面的优化步骤优化的还是之前的3dgs的位置,旋转,缩放等属性,内存中没有压缩,存储到外存时进行压缩,然后渲染的时候读取外存数据根据公式转换成高斯属性的形式之后进行渲染。
3.5.1 joint motion压缩
- 因为 position 非常敏感。论文提到,高斯位置稍微有点误差,渲染质量就会掉很多,所以不能瞎压。
- 每个 segment 保留第一帧的位置
- 后面的帧不是直接存绝对位置,而是存相对于“已恢复轨迹”的残差,再做量化
- rotation 也类似处理
- 最后再用 RANS 做无损熵编码(它吃进去的是“已经量化好的离散整数流”,吐出来的是“更短的二进制比特流”。假设某个 joint 的 x 残差量化后,连续 10 帧变成了:[0,0,0,1,0,0,−1,0,0,0]你看这个序列里0 出现特别多,1 和 -1 很少,那 RANS 就会给“0”分配很短的编码,给“1”“-1”分配稍长一点的编码)
- 例子:对于一个joint,假设他在
- 第一帧:10.000
- 第二帧:10.012
- 第三帧:10.026
- 如果你每帧都存绝对值,那每帧都要存一个完整 float32。但实际上它变化很小。你只需要存:
- 第 1 帧:10.000
- 第 2 帧残差:+0.012
- 第 3 帧残差:+0.014
- 这些残差动态范围更小,更容易量化成低 bit 数。论文这里对残差做 11-bit quantization。
- 为什么不是简单存相邻帧差
- 因为如果你每一帧都依赖上一帧的量化结果,误差会不断累计。 论文特别说,他们这个设计相较于只量化相邻帧残差,可以有效避免 error accumulation
- 举例:假设一个关节 x 坐标真实值是:
- 第1帧:0.00
- 第2帧:0.44
- 第3帧:0.88
- 第4帧:1.32
- 假设量化步长是 0.1。那每一帧差分都大约是:0.44
0.4,于是解码时,累计到第四帧误差就是0.12了。 - 本文做法:
- 第1帧,直接存:0.00
- 第2帧,参考当前已恢复轨迹只有第1帧,所以残差:0.44−0.0=0.44
0.4,回复后p=0.4 - 第3帧,现在参考的不是“真实第2帧 0.44”,而是“当前已恢复轨迹 0.4”,所以残差:0.88−0.4=0.48
0.5,恢复后:p=0.9,误差变成 0.02。 - 第4帧,再参考当前已恢复轨迹 0.9:1.32−0.9=0.42
0.4,恢复后p=1.3,误差还是0.02。没有累积效应。
3.5.2 skin 的 opacity 和 scale 用 codec compression
- skin 数量很多,180k 级别。如果还用精细残差方式去压 opacity 和 scale,存储会很大。
- 把每个 Gaussian 在每一帧的 opacity / scale 排成一个二维表:
- 高度 = Gaussian 编号
- 宽度 = 时间帧
- 这就变成一张“图”(8bit image)
- 然后用图像编码器 WebP/JPEG 去压它(这一步才是真正进一步大幅变小的地方。因为图像 codec 会利用图中的空间相关性和重复模式压缩。)
- 把每个 Gaussian 在每一帧的 opacity / scale 排成一个二维表:
- 为什么这样能压?
- 因为 temporal regularization 已经让这些属性在时间上很平滑。于是这个 2D LUT 会呈现很强的空间-时间相关性,像一张很平滑的图,图像 codec 就很擅长压这种东西。
- 同时,他们还做了一步排序:按每行平均值排序,让二维图更连续、更适合压缩。
3.5.3 SH 颜色系数用 persistent codebook
- 颜色属性最占空间,59 个参数里有 48 个是 color SH 系数,如果你直接压这些 SH 浮点数,还是会很大。 于是他们做了一个 persistent codebook
- 先对每个 SH 阶数 d=0,1,2,3 的系数做 K-Means 聚类,得到 codebook。然后每个 Gaussian 每一帧的 SH 系数,不再存完整向量,而只存它最接近哪个 codeword 的索引。
- 原来存一串浮点数
- 现在存一个整数 index
- 举例:
- 假设一共有10帧,一帧10个高斯,那么就有100个高斯,每个高斯0-1-2-3四阶系数,就是400个向量。 假设K=4,然后对0阶级聚类得到一个codebook,里面有4个向量。然后对1阶聚类得到一个codebook,里面有4个向量。......。然后对于某个高斯如果他的0阶系数跟2号向量最接近,那么它就只需要存一个只指向9的索引就行了。
- 先对每个 SH 阶数 d=0,1,2,3 的系数做 K-Means 聚类,得到 codebook。然后每个 Gaussian 每一帧的 SH 系数,不再存完整向量,而只存它最接近哪个 codeword 的索引。
- 为什么叫 persistent codebook
- 论文观察到,把 SH 系数变成 index 后,相邻帧里只有大约 1% 的 skin Gaussian SH 索引会变化。所以他们不再逐帧存所有索引,而是:
- 存第一帧所有索引,后面只存“哪些地方变了”
- 每次变化记成一个四元组 (t,d,i,k) 表示: 第 t 帧, 第 d 阶 SH, 第 i 个 Gaussian,变成索引 k
- 然后再排序、做长度编码
- 论文观察到,把 SH 系数变成 index 后,相邻帧里只有大约 1% 的 skin Gaussian SH 索引会变化。所以他们不再逐帧存所有索引,而是: