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

🦀 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 步流程(每张图都走一遍):

ini 复制代码
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 步流程详解](#7 步流程详解 "#%E4%BA%8C7-%E6%AD%A5%E6%B5%81%E7%A8%8B%E8%AF%A6%E8%A7%A3")
  3. 关键代码(用库调用)
  4. 前端效果展示
  5. [k 怎么选?3 个主成分的方差占比](#k 怎么选?3 个主成分的方差占比 "#%E4%BA%94k-%E6%80%8E%E4%B9%88%E9%80%893-%E4%B8%AA%E4%B8%BB%E6%88%90%E5%88%86%E7%9A%84%E6%96%B9%E5%B7%AE%E5%8D%A0%E6%AF%94")
  6. 踩坑提醒
  7. 接下来

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

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

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

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

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

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


二、7 步流程详解

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

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

像素数据

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

像素 位置 R G B
p1 p_1 p1 左上 200 100 100
p2 p_2 p2 右上 100 200 100
p3 p_3 p3 左下 200 100 200
p4 p_4 p4 右下 100 200 200

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

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

X= 200 100 100 100 200 100 200 100 200 100 200 200 X = \begin{bmatrix} 200 & 100 & 100 \\ 100 & 200 & 100 \\ 200 & 100 & 200 \\ 100 & 200 & 200 \end{bmatrix} X= 200100200100100200100200100100200200

n=4n = 4 n=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) Xˉ=(4200+100+200+100, 4100+200+100+200, 4100+100+200+200)=(150, 150, 150)

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

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

中心化后:
Xc=X−Xˉ= −50 −50 −50 −50 −50 −50 −50 −50 −50 −50 −50 −50 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} Xc=X−Xˉ= −50−50−50−50−50−50−50−50−50−50−50−50

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

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

公式: Cij = 1n−1 ∑p=1n Xp,i ⋅ Xp,j C_{ij} = \dfrac{1}{n-1} \sum_{p=1}^{n} X_{p,i} \cdot X_{p,j} Cij=n−11∑p=1nXp,i⋅Xp,j

对角元素 i=ji = j i=j,本质是方差):
CRR = CGG = CBB =13(502+502+502+502)=100003≈3333.33 C_{RR} = C_{GG} = C_{BB} = \frac{1}{3}(50^2 + 50^2 + 50^2 + 50^2) = \frac{10000}{3} \approx 3333.33 CRR=CGG=CBB=31(502+502+502+502)=310000≈3333.33

非对角元素 i≠ji \neq j i=j,本质是协方差):
CRG =13(50⋅(−50)+(−50)⋅50+50⋅(−50)+(−50)⋅50)=13(−10000)=−100003≈−3333.33 C_{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 CRG=31(50⋅(−50)+(−50)⋅50+50⋅(−50)+(−50)⋅50)=31(−10000)=−310000≈−3333.33
CRB = CGB =13(50⋅(−50)+(−50)⋅(−50)+50⋅50+(−50)⋅50)=13(0)=0 C_{RB} = C_{GB} = \frac{1}{3}(50 \cdot (-50) + (-50) \cdot (-50) + 50 \cdot 50 + (-50) \cdot 50) = \frac{1}{3}(0) = 0 CRB=CGB=31(50⋅(−50)+(−50)⋅(−50)+50⋅50+(−50)⋅50)=31(0)=0

协方差矩阵
C=100003 −1 −1 −0 −1 −1 −0 −0 −0 −1 −3333.33 −3333.33 −0 −3333.33 −3333.33 −0 −0 −0 −3333.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} C=310000 −1−1−0−1−1−0−0−0−1 ≈ −3333.33−3333.33−0−3333.33−3333.33−0−0−0−3333.33

直觉解读

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

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

步骤 4:SVD 分解

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

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

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

CC C 写成 100003 M \dfrac{10000}{3} M 310000M,其中
M= 1 −1 0 −1 1 0 0 0 1 M = \begin{bmatrix} 1 & -1 & 0 \\ -1 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} M= 1−10−110001

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

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

沿第 3 行展开(这一行有 2 个 0,最省事):
det⁡(M−λ′I)=(1−λ′)⋅det⁡ 1−λ′ −1 −1 1−λ′ \det(M - \lambda' I) = (1 - \lambda') \cdot \det\begin{bmatrix}1-\lambda' & -1 \\ -1 & 1-\lambda'\end{bmatrix} det(M−λ′I)=(1−λ′)⋅det1−λ′−1−11−λ′

