简介:这份基于Matlab的边缘检测资源包,面向计算机、电子信息及数学等专业学生,可作为图像处理课程设计、期末大作业或毕业设计的参考资料,重点解决边缘检测算法实现与验证问题。压缩包共14个文件,包含9个.m源代码文件、4张jpg测试图像和1份doc程序运行说明,整体仅139KB,结构紧凑。源码覆盖Canny、Susan、LOG等经典边缘检测算子,并提供相应阈值辅助函数;测试图像包含棋盘、荷花等典型场景,便于读者直观对比不同算子的检测效果。程序运行说明有助于快速理清运行流程,读者可在此基础上自行调整参数或扩展功能。资源已有414人浏览学习,适合具备一定Matlab基础、希望动手调试代码的开发者参考使用。
1. 从Matlab源码包拆边缘检测:Canny、LoG、Susan 一次跑通
这个压缩包解压之后,没有复杂的工程框架,就是几个.m文件加几张测试图:canny_edge.m、log_edge.m、susan.m、cannyFindLocalMaxima.m、susan_threshold.m,配了hehua.JPG、qipan.JPG、diaohua.jpg、xinglong.jpg。说实话,我第一反应是先把canny_edge.m拖进编辑器直接运行,结果发现输入参数得自己给,图像路径也要改,和纯调edge()门禁完全不一样。这也是很多课程设计里“源码能跑”和“真正看懂”之间的差距。下面就从预处理开始,把边缘检测从灰度化、滤波、梯度计算、非极大值抑制到双阈值连接拆开讲,顺便把 LoG 和 Susan 的参数坑也填上。适合要做期末大作业、课程设计,或者想摆脱一行调包思路的人。
2. 图像读取、灰度化与噪声抑制:边缘检测前的预处理逻辑
在运行canny_edge.m这类源码前,我一般会先把测试图读进来,统一转成灰度图。这个包里给的xinglong.jpg和hehua.JPG都是彩色图,直接传给边缘检测函数,要么报维度错误,要么在三通道上分别出边缘又被程序自己扔掉,结果非常不稳定。
2.1 从 imread 到 double:Matlab 图像处理的第一步
Matlab 的imread默认把彩色 JPEG 读成uint8,值域 0~255。uint8在做卷积时很容易溢出,比如两个很大的像素相减,结果可能变成负数,超出 0~255 的有效范围。所以我一般先im2double转成double,把值域映射到 0~1,再做滤波和梯度计算。下面是包外测试时我最常用的一段读取代码:
% 读取彩色图像并转灰度 img = imread('hehua.JPG'); if size(img, 3) == 3 gray = rgb2gray(img); else gray = img; end gray = im2double(gray); % 转成 double,值域 [0,1] whos gray这段代码里size(img, 3)是取第三维长度,等于 3 就说明是 RGB 图像,rgb2gray按 0.299R+0.587G+0.114B 的权重合成灰度。im2double会把uint8的 0~255 线性映射到 0~1,这也是后面imgradientxy和自定义卷积结果能统一比较的前提。如果读进来的是索引图像,还要先ind2gray或ind2rgb处理,不过这个包里都是普通 JPG,用不上。
从资源里ex6.m、ex7.m这些命名能看出,作者应该是按实验编号存的脚本,ex6.m可能是 Sobel 验证,ex7.m是 Canny 主流程。实际跑的时候不要在每个脚本里重复读图,最好把读取、灰度化、去噪写成一个公共小节,后面所有算子都复用同一份gray。
2.2 高斯滤波与中值滤波:为什么不能直接求梯度
边缘本质上是灰度突变区域,突变意味着相邻像素差很大,也就是梯度幅值高。但直接对原始图像求梯度,会把传感器噪声、压缩噪声也当成边缘放大。所以 Canny、LoG 这类算子在求梯度之前都会先平滑。高斯滤波的核由 sigma 决定,sigma 越大,图像越模糊,细小纹理被抑制,代价是真正的边缘位置也会偏移。
我一般在噪声不严重的图上用imgaussfilt(gray, sigma),sigma 取 1.0~1.5;如果图像有明显椒盐噪声,比如扫描图,就先用medfilt2(gray, [3 3])过一遍中值滤波,再做高斯。中值滤波对孤立噪声点效果比高斯好,而且不会把边缘磨得太圆。这里顺便说一句,很多人一提到边缘检测就想到 Prewitt 边缘检测原理,Prewitt 是两个 3x3 模板分别检测水平和垂直梯度,实现简单,但对噪声极其敏感。这个包里没有单独给 Prewitt 的.m文件,不过用fspecial('prewitt')可以自己验证,把它和 Canny 放在同一个预处理流程里对比时,Prewitt 的响应明显更碎。
下面是我对这个包里的图做预处理时的固定流程:
sigma = 1.2; gaussImg = imgaussfilt(gray, sigma); % 如果输入图像有椒盐噪声,先执行下面一行 % gaussImg = medfilt2(gray, [3 3]);imgaussfilt是 Matlab 自带的快速高斯滤波函数,比imfilter(gray, fspecial('gaussian', ...))更快,边界处理默认是'replicate'。medfilt2只对二维灰度图有效,彩色图要先转灰度,否则要用medfilt3或分通道处理。
2.3 平滑与基础算子参数对照
在这一步最容易踩的坑是“滤波窗口过大”。窗口越大,参与卷积的邻域像素越多,高频细节丢失越严重。所以我把常用算子和参数整理成一张表,方便做实验时对照:
| 处理方式 | 函数/模板 | 典型参数 | 适用场景 | 需要注意 |
|---|---|---|---|---|
| 高斯滤波 | imgaussfilt | sigma=1.0~1.5 | 高斯白噪声、一般图像平滑 | sigma过大边缘偏移 |
| 中值滤波 | medfilt2 | [3 3] 或 [5 5] | 椒盐噪声 | 窗口太大会抹掉细线 |
| LoG核 | fspecial('log') | hsize=2ceil(3sigma)+1 | 二阶导边缘 | 零交叉检测逻辑要自己写 |
| Prewitt算子 | fspecial('prewitt') | 无额外参数 | 理解梯度原理 | 对噪声敏感,边缘较粗 |
| Sobel算子 | fspecial('sobel') | 无额外参数 | 比Prewitt稍好 | 仍无法处理弱边缘 |
我自己的习惯是,先在小图上用imshow(gray)观察噪声类型,再决定用高斯还是中值。像qipan.JPG这种棋盘图,边缘本身非常锐利,sigma 稍微大一点就容易把角点圆角化,所以我用 sigma=1.0;而hehua.JPG背景纹理复杂,sigma=1.5 更干净。预处理做完,后面 Canny 和 LoG 才有可比性。
3. Canny 算子实现拆解:梯度、非极大值抑制与双阈值
Canny 是这个包的核心,canny_edge.m和cannyFindLocalMaxima.m两个文件就是完整实现。Canny 能不能出好效果,关键不在梯度计算,而在后面的非极大值抑制和滞后连接。只看edge()函数的人很难理解为什么 Canny 比 Sobel 的线更细、更连续。
3.1 Canny 边缘检测的标准五步流程
Canny 把边缘检测拆成五步:高斯平滑、计算梯度幅值和方向、非极大值抑制、双阈值分割、滞后连接。这个包里canny_edge.m应该就是按这个顺序写的,cannyFindLocalMaxima.m专职做局部极大值搜索。
用普通文本表示,梯度幅值M = sqrt(Gx.^2 + Gy.^2),方向theta = atan2(Gy, Gx)。但我不建议在源码里直接写sqrt和atan,Matlab 的imgradient和imgradientxy已经优化过,而且能同时返回方向数组。下面是按这个流程整理的简化版主函数骨架,和包里canny_edge.m的结构一致:
function [edgeMap] = canny_edge(grayImg, sigma, thLow, thHigh) % 输入:灰度图,高斯sigma,低阈值,高阈值 smoothImg = imgaussfilt(grayImg, sigma); % 用Sobel模板计算梯度 [Gx, Gy] = imgradientxy(smoothImg, 'sobel'); [Gmag, Gdir] = imgradient(Gx, Gy); % 非极大值抑制,得到细化后的梯度脊 nms = cannyFindLocalMaxima(Gmag, Gdir); % 双阈值:强边缘直接保留,弱边缘等滞后连接判断 strong = nms > thHigh; weak = (nms >= thLow) & (nms <= thHigh); edgeMap = hysteresis(strong, weak); endimgradientxy(smoothImg, 'sobel')返回Gx、Gy,默认的'sobel'是 3×3 模板,也可以换成'prewitt'或'central',这也是为什么 Canny 可以和 Prewitt 共用梯度计算基础。thLow和thHigh是双阈值,我用thLow=0.05、thHigh=0.15作为初值,实际调试时按照边缘断裂或噪声斑点的比例再调。
3.2 非极大值抑制:cannyFindLocalMaxima.m 为什么关键
cannyFindLocalMaxima.m做的事情是:沿着梯度方向看当前像素的幅值是不是局部最大,如果不是,就置 0。这样梯度幅值图上那些“宽”的边缘会被压缩成单像素宽。实现上,梯度方向被量化到四个扇区:水平、垂直、45°、135°。
function nms = cannyFindLocalMaxima(mag, dir) nms = zeros(size(mag)); [rows, cols] = size(mag); for i = 2:rows-1 for j = 2:cols-1 d = dir(i,j); if (abs(d) <= 22.5 || abs(d) >= 157.5) n1 = mag(i, j-1); n2 = mag(i, j+1); elseif (d >= 22.5 && d < 67.5) || (d <= -112.5 && d > -157.5) n1 = mag(i-1, j+1); n2 = mag(i+1, j-1); elseif abs(d) >= 67.5 && abs(d) < 112.5 n1 = mag(i-1, j); n2 = mag(i+1, j); else n1 = mag(i-1, j-1); n2 = mag(i+1, j+1); end if mag(i,j) >= n1 && mag(i,j) >= n2 nms(i,j) = mag(i,j); end end end end这段代码里dir是角度值,范围是 -180° 到 180°。abs(d) <= 22.5 || abs(d) >= 157.5对应水平边缘,因为梯度方向垂直于边缘,水平边缘的梯度方向接近竖直。这里最容易写错的是扇区边界,比如 67.5° 到底归到对角还是竖直,差一点会让边缘出现断裂或错位。我的建议是在对比图上直接打印dir的中位数,观察分布再调阈值。
3.3 双阈值与滞后连接
双阈值包含高低两个值。高于thHigh的像素确定为强边缘,低于thLow的直接丢弃,介于中间的标记为弱边缘。弱边缘如果和强边缘有连通关系,就保留,否则删除。滞后连接的常见做法是用bwlabel给强边缘连通域编号,再检查弱边缘是否落在强边缘邻近:
function edgeRes = hysteresis(strong, weak) edgeRes = strong; [L, ~] = bwlabel(strong, 8); % 如果弱边缘像素的8邻域中存在强边缘,就把它加入结果 for i = 2:size(weak,1)-1 for j = 2:size(weak,2)-1 if weak(i,j) && sum(sum(strong(i-1:i+1, j-1:j+1))) > 0 edgeRes(i,j) = 1; end end end end注意bwlabel默认按 8 连通编号,弱边缘必须落在强边缘的 8 邻域内才算连接。如果直接对weak & strong做imdilate再取交集,速度更快,但可读性不如循环。包里的cannyFindLocalMaxima.m函数名和常见 Canny 教学代码同名,说明作者参考的是 Gonzalez 那套教材实现。
3.4 Canny 与其他梯度算子的效果对比表
对于qipan.JPG这种棋盘格,边缘在四方向都有响应,Prewitt 和 Sobel 会把角点附近重复计算,形成两条交叉亮线,Canny 则能得到单像素宽的网格。差别见下表:
| 算子 | 边缘宽度 | 抗噪能力 | 连续性 | 双阈值控制 | 典型场景 |
|---|---|---|---|---|---|
| Prewitt | 2~3像素 | 弱 | 一般 | 无 | 棋盘、简单物体 |
| Sobel | 2像素 | 中 | 一般 | 无 | 通用入门 |
| Canny | 1像素 | 强 | 强 | 有 | 纹理复杂、弱边缘 |
实际跑canny_edge.m时,如果把thLow提到 0.1,弱边缘大量减少,荷花花瓣上的细纹会被滤掉;如果把thHigh降到 0.08,噪声斑点会连成短线。两个阈值合理范围需要看梯度幅值分布,我一般先histogram(nms(:)),选梯度幅值直方图的 30% 和 70% 分位点作为低高阈值起点。
4. LoG 与 Susan 边缘检测:从二阶导到非梯度思路
Canny 是典型的基于一阶梯度的算法。这个包里另外两个文件log_edge.m和susan.m走的是完全不同的路线:LoG 用二阶导数的零交叉,Susan 用邻域灰度相似性。理解它们之后,遇到低对比度图像就知道该选谁。
4.1 LoG 算子的原理与 log_edge.m 实现
LoG(Laplacian of Gaussian)先高斯平滑,再求拉普拉斯二阶导。理论上,灰度从亮到暗的过渡区域,梯度幅值会出现极大值,二阶导数在那个位置为零,所以“零交叉点”就是边缘。
Matlab 里可以直接用fspecial('log', hsize, sigma)生成 LoG 卷积核。我一般把核的尺寸设为ceil(sigma * 6),确保核边界的值衰减到接近 0。实现:
function edgeMap = log_edge(grayImg, sigma) hsize = ceil(sigma * 6); if mod(hsize, 2) == 0 hsize = hsize + 1; end logKernel = fspecial('log', hsize, sigma); logResp = imfilter(grayImg, logKernel, 'replicate'); edgeMap = zeros(size(logResp)); % 零交叉检测 for i = 2:size(logResp,1)-1 for j = 2:size(logResp,2)-1 patch = logResp(i-1:i+1, j-1:j+1); if max(patch(:)) > 0 && min(patch(:)) < 0 edgeMap(i,j) = 1; end end end endfspecial('log', hsize, sigma)生成的核同时包含高斯平滑和拉普拉斯运算。imfilter默认会做边界填充,'replicate'是复制边缘像素,比默认的'zero'效果更稳。零交叉检测这里用了 3×3 邻域,只要正负极值同时出现就标记为边缘。更严谨的做法是检查相邻像素是否异号,但对于课程设计级别,3×3 扫描已经够用。
LoG 对 sigma 非常敏感。sigma=0.5 时,卷积核太尖锐,响应会出现大量细碎边缘;sigma=2.0 时,边缘连续性好,但对细线的定位偏差也大。diaohua.jpg这种雕刻纹路复杂、边缘密集的图,sigma 取 1.5 左右比较平衡。如果直接用edge(gray, 'log'),Matlab 会自动选 sigma 并内部处理阈值,但效果往往不如自己控制。
4.2 Susan 算子的原理与 susan_threshold.m 参数
Susan 算子的全称是 Smallest Univalue Segment Assimilating Nucleus。它用一个小圆模板遍历图像,比较模板内每个像素与中心像素的灰度差,统计与中心像素灰度相近的区域面积(USAN)。
边缘或角点处的 USAN 面积会小于平坦区域,所以 USAN 越小,该中心越可能是角点或边缘点。susan_threshold.m里的核心参数应该是灰度差阈值t,我习惯取 25~35(0~255 范围)。t越大,认为相似的像素越多,USAN 面积变大,检测出的边缘越少;t越小,敏感度越高,但噪声会被当成边缘。
下面是一个简化版 Susan 边缘响应计算:
function susanRes = susan_threshold(grayImg, t) grayImg = im2double(grayImg) * 255; % 回到0~255方便设阈值 [rows, cols] = size(grayImg); susanRes = zeros(rows, cols); % 半径为1的圆形掩膜,共9个像素(含中心) mask = [0 1 0; 1 1 1; 0 1 0]; for i = 2:rows-1 for j = 2:cols-1 patch = grayImg(i-1:i+1, j-1:j+1); center = grayImg(i,j); diff = abs(patch - center); % 灰度差 < t 的像素计数 usan = sum(mask(:) & (diff(:) < t)); % USAN面积越小,响应越强 susanRes(i,j) = 9 - usan; end end end这个版本把 USAN 响应定义为“模板内不相似像素的个数”,数值越大越像边缘。实际工程里 Susan 还会用几何阈值g做二次判断,把低于g的响应置零,用来过滤伪响应。包里的susan.m应该负责主流程,susan_threshold.m负责阈值判断,我一般按“先算 USAN,再和 g 比较”的逻辑去读这两个文件。ex8.m和ex9.m的命名顺序也暗示作者把 LoG 和 Susan 的验证分开了,建议按ex6.m→ex8.m→ex9.m的顺序跑一遍。
4.3 三种算子在测试图上的横向对比
我用hehua.JPG做对比时发现一个明显现象:Canny 在花瓣边缘有连续闭环,LoG 在花瓣高光区域会出现双边缘,Susan 则把荷叶上的纹理也捡了进来。这说明没有绝对最优的算子,只有参数能不能匹配图像场景。下表是我对这组图的实际观感记录:
| 算子 | 棋盘图 | 荷花图 | 雕花图 | 参数敏感度 |
|---|---|---|---|---|
| Canny | 网格清晰 | 花瓣边缘连续 | 纹理边缘偏碎 | 高低阈值影响大 |
| LoG | 边缘有重影 | 高光处双边缘 | 细纹响应好 | sigma影响最大 |
| Susan | 棋盘角点过强 | 纹理响应较多 | 雕刻轮廓完整 | 对t和g都敏感 |
从这组结果来看,如果目标是做后续的 Hough 直线检测,优先用 Canny;如果是想保留雕刻这类细致纹理的轮廓,LoG 配合中等 sigma 反而更合适;如果图像亮度和对比度不均,Susan 对光照变化更强健。
4.4 参数调整建议
我一般会把log_edge和susan_threshold的输入参数抽出来放到脚本顶部,避免反复编辑。例如:
sigmaLog = 1.5; tSusan = 27; gSusan = 8;然后调用imshowpair并排显示结果,观察哪一个参数让边缘断得最少。注意 LoG 输出是二值图,Susan 的原始响应是连续灰度图,需要再设阈值或归一化才能和二值图直接对比。这块源码包里没有统一封装,我在实验里是单独写后处理函数处理的。
5. 用棋盘、荷花和雕花图验证边缘检测结果:参数微调与常见坑
5.1 棋盘和荷花:不同噪声下的调参侧重
qipan.JPG的高频边缘多,跑 Canny 先把 sigma 拉到 1.0 以内,避免网格交叉点被圆角化。hehua.JPG的低对比花瓣边缘需要降低低阈值到 0.03~0.05,同时记得用medfilt2预处理一下。diaohua.jpg的雕刻纹路适合 LoG,sigma 不要超过 2.0,否则相邻纹路会粘连。
5.2 结果保存与数据精度的坑
最后收尾时提一个常见坑:用imwrite(edgeMap, 'result.png')时,如果edgeMap是logical类型,Matlab 会直接写成二值图,没有问题;但如果是从canny_edge.m里拿到的double类型,值只有 0 和 1,直接imwrite会写成一个全黑或全白的图。这是因为imwrite对double数组按 0~1 范围处理,边界值 1 映射到 255,中间没有值就不会出现可见边缘。正确做法是先转成logical或uint8:
imwrite(uint8(edgeMap) * 255, 'result_edge.png');或者:
imwrite(logical(edgeMap), 'result_edge_binary.png');uint8(edgeMap) * 255把 double 二值图映射到 0/255,再保存就不会出现全黑全白的问题。另一个验证手段是在 Matlab 里直接对比边缘像素占比:sum(edgeMap(:)) / numel(edgeMap),棋盘图通常在 5%~10%,荷花图可能在 2%~8%。
本文还有配套的精品资源,点击获取