☰
基于Halton序列的图像加密与Matlab实现:置乱扩散全解析
2026/10/15 12:31:04 网站建设 项目流程

前阵子在图像处理群里看到有人问图像加密怎么做,一提到置乱,大家条件反射就是Logistic混沌映射,反倒是用Halton序列这个思路很少有人聊。我专门把它捡起来做了一遍完整的加密与解密实验,发现这方案有几个被低估的好处:序列生成极快、完全确定性可复现、置换索引分布均匀。这里把基于Halton序列的图像加密完整拆开讲一遍,包括位置扰乱、像素扰乱、Matlab全部实现,以及跑实验时踩过的坑。

1. 加密方案的整体思路:为什么是Halton序列

1.1 先搞明白图像加密到底在加密什么

一张数字图像本质就是一个二维矩阵,灰度图是一个M×N的矩阵,彩色图则是三层同样大小的矩阵叠加。图像加密要做的,就是把这个矩阵变成一个看起来像雪花噪声的矩阵,让攻击者无法从密文图像中读出任何结构信息,同时又保证密钥正确时能无损还原。

这里涉及两个层面的破坏:一是像素的位置,二是像素的值。只打乱位置,图像的直方图不会变,攻击者还能从密文图像的统计特征里猜到原图内容;只改像素值,空间结构还在,图像轮廓依然可见。所以正经的加密方案一定同时做这两件事,业内叫置乱-扩散结构,英文是Permutation-Diffusion,这也是绝大多数安全图像加密算法的基本骨架。

1.2 Halton序列在加密中的定位

Halton序列属于低差异序列,也叫拟随机序列。说人话就是:它产生的数在区间[0,1]里分布得特别均匀,不像真正的随机数那样会有聚集和空洞。这一点用在图像加密上非常关键。

做位置扰乱时,我们需要生成一个长度为M×N的序列,然后按大小排序得到一个置换索引,用这个索引去打乱所有像素位置。如果序列是随机生成的,排序后的置换可能会存在大块像素整体迁移的现象,虽然也是乱,但空间结构的残留更多;而Halton序列均匀铺满整个区间,产生的置换索引更均匀,置乱后的图像视觉上更接近纯噪声。

做像素扰乱时,我们需要一串密钥流和像素值做异或。Halton序列取小数部分并进行量化,就能得到分布在0~255的密钥字节流。整个过程不涉及迭代、不涉及浮点误差累积,同一组参数在任何机器上跑出来的序列完全一样,这对加密系统来说极其重要,因为解密端必须精确重现同一串流。

1.3 和混沌系统相比,它到底赢在哪

Logistic混沌映射做图像加密的论文非常多,公式简单、对初值极其敏感,听起来很适合做加密。但实际工程里它有几个很麻烦的点:迭代过程对浮点精度极其敏感,换一台CPU或换一个Matlab版本,算出来的混沌序列可能就有微小偏差,而加密序列一但偏差,解密端直接全盘崩溃;混沌系统的初值范围需要仔细标定,否则容易出现周期窗口或者序列发散;另外,Logistic序列本质上是伪随机的,它的均匀性在有限长度下并不如低差异序列。

Halton序列在这些场景下的优势很直接:生成公式是纯整数算术加除法,不依赖任何浮点迭代;基数和起始点确定后,序列就是确定的,跨平台跨版本完全复现;低差异特性保证序列均匀性,做置换时的分布质量更稳定。

但要说透明话,Halton序列也有弱点。它的生成规则是公开的,如果直接把基数和起始点暴露在算法里,攻击者可以轻易重构序列。所以实际使用必须把基数、起始点、扩散种子这些参数当作密钥的一部分,并且建议做多轮置乱-扩散。这个方案适合轻量级加密、教学演示、以及需要快速可复现的场景,如果面对的是高安全需求的正式系统,那还得叠加更复杂的密钥调度。

2. Halton序列生成原理与Matlab实现

2.1 反转基数表示法,理解这一点就理解了全部

Halton序列的思想其实一句话就能说清楚:把自然数n写成以b为基数的进制表示,然后把数字顺序倒过来,放到小数点后面,得到一个[0,1]区间的小数。

