一句话激活 Jacobian
针孔投影的本体两式
相机系下的点 (x,y,z)(z 为深度,向前为正)投到像素坐标:
u=f⋅zx+cx,v=f⋅zy+cy
其中 f 是以像素为单位的焦距(本系列真实数据 f=935.3074 px),(cx,cy) 是主点(光轴与成像面的交点,通常在画面中心附近)。除以 z 就是透视:同样的物理尺寸,离相机越远,在屏幕上越小。
Jacobian 是什么
雅可比矩阵(Jacobian matrix)是多元函数的一阶”速率表”:每个输出对每个输入求偏导,排成矩阵。这里输出 2 个(u,v)、输入 3 个(x,y,z),所以 J 是 2×3:
J=∂(x,y,z)∂(u,v)=[∂u/∂x∂v/∂x∂u/∂y∂v/∂y∂u/∂z∂v/∂z]
读法:J 的第 j 列回答”输入 j 动 1 个单位,两个输出各动多少”。给相机系里一个微小位移 Δp,屏幕上的一阶位移就是 Δpix≈J⋅Δp。
容易漏掉的两个点
- cx,cy 不进 J。它们是常数偏移,求偏导后消失。主点只影响椭圆”落在哪”,不影响”椭圆多大、什么形状”。
- J 不是常数矩阵。它的元素含 x,y,z,必须在具体点处取值——这正是后面”在 μ 处取值、每个高斯一个自己的 J“的原因,也是”线性近似”四个字的来源。
参考
概念①-a:协方差搬运律
一条公式
随机向量 x(协方差矩阵 Σ)过线性变换 y=Mx,新协方差为:
Cov(y)=MΣMT
推导一行就够:记 μ=E[x],则
Cov(Mx)=E[(Mx−Mμ)(Mx−Mμ)T]=ME[(x−μ)(x−μ)T]MT=MΣMT
矩阵乘法从左、右两侧分别提出来,右侧那个就是转置。
转置为什么不能丢
- 形状对不上:Σ 是 n×n,M 是 m×n。只乘一侧得到 MΣ(m×n),根本不是方阵,谈不上协方差。两侧同乘 MΣMT 才是 m×m。
- 直观原因:方差是”平方量”。一维情形 y=ax 有 Var(y)=a2Var(x)——a 出现两次。矩阵版本里,一个 M 管行、一个 MT 管列,正好对应那个平方。
- 附赠性质:Σ 半正定,则 MΣMT 也半正定,所以搬运出来的永远是合法的协方差(永远对应一个真实的椭圆/椭球)。
两程搬运,一条公式
3DGS 前向里这条定律连用两次:
Vrk=WΣWT,cov2d=JVrkJT
- 第一程:W 是 w2c(世界到相机)变换的旋转块,3×3。协方差只跟旋转走,平移不进协方差——椭球平移不改变形状。
- 第二程:J 是透视投影在高斯中心 μ 处的雅可比,2×3。JVrkJT 是 2×2,即屏幕椭圆。
注意第二程的 M=J 只是局部线性变换——透视投影本身不是线性的,这里已经埋下了”近似”的伏笔。
单位检查
- Σ、Vrk:米²(世界/相机系里的物理尺寸)
- J:像素/米(偏导 ∂u/∂x=f/z 的单位)
- cov2d:像素²——所以 cov2d 开根号才是像素半轴长
单位是自查利器:如果算出来的”屏幕椭圆”单位还是米,多半是少搬了一程。
参考
概念①-b:J 推导,逐格走
逐格求偏导
投影本体(cx 是常数,求导即消失):
u=f⋅zx+cx
对三个输入逐个求偏导:
- 对 x:z 当常数,f 当常数,得 ∂x∂u=zf
- 对 y:u 里根本没有 y,得 ∂y∂u=0
- 对 z:用商法则,∂z∂(zx)=−z2x,得 ∂z∂u=−z2fx
v=f⋅y/z+cy 完全对称。排成 2×3:
J=[f/z00f/z−fx/z2−fy/z2]
记忆结构:前两列是”横向放大率” f/z,第三列是”深度变化引起的径向滑动”——z 变一点,像素沿背离/朝向主点的方向滑,滑速正比于离主点的距离 x 或 y。
在 μ 处取值:每个高斯一个 J
J 的元素含 (x,y,z),必须代入具体点。3DGS 把它代入高斯中心在相机系的位置 μ:切平面在 μ 处与透视曲面相切,一阶邻域内两者贴合。
算例:μ=(0,0,4),f=935.3074 px:
f/z=935.3074/4=233.83,−fx/z2=0,−fy/z2=0
光轴正前方的点,第三列恰好为零,J 退化成 diag(233.83,233.83) 的效果——这就是”正对相机、居中”的玩具算例为什么干净。偏心的高斯第三列非零,椭圆会被透视”拽斜”。
易错点
- f 的单位是像素,不是毫米。物理焦距 4mm 不能直接代入;本系列数据 f=935.3074 px 已含像素密度换算。
- fx 与 fy 一般不相等。真实相机两个方向焦距略有差异;本数据集标定为等值。若 fx=fy,矩阵里 u 行用 fx、v 行用 fy,其余不变。
- 符号约定依赖坐标系。这里取相机系 z 向前为正;若你的代码用 OpenGL 式”z 向后为正”,第三列符号相应翻转。推导前先钉死约定。
手算 cov2d:椭球上了屏幕
算例设定
- 高斯轴长(1σ 半径)a=(0.3,0.1,0.1) m,三轴与世界系对齐
- 位置 μ=(0,0,4) m:光轴正前方、四米深
- W=I(世界系与相机系无旋转),f=935.3074 px
四步手算
第 1 步:平方得协方差。 协方差是轴长的平方(方差 = 标准差²):
Σ=diag(a2)=diag(0.09, 0.01, 0.01) m2
第 2 步:第一程搬运。 W=I,所以 Vrk=IΣIT=Σ,数值不变,只是”换了个坐标系的说法”。
第 3 步:准备 J 的标量因子。 光轴正前方时 J 的第三列为零(见 B44),有效部分就是 f/z:
f/z=935.3074/4=233.83 px/m,(f/z)2=233.832=54675 px2/m2
第 4 步:第二程搬运。 对角矩阵乘 diag(k,k) 等于各对角元乘 k2:
cov2d=diag(0.09×54675, 0.01×54675)=diag(4920.7, 546.7) px2
开根号回到半轴长:4920.7=70.2 px,546.7=23.4 px。
怎么读这组数字
- 70.2 × 23.4 px 是 1σ 半轴,不是椭圆的可见边缘。屏幕上实际画出的足迹按截断倍数放大(参考实现取 2.83σ 截断),可见范围约 2.83×70.2≈199 px 宽。
- 深度方向的第三根轴(0.1 m 那根,指向相机)在 J 的 2×3 乘法中被压掉——透视下”朝向镜头的厚度”不直接贡献屏幕面积。
- 数字含义快查:cov2d 单位 px²,半轴单位 px。这组数(4920.7/546.7/70.2×23.4)是第五集反向推导要复用的基准。
常见误解
- ”z 翻倍,椭圆面积缩一半”——错。f/z 进协方差时被平方,半轴按 1/z 缩,面积按 1/z2 缩:四米变八米,半轴从 70 px 缩到 35 px,面积只剩 1/4。
- 把轴长 0.3 m 直接乘 f/z 当半轴——这一步恰好数值上成立(0.09×54675=0.3×233.83),但仅限于 Σ 对角且 J 对角的特例;一般情形必须走 JΣJT 全程,非对角项(旋转)会改变结果。
概念①-c:线性化何时失效
“近似”两个字的分量
cov2d=JVrkJT 用的是协方差搬运律,而搬运律的严格前提是线性变换。透视投影 u=f⋅x/z 不是线性的——它是有理函数,等值面是弯的。用 J 搬协方差,等于在 μ 处作一阶泰勒展开,拿切平面代替弯曲的投影面。
高数的老话在这里一字不差:直线近似曲线,只在切点附近靠谱。高斯的空间延展越超出”切点附近”,误差越大。
误差从哪一项来
泰勒展开的下一阶是二阶偏导项。u=f⋅x/z 的二阶偏导:
∂x∂z∂2u=−z2f,∂z2∂2u=z32fx
注意它们携带的 z 幂次:二阶修正按 1/z2(甚至 1/z3)增长,而一阶项只有 1/z。z 减半,二阶误差翻四倍——深度是误差最敏感的旋钮。
三档对比(同一高斯,z=8→4→1.2 m):
| 深度 z | 1/z2 相对量级 | 现象 |
|---|
| 8 m | 1/64(基线) | 切平面与真实轮廓几乎重合 |
| 4 m | 1/16(×4) | 轮廓边缘开始分离 |
| 1.2 m | 1/1.44(×44) | 两条轮廓明显分家 |
画面上”白色实线(透视真实轮廓)与蓝色虚线(切平面近似)分家”,就是线性化在报警。
工程含义
- 受害最大的是”怼脸”高斯:离相机近、张角大的高斯,真实轮廓不再是椭圆(透视下呈扇形/香蕉形),椭圆近似画不准,常见于近处地面、墙角。
- 这是 EWA 投影的固有近似,不是实现 bug。原论文(Zwicker et al.)同样采用仿射近似,并明确讨论其局限。
- 实践对策(本系列后续涉及):致密化把大高斯拆小,缩小每个高斯的张角,让”切点附近”重新成立——所以”大高斯必须 split”不只是分辨率问题,也是线性化精度问题。
参考
概念②:逆矩阵怎么进指数
起点:二维高斯密度
屏幕上某像素相对椭圆中心的偏移记为 Δ=(dx,dy),二维高斯(Gaussian)密度:
N(Δ)=2π1∣Σ∣−1/2exp(−21ΔTΣ−1Δ)
这里的 Σ 就是前面算出的 cov2d。指数里的 Σ−1 不是装饰——它来自”按椭圆形状折算距离”的需求。
Σ⁻¹ 的几何:马氏距离
二次型 d2=ΔTΣ−1Δ 就是马氏距离(Mahalanobis distance)的平方。把 Σ 对角化看最清楚:若 Σ=diag(σx2,σy2),则
d2=σx2dx2+σy2dy2
每个方向的位移先除以该方向的标准差再平方:沿椭球长轴走 70 px 和沿短轴走 23 px,算出来的”距离”一样。瘦方向(σ 小)的位移被放大,密度衰减得快;胖方向被放宽,衰减得慢——等值线 d=const 恰好是一族同心椭圆,这正是”逆矩阵跑进指数”的全部原因:d=const 即 ΔTΣ−1Δ=const,是二次曲线方程。
四步代数:从密度到 α
- 写出密度:N(Δ)=(2π)−1∣Σ∣−1/2exp(−21ΔTΣ−1Δ)
- 提出常系数:(2π)−1∣Σ∣−1/2 与 Δ 无关,是”这个椭圆整体多亮”的标量
- 峰值归一:把这个系数丢掉,让椭圆中心处值恰为 1。形状信息全部在指数里,归一只改整体幅度,不改轮廓
- 乘不透明度 o:把幅度交给可学习参数,得到
α=o⋅exp(power),power=−21ΔTconicΔ,conic=cov2d−1
性质自查:Δ=0 时 α=o(中心处不透明度就是 o);o∈(0,1] 保证 α≤1,合成不会溢出。
conic 的名字与 2×2 求逆
conic 得名于二次曲线(conic sections):ΔTconicΔ=const 就是椭圆的标准方程。2×2 对称矩阵求逆有闭式,若 cov2d=[abbc],det=ac−b2:
conic=det1[c−b−ba]
这正是参考实现里 conic = {c*invdet, -b*invdet, a*invdet} 三行的来历——存三个数而不是四个,因为对称。
2.83σ 截断
高斯拖着无穷长的尾巴,屏幕足迹必须截断。沿长轴取 Δ=kσ,则 power=−21k2。参考实现取 k=2.83(≈22):
power=−21×2.832≈−4.00,e−4≈1.8%
截断处密度只剩峰值的约 1.8%,再往外对像素的贡献低于可感阈值——四舍五入”免费”扔掉了 98% 以上的尾巴。
参考
概念②续:+0.3 抗混叠
问题:亚像素高斯
远处/微型高斯投到屏幕上可能只有亚像素大小——例如半轴 0.23 px(方差 ≈0.053 px2)的迷你椭圆,直径不到半个像素。它在像素网格上移动时,会整格亮、整格灭:走一步,覆盖的像素集合突变一次,画面抖动、闪烁——这就是锯齿,即混叠(aliasing)。
病根是采样定理的老道理:信号的空间频率超过了采样网格能表达的上限。椭圆比像素格子还小,网格根本”采不到”它的连续形状,只剩粗糙的有/无判决。
修法:cov2d 对角线 +0.3
在求逆(算 conic)之前,给屏幕协方差的对角线各加 0.3:
cov2d′=cov2d+0.3I(单位:px2)
效果是把每个高斯强制膨胀到至少接近格子尺度。数值验证:半轴 0.23 px 的椭圆,方差 0.053;加 0.3 后 0.053+0.3=0.353,0.353=0.59 px——半轴从 0.23 涨到约 0.6 px,边缘跨过相邻像素、亮度渐变,锯齿被抹平。
几何解读:对角线加常数 = 与原椭圆同心、各向同性(圆形)的模糊核做卷积,等价于给渲染结果叠了一个最小尺度的高斯低通滤波器——正是抗混叠(anti-aliasing)滤波的标准做法。
为什么只加 0.3、只救小的
- 大椭圆无感:70 px 半轴的椭圆方差是 4920 px²,加 0.3 的相对改变约 6×10−5,轮廓纹丝不动。这个修正是”保底”,不是全局模糊。
- 0.3 是方差(px²),不是半轴长度。这是最容易记错的一点:对应的半轴下限是 0.3≈0.55 px。若错记成”半轴加 0.3”,亚像素高斯会被过度膨胀、画面发糊。
- 顺序重要:必须先加 0.3 再求逆得 conic,且参与截断半径计算的是加过的协方差。顺序反了,迷你高斯的足迹和密度会不一致。
实现出处
INRIA 参考实现的 CUDA 前向里就是这两行:cov2D[0] += 0.3f; cov2D[2] += 0.3f;(对角两个元素),随后才算行列式与 conic。antimatter15 的 WebGL 查看器语义相同。
参考
概念③:under 合成逐行演算
递推两行
沿视线把命中该像素的高斯从近到远排好,维护两个量:累计颜色 C 和透射率(transmittance)T——还剩多少光能穿过来。每个高斯两步:
C←C+ci⋅αi⋅T,T←T⋅(1−αi)
初值 C=0、T=1。αi 是当前高斯在该像素的不透明度,ci 是它的颜色。直觉:第 i 层能贡献的光,要先被前面所有层”挡剩下的比例” T=∏j<i(1−αj) 打过折。
逐行演算(4 个高斯叠一个像素)
| i | αi | ci | 进入时 T | 贡献 ciαiT | 离开 T×(1−αi) |
|---|
| 1 | 0.50 | 0.2 | 1.000 | 0.100 | 0.500 |
| 2 | 0.80 | 0.7 | 0.500 | 0.280 | 0.100 |
| 3 | 0.95 | 0.4 | 0.100 | 0.038 | 0.005 |
| 4 | 0.99 | 0.9 | 0.005 | 0.004 | 5×10−5 |
累计 C:0.100→0.380→0.418→0.422。注意第 4 个高斯 α=0.99 几乎全遮挡,贡献却只有 0.004——晚到的高斯,自身 α 再大,也翻不过 T 这道墙。
早退:T < 1e-4 凭什么敢跳
第 4 步后 T=5×10−5<10−4,第 5 个高斯直接跳过,记 n_contrib = 4。这不是玄学阈值,有一条严格的误差界:设颜色 c∈[0,1],被跳过的尾巴无论还有多少个高斯,其总贡献
Ctail=T⋅(后续合成出的颜色)≤T<10−4
尾巴上界就是当前 T。10−4 远小于 8-bit 量化的最小台阶(1/255≈4×10−3),跳过在显示层面不可见。早退同时是性能开关:近处一堵密实墙之后,成千人深的高斯全部免算。
易错点
- 顺序不能反。这套递推是经典的 front-to-back α 混合(与”over”算子同一数学、不同展开方向),依赖深度排序正确;顺序错了,遮挡关系整体颠倒。排序是下一集的主题。
n_contrib 必须存档,不只为了性能统计:反向传播要按完全相同的截断点逆序重放,少一个多一个,T 链就错位一个 (1−α) 因子——这是真实训练器踩过的坑。
- T 是”进入第 i 层之前”的值。贡献用旧 T,然后才乘 (1−αi)。两行顺序写反,第一层就错了。
参考
契约固化与玩具总表
可抄的伪代码
# 近 → 远,逐个混合(伪代码)
C = 0; T = 1
for i in 近到远:
α = o_i * exp(power_i) # power_i = -½Δᵀ·conic_i·Δ
C += c_i * α * T
T *= (1 - α)
if T < 1e-4:
n_contrib = i; break
这段就是整个 3DGS 前向的”最后一公里”。前面所有矩阵运算(Σ → Vrk → cov2d → conic)只为每行循环提供 α;真正的合成只有乘加两行加一个早退判断。
前向存档三样:T、acc、n_contrib
循环结束时,每个像素只存三样东西:
T:末端透射率。若未触发早退,它还承担背景合成:最终颜色 =C+T⋅cbg。
acc:C 的累计值(accumulated color)。第五集反向传播时,同一个累计量换个名字继续用。
n_contrib:早退截止下标。
为什么恰好是这三样?反向要沿 T 链逆序重放:由末端 T 逐步除回 Ti=T/(1−αi),逐层回收每层的透射率;重放到第几层由 n_contrib 界定。存中间所有 Ti 会爆显存(每像素每高斯一个数),存这三样则重放时零冗余——这是前向给反向留的最小充分遗产。第五集反向缺了 n_contrib、T 链错位一个 (1−α) 因子,是真实训练器里抓出来的 bug。
玩具总表(全集数字锚点)
| 量 | 值 | 出处 |
|---|
| 屏幕椭圆(0.3 m @ 4 m 深) | 1σ 半轴 70.2 × 23.4 px | B45 手算 |
| 椭圆中心 α | =o(取 0.9) | B48 峰值归一 |
| +0.3 膨胀下限 | 半轴 ≥ 0.55 px | B49 |
| 4 高斯叠一像素 | C=0.422,末端 T=5×10−5 | B50 演算 |
| 早退阈值 / 截止 | T<10−4,n_contrib = 4 | B50 |
这组数字是第五集反向推导的公共基准:每条反向公式都会回到”70×23 的椭圆、α=0.9、C=0.422”这套玩具算例上验算。