2×2 行列式按"主对角相乘 −- − 副对角相乘":
det⁡ 1−λ′ −1 −1 1−λ′ =(1−λ′)2−(−1)(−1)=(1−λ′)2−1 \det\begin{bmatrix}1-\lambda' & -1 \\ -1 & 1-\lambda'\end{bmatrix} = (1-\lambda')^2 - (-1)(-1) = (1-\lambda')^2 - 1 det1−λ′−1−11−λ′=(1−λ′)2−(−1)(−1)=(1−λ′)2−1

所以特征方程是:
(1−λ′)⋅(1−λ′)2−1=0(1 - \lambda') \cdot (1-\\lambda')\^2 - 1 = 0 (1−λ′)⋅(1−λ′)2−1=0
(1−λ′)⋅(1−λ′−1)⋅(1−λ′+1)=0(1 - \lambda') \cdot (1-\lambda'-1) \cdot (1-\lambda'+1) = 0 (1−λ′)⋅(1−λ′−1)⋅(1−λ′+1)=0
(1−λ′)⋅(−λ′)⋅(2−λ′)=0(1 - \lambda') \cdot (-\lambda') \cdot (2-\lambda') = 0 (1−λ′)⋅(−λ′)⋅(2−λ′)=0

解得 λ′=0, 1, 2\lambda' = 0,\ 1,\ 2 λ′=0, 1, 2。乘回 100003 \dfrac{10000}{3} 310000(并按从大到小排):
λ1=200003,λ2=100003,λ3=0 \lambda_1 = \dfrac{20000}{3},\quad \lambda_2 = \dfrac{10000}{3},\quad \lambda_3 = 0 λ1=320000,λ2=310000,λ3=0

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

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

  • λ1 \lambda_1 λ1( λ′=2\lambda'=2 λ′=2): M−2I= −1 −1 0 −1 −1 0 0 0 −1 M - 2I = \begin{bmatrix}-1 & -1 & 0 \\ -1 & -1 & 0 \\ 0 & 0 & -1\end{bmatrix} M−2I= −1−10−1−1000−1 → v3=0 v_3=0 v3=0, v1=−v2 v_1=-v_2 v1=−v2 → u1=(1,−1,0)T/2 u_1 = (1,-1,0)^T / \sqrt{2} u1=(1,−1,0)T/2
  • λ2 \lambda_2 λ2( λ′=1\lambda'=1 λ′=1): M−I= 0 −1 0 −1 0 0 0 0 0 M - I = \begin{bmatrix}0 & -1 & 0 \\ -1 & 0 & 0 \\ 0 & 0 & 0\end{bmatrix} M−I= 0−10−100000 → v1=v2=0 v_1=v_2=0 v1=v2=0, v3 v_3 v3 自由 → u2=(0,0,1)T u_2 = (0,0,1)^T u2=(0,0,1)T
  • λ3 \lambda_3 λ3( λ′=0\lambda'=0 λ′=0): M= 1 −1 0 −1 1 0 0 0 1 M = \begin{bmatrix}1 & -1 & 0 \\ -1 & 1 & 0 \\ 0 & 0 & 1\end{bmatrix} M= 1−10−110001 → v1=v2 v_1=v_2 v1=v2, v3=0 v_3=0 v3=0 → u3=(1,1,0)T/2 u_3 = (1,1,0)^T / \sqrt{2} u3=(1,1,0)T/2

UU U 排好(每列一个主成分,从大到小):
U= −0.7071 0 −0.7071 −0.7071 0 −0.7071 −0 1 −0 U = \begin{bmatrix} \phantom{-}0.7071 & 0 & \phantom{-}0.7071 \\ -0.7071 & 0 & \phantom{-}0.7071 \\ \phantom{-}0 & 1 & \phantom{-}0 \end{bmatrix} U= −0.7071−0.7071−0001−0.7071−0.7071−0

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

  • σ1= 20000/3 ≈81.65 \sigma_1 = \sqrt{20000/3} \approx 81.65 σ1=20000/3 ≈81.65
  • σ2= 10000/3 ≈57.74 \sigma_2 = \sqrt{10000/3} \approx 57.74 σ2=10000/3 ≈57.74
  • σ3=0 \sigma_3 = 0 σ3=0

Σ= −81.65 0 0 −0 57.74 0 −0 0 0 \Sigma = \begin{bmatrix} \phantom{-}81.65 & 0 & 0 \\ \phantom{-}0 & 57.74 & 0 \\ \phantom{-}0 & 0 & 0 \end{bmatrix} Σ= −81.65−0−0057.740000

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

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

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

步骤 5:取前 k 个主成分

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

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

步骤 6:投影(k = 2)

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

展开每一行:第 pp p 行投影 = 中心化像素向量与 u1 u_1 u1、 u2 u_2 u2 的点积。

  • u1=(1,−1,0)/2 u_1 = (1,-1,0)/\sqrt{2} u1=(1,−1,0)/2 点积 = Rc−Gc 2 \dfrac{R_c - G_c}{\sqrt{2}} 2 Rc−Gc
  • u2=(0,0,1) u_2 = (0,0,1) u2=(0,0,1) 点积 = Bc B_c Bc
像素 (Rc−Gc)/2 (R_c - G_c)/\sqrt{2} (Rc−Gc)/2 Bc B_c Bc 投影 (x(1),x(2))(x^{(1)}, x^{(2)}) (x(1),x(2))
p1 p_1 p1 (50−(−50))/2=100/2≈70.71(50-(-50))/\sqrt{2} = 100/\sqrt{2} \approx 70.71 (50−(−50))/2 =100/2 ≈70.71 −50-50 −50 (70.71, −50)(70.71,\ -50) (70.71, −50)
p2 p_2 p2 (−50−50)/2=−100/2≈−70.71(-50-50)/\sqrt{2} = -100/\sqrt{2} \approx -70.71 (−50−50)/2 =−100/2 ≈−70.71 −50-50 −50 (−70.71, −50)(-70.71,\ -50) (−70.71, −50)
p3 p_3 p3 (50−(−50))/2≈70.71(50-(-50))/\sqrt{2} \approx 70.71 (50−(−50))/2 ≈70.71 5050 50 (70.71, −50) (70.71,\ \phantom{-}50) (70.71, −50)
p4 p_4 p4 (−50−50)/2≈−70.71(-50-50)/\sqrt{2} \approx -70.71 (−50−50)/2 ≈−70.71 5050 50 (−70.71, −50)(-70.71,\ \phantom{-}50) (−70.71, −50)

Xproj= −70.71 −50 −70.71 −50 −70.71 −50 −70.71 −50 (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) Xproj= −70.71−70.71−70.71−70.71−50−50−50−50 (4×2)

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

步骤 7:重建(k = 2)

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

每像素的重建 = 投影分量沿 u1 u_1 u1 和 u2 u_2 u2 还原回去 + 加回均值:
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) Xrecon,p=xp(1)⋅u1+xp(2)⋅u2+(150,150,150)

像素 x(1)⋅u1 x^{(1)} \cdot u_1 x(1)⋅u1 x(2)⋅u2 x^{(2)} \cdot u_2 x(2)⋅u2 + Xˉ \bar{X} = 重建 RGB
p1 p_1 p1 70.71⋅(0.7071,−0.7071,0)=(50,−50,0)70.71 \cdot (0.7071,-0.7071,0) = (50,-50,0) 70.71⋅(0.7071,−0.7071,0)=(50,−50,0) −50⋅(0,0,1)=(0,0,−50)-50 \cdot (0,0,1) = (0,0,-50) −50⋅(0,0,1)=(0,0,−50) (150,150,150) (200, 100, 100)
p2 p_2 p2 −70.71⋅(0.7071,−0.7071,0)=(−50,50,0)-70.71 \cdot (0.7071,-0.7071,0) = (-50,50,0) −70.71⋅(0.7071,−0.7071,0)=(−50,50,0) −50⋅(0,0,1)=(0,0,−50)-50 \cdot (0,0,1) = (0,0,-50) −50⋅(0,0,1)=(0,0,−50) (150,150,150) (100, 200, 100)
p3 p_3 p3 70.71⋅(0.7071,−0.7071,0)=(50,−50,0)70.71 \cdot (0.7071,-0.7071,0) = (50,-50,0) 70.71⋅(0.7071,−0.7071,0)=(50,−50,0) 50⋅(0,0,1)=(0,0,50)50 \cdot (0,0,1) = (0,0,50) 50⋅(0,0,1)=(0,0,50) (150,150,150) (200, 100, 200)
p4 p_4 p4 −70.71⋅(0.7071,−0.7071,0)=(−50,50,0)-70.71 \cdot (0.7071,-0.7071,0) = (-50,50,0) −70.71⋅(0.7071,−0.7071,0)=(−50,50,0) 50⋅(0,0,1)=(0,0,50)50 \cdot (0,0,1) = (0,0,50) 50⋅(0,0,1)=(0,0,50) (150,150,150) (100, 200, 200)

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

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

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

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

核心结论 :这个玩具图天然是 2 维 的(受 R+G=300R+G=300 R+G=300 这一个约束限制),所以 k=2 完美重建。这跟"自然图通常 k=1 就够"并不矛盾 ------自然图里大量像素近似灰阶(肤色、天空、阴影),整个数据集的有效维度也远小于 3,但有效维数到底是 1 还是 2 还是 3,取决于数据本身 。本节用具体数字把 PCA 流程走通;真实图片的方差占比、长什么样、k 怎么选见 [§ 5](#§ 5 "#%E4%BA%94k-%E6%80%8E%E4%B9%88%E9%80%893-%E4%B8%AA%E4%B8%BB%E6%88%90%E5%88%86%E7%9A%84%E6%96%B9%E5%B7%AE%E5%8D%A0%E6%AF%94")。


2、PCA实现完整流程

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

X= R1 G1 B1 R2 G2 B2 ⋮ ⋮ ⋮ Rn Gn Bn 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} X= R1R2⋮RnG1G2⋮GnB1B2⋮Bn

nn n = 像素数,3 列 = RGB。

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

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

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

3. 协方差矩阵 C=XTX/(n−1)C = X^T X / (n-1) C=XTX/(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} C= Var(R)Cov(G,R)Cov(B,R)Cov(R,G)Var(G)Cov(B,G)Cov(R,B)Cov(G,B)Var(B)

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

4. SVD 分解

C=UΣVTC = U \Sigma V^T C=UΣVT

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

5. 保留前 k 个主成分

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

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

6. 投影

Xproj=Xcentered×P(n×k) X_{\text{proj}} = X_{\text{centered}} \times P \quad (n \times k) Xproj=Xcentered×P(n×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) Xrecon=Xproj×PT+Xˉ(n×3)

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


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

rust 复制代码
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
}

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


四、前端效果展示

打开页面后:

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

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

5.1 经验数字

k 数据量 视觉质量 累计方差占比(典型自然图)
1 1/3 失真 80~95%
2 2/3 较好 95~99%
3 100% 无损 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/3 1/\sqrt{3} 1/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 R+G 守恒,灰阶方向 (1,1,0)(1,1,0) (1,1,0) 的方差也是 0------这说明"灰阶方向是否占主"完全取决于数据。

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

把 § 二.0 的 4 像素玩具图替换为一张 100×100 自然照片(如风景照、人像),PCA 给出的 3 个奇异值通常呈下面这种形态:
σ1≈8500,σ2≈1200,σ3≈300 \sigma_1 \approx 8500,\quad \sigma_2 \approx 1200,\quad \sigma_3 \approx 300 σ1≈8500,σ2≈1200,σ3≈300

主成分 奇异值² = 方差 方差占比 直观解释
PC1 ≈ (0.58, 0.58, 0.58) 85002=72,250,0008500^2 = 72{,}250{,}000 85002=72,250,000 96.6% 灰阶轴(亮度变化)
PC2 ≈ (0.71, -0.71, 0) 12002=1,440,0001200^2 = 1{,}440{,}000 12002=1,440,000 1.9% "红-绿"色相偏差
PC3 ≈ (0.41, 0.41, -0.82) 3002=90,000300^2 = 90{,}000 3002=90,000 0.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{(粗略)} 存储压缩比=3k×累计方差占比1(粗略)

  • 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 = Cji C_{ij} = C_{ji} Cij=Cji

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

相关推荐
段一凡-华北理工大学12 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
SmalBox12 小时前
【节点】[Emission节点]原理解析与实际应用
unity3d·游戏开发·图形学
大卫小东(Sheldon)17 小时前
小鹤音形词典:一个用 Rust 写的离线编码查询桌面工具
ai·rust
就业发动机18 小时前
我用 Tauri 做了一个约 1.5 MB 的开源屏幕标注工具:MarkerOn
vue.js·rust
2601_9545267520 小时前
【硬核架构】打破 IT 与 OT 的数据孤岛!基于 Rust 异步协程的工业 IoT 网关重构,兼谈顶级自动化智能仪表厂家选型指南
物联网·架构·rust
white_ant21 小时前
17- 文本统计/处理工具使用指南
rust·个人开发·tauri
独孤留白21 小时前
Rust 类型转换全景指南 —— 从引用转换到序列化
rust
独孤留白1 天前
从C到Rust:Trait TryFrom TryInto 可能失败的类型转换
rust
雨师@1 天前
python通过rust编写组件扩展自己的能力
python·rust
程序员爱钓鱼1 天前
Rust 切片 Slice 详解:安全访问连续数据
前端·后端·rust