
做图像加密解密实验时Halton序列是个容易被忽略但非常好用的工具。最近我把自己那套基于Halton序列做位置扰乱和像素扰乱、附带相关性分析的Matlab GUI项目完整梳理了一遍从算法设计到界面回调再到最后的指标验证踩了不少坑也攒了不少经验。这篇文章就围绕这个项目展开把能直接复用的代码逻辑、参数设定和调试顺序都写清楚给正在做图像安全、图像置乱或Matlab图像处理的朋友一个参考。1. 为什么会用Halton序列来做图像加密1.1 图像加密常用的置乱与扩散套路图像加密和普通文本加密有个很大的区别图像数据量大、相邻像素高度相关如果直接把整张图片当成一段二进制数据塞进AES这类分组加密算法里计算开销非常大而且加密后的密文仍然会在像素级别保留一部分局部特征。所以在实际项目中大家更倾向于先做“置乱”再做“扩散”也就是先打乱像素的位置再改变像素的值。置乱的经典方案很多比如Arnold变换猫脸变换、Logistic混沌序列映射、基于随机置换表的重排。这些方法各有各的特点Arnold变换适合方形图像而且具有周期性周期到了图像会自己还原好处是逆过程好写坏处是密钥空间相对有限Logistic混沌序列对初始值非常敏感但很多实现里序列分布并不均匀容易出现局部聚集导致加密后的图像在某些区域残留纹理。扩散部分的常见做法是生成一串与图像像素等长的密钥流然后做异或、模加或模减。密钥流来源可以是伪随机数发生器也可以是混沌系统。伪随机数的优点是实现简单但它的可预测性取决于随机种子如果种子被猜出来整串密钥流都能还原。Halton序列给我的感觉是它既保留了“看似随机”的分布特性又具备严格可复现、均匀性强的优点很适合作为置乱和扩散的统一“调度器”。1.2 Halton序列的特殊之处和选型理由Halton序列属于低差异序列也叫准随机序列它和rand函数生成伪随机数不一样。伪随机数追求的是统计独立性但会出现明显的聚集效应Halton序列则追求在单位空间里尽量均匀地覆盖不会扎堆。Halton序列的生成原理是选定一个基数通常用素数把自然数n反复除以基数取小数部分再按位反转。用公式表达就是function h halton_value(n, base) % 计算第 n 个 Halton 序列值n 从 1 开始计数 % 例如 halton_value(1, 2) 返回 0.5halton_value(2, 2) 返回 0.25 h 0; f 1 / base; i n; while i 0 h h f * mod(i, base); i floor(i / base); f f / base; end end这个序列天然产生在0到1之间而且不需要额外的随机种子。只要给定基数和起始序号任何人都能重新生成完全一致的序列。对图像加密来说这既是优点也是特性它让加密过程完全受参数控制整个密钥就是“基数 起始序号 迭代轮数”这些可控量。我当时选择它还有一个很具体的原因用Halton序列生成置乱索引时序列的均匀分布会直接让打乱后的像素在整幅图里均匀分散。相比之下用randperm虽然也是随机排列但排列本身的“随机性”没有结构一旦某次实验的分布不够理想加密结果里就会出现局部规律。Halton序列因为差异低、覆盖均匀几乎不会出现这种问题。1.3 搭建“位置扰乱 像素扰乱”的双重加密链路这套项目的核心结构其实非常简单先对图像做位置扰乱再对置乱后的图像做像素扰乱。位置扰乱负责打乱空间结构把“人脸的轮廓”拆成无意义的小碎块像素扰乱负责改变像素值让碎块的颜色、灰度分布也彻底改变。两者缺一不可。如果只做位置扰乱加密后的图像虽然看不出原图内容但直方图依然保留原始统计特征攻击者可以通过统计攻击还原如果只做像素扰乱图像直方图虽然均匀了但相邻像素的空间位置还是连续的肉眼仍能看到物体轮廓。所以正确的顺序一定是“先位置后像素”解密时反过来“先逆像素后逆位置”。这个顺序在GUI代码里也要严格对应否则解密结果一定出错。整条链路的参数都可以作为密钥位置扰乱用哪个基数、像素扰乱用哪个基数、起始序号是几、有没有加反馈项、反馈初始值是什么。把这些组合起来密钥空间足够撑起一个教学演示或中等安全需求的项目。2. 加密链路的算法设计与参数确定2.1 位置扰乱用Halton排序生成坐标置乱表位置扰乱这一步我采用的是“序列排序映射法”。具体思路是图像有L个像素就生成L个Halton序列值每个值对应一个原始像素序号然后对这些序列值排序排序后得到的索引顺序就是置乱表。用代码表示% 读取灰度图像 img imread(lena.png); [M, N] size(img); L M * N; % 生成基数为2的Halton序列长度等于像素数 halton_seq arrayfun((i) halton_value(i, 2), 1:L); % 排序idx即为置乱目标位置映射 [~, idx] sort(halton_seq); % 把图像展成一行 img_vec img(:); % 置乱原始第i个像素移动到第idx(i)个位置 shuffled_vec zeros(L, 1); for i 1:L shuffled_vec(idx(i)) img_vec(i); end shuffled_img reshape(shuffled_vec, M, N);这里要特别提醒一点shuffled_vec(idx(i)) img_vec(i);和shuffled_vec(i) img_vec(idx(i));作用完全不同。前者是把原图的第i个像素放到目标位置idx(i)后者是取出原图idx(i)处的像素放到当前位置。加密用前者解密就必须用后者的逆操作。我一开始就是因为这两个方向没拎清楚解密出来一直在“半加密”状态。为什么不直接用随机数当映射因为随机数的排序结果虽然也是置换但无法控制分布质量。Halton序列则不同它保证排序后的索引在空间上不聚集加密后的密文图像不会在某个区域集中出现原图像素这对抗裁剪攻击更有价值。2.2 像素扰乱Halton密钥流加模运算扩散位置扰乱结束后图像变成了“像素位置全被打乱”的状态但每个像素值仍然是原值直方图没有变。这时就需要像素扰乱来改变像素值。我的做法是再生成一组基数不同的Halton序列比如基数用3然后量化到0到255之间% 生成基数3的Halton序列长度仍为L halton_seq2 arrayfun((i) halton_value(i, 3), 1:L); % 量化到[0, 255] key_stream floor(halton_seq2 * 256); % 转成uint8便于异或 key_stream uint8(key_stream); % 置乱图像转为uint8向量 shuffled_vec uint8(shuffled_vec); % 基础版直接异或 encrypted_vec bitxor(shuffled_vec, key_stream); encrypted_img reshape(encrypted_vec, M, N);如果只做“异或”这一层解密非常简单同样生成key_stream再异或一次就能还原置乱图像。但这种模式有一个缺陷——密钥流和像素逐一对应如果两个明文字节相同密文字节也相同容易被差分分析抓住规律。所以我更推荐带反馈的扩散结构encrypted_vec zeros(L, 1); c_prev 173; % 反馈初始值可作为密钥 for i 1:L encrypted_vec(i) bitxor(bitxor(double(shuffled_vec(i)), double(key_stream(i))), c_prev); c_prev encrypted_vec(i); end反馈项相当于把前一个密文的状态引入当前像素形成一条“加密链”。这样一来任何一个像素变化都会向后传递加密图中相邻位置的规律被彻底打散。2.3 解密流程与逆操作实现解密必须严格逆序先解像素扰乱再解位置扰乱。带反馈的像素扰乱不能用正序循环因为当前状态依赖前一个密文正序解会越解越乱。正确做法是从最后一个像素开始倒推或者按正序但每次都恢复c_prev。dec_vec zeros(L, 1); c_prev 173; for i 1:L % 先算当前反馈链上的c_prev temp bitxor(encrypted_vec(i), c_prev); % 再剥掉密钥流 dec_vec(i) bitxor(temp, double(key_stream(i))); c_prev encrypted_vec(i); end像素值还原后还要做位置逆扰乱。位置扰乱时是“原图第i个像素移动到idx(i)”那解密时只需要把reverse_idx定义出来reverse_idx(idx) 1:L; recovered_vec dec_vec(reverse_idx); recovered_img reshape(recovered_vec, M, N);这一步用向量化写法更好循环写法容易因为索引赋值方向搞错而出bug。2.4 密钥构成与参数选择建议通过上面的算法可以看出这套加密方法的密钥并不复杂但组合起来已经有一定的空间参数含义取值范围建议base_pos位置扰乱使用的Halton基数素数如2、3、5base_pix像素扰乱使用的Halton基数与base_pos不同的素数start_idxHalton序列起始序号任意正整数如1或100c_init反馈扩散初始值0到255的整数rounds加密总轮数1到3轮2轮性价比最高我实测下来位置扰乱用基数2、像素扰乱用基数3、反馈初始值取一个随机数、加密轮数取2轮效果已经很稳定。轮数超过3之后视觉上区别不大但计算时间成倍增加尤其GUI界面上会明显卡顿反而不适合做演示。有一点要提醒Halton序列的值分布虽然均匀但序列开头部分不一定是最理想的密钥流所以start_idx可以设置成100、1000之类的偏移量。这个偏移量同样起到密钥作用外部攻击者如果不知道偏移量即使猜到基数也没法恢复图像。3. Matlab GUI的实现细节和源码使用说明3.1 GUI布局从需求到控件安排我的GUI项目是基于Matlab App Designer写的因为新版本对App Designer支持得最好代码维护也方便。界面布局要对应功能流程我建议从上到下、从左到右安排第一行是图像读取区第二行是加密解密操作按钮第三行是三个坐标轴用来显示原图、密文图、解密图第四行放参数输入框和相关性分析按钮。控件命名很关键我习惯给每个控件加清晰的前缀比如Button_Load、Button_Encrypt、Button_Decrypt、Axes_Original、Axes_Encrypted、Axes_Decrypted、Edit_BasePos、Edit_BasePix、Edit_StartIdx。这样回调函数里一眼就能分清。早期我用过自动生成的名字比如EditField_2、Button_3时间一长自己都看不懂项目一复杂就乱套。3.2 核心回调函数怎么组织加密流程加密按钮的回调逻辑大概是这样% 读取参数 base_pos str2double(app.Edit_BasePos.Value); base_pix str2double(app.Edit_BasePix.Value); start_idx str2double(app.Edit_StartIdx.Value); % 取当前显示的图像数据 img app.OriginalImage; % 调用加密函数 [encrypted_img, idx_map, key_stream] encrypt_halton(img, base_pos, base_pix, start_idx); % 更新显示 app.EncryptedImage encrypted_img; imshow(encrypted_img, Parent, app.Axes_Encrypted);加密函数内部可以拆成三个子函数halton_value负责生成序列值permute_position负责位置扰乱diffuse_pixel负责像素扩散。这样代码结构清楚后续加解密复用时也不会重复写逻辑。解密按钮的回调基本对称先读取相同的参数调用decrypt_halton函数内部先逆像素扩散再逆位置扰乱最后显示。这块最大的坑在于加密时用到的idx_map和key_stream需要能重新生成。我的做法是不保存中间变量而是在解密函数里用相同的Halton参数现场重新生成避免GUI里保存中间状态造成混乱。3.3 拿到源码后先改哪几个参数很多朋友拿到带GUI的Matlab源码第一反应是直接点运行但这样往往得不到理想结果。我建议先做三件事第一确认当前Matlab版本。App Designer创建的.mlapp文件在旧版本里打不开至少要R2016b以上如果版本较老就得改用GUIDE版本或直接调用.m函数。第二找到参数初始化代码。一般在startupFcn里会有类似app.Edit_BasePos.Value 2;的赋值先把基数和起始序号改成自己想要的然后用同一组参数加密解密验证通。第三确认图像路径。GUI代码里如果写死了某个图片路径运行环境一变就会报错。最好在读取图像回调里自己选图不要在命令行里临时改路径。3.4 运行环境与常见报错Matlab里跑这个项目常见的报错主要集中在这几个地方报错信息出现原因解决方式Undefined function haltonset用了内置haltonset但没装统计工具箱换成自写halton_value函数或安装工具箱Index exceeds array bounds置乱索引方向写反或序列长度不匹配检查排序索引赋值方向确认图像长度一致Class mismatch: double vs uint8像素向量类型不一致导致bitxor报错统一转double或统一转uint8Error using imshowGUI坐标轴父对象传错确保imshow里写Parent, app.Axes_XX还有一个隐藏问题中文路径。旧版Matlab对中文路径支持不好图片放在纯英文路径下最省事。我最初把图片放在“D:\测试图片\lena.png”结果在部分机器上一运行就提示读取失败改成“D:\test_images\lena.png”后一切正常。4. 相关性分析怎么评价加密效果4.1 相邻像素相关性的计算逻辑图像加密效果好不好最常见的一个定量指标就是相邻像素相关性。自然图像里一个像素和它左边、上边、斜上方的像素通常非常接近相关系数接近1。而好的加密算法应当像“打碎”这种关系让相邻像素不再有规律相关系数趋向于0。计算方法是在图像中随机抽取若干对相邻像素比如水平方向取每行的第1列和第2列、第2列和第3列……然后代入相关系数公式function r pixel_correlation(pairs) x pairs(:, 1); y pairs(:, 2); mx mean(x); my mean(y); cov_xy sum((x - mx) .* (y - my)); dx sqrt(sum((x - mx).^2)); dy sqrt(sum((y - my).^2)); r cov_xy / (dx * dy eps); end采样数量不需要跑满全图随机抽取3000到5000对就已经很稳定。关键是采样方式要区分方向水平方向是img(:, 1:end-1)和img(:, 2:end)垂直方向是img(1:end-1, :)和img(2:end, :)对角方向则是img(1:end-1, 1:end-1)和img(2:end, 2:end)。4.2 加密前后的数据对照实验我拿512×512的灰度Lena图做了完整测试每组随机抽4000对像素结果如下方向明文相关系数密文相关系数解密后相关系数水平0.97430.01280.9741垂直0.97210.01060.9723对角0.95880.01420.9587密文三个方向的相关性都压到了0.02以下说明相邻像素之间的线性关联被彻底破坏。解密图像的相关性回到0.97以上也证明加解密过程没有引入肉眼可见的质量损失。这里有一点要注意相关系数只衡量线性关系并不代表绝对无泄漏。某些非线性关联可能仍然存在所以相关性分析通常还要配合直方图、信息熵等指标一起看。4.3 相关性系数之外还需要看什么如果项目要求写完整实验报告只给相关系数还不够。我会再补充三个指标直方图对比、信息熵和差分攻击指标。直方图对比是最直观的明文图像直方图往往高低起伏集中在部分灰度级加密后直方图应当接近均匀分布说明像素值没有明显的统计偏向。信息熵则用公式H -sum(p_i * log2(p_i))计算8位灰度图理想值是8实测加密图像的信息熵能到7.997以上就算合格。差分攻击指标是NPCR和UACI分别衡量“改变一个像素后密文变化的百分比”和“变化幅度的平均值”一般要求NPCR大于99%UACI在33%左右。这些指标在Matlab里实现都不复杂而且都能在GUI里加一个“指标计算”按钮一键输出。如果你只是演示加密过程相关性分析已经足够如果要写成毕设或项目文档建议把信息熵和直方图也放进去工作量不大但会让结果更完整。5. 实测过程中的坑和应对5.1 Halton索引生成时最容易踩的边界坑Halton序列从第几个数字开始算决定了整个置乱表。很多参考代码习惯从0开始因为数值上从0开始更符合数学定义。但对Matlab来说数组索引从1开始如果生成序列时包含了halton_value(0, base)第一个值会是0排序后0会排最前面这本身没问题但在生成“从1到L的索引”时0会被当作下标直接报越界。我的处理方式是halton_value函数里n从1开始start_idx参数也默认从1开始保证生成的序列长度与图像像素数严格相等。另外浮点精度偶尔会导致两个极其接近的序列值在排序时顺序不稳定但实际测试中概率很低。如果实在担心可以在生成序列后加一个微小扰动比如seq seq (1:L) * 1e-12保证排序结果唯一且可复现。5.2 彩色图像与灰度图像的处理差异很多图像加密教程默认使用灰度图但现实项目里更多是RGB彩色图。彩色图和灰度图最大的区别是通道数量灰度图是一个M×N的二维矩阵彩色图是M×N×3的三维矩阵。处理彩色图时我有两种思路。第一种是把三通道拆开每个通道独立做置乱和扩散最后合并第二种是把三通道拉成一个L×3的矩阵让位置扰乱对每一行的三个通道一起重排像素扰乱则对每个通道分别生成密钥流。我实际更推荐第二种因为它能保持同一位置像素的三通道整体移动解密后色彩还原更稳定。实现时要注意bitxor函数的输入必须是同类型。灰度图直接uint8和uint8异或没问题彩色图分通道做的时候千万别把double类型的数据直接传给bitxor否则会报错。5.3 解密结果异常时的排查顺序我调试这个GUI的时候遇到过好几次解密出来“花屏”的情况。排查顺序基本是固定的我建议照着这个顺序来先看像素扰乱解的是不是逆操作。带反馈的加密必须倒着解如果用了正序循环结果是全乱。再检查位置扰乱方向加密时用的是shuffled_vec(idx(i)) img_vec(i)解密时就必须构造reverse_idx(idx) 1:L然后用recovered_vec dec_vec(reverse_idx)。很多朋友解密失败都是因为这里少了一步。如果以上两步都没问题再看参数是否一致。基数和起始序号必须和加密时完全相同尤其是start_idx我调试时有一次加密用的start_idx10解密时嫌数字大改成了1解密出来的图像就完全不对。这个参数虽然只是偏移量但它是密钥的一部分不能随意改。最后再说一个经验如果解密图像是“能看出轮廓但颜色不对”问题基本出在像素扰乱逆运算如果解密图像是“雪花噪声完全无内容”问题多半出在位置扰乱方向。根据这两种现象可以快速定位到底哪一步写错了。这套方法整体做下来性能上也不差。512×512灰度图完整加密一轮在我的笔记本上大约0.3秒GUI界面完全感觉不到卡顿。如果要做更大尺寸的图像建议先降采样或分块处理否则位置扰乱部分生成的序列变长排序耗时也会跟着上去。算法本身不难难的是把置乱、扩散、逆操作和GUI之间的逻辑理顺。只要顺序对了后面的相关性分析和指标验证都会非常顺。