简介:这是一份二维图像压缩感知重构算法的MATLAB完整实现资源,面向数字图像处理、计算机视觉、医学成像等领域的初学者与进阶研究者,可供课程设计与科研入门使用。压缩包共26个文件,内部以18个.m源码为核心,配合7个.fig图形界面文件与1张示例bmp图,整体仅1.59MB,方便下载即用。代码覆盖测量矩阵生成、图像观测采样、经典重构算法(OMP、ISTA、FISTA及正则化最小二乘)以及PSNR/MSE性能评估,并附可视化对比界面,能够直观展示原始图像与重建结果。目前已有4295人学习使用,具有较强的实践参考价值。通过研读这套代码,可深入理解稀疏表示、观测矩阵设计等核心概念,并进一步拓展到不同测量矩阵效果比较、噪声环境下的鲁棒重构,甚至与深度学习方法结合的创新研究。
1. 二维图像压缩感知重构:只采三成像素,还原一张完整图
压缩感知用在二维图像上,最反直觉的一点是:我们根本不需要把每个像素都测一遍。按照奈奎斯特采样定律,想还原一个信号,采样率至少是信号最高频率的两倍;但压缩感知告诉你,只要信号在某个变换域里足够稀疏,用远低于传统标准的采样率、配合一个精心设计的观测矩阵,照样能把原图重构出来。这套思路落到二维图像上,就是标题里说的压缩感知重构算法程序——一份完整的MATLAB代码包,从观测矩阵生成、稀疏变换到OMP之类的重构算法,全部跑通。适合正在做图像处理课题、需要快速对比不同重构算法效果、或者想从零搭一套压缩感知实验环境的从业者。接下来我会把这份代码拆开讲:每个模块为什么这么写、参数怎么调、以及哪些地方最容易翻车。
2. 压缩感知重构二维图像:稀疏基、观测矩阵与重构算法的选型依据
2.1 二维图像为什么能压缩感知重构
一张二维灰度图,本质上是一个二维离散信号,用矩阵表示时每个元素就是像素灰度值。压缩感知的理论基础建立在三个前提上:信号本身或信号在某个变换域里满足稀疏性、观测矩阵与稀疏基之间满足非相干性、重构算法能够从欠定方程组里恢复出唯一解。对二维图像来说,第一个前提天然成立——自然图像在DCT、小波或傅里叶变换下,绝大多数系数都趋近于零,只有少数大系数承载了大部分能量。这就是为什么JPEG能压缩得那么狠,也是压缩感知敢用少于像素总数的测量次数去重构图像的根本原因。
第二个前提决定观测矩阵怎么选。观测矩阵的任务是把高维图像投影到低维测量空间,这个投影过程必须保证不同稀疏信号映射到不同的测量结果上,否则信息就混在一起没法还原。数学上讲,观测矩阵要和稀疏基满足有限等距性质(RIP),工程上讲,通常的做法是让观测矩阵尽可能随机——随机性和非相干性在绝大多数情况下是挂钩的。第三个前提对应重构算法的选择,这也是整个代码包里最容易看出效果差异的环节。
2.2 稀疏基选DCT还是小波
稀疏基决定了图像“稀疏”发生在哪个域,直接影响到重构质量的上限。MATLAB里最常用的两个选择是DCT字典和小波字典。DCT的优点是系数集中在低频,能量聚集性好,代码实现简单,用dctmtx一步就能生成变换矩阵;缺点是高频细节的稀疏性不如小波,对有锐利边缘的图像容易产生振铃效应。小波能同时兼顾低频近似和高频细节,稀疏性更好,重构出来的边缘更干净,但MATLAB里用小波做压缩感知通常要借助小波工具箱,或者自己写变换矩阵,复杂度更高。
从实测角度看,如果图像以平滑区域为主——比如医学影像的软组织、遥感影像的平原——DCT完全够用,而且代码跑得快、不依赖额外工具箱。如果图像纹理复杂、边缘多,比如指纹、城市遥感图,用小波基做稀疏表示在相同采样率下往往能多出1~2dB的PSNR。我在搭建自己的实验环境时,默认先上DCT,把整条链路跑通之后再切换稀疏基对比效果。这样定位问题更高效。
2.3 观测矩阵选高斯还是哈达玛
观测矩阵的选型直接影响测量过程的计算成本、存储压力和重构稳定性。高斯随机矩阵是理论最成熟的选择,randn(M, N)一行代码就能生成,且几乎与任何正交稀疏基都满足非相干性,RIP性质有概率保证。但它的缺点是矩阵元素全是浮点数,当图像分辨率稍高时内存占用会非常夸张——一张256×256的图像,做30%采样的全图观测,观测矩阵就有19660×65536个浮点数,约10GB内存,普通机器直接爆掉。哈达玛矩阵的优点是元素只有±1,乘法变成加减法,计算量和存储量都小一个量级,而且正交性质好;缺点是它与某些稀疏基的相干性偏高,需要配合随机行采样来降低相关性。
工程上我一般遵循这个原则:小块实验用高斯矩阵,追求理论上的最优重构质量;大图或者实时性要求高的场景,改成随机采样的部分哈达玛矩阵。还有一种折中做法是用贝努利矩阵,元素取±1且以概率方式随机生成,稀疏性好、相乘快,但重构质量略逊于高斯矩阵。观测矩阵的归一化也容易被忽略——每一行都应该除以sqrt(M),否则测量值的能量会随M变化,导致重构算法的停止条件失去意义。这部分对应的MATLAB代码我在下一章展开,先把原理选型定下来,后面调参才有基准。
3. 用MATLAB实现观测过程:从全图测量到分块测量的代码落地
3.1 高斯观测矩阵与向量化测量的最小实现
压缩感知的观测过程在数学模型上非常简单:y = Phi * x,其中x是图像摊平后的列向量,Phi是观测矩阵,y是测量向量。下面是针对小尺寸图像(比如64×64)最直接的高斯观测实现,这段代码适合先跑通流程、验证重构算法正确性。
% 生成测试图像:使用Shepp-Logan体模,64x64 img = phantom(64); img = im2double(img); [Nx, Ny] = size(img); x = img(:); % 图像列向量化,长度 N = Nx*Ny N = length(x); % 观测率:取30%的像素数作为测量数 M = round(0.3 * N); % 高斯观测矩阵,除以 sqrt(M) 保证能量守恒 Phi = randn(M, N) / sqrt(M); % 测量过程:投影到低维空间 y = Phi * x;这段代码的逻辑很直白:先读取图像并转成双精度,x = img(:)把二维矩阵按列拼接成一维向量,这是MATLAB里所有压缩感知代码的通用入口。Phi的每一行相当于一个测量基向量,用randn生成标准正态分布随机数,再除以sqrt(M)做归一化。这个归一化是必须的——如果不除,y的能量会随M线性增长,后续重构算法里用残差范数判断停止迭代时就会出现判断失真。
参数说明:M决定采样率,0.3表示只用30%的测量值重构图像。这个值不是随便拍的,经验上自然图像在DCT域做到0.2~0.3的采样率就能获得肉眼可辨识的重构结果,到0.5以上PSNR会明显提升。phi矩阵的规模是M x N,所以M越小内存压力越小,但重构质量也越低。实际跑这段代码建议从phantom(64)开始,因为体模图结构简单、稀疏性好,容易调通;直接上自然照片的话,同样的采样率可能只剩一堆马赛克,让人误以为是代码错了。
3.2 分块观测:把测不了的大图拆成能跑的小块
全图观测的问题在3.1的代码里已经暴露了——64×64的图像,N只有4096,30%采样率下Phi是1229×4096,double类型约占40MB内存,还能跑。换成常见的256×256图像,N是65536,Phi直接突破10GB,任何一台普通办公电脑都扛不住。分块处理是解决这个问题的标准方案:把图像切成8×8或16×16的小块,对每个块单独观测和重构,最后拼回完整图像。
% 分块压缩感知观测与重构:块大小16x16 img = phantom(256); img = im2double(img); [Nx, Ny] = size(img); blockSize = 16; Pb = blockSize * blockSize; % 每块的像素数 Mb = round(0.3 * Pb); % 每块需要的观测数 Phi_b = randn(Mb, Pb) / sqrt(Mb); % 块级观测矩阵,所有块共用 % 划分图像块并逐块观测 numBlockX = Nx / blockSize; numBlockY = Ny / blockSize; y_all = cell(numBlockX * numBlockY, 1); blockIdx = 0; for i = 1:numBlockX for j = 1:numBlockY blockIdx = blockIdx + 1; xb = img((i-1)*blockSize+1 : i*blockSize, ... (j-1)*blockSize+1 : j*blockSize); xb_vec = xb(:); y_all{blockIdx} = Phi_b * xb_vec; % 对每个块做同样的观测 end end这段代码里的关键设计是所有块共用一个Phi_b,而不是每个块单独生成一个观测矩阵。这样做有两个好处:第一,内存占用从原来全图观测的10GB降到Mb x Pb的规模——16×16的块,Mb是77,Phi_b只有77×256,几乎不占内存;第二,所有块用同一个观测矩阵,重构时的稀疏基也可以保持一致,便于批量处理。y_all用cell数组存每一块的测量向量,因为各块的观测结果相互独立,后续重构时可以逐块恢复再拼接。
这里有一个值得注意的折中:分块尺寸不能太小。8×8的块虽然计算更快,但每块的像素只有64个,本身的稀疏性就差,重构质量明显下降。16×16是我踩过很多坑之后觉得比较稳的下限,32×32效果更好但块内计算量按四次方增长。实际操作中,先试16×16,观察重构质量再决定是否加大块尺寸。分块带来的另一个问题是块效应——相邻块的边界处可能出现灰度跳变,后处理时可以用简单的重叠分块或者滤波去缓解,但多数实验场景下肉眼几乎察觉不到。
4. OMP重构算法的MATLAB实现:支撑集、残差与迭代停止
4.1 稀疏基与稀疏系数估计
观测只是压缩感知的前半段,真正决定重构质量的是后半段的算法。重构的本质是解一个欠定方程组——观测矩阵Phi是一个M x N的矩阵,M远小于N,所以方程y = Phi * x理论上无解或有无数解。压缩感知通过稀疏性约束把这个病态问题变成可解的:已知图像在某个变换基Psi下是稀疏的,即x = Psi * s且s只有少数非零元素,那么问题就转变成找到最稀疏的s使得y = Phi * Psi * s成立。
MATLAB里用DCT作为稀疏基时,先要生成DCT变换矩阵。这里有个容易混淆的实现细节:dctmtx生成的矩阵是对称正交的,用它做变换时正变换和逆变换用同一个矩阵即可。
% 生成DCT稀疏基矩阵 N = 256; % 信号长度,对应一个16x16块的像素数 Psi = dctmtx(N); % 256x256的DCT基矩阵,正交且对称 % 验证正交性:Psi' * Psi 应为单位矩阵 orthCheck = Psi' * Psi; fprintf('Psi正交性检查(应为1): %.6f\n', max(abs(orthCheck(:) - eye(N)), [], 'all'));注意上面代码里的N是块内像素数,不是整个图像的像素数。dctmtx(N)生成的矩阵第k行实际上是对应第k个DCT基函数的离散采样,把任意长度N的信号投影到这个矩阵上,得到的就是DCT系数。验证正交性这步很有必要——我之前遇到过用自定义稀疏基时忘了归一化,导致重构出来的图像整体偏暗,检查半天才发现是稀疏基的范数不对。
重构算法里真正求解的是感知矩阵A = Phi * Psi,这个A是M x N的,代表观测和稀疏变换的复合效果。OMP算法每次迭代做的事情就是:从A的N个列里找出与当前残差最相关的一列,把该列加入支撑集,然后用支撑集上的最小二乘解更新系数。整个过程就是“挑最相关的原子→拟合→算残差→再挑”的循环。
4.2 OMP主循环代码与逻辑说明
下面给出一个适合二维图像分块重构的OMP实现。完整性和可读性优先,没有做过度的性能优化,核心逻辑一目了然。
function x_hat = omp_reconstruct(Phi, Psi, y, K) % OMP算法重构信号 % 输入: % Phi - 观测矩阵, M x N % Psi - 稀疏基矩阵, N x N % y - 测量向量, M x 1 % K - 稀疏度估计,即最多允许的迭代次数 % 输出: % x_hat - 重构出的信号, N x 1 A = Phi * Psi; % 感知矩阵 M x N [M, N] = size(A); r = y; % 初始化残差为测量向量 idxSet = []; % 支撑集索引 x_sparse = zeros(N, 1); % 稀疏系数,在Psi域表示 threshold = 1e-6; % 残差停止阈值 for t = 1:K % 第1步:计算残差与A各列的内积 proj = A' * r; % N x 1,每个元素代表对应原子的相关性 [~, pos] = max(abs(proj)); % 找到最相关的原子索引 % 第2步:若该原子已在支撑集,换下一个最相关的 maxIter = 0; while ismember(pos, idxSet) && maxIter < N proj(pos) = -inf; % 屏蔽已选过的原子 [~, pos] = max(abs(proj)); maxIter = maxIter + 1; end if maxIter >= N break; % 所有原子都被选过,提前终止 end % 第3步:更新支撑集,用最小二乘重新拟合 idxSet = [idxSet, pos]; A_sub = A(:, idxSet); % 只取支撑集对应的列 coef = A_sub \ y; % 最小二乘解 % 第4步:更新残差 r = y - A_sub * coef; % 第5步:提前停止判断 if norm(r) < threshold break; end end % 由稀疏系数还原信号 x_sparse(idxSet) = coef; x_hat = Psi * x_sparse; end逐段拆解这段代码的工程含义。第1步里A' * r计算的是残差与每个原子的内积,内积绝对值越大说明该原子与当前残差的相关性越强,这对应OMP的“贪婪”选择策略。第2步的while循环处理重复选列的问题——因为感知矩阵A的列之间并不完全正交,同一个原子可能多次被判定为最相关,如果不屏蔽已选原子,算法就会原地打转。第3步是OMP的核心,用最小二乘拟合当前支撑集对应的系数,这一步让已选原子的贡献被精确估计;第4步的残差更新把这些贡献从测量值里减掉,剩下的部分交给下一轮迭代去解释。
参数说明:K是稀疏度的估计值,太大会引入多余的迭代和噪声拟合,太小则欠拟合。经验做法是先设一个偏大的值(比如round(M/2)),靠残差阈值提前终止来兜底;threshold设为1e-6在大多数图像重构场景下已经足够精确,过小会让算法多跑几十轮而PSNR几乎不变。另外注意A_sub \ y这个写法,MATLAB会自动选择高斯消元或LU分解求解最小二乘,比显式写inv(A_sub'*A_sub)*A_sub'*y数值稳定性更好,后者在矩阵接近奇异时误差会大得多。
4.3 三种迭代停止条件的工程取舍
OMP的迭代停止方式没有标准答案,不同场景下选择不同策略,重构质量和计算速度的平衡点差别很大。我总结下来有三类常见的停止条件,各自有明确的适用场景。
第一种是固定稀疏度K,迭代到K轮直接停。这种最省心,适合对图像稀疏性有先验知识的情况,比如知道某类图像在DCT域里主要能量集中在前多少个系数。缺点是K估少了重构不完整,K估多了算法会把噪声也当作有效信号拟合进去,图像会出现颗粒状噪声。第二种是残差范数阈值停止,也就是4.2代码里的norm(r) < threshold判断。这种做法的优点是能自适应不同图像的稀疏程度,但阈值的量纲跟测量值的能量有关,换图像或换采样率时要重新调,否则阈值形同虚设。第三种是相对能量停止——当残差能量降到原始测量能量的某个比例时停止,比如norm(r) / norm(y) < 0.01。这种做法比单纯阈值更鲁棒,因为做了归一化,不用考虑测量能量的绝对值。
我自己平时默认用第二种加第三种组合:norm(r) < 1e-6 || norm(r) / norm(y) < 1e-3。前一个保证绝对精度,后一个防止不同图像引起的相对偏差。还有一点容易被忽略:停止条件不是越严越好。强制迭代到残差极小,本质上是在拟合观测过程里包含的数值噪声,特别当图像本身有轻微噪声时,过度迭代会让重构结果比真实图像更粗糙。在调参时如果发现PSNR先升后降,大概率就是迭代过头了。
5. 压缩感知重构避坑指南:从全黑重构到内存溢出的五个典型问题
5.1 重构结果全黑或全是NaN
现象:跑完OMP后图像全黑,或者x_hat里有大量NaN。
原因排查:最常见的是观测矩阵或稀疏基里出现了NaN。randn生成的标准正态分布理论上不会产生NaN,但如果代码里手写了某种归一化,且某一行恰好全为零——比如从文件读取观测矩阵后没检查空行——这行做除法时就会产生NaN。另一个原因是感知矩阵A的条件数过大,A_sub \ y求解时出现数值崩溃。
解决:在观测和重构之间加一行检查代码,输出max(abs(Phi(:)))和max(abs(Psi(:)))是否有限。如果是自己写的稀疏基矩阵,务必检查每一列的范数是否为1,不为1的话整个OMP的原子选择逻辑都会失真。还有一个小技巧是只对支撑集做最小二乘时给一个小量正则项:coef = (A_sub' * A_sub + 1e-8 * eye(size(A_sub, 2))) \ (A_sub' * y),能有效避免奇异矩阵的求解崩溃。
5.2 重构图像全是均匀的灰度块,没有细节
现象:图像轮廓隐约可见,但像被重度模糊过,完全没有纹理细节。
原因:稀疏度K设得太小,或者稀疏基和图像本身的匹配度太差。K太小意味着算法只找了少数几个大系数来解释图像,低频信息是有了,高频细节被舍弃。另一个常见原因是观测矩阵和稀疏基的相干性过高——比如用了全取哈达玛矩阵却没做随机行置换,导致部分高频成分在测量阶段就被系统性地丢失,这不是算法能补回来的,属于信息采集阶段的硬损失。
解决:先加大K重现跑一遍,观察细节是否出现。如果K翻倍后依然模糊,大概率是观测矩阵的问题。把观测矩阵换成高斯随机矩阵,或者对哈达玛矩阵做随机行置换,让测量过程更均匀地覆盖图像的各种频率成分。实测里,用randperm对哈达玛矩阵的行打乱,重构质量能提升2~3dB。
5.3 内存溢出或MATLAB直接卡死
现象:运行到生成观测矩阵那一步时,MATLAB报“内存不足”错误,或者电脑风扇狂转、MATLAB长时间未响应。
原因:这就是3.2节里提到的全图观测矩阵爆炸。256×256图像、30%采样率的全图观测矩阵需要约10GB内存,如果边上有其他程序运行、或者系统虚拟内存有限,MATLAB会直接崩掉。更深层的原因是初学者往往把数学公式里的矩阵照搬进代码,没意识到大矩阵在计算机里的存储开销。
解决:无条件切换到分块处理。先用16×16的块把整条链路跑通,确认重构效果之后再考虑加大块尺寸。另外也可以用single精度代替默认的double——观测矩阵的精度对重构结果影响很小,但内存占用直接减半。这个优化在M接近N时效果特别明显。
5.4 重构效果对同一类图像时好时坏
现象:同一份代码、同样的参数,换一张图像测试重构质量波动很大,PSNR相差能到5dB以上。
原因:高斯观测矩阵是随机的,每次运行生成的Phi不同,而不同观测矩阵对图像的感知能力确实有差异。这是压缩感知的固有现象——随机观测矩阵满足的是概率意义上的性质,个别坏的观测矩阵会碰上个别结构特殊的图像,实测效果就差得离谱。另一个原因是测试图像本身的性质差异,含大量细节的图本来就比平滑图更难重构。
解决:实验时固定随机种子。在代码开头加一句rng(42),保证每次跑出来的观测矩阵一致,这样复现结果和对比算法才靠谱。做多图对比实验时,尽量选择同一类图像,或者报告多次随机种子的平均PSNR而不是单次结果。这个习惯能省掉大量“为什么昨天能跑通今天就不行”的排查时间。
5.5 OMP一直不收敛,迭代K次后残差仍然很大
现象:norm(r)在整个迭代过程里下降很慢,跑到K轮被强停,重构图像是一团乱码。
原因:感知矩阵A的条件数太大或者M相对于N太小。当M和N的比例过低时,比如10%采样率,可用信息量本身就严重不足,OMP这种贪心算法容易陷入局部最优,怎么迭代都拟合不好。还有一种情况是信号在实际变换域里根本不稀疏,比如一张纯随机噪声图,任何稀疏基都拿它没办法,这时候压缩感知的理论前提就不成立。
解决:先确认信号确实稀疏,用dct2直接对原图做变换,统计系数绝对值大于某个阈值的数量占总系数的比例。如果稀疏度超过50%,就不应该用压缩感知硬做。其次检查采样率,低于0.15时考虑提高M,或者在分块方案里增大块尺寸——块大了之后每块的稀疏性会更好。最后检查块之间的灰度范围是否需要归一化,某些块整体偏暗会让OMP误判最相关原子。
6. 让重构结果稳定可复现:验证指标与三个调试习惯
最后落到验证方法上。空口说“重构效果不错”不算数,我平时习惯用两个指标把关:PSNR衡量像素级的误差,SSIM衡量结构相似度。PSNR低于25dB时图像基本不可用,28~32dB属于能看出轮廓、细节模糊的水平,35dB以上才是接近原图的质量。SSIM在0.9以上说明图像结构保真度很好,如果PSNR高但SSIM低,往往是图像被过度平滑了——细节没了但误差不大。验证时我会同时算这两个指标,再配合肉眼观察,三者对照才能判断算法是否真正可靠。
% 重构结果验证:计算PSNR和SSIM % img - 原始图像, Nx x Ny % img_hat - 重构图像, Nx x Ny psnr_val = 10 * log10(1 / mean((img(:) - img_hat(:)).^2)); ssim_val = ssim(img_hat, img); fprintf('PSNR: %.2f dB, SSIM: %.4f\n', psnr_val, ssim_val);这里PSNR的计算假设图像灰度范围是0到1,如果图像是0到255的uint8格式,分子要改成255^2,否则算出来的PSNR会比实际偏低约48dB,完全失真。ssim函数需要图像处理工具箱支持,没有工具箱的话可以用结构相似度的简化实现替代,但效果判断上差异不大。
除了指标,我还有三个固定的调试习惯。第一个习惯是固定随机种子再加实验编号——每改一次参数就把参数和结果记到一张表里,不然调参调到最后根本不知道哪个参数组合是最优的。第二个习惯是先跑小图验证正确性再放大——先用phantom(64)把代码链路跑通,确认没有逻辑错误后,再换成phantom(128)或phantom(256),这一步能过滤掉绝大多数编码错误。第三个习惯是每次修改算法后保存重构图像而不是只记指标——有时候PSNR涨了0.5dB但图像出现明显伪影,肉眼能看到指标看不到的东西。
这三个月习惯听起来琐碎,但确实是我在这个方向上栽过跟头换来的。刚接触压缩感知重构时,我凭着一股热情直接拿256×256的图像开跑,代码跑了一整夜,结果因为观测矩阵没归一化,重构出来是一幅均匀灰图,白白浪费一个晚上。后来老老实实按小图验证、固定随机种子、逐项排查的流程走,反而再也没有出现“跑一晚上发现是低级错误”的情况。希望帮到你——如果你也正卡在图像重构效果不理想这一步,试着按上面的避坑清单逐项过一遍,大概率能找到问题所在。
本文还有配套的精品资源,点击获取