简介:基于Matlab主成分分析的图像压缩与重建完整代码包,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计参考。资源通过PCA去除图像数据相关性,将信息浓缩至少数主成分特征图像,实现压缩,并可按需恢复不同层次的重建效果。包内共8个文件,以5个Matlab脚本为核心,覆盖主成分分析、图像压缩、重建及示例运行流程;附带2个txt说明文档,分别介绍技术原理与使用说明;另有1张示例图片用于测试验证。压缩包仅126KB,结构紧凑,便于快速下载和本地调试。目前已有204人学习使用,适合具备一定Matlab基础、希望深入理解PCA在图像处理中应用的读者。
1. 用PCA做图像压缩,先想清楚你在压缩什么
图像压缩的本质不是“把像素变少”,而是利用像素之间的统计相关性,用少数几个不相关的分量去近似原始数据。主成分分析恰好干这件事:将原始图像分块后,每个块被看作高维空间里的一个样本,协方差矩阵的特征向量构成新的正交基,而特征值大小直接决定每个基方向保留的信息量。这个项目里,pcaimage.m就是基于这一思路实现的:先做中心化,再求协方差矩阵、做特征分解,取前 k 个特征向量作为压缩基,把图像块投影到低维子空间,重建时再线性组合回来。和 DCT、小波那种固定变换不同,PCA 的基是从图像自身学出来的,所以对纹理密集、方向性强的图像往往能用更少的系数达到同样的目测质量。适合正在做“图像处理/模式识别”课程设计或毕业设计的人,也适合想搞清楚 PCA 为什么能压缩的人。
2. 从协方差矩阵到主成分:图像压缩的数学前提与 Matlab 实现
2.1 图像矩阵的向量化与数据中心化
一张灰度图像在 Matlab 里就是一个 m×n 的矩阵。直接对这个矩阵做特征分解没有意义,必须先把图像切分成固定大小的子块,再把每个子块展成一个列向量。假设分块大小为 b×b,那么每个块就是一个 b² 维样本。一张 256×256 的图像分成 8×8 块后,会有 32×32 = 1024 个样本,每个样本 64 维。把这 1024 个样本按行堆叠,得到矩阵 X,形状是 1024×64。
数据中心化是 PCA 前必须做的一步。计算每个维度的均值 mu,然后让每个样本减去均值,即 Xc = X - mu。原因是协方差矩阵的定义本质上计算的是“变量之间的联合波动”,如果不去均值,第一主成分会被整体亮度偏移主导,而不是像素之间的纹理结构。在 Matlab 中,均值用 mean(X,1) 得到 1×64 的行向量,广播到每一行即可。
这里有一个容易被忽略的细节:图像块在展成向量时,空间相邻关系被打散了。比如块内第 1 个像素和第 9 个像素在视觉上可能挨着,但在向量里相隔很远。PCA 的协方差矩阵只统计“数值上的相关性”,不关心位置,所以只要所有块都按相同的顺序展平,这种空间信息会隐含在协方差结构中,不需要额外处理。
2.2 协方差矩阵计算与特征分解:eig 与 svd 的选择
理论上,样本矩阵 Xc 的协方差矩阵是 C = Xc' * Xc / (n-1),其中 n 是样本数。然后用 eig(C) 求特征值和特征向量。这个做法的门槛是:C 的大小是 b² × b²,分块 8×8 时只有 64×64,计算非常快。但如果把整幅图像按行向量当作样本,或者 b 取 32,协方差矩阵会变成 1024×1024 甚至更大,内存和耗时都会急剧上升。
更稳健的做法是用奇异值分解 svd。当样本数 n 比维度 d 小时,Xc 的 SVD 可以直接给出 Xc' * Xc 的特征向量,且数值稳定性更好,避免了对对称矩阵求 eig 时出现微小负特征值。项目里pcaimage.m用的是 eig,这在块尺寸较小、样本数远大于维度时完全够用。如果后续你想处理 16×16 或更大的块,建议改成:
[U, S, V] = svd(Xc, 'econ'); Dvals = diag(S).^2 / (size(Xc,1) - 1); coeffAll = V;这里的逻辑是:Xc是 n×d 矩阵,svd返回的V是 d×d 特征向量矩阵,S的奇异值平方除以 n-1 就是协方差矩阵的特征值。这样做的好处是避免了构造 d×d 的协方差矩阵,内存占用从 O(d²) 降到 O(d·n),在块数量很大时能明显感知到差异。
2.3 pcaimage.m 核心代码:投影、量化、重建
下面的代码是对pcaimage.m功能的完整复刻,支持自定义分块大小和保留主成分个数。输入是单通道灰度图,输出为压缩系数、特征向量、均值向量和重建图像。
function [score, coeff, mu, recon] = pcaimage(I, k, blockSize) % pcaimage 基于PCA的图像压缩与重建 % I: 灰度图像,uint8或double均可 % k: 保留的主成分个数 % blockSize: 正方形分块边长,默认8 if nargin < 3 blockSize = 8; end I = double(I); [rows, cols] = size(I); % 裁剪到分块大小的整数倍 rows = rows - mod(rows, blockSize); cols = cols - mod(cols, blockSize); I = I(1:rows, 1:cols); % 分块,每列是一个块 blocks = im2col(I, [blockSize blockSize], 'distinct'); % 维度: blockSize^2 x numBlocks X = blocks'; % 每行一个样本,形状: numBlocks x blockSize^2 mu = mean(X, 1); % 各维均值 Xc = X - mu; % 中心化 % 协方差矩阵与特征分解 C = (Xc' * Xc) / (size(Xc, 1) - 1); [V, D] = eig(C); d = diag(D); [~, idx] = sort(d, 'descend'); V = V(:, idx); % 按特征值降序排列 % 取前k个特征向量,投影得到压缩系数 coeff = V(:, 1:k); score = Xc * coeff; % 压缩后的系数矩阵 % 重建:系数乘以特征向量转置,再加回均值 Xrec = score * coeff' + mu; recon = col2im(Xrec', [blockSize blockSize], [rows cols], 'distinct'); recon = uint8(round(recon)); % 回到uint8显示范围 end关键参数有三个。blockSize控制压缩粒度,块越小,协方差矩阵维度越低,但需要保存的均值向量和特征向量数量也越多;一般取 8 或 12。k是保留的主成分个数,k 越小压缩率越高,图像细节丢失越明显。score的尺寸是 numBlocks × k,加上 k×1 的特征向量和 1×blockSize² 的均值向量,总数据量为 numBlocks×k + k×blockSize² + blockSize²,与原始像素数 numBlocks×blockSize² 相比,压缩率约为 k/blockSize²(当块数较大时,特征向量和均值的存储可以忽略)。例如 blockSize=8,k=16 时,压缩率约为 25%,也就是只保留原来四分之一的数值量。
3. 主成分个数 k 的选取:压缩率与重建质量的平衡
3.1 累积贡献率:cumsum 与 explained
选 k 最直接的依据是累积贡献率。特征值 D 表明每个主成分解释的方差大小,按降序排列后,计算前 k 个特征值之和占总和的百分比。当这个百分比达到 85%~95% 时,一般认为重建图像能保留大部分结构。由于eig直接返回的特征值可能有数值噪声,推荐先过滤掉负值:
Dvals = diag(D); Dvals(Dvals < 0) = 0; % 数值截断 explained = Dvals / sum(Dvals) * 100; cumContribution = cumsum(explained); k_90 = find(cumContribution >= 90, 1); fprintf('达到90%%贡献率需要k=%d\n', k_90);cumsum是累积求和,find(..., 1)返回第一个满足条件的位置。注意 k 值不是越大越好:当 k 接近 blockSize² 时,压缩率趋近 1,失去了压缩意义,而且协方差矩阵的高阶特征向量往往对应高频噪声,强行保留这些分量会让重建图像出现颗粒感。我一般会先在 k=4、8、16、32 上各跑一次,对比累积贡献率和目测效果,而不是直接取一个固定阈值。
3.2 块划分与全局 PCA:不同策略的压缩率对比
全局 PCA 把整幅图拉伸成一个大向量,只产生一组主成分,适合人脸识别这类所有图像共享相同基底的场景。图像压缩更常用的是分块 PCA:每个图像块独立贡献样本,块内的局部纹理结构被单独建模。分块选择会直接影响压缩率。
| blockSize | 块数(256×256) | 维度 | k=16时压缩率 | 备注 |
|---|---|---|---|---|
| 4 | 4096 | 16 | 100%(无压缩) | 每个块只有16维,k=16等于全保留 |
| 8 | 1024 | 64 | 25% | 平衡细节与压缩率的经典选择 |
| 16 | 256 | 256 | 6.25% | 压缩率高,但块边缘块效应明显 |
| 32 | 64 | 1024 | 1.56% | 块数量少,协方差估计不稳定 |
从表格可以看到,blockSize=4 时 k=16 根本没有压缩意义,因为主成分数量等于原维度。blockSize=16 虽然压缩率低,但每个块只用了 256 个样本估计协方差矩阵,而维度也是 256,样本数不足会导致协方差矩阵奇异性,特征分解不稳定。所以 blockSize 取 8 或 12 是一个兼顾统计可靠性与压缩效率的区间。项目压缩包里的liftingbody.png是 512×512 的灰度图,用 blockSize=8 时能产生 4096 个块,样本充足,协方差估计稳定,适合直接跑通流程。
3.3 参数调整与结果验证
当你修改k时,需要同时关注压缩率和重建误差。压缩率的计算不能只看主成分数量,还要把均值向量和特征向量的存储算进去。对于小图像,这部分开销占比不小;对于大图像则可以忽略。给出一个实用的参数扫描脚本:
I = imread('liftingbody.png'); kList = [4 8 16 32]; for i = 1:length(kList) k = kList(i); [score, coeff, mu, recon] = pcaimage(I, k, 8); mse = mean((double(I(:)) - double(recon(:))).^2); ratio = (size(score,1)*k + k*64 + 64) / numel(I); fprintf('k=%2d MSE=%6.2f compression_ratio=%.4f\n', k, mse, ratio); end注意,这里 MSE 的计算是在原始图像裁剪后与重建图像之间比较。如果图像尺寸不是 blockSize 的整数倍,pcaimage内部已经做了裁剪,所以外部比较前也要先裁剪,否则行列数对不上会直接报错。这个脚本的输出会告诉你一个反直觉的现象:k 从 4 增加到 8,MSE 下降非常明显;从 16 增加到 32,MSE 变化却很小。这说明前几个主成分已经捕获了绝大多数亮度低频信息,后面的主成分只是在补充边缘和细节,视觉改善有限,但存储开销线性增加。
4. 压缩与重建流程的工程化:数据流、批处理与常见坑
4.1 从 pcasample.m 到 main.m 的数据流设计
项目压缩包里有pcasample.m、main.m、main2.m、main3.m多个入口,它们的关系通常是:pcasample.m演示 PCA 在二维样本上的效果,帮助理解数据投影;main.m调用pcaimage.m对一幅图做压缩和重建;main2.m和main3.m分别演示不同 k 或不同分块下的对比结果。这样分层的好处是把“算法核心”和“实验脚本”分开,调试时只需要改主脚本,不用动核心函数。
实际工程中,我会把所有待压缩图像放在一个目录里,脚本循环读取。核心代码只处理单张灰度图,批处理逻辑放在外层:
srcDir = 'images'; files = dir(fullfile(srcDir, '*.png')); outDir = 'output'; if ~exist(outDir, 'dir'), mkdir(outDir); end for i = 1:length(files) I = imread(fullfile(srcDir, files(i).name)); if size(I, 3) == 3 I = rgb2gray(I); end [score, coeff, mu, recon] = pcaimage(I, 16, 8); imwrite(recon, fullfile(outDir, sprintf('recon_%02d.png', i))); % 保存压缩数据,供后续重建使用 save(fullfile(outDir, sprintf('pca_data_%02d.mat', i)), 'score', 'coeff', 'mu', 'blockSize'); end这里把score、coeff、mu保存下来,相当于完成了一次“编码”。重建时不需要原始图像,只需要这三个变量和小图块排列顺序。save到.mat文件是 Matlab 中最简单的序列化方式,如果希望压缩结果能跨语言使用,可以把score和coeff分别写成二进制文件或 CSV,供 Python/C++ 端读取。
4.2 常见问题:uint8 溢出、特征向量符号、内存占用
第一个高频坑是重建图像出现大片黑色或白色条纹。原因通常是用 uint8 直接做减法,或者重建值超出了 0~255 范围。PCA 的重建结果在数学上是浮点数,中心化后的均值回加可能出现负值或超过 255 的值。正确做法是先对重建矩阵做线性拉伸,再转 uint8:
recon = recon - min(recon(:)); recon = recon / max(recon(:)) * 255; recon = uint8(round(recon));min和max在这里做了线性映射,虽然会改变绝对像素值,但对灰度图像影响很小,因为 PCA 重建整体趋势是一致的。如果追求无偏,可以不做拉伸,直接用uint8(round(recon)),但视觉效果可能偏灰或偏暗。
第二个坑是特征向量符号不唯一。对同一个协方差矩阵做特征分解,不同版本的 Matlab 或不同的 LAPACK 实现可能返回符号相反的coeff,导致重建结果变暗或出现负像。这不会影响图像结构,因为score也会相应变化。但如果你的脚本里固定了某个特征向量用于可视化,两次运行可能看到方向翻转,这是正常现象。调试时不要直接对比coeff,应该对比重建图像的 MSE 或 SSIM。
第三个坑是内存爆炸。处理较大图像时,im2col会产生一个 blockSize² × numBlocks 的密集矩阵,如果 blockSize=16,512×512 的图会有 1024 个块,矩阵只有 256×1024,还算小;但如果对超分辨率图像直接做全局 PCA,协方差矩阵大小随维度平方增长,很容易达到几十 GB。遇到这种情况,建议对图像分块分批估计协方差矩阵,或者干脆使用 4.3 节的通道分离策略。
4.3 多通道图像(RGB)的 PCA 处理
pcaimage.m接收的是二维矩阵,RGB 彩色图传入时会报错或只取第一维。项目中main.m如果直接读liftingbody.png这种灰度图没问题,但换了彩色图就要先做通道处理。常见做法有三种:转灰度图、对三个通道分别做 PCA、将三个通道拼接成三维样本。
对三个通道分别做 PCA 是最好的方法,因为通道间存在颜色相关性,分别压缩会得到不同的 k 值,但实现简单,且重建后能保留原始色彩。核心逻辑:
reconRGB = zeros(size(I)); for c = 1:3 [~, ~, ~, reconRGB(:,:,c)] = pcaimage(I(:,:,c), k, blockSize); endI(:,:,c)提取单通道,pcaimage内部会将其转为 double 处理。需要注意,三个通道的均值向量和特征向量不同,压缩系数也各自独立。如果你希望进一步压缩,可以把三个通道的score矩阵垂直拼接后,再对拼接矩阵做一次 PCA,但这已经属于两级压缩,实现复杂度高,项目源码里没有体现,实际场景中很少用。
5. 验证重建质量:MSE、PSNR、SSIM 与主成分可视化
5.1 三个指标的计算代码
重建质量的客观评价不能只靠眼睛,需要用数值指标来判断 k 的取值是否合理。MSE 反映像素级平均误差,PSNR 是对 MSE 的对数压缩,SSIM 则从亮度、对比度、结构三个维度评估感知相似度。以下是直接可用的 Matlab 实现:
function [mse, psnr, ssimVal] = imgQuality(orig, recon) % orig和recon都是uint8灰度图,尺寸必须一致 origD = double(orig); reconD = double(recon); mse = mean((origD(:) - reconD(:)).^2); psnr = 10 * log10(255^2 / mse); ssimVal = ssim(orig, recon); endssim函数在 Image Processing Toolbox 中提供,如果版本太老没有这个函数,可以用ssim(orig, recon)的替代实现,即分别计算均值、方差和协方差,然后按公式组合。PSNR 对像素值的动态范围很敏感,如果重建值整体偏暗,PSNR 会非常低,但视觉上可能只是对比度问题,所以需要结合 SSIM 一起看。SSIM 大于 0.9 一般可以认为结构保持得不错。
5.2 主成分特征图像与 liftingbody.png 的观察
主成分特征向量本身也是 blockSize×blockSize 的图像,读取第 i 列并 reshape,就能看到 PCA 学到的“基模式”。第一主成分通常接近均值模式,也就是整体的亮度分布;后面的主成分会依次呈现不同方向的边缘和纹理。可视化代码如下:
figure; for i = 1:min(6, k) subplot(2, 3, i); imagesc(reshape(coeff(:, i), [blockSize blockSize])); colormap(gray); title(sprintf('PC %d', i)); axis off; end对于liftingbody.png这张图,背景是暗色,中央是亮色的织布机结构。运行后你会发现前 3 个主成分已经勾勒出织布机的大致轮廓,第 4 到第 8 个主成分补充了布纹的交错线条。如果 k 小于 8,重建结果的边缘会变得模糊,像蒙了一层纱;k 大于 20 之后,肉眼几乎无法区分原图和重建图,但 MSE 还在缓慢下降,说明那些主成分对应的是噪点级别的波动,对结构贡献很小。
5.3 一个可复用的技巧:增量 PCA 处理大图像
整幅图像一次性做 PCA,当块数量达到数万时,Xc' * Xc的求和过程会损失精度,特别是图像包含大面积平坦区域时,协方差矩阵的条件数很大。一个稳妥的技巧是分批次更新协方差矩阵,而不是一次性把全部块载入内存。思路是维护一个累加矩阵S_xx和累加均值向量S_x,每读取一批图像块就更新一次:
S_x = 0; S_xx = 0; n = 0; for bi = 1:numBlocks block = getBlock(I, bi, blockSize); % 自定义取块函数 x = block(:)'; n = n + 1; delta = x - mu; % 这里的mu是逐步更新的均值 mu = mu + delta / n; S_xx = S_xx + delta' * (x - mu); % Welford方法同时更新协方差累加项 end C = S_xx / (n - 1);这个做法参考了在线均值和协方差估计的 Welford 算法,能在不牺牲数值稳定性的前提下处理任意大的图像。后续对S_xx做特征分解,得到的主成分与一次性计算的结果几乎一致,但内存占用从 O(numBlocks × blockSize²) 降到了 O(blockSize²)。对医学超分辨率重建或遥感大图的场景,这一条比调 k 更关键,它决定了算法能不能在实际机器上跑完。
本文还有配套的精品资源,点击获取