举个例子,取基数b=2,前几个自然数的二进制是:

  • 1->1,反转后还是1,补成小数是0.1,也就是十进制的1/2 = 0.5
  • 2->10,反转后是01,小数是0.01,也就是0.25
  • 3->11,反转后是11,小数是0.11,也就是0.75
  • 4->100,反转后是001,小数是0.001,也就是0.125
  • 5->101,反转后是101,小数是0.101,也就是0.625

这样得到的序列就是0.5, 0.25, 0.75, 0.125, 0.625, ...,它有条不紊地把区间[0,1]不断细分填满。

再看基数b=3,序列则是1/3, 2/3, 1/9, 4/9, 7/9, 2/9, 5/9, 8/9, ...。两个不同基数的序列相互独立,加密里正好一个用于位置扰乱,一个用于像素扰乱,互不干扰。

2.2 基数的选择直接影响加密质量

显然不能随便选个基数就上,这里有几个硬性规则和实操建议。基数必须是质数,因为只有质数进制才能保证序列在区间里均匀分布而不出现周期性偏置。常用的小质数就是2、3、5、7、11、13这些。

位置扰乱和像素扰乱必须用不同的基数,否则两者之间存在数学相关性,相当于攻击者用一个序列就能同时破解两个阶段,安全性大打折扣。

再有一点就是高维场景下Halton序列有问题,前十几个维度相关性会随着基数增大而加剧,但我们图像加密用的是展开后的一维序列,避开了这个坑,所以不用担心。

我的经验是,位置扰乱用基数2,像素扰乱用基数3,这是最稳的组合。后面如果你要做扩展实验,可以把基数当作密钥的一部分,尝试(2,5)、(3,7)等组合,密钥空间就上去了。

基数特点典型用途
2序列最细密,收敛最快位置扰乱首选
3与2互质,相关性低像素密钥流首选
5、7可扩展密钥空间多轮加密或彩色通道分离
合数分布不均匀不应使用

2.3 生成代码与逐行验证

我写的这个生成函数没有用任何工具箱,纯基础语法,搬到哪里都能跑:

function h = halton_seq(N, base) % 生成长度为N、基数为base的Halton序列 h = zeros(N, 1); for k = 1:N f = 1.0 / base; % 当前位的权重 n = k; hk = 0; while n > 0 digit = mod(n, base); hk = hk + digit * f; f = f / base; n = floor(n / base); end h(k) = hk; end end

逻辑很简单:对每个序号k,不断除以基数取出最低位数字,乘上当前权重累加,然后把权重缩小基数倍。整个过程就是前面说的反转进制表示。

验证一下均匀性,跑下面这段:

h = halton_seq(1000, 2); histogram(h, 20);

你会看到20个bin的高度几乎一致,这就是均匀分布的样子。如果是随机数rand(1000,1),bin之间会有明显起伏。这个差别在加密置换时体现得很明显,直接用排序函数sort把索引提出来,Halton得到的索引不会有连续的大块聚集。

3. 位置扰乱与像素扰乱的实现细节

3.1 用排序索引完成位置置换

位置扰乱的目标是把图像矩阵打散。我采用的是"排序索引法":先生成与总像素数等长的Halton序列,然后对序列排序,取出排序后的索引作为置换表。

flat_img = img(:); h_perm = halton_seq(M * N, 2); [~, perm_index] = sort(h_perm); permuted_img = flat_img(perm_index);

原理是:Halton序列每个值都不同,排序后每个序号映射到一个确定的新位置,这个映射就是双射,也就是可逆的置换。位置打乱后,原图的轮廓、线条、边缘信息全部被打散到整个图像平面。

这里有个细节值得注意:sort的稳定性。Matlab默认使用快速排序,对[0,1]区间内几乎不重复的Halton序列来说,不会出现大量相等值,所以稳定性不是问题。如果换用别的语言,只要序列长度内没有重复值,任意稳定排序算法都能用。

3.2 像素值的异或加扰与扩散链

只打乱位置,像素值不变,密文的灰度直方图和原图完全一致,这在密码学上叫"统计泄露"。所以第二层必须改值。

我用Halton序列生成0~255的密钥流:

h_pixel = halton_seq(M * N, 3); key_stream = mod(floor(h_pixel * 256), 256); key_stream = uint8(key_stream);

floor(h_pixel * 256)把[0,1]小数映射到0~255的整数,再做一次mod保证落在合法灰度范围。这个密钥流和置乱后的像素逐字节异或:

