
做图像加密实验很多人搜到的第一个算法就是Arnold置乱变换也就是俗称的猫映射Cat Map。网上关于它的MATLAB代码确实一抓一大把但能用、能跑、效果又对的并不多。要么只处理正方形图像要么跑完发现图只是错位了几个像素要么压根没人告诉你要逆变换才能还原。这篇文章我打算把这个实验从头讲透Arnold变换的公式怎么理解、MATLAB代码怎么写才不出错、加密效果用什么指标衡量、有哪些防不胜防的坑以及——为什么它严格来说只算“混淆”而不是“加密”。1. 先捋清楚Arnold置乱在图像加密流程中的真实角色1.1 置乱不等于加密它只做像素位置混淆我第一次用Arnold变换做实验时以为加密就是把图像打乱。后来才发现这个理解偏差很大。图像加密通常拆成两步混淆Confusion和扩散Diffusion。混淆负责把像素的位置打乱让你看不出原始轮廓扩散负责把像素值本身改掉让单个像素的变化能影响到整幅图像。Arnold置乱变换干的只是混淆这一件事而且是最基础的混淆——它只挪位置不改像素值。换句话说你拿一张灰度图进去出来的图每个像素的灰度值一个都没变只是所有像素换了个座位。这也是为什么很多教材把Arnold叫作“像素置乱”而不是“像素加密”。明白了这个定位后面很多现象你就能自己解释了为什么置乱后直方图不变因为灰度值集合根本没变。为什么有些攻击手段防不住因为它没改变像素值的统计特性。1.2 一个经典实验结果置乱后直方图纹丝不动你可以做个很直观的验证把原图的直方图画出来再做50轮Arnold置乱再画直方图。两张直方图几乎完全重合信息熵也基本不变。我第一次看到这个结果时还挺震惊的感觉“加密了个寂寞”。但这恰恰说明了置乱的边界。如果后面不接扩散层敌手只要拿到你加密后的图统计灰度分布就能猜出大致内容类型甚至通过选择明文攻击恢复部分结构。所以你在课程报告里千万别写“Arnold变换是一种安全的加密算法”这种结论正确的说法是“Arnold变换实现了像素位置的混淆可作为图像加密系统的前置环节”。那么为什么它只挪位置就能让图像看起来完全变样因为人的视觉对结构比对灰度更敏感。原本相邻的像素被拆开丢进完全不同的位置轮廓、纹理、边缘这些结构信息就全碎了。但这种“碎”是结构层面的不是统计层面的。这个概念先立住后面说加密流程设计时还要用到。2. 猫映射的数学原理一行公式背后藏了三次几何操作2.1 离散公式与矩阵形式Arnold变换的连续形式是作用在单位正方形上的图像处理用的是离散形式。设图像尺寸为N×N像素原坐标为(x, y)一次变换后的坐标为(x, y)公式是x (x y) mod Ny (x 2y) mod N写成矩阵形式更清晰[x] [1 1] [x] [y] [1 2] [y] (mod N)矩阵A [[1, 1], [1, 2]]就决定了变换的所有性质。N是图像边长必须是方阵否则模运算没有统一周期。坐标x、y从0开始计数这一点在写MATLAB代码时最容易出问题后面我会专门讲。这个公式怎么来的它其实是从猫映射Arnolds Cat Map的连续版本离散化来的。连续版本把一个点(x, y)映射到(x y, x 2y)再对1取模因为原变换定义在单位正方形上取模相当于把落出去的坐标折回正方形内部。离散化之后把单位正方形换成N×N网格取模换成mod N就成了上面这套公式。2.2 可逆性来自行列式1周期性来自模运算为什么这个变换可以还原因为矩阵行列式是1×2 - 1×1 1整数矩阵行列式为1意味着它的逆矩阵也是整数矩阵。也就是说坐标变换是双射每个像素都有唯一的来源和去处没有两个像素会被映射到同一个位置。你在MATLAB里跑一遍会发现置乱后的图每个像素都能找到原图对应位置不存在信息丢失。再深一层这个变换还有一个重要特征它是周期性的。因为所有坐标都落在有限集合{0, 1, ..., N-1}上一个双射作用在有限集合上反复迭代一定会在某一步回到起点。这个周期T和N有关常见经验值是T约等于3N但并不是严格的3N实际周期要按具体N跑出来才算数。周期存在带来两个结果坏消息是如果你把置乱当作加密攻击者只要知道你用了多少轮再补上(T - n)轮正向变换就能还原等于没有加密好消息是周期可以用来自动还原很多实验代码里不写逆变换而是直接置乱到周期复位这个做法虽然能跑但我不推荐在正式实验里这么用。2.3 几何直观斜向拉伸再折叠回正方形矩阵A的特征值是(3 ± √5)/2约等于2.618和0.382一个是拉伸方向一个是压缩方向。连续变换的几何效果是整个正方形被沿斜方向拉成一条细长的平行四边形然后因为取模又被切回正方形内部。经典演示图里一只猫脸就是这样被拉成斜条纹、再折叠成看似杂乱的图案。所以一次迭代不是“把像素随机打乱”而是“树干按45度方向被拉长然后像叠被子一样折回方框”。迭代次数越多折叠次数越多像素位置就越显得随机。这也是为什么只做1轮置乱时图像还能看出明显的斜向条纹结构做到5轮以上视觉上基本就完全打散了。理解了这层几何你就能理解为什么要做多轮而不是一轮。置乱强度不是由单轮决定的而是由“剪切折叠”的累积次数决定的。继续往下写代码时你的脑子里要时刻有“剪切折叠”这个画面很多bug其实是你没想清楚这一轮之后像素到底去了哪个位置。3. MATLAB实现从双重循环到坐标映射表3.1 先写一个最直白的双重循环版本我建议你一开始别追求什么高深写法就把数学公式翻译成代码跑通了再说。以下是基础版灰度图和彩色图通吃因为它直接对第三维用冒号赋值function out arnold_forward(img) % Arnold置乱正变换 [N, M, C] size(img); if N ~ M error(Arnold变换要求图像宽高相等请先裁剪或补零); end out zeros(size(img), class(img)); for i 1:N for j 1:N % 公式里坐标从0开始MATLAB索引从1开始所以要-1和1 x0 i - 1; y0 j - 1; x1 mod(x0 y0, N); y1 mod(x0 2*y0, N); out(x1 1, y1 1, :) img(i, j, :); end end end这段代码的逻辑是遍历原图每个像素算它在新图中的位置然后搬过去。你用一张小图比如64×64跑一次再用imagesc或imshow对比一下应该能看到明显的斜向条纹被打乱的效果。这里有个特别重要的细节公式里的(x y)是对原坐标的一轮更新而不是你在循环里写一个累加变量。我第一次写的时候就犯过错误写成了x x y然后直接赋值给下一个循环结果整个图被无限拉伸跟预期完全不一样。正确的做法是每一轮迭代都基于上一轮的结果重新调用这个函数或者在单次函数内部用临时变量。3.2 用meshgrid做向量化加速双重循环在N比较大时很慢比如512×512的图两层循环要跑26万次。虽然MATLAB的JIT能加速一部分但做周期检测时N可能很大循环几百次就很痛苦了。向量化的思路很简单先把所有坐标网格生成出来一次性算出映射关系然后通过线性索引批量搬移像素。function out arnold_fast(img) [N, ~, C] size(img); % 生成0基坐标网格 [X, Y] meshgrid(0:N-1, 0:N-1); Xn mod(X Y, N); % 新x坐标 Yn mod(X 2*Y, N); % 新y坐标 % 新位置的线性索引行是Yn1列是Xn1 idx sub2ind([N, N], Yn(:) 1, Xn(:) 1); out zeros(size(img), class(img)); for c 1:C tmp img(:, :, c); plane zeros(N, N); plane(idx) tmp; % 原图第k个像素放到新位置idx(k) out(:, :, c) plane; end end这里要理解一个顺序问题plane(idx) tmp的含义是tmp里的第k个像素被放进plane的idx(k)位置。因为meshgrid生成的X和Y是按逐列展开的tmp的展开顺序天然对应了原图的线性顺序所以这样写是对的。你要是第一次用线性索引搬像素建议自己拿4×4小矩阵手推一遍很快就能搞明白。实测下来512×512的灰度图双重循环一次大约要几十毫秒到上百毫秒取决于机器向量化版本基本是毫秒级。周期检测、批量处理这些场景一定要用向量化版本。3.3 逆变换怎么写才正确还原就看逆矩阵。前面说过A的行列式为1所以A的逆是整数矩阵A⁻¹ [[2, -1], [-1, 1]]离散逆变换公式为x (2x - y) mod Ny (-x y) mod NMATLAB里mod对负数会返回正余数比如mod(-1, 5)结果是4这正好帮我们省了处理负数坐标的麻烦。逆变换代码function out arnold_inverse(img) [N, ~, C] size(img); out zeros(size(img), class(img)); for i 1:N for j 1:N x1 i - 1; y1 j - 1; x0 mod(2*x1 - y1, N); y0 mod(-x1 y1, N); out(x0 1, y0 1, :) img(i, j, :); end end end一个自查技巧连续做一次正变换再连续做一次逆变换结果应该和原图完全相同。我建议每次写完正变换就顺手把逆变换写了然后立即用isequal验证。如果不等绝大多数情况是坐标换算里少了某个-1或1而不是公式本身的问题。更通用的版本可以带参数a、b正向用(x ay, bx (a*b1)y)逆矩阵为[[ab1, -a], [-b, 1]]。这样密钥空间可以从“迭代次数”扩展成(a, b, n)三元组后面的安全讨论会用得上。4. 实验闭环灰度图加密、解密与周期复位4.1 完整示例加密-解密-周期复位三种路径灰度图是理解整个流程的最好载体。下面是一个完整的实验脚本包含三种还原路径% demo_arnold_gray.m img imread(cameraman.tif); % 256x256 灰度图 [N, M, ~] size(img); if N ~ M img img(1:min(N,M), 1:min(N,M)); % 简单裁剪成方形 end n 50; % 置乱轮数常用5~100 % 加密路径正向置乱n轮 enc img; for k 1:n enc arnold_fast(enc); end % 解密路径A逆变换n轮 decA enc; for k 1:n decA arnold_inverse(decA); end % 解密路径B利用周期性继续正向变换(T-n)轮 T arnold_period(img); % 周期检测见4.2 decB enc; for k 1:(T - n) decB arnold_fast(decB); end figure; subplot(2,2,1); imshow(img); title(原图); subplot(2,2,2); imshow(enc); title([置乱, num2str(n), 轮]); subplot(2,2,3); imshow(decA); title(逆变换还原); subplot(2,2,4); imshow(decB); title(周期复位还原); disp([Arnold周期 T , num2str(T)]);路径A是标准做法只要逆变换公式没问题多少轮都能还原。路径B利用了周期性本质上是把剩余的(T-n)轮正向变换执行完。路径B在数学上没问题但不建议作为正式解密手段原因后面聊安全性时再说。跑通这个脚本你就把置乱、还原、周期三条线都串起来了。4.2 用代码实测周期不要背384这种数很多人喜欢背“512×512的Lena图周期是384”但换个尺寸这个数就不对了。周期跟N的因式分解有关必须实测。方法很简单从原图开始反复做正向变换直到某一步的图像和原图完全相等那个步数就是周期。function T arnold_period(img) original img; T 0; while true img arnold_fast(img); T T 1; if isequal(img, original) break; end if T 10000 error(周期过长请检查输入是否为方形图像); end end end小尺寸图像的周期很快几十毫秒就出来了512×512的图可能要几分钟建议用向量化版本跑。跑周期检测时顺便可以发现一个有趣现象置乱到T/2附近时图像看起来最“乱”再继续置乱反而会逐渐显露出某种规律最后在T轮完全复原。这个规律也提醒你并不是轮数越多越好做过头反而会向原图回归。4.3 效果评测信息熵与相邻像素相关性置乱效果不能只靠眼睛看要有数字指标。两个最常用的指标是信息熵和相邻像素相关系数。信息熵衡量像素值分布的不确定性公式是H -Σ p(i)·log2(p(i))。MATLAB实现function H info_entropy(img) p imhist(img(:)); p p / sum(p); p(p 0) []; H -sum(p .* log2(p)); end但你要清楚Arnold变换不改变像素值集合所以信息熵在置乱前后几乎不变。如果某个报告里写“置乱后熵值从7.1提升到7.9”那一定不是纯Arnold做的而是加了扩散或者灰度映射。这个指标在这组实验里更多是“验证没被破坏”而不是“证明加密有效”。相邻像素相关性衡量的是图像有没有结构残留。自然图像相邻像素相关性极高通常在0.9以上置乱后应该剧烈下降到接近0。实现function r corr_adjacent(img, direction) img double(img); if direction h a img(:, 1:end-1); b img(:, 2:end); else a img(1:end-1, :); b img(2:end, :); end a a(:); b b(:); r sum((a - mean(a)).*(b - mean(b))) / ... sqrt(sum((a - mean(a)).^2) * sum((b - mean(b)).^2)); end我对cameraman.tif实测的经验值是原图水平相邻像素相关系数约0.95垂直约0.94做20轮置乱后两者都掉到0.01上下。你跑出来的具体数字不一定一样但因为置乱彻底破坏了相邻关系趋势一定是“从接近1跌到接近0”。建议在实验报告里给出置乱前、置乱1轮、置乱5轮、置乱20轮的相关性变化曲线比单独贴一张乱图有说服力得多。5. 彩色图像的三通道置乱策略与边界处理5.1 三通道分别置乱最稳妥的做法彩色图像本质上就是三个灰度平面叠在一起。最简单也最稳的做法是RGB三个通道分别执行相同的Arnold置乱轮数一致。前面给的arnold_forward本来就支持彩色图因为它用out(x11, y11, :) img(i, j, :)搬运整条色彩向量。% 彩色图加密三通道同一套变换 enc_color arnold_fast(img_color); % 直接一次到位 % 还原也是一样 dec_color arnold_inverse(enc_color);这种做法有三个通道各自使用相同的坐标映射置乱后三个通道的“图案乱法”完全一致合并成RGB后整体视觉上就是同一副被打乱的彩色图不会出现彩虹状色偏。原因在于每个像素的R、G、B三值作为一个整体被搬走三者没有拆散所以通道间关系被完整保留。不过这种策略有个性能问题编码后单个通道内的纹理被拆散了但通道与通道之间的相关性没变。对彩色图做相关性分析时除了水平、垂直还应该算R-G、R-B、G-B通道间相关性。纯Arnold置乱后通道间相关系数依然很高红苹果还是红苹果只是位置乱了。5.2 更彻底的通道融合置乱思路如果想让三通道之间的相关性也被打散思路是把三通道当成一个整体参与置乱。常见做法有两种其一把RGB三个通道转成YCbCr或HSV后再对亮度分量做置乱、对色度分量做不同程度的置乱其二把三个通道按像素位置重新组合比如把R、G、B分量分拆成三个维度做一个扩展的3D Arnold变换。3D Arnold的公式比2D复杂不少而且周期特性也更难分析课程实验一般用不上。如果你的题目只要求“彩色图像Arnold置乱”三通道分别置乱完全够用。我自己的习惯是彩色图实验里特别标注“本实验对RGB三通道独立执行相同参数的Arnold置乱通道间相关性不在置乱层处理范围内。”这句话能帮你避免被老师追问“为什么红色区域还是能看出来”的尴尬。5.3 非方形图像的补零处理Arnold变换要求方形图像但真实图片极少是方的。两种常见处理方式裁剪和补零。裁剪会损失边缘内容对图像内容本身不友好补零则是把图像放到一个N×N的黑色画布上其中N取max(h, w)置乱后再裁回原尺寸。[h, w, ~] size(img); N max(h, w); padded zeros(N, N, size(img, 3), class(img)); padded(1:h, 1:w, :) img; % 对padded做置乱/逆置乱 enc_padded arnold_fast(padded); dec_padded arnold_inverse(enc_padded); % 裁回原尺寸 dec dec_padded(1:h, 1:w, :);补零的操作在置乱后会在图像边缘产生一块明显的黑色“补丁”混进图像内容这不是bug而是预期效果。解密裁回后补丁消失不影响原图数据。如果你希望补丁不要那么显眼也可以把补丁区域填充为随机噪声但那样需要把填充区域坐标记录在密钥里一般实验没必要搞这么复杂。6. 我在实验里踩过的坑从结果异常到定位根因6.1 uint8溢出问题2551不等于256MATLAB里uint8是一个饱和整数类型255 1的结果不是256而是255。你如果直接对uint8图像矩阵做类似A 1的操作一旦有像素处于255加法结果就会饱和置乱结果看起来就像“某些区域颜色不对”。这类坑在向量化代码里最容易出现因为循环版本里我用的是循环变量i、j做计算mod的输入是double不会触发饱和。但如果你写成Xn mod(X Y, N)且X、Y是从uint8矩阵操作来的就要小心类型问题。最稳妥的办法坐标计算前显式转doubleX double(X); Y double(Y);像素赋值到输出矩阵时MATLAB会自动转回uint8不存在截断问题因为mod结果一定落在0到N-1。这个坑很隐蔽我把一样代码跑出过整张图泛白的结果查了半天才发现是uint8饱和。6.2 坐标原点混淆公式是0基MATLAB是1基这是Arnold实验里出现频率最高的错误。公式里的坐标(x, y)是0到N-1MATLAB的数组索引是1到N。第一次写代码的人经常写成x1 mod(i j, N); % 错i、j是1基这里的起点差了一个结果就是整张图的置乱方向偏了一个像素图像看起来似乎“变乱了”但仔细看会发现只是斜向平移了一格。这种错误尤其坑人因为视觉上效果还挺像那么回事不仔细检查根本发现不了。自查办法只有一个用N比较小的矩阵比如4×4手算几个像素的理论位置再和代码输出对比。我建议调试时打印出变换前后的坐标映射表N 4; [X, Y] meshgrid(0:N-1, 0:N-1); map [X(:), Y(:), mod(X(:)Y(:), N), mod(X(:)2*Y(:), N)]; disp(map);对着这张映射表看几个点你的坐标换算对不对一目了然。6.3 循环次数与“看起来只移了一格”的错觉还有一种情况是代码完全正确但只做了1轮置乱图像只呈现轻微的斜向条纹看起来像个“失败的加密”。这不是bug是轮数太少了。Arnold单轮变换只做一次剪切折叠结构信息还在。课程作业里常见的问题是“置乱5轮够不够”我的经验是小图5轮就有明显的打散效果256×256以上的图建议10到50轮但也不要无脑往上堆因为周期T的存在意味着超过一定轮数后图像会重新向原图靠近。轮数选择最好结合周期性分析做T/3到T/2轮通常视觉上和统计指标上都能达到不错的置乱效果。你可以在实验脚本里做一个循环轮数扫描把每一轮的相关性系数存下来画曲线曲线到达最低点附近对应的轮数就是这组参数下的较优选择。6.4 周期复位不是正经解密密钥空间也偏小我见过不少代码用“正向置乱T-n次”来解密也就是前面说的周期复位路径。这能跑通但会给实验报告埋下一个隐患它暗示了解密依赖周期T而不是逆变换周期一旦被对方算出加密就形同虚设。实际系统里应该用逆变换解密周期只作为理论性质讨论。更关键的坑在密钥空间。标准Arnold只有迭代次数n一个密钥分量即使扩展成(a, b, n)密钥空间也只有大约N²乘以轮数范围量级。对256×256的图像这个量级约等于几十万到几百万暴力搜索毫秒级就能完成。所以拿Arnold单独当加密算法在安全层面是完全不合格的。这也是为什么我始终强调这个实验是“数字图像处理课程里理解混淆概念的实验”不是“可以交付的加密系统”。你在报告的结论部分一定要把这个边界写清楚别给自己留下被质疑的把柄。7. 从课程实验到可用加密再加一层像素扩散7.1 为什么只靠置乱扛不住直方图/选择明文分析前面已经反复提到Arnold不改变像素值的统计分布。攻击者拿到置乱图像后第一眼虽然看不出内容但统计直方图、灰度共生矩阵这类特征仍然保留了大量原始信息。直方图匹配可以套出大致亮度分布选择明文攻击可以输入全黑、全白、单点亮点等特殊图像观察置乱后的轨迹反推坐标映射关系。所以一个相对完整的图像加密流程常规思路是“先混淆位置再扩散像素值”。混淆让结构性信息丢失扩散让统计性信息也丢失。两者缺一不可。Arnold在这个流程里作为混淆层完全合理但还需要另一个层来改变像素值。7.2 用Logistic混沌序列补一个扩散层的思路扩散层最简单的做法是用混沌序列对像素值做按位异或。经典的Logistic映射递推式是x(n1) μ·x(n)·(1 - x(n))密钥取初值x(0)和参数μ通常取3.9到4之间迭代出和图像像素数一样长的混沌序列量化到[0, 255]后与原像素异或。这样每个像素的值都被修改直方图被彻底均匀化信息熵显著提高。这只是最基础的扩散。真正的强扩散要做到“一个像素的改变能影响到很多像素”这意味着密文反馈。常见做法是把加密后的前一个像素值混入当前像素的异或过程类似CBC模式。这样一来修改任意一个明像素后面所有密文都会跟着变。课程实验做到异或扩散这层已经足够展示“置乱扩散”的完整思想了。7.3 一个完整的“置乱扩散”流程建议如果你要交大作业我建议整体流程设计成密钥层用Logistic映射的初值x(0)和参数μ生成混沌序列。扩散层混沌序列与前一个密文像素值反馈逐像素异或或模加。置乱层对扩散后的图像做5到50轮Arnold置乱密钥为(a, b, n)。解码顺序先Arnold逆置乱再做逆扩散。这样设计的好处是两端都有密钥支撑且混淆和扩散形成互补。你自己写实验代码时可以先跑通纯Arnold的部分再叠加扩散层验证信息熵确实提升了。代码层次清楚报告也容易写置乱层管“位置”扩散层管“数值”两者结合才叫图像加密。最后说一点我自己的体会。做这个实验最容易被带偏的地方就是把置乱当成加密的全部。我反反复复跑了几十次Arnold之后才真正想明白它在整个加密系统里只是“打散像素位置”这一环真正让密文强度上去的是扩散与迭代设计。如果你也在写这类课程作业建议在报告里把“混淆”和“扩散”两个概念分清楚再用信息熵、像素相关性和直方图三个指标分别验证每层的贡献。这样即便代码是我上面给出的基础版本你的实验结论也能站得住脚。