Rust图像处理第20节-PCA 主成分分析:把图片压缩到 3 个数字

4 阅读5分钟

🦀 Rust + WASM 实战系列 第 20 篇 阅读时间:约 6 分钟 | 实战可运行

📌 写在前面

RGB 图像每个像素有 3 个数字(R, G, B)。100×100 的图 = 10000 个像素 = 30000 个数字

PCA 做的事:找 3 个"最有代表性"的方向,把 30000 维数据压成 k 维(k ≤ 3)——信息损失最小。

直觉:10000 个像素的 RGB 三通道里,大部分变化都沿着 2~3 个方向——抓住这 2~3 个方向,就抓住了图的大部分信息。


🚀 TL;DR

7 步流程(每张图都走一遍):

1. 像素展平为 (n × 3) 矩阵
2. 中心化(每列减均值)
3. 算协方差矩阵 C = X^T × X / (n-1)(3 × 3)
4. SVD 分解:C = U × S × V^T
5. 保留前 k 个主成分(U 的前 k 列)
6. 投影:X_projected = X_centered × P(n × k)
7. 重建:X_reconstructed = X_projected × P^T + mean

代码用 nalgebraDMatrix.transpose() * &DMatrix 算协方差,SVD::new() 分解,库调用搞定。


📖 目录

  1. 核心思路:把图看成"数据点云"
  2. 7 步流程详解
  3. 关键代码(用库调用)
  4. 前端效果展示
  5. k 怎么选?3 个主成分的方差占比
  6. 踩坑提醒
  7. 接下来

一、核心思路:把图看成"数据点云"

把图片每个像素看成一个3 维点 (R,G,B)(R, G, B)

100 × 100 图 = 10000 个点(每个点是 RGB 三维向量)

这些点聚集在某个区域(不是随机散布)——比如所有像素可能都集中在 (200,150,100)(200, 150, 100) 附近。

PCA 找的是:这些数据变化最剧烈的几个方向(主成分)。

类比:一个倾斜的椭圆——长轴方向 = 第一主成分,短轴方向 = 第二主成分。


二、7 步流程详解

1、先看一个完整示例(4 像素玩具图)

为了把抽象公式变成可手算的数字,下面用一张 2×2 玩具图(4 个像素)走完 7 步。所有中间结果显式列出

像素数据

4 个像素(RGB,alpha 省略):

像素位置RGB
p1p_1左上200100100
p2p_2右上100200100
p3p_3左下200100200
p4p_4右下100200200

关键观察:4 个像素的 R、G、B 互不相同——是个彩色图。但有一个不变量:每个像素都满足 R+G=300R + G = 300。这意味着 4 个点全部落在"R+G=300R+G=300"这个 2 维平面上(3 维空间里的平面是 2 维),所以数据本质只有 2 个自由度——k = 2 就能完美重建,k = 1 会丢信息。

步骤 1:展平为 (4×3)(4 \times 3) 矩阵

X=[200100100100200100200100200100200200]X = \begin{bmatrix} 200 & 100 & 100 \\ 100 & 200 & 100 \\ 200 & 100 & 200 \\ 100 & 200 & 200 \end{bmatrix}

n=4n = 4(行数 = 像素数),3 列对应 R/G/B。

步骤 2:中心化

每列均值(本例三列均值恰好都是 150,是数据对称带来的巧合):

Xˉ=(200+100+200+1004, 100+200+100+2004, 100+100+200+2004)=(150, 150, 150)\bar{X} = \left( \frac{200+100+200+100}{4},\ \frac{100+200+100+200}{4},\ \frac{100+100+200+200}{4} \right) = (150,\ 150,\ 150)

减均值只做一件事:把坐标原点搬到数据中心。 它是一次整体平移——所有点朝同一方向挪同样的距离,点云的形状、朝向、维数都不变(平移永远不会改变点之间的相对关系)。

