ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于分块矩阵乘法与模运算的彩色图像加密Matlab实现

基于分块矩阵乘法与模运算的彩色图像加密Matlab实现 做图像处理课程设计的时候很多人会碰到这样一个题目基于分块矩阵乘法 模运算的彩色图像加密函数附Matlab代码。我第一次拿到这个题目时第一反应是直接用矩阵乘法就能加密那解密怎么办把这个问题想透以后反而觉得这是一个非常适合入门图像加密的方案——代码量不大效果直观而且把“扩散”“混淆”这两个密码学核心概念都体现得清清楚楚。下面把我对这套方法的理解和一套能直接跑通的Matlab实现完整写出来适合正在做图像加密大作业、或者想了解矩阵在图像安全领域怎么用的朋友参考。1. 为什么这个方案要把分块矩阵乘法和模运算组合在一起1.1 图像加密要解决的两个基本问题数字图像本质上就是一个二维矩阵彩色图像则是三个二维矩阵叠在一起每个像素取值范围通常是0到255。图像加密的目标是把这张图变得面目全非让攻击者看不出任何原始内容。围绕这个目标业界一般拆成两个基本操作置乱Permutation打乱像素的位置让大象变成一团噪点扩散Diffusion改变像素的值让每个密文像素都受到多个明文像素的影响从而掩盖统计规律。很多新手教程只做像素重排比如把图像按随机序列重新摆放。这种方案的问题很明显直方图还是原图那个直方图峰值和分布形状一点没变攻击者做一次统计就能猜出内容的亮度范围、色调倾向甚至能通过对比直方图定位到是哪类图像。所以真正有效的加密必须同时改变位置和像素值缺一不可。分块矩阵乘法恰好一步就能同时完成这两件事——这是这个题目最巧妙的地方。1.2 分块矩阵乘法如何同时完成置乱扩散假设密钥矩阵A是 n×n图像块B也是 n×n那么加密过程写成C (A × B) mod 256展开来看C(i, j) (Σ A(i, k) × B(k, j)) mod 256也就是说密文块里的每一个像素都是明文块中某一整列像素的加权和。想象一下原来一个像素的值是200经过矩阵乘法后它被打散成若干份混进了一整列像素的结果里与此同时密文像素的值已经不是原图像素值本身了。这个过程同时改变了像素的位置关系因为每个密文像素同时依赖多个明文像素和像素的数值所以它天然就是一个同时包含置乱与扩散的操作。那为什么一定要分块而不是直接用一个大矩阵和整幅图像相乘主要有三个原因计算复杂度矩阵乘法的复杂度随矩阵尺寸按 O(n³) 增长。对一幅 512×512 的灰度图直接做一次512阶矩阵乘法大约需要1.3亿次乘加运算如果切成8×8的小块每块只需要512次运算整幅图共有4096个块加起来约210万次运算速度快了两个数量级。密钥规模整幅图做矩阵乘法需要512×512的密钥矩阵也就是26万个密钥元素管理和存储都是负担。切成8×8后密钥矩阵只有64个元素安全性和效率平衡得很好。扩散范围主流分组密码比如AES的扩散范围本来就是一个有限的分组而不是整条消息。8×8块的扩散范围已经能打乱局部结构和纹理块与块之间再用相同密钥进行“独立处理”相当于并行加密效率很高。我实际测试过不同的分块尺寸这里给出一组参考对比分块大小每个块乘法次数密钥元素数扩散范围性能感受4×464164×4邻域极快8×8512648×8邻域很快16×16409625616×16邻域较慢整图512×512约1.3亿262144全图非常慢综合来看8×8是我个人最推荐的分块尺寸论文和课程设计里出现的频率也最高。1.3 模运算在这里扮演的双重角色矩阵乘法做完后结果里的数值可能远超255甚至可能出现负数——因为图像块里都是0到255的像素但密钥矩阵如果含有较大的数加权和很容易溢出。密文图像是没法存放大干255的数值的所以必须对256取模把结果拉回0到255的有效区间。但模运算还有一个更本质的作用它让解密变成了可能。在线性代数中一个矩阵是否可逆取决于它的行列式是否为0。但在模256的意义下可逆条件要严格得多矩阵必须在“模256”这个代数结构里存在逆矩阵也就是存在一个矩阵 A_inv使得mod(A_inv × A, 256) I这个条件和普通的矩阵可逆并不是一回事。要想让这个条件成立行列式 det(A) 必须与256互素。这个“互素”的要求是整个方案里最容易踩坑的地方我把它单独放在第2节详细讲。2. 加密与解密的数学基础模逆矩阵的求法2.1 加密和解密的公式表达设明文块为X密钥矩阵为A密文块为C加密过程是C mod(A × X, 256)解密时不能直接用 inv(A) 乘上去因为普通线性代数里的逆矩阵没有“取模后仍然等于单位阵”的保证。我们需要的是A在模256下的逆矩阵 A_inv满足mod(A_inv × A, 256) I然后用它去恢复明文X mod(A_inv × C, 256)你的Matlab代码里如果只写X inv(A) * C大概率会得到一堆小数和负值转回uint8之后彻底乱掉。正确的做法是先把A在模256下的逆矩阵求出来再做乘法取模。2.2 模逆矩阵的具体求法求模逆矩阵有一个非常成熟的套路用伴随矩阵。回忆线代知识A⁻¹ (1 / det(A)) × adj(A)其中 adj(A) 是A的伴随矩阵。在模算术里“除以det(A)”要换成“乘以det(A)的模逆元”。整个流程分四步计算 d round(det(A))用扩展欧几里得算法求 x使得 mod(d × x, 256) 1也就是d模256的乘法逆元计算伴随矩阵 adj round(d × inv(A))这里的 inv(A) 是普通实数域逆矩阵乘回d之后理论上得到整数伴随矩阵用 round 消除浮点误差最终 A_inv mod(x × adj, 256)。对应的Matlab核心代码是这样function A_inv mod_inverse_matrix(A, m) % 计算矩阵A在模m条件下的逆矩阵 % 要求gcd(det(A), m) 1 d round(det(A)); [g, x, ~] gcd(d, m); if g ~ 1 error(矩阵行列式与模数不互质无法求模逆矩阵); end % 伴随矩阵乘回行列式后得到整数矩阵 adj round(d * inv(A)); % 模逆矩阵 A_inv mod(x * adj, m); end这里有个容易忽略的细节round(d * inv(A))这一步理论上 adj 的每个元素都应该是精确整数但由于Matlab浮点运算有舍入误差计算出来的结果可能是 1.9999 或者 257.0001 这种值。如果不取整后面mod出来的结果就会带着误差解密后的图像像素可能就差那么一两个灰度级。加一个round能彻底解决这个问题。验证方法也很简单key.I mod(key.A * key.A_inv, 256);如果key.I是8×8的单位阵说明你的模逆矩阵求对了。这一步我建议写进代码里做个断言省得解密失败时回头排查半天。2.3 密钥矩阵必须满足的条件行列式与256互素上一节提到的“行列式与256互素”到了具体数值上其实是个很干净的结论256 2⁸它的因子只有2这一个质因子。所以det(A)与256互素就等价于 det(A) 必须为奇数。为什么行列式是偶数就一定失败核心原因是模256这个“环”里偶数没有乘法逆元。我们知道任何矩阵求逆都躲不开克莱默法则最终都要除以det(A)。在模256的世界里“除以2”等价于“乘以2的逆元”可是2在模256下并不存在逆元因为不存在一个整数k能让 2k ≡ 1 (mod 256)。所以行列式为偶数时A在模256下必然不可逆加密过程不可逆解密必然失败。举个例子假设密钥矩阵A [1 0; 0 2]这个矩阵行列式为2加密时第二行的像素全部翻倍再取模。像素值100变成200还能还原但像素值200变成400模256后变成144此时你没法判断加密前到底是72还是200信息已经永久丢失了。所以生成密钥的时候安全做法是这样function key generate_key(n, m) % 随机生成n×n密钥矩阵要求行列式与m互素 while true A randi([0, m-1], n, n); if gcd(round(det(A)), m) 1 break; end end key.A A; key.A_inv mod_inverse_matrix(A, m); key.size n; key.mod m; end这个while true循环平均跑几次就能跳出因为随机整数矩阵行列式为奇数的概率并不低。如果你实在不放心也可以在生成后打印一下gcd(round(det(A)), 256)看到1就可以放心用。3. Matlab实现一版能直接跑通的完整代码3.1 整体流程设计我把整个加密解密流程拆成六个步骤读入彩色图像转成double类型分离R、G、B三个通道对每个通道分别做分块处理不足8的倍数的边缘补零每个8×8块左乘密钥矩阵A对256取模拼回完整通道合并成密文图像解密时用模逆矩阵重复第4步再裁剪掉补零区域。为什么要分别处理三个通道因为彩色图像每个通道都是独立的二维矩阵合在一起会破坏通道之间的独立性而且解密后还要重新拆分反而麻烦。分开处理逻辑清晰加密效果也没差别。3.2 加密函数与解密函数的核心实现下面是完整的加密函数function img_enc block_encrypt(img, key) % 彩色图像分块矩阵乘法加密 % img: 任意尺寸的彩色图像(uint8) % key: 由generate_key生成的密钥结构体 img double(img); [M, N, C] size(img); bs key.size; m key.mod; A key.A; Mp ceil(M / bs) * bs; Np ceil(N / bs) * bs; img_enc zeros(Mp, Np, C); for c 1:C channel img(:,:,c); padded zeros(Mp, Np); padded(1:M, 1:N) channel; for i 1:bs:Mp for j 1:bs:Np block padded(i:ibs-1, j:jbs-1); enc_block mod(A * block, m); padded(i:ibs-1, j:jbs-1) enc_block; end end img_enc(1:M, 1:N, c) padded(1:M, 1:N); end img_enc uint8(img_enc); end解密函数几乎一模一样只有两个变化密钥换成了A_inv其余逻辑完全对称function img_dec block_decrypt(img_enc, key) % 彩色图像分块矩阵乘法解密 img_enc double(img_enc); [M, N, C] size(img_enc); bs key.size; m key.mod; A_inv key.A_inv; Mp ceil(M / bs) * bs; Np ceil(N / bs) * bs; img_dec zeros(Mp, Np, C); for c 1:C channel img_enc(:,:,c); padded zeros(Mp, Np); padded(1:M, 1:N) channel; for i 1:bs:Mp for j 1:bs:Np block padded(i:ibs-1, j:jbs-1); dec_block mod(A_inv * block, m); padded(i:ibs-1, j:jbs-1) dec_block; end end img_dec(1:M, 1:N, c) padded(1:M, 1:N); end img_dec uint8(img_dec); end这里有个挺容易想错的细节在分块循环中padded矩阵正在被逐块更新下一个循环读取的块会读到已经被加密过的数据吗答案是不会——因为每个块的位置完全不重叠当前块只会写回自己的区域不影响其他区域内像素的读取。循环按行优先顺序遍历每个块的输入都是原始像素或者边缘补零区域不会发生交叉污染。3.3 彩色图像三通道处理与分块边界对于尺寸刚好能被8整除的图像padding逻辑完全不会触发。但实际测试中我遇到过很多非8倍数尺寸的图像比如某张测试图是500×400这种情况下500 8×62 4所以高度补到504400 能被8整除宽度不用补加密后的padding区域在解密时会被填回0裁剪回500×400后不影响内部像素。如果你不希望边缘出现补零的痕迹也可以改成边缘复制padding也就是用最边缘的像素值填充补零区域。视觉上会好看一些但解密后需要把填充区域裁掉和补零方案差别不大。我自己的代码习惯是补零因为逻辑最简单。三通道的处理方式是把[M, N, C]循环拆开每个通道走一遍同样的分块逻辑。有人会把三个通道直接拼接成一个超大的二维矩阵再一次性加密这样做能少写一层循环但解密之后还得按比例裁剪而且一旦某个通道的尺寸不一致就会出大问题。多写一个for c 1:C循环的成本几乎可以忽略代码可读性却好得多。3.4 主程序与运行效果写一个demo主程序把整个流程串起来function demo_block_cipher() % 分块矩阵乘法模运算的图像加密演示 img imread(lena.png); % 改成你自己的图像路径 if size(img, 3) ~ 3 error(演示程序只支持RGB彩色图像); end % 生成8×8密钥 key generate_key(8, 256); % 加密解密 img_enc block_encrypt(img, key); img_dec block_decrypt(img_enc, key); % 验证解密误差 err sum(abs(double(img(:)) - double(img_dec(:)))); fprintf(解密误差像素总和: %d\n, err); % 显示结果 figure; subplot(1,3,1); imshow(img); title(原始图像); subplot(1,3,2); imshow(img_enc); title(加密图像); subplot(1,3,3); imshow(img_dec); title(解密图像); end如果密钥生成正确、模逆矩阵计算无误err应该精确等于0解密图像和原始图像逐像素一致。这一步跑通之后你的基础加密流程就完整了。4. 加密效果的客观评估直方图、相关系数与信息熵4.1 从肉眼到数值解密无失真加密后的图像肉眼看上去是一团均匀的“雪花噪点”完全看不出原图轮廓。这是矩阵乘法把像素值打散后的典型效果。但肉眼判断不够专业课程设计和论文评审通常要求给出量化指标。最重要的指标有三个直方图分布、相邻像素相关系数、信息熵。下面逐个来说。4.2 直方图从有规律到近似均匀原始图像的直方图通常有明显的峰值和起伏反映了图像内容的亮度分布。加密后由于每个密文像素都是多个明文像素的加权和再取模直方图会被摊平趋近于均匀分布。用Matlab统计直方图的代码% 分别统计三个通道的直方图 figure; for c 1:3 subplot(2,3,c); histogram(img(:,:,c), 0:255); title([原始图像R通道 num2str(c)]); subplot(2,3,c3); histogram(img_enc(:,:,c), 0:255); title([加密图像R通道 num2str(c)]); end我自己测试的一张测试图上原图红色通道直方图集中在80到180之间有明显的山峰加密后的直方图在0到255范围内分布得相对平整各灰度级出现频率基本落在同一个量级。这意味着统计攻击很难从密文推断出原始图像的亮度分布。4.3 相关系数与信息熵两个最常用的量化指标相邻像素相关系数反映了图像相邻像素之间的相关性。自然图像里相邻像素的值高度相关相关系数经常在0.9以上好的加密算法应该把这个系数打到接近0。计算横向相邻像素相关系数的公式为r Σ[(xᵢ - x̄)(yᵢ - ȳ)] / √(Σ(xᵢ - x̄)² · Σ(yᵢ - ȳ)²)代码实现function r corr_coef(channel) % 计算相邻像素横向相关系数 [H, W] size(channel); x double(channel(:, 1:W-1)); y double(channel(:, 2:W)); x x(:); y y(:); x_mean mean(x); y_mean mean(y); numerator sum((x - x_mean) .* (y - y_mean)); denominator sqrt(sum((x - x_mean).^2) * sum((y - y_mean).^2)); r numerator / denominator; end对原始图像和加密图像的三个通道分别调用这个函数你会看到原始图像相关性往往在0.9以上加密后通常能降到0.1以下甚至接近0。信息熵衡量图像灰度值的随机程度。8位灰度图的最大熵是8原始图像的信息熵通常在6到7.5之间加密后的密文图像信息熵应该逼近7.99。function H image_entropy(im) % 计算单通道图像的信息熵 p imhist(im(:)); p p / sum(p); p(p 0) []; H -sum(p .* log2(p)); end表格形式整理三类指标在测试图中的典型表现指标原始图像加密图像横向相邻像素相关系数0.92~0.980.01~0.08信息熵每通道6.5~7.57.95~7.99直方图分布有明显峰值接近均匀这三个指标是图像加密方向论文里最常出现的验证手段。你的实验报告里只要放上加密前后的对比图、三个通道的直方图再加一张表格列相关系数和熵的变化这一章基本就达标了。5. 实测中踩过的坑与进阶改进方向5.1 最容易翻车的三个细节我在调试这个加密函数时踩过几个坑这里直接列出来能帮你省下大把时间。第一个坑直接对uint8型图片做乘法运算。Matlab读入图像后的默认类型是uint8如果你不转double直接执行A * img结果超过255时会被截断而不是取模加密出来全是黑块或者严重变形的色块。解决方式很简单一进入函数就img double(img)计算完再uint8(img)转回显示类型。第二个坑混淆mod和rem。矩阵乘法中间结果可能出现负数这时候mod(-3, 256)返回253而rem(-3, 256)返回-3。解密时如果用错函数负数值会被uint8强制变成0或某个错误值图像严重损坏。加密解密全程都应该用mod不要用rem。第三个坑行列式是浮点数直接判断奇偶会出错。det(A)的返回值经常是2.0000、15.0000这样带小数尾巴的值直接mod(det(A), 2)判断奇偶可能因为浮点误差得出错误结果。一定要先round(det(A))再判断。这个细节藏得很深我第一次就是栽在这里解密出来的图像只有前半部分正常后半部分全花。5.2 更优雅的替代方案换成模257素数域前面说了模256方案要求行列式为奇数这个限制虽然不麻烦但确实是个约束。如果你希望密钥矩阵几乎可以随便生成更优雅的做法是把模数从256换成257。原因在于257是一个素数模257的世界和普通实数域结构更接近只要 det(A) 不等于0A就一定可逆。像素值本来是0到255直接映射到0到256的257域中会有一个“0和256重叠”的问题所以通常采用偏移1映射加密前每个像素加1变成1到256参与模257运算解密得到的结果再减1恢复成0到255的像素值。核心改动就几行% 加密前 X X 1; C mod(A * X, 257); C C - 1; % 解密时 C C 1; X mod(A_inv * X, 257); X X - 1;处理后的密文图像依旧是0到255的像素值可以直接用uint8显示。而密钥生成从“要求行列式为奇数”变成了“要求行列式不为0”——在模257下这会自动满足概率超过99.6%。对比项模256方案模257方案模数特征合数(2⁸)素数密钥可逆条件行列式为奇数行列式不为0额外映射不需要加密前1、解密后-1对初学者的理解成本较低略高如果目标是做原理验证模256方案更符合题目的字面意思也更容易讲清楚“为什么取模必须选对模数”。如果项目后续要做真实应用我建议换成模257方案鲁棒性好很多。5.3 安全性还能怎么加强固定密钥矩阵A做全图所有块加密这个方案本身的安全性上限不高。最明显的弱点在于攻击者只要拿到一对明密文块解线性方程组就能反推出A。这是教科书里典型的已知明文攻击。如果你想让方案更接近实用可以考虑下面几个方向先置乱再扩散加密前先用Arnold变换把像素位置彻底打乱再分块做矩阵乘法置乱和扩散叠加起来抗分析能力会强不少多轮迭代把“分块乘法取模”作为一个轮函数重复运行3到5轮每轮之间把块与块之间的位置做一个循环移位扩散范围就能覆盖到全图块间密钥动态化用混沌序列或者伪随机数发生器根据块的位置生成不同的子密钥矩阵而不是全图共用一个A。密钥空间变大已知明文攻击难度也会指数级上升。这些扩展方向每一个都能单独写成一篇完整文章而且都是课程设计里不错的加分项。我个人的实际体会是做这个题目别急着抄代码先把“A到底要满足什么条件”想清楚后面解密就顺理成章。花二十分钟把模逆矩阵的推导过程自己走一遍比调试一整天乱码划算得多。如果你要写课程设计报告把第1.3节和第2.3节的内容扩写进原理部分就是非常完整且能体现思考深度的章节。这套代码的加密效率、可读性和扩展性在同类方案里都算相当好用拿去做项目打底或者课设核心都撑得住。
返回列表