☰
基于边缘检测与霍夫圆变换的圆形缺陷检测实战
2026/10/31 14:14:14 网站建设 项目流程

简介:围绕图像边缘检测与圆形缺陷定位任务,这份MATLAB代码包面向工业质检、医学影像等应用场景,提供了一套从图像预处理到缺陷可视化的完整实现。代码基于Canny边缘检测与霍夫变换圆检测,涉及灰度化、降噪、小孔填充、边界跟踪、端面检测与连通域标记等步骤,可用于识别圆形工件表面的瑕疵并标注位置。压缩包共6个文件,包含5个MATLAB脚本和1张示例图像,整体仅25KB,结构精简,便于逐模块阅读和调试。目前已有1678人学习下载,适合图像处理初学者或需要快速搭建缺陷检测原型的开发者参考。通过运行示例,读者可直观理解边缘检测、圆心定位与缺陷对比分析的关键流程,并能按需调整霍夫变换参数或滤波器,适配不同尺寸和质量的圆形目标,为进一步引入深度学习缺陷分类预留了清晰的技术路径。

1. 基于边缘检测的圆形缺陷检测与定位:把“检测圆”和“查出缺陷”一次做完

用 MATLAB 做视觉检测,最常规的需求就是检测圆、定位缺陷。这个项目就是典型样本:基于 Canny 边缘检测和霍夫圆变换检测圆形轮廓,再结合连通性分析和边界跟踪定位缺陷,最后可视化输出。它适合工业质检、轴承缺陷检测、零件端面检测等场景。拆这个项目时,真正值得关注的不是单点算法怎么用,而是整条链路怎么串起来——边缘检测的参数怎么定、小孔填充为什么放在前一步、连通域标记在缺陷判定里充当什么角色,这些才是决定检测能不能落地的东西。

2. 预处理与边缘检测:为什么第一步是滤波,而不是直接找圆

2.1 读图与灰度化:从 RGB 到二值空间的常用路径

做圆检测之前,图像必须先转换成灰度图。工业现场拿到的图片多数是彩色的,而 Canny 边缘检测和霍夫变换都基于梯度计算,直接用 RGB 图会导致三个通道差异带来大量无用边缘。常见做法是先把图像灰度化,然后根据光照情况决定是否做直方图均衡化来增强对比度。

在 MATLAB 中常见做法是:

img = imread('QQ.jpg'); if size(img, 3) == 3 gray = rgb2gray(img); else gray = img; end gray_smoothed = imgaussfilt(gray, 1.2);

高斯滤波的 sigma 值直接决定边缘的细腻程度:sigma 过小,噪声抑制不够,Canny 会输出一堆碎边;sigma 过大会把细小缺陷的边缘一起磨掉。一般我习惯从 1.0 起步,遇到噪声明显的图再升到 1.5 左右。这个项目里的 fillsmallholes.m 放在这之后,是因为高斯滤波只能模糊噪声,不能消除光照不均匀造成的暗斑和孔洞,形态学操作才是处理它们的手段。

2.2 fillsmallholes.m 的设计逻辑:不要无脑填充全部孔洞

fillsmallholes.m 的功能是填补二值图像中的小型连通孔洞。它通常和 imfill 配合,但直接全图 imfill 会把真实缺陷形成的暗区一起填掉,所以更稳妥的做法是:先标记所有孔洞区域,再按面积阈值决定哪些填充、哪些保留。

function bw_filled = fillsmallholes(bw, max_area) bw_filled = bw; holes = ~bw; holes_label = bwlabel(holes, 8); stats = regionprops(holes_label, 'Area', 'PixelIdxList'); for k = 1:length(stats) if stats(k).Area <= max_area bw_filled(stats(k).PixelIdxList) = 1; end end end

max_area 是核心参数。设置得太大,真实缺陷会被当成孔洞填掉,后续检测直接漏检;设置得太小,又起不到消除噪声的作用。如果圆形工件的直径在 500 像素左右,max_area 设在 20 到 50 像素面积之间比较合适。要特别注意,这个函数的输入是二值图而不是灰度图,项目里如果直接对灰度图做孔洞填充,得到的效果会完全不一样。

2.3 Canny 边缘检测:高低阈值怎么设,不丢边缘也不产碎边

Canny 检测在 MATLAB 中写起来很简单:

edges = edge(gray_smoothed, 'canny', [low_thresh high_thresh]);

