1. 为什么值得把这本书的Matlab函数单独整理一遍
如果你正在啃《数字图像处理》这本书,不管是本科选修、考研复习还是研究生阶段补基础,大概率都会遇到一个很现实的问题:书上的公式推导看懂了,原理也大致明白了,但一打开Matlab就不知道从哪个函数下手。这本书的理论体系非常完整,从灰度变换、频域滤波到形态学、分割、压缩,几乎覆盖了数字图像处理的全部经典内容,但它的代码示例是分散在各个章节里的,而且不同版本用的函数风格还不完全统一。
我自己前前后后把这本书过了三遍,第一遍纯看理论,第二遍跟着书敲代码,第三遍才真正把里面出现的Matlab函数按功能模块梳理成了一张自己的速查表。这个整理过程最大的价值在于:当你面对一个实际的图像处理任务时,你能快速定位到应该用哪个函数、参数怎么设、有没有替代方案,而不是翻遍全书去找那一段示例代码。
这篇文章就是把我自己整理的这份函数汇总分享出来,同时补充一些书上没写透但实际用起来很关键的细节。适合正在学这门课的学生、需要快速上手图像处理的工程师,以及想从理论过渡到代码实现的任何人。不管你用的是哪个版本的Matlab,这些核心函数的用法基本是通用的,差异我会在具体地方标注出来。
2. 图像读写与基础操作:一切从这里开始
2.1 图像的读取、显示与保存
任何图像处理任务的第一步都是把图像读进来。Matlab里最核心的读取函数是imread,它的基本用法很简单:
img = imread('lena.png');但这里有几个实际使用中容易踩的坑。第一,imread返回的数据类型取决于图像文件的位深,灰度图通常是uint8,但如果是16位TIFF或者某些科研图像格式,可能返回uint16。很多人在后续做算术运算时忘了转换类型,结果出现截断或者溢出,图像看起来一片白或者一片黑。我的习惯是读完图像后立刻确认类型:
img = imread('lena.png'); disp(class(img)); % 查看数据类型 disp(size(img)); % 查看尺寸显示图像用imshow而不是image,这一点书上写得很清楚但初学者经常混用。imshow会自动处理数据范围的映射,而image默认按索引色显示,直接拿来显示灰度图往往得到一堆奇怪的颜色。如果你同时要看多张图做对比,subplot配合imshow是标配:
figure; subplot(1,2,1); imshow(img); title('原图'); subplot(1,2,2); imshow(edge(img,'canny')); title('边缘检测');保存图像用imwrite,注意如果图像数据是double类型且范围在[0,1]之间,直接imwrite会出问题,需要先转回uint8:
imwrite(im2uint8(img_double), 'output.png');注意:
im2uint8和uint8不是一回事。uint8(1.5)得到的是2(四舍五入),而im2uint8(1.5)会先把数据缩放到[0,255]再转换。对于[0,1]范围的double图像,必须用im2uint8。
2.2 图像类型转换与色彩空间
这本书里大量涉及灰度图和彩色图的互相转换。rgb2gray是最常用的,但它的转换公式不是简单的三通道平均,而是按人眼对不同波长光的敏感度加权:
gray = rgb2gray(img); % 0.2989*R + 0.5870*G + 0.1140*B如果你需要做颜色分割或者分析色彩信息,rgb2hsv、rgb2lab这些转换函数就派上用场了。HSV空间在颜色阈值分割时特别有用,因为H通道直接对应色调,不受光照强度影响。我做过一个项目需要提取图像中的红色区域,在RGB空间调阈值调了半天效果都不稳定,换到HSV空间后只用了三个阈值就搞定了:
hsv = rgb2hsv(img); h = hsv(:,:,1); s = hsv(:,:,2); v = hsv(:,:,3); red_mask = (h < 0.05 | h > 0.95) & s > 0.3 & v > 0.2;数据类型转换方面,im2double和double的区别值得单独说。im2double会把uint8的[0,255]映射到[0,1],而double只是改类型不改数值。做频域变换、滤波这些运算时,通常需要先im2double,否则数值范围不对会导致结果异常。
3. 空域滤波与增强:从灰度变换到卷积核
3.1 灰度变换函数族
冈萨雷斯书里讲灰度变换那一章,核心函数其实就几个,但组合起来能玩出很多花样。最基础的线性变换可以直接用算术运算实现,但Matlab提供了更规范的函数:
% 线性拉伸 J = imadjust(I, [0.3 0.7], [0 1]); % 伽马校正 J = imadjust(I, [], [], 0.5); % gamma=0.5,提亮暗部 % 对数变换(需要手动实现) J = log(1 + im2double(I)); J = mat2gray(J);imadjust的第二个参数是输入强度范围,第三个是输出范围,第四个是伽马值。这里有个经验:对于曝光不足的图像,gamma取0.3到0.6之间通常效果不错;曝光过度的图像,gamma取1.5到3.0。但具体值还是要看直方图分布,没有万能参数。
直方图均衡化用histeq,这个函数在书上出现频率极高:
J = histeq(I); J = histeq(I, 64); % 指定输出灰度级数histeq默认把图像均衡到64个灰度级,对于大多数8位图像来说够用了。但如果你处理的是医学图像或者遥感图像,可能需要保留更多灰度级,这时候第二个参数就要调大。另外,adapthisteq是自适应直方图均衡化,适合处理局部对比度差异大的图像,比如背光拍摄的照片:
J = adapthisteq(I, 'ClipLimit', 0.02, 'NumTiles', [8 8]);ClipLimit控制对比度增强的上限,值越大增强越猛但也越容易放大噪声。NumTiles决定分块大小,块越小局部适应性越强但计算越慢。我一般从[8 8]开始试,效果不够再调到[16 16]。
3.2 卷积与相关操作
空域滤波的本质就是卷积。Matlab里做卷积有两个层次:一是用imfilter直接对图像操作,二是用conv2做纯矩阵卷积。imfilter更常用,因为它自动处理边界填充:
h = fspecial('average', [3 3]); J = imfilter(I, h, 'replicate');imfilter的第三个参数是边界填充方式,常见的有'replicate'(复制边缘像素)、'symmetric'(镜像对称)、'circular'(循环填充)。默认是补零,但补零会在图像边缘产生暗边,做滤波时通常用'replicate'或'symmetric'更合理。
fspecial是生成各种经典滤波核的快捷函数,书上涉及的包括:
| 核类型 | 调用方式 | 典型用途 |
|---|---|---|
| 均值滤波 | fspecial('average', [3 3]) | 平滑去噪 |
| 高斯滤波 | fspecial('gaussian', [5 5], 1.5) | 平滑,保留边缘较好 |
| 拉普拉斯 | fspecial('laplacian', 0.2) | 边缘增强 |
| 锐化 | fspecial('unsharp', 0.2) | 钝化掩蔽锐化 |
| 运动模糊 | fspecial('motion', 20, 45) | 模拟运动模糊 |
高斯核的第二个参数是标准差,标准差越大模糊越厉害。我一般从1.0开始试,对于512x512的图像,标准差1.5到2.0之间的高斯核能去掉大部分高频噪声同时保留主要结构。
实操心得:
imfilter默认输出和输入同类型。如果输入是uint8,滤波后的负值会被截断为0,导致结果偏差。做拉普拉斯锐化这类会产生负值的操作时,务必先转成double:J = imfilter(im2double(I), h, 'replicate');
3.3 中值滤波与统计滤波
中值滤波在去椒盐噪声方面是经典方法,函数是medfilt2:
J = medfilt2(I, [3 3]); J = medfilt2(I, [5 5], 'symmetric');窗口大小必须是奇数。3x3适合轻度噪声,5x5适合中度噪声,再大就会明显模糊图像细节。medfilt2的第二个参数是邻域大小,第三个是边界填充方式,和imfilter类似。
统计滤波方面,ordfilt2可以实现排序滤波,中值滤波其实是它的特例:
J = ordfilt2(I, 5, ones(3,3)); % 3x3邻域内第5大的值,即中值ordfilt2的第二个参数是排序后的位置索引,对于3x3邻域共9个元素,第5个就是中值。这个函数在实现自定义排序滤波时很有用,比如最大值滤波(取第9个)或最小值滤波(取第1个)。
4. 频域处理:傅里叶变换与滤波
4.1 傅里叶变换的实现细节
频域处理是这本书的重头戏,Matlab对应的核心函数是fft2和ifft2。但直接调用这两个函数得到的频谱是未中心化的,零频率在左上角,不便于观察和滤波。标准做法是:
F = fft2(im2double(I)); F_shifted = fftshift(F); S = log(1 + abs(F_shifted)); % 对数变换便于显示 imshow(S, []);fftshift把零频率移到中心,log(1+abs(...))是压缩动态范围的标准操作。这里abs取的是复数模,log压缩后频谱的明暗对比才适合人眼观察。
滤波时,先构造频域滤波器,然后和频谱逐元素相乘,再反变换回来:
[M, N] = size(I); D0 = 30; % 截止频率 H = zeros(M, N); for u = 1:M for v = 1:N D = sqrt((u-M/2)^2 + (v-N/2)^2); H(u,v) = double(D <= D0); % 理想低通 end end G = F_shifted .* H; J = real(ifft2(ifftshift(G)));这段代码是理想低通滤波器的实现。注意ifftshift是fftshift的逆操作,反变换前必须把频谱移回原位。real取实部是因为反变换结果理论上应该是实数,但浮点运算会引入极小的虚部。
4.2 常用频域滤波器对比
书上介绍了三种基本低通滤波器:理想、巴特沃斯、高斯。它们的频域响应函数不同,效果也有明显差异:
| 滤波器类型 | 传递函数 | 特点 | 振铃效应 |
|---|---|---|---|
| 理想低通 | D<=D0时1,否则0 | 截止陡峭 | 严重 |
| 巴特沃斯 | 1/(1+(D/D0)^(2n)) | 过渡平滑,n可调 | 中等 |
| 高斯 | exp(-D^2/(2*D0^2)) | 最平滑 | 几乎没有 |
巴特沃斯滤波器的阶数n控制过渡带陡峭程度,n=1时接近高斯,n=5时接近理想。实际使用中,n取2到3之间是比较好的折中。高斯滤波器没有振铃效应,但过渡带最宽,截止特性最"软"。
高通滤波器可以用低通取反得到:H_hp = 1 - H_lp。但这样得到的高通滤波器在零频率处增益为0,会丢失图像的直流分量,导致结果整体偏暗。更好的做法是用H_hp = 1 - H_lp后再加一个常数偏移,或者直接用高通滤波器的标准形式。
4.3 同态滤波的Matlab实现
同态滤波是书上频域增强部分的一个重点,用于同时压缩动态范围和增强对比度。它的核心思想是把图像分解为照度分量和反射分量,在频域分别处理:
I_double = im2double(I); I_log = log(1 + I_double); F = fftshift(fft2(I_log)); [M, N] = size(I); D0 = 30; gamma_L = 0.5; gamma_H = 2.0; c = 1; H = zeros(M, N); for u = 1:M for v = 1:N D = sqrt((u-M/2)^2 + (v-N/2)^2); H(u,v) = (gamma_H - gamma_L) * (1 - exp(-c * (D^2 / D0^2))) + gamma_L; end end G = F .* H; J = exp(real(ifft2(ifftshift(G)))) - 1; J = mat2gray(J);这段代码里gamma_L控制低频增益(压缩动态范围),gamma_H控制高频增益(增强细节)。典型取值是gamma_L=0.5、gamma_H=2.0。c控制滤波器过渡的陡峭程度,一般取1左右。同态滤波的计算量比较大,因为要对每个像素做双重循环,实际使用时可以用矩阵运算向量化加速。
5. 形态学处理:从结构元素到重建
5.1 结构元素的创建与选择
形态学操作的第一步是定义结构元素,Matlab用strel函数:
se = strel('square', 3); % 3x3方形 se = strel('disk', 5); % 半径5的圆形 se = strel('line', 10, 45); % 长度10,角度45度 se = strel('rectangle', [3 5]); % 3行5列矩形结构元素的形状和大小直接决定形态学操作的效果。方形结构元素适合处理矩形特征,圆形适合处理各向同性的特征,线形适合提取特定方向的线条。书上在讲边界提取、骨架化时用的都是特定形状的结构元素,实际使用时需要根据目标特征来选择。
strel还支持分解操作,比如一个大的方形结构元素可以分解为多个小结构元素的组合,这样能大幅减少计算量:
se_large = strel('square', 15); se_seq = strel('square', 3); % 分解后等价于用3x3做多次膨胀5.2 基本形态学操作
腐蚀和膨胀是最基础的两个操作:
J_erode = imerode(I, se); J_dilate = imdilate(I, se);开运算和闭运算是它们的组合:
J_open = imopen(I, se); % 先腐蚀后膨胀 J_close = imclose(I, se); % 先膨胀后腐蚀开运算能去除小亮点(比结构元素小的亮区域),闭运算能填充小暗点。这个"小"是相对于结构元素尺寸而言的。我做过一个去除图像中细小噪点的任务,用3x3方形做开运算,效果比中值滤波还好,因为中值滤波会模糊边缘而开运算不会。
形态学梯度是膨胀减腐蚀:
J_grad = imdilate(I, se) - imerode(I, se);这个操作能提取出目标的轮廓,轮廓宽度等于结构元素的尺寸。书上在边缘检测那一章提到过这个方法,实际用起来比Sobel算子得到的边缘更"干净",但定位精度稍差。
5.3 形态学重建与孔洞填充
形态学重建是书上比较高级的内容,Matlab对应的函数是imreconstruct:
marker = I; mask = I; J = imreconstruct(marker, mask);重建操作需要两个输入:标记图像和掩膜图像。标记图像经过反复膨胀,但每次膨胀后都和掩膜取交集,直到图像不再变化。这个操作在去除图像中的小孔洞、提取连通区域时非常有用。
孔洞填充可以用重建来实现:
I_fill = imfill(I, 'holes');imfill的'holes'选项会填充所有封闭的孔洞。但要注意,如果孔洞和边界连通,imfill不会填充它,因为从边界看它是"开放"的。这时候需要先用形态学闭运算把边界封上,再填充。
实操心得:形态学操作对结构元素尺寸非常敏感。我一般先用
imopen配合不同尺寸的结构元素试一遍,观察哪个尺寸能去掉噪声又不损伤目标,然后再确定最终参数。这个过程没有捷径,只能靠试。
6. 图像分割:从边缘到区域
6.1 边缘检测算子对比
边缘检测是分割的基础,Matlab提供了edge函数作为统一入口:
BW = edge(I, 'sobel'); BW = edge(I, 'prewitt'); BW = edge(I, 'roberts'); BW = edge(I, 'log'); BW = edge(I, 'canny'); BW = edge(I, 'canny', [0.1 0.3], 1.5);Canny算子是最常用的,因为它有双阈值和边缘连接机制,得到的边缘最完整。edge函数对Canny的调用支持两个额外参数:阈值向量和sigma。阈值向量是[低阈值 高阈值],sigma是高斯平滑的标准差。
Canny的三个参数(高阈值、低阈值、sigma)需要根据图像特点调整。我的经验是:sigma先取1.0到2.0之间,高阈值取0.1到0.3之间,低阈值取高阈值的0.4倍左右。如果边缘太多太碎,提高高阈值;如果边缘断裂太多,降低低阈值或增大sigma。
Sobel和Prewitt算子计算简单但边缘较粗,适合对速度要求高、精度要求不高的场景。LoG算子(高斯拉普拉斯)对噪声敏感,使用前需要先做高斯平滑。Roberts算子最简单,但对噪声最敏感,实际很少单独使用。
6.2 阈值分割与Otsu方法
全局阈值分割用im2bw或者新版本的imbinarize:
level = graythresh(I); % Otsu方法自动求阈值 BW = imbinarize(I, level);graythresh返回归一化到[0,1]的阈值,imbinarize用它来二值化。Otsu方法的核心是最大化类间方差,对于双峰直方图的图像效果很好。但如果直方图不是双峰,或者目标背景面积差异很大,Otsu的效果就会打折扣。
自适应阈值分割适合光照不均匀的图像:
BW = imbinarize(I, 'adaptive', 'Sensitivity', 0.5);Sensitivity参数控制阈值的敏感度,值越大越容易把像素判为目标。对于背景较暗、目标较亮的图像,这个参数取0.4到0.6之间比较合适。
6.3 区域生长与分水岭
区域生长需要手动指定种子点,Matlab没有直接的函数,但可以用bwselect交互式选择或者手动指定坐标:
seed_row = 100; seed_col = 150; J = regiongrowing(I, seed_row, seed_col, 0.05);regiongrowing不是Matlab自带函数,需要自己实现或者从File Exchange下载。核心逻辑是:从种子点开始,检查邻域像素与当前区域均值的差异,如果小于阈值就加入区域,直到没有新像素加入。
分水岭算法用watershed函数:
D = -bwdist(~BW); % 距离变换取负 D(~BW) = -Inf; L = watershed(D);分水岭容易产生过分割,通常需要先做形态学开闭运算平滑,或者用imextendedmin和imimposemin标记前景和背景来引导分割。书上讲分水岭时用的是简单的距离变换,实际使用时几乎都要加预处理和后处理。
7. 常见问题与排查技巧实录
7.1 数据类型引发的典型问题
这是初学者最容易踩的坑,没有之一。Matlab图像处理中涉及的数据类型有uint8、uint16、double、logical,不同函数对输入类型的要求不同,混用就会出问题。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 滤波后图像全白 | uint8运算溢出 | 先im2double再滤波 |
| 频域滤波结果异常 | 未做fftshift | 变换后加fftshift |
| 形态学操作无效果 | 结构元素太大或太小 | 调整strel尺寸 |
| 边缘检测结果全黑 | 阈值设得太高 | 降低阈值或调sigma |
| 二值化后全白 | 阈值方向反了 | 检查前景背景定义 |
我自己的习惯是:任何涉及算术运算的操作,先把图像转成double;任何涉及显示的操作,确认数据范围在[0,1]或[0,255];任何涉及逻辑运算的操作,确认输入是logical类型。
7.2 函数版本差异与替代方案
Matlab不同版本之间有些函数会改名或者废弃。比如im2bw在新版本中推荐用imbinarize替代,graythresh虽然还能用但官方建议用otsuthresh。这些变化不影响核心逻辑,但如果你照着老版本的书写代码,可能会遇到警告。
另一个常见问题是工具箱依赖。edge、imfilter、strel这些函数都在Image Processing Toolbox里,如果你没有安装这个工具箱,这些函数都用不了。替代方案是用基础Matlab函数手动实现,比如用conv2代替imfilter,用矩阵运算实现形态学操作。但手动实现效率低很多,对于大图像可能会很慢。
7.3 性能优化与向量化技巧
书上给的示例代码很多是双重循环,在教学场景下没问题,但处理大图像时速度堪忧。Matlab的精髓是向量化运算,能用矩阵操作的就不要用循环。
以频域滤波为例,构造滤波器时可以用meshgrid代替双重循环:
[M, N] = size(I); [U, V] = meshgrid(1:N, 1:M); D = sqrt((U - N/2).^2 + (V - M/2).^2); H = double(D <= D0);这样一行代码就完成了原来双重循环的工作,速度快几十倍。类似的技巧在形态学、滤波、分割中都可以用。核心原则是:尽量用矩阵整体运算,避免逐像素操作。
实操心得:如果你发现某段代码跑得特别慢,先检查有没有可以向量化的循环。Matlab的循环开销很大,尤其是嵌套循环。我优化过一个形态学重建的代码,把双重循环改成矩阵运算后,运行时间从十几秒降到了不到一秒。
8. 把函数汇总变成自己的工具箱
整理这份函数汇总的过程中,我最大的体会是:光看函数列表没用,必须动手把每个函数跑一遍,观察输入输出,调整参数看效果变化。我自己的做法是建了一个脚本文件,把每个函数的最小可运行示例都写进去,需要的时候直接改参数运行。
另外,Matlab的函数文档非常详细,每个函数都有完整的参数说明和示例。遇到不熟悉的函数,直接在命令行输入doc 函数名就能打开文档。但文档不会告诉你的是:哪些参数在实际使用中最关键、哪些组合容易出问题、不同函数之间怎么配合。这些经验只能靠动手积累。
如果你也在学这本书,建议不要只盯着公式推导,多花时间在Matlab里试。同一个滤波操作,用不同的结构元素、不同的参数跑一遍,观察结果差异,比看十遍书都管用。图像处理说到底是一门实践性很强的学科,函数用得多了,自然就有感觉了。