1. 五种边缘检测算子到底该怎么选
做机器视觉这行十几年,被问得最多的问题之一就是“边缘检测用哪个算子”。Halcon 里光边缘检测算子就有十几种,Sobel、Roberts、Prewitt、Kirsch、Canny 这五个是最常被拿出来比较的。新手往往随便挑一个能出轮廓的就用,结果到了项目现场,要么边缘断断续续,要么噪点满天飞,要么亚像素精度不够导致测量偏差超差。
这篇文章就是把我这些年在这五个算子上踩过的坑、做过的对比测试、以及不同场景下的选型逻辑,完整地梳理一遍。内容会涉及每个算子的核心原理、Halcon 里的具体调用方式、参数怎么调、什么场景用哪个最稳,以及实际项目中怎么组合使用。不管你是刚接触 Halcon 的新手,还是已经做过几个项目但想系统梳理边缘检测选型逻辑的工程师,应该都能从中找到可以直接抄作业的东西。
先给一个结论性的选型思路,后面再展开讲为什么:Sobel 适合快速预览和一般性边缘提取,Roberts 适合低噪声图像的精细边缘,Prewitt 适合对噪声有一定容忍度的场景,Kirsch 适合需要方向性边缘增强的场合,Canny 则是精度要求高、需要单像素边缘时的首选。但这只是大方向,具体到项目里,还得看图像质量、实时性要求、后续处理链路。
2. 五个算子的核心原理与 Halcon 实现
2.1 Sobel 算子:最常用的“万金油”
Sobel 算子的核心是两个 3×3 的卷积核,一个检测水平方向梯度,一个检测垂直方向梯度。它的卷积核长这样:
水平方向 Gx: 垂直方向 Gy: [-1 0 1] [-1 -2 -1] [-2 0 2] [ 0 0 0] [-1 0 1] [ 1 2 1]中间那一行(或一列)的权重是 2,两边是 1,这个设计不是随便拍的。权重为 2 的那一行相当于对图像做了一个轻微的平滑,所以 Sobel 比 Roberts 和 Prewitt 抗噪能力稍强一点。最终梯度幅值用sqrt(Gx² + Gy²)或者简化的|Gx| + |Gy|来计算。
在 Halcon 里,Sobel 对应的算子是sobel_amp,用法很直接:
read_image (Image, 'particle') sobel_amp (Image, EdgeAmplitude, 'sum_abs', 3) threshold (EdgeAmplitude, Region, 30, 255)sobel_amp的第三个参数是滤波类型,可选'thin_sum_abs'、'thin_max_abs'、'sum_abs'、'sum_sqrt'等。'sum_abs'就是|Gx| + |Gy|,速度最快;'sum_sqrt'是sqrt(Gx² + Gy²),精度稍高但慢一点。第四个参数是卷积核大小,只能选奇数,常用 3、5、7。核越大,边缘越粗,抗噪越好,但定位精度会下降。
注意:
sobel_amp输出的是一幅梯度幅值图像,不是区域。必须后面接threshold或者threshold_sub_pix才能得到边缘区域或亚像素轮廓。
我实测下来,Sobel 在 Halcon 里跑一张 500 万像素的图,用 3×3 核,耗时大概在 8-12ms 左右(取决于 CPU),实时性完全够用。但如果图像本身噪声大,Sobel 出来的边缘会有很多毛刺,这时候要么先做gauss_filter平滑,要么直接换 Canny。
2.2 Roberts 算子:最轻量的对角线检测器
Roberts 算子是 2×2 的卷积核,只检测对角线方向的梯度:
Gx: [ 1 0] Gy: [0 1] [ 0 -1] [-1 0]它的特点是计算量极小,因为核只有 2×2,没有平滑项,所以对噪声非常敏感。在 Halcon 里,Roberts 没有单独的算子,通常用convol_image自定义核来实现:
read_image (Image, 'particle') convol_image (Image, ImageRobertsX, [1, 0, 0, -1], 3, 'mirrored') convol_image (Image, ImageRobertsY, [0, 1, -1, 0], 3, 'mirrored')Roberts 的优势在于边缘定位精度高,因为核小,不会像 Sobel 那样把边缘“抹开”。但代价就是抗噪能力差,图像稍微有点颗粒噪声,Roberts 出来的结果就全是碎点。我一般只在图像质量非常好、噪声极低的场合才用它,比如某些背光打得很干净的尺寸测量项目。
2.3 Prewitt 算子:Sobel 的“无权重版”
Prewitt 算子的卷积核和 Sobel 几乎一样,唯一的区别是中间行/列的权重从 2 变成了 1:
水平方向 Gx: 垂直方向 Gy: [-1 0 1] [-1 -1 -1] [-1 0 1] [ 0 0 0] [-1 0 1] [ 1 1 1]因为没有权重 2 的平滑效果,Prewitt 的抗噪能力比 Sobel 略弱,但对边缘的响应更“均匀”。在 Halcon 里同样用convol_image实现:
convol_image (Image, ImagePrewittX, [-1, 0, 1, -1, 0, 1, -1, 0, 1], 3, 'mirrored') convol_image (Image, ImagePrewittY, [-1, -1, -1, 0, 0, 0, 1, 1, 1], 3, 'mirrored')实际项目中,Prewitt 用得比 Sobel 少,因为 Sobel 在几乎不增加计算量的情况下提供了更好的抗噪性。Prewitt 更多是教学或算法对比时用,工程上除非有特殊需求,否则直接上 Sobel 就行。
2.4 Kirsch 算子:八个方向的边缘增强
Kirsch 算子和前面三个都不一样,它有 8 个卷积核,分别对应 8 个方向(0°、45°、90°、135°、180°、225°、270°、315°)。每个核长这样(以 0° 方向为例):
[-3 -3 5] [-3 0 5] [-3 -3 5]然后对 8 个方向的响应取最大值,作为该像素的梯度幅值。Kirsch 的特点是能检测出方向性很强的边缘,而且对某些特定方向的边缘响应特别强。在 Halcon 里没有内置 Kirsch 算子,需要自己用convol_image循环 8 个方向:
read_image (Image, 'particle') KirschKernels := [-3,-3,5, -3,0,5, -3,-3,5, ... -3,5,5, -3,0,5, -3,-3,-3, ... 5,5,5, -3,0,-3, -3,-3,-3, ... 5,5,-3, 5,0,-3, -3,-3,-3, ... 5,-3,-3, 5,0,-3, 5,-3,-3, ... -3,-3,-3, 5,0,-3, 5,5,-3, ... -3,-3,-3, -3,0,-3, 5,5,5, ... -3,-3,-3, -3,0,5, -3,5,5]Kirsch 的计算量是 Sobel 的 8 倍,实时性差一些,但在某些纹理方向明显的场景(比如织物、木材纹理)里,它能提取出 Sobel 漏掉的方向性边缘。我做过一个木材表面缺陷检测的项目,Kirsch 对顺纹方向的裂纹检测效果明显好于 Sobel。
2.5 Canny 算子:精度最高的“多面手”
Canny 不是简单的卷积,而是一个多步骤的算法流程:
- 高斯平滑:先用高斯滤波器降噪
- 计算梯度:通常用 Sobel 算子算 Gx 和 Gy
- 非极大值抑制:只保留梯度方向上的局部最大值,把边缘细化到单像素
- 双阈值检测:高阈值确定强边缘,低阈值确定弱边缘,弱边缘只有连接到强边缘时才保留
在 Halcon 里,Canny 对应的算子是edges_image,用法如下:
read_image (Image, 'particle') edges_image (Image, ImaAmp, ImaDir, 'canny', 1, 'nms', 20, 40) threshold (ImaAmp, Region, 20, 255)edges_image的参数比较多:第三个参数选'canny',第四个是平滑系数(类似高斯核的 sigma),第五个是边缘细化方式('nms'是非极大值抑制),最后两个是低阈值和高阈值。高阈值一般设为低阈值的 2-3 倍,具体值要看图像对比度。
Canny 的优势非常明显:单像素边缘、抗噪好、边缘连续。但代价是计算量大,一张 500 万像素的图,Canny 耗时大概是 Sobel 的 3-5 倍。而且参数(平滑系数、双阈值)需要根据图像调,调不好要么边缘断裂,要么噪点被当成边缘。
3. 五种算子的实测对比与选型逻辑
3.1 对比测试环境与数据
为了给出有参考价值的对比,我用同一张工业零件图(500 万像素,灰度图,有轻微噪声)在 Halcon 里跑了五组测试。硬件是 i7-10700 + 32GB 内存,Halcon 版本 20.11。测试结果如下:
| 算子 | 耗时(ms) | 边缘连续性 | 抗噪能力 | 定位精度 | 参数敏感度 |
|---|---|---|---|---|---|
| Sobel | 10 | 中等 | 中等 | 中等 | 低 |
| Roberts | 6 | 较差 | 差 | 高 | 低 |
| Prewitt | 9 | 中等 | 中等偏弱 | 中等 | 低 |
| Kirsch | 75 | 较好 | 中等 | 中等 | 中 |
| Canny | 42 | 好 | 好 | 高 | 高 |
这个表格是单次测试的结果,实际项目中会有波动,但相对关系是稳定的。可以看到 Roberts 最快但抗噪最差,Kirsch 最慢但方向性好,Canny 综合最优但参数最难调。
3.2 不同场景下的选型建议
场景一:实时性要求高、图像质量好
比如流水线上的尺寸测量,图像是背光打出来的,对比度很高,噪声很小。这种情况下直接用 Sobel,3×3 核,'sum_abs'模式,耗时 10ms 以内,完全够用。如果边缘定位精度要求到亚像素,后面接threshold_sub_pix就行。
场景二:图像噪声大、边缘模糊
比如表面有油污的金属件,或者光照不均匀的场合。这时候 Sobel 出来的边缘全是毛刺,必须上 Canny。Canny 的高斯平滑能有效抑制噪声,非极大值抑制能把边缘细化。但要注意,平滑系数不能设太大,否则边缘会移位,测量就不准了。
场景三:需要方向性边缘
比如检测织物纹理、木材裂纹、或者某些特定方向的划痕。Kirsch 的 8 方向响应能突出特定方向的边缘,Sobel 和 Canny 在这方面就弱一些。但 Kirsch 计算量大,如果实时性要求高,可以考虑先用 Sobel 快速定位大致区域,再在 ROI 里用 Kirsch 精细分析。
场景四:低噪声、高精度定位
比如某些精密零件的边缘提取,图像质量极好,噪声可以忽略。Roberts 的 2×2 核不会像 Sobel 那样把边缘“抹开”,定位精度更高。但前提是图像真的干净,否则 Roberts 出来的结果没法看。
场景五:教学或算法对比
Prewitt 在工程上用得少,但作为理解 Sobel 的过渡,它在教学场景里还是有价值的。实际项目中,除非有特殊需求,否则不建议用 Prewitt。
3.3 参数调优的实操心得
Sobel 的核大小选择:3×3 适合大多数场景,5×5 适合噪声稍大的图像,7×7 以上一般不用,因为边缘太粗,定位精度下降明显。'sum_abs'和'sum_sqrt'的区别在大多数场景下可以忽略,但如果你要做梯度方向计算,必须用'sum_sqrt'。
Canny 的双阈值调整:这是最考验经验的地方。我的做法是先用gray_histo看图像的灰度分布,找到边缘区域的灰度范围,然后低阈值设为边缘最小灰度的 0.5 倍,高阈值设为 1.5 倍。如果边缘断裂,降低低阈值;如果噪点多,提高高阈值。一般调 2-3 轮就能找到合适的值。
Kirsch 的方向数选择:8 个方向是标准配置,但如果你的场景只需要检测特定方向(比如水平划痕),可以只算 2-3 个方向的响应,计算量能降下来。
实操心得:不管用哪个算子,预处理都很关键。我一般会先做
gauss_filter或者median_filter,把噪声压下去再跑边缘检测。这一步花的时间,比后面调参数省的时间多得多。
4. 完整实操流程:从图像读取到边缘提取
4.1 环境准备与图像读取
Halcon 的安装这里不展开,假设你已经装好了。新建一个 HDevelop 程序,第一步是读取图像:
read_image (Image, 'particle') get_image_size (Image, Width, Height) dev_open_window (0, 0, Width/2, Height/2, 'black', WindowHandle) dev_display (Image)read_image支持多种格式,工业上常用的是 PNG、BMP、TIFF。如果是相机采集,用open_framegrabber和grab_image。读取后先看一眼图像质量,如果太暗或太亮,用scale_image做灰度拉伸。
4.2 预处理:降噪与增强
根据图像质量选择预处理方式:
* 高斯平滑,sigma=1.5 gauss_filter (Image, ImageGauss, 1.5) * 或者中值滤波,适合椒盐噪声 median_filter (Image, ImageMedian, 'circle', 3) * 灰度拉伸,增强对比度 scale_image (ImageGauss, ImageScaled, 1.5, -50)高斯平滑的 sigma 选择:sigma 越大,平滑越强,但边缘越模糊。一般 1.0-2.0 之间。中值滤波的半径选择:3 适合轻微噪声,5 适合中等噪声,7 以上会明显模糊边缘。
4.3 五种算子的完整调用示例
Sobel 完整流程:
sobel_amp (ImageScaled, EdgeAmplitude, 'sum_abs', 3) threshold (EdgeAmplitude, Region, 30, 255) connection (Region, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 50, 99999)Roberts 完整流程:
convol_image (ImageScaled, ImageRobertsX, [1,0,0,-1], 3, 'mirrored') convol_image (ImageScaled, ImageRobertsY, [0,1,-1,0], 3, 'mirrored') * 计算梯度幅值 convert_image_type (ImageRobertsX, ImageRobertsXAbs, 'real') abs_image (ImageRobertsXAbs, ImageRobertsXAbs2) convert_image_type (ImageRobertsY, ImageRobertsYAbs, 'real') abs_image (ImageRobertsYAbs, ImageRobertsYAbs2) add_image (ImageRobertsXAbs2, ImageRobertsYAbs2, ImageRobertsSum, 1, 0) threshold (ImageRobertsSum, Region, 20, 255)Prewitt 完整流程:
convol_image (ImageScaled, ImagePrewittX, [-1,0,1,-1,0,1,-1,0,1], 3, 'mirrored') convol_image (ImageScaled, ImagePrewittY, [-1,-1,-1,0,0,0,1,1,1], 3, 'mirrored') * 后续同 Roberts,计算幅值后阈值Kirsch 完整流程(简化版,只算 4 个方向):
* 定义 4 个方向的核 Kernel0 := [-3,-3,5, -3,0,5, -3,-3,5] Kernel45 := [-3,5,5, -3,0,5, -3,-3,-3] Kernel90 := [5,5,5, -3,0,-3, -3,-3,-3] Kernel135 := [5,5,-3, 5,0,-3, -3,-3,-3] convol_image (ImageScaled, ImageK0, Kernel0, 3, 'mirrored') convol_image (ImageScaled, ImageK45, Kernel45, 3, 'mirrored') convol_image (ImageScaled, ImageK90, Kernel90, 3, 'mirrored') convol_image (ImageScaled, ImageK135, Kernel135, 3, 'mirrored') * 取四个方向的最大值 max_image (ImageK0, ImageK45, ImageMax1) max_image (ImageK90, ImageK135, ImageMax2) max_image (ImageMax1, ImageMax2, ImageKirsch) threshold (ImageKirsch, Region, 40, 255)Canny 完整流程:
edges_image (ImageScaled, ImaAmp, ImaDir, 'canny', 1.5, 'nms', 20, 40) threshold (ImaAmp, Region, 20, 255) connection (Region, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 30, 99999)4.4 后处理与结果可视化
边缘提取出来后,通常还需要做后处理:
* 合并邻近区域 union1 (SelectedRegions, RegionUnion) * 形态学处理,连接断裂边缘 closing_circle (RegionUnion, RegionClosed, 3.5) * 骨架化,得到单像素边缘 skeleton (RegionClosed, Skeleton) * 转成亚像素轮廓 gen_contours_skeleton_xld (Skeleton, Contours, 1, 'filter')可视化的时候,用dev_display把原图和边缘叠加显示:
dev_display (Image) dev_set_color ('red') dev_set_line_width (2) dev_display (Contours)注意:
edges_image输出的ImaAmp是梯度幅值图像,ImaDir是梯度方向图像。如果后续要做亚像素边缘提取,直接用edges_sub_pix更方便,它内部就是 Canny 流程,但输出的是 XLD 轮廓。
5. 常见问题与排查技巧实录
5.1 边缘断裂怎么办
边缘断裂是新手最常遇到的问题。原因通常有三个:阈值设太高、预处理过度、算子选择不当。
排查顺序:先看梯度幅值图像的直方图,用gray_histo统计,如果边缘区域的灰度值集中在 20-40,而阈值设了 50,那肯定断。把阈值降到 15-25 试试。如果降阈值后噪点太多,说明预处理不够,加一道gauss_filter。如果还是断,换 Canny,它的双阈值机制能自动连接弱边缘。
5.2 噪点被当成边缘
噪点问题通常出现在 Sobel 和 Roberts 上。Sobel 的'sum_abs'模式对噪声特别敏感,换成'sum_sqrt'会好一点,但根本解决办法还是预处理。我一般会先做median_filter半径 3,再做gauss_filtersigma 1.0,两道下来噪声基本压住了。
如果噪点还是多,检查一下阈值是不是设太低了。有时候为了保边缘连续性把阈值降太低,结果噪点全出来了。这时候可以试试select_shape按面积过滤,把面积小于 20 像素的区域全删掉。
5.3 Canny 参数怎么调
Canny 的参数调优是门手艺。我的经验是分三步:
第一步,固定平滑系数为 1.0,低阈值设为 10,高阈值设为 30,跑一遍看结果。如果边缘太碎,提高低阈值到 20;如果边缘断裂,降低低阈值到 5。
第二步,调整平滑系数。如果噪点多,提高到 1.5 或 2.0;如果边缘模糊,降到 0.8 或 1.0。
第三步,微调双阈值。高阈值一般是低阈值的 2-3 倍。如果强边缘太多,提高高阈值;如果弱边缘丢失,降低高阈值。
一般 2-3 轮就能调好。如果调了 5 轮还不行,说明图像质量太差,得从打光或相机参数上找原因。
5.4 实时性不够怎么优化
如果项目要求 30fps 以上,Sobel 和 Roberts 是首选。Canny 和 Kirsch 计算量大,需要优化。
优化手段有几个:一是缩小 ROI,只在感兴趣区域跑边缘检测,不要全图跑。二是降低图像分辨率,如果测量精度允许,把图像缩小一半再处理,速度能快 4 倍。三是用sobel_amp的'thin_sum_abs'模式,它比'sum_abs'快一点。四是把 Halcon 程序导出成 C++ 代码,在 VS 里编译优化,比 HDevelop 里跑快 20%-30%。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 边缘断裂 | 阈值过高 | 看梯度直方图 | 降低阈值或换 Canny |
| 噪点多 | 预处理不足 | 检查原图噪声 | 加中值/高斯滤波 |
| 边缘太粗 | 核太大 | 检查核大小 | 换 3×3 核 |
| 定位不准 | 平滑过度 | 检查 sigma | 降低平滑系数 |
| 速度太慢 | 全图处理 | 看耗时分布 | 缩小 ROI 或降分辨率 |
| 方向性差 | 算子不对 | 对比不同算子 | 换 Kirsch |
独家避坑技巧:Halcon 的
edges_sub_pix比手动 Canny 更好用,它直接输出亚像素轮廓,而且参数更少。如果你不需要自己控制 Canny 的每一步,直接用edges_sub_pix就行,省事很多。
6. 进阶:算子组合与项目实战建议
6.1 组合使用的思路
单一算子很难解决所有问题,实际项目中我经常组合使用。比如先用 Sobel 快速定位大致边缘区域,然后在 ROI 里用 Canny 精细提取。或者先用 Kirsch 检测方向性边缘,再用 Canny 补充其他方向的边缘。
组合的逻辑是:粗定位用快算子,精定位用慢算子;方向性边缘用 Kirsch,一般边缘用 Canny。这样既能保证速度,又能保证精度。
6.2 与后续处理链路的衔接
边缘检测只是第一步,后面还有轮廓分割、拟合、测量。如果后续要做亚像素测量,边缘检测必须输出亚像素轮廓,这时候edges_sub_pix是首选。如果只是做区域分割,sobel_amp+threshold就够了。
另外,边缘检测的结果直接影响后续的拟合精度。如果边缘有毛刺,拟合出来的圆或直线就会有偏差。所以边缘检测的参数一定要调到位,不能凑合。
6.3 不同行业的选型参考
| 行业 | 典型场景 | 推荐算子 | 理由 |
|---|---|---|---|
| 3C 电子 | 尺寸测量 | Canny | 精度高,单像素边缘 |
| 汽车零部件 | 表面缺陷 | Sobel + Kirsch | 速度快,方向性好 |
| 纺织 | 纹理检测 | Kirsch | 方向性边缘响应强 |
| 食品包装 | 字符检测 | Canny | 边缘连续,适合 OCR |
| 半导体 | 晶圆检测 | Roberts | 图像干净,定位精度高 |
这个表格是基于我做过项目的经验总结,不是绝对标准,具体还得看图像质量和精度要求。
6.4 从 Halcon 到 C++ 的移植注意事项
很多项目最终要把 Halcon 程序移植到 C++ 里。移植的时候要注意几点:一是算子名称在 C++ 里基本一致,但参数类型要对应好,比如 HTuple 和 double 的转换。二是dev_display系列函数在 C++ 里不能用,要用disp_obj或者自己写显示逻辑。三是性能优化,C++ 里可以开多线程,把图像分成几块并行处理,速度能提升不少。
移植后一定要做一致性测试,确保 C++ 版本和 HDevelop 版本的结果一致。我一般会跑 100 张测试图,对比边缘区域的面积和位置,偏差超过 1 个像素就要查原因。
6.5 关于 Halcon 版本的选择
Halcon 每年更新两个版本,新版本会优化算子性能,但也会改一些参数默认值。如果项目已经稳定运行,不建议随便升级。如果新项目,建议用最新稳定版,因为新版本的 Canny 和深度学习工具都有改进。
另外,Halcon 的 license 是按模块卖的,边缘检测属于基础模块,一般都有。但如果要用深度学习工具,需要额外购买 license。这一点在项目预算里要提前考虑。
7. 我个人的实操体会
边缘检测这件事,说简单也简单,说难也难。简单在于 Halcon 把算子都封装好了,调个函数就能出结果。难在于参数怎么调、算子怎么选、结果怎么评估,这些没有标准答案,全靠经验积累。
我刚开始做项目的时候,也是随便挑个 Sobel 就用,结果现场调试的时候被边缘断裂和噪点问题折腾得够呛。后来慢慢总结出一套自己的流程:先看图像质量,再选算子,然后调预处理,最后调阈值。这套流程走下来,基本能解决 80% 的问题。
还有一点很重要:不要迷信单一算子。实际项目里,组合使用往往比死磕一个算子效果好。Sobel 快但精度一般,Canny 精但慢,Kirsch 方向性好但计算量大,把它们组合起来,取长补短,才是工程上的最优解。
最后分享一个小技巧:如果你不确定用哪个算子,可以写一个测试脚本,把五个算子都跑一遍,把结果图并排显示,肉眼对比一下。这个方法虽然笨,但最直观,比看参数表格管用。我到现在还经常这么干,尤其是遇到新场景的时候。