但阈值选择是个经验活。low_thresh 太高,边缘断裂,霍夫圆检测的投票数量不足;low_thresh 太低,纹理噪声全被当成边缘,出现大量假圆。另一个常用方案是先用 edge 自动计算阈值,再手动微调:

[~, thresh_out] = edge(gray_smoothed, 'canny'); edges = edge(gray_smoothed, 'canny', [thresh_out(1)*0.6 thresh_out(2)*1.2]);

这比直接手设阈值稳得多。在圆形缺陷检测场景中,目标是从圆形轮廓上找局部异常,边缘的“连续性”比“细节丰富度”更重要,阈值方向宁可稍微偏低,后续用霍夫变换的投票机制过滤非圆边缘。

3. 霍夫圆变换与圆心定位:从边缘图像到候选圆的关键一跳

3.1 霍夫圆变换原理:为什么要在参数空间投票

霍夫圆变换的原理不复杂:图像空间中每个边缘像素点,在参数空间(圆心横坐标 a、纵坐标 b、半径 r)中对应一个圆锥曲面;多个边缘像素的圆锥曲面相交于同一点,说明它们共同属于同一个圆。MATLAB 里常用 imfindcircles 实现:

[centers, radii, metric] = imfindcircles(edges, [r_min r_max], ... 'ObjectPolarity', 'bright', 'Sensitivity', 0.92, 'Method', 'PhaseCode');

r_min 和 r_max 是圆的半径范围,直接影响检测速度和结果。范围太宽会带来大量无效投票,范围太窄会把真实圆的半径边缘切掉。Sensitivity 是从 0 到 1 的宽松程度参数:0.9 以上几乎可以容忍相当大的形变,但也会引入假圆;一般先 0.9 起步,假圆太多再降到 0.85。ObjectPolarity 也很关键,表示检测的是亮背景下的暗圆还是暗背景下的亮圆,选错方向会导致检测结果为空。

3.2 EndFaceDetection.m 的实现路径:从边缘到圆心的三步

EndFaceDetection.m 是整个项目的主检测模块,名字里的 EndFace 说明它检测的是零件的端面,通常是轴承端面或圆形工件端面。这个模块的常见实现路径是:先对边缘图像做霍夫圆检测,拿到圆心和半径候选;再用连通域分析和边界跟踪做二次确认;最后把最佳圆绘制在原图上。

[centers, radii, metric] = imfindcircles(edges, [40 150], 'Sensitivity', 0.9); [~, idx] = sort(metric, 'descend'); centers = centers(idx(1:min(3, length(idx))), :); radii = radii(idx(1:min(3, length(idx))));

注意,不要把所有候选圆都当成真实结果,通常只取 metric 最高的前 1 到 3 个。如果工件就是单个圆,取一个就够;视野里有多个圆时,才需要多圆处理逻辑。EndFaceDetection.m 里做的就是这个筛选工作——先把候选圆按置信度降序排列,再截取前几个,最后结合工件尺寸约束做判断。

3.3 边界跟踪与端面检测的衔接:BoundaryTrack.m 的角色

BoundaryTrack.m 是边界跟踪模块,作用是沿着检测到的圆边缘按顺序串起边缘像素,形成闭合轮廓。这条轮廓在后面的缺陷定位中至关重要,因为缺陷就表现为轮廓上某一段和理想圆的偏差。

function boundary = BoundaryTrack(bw_edge, start_point) boundary = bwtraceboundary(bw_edge, start_point, 'N', 8, 'clockwise'); end

bwtraceboundary 需要起始点和搜索方向。起始点通常取圆的最高处像素,从北方向开始顺时针绕行。跟踪出来的边界是 N×2 的坐标数组,后续缺陷判断可以直接对这个数组做距离分析。这里的衔接点要特别注意:霍夫圆变换得到的是离散的圆参数,边界跟踪得到的是连续像素链,只有两者在同一坐标系下对齐,才能计算每个边缘像素到理想圆的距离。

4. 缺陷定位与可视化:从偏差计算到连通域标记

4.1 缺陷判定逻辑:理想圆与实际边缘的距离测量

拿到理想圆的圆心 (cx, cy) 和半径 r,再拿到实际边界点集 boundary,就可以逐个计算偏差。这里的关键是定义“缺陷”:边缘点到理想圆周的距离超过一定阈值,就认为该点属于缺陷区域。

dist = sqrt((boundary(:,1) - cx).^2 + (boundary(:,2) - cy).^2); deviation = dist - r; defect_mask = abs(deviation) > 2;