为什么要搬原点? PCA 的主方向是从原点出发去拟合这团点的。如果原点留在黑色 (0,0,0)(0,0,0),拟合方向会被"整团点离原点多远"(绝对位置 / 整体亮度)带偏;把原点挪到数据中心后,主方向才只反映点与点之间怎么散开——这才是我们想要的"偏离模式"。

中心化后:

Xc=XXˉ=[505050505050505050505050]X_c = X - \bar{X} = \begin{bmatrix} \phantom{-}50 & -50 & -50 \\ -50 & \phantom{-}50 & -50 \\ \phantom{-}50 & -50 & \phantom{-}50 \\ -50 & \phantom{-}50 & \phantom{-}50 \end{bmatrix}

观察(验证"R+G=300"那个不变量真的生效):去均值后 R+G 在每行都是 0——即数据全落在 R+G=0R+G=0 这个过原点的平面里(3 维空间里的平面 = 2 维)。这就是为什么后面 PC3 对应方向 (1,1,0)(1,1,0) 的方差是 0——沿着 (1,1,0)(1,1,0) 方向没有任何变化

步骤 3:算 3×3 协方差矩阵

公式:Cij=1n1p=1nXp,iXp,jC_{ij} = \dfrac{1}{n-1} \sum_{p=1}^{n} X_{p,i} \cdot X_{p,j}

对角元素i=ji = j,本质是方差):

CRR=CGG=CBB=13(502+502+502+502)=1000033333.33C_{RR} = C_{GG} = C_{BB} = \frac{1}{3}(50^2 + 50^2 + 50^2 + 50^2) = \frac{10000}{3} \approx 3333.33

非对角元素iji \neq j,本质是协方差):

CRG=13(50(50)+(50)50+50(50)+(50)50)=13(10000)=1000033333.33C_{RG} = \frac{1}{3}(50 \cdot (-50) + (-50) \cdot 50 + 50 \cdot (-50) + (-50) \cdot 50) = \frac{1}{3}(-10000) = -\frac{10000}{3} \approx -3333.33
CRB=CGB=13(50(50)+(50)(50)+5050+(50)50)=13(0)=0C_{RB} = C_{GB} = \frac{1}{3}(50 \cdot (-50) + (-50) \cdot (-50) + 50 \cdot 50 + (-50) \cdot 50) = \frac{1}{3}(0) = 0

协方差矩阵

C=100003[110110001][3333.333333.3303333.333333.330003333.33]C = \frac{10000}{3} \begin{bmatrix} \phantom{-}1 & -1 & \phantom{-}0 \\ -1 & \phantom{-}1 & \phantom{-}0 \\ \phantom{-}0 & \phantom{-}0 & \phantom{-}1 \end{bmatrix} \approx \begin{bmatrix} \phantom{-}3333.33 & -3333.33 & \phantom{-}0 \\ -3333.33 & \phantom{-}3333.33 & \phantom{-}0 \\ \phantom{-}0 & \phantom{-}0 & \phantom{-}3333.33 \end{bmatrix}

直觉解读

  • CRR=CGG=CBBC_{RR} = C_{GG} = C_{BB}:三通道各自波动幅度一样大。
  • CRG=CRRC_{RG} = -C_{RR}:R 和 G 完全反相关——R 涨多少 G 就跌多少(因为 R+G=300R+G=300 是常数)。
  • CRB=CGB=0C_{RB} = C_{GB} = 0:B 跟 R/G 完全独立——B 想怎么变就怎么变,不受 R+G 约束。

协方差矩阵把"R+G=300R+G=300、B 自由"这个数据特性完美编码进去了。

步骤 4:SVD 分解

CC 是实对称矩阵(C=CTC = C^T),所以 SVD 等价于特征分解:C=UΣUTC = U \Sigma U^T(即 V=UV = U),UU 的列就是特征向量,Σ\Sigma 的对角元是特征值(也就是每个主成分方向的方差)。

