ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Matlab边缘检测源码拆解:Canny、LoG与Susan算子从原理到调参

Matlab边缘检测源码拆解:Canny、LoG与Susan算子从原理到调参 简介这份基于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 到 doubleMatlab 图像处理的第一步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.299R0.587G0.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 平滑与基础算子参数对照在这一步最容易踩的坑是“滤波窗口过大”。窗口越大参与卷积的邻域像素越多高频细节丢失越严重。所以我把常用算子和参数整理成一张表方便做实验时对照处理方式函数/模板典型参数适用场景需要注意高斯滤波imgaussfiltsigma1.0~1.5高斯白噪声、一般图像平滑sigma过大边缘偏移中值滤波medfilt2[3 3] 或 [5 5]椒盐噪声窗口太大会抹掉细线LoG核fspecial(log)hsize2ceil(3sigma)1二阶导边缘零交叉检测逻辑要自己写Prewitt算子fspecial(prewitt)无额外参数理解梯度原理对噪声敏感边缘较粗Sobel算子fspecial(sobel)无额外参数比Prewitt稍好仍无法处理弱边缘我自己的习惯是先在小图上用imshow(gray)观察噪声类型再决定用高斯还是中值。像qipan.JPG这种棋盘图边缘本身非常锐利sigma 稍微大一点就容易把角点圆角化所以我用 sigma1.0而hehua.JPG背景纹理复杂sigma1.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和atanMatlab 的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是双阈值我用thLow0.05、thHigh0.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, j1); elseif (d 22.5 d 67.5) || (d -112.5 d -157.5) n1 mag(i-1, j1); n2 mag(i1, j-1); elseif abs(d) 67.5 abs(d) 112.5 n1 mag(i-1, j); n2 mag(i1, j); else n1 mag(i-1, j-1); n2 mag(i1, j1); 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:i1, j-1:j1))) 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 则能得到单像素宽的网格。差别见下表算子边缘宽度抗噪能力连续性双阈值控制典型场景Prewitt2~3像素弱一般无棋盘、简单物体Sobel2像素中一般无通用入门Canny1像素强强有纹理复杂、弱边缘实际跑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 实现LoGLaplacian 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:i1, j-1:j1); 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 非常敏感。sigma0.5 时卷积核太尖锐响应会出现大量细碎边缘sigma2.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~350~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:i1, j-1:j1); 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的雕刻纹路适合 LoGsigma 不要超过 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或uint8imwrite(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%。本文还有配套的精品资源点击获取
返回列表