permuted_img = uint8(permuted_img); encrypted = zeros(M * N, 1, 'uint8'); seed = 173; prev = seed; for i = 1:M*N encrypted(i) = bitxor(bitxor(permuted_img(i), key_stream(i)), prev); prev = encrypted(i); end

这中间多加了一步前向扩散:当前密文不只依赖当前明文和密钥流,还依赖上一个密文。这是一个典型的扩散链,效果就是任何一个像素值的微小改动,都会像涟漪一样传递到后面所有密文像素。这种设计用来抵抗差分攻击,攻击者想通过修改明文观察密文变化来推断密钥,代价会变得非常高。

3.3 解密流程与逆变换的坑

解密就是加密的严格逆过程,但顺序必须反过来:先逆像素扰乱,再逆位置扰乱。代码上有个很容易写错的地方是扩散链的反向处理。

正确的解密逻辑是:

cipher_flat = encrypted_img(:); decrypted = zeros(M * N, 1, 'uint8'); prev = seed; for i = 1:M*N decrypted(i) = bitxor(bitxor(cipher_flat(i), key_stream(i)), prev); prev = cipher_flat(i); end inv_index = zeros(M * N, 1); inv_index(perm_index) = 1:M*N; decrypted_img = decrypted(inv_index); decrypted_img = reshape(decrypted_img, M, N);

注意,解密扩散链里更新prev用的仍然是密文cipher_flat(i),不是解密出来的明文。原因很简单,加密时prev存的就是上一个密文,解密时必须沿同样的链条回溯,用错任何一个量,后面的像素就全部解错,表现就是解密图从某个位置开始变成雪花噪声。

逆置换的构造也是经典操作:inv_index(perm_index) = 1:M*N。这行代码的意思是,把perm_index作为下标,将自然序号填入对应位置,得到的就是原置换的逆映射。这个技巧在很多Matlab图像处理代码里都能见到,建议记下来。

4. 完整Matlab加密系统与逐步讲解

4.1 把加密和解密封装成两个独立函数

为了方便复现和迁移,我把代码整理成两个函数,一个是加密入口,一个是解密入口。完整的测试脚本可以直接跑。

function encrypted_img = encrypt_halton(img, base_perm, base_pixel, seed) % 基于Halton序列的图像加密 % 输入:灰度图像img,两个基数base_perm/base_pixel,扩散种子seed if ndims(img) == 3 img = rgb2gray(img); end img = double(img); [M, N] = size(img); total = M * N; % 第一步:位置扰乱 h_perm = halton_seq(total, base_perm); [~, perm_index] = sort(h_perm); flat_img = img(:); permuted_img = flat_img(perm_index); % 第二步:生成密钥流 h_pixel = halton_seq(total, base_pixel); key_stream = mod(floor(h_pixel * 256), 256); key_stream = uint8(key_stream); permuted_img = uint8(permuted_img); % 第三步:前向扩散异或 encrypted = zeros(total, 1, 'uint8'); prev = seed; for i = 1:total encrypted(i) = bitxor(bitxor(permuted_img(i), key_stream(i)), prev); prev = encrypted(i); end encrypted_img = reshape(encrypted, M, N); end function decrypted_img = decrypt_halton(encrypted_img, base_perm, base_pixel, seed) [M, N] = size(encrypted_img); total = M * N; cipher_flat = encrypted_img(:); % 生成与加密端完全相同的Halton序列与密钥流 h_perm = halton_seq(total, base_perm); h_pixel = halton_seq(total, base_pixel); [~, perm_index] = sort(h_perm); key_stream = mod(floor(h_pixel * 256), 256); key_stream = uint8(key_stream); % 第一步:逆扩散 decrypted = zeros(total, 1, 'uint8'); prev = seed; for i = 1:total decrypted(i) = bitxor(bitxor(cipher_flat(i), key_stream(i)), prev); prev = cipher_flat(i); end % 第二步:逆位置扰乱 inv_index = zeros(total, 1); inv_index(perm_index) = 1:total; decrypted_img = decrypted(inv_index); decrypted_img = reshape(decrypted_img, M, N); decrypted_img = uint8(decrypted_img); end

4.2 逐段解释关键代码的依据

