简介:一份基于Matlab主成分分析(PCA)的图像压缩与重建实现资料,面向计算机、电子信息工程、数学等专业学生,适用于课程设计、期末大作业或毕业设计中的算法验证与代码参考。资源包含5个.m源码文件、2个txt说明文档和1张png示例图片,共8个文件,压缩包仅126KB,结构紧凑,便于下载与快速部署。源码围绕PCA去相关、主成分特征图像提取、图像压缩与按需重建展开,配有技术说明和说明文档,可帮助读者理解如何将图像信息浓缩到少数主成分中,并依据主成分内容恢复不同层次质量的图像。目前已有204人学习下载。通过对照源码和文档,读者可掌握PCA图像压缩的基本流程、参数调节思路及重建效果评估方法,适合需要动手实践和二次开发的入门及进阶学习者。
1. PCA图像压缩的核心思路:把图像当成数据矩阵来看
主成分分析做图像压缩,本质是把「像素矩阵」重写成「样本×特征」的数据矩阵,再在这个矩阵上找方差最大的投影方向。一幅512×512灰度图有262144个值,按8×8分块后变成4096个样本、每样本64维。自然图像相邻像素高度相关,64维里真正有效的信息往往只有十几个维度,PCA做的就是把它们找出来,把其余维度丢弃,再用保留的方向把图像重建回来。
相比固定丢弃高频系数的阈值滤波,PCA按图像自身分布自适应选方向,不需要人工定频带;相比JPEG,它在低码率下保留更多全局结构,缺点是计算量更大、分块接缝明显。它是深度学习自编码器流行之前最经典的线性降维基线,很适合Matlab课程设计、图像压缩实验对比,以及想验证「线性模型对图像到底能压到多少」的工程师。下文代码全部在Matlab R2018b及以上可用,只依赖eig、svd和基础矩阵运算,不涉及额外工具箱。
2. Matlab里从零实现PCA图像压缩:从分块到特征分解
2.1 为什么先分块再按行展开
把整幅图像直接做PCA,常见做法是把每一行当作一个样本,把列数当作特征数。512×512的图像会得到512个样本、512维特征,协方差矩阵是512×512,分解不算慢,但问题在于整行像素包含的天空、建筑、纹理混在一起,主成分描述的是全局行统计,重建时细节和边缘会被平均掉。分块后每个8×8块只覆盖局部纹理,块与块之间的统计特性更接近,前几个主成分就能抓住块内的主要灰度变化。这和JPEG选8×8做DCT是同一个直觉:自然图像的局部统计比全局统计稳定得多。
分块尺寸还决定了样本数和特征维数的比例,这个比例直接影响协方差估计的可靠性。8×8分块下dim=64,512×512图像对应4096个样本,样本/特征比达到64,协方差矩阵估计足够稳。若改用16×16分块,dim变成256,同样图像下样本数降到1024,比例只有4,特征向量开始出现过拟合,重建图像会出现与训练块相关的伪纹理。所以「先分块、再按行展开」不只是为了局部性和计算开销,更是为了让协方差统计站得住脚。
2.2 协方差矩阵与特征分解的最小实现
下面这个函数是整套压缩的核心:输入灰度图像、块尺寸和保留主成分数k,输出重建图像。所有中间量都保留在变量名里,方便把它拆成独立的压缩端和解压端脚本。
function recon = pca_block_compress(img_gray, blockSize, k) [rows, cols] = size(img_gray); rBlocks = floor(rows / blockSize); cBlocks = floor(cols / blockSize); nBlocks = rBlocks * cBlocks; dim = blockSize * blockSize; % 1. 分块并展平: X的每一行是一个块, 共nBlocks行 X = zeros(nBlocks, dim); idx = 0; for i = 1:rBlocks for j = 1:cBlocks r0 = (i-1)*blockSize + 1; c0 = (j-1)*blockSize + 1; block = img_gray(r0:r0+blockSize-1, c0:c0+blockSize-1); idx = idx + 1; X(idx, :) = block(:)'; end end % 2. 中心化: 每个块减去平均块, PCA必须在零均值数据上做 meanX = mean(X, 1); % 1×dim 平均块 Xc = X - meanX; % 中心化后的样本矩阵 % 3. 协方差矩阵并做特征分解 C = (Xc' * Xc) / (nBlocks - 1); % dim×dim [V, D] = eig(C); [~, order] = sort(diag(D), 'descend'); % 特征值降序排列 V = V(:, order); % 4. 截断到前k个主成分并投影 Vk = V(:, 1:k); score = Xc * Vk; % nBlocks×k, 压缩后的核心数据 % 5. 重建: 投影坐标乘回特征向量, 加回平均块 reconX = score * Vk' + meanX; % 6. 写回图像矩阵 recon = zeros(rows, cols); idx = 0; for i = 1:rBlocks for j = 1:cBlocks idx = idx + 1; r0 = (i-1)*blockSize + 1; c0 = (j-1)*blockSize + 1; recon(r0:r0+blockSize-1, c0:c0+blockSize-1) = ... reshape(reconX(idx, :), blockSize, blockSize); end end recon = max(0, min(255, recon)); % 裁剪越界像素 end这段代码里最容易被忽略的是第2步中心化。meanX必须进入重建通路:score乘回Vk得到的只是块间差异量,没有加回平均块的图像是整体偏灰的残差图。把score、Vk、meanX一起保存成.mat或二进制文件时,加载顺序必须和这里一致。第3步用eig(C)处理64×64的小矩阵完全够用,但如果blockSize取16以上,dim变成256,协方差矩阵开始病态,应改用下一节的SVD写法。
提示:输入图像必须转成double再送入函数。uint8在减法和矩阵乘法中会溢出,典型症状是重建图像出现整块偏白或偏黑的条纹。
2.3 用SVD替代特征分解的数值稳定写法
eig(C)分解的对象是C = Xc'*Xc/(n-1),先做一次矩阵乘法会放大Xc中异常块的影响。直接对Xc做SVD,右奇异向量V就是协方差矩阵的特征向量,奇异值平方除以(n-1)就是特征值,绕开了平方放大效应。
% 用econ SVD替换2.2节第3~4步 % Xc 为中心化后的样本矩阵, nBlocks×dim [~, S, V] = svd(Xc, 'econ'); % Xc = U*S*V' eigVals = diag(S).^2 / (nBlocks - 1); % 特征值序列 Vk = V(:, 1:k); score = Xc * Vk; % 投影系数与eig路径等价econ模式在nBlocks大于dim时返回dim×dim的完整V,在nBlocks小于dim时返回nBlocks×nBlocks的V,此时k不能超过nBlocks。另有eigs(C, k, 'largestabs')只算前k个特征向量,适合dim上万的大分块场景,但对64维小矩阵没有性能收益,反而要处理收敛警告。我的默认选择是:dim≤128用svd('econ'),dim更大且只关心前k个主成分时用eigs。
| 比较项 | eig(C) | svd(Xc,'econ') | eigs(C,k) | | 计算对象 | dim×dim协方差 | nBlocks×dim原始数据 | dim×dim协方差(迭代) | | 特征值来源 | 直接分解 | 奇异值平方/(n-1) | 部分最大特征值 | | 数值稳定性 | 病态时可能出现负特征值 | 最稳, 推荐默认使用 | 依赖收敛容差 | | 适用场景 | 小dim、教学演示 | 通用首选 | dim大且k固定 |
「负特征值」值得单独说:图像块高度相似时协方差矩阵接近秩亏,浮点误差可能把本应为0的最小特征值算成微小的负数。eig照常返回,排序后这些负值落到末尾,不影响前k个结果;但如果你用特征值算能量占比,求和时负值会拉低累计贡献率,判断95%阈值前需要先对特征值做max(0,·)截断。
3. 主成分数量k与分块大小的参数权衡
3.1 用累计贡献率决定保留多少主成分
k是压缩质量和存储量之间唯一的旋钮。选k有两条路:按目标压缩比反推k,或按信息保留程度正推k。后者更常用,定义是特征值降序排列后前k个之和占总能量的比例。经验阈值取95%:对多数自然图像,8×8分块下k在10到25之间就能达到这个数。
% 承接2.2节的特征值结果 eigVals = max(0, diag(D)); % 截断负特征值 totalEnergy = sum(eigVals); cumRatio = cumsum(eigVals) / totalEnergy; k_95 = find(cumRatio >= 0.95, 1, 'first'); k_99 = find(cumRatio >= 0.99, 1, 'first'); fprintf('95%%能量需k=%d, 99%%能量需k=%d\n', k_95, k_99);cumsum得到每个k对应的累计占比,find取第一个越过阈值的下标。注意这里的能量是训练块的方差统计量,只代表块内方差的重建程度,不代表视觉质量。平滑图像如天空和皮肤,k=8时PSNR已经很高;高纹理图像如树叶、布料,k=20仍然能看到细节丢失。贡献率适合定初值,最终还要用PSNR和肉眼微调。
3.2 压缩比的计算公式与实验数据
压缩比必须把模型参数算进去,否则会虚高。灰度图原始存储是rows×cols字节,压缩后存储包括三部分:每个块的投影系数score(nBlocks×k)、k个特征向量(k×dim)、平均块向量meanX(dim)。
originalBytes = rows * cols; compressedBytes = nBlocks * k + dim * k + dim; % 第一项: score矩阵, 每个块k个系数 % 第二项: Vk特征向量, 每列dim维, 共k列 % 第三项: meanX平均块, dim维 compressionRatio = originalBytes / compressedBytes; % score和Vk按与原像素相同的浮点精度计算, 不含量化以512×512灰度图、blockSize=8为例,nBlocks=4096、dim=64,不同k的理论压缩比和典型重建质量如下。PSNR列给的是自然图像上常见的范围,精确值依赖具体图片纹理。
| 保留主成分数k | 压缩后存储(字节) | 压缩比 | 典型PSNR(dB) | | 4 | 4×4096+256+64=16644 | 15.8 | 23~26 | | 8 | 8×4096+512+64=33344 | 7.9 | 27~29 | | 16 | 16×4096+1024+64=66624 | 3.9 | 31~33 | | 32 | 32×4096+2048+64=133184 | 2.0 | 36~38 | | 64 | 64×4096+4096+64=266304 | 0.98 | 无损 |
k=64时压缩比小于1,因为特征向量和平均块成为额外开销。这恰好说明PCA压缩的收益完全来自块内维度的相关性:相关性越强,k可以越小。实际工程里score和Vk通常会量化成8位整数,压缩比还能再提高,但量化误差会直接体现为重建图像上的颗粒噪点,量化位宽是压缩比和噪声之间的另一个旋钮。
3.3 分块大小对压缩质量和边界效应的影响
blockSize取4、8、16各有代价。4×4的dim=16,块内方差小,平均k=3到5就能达到95%能量,但块数目变成16384,score存储量反而上升,且每块统计噪声大,重建后容易出现颗粒感。16×16的dim=256,能量更集中,但样本数降到1024,协方差估计不稳,块间不连续处会出现明显马赛克边界。8×8是压缩率和视觉质量都比较平衡的默认值。
边界问题有两个来源。第一是图像尺寸不能被blockSize整除,直接floor会丢掉最右和最下的像素,重建尺寸比原图小。常见做法是镜像填充后再分块,重建后裁剪回来。第二是重建后的块边界灰度跳变:PCA对每个块独立重建,接缝处没有平滑约束,k越小越明显。
% 镜像填充处理不能被整除的边界 padR = blockSize - mod(rows, blockSize); padC = blockSize - mod(cols, blockSize); if padR == blockSize, padR = 0; end if padC == blockSize, padC = 0; end imgPad = padarray(img_gray, [padR padC], 'symmetric', 'post'); % 对imgPad执行分块PCA, 得到reconPad后裁剪回原尺寸 recon = reconPad(1:rows, 1:cols);接缝跳变没有零代价解法:加大k能减轻但不能消除,因为每个块的均值是逐块独立估计的,属于DC分量层面的不连续。彻底消除要改用重叠分块后加权平均,但存储量会乘上重叠倍率,压缩比下降。对课程设计和大多数比对实验,接受轻微接缝、把k调到PSNR超过30dB是性价比最高的选择。
4. 彩色图像压缩与重建误差量化
4.1 三通道分别PCA与YCbCr处理的差别
灰度版本直接扩展到彩色,最朴素的做法是对R、G、B三个通道分别调用pca_block_compress再cat回来。问题有两个:RGB三通道强相关,分开处理等于无视这部分冗余;人眼对绿色最敏感、对蓝色最不敏感,三通道共用同一个k在感知上不是最优分配。更常见的是先转YCbCr,对亮度Y保留较多主成分,对Cb、Cr保留较少k。这一步和JPEG的色度下采样是同一个原理:人眼对色度高频不敏感。
% 彩色图像压缩入口: YCbCr通道独立PCA img = imread('lena.png'); ycbcr = rgb2ycbcr(img); kY = 24; kC = 8; % 亮度多留, 色度少留 Yc = pca_block_compress(double(ycbcr(:,:,1)), 8, kY); CbRe = pca_block_compress(double(ycbcr(:,:,2)), 8, kC); CrRe = pca_block_compress(double(ycbcr(:,:,3)), 8, kC); reconYCbCr = cat(3, Yc, CbRe, CrRe); reconRGB = ycbcr2rgb(uint8(reconYCbCr));kY和kC的比例一般取2:1到3:1。色度通道即使只留4到8个主成分,肉眼也很难看出偏色,但PSNR会明显低于亮度通道,所以评估彩色结果时应分开统计三个通道的PSNR,而不是对RGB通道误差取平均。RGB直接三通道处理的好处是代码简单,缺点是压缩比低;YCbCr路径多一次颜色空间转换,适合对压缩比有要求的场景。
| 方案 | 通道处理 | 典型k分配 | 压缩比收益 | 视觉表现 | | RGB直接三通道 | 每条通道独立PCA | 三通道同一k | 基准 | 色度细节浪费比特 | | YCbCr独立PCA | 亮度多留、色度少留 | Y取2~3倍C | 提升10~20% | 感知质量更好 | | YCbCr加色度降采样 | 色度先降采样再PCA | 在2基础上再减 | 提升30%以上 | 彩色边缘轻微软化 |
4.2 PSNR与SSIM作为重建质量的量化指标
重建质量的量化不能只看压缩比。PSNR衡量逐像素误差,SSIM衡量结构相似性,两者配合才能说明「压缩后看起来像不像原图」。PSNR高于30dB通常认为视觉可接受,高于35dB很难看出差别;SSIM接近0.95以上表示结构保持良好。要区分的是,这里讨论的是同分辨率的有损重建,和超分辨率重建那种先降采样再放大到高分的问题不是一回事,评估口径完全不同。
% 计算PSNR和SSIM, 需要Image Processing Toolbox mseVal = mean((double(img_gray) - recon).^2, 'all'); psnrVal = 10 * log10(255^2 / mseVal); ssimVal = ssim(uint8(recon), uint8(img_gray)); fprintf('MSE=%.3f PSNR=%.2fdB SSIM=%.4f\n', mseVal, psnrVal, ssimVal);两个细节决定结果是否正确。mseVal必须用'all'参数做全数组平均,R2018b之后mean(mean(·))会警告且语义不干净。ssim函数要求输入为uint8或值域一致的double,recon若没裁剪到[0,255]就转uint8,超出部分静默截断,SSIM会莫名偏高。所以在pca_block_compress末尾保留max(0,min(255,·))那一步是必要的,不是可选项。
提示:SSIM对高斯模糊的容忍度比PSNR高。若PSNR下降但SSIM接近,说明误差集中在中高频纹理;若PSNR很高但SSIM偏低,多半存在结构性块状伪影,优先查分块接缝而不是主成分截断。
4.3 源码包的目录组织与复用方式
这类工程通常会分成四个部分:入口脚本、核心函数、量化评估脚本和说明文档。后续换图片时只需要在入口脚本里改文件路径和k值,核心函数不必动。
pca_image_compress/ ├── main_compress.m # 入口: 读图、调参数、调核心函数 ├── pca_block_compress.m # 灰度分块PCA压缩与重建 ├── pca_color_compress.m # 彩色YCbCr通道封装 ├── compute_metrics.m # PSNR/SSIM/压缩比统计 ├── images/ │ ├── lena.png │ └── test_building.png └── 说明文档.pdf入口脚本的骨架是:imread读图→rgb2gray或rgb2ycbcr→调用pca_block_compress→调用compute_metrics→用subplot把原图、重建图、误差图并排显示。k值用第3章的k_95作为初值,再手动往小调直到PSNR刚好掉到30dB附近,这个位置通常接近率失真曲线的拐点。源码包附带的图片建议统一存成png或bmp,避免jpeg的二次压缩块效应混进PCA本身的误差统计。
5. 重建图像的验证与调试技巧
5.1 用误差图定位分块边界
PSNR数字说不清问题在哪,误差图能直接暴露空间分布。把abs(原图-重建图)用imagesc画出来,正常结果是边缘和高纹理区域亮、平坦区域暗,这是高频信息被截断的表现。但如果亮线整齐出现在每个blockSize的倍数位置,问题出在分块接缝而不是主成分选择。
errMap = abs(double(img_gray) - recon); figure; subplot(1,3,1); imshow(uint8(img_gray)); title('原图'); subplot(1,3,2); imshow(uint8(recon)); title('重建图'); subplot(1,3,3); imagesc(errMap); axis image; colorbar; title('误差图');接缝型误差按顺序排查:先确认padarray用的是'post'方向,填'pre'会让图像整体偏移;再确认重建后裁剪尺寸与被填充前一致;最后才考虑重叠分块加权平均。如果误差图呈斜向条纹,几乎可以断定是reshape与块写入的行列顺序不一致——block(:)按列展开,reshape按列填回,展开和写回必须都沿列优先方向,混用行列顺序就会出现斜纹。
5.2 特征向量符号翻转与无损自检
eig和svd返回的特征向量符号是任意的,同一份数据每次运行可能差一个负号。这不影响重建,score和Vk成对翻转后相乘结果一致。但如果压缩端和解压端分开实现,两端各自对特征向量做了不同处理,重建就会在部分块出现灰度反转。自查方法是保存Vk时同时存一个参考向量,加载后对比符号。
% 无损自检: k=dim时重建应完全一致 reconTest = pca_block_compress(img_gray, 8, 64); isIdentical = isequal(uint8(reconTest), uint8(img_gray)); fprintf('无损自检通过: %d\n', isIdentical); % 返回0时, 检查padarray裁剪和uint8截断的位置最后给一个适合源码包二次开发的拆法:把pca_block_compress拆成compress和decompress两个函数——compress只输出score、Vk、meanX和尺寸信息,decompress只接收这四个输入重建图像。拆分后可以在中间对score做量化,模拟真实存储位宽对重建的影响。调试顺序固定为:先跑通k=dim的无损自检,再把k调到k_95看PSNR,最后用误差图决定是否需要调整blockSize。按这个顺序走,分块PCA的绝大多数问题都能定位到具体函数和具体行。
本文还有配套的精品资源,点击获取