算 3 个特征值——解特征方程 det(CλI)=0\det(C - \lambda I) = 0

注意:这里用手动计算,计算机中可以通过svd直接求解出VUΣ

CC 写成 100003M\dfrac{10000}{3} M,其中

M=[110110001]M = \begin{bmatrix} 1 & -1 & 0 \\ -1 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}

λ=3λ10000\lambda' = \dfrac{3\lambda}{10000},则特征方程化为 det(MλI)=0\det(M - \lambda' I) = 0

小检查(迹)MM 的迹是 1+1+1=31+1+1=3,所以 λ1+λ2+λ3=100003×3=10000\lambda_1+\lambda_2+\lambda_3 = \dfrac{10000}{3} \times 3 = 10000——后面算完可以核一下。

沿第 3 行展开(这一行有 2 个 0,最省事):

det(MλI)=(1λ)det[1λ111λ]\det(M - \lambda' I) = (1 - \lambda') \cdot \det\begin{bmatrix}1-\lambda' & -1 \\ -1 & 1-\lambda'\end{bmatrix}

2×2 行列式按"主对角相乘 - 副对角相乘":

det[1λ111λ]=(1λ)2(1)(1)=(1λ)21\det\begin{bmatrix}1-\lambda' & -1 \\ -1 & 1-\lambda'\end{bmatrix} = (1-\lambda')^2 - (-1)(-1) = (1-\lambda')^2 - 1

所以特征方程是:

