简介:这是一份基于小波变换的图像融合MATLAB仿真资源,配有可交互的图形界面和完整的代码操作视频,主要面向本硕博阶段需要学习图像融合算法、完成课程设计或进行科研验证的读者。资源在MATLAB 2021a及更高版本中运行,通过Runme_.m总程序启动图形界面,操作视频完整演示了从读取图像、执行小波融合到查看结果的全过程,能够帮助使用者快速理解程序结构与运行逻辑。包内共31个文件,包括一个主程序m文件、一个图形界面fig文件、一个avi操作录屏,以及28张用于算法验证的tif测试图像,测试图像同时涵盖灰度图与彩色图,压缩包整体约7.75MB,文件结构简明清晰,便于对照代码逐一理解小波分解、系数融合和图像重构等核心步骤。已有953人学习下载,该资源既适合作为本科毕业设计与研究生课程的参考资料,也为后续改进融合规则、扩展自己的实验数据提供了可直接修改的运行框架。
1. 从“两幅图合成一张”说起:小波融合仿真到底在做什么
图像融合通常不是“拿两张图直接加权平均”就能交差的。红外图里目标亮但背景细节少,可见光图里纹理清楚但目标不突出,如果只在像素层做加法,得到的往往是一张对比度下降、边缘发糊的图。小波变换的做法是把图像拆成低频近似和高频细节,再在两套系数上分别执行融合规则,最后逆变换回空间域。这也是标题里“小波变换”和“图像融合”连在一起的原因:低频保留整体亮度结构,高频负责纹理边缘,两个部分各用各的策略。
这套仿真在 MATLAB 里落地并不复杂:核心是一个三层左右的小波分解、两个融合规则、一次逆变换,加上一个能调参数的 GUI 界面。适合正在做数字图像处理课设、毕业设计,或者需要快速出一版融合 demo 的工程师。关键在于把“分解到第几层”“高频系数怎么选”“GUI 回调怎么传参数”三件事理顺。下面按从原理到接口的顺序,把能直接复现的代码和参数一次讲清。
2. 小波分解与融合规则:低频取平均、高频取最大,以及层数怎么定
2.1 为什么拆成低频和高频再融合
图像经过小波变换后,一层分解会得到四个子带:LL(低频近似)、LH(水平细节)、HL(垂直细节)、HH(对角细节)。下一层继续只对 LL 做分解,形成金字塔式的系数结构。低频子带本质上是原图的模糊版本,包含大部分能量和亮度信息;三个高频子带分别记录不同方向的边缘与纹理突变。
如果直接在原图上融合,像素级加权的病态问题是:两个输入里亮度差异大的区域,平均后出现光晕;一个清晰一个模糊的区域,平均后清晰度被拖低。把系数分开处理以后,低频和高频可以用不同规则,比如低频拉普拉斯金字塔式加权,高频用绝对值取大。这也是多数小波融合方案比空域加权更稳的原因:融合发生在“信息的频段”而不是“像素本身”。
2.2 一个最小可用的融合函数
下面这个函数用 MATLAB 自带的小波工具函数实现两层分解融合,可以直接复制为waveFuse.m,是后续 GUI 回调里要调用的核心计算部分。
function F = waveFuse(img1, img2, wname, level, rule) % 基于小波变换的图像融合 % img1/img2 : 灰度图,double 类型,尺寸一致 % wname : 小波基,如 'db2' / 'sym4' % level : 分解层数,如 2 或 3 % rule : 高频融合规则,'max' 绝对值取大 | 'avg' 平均 % 对两幅图分别做 level 层小波分解 [C1, S1] = wavedec2(img1, level, wname); [C2, S2] = wavedec2(img2, level, wname); % S1 和 S2 应当一致,因为输入尺寸相同 % 低频部分直接用两幅图的 LL 全平均 % LL 在低频系数向量最前面,长度是 S1(1,1)*S1(1,2) ll_len = S1(1,1) * S1(1,2); C_fuse = (C1(1:ll_len) + C2(1:ll_len)) / 2; % 低频系数是不需要看方向的,平均能避免亮度漂移 % 高频部分:按每条系数绝对值比较 % 从 ll_len+1 开始到末尾都是三个方向的高频细节 idx0 = ll_len + 1; for k = idx0 : numel(C1) if strcmp(rule, 'max') if abs(C1(k)) >= abs(C2(k)) C_fuse(k) = C1(k); else C_fuse(k) = C2(k); end else C_fuse(k) = (C1(k) + C2(k)) / 2; end end % 重构回空间域 F = waverec2(C_fuse, S1, wname); F = mat2gray(F); % 归一化到 [0,1],方便 imshow / imwrite end参数说明:
wname选db2或sym4在小尺寸图像上差异不明显,db2计算量更小;如果图里有细密纹理,sym4的对称性更好,边缘伪影略轻。课设场景固定用一种即可。level推荐 2 或 3。层数越高,滤波器的下采样次数越多,图像越小的子带越可能只剩几个像素,融合就失去了局部性。rule的高频用max是融合里最常用的策略:哪个系数绝对值大,说明该处边缘强度更高,直接保留它。若想平滑过渡可以选avg,但清晰度会下降。这里用strcmp每次比较判断会有性能损耗,单张图融合无所谓;如果做视频帧融合,建议把分支提到 for 循环外面。
2.3 层数、小波基对结果的影响
| 分解层数 | 低频占比 | 高频细节保留 | 适用场景 | 重构耗时(512×512) |
|---|---|---|---|---|
| 1 | 小,融合倾向像素级 | 最多,但边缘易出现“断裂感” | 纹理差异大的医学图像 | 最短 |
| 2 | 适中 | 边缘较完整 | 大多数红外+可见光融合 | 中等 |
| 3 | 大,整体亮度稳定 | 细节有损 | 需要压噪声、提目标亮度的场景 | 略长 |
| 4 及以上 | 过大 | 明显丢失细纹理 | 大尺寸遥感影像,不建议普通图像使用 | 明显变慢 |
选层数时还要考虑一个容易被忽略的点:wavedec2要求图像长宽至少能被2^level整除。如果 512×512 做 3 层没问题,但 300×200 做 3 层就会报错。常见做法是在预处理阶段用imresize把尺寸归一到 256×256 或 512×512,或者在分解前手动裁剪。GUI 里如果开放了层数选择,要同步检查输入尺寸。
3. 能一次跑通的 matlab 仿真:从预处理到重构的完整工程
3.1 读图、灰度对齐、尺寸检查的预处理
实际工程里两个输入图往往来自不同传感器,尺寸、通道数、亮度范围都不一样。比如红外图经常是单通道 uint8,可见光图是三通道 uint8。如果直接送进wavedec2,会因为维度不一致报错。下面这段主脚本展示标准处理链路。
%% 主脚本:加载两幅图,调用融合函数,展示结果 clear; clc; close all; % 1. 读入两张图 I1 = imread('infrared.png'); % 红外图 I2 = imread('visible.jpg'); % 可见光图 % 2. 统一转为灰度 double,并归一化 if size(I1, 3) == 3 I1 = rgb2gray(I1); end if size(I2, 3) == 3 I2 = rgb2gray(I2); end I1 = im2double(I1); I2 = im2double(I2); % 3. 尺寸不一致时,以第一张为准做裁剪,而不是直接拉伸 % 直接 imresize 会改变空间分辨率,融合出来的边缘有“橡皮感” [h, w] = size(I1); I2 = I2(1:min(h,size(I2,1)), 1:min(w,size(I2,2))); I1 = I1(1:size(I2,1), 1:size(I2,2)); % 4. 调用融合函数(这里用 db2、2 层、高频绝对值取大) F = waveFuse(I1, I2, 'db2', 2, 'max'); % 5. 显示原图与融合结果 figure('Name', '小波图像融合结果'); subplot(1, 3, 1); imshow(I1); title('Input 1'); subplot(1, 3, 2); imshow(I2); title('Input 2'); subplot(1, 3, 3); imshow(F); title('Fused'); % 6. 保存结果,imwrite 需要 uint8 imwrite(uint8(F * 255), 'fused_result.png');这段脚本的逻辑说明:
- 第二步的
im2double把 uint8 转到 [0,1] 浮点,避免后面abs、除法出现整数截断。很多人融合结果发灰,往往是 uint8 计算除二之后小数被舍弃。 - 第三步的“裁剪而非拉伸”是关键:
imresize到同一尺寸虽然简单,但插值会引入虚假纹理,高频系数里会混入不属于任何一张输入图的信息。对图像融合的仿真实验来说,裁剪到公共区域是更严谨的做法。 - 第五步用同一个 figure 分三个 subplot 对比,便于在 GUI 外先验证算法本身是否正确。
3.2 融合函数的三种调用方式与参数传递
如果你的工程不需要 GUI,直接改主脚本里的waveFuse参数即可。常用三种变体:
% 方式1:默认参数,固定 db2、2 层、高频取最大 F1 = waveFuse(I1, I2, 'db2', 2, 'max'); % 方式2:换小波基,观察边缘保留差异 F2 = waveFuse(I1, I2, 'sym4', 2, 'max'); % 方式3:高频改用平均,结果会更平滑但清晰度下降 F3 = waveFuse(I1, I2, 'db2', 2, 'avg');这里有个实际经验:level和rule不是独立参数。当level提高时,高频子带数量变多但单个子带尺寸变小,max规则对噪声更敏感,此时在循环前加一个中值滤波预处理,比在系数域做阈值更简单有效。我一般会在主脚本里放一个use_median开关,为后续 GUI 里加滤波选项预留位置:
% 可选:融合前中值滤波去噪,核大小 3x3 if use_median I1 = medfilt2(I1, [3 3]); I2 = medfilt2(I2, [3 3]); end3.3 常见运行报错与处理对照
| 报错信息 | 发生原因 | 处理办法 |
|---|---|---|
Error using wavedec2 X must be a 2-D matrix | 输入是 RGB 三通道或非二维矩阵 | 调rgb2gray或取单通道 |
The input image is too small to decompose | 图像尺寸小于2^level | 减小level到 1 或 2,或先imresize放大 |
Index exceeds the number of array elements | 尺寸不一致,S1、S2 行数不同 | 按 3.1 的方式统一裁剪,不要直接 resize |
Fused result is all gray/blank | 高频全被置零或低频被错误清零 | 检查 C_fuse 初始化和ll_len是否被赋值 |
| GUI 中点击按钮卡死 | 融合计算阻塞了回调线程 | 用drawnow分割任务,或用parfor改进(见第 4 章) |
这些错误里最容易忽略的是第二个:GUI 里用户选了 3 层分解,但载入的图是 200×150,2^3=8虽小于边长,但wavedec2内部要求每一层维度都是偶数,200 除以两次 2 后余数不为 0 同样会报错。稳妥的做法是在进入融合前加一句检查:
if mod(size(I1,1), 2^level) ~= 0 || mod(size(I1,2), 2^level) ~= 0 uialert(app.UIFigure, '图像尺寸无法被 2^level 整除,请裁剪或缩放', '尺寸错误'); return; end4. 带 GUI 界面:用 App Designer 把融合参数变成可视操作
4.1 为什么选 App Designer 而不是 GUIDE
老版本的 GUIDE 在 MATLAB R2016a 之后就不再推荐新工程使用,新安装的 R2023b、R2024a 里 GUIDE 功能虽然还在,但组件库和回调管理都比较旧。App Designer 的受益点有两个:一是回调函数自动生成骨架,不用手动写handles传递;二是坐标轴控件 UI 与绘图分离,不会出现在 GUIDE 里常见的“图被 figure 覆盖”问题。
对“带 GUI 界面”这个交付需求,App Designer 生成的是.mlapp文件,双击即可运行,不需要额外脚本引导。代码操作视频里演示起来也更直观。整体布局建议如下:
| 控件类型 | 名称(Tag) | 作用 |
|---|---|---|
| 按钮 | LoadImage1Button | 读取第一张输入图 |
| 按钮 | LoadImage2Button | 读取第二张输入图 |
| 下拉框 | WaveletDropDown | 选择小波基:db1/db2/sym2/sym4 |
| 旋钮 | LevelKnob | 选择分解层数 1~4 |
| 下拉框 | RuleDropDown | 选择高频融合规则:max/avg |
| 坐标轴 | FusionAxes | 显示融合结果 |
| 标签 | StatusLabel | 显示运行状态与报错信息 |
| 按钮 | ExportButton | 保存融合结果到文件 |
4.2 回调函数与数据传递的核心写法
App Designer 里不同回调共享数据的方式有三种:存储到app的公开属性、用handles结构体、嵌套函数直接访问。推荐在properties (Access = private)里声明两个变量用于存图:
properties (Access = private) img1 = [] % 第一张输入图,存 double 灰度图 img2 = [] % 第二张输入图 end读取图片的回调:
function LoadImage1ButtonPushed(app, event) [file, path] = uigetfile({'*.png;*.jpg;*.bmp', 'Image Files'}); if isequal(file, 0) return; % 用户取消选择 end fullpath = fullfile(path, file); tmp = imread(fullpath); % 统一转灰度 double 存到 app 属性,后续融合函数直接用 if size(tmp, 3) == 3 tmp = rgb2gray(tmp); end app.img1 = im2double(tmp); app.StatusLabel.Text = ['已加载: ', file]; end融合按钮的回调是重头戏。注意这里要从两个下拉框和旋钮拿参数,再调用第 2 章的waveFuse:
function FuseButtonPushed(app, event) % 检查两张图是否都已加载 if isempty(app.img1) || isempty(app.img2) app.StatusLabel.Text = '请先加载两张输入图'; return; end % 尺寸对齐:统一裁剪到公共区域 [h, w] = size(app.img1); app.img2 = app.img2(1:min(h,size(app.img2,1)), 1:min(w,size(app.img2,2))); app.img1 = app.img1(1:size(app.img2,1), 1:size(app.img2,2)); % 从控件读参数 wname = app.WaveletDropDown.Value; level = app.LevelKnob.Value; rule = app.RuleDropDown.Value; % 调核心融合函数 F = waveFuse(app.img1, app.img2, wname, level, rule); % 显示到坐标轴控件 imshow(F, 'Parent', app.FusionAxes); app.StatusLabel.Text = ['融合完成: ', wname, ', level=', num2str(level)]; end参数说明:
uigetfile的返回值file是文件名、path是文件夹路径,用fullfile拼接后才能传给imread。直接imread(file)只在当前工作目录下有效,用户从其他文件夹选图就会报错。- 回调函数名字里的
Pushed是控件事件名的固定后缀,不能改。app是 App Designer 自动生成的实例对象,所有组件都能通过app.组件Tag访问。 imshow(F, 'Parent', app.FusionAxes)是指定在 UI 的坐标轴里绘图的唯一方式。如果写成imshow(F),图会弹到独立 figure 窗口,而不是嵌在 GUI 界面内。
4.3 在 GUI 里加入小波分解过程可视化
这是进阶但常见的要求:不只显示最终融合结果,还要展示分解后的各层系数。实现方式在 App Designer 里要新增一个坐标轴数组或一个标签页组件。代码核心是:
function ShowDecomposeButtonPushed(app, event) [C, S] = wavedec2(app.img1, 2, 'db2'); % 用 wrcoef2 提取各层细节进行显示 figure('Name', 'Wavelet Coefficients'); for k = 1:2 subplot(2, 2, k); A = wrcoef2('a', C, S, 'db2', k); % 第 k 层低频重构 imshow(mat2gray(A)); title(['Level ', num2str(k), ' Approx']); subplot(2, 2, k + 2); D = wrcoef2('d', C, S, 'db2', k); % 第 k 层高频重构(合成) imshow(mat2gray(abs(D))); title(['Level ', num2str(k), ' Detail']); end end这段代码里的wrcoef2参数中,'a'表示重构近似分量,'d'表示重构细节分量,后面的k是层数。注意 GUI 里弹出独立 figure 并不破坏 App Designer 结构,因为它是临时查看用的,不是主界面的一部分。
5. 融合质量怎么验证、参数陷阱与操作视频录制细节
5.1 用指标替代肉眼看图:熵、标准差与互信息
很多人在 GUI 里调完参数,只看“感觉更亮了一点”就交差了。但融合算法是否真的优于原图或优于另一组参数,需要数值指标支撑。三个在论文和课设报告里最常用的指标:
function [ent, stdv, mi] = fusionMetrics(F, I1, I2) % 计算融合图像常用质量指标 % ent : 信息熵,越大表示信息量越丰富 % stdv : 标准差,反映对比度 % mi : 融合图与两张原图的互信息之和,越大表示保留信息越多 % 1. 信息熵 p = imhist(uint8(F * 255)) / numel(F); p(p == 0) = []; % 去掉 0 概率项,避免 log2(0) 警告 ent = -sum(p .* log2(p)); % 2. 标准差 stdv = std2(F); % 3. 互信息(简单版本):分别算 F 与 I1、I2 的互信息后相加 mi = mutualInfo(F, I1) + mutualInfo(F, I2); end function m = mutualInfo(X, Y) % 离散化到 256 级后算联合直方图 Xq = uint8(X * 255) + 1; Yq = uint8(Y * 255) + 1; joint = accumarray([Xq(:), Yq(:)], 1, [256 256]); pXY = joint / sum(joint(:)); pX = sum(pXY, 2); pY = sum(pXY, 1); % 逐元素计算 p * log(p / (px * py)) m = sum(sum(pXY .* log((pXY + eps) ./ (pX * pY + eps)))); end使用建议:在 GUI 里增加一个“计算指标”按钮,回调里调fusionMetrics,把结果显示到StatusLabel或专门的文本框。对比不同level参数时,记录ent和stdv的数值变化,比主观描述有说服力得多。注意互信息的单位是 bit,两个图像都被归一化到 [0,1] 后计算才有可比性。
5.2 参数陷阱:三处容易让结果“翻车”的地方
第一处是低频平均处理。很多新手自然以为“低频取平均”就是C_fuse(1:ll_len) = (C1(1:ll_len) + C2(1:ll_len)) / 2,这在两张图亮度接近时没问题。但当红外图整体偏亮时,平均后融合图会在亮区发白,暗区细节丢失。更稳的做法是低频用“基于局部能量的加权平均”:将 LL 子带切块,计算每个块的能量占比,用占比作为权重。代码改动不大,但融合质量有明显提升。
第二处是abs取最大时没有考虑符号。高频系数有正有负,绝对值最大不一定代表结构信息最丰富,某些情况下它只是噪声尖峰。一个折中的办法是比较前先做个 3×3 邻域均值滤波判断,即abs(conv2(C1(k), ones(3)/9))和abs(conv2(C2(k), ones(3)/9))比较,能降低孤立噪点被选中。
第三处是 GUI 的旋钮返回值不是整数。app.LevelKnob.Value在用户旋转时返回小数,比如 2.7,直接传给wavedec2会报错。回调里必须加round():
level = round(app.LevelKnob.Value);这个细节在实际演示时最容易暴露,因为手动旋钮很难精确停在整数值上。
5.3 代码操作视频怎么录出“能跟着做”的效果
标题里包含“+代码操作视频”,交付的往往是一个录屏或讲解视频。录制时有一个原则:先录完整流程,再补录关键局部,不要一条视频从头录到尾。推荐流程是:
- 第一段(30 秒):打开 App Designer,运行
.mlapp文件,加载两张样例图,演示融合按钮。 - 第二段(2 分钟):修改下拉框的
wname、旋钮的level,展示结果差异,并配合 imshow 对比。这一步直接展示了 GUI 参数与算法结果的关系。 - 第三段(1 分钟):打开
waveFuse.m源码,指向abs取最大的那一段代码,口头补充一句“这里如果改成平均值,边缘就会发糊”,对应视频文字里写“小波变换图像融合规则对比”。 - 第四段(30 秒):点击导出按钮保存结果,展示目标文件夹里生成的
fused_result.png。
录制工具用 MATLAB 自带record功能或者系统录屏都可以,但建议输出分辨率保持 1920×1080,码率不低于 8Mbps,否则代码区的文字在压缩后看不清。代码字体在 MATLAB 的“预设-字体”里调成等宽字体,字号 14 以上,方便手机端观看。最后在视频里每一步鼠标操作之间停顿 1 秒,方便观众跟按。
如果截图说明和画面标注也可选。操作视频的价值不在画面精美,而在于让一个只装了 MATLAB 的人能看着你的操作顺序,自己把.mlapp跑起来,这就达到了交付要求。
本文还有配套的精品资源,点击获取