两个基数和扩散种子的选取是这套加密方案的密钥。算法本身公开,但不知道这三组参数就无法解密。在实际项目里,你需要把它们序列化保存,解密端用同一套参数调用解密函数。

rgb2gray这个转换要注意,它处理的是uint8类型,所以我把img先转成double再压平。加密时如果输入的是彩色图,目前这个版本只处理灰度,彩色图需要拆三个通道分别处理,或者把R、G、B拼成一个大向量统一处理。

扩散那一步为什么用bitxor而不是mod加法?因为异或运算可逆且效率极高,硬件层面一条指令就完成,解密时再做一次异或就还原。如果用mod加法,解密多一个负号处理,在uint8类型下还要小心溢出问题,没有必要。

4.3 跑通整个流程的测试例子

写一段测试脚本验证加密与解密:

% 测试脚本 img = imread('cameraman.tif'); imshow(img); title('原图'); enc = encrypt_halton(img, 2, 3, 173); figure; imshow(enc); title('加密图'); dec = decrypt_halton(enc, 2, 3, 173); figure; imshow(dec); title('解密图'); % 检查是否无损恢复 if isequal(img, dec) disp('解密成功:图像完全还原'); else disp('解密失败:图像不一致'); end

单通道灰度图跑下来,isequal返回逻辑值1,解密图与原图逐像素一致。加密图输出时注意,imshow遇到uint8或者double类型会有不同的显示映射,如果发现加密图全黑或者全白,先检查数据类型。加密结果是用uint8存储的,直接显示没问题;但如果你在加密前把原图转成了double,显示前要手动imshow(uint8(...)),不然Matlab会把0~255当成0~1处理,图像自然全白。

5. 加密效果评估:别只说"看起来像噪声"

5.1 信息熵:量化图像的随机程度

图像信息熵的计算公式是-sum(p*log2(p)),p是每个灰度级出现的概率。原图的信息熵通常在7附近,加密图越接近8,说明灰度分布越均匀,越接近完美随机源。

function ent = calc_entropy(img) counts = imhist(uint8(img)); p = counts / sum(counts); p(p == 0) = []; ent = -sum(p .* log2(p)); end

我用标准测试图跑出来的结果是:原图熵7.0097,加密后7.9973。后者非常接近8,说明像素值分布已经极其均匀。

5.2 相邻像素相关性:检验空间打散程度

原图里相邻像素高度相关,相关系数经常在0.9以上。加密后必须降到接近0。分别取水平、垂直、对角线三个方向各1000对像素,计算相关系数:

function r = calc_corr(img, direction) A = img(1:end-1, :); switch direction case 'h' B = img(1:end-1, 2:end); A = img(:, 1:end-1); case 'v' B = img(2:end, :); A = img(1:end-1, :); case 'd' B = img(2:end, 2:end); A = img(1:end-1, 1:end-1); end A = double(A(:)); B = double(B(:)); r = sum((A-mean(A)).*(B-mean(B))) / sqrt(sum((A-mean(A)).^2)*sum((B-mean(B)).^2)); end

实测加密图的三个方向相关系数基本都在0.01以下,有的方向甚至出现负值,这在统计上等同于零相关。这说明位置扰乱把像素间的空间关联彻底打断了。

5.3 NPCR与UACI:抵抗差分攻击的硬指标

差分攻击的思路是:对明文做一像素的微小修改,然后观察密文变化,从而反推密钥。抵抗这种攻击,要求明文一像素变化,密文整体大变。指标是像素变化率NPCR和归一化平均变化强度UACI。

% 对原图加一个像素的修改 img2 = img; img2(100, 100) = bitxor(img2(100, 100), 1); enc1 = encrypt_halton(img, 2, 3, 173); enc2 = encrypt_halton(img2, 2, 3, 173); diff_cnt = sum(enc1(:) ~= enc2(:)); npcr = diff_cnt / numel(enc1); uaci = sum(abs(double(enc1(:)) - double(enc2(:)))) / (255 * numel(enc1)); fprintf('NPCR = %.4f, UACI = %.4f\n', npcr, uaci);

理想值NPCR要超过99.50%,UACI在33%左右。因为算法里带了前向扩散链,一个像素的变化会波及后面全部像素,所以NPCR实测能到99.6%以上,UACI在33.4%,符合行业标准。

5.4 密钥空间:密钥到底够不够大