这里的 2 像素阈值是经验值。图像分辨率高、物体轮廓大时,可以放宽到 3 到 5 像素;高精度工件则需要到 1 像素。阈值过高会漏掉细小裂纹,阈值过低会把边缘噪声全部标成缺陷,产线根本没法用。实际落地时,我建议先用标准工件图跑一遍,统计 deviation 的标准差,用标准差的两倍作为缺陷判定阈值,而不是凭感觉定数字。

4.2 ConnectivityMark.m 与 connect.m:连通域标记如何映射到缺陷区域

缺陷像素通常不是单独一个点,而是聚成一条线或一片区域。ConnectivityMark.m 在这个项目里做的是连通性标记:

function labeled = ConnectivityMark(mask) labeled = bwlabel(mask, 8); end

bwlabel 的第二个参数是连通性,一般用 8 邻域。标记之后,每个缺陷区域就有了独立的标签编号,可以统计数量、面积、位置。connect.m 做的是另一件事——把距离较近但尚未连通的缺陷区域合并成一个整体。常用实现是形态学闭运算或基于距离的聚合:

se = strel('disk', 3); connected = imclose(mask, se); labeled = bwlabel(connected, 8);

闭运算的作用是把断裂的缺陷连上。如果不做这个连接步骤,同一条裂纹因为边缘强度不均可能被拆成多个独立区域,缺陷数量统计就会出错。strel 的 disk 半径选择也很讲究:半径太小连不起来,半径太大又可能把两个独立的缺陷合并成一个,需要用缺陷之间的最小间距来反推。

4.3 可视化:用颜色把缺陷标注出来

缺陷检测的最终输出,是把缺陷区域用醒目颜色覆盖在原图上,方便人工复检。常见做法是提取缺陷边界后用 bwboundaries 获取轮廓坐标,再在原图上绘制:

[B, L] = bwboundaries(defect_bw, 'noholes'); imshow(img_original); hold on; for k = 1:length(B) boundary = B{k}; plot(boundary(:,2), boundary(:,1), 'r', 'LineWidth', 2); end viscircles(centers(1,:), radii(1), 'Color', 'g', 'LineWidth', 1); hold off;

这一步不要只画缺陷边界,也要把理想圆画出来。绿色理想圆配合红色缺陷边界的对比非常直观,无论是人工复核还是写进质检报告都方便。另外,缺陷区域的面积和位置坐标也应该保存下来,方便后续统计缺陷率,而不只是显示一张图。

5. 圆形缺陷检测的常见踩坑与排查:六个翻车场景和经验

5.1 霍夫圆变换检测不到圆,一找就是零结果

现象:imfindcircles 返回空数组,一个候选圆都没有。

原因通常有三个:Canny 阈值设太高导致边缘断裂严重,边缘图上的轮廓不连续;半径范围设置不对,与真实圆的半径差太多;或者图像对比度过低,圆形轮廓在梯度图里不够明显。

解决:先用 imshow(edges) 亲眼确认边缘图质量。轮廓清晰完整,那就是半径范围的问题,把 r_min 和 r_max 分别放宽 20%。轮廓断裂成虚线,就把 Canny 的低阈值降下来。整个边缘图都很淡,回退到上一步,增强对比度后再做边缘检测。这个排查顺序能省掉大量盲调参数的时间。

5.2 圆心定位整体偏移,缺陷全部出现在同一侧

现象:检测出的圆心坐标总偏向一个方向,比如所有缺陷都集中在某个固定方位。

原因:霍夫圆变换对边缘分布不均很敏感。如果圆形轮廓的一部分边缘被光照阴影吞掉,投票峰值会向高密度边缘一侧偏移,导致圆心整体偏移。这个问题在真实产线光照环境下非常常见,尤其是端面有金属反光时。

解决:比较靠谱的办法是用边界跟踪得到的像素点做最小二乘圆拟合,重新估计圆心和半径,替代或校正霍夫变换的结果。另一个思路是先对边缘图做对称处理,再利用整个圆周的信息重新投票。从项目角度看,最简单有效的步骤是在霍夫检测之后,加一次拟合校正:

[xc, yc, R] = circle_fit(boundary(:,1), boundary(:,2));

拟合得到的圆心比霍夫结果更稳定,尤其适合后续距离测量。

5.3 小孔填充过度,把真实缺陷填成了干净边缘

现象:某些图检测不出缺陷,但肉眼明显能看到瑕疵。

