PLY 字段表:62 个字段对回 14 个参数
字段清单与计数
3DGS 训练导出的 PLY(实测 splatfacto 产物)每个顶点携带 62 个 float 字段:
| 字段 | 个数 | 含义 |
|---|
x, y, z | 3 | 位置 μ(训练坐标系) |
nx, ny, nz | 3 | 法线占位,训练不用,恒为 0 |
f_dc_0..2 | 3 | SH 零阶(直流)系数,RGB 各一 |
f_rest_0..44 | 45 | SH 1–3 阶系数,15×3 通道 |
opacity | 1 | 不透明度的 logit |
scale_0..2 | 3 | 对数尺度 s,不是轴长 |
rot_0..3 | 4 | 旋转四元数 q |
合计 3+3+3+45+1+3+4=62。去掉占位法线,每高斯的有效参数是 14 个基础参数(μ 3、q 4、s 3、o 1、DC 3)加 45 个 SH 高阶系数。SH 每通道 DC 到 3 阶共 1+3+5+7=16 个基,f_dc 存第 1 个,f_rest 存后 15 个 × 3 通道 = 45。
三个”存的不是你想要的”
scale 存对数。文件里是 s=loga,用之前必须恢复:
a=es
把 s 当 a 直接用的后果不只是”胖一圈”:s 可以为负(s=−1 表示轴长 e−1≈0.37),当成轴长就成了负边长,协方差直接非法。加载端实证:vksplat ply.cpp:207-209 对 scale 做 exp 恢复,并做消毒钳制 clamp(s,−13.8,log100)——即轴长限制在约 10−6 到 100 米之间,挡住训练发散出的极端值。
opacity 存 logit。文件里是 o=logit(α)=ln1−αα,渲染前要过 sigmoid:
α=σ(o)=1+e−o1
实测文件中大量出现 −2.197,恰好是 logit(0.1)=ln(1/9)≈−2.1972——这是训练里 opacity 初始化(INRIA 惯例初始 α=0.1)留下的化石。看到一片 −2.197 说明那批高斯从没被优化推动过。
f_dc 是偏移前的原始系数。渲染颜色是:
c=0.5+C0⋅fdc,C0=2π1≈0.2821
文件里存的 fdc 不含那个 0.5 基础偏移。直接拿 C0⋅fdc 当颜色,整个场景会偏暗一截(C0⋅fdc 的值域大致在 ±0.5)。这个 0.5 是 EP01 讲 SH 颜色时的伏笔,在真实文件上兑现。
容易踩的坑
- 字段顺序按上面表格来,但解析时不要按位置猜,按 PLY header 里的
property 声明建索引——不同导出工具字段顺序可能不同。
nx, ny, nz 看似法线实为占位,不要拿去做光照。
- 四元数是否归一化,文件格式本身不保证——实测模长最大 4.3,见深潜二。
参考
重定向:dataparser 的静默坐标替换
发生了什么
nerfstudio 的 dataparser 在读入相机数据时会做一次自动重定向(auto-orientation):估计场景的”向上”方向与尺度,对相机位姿施加一个相似变换——实测缩放 k=0.042 外加一个旋转 R。动机是工程性的:把场景归一化到单位量级的尺度、摆正朝向,让优化器和学习率在同一套数量级上工作,调参经验(INRIA 那套超参)才通用。
关键是:这一步只发生在训练内部。点云初始化、相机位姿、优化过程全部在变换后的”歪系”里进行,导出的 PLY 里每个高斯的 μ、q、s 都是歪系坐标。你想把这份 PLY 放回原始世界(比如和别的资产对齐),尺度差 1/0.042≈23.8 倍,朝向也不对。
逆变换的写法与顺序
设训练前 dataparser 施加的是
ptrain=kRp原
则还原是逆变换,顺序必须反过来——先除尺度,再逆转:
p原=R−1(kpply)
复合变换求逆要倒序:(kR)−1=R−1k−1。先转回再除尺度是错的——旋转不改变模长但会改变分量的轴向,两步对调后点在错误的轴上被缩放。
只换位置不够:每个参数都要跟着变
把高斯从歪系搬回原系,不是一个 μ 的事:
- 尺度:轴长在原系里是 a原=aply/k(对数尺度则 s原=sply−logk)。
- 旋转:椭球朝向要跟着转回,q原=qR−1⊗qply(⊗ 为四元数乘法,qR−1 是逆旋转对应的四元数)。
- SH 系数:颜色是方向的函数,坐标系转了系数必须跟着转,否则高光漂移——这是深潜三的整个主题。
相机位姿必须同轨
最常漏的一条:相机外参(c2w)必须走同一套逆变换。点云回了原系、相机还留在歪系(或反过来),渲染出来的视角就整体错位。两边同轨的检验很简单:拿训练时的某个机位,在还原后的场景里渲染,应与原训练渲染逐像素一致。
容易踩的坑
- k 和 R 要从 dataparser 的变换记录里取(nerfstudio 会保存 transform/scale),不要凭点云包围盒反推——反推分不出旋转和尺度的归属。
- 逆变换是相似变换,不改变高斯数量、不改变协方差的形状比——如果还原后计数变了或椭球扁圆比变了,错在变换代码不在数据。
参考
四元数没归一化:模长 4.3 的代价
约定与实测
用四元数表示旋转的约定是单位四元数:∥q∥=w2+x2+y2+z2=1。实测 nerfstudio 导出的 PLY,四元数模长分布拖着长尾,最大到 4.3——绝大多数贴着 1,少数严重超标。
为什么 R(q) 构造式会”边转边膨胀”
EP01 抄的标准构造式(q=(w,x,y,z)):
R(q)=1−2(y2+z2)2(xy+wz)2(xz−wy)2(xy−wz)1−2(x2+z2)2(yz+wx)2(xz+wy)2(yz−wx)1−2(x2+y2)
这个形式默认 ∥q∥=1:对角元的 "1" 就是 w2+x2+y2+z2 的缩写。把 q′=λq 代进去,每一项都变成 λ2 的二次项,而对角元的常数 1 不变:
R(λq)=λ2R(q)+(1−λ2)I
λ=4.3 时 λ2≈18.5:矩阵是”18.5 倍的真旋转”再叠加一个 ”−17.5 倍单位阵”,既不是旋转也不是均匀缩放。用它构造协方差 Σ=Rdiag(a2)RT,Σ 不再是半正定的椭球——表现为边转边膨胀、椭球畸变、朝向跑偏。
注意区分另一件常被混用的事:四元数的三明治积 v↦qvqˉ 自带 λ2 因子,写成 qvq−1 或除以 ∥q∥2 就对模长免疫。但上面这张展开成 3×3 的构造表不免疫——它把归一化假设编译进了对角元的常数 1 里。
排查与修复
排查一行统计就够:画 ∥q∥ 的分布直方图,不齐就是它(示意代码):
import numpy as np
q = load_rot_fields() # (N, 4),来自 rot_0..3
n = np.linalg.norm(q, axis=1)
print(n.min(), n.max()) # 健康:1.0 1.0;实测:… 4.3
修复同样一行:加载时逐个归一化
q←∥q∥q
直觉:四元数在 R4 里的方向是旋转,长度是赘肉。q 与 λq 表示同一个旋转(单位化后逐分量相同),归一化不丢任何旋转信息。修完复查一次分布,只剩 ∥q∥=1 一根柱,任务完成。
容易踩的坑
- “大多数是 1”不等于没问题。分布主体贴 1、长尾到 4.3,超标的每个高斯都在悄悄畸变——大场景里几千个畸形椭球足以毁掉局部观感,而整体 PSNR 可能毫无察觉。
- 归一化要在构造 R(q) 之前做,放在渲染管线里每帧做一次是纯浪费。
- 模长为 0 的退化四元数要单独挡(除零),实测数据里没有,但消毒代码不该省。
参考
SH 不随旋转:转了世界,忘了转颜色
机理:系数绑在方向上
3DGS 的视角相关颜色用球谐(Spherical Harmonics, SH)展开:每个高斯存一组系数 S={clm},渲染时沿”高斯中心指向相机”的方向 d 求值:
c(d)=∑l,mclmYlm(d)
基函数 Ylm(d) 是定义在世界坐标系方向球面上的固定函数。所以系数 clm 的含义是”世界系里朝 (θ,φ) 方向看,颜色偏多少”——它天生绑死在坐标系上。
换坐标系时(比如深潜一的逆重定向 R−1),高斯的位置、旋转都变换了,世界系方向 d 变成了 d′=R−1d。如果系数原样照抄:
c新(d′)=∑clmYlm(d′)=∑clmYlm(R−1d)=c旧(R−1d)
也就是每个方向上的颜色被转反了:本该朝东的高光还在旧坐标的”东”。几何(位置、椭球)全对,颜色却整体漂移。
修法:左乘 SH 旋转矩阵
球谐的一个核心性质:旋转后的基函数可以写成同阶基函数的线性组合。即存在矩阵 RSH(R) 使
Y(Rd)=RSH(R)Y(d)
其中 Y(d) 是把该高斯全部 SH 基函数值堆成的向量。于是换系时系数左乘即可:
S′=RSH(R)⋅S
两个要紧的结构性质:
- 分块对角,不混阶。旋转只把 l 阶基在同阶内部重排,RSH 按 l=0,1,2,3 分成 1×1,3×3,5×5,7×7 四块。DC 块恒为 [1]——所以
f_dc 不用动,只有 f_rest 的 45 个系数需要变换,且 1 阶、2 阶、3 阶各自乘各自的块。
- 每通道独立。RGB 三个通道各持一份 16 系数,分别左乘同一个 RSH,共三次。
症状指纹
这个 bug 的指纹非常好认:几何全对,颜色发怪。地平线正、铅垂线直、尺度对,但金属不反光、高光漂到错误的一侧、光泽随视角变化的方向感错位。排查时一旦确认几何无误而颜色异常,SH 没跟旋转是头号嫌疑。注意 RSH 没有简单的解析表达式,怎么把它造出来是下一块的主题。
参考
数值构造 SH 旋转矩阵
为什么不走解析式
SH 旋转矩阵的解析理论是存在的——Wigner D-matrix(经实形式变换后得到实的 SH 旋转矩阵),但那条路要处理复球谐、Clebsch–Gordan 系数和一堆符号约定,实现一步错就全盘错。工程上换一个思路:RSH 说到底只是一个 16×16 的线性映射,用采样把它的每一列”量”出来。
四步构造
要求解的关系来自定义本身——对任意方向 d,“先转方向再展开”与”先展开再乘矩阵”必须相等:
Y(Rd)=RSH⋅Y(d)
- 铺方向:取 K 个方向 d1,…,dK 铺满球面。3 阶共 16 个基,理论上 K≥16 即可定解,实际取 K 远大于 16(几十到几百)做超定,压制采样偏斜。用斐波那契球面或正二十面体细分均可,忌讳用”经纬网格”——两极密集、赤道稀疏,最小二乘会偏向极地。
- 算基函数:对每个方向求 16 个 SH 基函数值,堆成两个矩阵:
- Aij=Yj(di),尺寸 K×16(转前)
- Bij=Yj(Rdi),尺寸 K×16(转后)
- 最小二乘:解 B=AM,得 M=A+B=(ATA)−1ATB。所求 RSH=MT。由于 SH 基在球面上正交且采样均匀,ATA 接近对角阵,条件数很好,普通 QR 或 SVD 求伪逆都稳。
- 验收:在另一组没参与求解的方向上检查残差
∥Y(Rd)−RSHY(d)∥≈10−6
残差量级百万分之一即达标。务必用留一验证:用求解集自评会掩盖过拟合采样分布的错误。
为什么这个方法是对的
RSH 的存在性是球谐的理论性质(同阶基函数张成旋转不变子空间),不依赖构造方法。数值法只是把这个必然存在的矩阵用线性回归”称”出来——K 个方程约束 16×16 的矩阵看似欠定,实则矩阵的自由度被”分块对角、块内正交”的结构大幅压缩,超定采样足以钉死它。代价是精度限于浮点和采样质量,换来的是几十行代码和可验证的残差。
容易踩的坑
- 基函数的归一化约定要前后一致。A 和 B 用同一套基定义,且必须与渲染端(EP01/EP07 那张基函数表)逐系数对齐——连 Condon–Shortley 相位这类符号约定不一致都会变成系统性颜色错误,而残差自检查不出来(它只验证内部自洽)。终极验证是端到端:旋转前后渲染同一视角,颜色对得上才算数。
- 一次构造,处处复用。RSH 只由旋转 R 决定,与场景无关——换系时构造一次,百万高斯共用。
- DC 分量(l=0)经任何旋转都不变,它的 1×1 块恒为 [1],可以跳过,但留着不动也无害。
参考