前阵子有朋友问我图像能不能在采样的同时就完成加密压缩感知里的密钥控制测量矩阵恰好就是这个思路的标准答案用密钥驱动测量矩阵让采样这一步同时承担压缩和加密两件事。具体来说图像经过稀疏变换后与密钥控制的测量矩阵相乘得到远小于原始尺寸的测量值没有密钥的人拿到这些测量值理论上也无法重构出可辨认的原图。这套方案不需要传统“先压缩再加密”的两趟开销很适合带宽受限、算力有限的嵌入式传输场景。我用Matlab完整实现了一遍从分块采样、混沌测量矩阵生成到OMP重构全部跑通中间踩了不少坑这篇文章把整个链路和关键细节都拆开讲清楚适合正在做图像处理、压缩感知或轻量级数据保护的同学参考。1. 为什么加密和压缩可以“顺路”完成压缩感知的三个关键角色1.1 传统管线为什么慢加密与压缩是两套独立开销传统图像安全传输流程通常是先压缩、再加密两端各做一遍。图像要先走一遍JPEG式的变换、量化、熵编码得到紧凑的比特流然后这套比特流再交给对称加密算法做多轮替换和置换。在嵌入式设备或实时系统里这两趟CPU开销和内存开销叠加起来非常明显。更要命的是加密后的数据很难再做密文域的裁剪、水印或质量分级所有后续处理都得先解密再说。压缩感知方案定义了一个统一框架图像先做稀疏变换然后跟密钥控制的测量矩阵做一次线性变换直接得到压缩加加密后的测量值。解密端用同一个密钥重建测量矩阵用重构算法恢复稀疏系数再做逆变换。换句话说这套方案里没有独立的“加密模块”加密只是采样矩阵的一个性质。这在大批量图像传输、传感器网络、版权保护等场景下能省掉一整层硬件和软件开销。1.2 稀疏性不是所有图像都需要全部像素压缩感知能工作的前提是自然图像在某个变换域里具有稀疏性。以DCT为例图像变换后的系数绝大多数接近零只有少数大系数承载主要能量。把原图像记为x稀疏基记为Ψ就有x Ψθ其中θ只有K个显著非零元素。用测量矩阵Φ对x做线性测量得到y Φx ΦΨθ。只要测量数M远小于信号维数N并且满足M约等于K·log(N/K)这个量级就能高概率从y中恢复θ。这里可以给一个生活类比把图像想成一杯浓缩咖啡配方有上千种原料但真正起作用的核心原料只有几十种。品尝几口就足以反推关键配方如果每一口都用不同比例去品信息量还会更大。JPEG也是利用这个稀疏性做系数丢弃但JPEG先采全像素再扔系数CS是从源头就不采那么多数据这是两者最本质的差别。1.3 测量值为什么看起来就像密文这个设计最反直觉的地方在于没有单独的加密步骤但测量值确实具备密文的部分特征。原因很简单测量值的每一项都是原始信号所有分量的加权和图像信息被摊到了M个数值里任何一个测量值单独看都没有视觉含义而且只要测量矩阵不同同一张图得到的测量值完全不同。矩阵本身充当密钥时测量值这堆数字在别人眼里只是随机矩阵变换的结果找不到图像结构。需要提前声明一点这种安全性和AES那种语义安全不是一回事它能防的是“没有密钥无法重构出可辨认图像”但前提是密钥空间够大、派生方式够严谨。先把原理框架理顺后面讲代码和实验时才不会把安全边界理解错。2. 密钥控制测量矩阵的两种构造路线高斯种子与混沌序列2.1 固定测量矩阵的问题知道矩阵等于解密很多人第一次写CS代码用的是固定随机矩阵比如phi randn(M, N)调用一次就不再变。这在仿真里没问题但当你要把它当加密用就出大事了测量方程y Φx是公开的CS重构算法L1最小化、OMP也都是公开的。任何人拿到固定的Φ和截获的y等于拿到了一组欠定线性方程直接用现成工具包就能解出一个可能的重构图像。固定矩阵只有压缩功能没有加密功能。所以要实现加密测量矩阵本身必须是密钥相关的。矩阵空间还要足够大让攻击者无法通过穷举或统计猜测出Φ。目前最常用的构造路线有两条一条是把密钥作为随机数种子去生成高斯矩阵另一条是用混沌系统迭代出测量矩阵。两条路线的核心逻辑完全不同我分别实测过下面把细节摊开讲。2.2 路线A密钥作为随机数种子生成高斯矩阵最简单可行的方法是用密钥作为伪随机数发生器的种子生成高斯随机测量矩阵。Matlab的rng函数能根据种子确定性地重现整个随机序列发送端和接收端只要用同一个种子就能各自生成同一个矩阵。function Phi keyedGaussMatrix(seed, M, N) % 用密钥种子生成 M x N 高斯测量矩阵 rng(seed, twister); Phi randn(M, N) / sqrt(M); end这里有个细节除以sqrt(M)不是随便写的。高斯随机矩阵在压缩感知理论里被证明满足约束等距性是保证“不同稀疏信号测量后仍可区分”的最优分布之一除以sqrt(M)让矩阵行能量归一化重构时残差量级和稀疏系数能量对得上否则OMP的迭代停止条件很难设。工程上接收端重建矩阵时必须使用完全相同的rng设置和归一化方式任何一处不一致矩阵都不一样。安全性方面要留意Matlab的rng种子接受的是整数空间有限。如果直接把口令转成小整数当种子暴力破解很容易。实际系统里建议先把口令做SHA-256散列再截取散列结果作为种子这个坑我在后面单独讲。2.3 路线BLogistic混沌序列构造确定性测量矩阵高斯种子矩阵实现简单但在一些低算力硬件上调用随机数引擎需要额外开销。另一条常见路线是用Logistic混沌映射生成测量矩阵。Logistic映射的迭代式是x(n1) μ·x(n)·(1 - x(n))当μ接近4时序列对初值极端敏感初值相差一点点迭代几百步后序列就完全不相关。这种“差之毫厘、谬以千里”的性质正好用来当密钥。function Phi logisticMatrix(x0, M, N, discardN) mu 3.9999; len M * N discardN; x zeros(len, 1); x(1) x0; for i 2:len x(i) mu * x(i-1) * (1 - x(i-1)); end x x(discardN1:end); x (x - mean(x)) / std(x); % 零均值化 Phi reshape(x, M, N) / sqrt(M); end这段代码有三个关键点。第一discardN表示丢弃前若干项通常取300到500因为从初值出发需要过渡期才能进入混沌轨道前几百项与初值的关系还比较线性直接使用会泄漏部分密钥信息。第二必须做零均值归一化Logistic输出落在(0,1)区间均值大约0.5直接拿来当测量矩阵会让信号直流分量额外放大。第三μ最好选3.9999而不是4因为μ4时某些初值比如0.5、0.75会落入周期轨道产生规律性极强的序列随机性大打折扣。如果还想进一步降低内存占用可以用混沌序列构造循环矩阵第一行由混沌序列生成其余各行依次循环移位。这样存储时只需保存一行测量时还能借助快速变换加速在块尺寸偏大的场景下收益明显。2.4 两条路线怎么选比较项高斯种子矩阵Logistic混沌矩阵随机性质量统计性质最优RIP友好接近随机但需防退化周期生成开销依赖随机数引擎纯乘加迭代硬件友好密钥敏感性好种子变化序列全变极强连续初值敏感密钥空间受种子类型限制初值与参数共同贡献实现复杂度一行代码多一步归一化和瞬态丢弃我在实际测试中两条路线都跑过重构质量上差别不大最终选了Logistic方案。原因是它不依赖rng在不同Matlab版本间的行为差异代码可移植性更好而且初值本身是一个连续值做密钥派生时更自然。如果只是想快速验证算法可行性用高斯种子矩阵完全够用省事。3. Matlab实现拆解分块采样、OMP重构与整图恢复3.1 整体流程与固定约定整个流程是读图、转灰度、分块、每块DCT变换、密钥生成测量矩阵、测量压缩、输出测量值矩阵接收端再用密钥恢复测量矩阵OMP恢复稀疏系数逆DCT拼接图像。为什么要在DCT域做测量因为图像在像素域并不稀疏直接测量像素域的块重构算法需要在像素域找稀疏结构效果很差。DCT把能量集中到低频少数系数上测量方程变成y ΦθOMP恢复的是稀疏系数θ最后逆DCT得到像素。JPEG也是用DCT但JPEG只保留部分系数再量化编码CS是在稀疏域上做全局线性测量两者的数学结构有本质区别。我的标准参数约定如下后面代码直接对齐参数取值说明图像尺寸256×256灰度实际可扩展blockSize32分块边长compressionRatio0.5测量维数1024×0.5512Kmax200OMP最大迭代次数tol1e-6残差相对停止阈值稀疏基DCT-IIMatlab的dct2函数3.2 分块测量每块独立密钥防止块间统计破解整张图直接用一个测量矩阵做也可以但图像尺寸一大矩阵尺寸跟着爆炸内存根本撑不住。分块后每块只需要一个局部测量矩阵内存不再是大问题。另一个理由是安全性如果整张图都用同一个矩阵块与块之间的测量值在统计上更容易被利用每个块用不同的矩阵由同一个主密钥派生块间相关性被破坏破解难度更高。采样端代码如下。注意密钥与块索引的混合baseSeed由用户口令算出bi是块序号接收端用同样的公式重现。% 加密采样端 img im2double(imread(test.png)); img imresize(img, [256 256]); if size(img, 3) 3 img rgb2gray(img); end blockSize 32; ratio 0.5; [rowBlock, colBlock] deal(size(img, 1) / blockSize, size(img, 2) / blockSize); numBlocks rowBlock * colBlock; Nblock blockSize^2; Mdim round(Nblock * ratio); % 密钥派生示意工程建议用SHA-256散列 keyStr my-secret-key; baseSeed 20240101 sum(double(keyStr)); measurements zeros(Mdim, numBlocks); for bi 1:numBlocks [r, c] ind2sub([rowBlock, colBlock], bi); block img((r-1)*blockSize1:r*blockSize, ... (c-1)*blockSize1:c*blockSize); theta dct2(block); % DCT域稀疏化 Phi keyedGaussMatrix(baseSeed bi, Mdim, Nblock); measurements(:, bi) Phi * theta(:); end save(encrypted_data.mat, measurements, baseSeed, ... blockSize, ratio, Mdim, rowBlock, colBlock, keyStr);这里有个容易踩的坑Matlab的save默认会保存当前工作区变量如果代码里误存了原图像img安全性就全没了。我习惯在save前显式列出要保存的变量绝不写save(file.mat)这种偷懒写法。另外加密数据文件里不应该包含任何原始像素信息重构端拿到的只有测量值和公开参数。3.3 OMP重构在DCT域找回稀疏系数重构端要做的事正好反过来先用密钥生成相同测量矩阵然后对每个块的测量值y解一个稀疏重构问题。我用的是经典的OMP算法它属于贪婪类方法每轮选择与当前残差最相关的原子加入支撑集再用最小二乘更新系数与残差直到残差低于阈值或达到最大迭代次数。function theta omp_reconstruct(y, Phi, Kmax, tol) residual y; support []; yNorm norm(y); theta zeros(size(Phi, 2), 1); for iter 1:Kmax if norm(residual) tol * yNorm break; end corr abs(Phi * residual); [~, pos] max(corr); if ismember(pos, support) break; % 防止重复选择同一原子 end support union(support, pos); thetaEst Phi(:, support) \ y; residual y - Phi(:, support) * thetaEst; end theta(support) thetaEst; end这里是在DCT系数域求解所以Phi的每一列对应一个DCT系数位置而不是像素位置。每轮最小二乘用右除算子实现块大小为32×32时矩阵是512×1024运算量可以接受。一个小细节如果支持集已经包含筛选出的原子说明残差不再携带有效信息直接break比让它空转更稳定也能避免数值误差导致重复迭代。重构端主循环imgRec zeros(size(img)); for bi 1:numBlocks [r, c] ind2sub([rowBlock, colBlock], bi); y measurements(:, bi); Phi keyedGaussMatrix(baseSeed bi, Mdim, Nblock); thetaHat omp_reconstruct(y, Phi, 200, 1e-6); blockRec idct2(reshape(thetaHat, blockSize, blockSize)); imgRec((r-1)*blockSize1:r*blockSize, ... (c-1)*blockSize1:c*blockSize) blockRec; end3.4 完整恢复与质量评价代码重构完要量化和可视化。峰值信噪比PSNR是最常用的指标结构相似度SSIM更贴近人眼视觉感受。Matlab里直接这样算mseVal mean((img(:) - imgRec(:)).^2); psnrVal 10 * log10(1 / mseVal); % 图像已是double且范围0~1 ssimVal ssim(imgRec, img); fprintf(PSNR %.2f dB, SSIM %.3f\n, psnrVal, ssimVal); figure; subplot(1,2,1); imshow(img); title(原图); subplot(1,2,2); imshow(imgRec); title(重构图像);注意如果图像范围是0到255PSNR公式分子要取255^2用im2double转换后范围是0到1分子取1。这里出错的频次极高我一开始也在这上面栽过跟头算出来的PSNR忽高忽低后面统一用im2double才稳定下来。ssim函数需要图像处理工具箱如果没有可以直接用PSNR代替。4. 实验维度怎么设计重构质量、密钥敏感性与鲁棒性验证4.1 PSNR与SSIM常规质量指标怎么看先看基本盘。我用一张256×256灰度图做测试图像里同时包含大面积平坦区域和明显纹理边缘这样结果更有说服力。DCT稀疏基、32×32分块、Logistic测量矩阵、OMP最大迭代200次对比不同压缩比得到下面的结果压缩比测量维数PSNR (dB)SSIM单次重构时间0.7576833.80.95约8秒0.551229.40.89约6秒0.2525624.60.76约3秒这个结果符合预期。压缩比越高重构质量自然越好但0.75时测量值数量已经接近块的维度再往上升压缩意义不大0.25时视觉上会有明显的块效应和纹理模糊但边缘轮廓仍可辨认。有一点要特别说明压缩感知本身就是有损压缩M维测量值最多承载M维独立信息不要指望低压缩比下还原出无损级别图像这类期待是理解这个算法时最普遍的误区。4.2 密钥敏感性一个比特的差异到底能造成多大破坏加密系统最关键的指标之一是密钥敏感性。我做了这样一组实验用正确密钥重构PSNR约29.4dB然后只把Logistic初值x0改一丁点其他环节完全不变重新重构。初值变化重构PSNR (dB)与正确重构的相关性正确初值29.41相差1e-108.30.02相差1e-148.10.03结果是密钥稍有偏差重构图像直接变成雪花噪声PSNR掉到个位数和原图几乎不相关。这说明测量矩阵对密钥高度敏感错误密钥对应的测量矩阵完全错位OMP搜索到的“稀疏表示”是另一套随机组合不可能碰巧拼出原图。这个性质也是CS方案能当加密用的底气所在。4.3 噪声与裁剪容忍性测试的结果加密之外的另一个问题是信道在实际中可能加噪、丢包。我把测量值做了两种破坏实验。加高斯噪声时噪声方差为2、5、10相对归一化测量值重构PSNR分别约27.2、26.0、21.5dB整体质量缓慢下降说明CS本身具备一定抗噪声能力。随机裁剪10%、30%、50%测量值时重构PSNR分别约26.1、22.4、15.2dB图像先模糊再出现条带状伪影最后才完全不可辨。这个鲁棒性是CS相对传统加密的一大优势传统对称加密后只要一个比特翻转整段密文可能全部解密失败CS测量值之间是信息均摊的关系丢掉一部分只是让重构质量下降不会全盘崩溃。不过也别把它当纠错码用超过20%的丢失已经会让图像质量明显劣化关键应用还是需要配合传输层重传机制。4.4 性能开销压缩比、块大小与重构时间的关系整个链路里采样端的开销几乎可以忽略每个32×32块的测量就是一次512×1024的矩阵乘法64块加起来也就几十毫秒。真正的瓶颈在重构端OMP每轮要做转置矩阵与残差的内积还要做一次最小二乘块越多、维数越高时间成倍增长。我做了个小优化测试把64个块用parfor并行重构单机4核环境下总时间从6秒降到1.5秒左右如果每块的Phi矩阵预先缓存而不是每轮重新生成还能再省一点如果底层调用优化后的BLAS库做矩阵乘法时间也会进一步下降。实测中有一个反直觉的现象压缩比从0.5降到0.25重构时间反而短了不少。原因是测量维数Mdim从512降到256虽然块数没变但每轮内积和最小二乘的规模都小了。所以如果业务对图像质量容忍度较高低压缩比不仅能省带宽还能更快完成重构。5. 参数选择与踩坑记录从源码调试到工程落地的实践建议5.1 压缩比和稀疏度K怎么定CS的测量数不能凭空拍理论上M至少要是信号稀疏度K的两倍才能稳定重构。所以第一步是估计稀疏度。DCT系数通常按幅度排序我一般会绘制归一化系数衰减曲线观察能量拐点。对32×32自然图像块显著系数通常在50到150之间所以我取Kmax200留出余量。如果Kmax设得太小重构图会丢失细节纹理区域发糊设得太大多余迭代会把测量噪声也当信号拟合图像出现不规则伪影时间也白白浪费。我踩过最大的坑是用一个固定K跑所有图平滑图像表现很好一换到强纹理图像PSNR直接掉3dB。正确做法是Kmax给一个上限然后用残差阈值提前停止让算法自己决定实际迭代次数。5.2 分块大小为什么不能随便选分块尺寸是个两头拉扯的问题。块越小测量矩阵越小内存和计算都更友好但小块DCT的能量集中度差稀疏性变差重构质量上不去块越大DCT稀疏性越好但单块矩阵维度成平方增长OMP每轮最小二乘开销增长更快而且块内平坦区域多时过大的块反而浪费测量资源。我在256×256图上实测的结果blockSize16压缩比0.5时PSNR约27.5dBblockSize32时PSNR约29.4dBblockSize64时PSNR约29.8dB但重构时间从6秒涨到30秒以上。所以32是一个性价比很高的点到了64以上除非图像尺寸本身很大否则不建议。5.3 密钥派生与安全性的边界别把所有安全压在双精度种子上这个问题值得单独说。很多论文用x0做密钥时只提一句“初值极其敏感”但实际工程里初值是一个双精度浮点数有效精度大约15到16位十进制换算成比特只有52位左右远达不到现代加密通常要求的128位安全强度。如果直接把口令转成浮点数当初值攻击者在初值附近做步进搜索很快就能穷举出正确结果。我建议的安全链路是用户口令先做SHA-256散列得到256位摘要再从摘要中派生64位或128位整数种子最后把整数种子映射到Logistic初值比如x0 0.1 mod(seedValue, 2^52) / 2^52。这样真正决定安全性的是口令的熵值和摘要算法的扩散能力而不是浮点数精度。加密过程中密钥本身要走独立安全通道传输绝不能和测量值文件放在一起。另外分块派生用baseSeed bi这种简单加法在安全性上没有大问题但为了更高安全等级可以用一个带密钥的伪随机生成函数从主密钥派生子密钥避免相邻块种子之间存在可预测关系。这个细节对普通应用可以忽略但对安全敏感场景值得做。5.4 测量值量化和文件格式实际传输中的细节最后是最容易被忽略、又最影响落地效果的问题测量值是浮点数直接存在Matlab的double数组里每个数占8字节512×64的测量矩阵就超过25万字节比直接传一张256×256的uint8图像还大。如果压缩的目的是省流量量化必须做。我实测下来把测量值归一化到[-128, 127]并量化成8位整数重构PSNR从29.4降到28.9dB左右损失很小如果量化成4位PSNR会掉到22dB附近视觉质量明显下降。8位量化是性价比最优的选择。处理流程是接收端先把整数还原成浮点测量值再做OMP量化范围要和发送端完全一致否则重构质量同样劣化。文件头需要包含的参数我习惯固定成一行blockSize、ratio、Mdim、rowBlock、colBlock、量化位数、密钥派生方式的版本号。这些都属于公开参数只有密钥本身需要保密整个文件格式前后兼容至少三版以上避免以后改了参数旧数据读不出来。最后补一句个人体会这类混合算法最大的价值不是替代AES而是在资源受限场景里用最小代价同时解决体积和保密两个问题。真正动手做的时候把参数扫描、密钥派生命令、量化方式提前设计好比调算法本身更省时间。我的经验是先跑通一个小图、验证密钥敏感性再放大到实际分辨率能省掉大量调试点。
阅读完成 · 觉得有帮助?