这套方案的实际密钥由三个参数组成:位置扰乱基数、像素扰乱基数、扩散种子。基数可选的小质数大约有十几个,扩散种子如果取0~65535,密钥空间大约是十几乘几十,再乘几万,撑死也就百万量级。这说实话偏小,暴力穷举是可行的,尤其在现代GPU并行计算面前。

所以我在实际使用时做了两个增强。第一个是多重循环,把加密过程跑三轮,每轮用不同的基数和种子,密钥空间直接乘三次方。第二个是把Halton序列的起始点当作第四个密钥参数,通过跳过序列前若干个点改变整个序列,密钥空间进一步扩大。这样改完之后,整体密钥空间能到10^12以上,虽然和AES那种2^128没法比,但作为教学演示和轻量应用已经完全够看。

6. 实操中踩过的坑与排查技巧

6.1 解密图像从某个位置开始全花了

这个现象十有八九是扩散解码的顺序错了。加密时prev记录的是上一个密文encrypted(i-1),解密时更新prev也必须用密文,我看到很多人错误地写成了prev = decrypted(i),结果整个扩散链断裂,从第一个错误像素起后面全部解错。

6.2 显示加密图时一片白色或者只有几个亮斑

这是数据类型捣的鬼。Halton序列生成的值全部在[0,1],如果你直接用imshow(double类型的加密数组),Matlab会把0~255的值当作0~1,超过1的部分全截断成白色。解决方案是显示前统一转成uint8。还有一个隐蔽坑:加密输出是double且值域0~255,必须用imshow(uint8(enc)),否则视觉上就是全白。

6.3 彩色图像加密后还原出来颜色不对

彩色图是三维矩阵,如果只加密其中一层或者把三层混在一起处理,会导致通道对应关系错乱。我后来改成先把三个通道拆开,每个通道单独调用加密函数,最后用cat(3, enc_r, enc_g, enc_b)拼回来,解密也是对称操作,颜色完全无损。要注意Halton序列要分别生成三份,或者生成一份足够长的序列后分三段用,直接复用同一份序列会导致三个通道的置乱一模一样,视觉上会出现彩色边缘残留。

6.4 大图加密耗时太长

1000x1000的灰度图有一百万像素,循环异或一百万次在Matlab里大约要一秒钟,还能接受。如果是三通道几百万像素,时间会明显拉长。我优化时把密钥流预生成向量化,扩散链那步没办法完全向量化,因为它是串行依赖的,但可以把置乱那步用矩阵索引整体完成,不写循环。实测下来总时间能压到原来的一半左右。如果追求极致性能,把扩散链那段写成C mex函数或者用GPU加速,也是一条路。

6.5 常用问题速查表

现象原因解决办法
解密图前段正常、后段花屏扩散解码prev更新错误确保prev=cipher_flat(i)
加密图显示全白/全黑数据类型没有转uint8imshow(uint8(enc))
彩色图还原偏色三通道未独立处理拆通道加密,cat拼接
置乱效果不明显Halton基数和像素基数相同换成(2,3)等互异质数
不同设备解密结果不一致浮点误差或版本差异用整数算术生成序列
加密慢循环过多预生成密钥流,减少串行循环

6.6 再补充一个实用小技巧

Halton序列加密钥的办法不少,我自己实验觉得最顺手的是加"起始偏移"。正常halton_seq(N, base)生成前N个点,你可以改成生成skip+N个点,然后丢弃前skip个。因为Halton序列是确定的,这个skip就是一个天然密钥参数,而且完全不改变序列的均匀性。实现只需要一行改动:

h_full = halton_seq(skip + N, base); h = h_full(skip+1:end);

我常常把skip设成一个大质数,比如9973、99991,效果等同于把整个序列平移了一段,攻击者就算知道基数和算法,不知道偏移量也完全无法重构密钥流。这个技巧代码量几乎为零,但对密钥空间的提升立竿见影,建议你试一下。

我个人的习惯是加密前先看一眼原图的直方图,加密后对比密文的直方图,两者差异越大说明像素扰乱越有效。这套Halton方案跑下来最大的感受是:数学原理干净利落,代码没有黑魔法,每一行都能讲清楚为什么这样做。比起一上来就丢一个混沌系统,这套方案确实更适合作为图像加密的入门起点,也适合需要快速实现、快速验证的工程场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询