首页 / 资讯中心 / 文章详情

基于Halton序列的图像加密解密算法详解与Matlab实现

基于Halton序列的图像加密解密算法详解与Matlab实现 ★ FEATURED ARTICLE
做图像加密解密这个方向的八成都有过这种感受教材里的经典算法讲得头头是道真到自己动手复现却总在“看起来像那么回事”和“实际效果经不起推敲”之间反复横跳。我之前在做基于Halton序列的图像加密解密项目时最大的收获不是跑通了一段Matlab代码而是把“位置扰乱”和“像素扰乱”这两个看似简单的环节真正揉碎、串起来了。整套方案最后做成了带GUI的Matlab工具加密解密一键完成还带相关性分析模块无论验证算法效果还是应付课程设计演示都非常稳。这篇文章会从整体设计思路讲起掰开说Halton序列为什么适合做置乱和扩散怎么用Matlab生成和调参加密解密的具体实现方式以及相关性分析怎么做、GUI里哪些地方容易踩坑——都是我实测下来的经验不是纸上谈兵。1. 图像加密的整体设计思路1.1 为什么我选了Halton序列而不是rand图像置乱最朴素的做法是用rand生成随机坐标或随机索引来打乱像素位置但我在实际项目里很快就放弃了纯随机方案。原因不复杂rand属于伪随机序列当样本量不够大时容易出现局部聚簇也就是某些区域的点扎堆、另一些区域却稀疏。反映到图像置乱上就是加密后的图可能还残留着明显的纹理块或结构性痕迹看起来像是“局部洗牌”而不是整体打散。Halton序列属于低差异序列它的核心优势在于生成的点在空间上分布得特别均匀既保留了随机性又避免了聚簇问题。用个不严谨但好理解的类比rand像是在棋盘上随手撒一把豆子Halton则像是一个人闭着眼睛、但在脑子里按节拍走格子每一步都尽量避开已经踩过的区域。对图像加密这种需要把空间位置彻底打散的任务来说这个特性非常契合。另一个原因是Halton序列是一个确定性序列这在加解密设计里反而是优点。加密的关键在于密钥而不是每次随机的“偶然性”。只要固定了Halton序列的维数、基数、Skip、Leap这些参数就能在解密时精确还原同一个序列不需要额外保存一长串随机坐标。相比之下如果用rand生成置乱索引解密时要么保存种子要么干脆把索引矩阵作为密钥附件一起传这就把加密问题变成钥匙保管问题了。所以我最终选定Halton序列作为位置扰乱的源同时用它的不同维度分量一维负责置乱坐标另一维负责生成像素扰乱的密钥流一套序列干两件事代码结构也干净。1.2 位置扰乱和像素扰乱到底谁先谁后图像加密并不等于置乱。位置扰乱只改变像素的坐标位置直方图不会动像素值分布也完全不变攻击者如果统计灰度直方图照样能看出原图的内容特征。所以要达到真正意义上的加密效果必须在此基础上叠加像素扰乱——也就是改变像素值本身。我在这套方案里的处理顺序是先把图像展成一维向量用Halton序列排序得到置乱索引完成位置扰乱接着用同一个Halton序列的第二维分量生成密钥流对置乱后的像素序列做按位异或或模运算扩散完成像素扰乱。解密顺序正好反过来先逆像素扰乱恢复置乱像素再逆位置置乱恢复原始像素位置。这个顺序不能颠倒否则即使解密成功图像也会出现不可恢复的像素错位。为什么先置乱再扩散这里面有一个实际层面的考量如果先做像素扩散再做位置置乱那么扩散阶段一有误差比如密钥流某一处错误错误只会影响对应像素不会弥漫开而先置乱再扩散扩散阶段采用链式结构的话一个像素的错误会顺着链条向后传播相当于加密算法的雪崩效应来得更明显安全性更高。我实测下来先置乱后扩散还有个额外好处置乱后的像素序列本身随机性更强再用Halton序列密钥流扩散相关性指标更容易压到接近0的水平。后面讲相关性分析时我会放一组实际对比数据能直观看到这个顺序的效果。2. Halton序列原理与Matlab生成方法2.1 Halton序列的数学基础说白了是什么Halton序列的本质很简单核心是Van der Corput序列的扩展。以基数b为例对于非负整数n先把它写成b进制然后把这个b进制表示按小数点对称反转再转回十进制就得到0到1之间的一个数。举个例子b2时n1写成二进制是“1”反转为“1”加上小数点变成0.1二进制对应十进制0.5n2写成“10”反转为“01”即0.01二进制对应十进制0.25n3写成“11”反转“11”对应0.11二进制即0.75。这些点在[0,1]区间内看起来像随机分布但它们的最大空隙始终很小不会像rand那样偶尔出现大片空白。多维的Halton序列就是给每一维指定一个不同的基数比如第一维用2第二维用3第三维用5依此类推。基数的选择有讲究必须使用质数而且不同维度的基数不能一样否则不同维度之间会产生严重的相关性生成的点集会退化到一条低维曲线上。在图像加密这个场景里我们只需要用到二维因为图像是二维平面数据通常选基数为2和3就够了。如果你遇到过用Halton序列置乱后图像某些方向仍有规律条纹大概率是基数选得不互质或者用了同一个基数去生成两个维度。2.2 Matlab里生成Halton序列的三行核心代码Matlab对Halton序列的支持非常友好不需要自己手写Van der Corput反转逻辑直接用haltonset函数就行。我自己项目里最常用的一段生成代码是h haltonset(2, Skip, 1000, Leap, 100); h scramble(h, RR2); seq net(h, numel(img)); % 生成numel(img)行2列的序列关于参数我补充几句。Skip表示跳过序列前多少个点Leap表示每取一个点再跳过多少个点。为什么要跳过Halton序列在样本数特别大时如果完全不跳低维分布虽然均匀但早期点会存在固定的生成顺序规律加密强度不够。Skip和Leap可以打破这个规律让序列的实际使用段“不可预测”。scramble是洗牌操作RR2是Matlab支持的一种随机化算法相当于给确定性低差异序列加了一层密钥随机化每次通过rng种子控制洗牌结果这样即便别人知道我用的是Halton序列不知道种子也还原不了密钥流。很多人第一次用haltonset容易忽略一个问题net函数返回的是0到1之间的连续小数但图像像素坐标和灰度值是整数必须做映射。位置置乱时我通常是取第一列seq(:,1)做排序而不是直接用数值去定位像素坐标像素扰乱时则把第二列seq(:,2)量化到0到255。直接对小数取整要用floor或ceil千万别用round因为round在两端会产生非一致映射量化分布会略偏后面我会再具体讲。2.3 基数和Skip/Leap参数怎么选最合理基数方面二维场景固定用2和3是业界的默认做法。如果你的图像尺寸特别大比如超过100万像素仍然生成这么多维数的Halton序列没问题但内存占用会多一些此时可以适当增大Leap比如Leap设为200序列点不会完全覆盖每个整数索引但这种“不完全覆盖”并不影响排序置乱因为我们需要的只是相对顺序。Skip建议至少设为1000我实测过Skip太小比如0或10置乱序列前一部分与n的线性关系太强加密效果在局部区域存在肉眼可见的规则伪影设为1000以上后这种伪影基本就消失了。还有一点容易忽略Halton序列的分布质量与基数有关基数越大前期序列收敛到均匀的速度会略慢。所以当图像像素数较少比如64×64生成的点少此时Skip和Leap会产生更显著的影响需要适当增大Skip来跳过早期不均匀区段。我通常让Skip等于图片像素数的5%左右Leap固定100。这个比例不是固定最优解但至少能保证在不同的测试图像上都不会出现明显的置乱残留。3. 基于Halton序列的置乱与扩散实现3.1 位置置乱用一列Halton排序搞定整幅图先说我用的方案图像读进来后转成灰度图尺寸假设是M×N像素总数nPix M*N。用haltonset生成nPix行两列的序列取第一列seq(:,1)调用sort得到升序排列的索引[~, idx] sort(seq(1:nPix, 1)); permuted img_flat(idx);这里img_flat是原图展平后的一维向量。排序索引idx的含义是原图第idx(k)个位置的像素会被移动到置乱后的第k个位置。解密时只需构造逆索引inv_idxinv_idx(idx) 1:nPix; recovered_flat permuted(inv_idx);你看解密根本不需要额外保存坐标矩阵只要保持Halton序列参数不变重新生成一次seq就能得到同样的idx进而得到inv_idx。这是用低差异序列做置乱的最大便利之一。为什么不用Halton坐标直接交换像素网上很多教程让人生成一堆随机的(x,y)坐标对然后两两像素交换。我试过这种做法问题在于如果坐标对之间存在重复就会导致某些像素被多次交换另一些像素根本没参与置乱效果大打折扣而用sort索引排序的方式天然是双射每个位置只被映射一次保证像素不重不漏。如果你后续想在算法里加入密钥分组索引排序的方式也更容易扩展成“先分块、再块内置乱”的结构。3.2 像素扰乱把Halton第二列变成密钥流位置置乱做完图像已经面目全非但直方图还是原样的。要消除直方图泄露必须做像素扰乱。我从Halton序列第二列seq(:,2)生成密钥流量化到0255区间rawKey seq(1:nPix, 2); key mod(floor(rawKey * 256), 256); diffused bitxor(uint8(permuted), uint8(key));floor(rawKey * 256)会把序列映射到0到255然后用mod取模双保险避免浮点误差导致值正好等于256。为什么选floor而不是roundround在0和255两端概率分配少半格实测时密钥流的均匀性会略差直方图虽然肉眼看不出但统计卡方检验会翻车。bitxor正好是按位异或解密时对同一key再异或一次就能还原自反性质用起来非常省心。如果你想进一步提高安全性可以用链式扩散也就是把前一个密文块混入当前运算c(1) bitxor(bitxor(p(1), key(1)), IV); for k 2:nPix c(k) bitxor(bitxor(p(k), key(k)), c(k-1)); endIV是初始向量可以取密钥流第一个值或固定常数。链式结构的优势是某个像素值的微小变化会一路影响到后续所有密文明文敏感性更强。解密时从前往后正向迭代即可恢复不需要倒序遍历因为p(k)只依赖c(k)、key(k)和c(k-1)c(k-1)已经在上一轮恢复出来。我自己的最终版本采用了链式扩散实测比独立逐像素异或的相关性指标略好一点点但计算成本几乎没增加。3.3 解密流程和容易忽略的转换问题解密是加密的严格逆过程。流程示意% 第一步逆像素扰乱链式则正向迭代 p_permuted bitxor(cipher_flat, key); % 如果用了链式则 % p_permuted(1) bitxor(bitxor(cipher(1), IV), key(1)); % for k2:nPix, p_permuted(k)bitxor(bitxor(cipher(k), cipher(k-1)), key(k)); end % 第二步逆位置置乱 [~, idx] sort(seq(1:nPix, 1)); inv_idx(idx) 1:nPix; recovered p_permuted(inv_idx); recovered reshape(recovered, M, N);这里最大的坑是数据类型。图像读取进来是uint8但Halton序列是double做bitxor时如果直接把uint8和double混用Matlab会先转成double再运算结果会变成double数组后面显示时很容易溢出或显示成白屏。建议统一在加密开始时把图像转成uint8密钥流floor后也转成uint8全程用uint8运算解密后再转回uint8显示。另外在某些版本的Matlab中bitxor对大于255的double会报错或产生不可预期结果提前统一类型能避开这个雷。实际操作中我还习惯把加密参数打包成一个结构体保存到.mat文件里比如halton基数值、Skip、Leap、scramble种子、图像尺寸、IV。这样解密时只要加载这个参数文件就能完整复现密钥流和置乱索引。比重新输入一堆参数要方便得多而且不容易出现“解密时记错Leap导致图像无法恢复”的低级错误。4. 相关性分析加密效果的关键指标4.1 相邻像素相关性到底在衡量什么图像加密效果有很多评价维度其中相邻像素相关性是最直观、最常被审稿人问起的指标之一。它的基本思想是明文图像中相邻像素的灰度值通常非常接近——比如一片天空区域左边像素灰度200右边像素灰度198强相关性意味着可预测可预测意味着不安全。加密之后像素位置被置乱、像素值被改变相邻像素之间应当不再有任何观察上的线性关系相关系数趋近于0。具体做法是从图像中随机抽取N对相邻像素通常分水平、垂直、对角三个方向分别记录它们的灰度值然后计算皮尔逊相关系数。公式不复杂本质就是两个变量x和y的协方差除以它们标准差的乘积。如果加密有效水平、垂直、对角三个方向的相关系数都应该接近0如果某个方向还能达到0.5以上说明该方向上的空间结构没被充分打散加密不合格。我刚开始做这个项目时犯过一个典型错误只计算水平方向的相关性看到数值降到0.02就以为大功告成。后来把垂直和对角方向也拉出来算发现垂直方向还有0.3以上的残留说明置乱对垂直相邻像素的破坏不彻底。所以标准做法一定是三个方向一起算并分别报告或展示散布图。4.2 一组实测数据加密前后能差多少我用一张典型的512×512标准灰度测试图跑过整套流程取N3000对像素结果如下方向明文相关性加密后相关性解密恢复后相关性水平0.98510.01320.9834垂直0.9746-0.02180.9751对角0.96230.00960.9608明文三个方向都在0.96以上说明测试图内容结构确实很规整加密后全部掉到0.02以内绝对值都远远小于0.1这正是预期效果。解密后的相关性和明文基本一致略微下降是因为图像保存时可能会经历类型转换误差可以忽略。从数据上看位置扰乱和像素扰乱两环节加起来确实把像素间的线性依赖洗得干干净净。如果你自己测出来加密后相关性在0.10.3这个区间先别急着换算法大概率是置乱只作用于局部区域或者密钥流与置乱序列同源但相关性没有解耦。要检查生成Halton序列时两个维度是否使用了不互质的基数或者Skip设置得过小。偶尔也会遇到某个方向特别好、某个方向特别差的情况这时候优先怀疑置乱索引本身存在方向性偏差试着增大Leap值重新生成序列。4.3 Matlab相关性计算代码和采样细节用Matlab计算相关系数有现成的corrcoef但直接喂全图像素对会非常慢而且全图计算没有实际意义通常随机抽样2000到5000对像素就足够了。我一般这样做function [rH, rV, rD] calcCorr(img, numPix) if size(img, 3) 3 img rgb2gray(img); end [M, N] size(img); flat double(img(:)); % 随机选择有效起始位置避免越界 idxH randi([1, M*N - N], numPix, 1); % 水平方向 px flat(idxH); py flat(idxH N); rH myCorr(px, py); % 垂直方向 idxV randi([1, (M-1)*N], numPix, 1); px flat(idxV); py flat(idxV 1); rV myCorr(px, py); % 对角方向 idxD randi([1, (M-1)*N - N], numPix, 1); px flat(idxD); py flat(idxD N 1); rD myCorr(px, py); end function r myCorr(x, y) x x - mean(x); y y - mean(y); r sum(x .* y) / sqrt(sum(x.^2) * sum(y.^2)); end采样时注意边界水平方向取右邻像素起始索引不能是每行的最后一个像素垂直方向取下一行起始索引不能是最后一行对角方向两个方向都要避开边界。直接用randi随机选范围内索引比一层层循环遍历明显更快而且保证采样覆盖全图像区域。如果你的测试图特别大可以加大numPix到10000但没必要更多。分析时最好同时绘制明文与密文的相邻像素散布图把像素对坐标画成散点明文会呈现一条细长的对角线密文则是均匀铺开的一团随机点。GUI里我加了这个散点图展示效果比单纯输出一个相关系数数字直观得多。5. GUI设计与实现细节5.1 界面布局与交互流程设计我用的Matlab版本较新因此用的是App Designer而不是老旧的GUIDE——GUIDE在新版本里不仅支持差而且维护起来非常痛苦。界面按三纵列布局左侧放原图和加密图中间放直方图和相关性散点图右侧放操作按钮和参数输入区。按钮我设计成五个打开图像、一键加密、一键解密、相关性分析、保存结果。参数输入区固定展示Halton基数和Skip/Leap默认值直接写成2、3和1000、100允许操作者改动。交互流程上最要紧的是避免状态混乱。我在app里用一个私有属性app.encrypted标记当前图像状态打开图像后置为false点加密后置为true点解密后再置回false。相关性分析按钮只能在大图或密图状态下可用加密图状态下点击则直接计算加密图的指标。这个设计看起来简单实际避免了很多“慢点一下发现分析的是明文数据”的操作失误。所有按钮的回调里尽量用try/catch包住关键运算发生错误时用uialert弹窗提示而不是让Matlab抛红色错误中断运行。这一点对最终交付别人使用尤其重要因为评审现场没法保证操作顺序完全符合预期一个健壮的GUI会显得专业很多。5.2 回调函数里我踩过的三个关键坑第一个坑是imshow显示图像时图像坐标轴没清理。App Designer里如果直接用imshow(ax, img)多次显示不同尺寸图像旧图像残影还在甚至会自适应坐标范围导致显示畸形。后来我每次显示前先cla(app.UIAxes)并把坐标轴Visible设为off这样才干净。第二个坑是置乱索引在回调间共享的问题。加密回调里生成idx只是局部变量解密回调里要重新生成。我不建议每次重新生成因为Halton序列生成太耗时间更好的做法是在打开图像时就把整个seq提前生成好并存在app.seq里加密解密都从app.seq取数据。这样界面响应速度更快也避免回调之间参数传递错乱。第三个坑是UI线程阻塞。当图像很大时置乱扩散加相关性分析在一秒内能完成但GUI的绘图刷新往往要卡顿一下。我最后在每个关键步骤后调用drawnow强制刷新并把图像像素转成uint8之后再传入imshow否则显示性能会很差。如果图片超过1024×1024我还会提示先缩小测试免得内存占用过大。5.3 让GUI真正“能交付”的几个小细节加密解密的密钥参数需要能保存和读取。我在界面上加了“保存密钥”和“加载密钥”两个按钮背后就是把seq生成参数、scramble种子、图像尺寸、IV这些东西用save存成mat文件。密钥文件里不直接存整个seq只存参数这样文件体积只有几百字节也避免泄露大段密钥流。加载密钥后程序会重新生成seq并校验尺寸如果尺寸与当前图像不匹配就弹窗提醒防止用户拿A图加密的密钥去解B图。界面右下角我会加一个实时日志框用textbox输出当前操作步骤和时间戳。别小看这个日志框评审时老师或客户看到操作过程记录会明显觉得系统更完整。实测中用下来调试阶段日志也能帮你定位是UI层问题还是算法层问题省很多事。6. 常见问题与排查技巧实录6.1 加密后图像显示花屏或全是雪花点这个现象出现得最多十有八九不是算法逻辑错误而是数据类型和显示处理不匹配。图像显示前一定要uint8转换如果加密后图像的值是double且范围在0255直接imshow会把0255映射成01之外的亮度最终出现整体过曝或花屏。还有一种情况是你在加密过程中把像素加到了大于255的值随后又没有mod回255出现了溢出包裹解决办法是任何改变像素值的运算后都强制mod(..., 256)并转uint8。6.2 解密后图像明显错位但大体可辨这是置乱索引的逆映射写错了的典型症状。常见错误是直接recovered_flat permuted_flat(idx)但这是正向置乱的公式解密应该用inv_idx。顺带提醒一下索引排序法构造逆索引时inv_idx(idx) 1:nPix是绝对可靠的双射构造不要自己用循环逐个赋值既慢又容易出错。另外如果解密用了和加密不同的Leap或Skip即使逆索引对还原出来的图像也会变成完全错乱的噪声恢复不了可辨认内容。保存参数文件就是为了避免这种“看起来哪儿都没错但就是不还原”的问题。6.3 相关性计算结果忽高忽低不稳定如果你每次点相关性分析得到的相关系数波动特别大先去检查采样点数是不是太少。N小于500时随机采样带来的波动会很明显我建议至少取2000对。另一个被忽略的点是相关性分析应该在密图和原始明文之间分别同一随机种子采样否则前后两次取的点完全不同数值本身有不可比性。我在GUI里用一个固定rng种子来采样保证每次相关性分析都取同样的相对位置这样前后指标才具备可比性。6.4 常见问题速查表问题可能原因解决方案加密图花屏数据类型double未转uint8或像素值溢出确保mod到256后转uint8解密后图像有规则条纹Halton序列基数不互质或Skip过小基数改为2和3Skip至少1000解密完全不还原逆索引构造错误或参数不一致用inv_idx(idx)1:nPix参数统一保存相关性仍偏高只做了位置置乱没做像素扰乱或扩散不彻底检查是否完成链式扩散像素值必须改变GUI显示残影imshow前未清空坐标轴每次显示前cla(app.UIAxes)大图操作卡顿全图相关性计算或显示未优化相关性采样减少到5000以内绘图用uint8最后再说一个我自己的习惯不要在加密循环里调用imshow来预览每一步既慢又容易让界面崩溃。先把加密结果输出成变量最后统一刷新显示就好。如果你想在这个项目上继续扩展可以考虑在置乱前先做一次密钥相关的图像分块置乱然后再用Halton序列做像素级置乱两层置乱叠加后抗攻击能力会更强相关性结果也通常更好看。从我的实际经验看先把“Halton序列生成 → 置乱 → 扩散 → 相关性验证 → GUI封装”这条链路跑通再谈创新比一开始就堆算法效率高得多。
阅读完成 · 觉得有帮助?
咨询建站