原因:fillsmallholes.m 的 max_area 设得过大,把缺陷形成的暗区当成小孔填掉了。还有一种可能,是高斯滤波的 sigma 太大,细小裂纹被完全磨平,边缘检测阶段就已经丢失信息。

解决:把 max_area 调小一半,重新对比填孔前后的二值图。判断标准很简单:被填掉的最小孔洞到底是不是缺陷。如果同一张图里既有噪声孔又有真实缺陷,建议用连通域面积直方图找到分界点,而不是拍脑袋定阈值。从那以后我遇到这种问题,第一步永远是做填孔前后对比图,看损失了哪些真实信息。

5.4 缺陷区域被拆散,一个缺陷被数成多个

现象:统计缺陷数量时,明显同一条裂纹被标成 3 到 4 个独立区域。

原因:Canny 在缺陷边缘强度较弱的段落出现断裂,导致缺陷像素在空间上不连通,bwlabel 把它们当成多个独立区域。这在划痕类缺陷中尤其突出——划痕深浅不一,浅的部分边缘响应弱,像素断档。

解决:加大 connect.m 中闭运算的结构元半径,或者基于距离变换做聚合。合并后再用 bwlabel 重新统计,缺陷数量通常就能对上。如果还不对,检查光照条件,确认是不是阴影区域没有成像,导致部分缺陷完全没有进入图像。

5.5 椭圆度被误判为缺陷,合格工件大量报警

现象:标准圆工件被标出一圈缺陷,位置相对分布,看起来像是左右对称的偏差。

原因:相机光轴与工件端面不垂直时,正圆在图像上会变成椭圆。用标准圆方程计算偏差,会在椭圆的长轴和短轴方向产生系统性误差,全部被判成缺陷。

解决:先做相机标定,确认光轴垂直,这是根治方案。另一个处理办法是改用椭圆拟合:对边界跟踪得到的点集做椭圆拟合,再用椭圆方程计算每个点到椭圆的法向距离,把系统性的椭圆偏差分离出去,剩下的才是真正的局部缺陷。这个思路在端面检测里非常实用,能显著降低误检率。

5.6 connect.m 在灰度图上直接调用,结果全图连成一片

现象:connect.m 用 imclose 处理后,整个图像糊成一团,不是预期的缺陷区域。

原因:形态学闭运算虽然可以处理灰度图,但参数和效果与二值图完全不同。灰度图加 disk 结构元的闭运算,会把亮度相近的纹理全部融合,输出结果根本不是缺陷掩膜。

解决:把连接操作限制在二值掩膜上,并在调用前做类型检查:

if ~islogical(mask) mask = imbinarize(mask); end

项目代码里如果存在这个问题,加两行类型检查比改结构元参数省事得多。暴露出来的教训就是:在 MATLAB 里,形态学函数和逻辑图的匹配关系要时刻注意,二值图操作就别拿灰度图往里塞。

6. 从检测到复检:用三张对照图快速验证检测结果

一个圆缺陷检测模块能不能上线,光看检测率还不够,还要看误检率和漏检率。我一般会准备三张对照图:标准无缺陷图、带已知缺陷的实拍图、含强噪声干扰的挑战图。每次改参数后,用同一套流程跑这三张图,看三件事——标准图上有没有假缺陷、缺陷图上缺陷是否全部被标出、噪声图上有没有引发虚警。

function report = run_validation(img_ok, img_defect, img_noise) result_ok = detect_circle_defect(img_ok); result_def = detect_circle_defect(img_defect); result_noise = detect_circle_defect(img_noise); report.ok_false_alarm = length(result_ok.defect_regions); report.defect_detected = length(result_def.defect_regions); report.noise_alarm = length(result_noise.defect_regions); end

这套流程的用法是:在调节 fillsmallholes.m 的 max_area 或 Canny 阈值后,重新跑一遍三图校验。很多参数单张图上看着没问题,换一张光照不同的工件图就露馅了。三图校验能逼着你在标准图和挑战图之间找平衡,而不是单张图调到完美就结束。另外,把检测到的缺陷面积历史记录保存下来,做成趋势图,比单张图的绝对数值更有参考价值,因为产线光照和工件批次的变化都会反映在趋势里。

从那以后,每次调参数,我都强制走一遍三图验证流程,标准图不过就不允许上线试跑。小孔填充、边缘阈值、霍夫灵敏度这三组参数相互影响,单独调一个很难判断到底是哪个环节出了问题。愿意花这一步工夫,后面进现场调试时能少熬几个夜,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询