(1λ)[(1λ)21]=0(1 - \lambda') \cdot [(1-\lambda')^2 - 1] = 0
(1λ)(1λ1)(1λ+1)=0(1 - \lambda') \cdot (1-\lambda'-1) \cdot (1-\lambda'+1) = 0
(1λ)(λ)(2λ)=0(1 - \lambda') \cdot (-\lambda') \cdot (2-\lambda') = 0

解得 λ=0, 1, 2\lambda' = 0,\ 1,\ 2。乘回 100003\dfrac{10000}{3}(并按从大到小排):

λ1=200003,λ2=100003,λ3=0\lambda_1 = \dfrac{20000}{3},\quad \lambda_2 = \dfrac{10000}{3},\quad \lambda_3 = 0

验证:λ1+λ2+λ3=200003+100003+0=10000\lambda_1 + \lambda_2 + \lambda_3 = \dfrac{20000}{3} + \dfrac{10000}{3} + 0 = 10000

找 3 个特征向量——对每个 λ\lambda(MλI)v=0(M - \lambda' I) v = 0

  • λ1\lambda_1λ=2\lambda'=2):M2I=[110110001]M - 2I = \begin{bmatrix}-1 & -1 & 0 \\ -1 & -1 & 0 \\ 0 & 0 & -1\end{bmatrix}v3=0v_3=0v1=v2v_1=-v_2u1=(1,1,0)T/2u_1 = (1,-1,0)^T / \sqrt{2}
  • λ2\lambda_2λ=1\lambda'=1):MI=[010100000]M - I = \begin{bmatrix}0 & -1 & 0 \\ -1 & 0 & 0 \\ 0 & 0 & 0\end{bmatrix}v1=v2=0v_1=v_2=0v3v_3 自由 → u2=(0,0,1)Tu_2 = (0,0,1)^T
  • λ3\lambda_3λ=0\lambda'=0):M=[110110001]M = \begin{bmatrix}1 & -1 & 0 \\ -1 & 1 & 0 \\ 0 & 0 & 1\end{bmatrix}v1=v2v_1=v_2v3=0v_3=0u3=(1,1,0)T/2u_3 = (1,1,0)^T / \sqrt{2}

UU 排好(每列一个主成分,从大到小):

U=[0.707100.70710.707100.7071010]U = \begin{bmatrix} \phantom{-}0.7071 & 0 & \phantom{-}0.7071 \\ -0.7071 & 0 & \phantom{-}0.7071 \\ \phantom{-}0 & 1 & \phantom{-}0 \end{bmatrix}

奇异值 σi=λi\sigma_i = \sqrt{\lambda_i}

  • σ1=20000/381.65\sigma_1 = \sqrt{20000/3} \approx 81.65
  • σ2=10000/357.74\sigma_2 = \sqrt{10000/3} \approx 57.74
  • σ3=0\sigma_3 = 0
Σ=[81.6500057.740000]\Sigma = \begin{bmatrix} \phantom{-}81.65 & 0 & 0 \\ \phantom{-}0 & 57.74 & 0 \\ \phantom{-}0 & 0 & 0 \end{bmatrix}

3 个主成分方向UU 的 3 列):

主成分方向向量(归一化)物理含义σ2\sigma^2 = 方差方差占比
PC1(1,1,0)/2(0.707, 0.707, 0)(1,-1,0)/\sqrt{2} \approx (0.707,\ -0.707,\ 0)"红-绿"差异轴:R 涨 G 跌(因为 R+GR+G 守恒)20000/36666.6720000/3 \approx 6666.672/3 ≈ 66.7%
PC2(0,0,1)(0,0,1)"蓝"独立轴:B 自由变化10000/33333.3310000/3 \approx 3333.331/3 ≈ 33.3%
PC3(1,1,0)/2(0.707, 0.707, 0)(1,1,0)/\sqrt{2} \approx (0.707,\ 0.707,\ 0)"灰阶"方向——但这里没有变化00%

结论:PC1 + PC2 的方差占比 = 100%,两个方向就够。PC3 沿 (1,1,0)(1,1,0) 方向方差为 0,正好对应"R+GR+G 守恒"那个约束——SVD 把数据里的 2 维结构精确地挖了出来。

步骤 5:取前 k 个主成分

  • k=1k = 1P=u1=(1/2, 1/2, 0)TP = u_1 = (1/\sqrt{2},\ -1/\sqrt{2},\ 0)^T(3×1)——只捕捉 R-G 差异,B 通道被丢掉,重建会丢信息
  • k=2k = 2P=[u1  u2]P = [u_1\ |\ u_2](3×2)——把 R-G 差异和 B 都捕捉了,完美重建(因为数据天然只有这 2 个自由度)
  • k=3k = 3P=UP = U——u3u_3 对应奇异值为 0,加了也不增加信息(与 k=2 等价)

关键:本例是 k=2k=2 完美、k=1k=1 失真、k=3k=3 浪费。这跟前面 §5 讲的"自然图 k=1 就够"不同——自然图的灰阶轴真的是数据主方向,而这个玩具图里灰阶方向是死的(方差为 0),所以主方向变成 R-G 差异轴。

步骤 6:投影(k = 2)

公式:Xproj=XcPX_{\text{proj}} = X_c \cdot P,其中 P=[u1  u2]P = [u_1\ |\ u_2](3×2 矩阵)。

展开每一行:第 pp 行投影 = 中心化像素向量与 u1u_1u2u_2 的点积。

  • u1=(1,1,0)/2u_1 = (1,-1,0)/\sqrt{2} 点积 = RcGc2\dfrac{R_c - G_c}{\sqrt{2}}
  • u2=(0,0,1)u_2 = (0,0,1) 点积 = BcB_c
像素(RcGc)/2(R_c - G_c)/\sqrt{2}BcB_c投影(x(1),x(2))(x^{(1)}, x^{(2)})
p1p_1(50(50))/2=100/270.71(50-(-50))/\sqrt{2} = 100/\sqrt{2} \approx 70.7150-50(70.71, 50)(70.71,\ -50)
p2p_2(5050)/2=100/270.71(-50-50)/\sqrt{2} = -100/\sqrt{2} \approx -70.7150-50(70.71, 50)(-70.71,\ -50)
p3p_3(50(50))/270.71(50-(-50))/\sqrt{2} \approx 70.715050(70.71, 50)(70.71,\ \phantom{-}50)
p4p_4(5050)/270.71(-50-50)/\sqrt{2} \approx -70.715050(70.71, 50)(-70.71,\ \phantom{-}50)
Xproj=[70.715070.715070.715070.7150](4×2)X_{\text{proj}} = \begin{bmatrix} \phantom{-}70.71 & -50 \\ -70.71 & -50 \\ \phantom{-}70.71 & \phantom{-}50 \\ -70.71 & \phantom{-}50 \end{bmatrix} \quad (4 \times 2)

数据量从 4×3=124 \times 3 = 12 个数压到 4×2=84 \times 2 = 8 个数(节省 33%)。如果想更省、选 k=1,那就只剩 4 个数,但 B 通道就丢了——下一步能看到丢多少。

步骤 7:重建(k = 2)

公式:Xrecon=XprojPT+XˉX_{\text{recon}} = X_{\text{proj}} \cdot P^T + \bar{X}

每像素的重建 = 投影分量沿 u1u_1u2u_2 还原回去 + 加回均值:

Xrecon,p=xp(1)u1+xp(2)u2+(150,150,150)X_{\text{recon},p} = x^{(1)}_p \cdot u_1 + x^{(2)}_p \cdot u_2 + (150, 150, 150)
像素x(1)u1x^{(1)} \cdot u_1x(2)u2x^{(2)} \cdot u_2+Xˉ\bar{X}= 重建 RGB
p1p_170.71(0.7071,0.7071,0)=(50,50,0)70.71 \cdot (0.7071,-0.7071,0) = (50,-50,0)50(0,0,1)=(0,0,50)-50 \cdot (0,0,1) = (0,0,-50)(150,150,150)(200, 100, 100)
p2p_270.71(0.7071,0.7071,0)=(50,50,0)-70.71 \cdot (0.7071,-0.7071,0) = (-50,50,0)50(0,0,1)=(0,0,50)-50 \cdot (0,0,1) = (0,0,-50)(150,150,150)(100, 200, 100)
p3p_370.71(0.7071,0.7071,0)=(50,50,0)70.71 \cdot (0.7071,-0.7071,0) = (50,-50,0)50(0,0,1)=(0,0,50)50 \cdot (0,0,1) = (0,0,50)(150,150,150)(200, 100, 200)
p4p_470.71(0.7071,0.7071,0)=(50,50,0)-70.71 \cdot (0.7071,-0.7071,0) = (-50,50,0)50(0,0,1)=(0,0,50)50 \cdot (0,0,1) = (0,0,50)(150,150,150)(100, 200, 200)

k = 2 重建精确等于原图! 每个像素的 3 个通道都对得上原始值。

对比 k = 1 会怎样? 假如刚才投影时只取 u1u_1(k=1),B 通道被丢,重建时 p1p_1 只会得到 (200,100,150)(200, 100, 150)——B 通道被均值"填"回去了,但跟真实值 100100 差 50。这正说明:k 选多大,看数据本身有几个自由度——本例 2 个自由度,k=2 才能完美。

小结:4 像素玩具图告诉我们什么

步骤关键数字直觉
X (n × 3)4×3 = 12 个数每像素 1 行
中心化每像素减 (150,150,150)原点搬到数据中心;R+G=300 → 中心化后 R+G=0
协方差 C100003[110110001]\frac{10000}{3}\begin{bmatrix}1&-1&0\\-1&1&0\\0&0&1\end{bmatrix}R 和 G 完全反相关;B 与 R/G 独立
SVDPC1 = (0.71,-0.71,0),σ12=200003\sigma_1^2 = \frac{20000}{3}
PC2 = (0,0,1), σ22=100003\sigma_2^2 = \frac{10000}{3}
PC3 = (0.71,0.71,0), σ32=0\sigma_3^2 = 0
2 个非零方向,PC3 沿灰阶方向但没变化
投影 k=2(±70.71, ±50)(\pm 70.71,\ \pm 50) 四个12 → 8 个数
重建与原图精确相等k = 2 才完美(k=1 丢 B 通道)

核心结论:这个玩具图天然是 2 维的(受 R+G=300R+G=300 这一个约束限制),所以 k=2 完美重建。这跟"自然图通常 k=1 就够"并不矛盾——自然图里大量像素近似灰阶(肤色、天空、阴影),整个数据集的有效维度也远小于 3,但有效维数到底是 1 还是 2 还是 3,取决于数据本身。本节用具体数字把 PCA 流程走通;真实图片的方差占比、长什么样、k 怎么选见 § 5


2、PCA实现完整流程

1. 像素展平为 (n×3)(n \times 3) 矩阵

X=[R1G1B1R2G2B2RnGnBn]X = \begin{bmatrix} R_1 & G_1 & B_1 \\ R_2 & G_2 & B_2 \\ \vdots & \vdots & \vdots \\ R_n & G_n & B_n \end{bmatrix}

nn = 像素数,3 列 = RGB。

2. 中心化(每列减均值)

Xcentered=XXˉX_{\text{centered}} = X - \bar{X}

其中 Xˉ\bar{X} 是每列的均值向量。减掉均值后,数据中心在原点——PCA 找的是"偏离方向"。

3. 协方差矩阵 C=XTX/(n1)C = X^T X / (n-1)

C=[Var(R)Cov(R,G)Cov(R,B)Cov(G,R)Var(G)Cov(G,B)Cov(B,R)Cov(B,G)Var(B)]C = \begin{bmatrix} \text{Var}(R) & \text{Cov}(R,G) & \text{Cov}(R,B) \\ \text{Cov}(G,R) & \text{Var}(G) & \text{Cov}(G,B) \\ \text{Cov}(B,R) & \text{Cov}(B,G) & \text{Var}(B) \end{bmatrix}

3×3 矩阵——描述 R、G、B 三通道两两之间的相关性

4. SVD 分解

C=UΣVTC = U \Sigma V^T
  • UU:3×3 正交矩阵,每列是一个主成分方向
  • Σ\Sigma:3 个奇异值,每个值表示该主成分的"信息量"

5. 保留前 k 个主成分

UU 的前 k 列 → 组成 PP(3 × k 矩阵)。

  • k=1k = 1:1 个方向 → 数据量从 3 维压到 1 维(严重失真)
  • k=2k = 2:2 个方向 → 3 维压到 2 维(较好)
  • k=3k = 3:3 个方向 → 不压缩(理论无损)

6. 投影

Xproj=Xcentered×P(n×k)X_{\text{proj}} = X_{\text{centered}} \times P \quad (n \times k)

把 10000 像素的 3 维 RGB → 10000 像素的 k 维。

7. 重建

Xrecon=Xproj×PT+Xˉ(n×3)X_{\text{recon}} = X_{\text{proj}} \times P^T + \bar{X} \quad (n \times 3)

把 k 维数据反投影回 3 维 RGB + 加回均值 = 重建图。


三、关键代码(用库调用)

use nalgebra::{DMatrix, DVector};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn pca_compress(pixels: &[u8], width: u32, height: u32, k: u32) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let n = w * h;
    let k = (k as usize).min(3).max(1);

    // 1. 像素 → 矩阵 (n × 3)
    let mut data = DMatrix::<f64>::zeros(n, 3);
    for i in 0..n {
        let idx = i * 4;
        data[(i, 0)] = pixels[idx] as f64;
        data[(i, 1)] = pixels[idx + 1] as f64;
        data[(i, 2)] = pixels[idx + 2] as f64;
    }

    // 2. 中心化
    let mean = data.row_mean();
    let mut centered = data.clone();
    for j in 0..3 {
        for i in 0..n {
            centered[(i, j)] -= mean[j];
        }
    }

    // 3. 协方差矩阵
    let cov = (centered.transpose() * &centered) / ((n - 1) as f64);

    // 4. SVD 分解
    let svd = nalgebra::linalg::SVD::new(cov, true, true);
    let u = svd.u.unwrap();

    // 5. 取前 k 个主成分
    let p = u.columns(0, k).clone();

    // 6. 投影
    let projected = &centered * &p;

    // 7. 重建 + 加回均值
    let mut reconstructed = projected * p.transpose();
    for i in 0..n {
        reconstructed[(i, 0)] += mean[0];
        reconstructed[(i, 1)] += mean[1];
        reconstructed[(i, 2)] += mean[2];
    }

    // 写回 RGBA(alpha 复制)
    let mut out = vec![0u8; pixels.len()];
    for i in 0..n {
        let dst_idx = i * 4;
        out[dst_idx]     = reconstructed[(i, 0)].clamp(0.0, 255.0) as u8;
        out[dst_idx + 1] = reconstructed[(i, 1)].clamp(0.0, 255.0) as u8;
        out[dst_idx + 2] = reconstructed[(i, 2)].clamp(0.0, 255.0) as u8;
        out[dst_idx + 3] = pixels[dst_idx + 3];
    }

    out
}

直接调用库,没自己写方法


四、前端效果展示

e88bd4ef-cf79-4c2f-a2c3-a195b5a16943.png

打开页面后:

  1. 上传一张图片
  2. 拖动 k 滑块(1~3),实时看重建效果
  3. 下方有 3 个主成分的方差占比柱状图——直观看到 PC1 通常占大头
  4. 对比同一张图在不同 k 下的质量:
    • k = 1:严重失真(颜色偏移、细节丢失)
    • k = 2:较好(能看出原图,颜色略有偏差)
    • k = 3:理论无损

五、k 怎么选?3 个主成分的方差占比

5.1 经验数字

k数据量视觉质量累计方差占比(典型自然图)
11/3失真80~95%
22/3较好95~99%
3100%无损100%

经验法则

  • PC1 通常占 80~95%(一维就够)
  • PC1 + PC2 通常占 95~99%(2 维基本无损)
  • PC3 只占 1~5%——几乎总是可以扔掉

5.2 为什么 PC1 ≈ (0.577, 0.577, 0.577)?

自然图的 PC1 通常极其接近灰阶轴——三个分量都约 0.577(= 1/31/\sqrt{3})。

这是物理结果,不是巧合

  1. 同一物体的 R/G/B 几乎同步变化。比如把一张叶子从亮处移到阴影:R、G、B 同时按比例降低(亮度降但色相不变)。这种"整体变亮/变暗"的像素差异,方向就是 (1,1,1)
  2. 颜色恒常性:人眼对绝对亮度敏感,但对"相对色相"钝感。所以拍摄同一场景时,三通道几乎总是成比例变化——协方差矩阵的"长轴"自然指向 (1,1,1) 方向。
  3. 极端的"灰阶图"(R=G=B)会得到 PC1 严格沿 (1,1,1)、占 100%——但这是 R=G=B 这个强约束导致的特例,不是一般情形。§ 二.0 的玩具图虽然 R≠G≠B,但因为 R+GR+G 守恒,灰阶方向 (1,1,0)(1,1,0) 的方差也是 0——这说明"灰阶方向是否占主"完全取决于数据。

5.3 一个具体自然图的方差占比

把 § 二.0 的 4 像素玩具图替换为一张 100×100 自然照片(如风景照、人像),PCA 给出的 3 个奇异值通常呈下面这种形态:

σ18500,σ21200,σ3300\sigma_1 \approx 8500,\quad \sigma_2 \approx 1200,\quad \sigma_3 \approx 300
主成分奇异值² = 方差方差占比直观解释
PC1 ≈ (0.58, 0.58, 0.58)85002=72,250,0008500^2 = 72{,}250{,}00096.6%灰阶轴(亮度变化)
PC2 ≈ (0.71, -0.71, 0)12002=1,440,0001200^2 = 1{,}440{,}0001.9%"红-绿"色相偏差
PC3 ≈ (0.41, 0.41, -0.82)3002=90,000300^2 = 90{,}0000.1%"黄-蓝"色相偏差

怎么看这个表:总和 ≈ 73,780,000。PC1 = 72,250,000 / 73,780,000 ≈ 97.9%(具体数字随图像变化,但量级稳定)。1 个数字就能描述 96~98% 的差异——这就是 PCA 强大的原因。

5.4 为什么 RGB 图像这么容易压缩?

把上面的方差占比代入压缩比:

存储压缩比=k3×1累计方差占比(粗略)\text{存储压缩比} = \frac{k}{3} \times \frac{1}{\text{累计方差占比}} \text{(粗略)}
  • k = 1:压缩到 1/3 数据量,但保留 97% 信息 → 300% 效率
  • k = 2:压缩到 2/3 数据量,但保留 99% 信息 → 150% 效率

而 PNG/JPEG 不做这种"按方差排序"的取舍——它们用所有 3 个通道存数据,没有利用通道间相关性。PCA 之所以"信息密度高",正是因为 RGB 三通道高度相关(协方差矩阵的非对角元素接近对角元素),整张图的有效自由度远小于 3

5.5 何时 PC1 占大头?

PC1 主导依赖于"R/G/B 同步变化"。下面这些图会破坏这个假设:

图像类型典型 PC1 占比为什么
自然风景 / 人像80~95%灰阶轴主导(亮度变化)
卡通 / 二次元插画50~70%高饱和度色块(红 vs 黄 vs 蓝),通道同步
抽象彩色噪点图30~40%各通道独立波动,几乎无相关性
单色 / 调色板图99~100%退化情形(方差为 0)

实操建议:先看 PC1 占比再选 k。如果 PC1 ≥ 90%,大胆用 k = 1;如果 PC1 < 60%,说明图像偏"彩色抽象"风格,k = 2 或 k = 3 都行


六、踩坑提醒

1. 协方差矩阵是对称的

Cij=CjiC_{ij} = C_{ji}

SVD 分解也是对称矩阵,库会自动处理——但计算时只算上三角可以省一半时间。

2. 协方差 = 0 时 SVD 可能不稳定

如果图像纯色(所有像素 RGB 一样),协方差矩阵全 0,SVD 会除以 0

解决:库会返回 Option判 None

3. 像素数 < 4 时无法算协方差

n - 1 在分母——n = 0n = 1除以 0

解决:前端限制图片最小尺寸(10×10 以上)。

4. 调色板和 PCA 不能混

PCA 是逐像素独立——不像鱼眼那样"看邻居"。压缩后颜色可能略偏(因为协方差矩阵是全局的,不是局部的)。


七、接下来

任务 21:最小二乘回归——用矩阵求逆解"拟合"问题,做图像去噪。

PCA:找数据的主方向(无监督) 回归:找 y = ax + b 的最佳拟合线(监督)

应用:图片去噪、颜色校准——用线性关系"修正"问题像素。

任务 22:协方差矩阵手写——深挖 PCA 内部原理,库 vs 手算对比。


一句话总结

PCA = 找 3 个"最有代表性"的方向,把 RGB 像素从 3 维压到 k 维

算法核心:协方差矩阵 + SVD 分解——全部 nalgebra 库调用

拖 k 滑块看效果:k=1 大幅失真,k=2 基本无损,k=3 完全无损


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #图像处理 #PCA #主成分分析 #协方差 #SVD #nalgebra