Halcon圆形检测实战:从阈值分割到卡尺拟合的完整指南
2026/9/17 12:44:00 网站建设 项目流程

做机器视觉这行,十有八九都躲不过“找圆”这个需求。小到纽扣电池的极片定位,大到风电轴承的滚子分拣,圆形检测几乎是视觉引导、尺寸测量、缺陷判别的底层地基。但“用Halcon查找圆形”这个需求,实际落地时远不是调一个算子那么简单:同一个圆,在背光打光、同轴光打光、自然光漫反射条件下,处理路径完全不同;同一张图,是找镂空孔、找反光金属端面、还是找暗色橡胶圈,选型逻辑也完全不同。

这篇就结合我这些年用Halcon做项目积累的经验,把圆形检测从思路选型到工程落地的完整链路拆开讲一遍。里面所有代码都是HDevelop语法,我尽量写得能直接跑通,方便你拿着自己的图去验证。

1. 圆形查找的三条技术路线:先选对思路再动手

很多新手拿到一张图就急着往上堆算子,这是最容易翻车的做法。圆形检测在Halcon里表面上看是“一个函数的事”,实际上底层对应三条完全不同的技术路线,适用场景天差地别。选错了路线,后面调参调到怀疑人生也拿不到稳定结果。

第一条路线:阈值分割加Blob分析。这是最直观、也最“视觉工程师本能”的做法。核心逻辑是:目标圆和背景之间存在明显的灰度差,先用threshold算子把目标从背景里抠出来,再用connection把连通的像素块拆成独立区域,然后用select_shape按面积、圆度等几何特征筛掉杂讯,最后用smallest_circle或者fit_circle_contour_xld求圆心和半径。这条路线在背光照明下效果极好,因为背光打出来就是黑白分明的剪影,圆就是圆,干扰少得可怜。但如果目标圆和背景的灰度差不够大,或者图里有大量反光、纹理、脏污,单靠阈值分割就会崩。

第二条路线:Hough变换检测圆。hough_circles算子是很多教程里的“明星函数”,看着一行代码就把圆找出来了,但它的参数其实非常敏感。Hough变换的本质是把图像空间中的每一个边缘点映射到参数空间,然后在参数空间里做累加投票。圆在参数空间里是三维的(圆心x、圆心y、半径r),所以投票过程既消耗内存又消耗算力。Halcon里的hough_circles已经做了大量优化,但对噪声还是很敏感。边缘点一多、干扰一杂,投票结果就容易漂移。这条路线适合灰度差没那么明显、但边缘轮廓还算完整的场景,比如自然光下拍金属零件的外圆。

第三条路线:卡尺法加几何拟合。这条路线是工业项目里的“老油条”做法,也是精度最高、最抗干扰的做法。核心逻辑是:先确定一个大致的目标位置和半径范围,然后在圆弧上均匀分布若干卡尺(也就是测量矩形Region),每个卡尺沿法线方向扫描边缘,在边缘位置附近用亚像素精度找边缘点,最后把所有这些边缘点丢给fit_circle_contour_xld做圆拟合,输出亚像素精度的圆心和半径。这条路线不依赖全局阈值,对局部光照变化、边缘毛刺都有很强的鲁棒性,测量精度可以做到0.01个像素级别。代价是需要给一个初始位置和半径范围,本质上是一个“先粗定位、再精测量”的思路。

一句话总结选型逻辑:背景干净用阈值分割,边缘清晰但灰度杂乱用Hough,工业测量场景直接用卡尺法加拟合。下面我把这三条路线的具体实现一个个讲透。

2. 阈值分割快速实现:从读图到输出圆心坐标的完整流程

先讲最常用的Blob分析路线,毕竟很多初学项目的第一步都是从这张图开始的。

假设你手头有一张背光拍摄的垫片图,背景是亮的,圆形垫片是暗的,那么完整流程是这样的:

* 读取图像 read_image (Image, 'washer.png') * 灰度化,如果不是灰度图就转一下 rgb1_to_gray (Image, GrayImage) * 阈值分割,暗色目标在前景,所以选Dark参数 threshold (GrayImage, Region, 0, 128) * 拆分连通域 connection (Region, ConnectedRegions) * 按面积筛掉小噪点 select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 500, 999999) * 按圆度筛选 select_shape (SelectedRegions, CircularRegions, 'circularity', 'and', 0.5, 1.0) * 对每个圆形区域拟合最小外接圆 smallest_circle (CircularRegions, Row, Column, Radius) * 在图上画出来看看效果 gen_circle_contour_xld (ContCircle, Row, Column, Radius, 0, 6.28318, 'positive', 1.0) dev_display (Image) dev_display (ContCircle)

这套流程跑下来,如果背景够干净,基本一次就能拿到满意的圆心和半径。但你实际项目里遇到的图大概率没这么乖巧,这时候就需要注意几个关键细节。

第一个细节:阈值分割时不要一上来就写死阈值。光照波动是产线常态,上午九点和下午三点的环境光都不一样,更别说设备开机预热后的光源衰减。如果直接threshold写死0到128,早晚要出事。靠谱的做法是用灰度直方图来确定阈值,或者用binary_threshold这个算子自动计算阈值。

binary_threshold (GrayImage, Region, 'max_separability', 'dark', UsedThreshold)

binary_threshold内部跑的是最大类间方差法,就是统计学里的OTSU算法。它会自动找一个阈值,让前景和背景两个类的类间方差最大,这样即使整体亮度偏移,阈值也会跟着偏移,鲁棒性好很多。

第二个细节:开运算去噪。阈值分割出来的区域边缘容易带毛刺,内部也容易有小孔洞。建议在threshold之后加一步开运算,用小半径的圆形结构元素把孤立噪点去掉:

* 圆形结构元素,半径3.5 opening_circle (Region, RegionOpening, 3.5) * 闭合内部孔洞 closing_circle (RegionOpening, RegionClosing, 3.5)

第三个细节:擅用select_shape的更多几何特征。area和circularity只是基本款,配合'width'、'height'、'anisometry'(长短轴比)可以更精准地锁定目标。比如要找一个圆环的外圆,可以先把圆环区域通过fill_up填满,再求外圆,否则smallest_circle拟合出来的可能是内圆边缘。

* 圆环区域填满成实心 fill_up (CircularRegions, RegionFillUp) smallest_circle (RegionFillUp, Row, Column, Radius)

这种“先填洞再拟合”的手法,在检测垫片、法兰、齿轮这类带孔零件时几乎每天都要用。

3. Hough变换检测圆:边缘复杂的场景怎么拿稳结果

接下来讲Hough变换。当你遇到的图没法靠阈值干净分割时,Hough往往是被寄予厚望的备选方案。比如自然光下拍一个磨砂金属外壳上的圆形Logo,目标和背景的灰度差不大,阈值分割一塌糊涂,但边缘轮廓还算能辨认,这时候hough_circles可以上来救场。

看一下基本用法:

read_image (Image, 'metal_logo.png') rgb1_to_gray (Image, GrayImage) * 提取边缘,Hough变换在边缘图上跑 edges_sub_pix (GrayImage, Edges, 'canny', 1.5, 20, 40) * Hough检测圆,半径范围要根据实际图像大致估一下 hough_circles (Edges, Circles, Radius, 0, 3.14159, 30, 50, 0.2, 0.4, 0.1) * 输出结果 area_center_xld (Circles, Area, Row, Column)

但这里我必须强调:hough_circles的参数非常拧巴,新手容易直接被参数搞崩溃。参数里包含半径上下限、角度步长、累加器阈值、边缘幅度阈值、圆心距离阈值等等。我实际用的经验是:

  • 半径范围(Radius参数)不宜过大,最好把目标圆半径的波动范围压缩在正负10个像素以内,范围越大,参数空间的投票分布越稀疏,误检率越高。
  • 累加器阈值(AccThreshold)不要默认,需要根据图像里边缘点的密度调试。调小了会检测出一堆假圆,调大了真圆也会被丢掉。经验值一般在0.2到0.5之间,具体用小步长去试。
  • 边缘幅度阈值(EdgeThresh)和边缘宽度直接相关。图像边缘锐利时可以适当提高,把模糊的干扰边缘过滤掉;边缘发虚时就得降低,否则圆轮廓根本进不了投票池。

Hough变换虽然听着“自动化”,但它在工业场景里反而是最不省心的。因为它的参数和图像内容相关性太强,换一个产品就要重新调一轮,维护成本很高。所以我个人对Hough的定位是:适合做一次性分析、离线验证、或者目标圆特别规整的OCR/定位场景,不适合做产线上的长期稳定测量工具。

另外,如果圆是不完整的圆弧,比如机器人夹爪抓取的工件只露出一段弧,hough_circles也能用,但投票结果会变差。这时候可以考虑把整圆检测换成圆弧检测思路,或者直接转卡尺法——只在一段圆弧上布卡尺,拟合圆弧,输出圆心和半径。

4. 多目标筛选与轮廓拟合:工件上十几个孔怎么做到一个不落

实际项目里另一个高频需求是:一张图上有一堆圆,要把它们全部找出来,并且区分大小圆。比如PCB板上有不同直径的定位孔,或者注塑件上有多个安装孔位,这时候单纯跑一遍Blob分析或者Hough会漏检或误检。

处理这种场景,我的习惯是三步走:先分割,再筛选,最后逐类精定位。

第一步,分割。按灰度阈值把所有的孔区域都抠出来。这一步的要点是:宁可多抠,不能少抠。把阈值放宽一些,让边缘稍微过分割也没关系,后面有筛选兜底。

第二步,筛选。用select_shape把圆按面积分成几个区间。比如图上明显有直径5mm和直径8mm两类孔,那就统计一下两类孔的面积范围,分别筛:

* 筛小孔 select_shape (AllRegions, SmallHoles, 'area', 'and', 500, 1200) * 筛大孔 select_shape (AllRegions, LargeHoles, 'area', 'and', 1500, 3000)

第三步,对每一类孔做圆形度校验和轮廓拟合。圆形度校验用'circularity'特征就够用,一般要求0.8以上。但要注意,如果孔边缘有倒角或者毛刺,圆形度会偏低,阈值别卡太死。

* 先掏出来满足圆度要求的区域 select_shape (SmallHoles, SmallHolesCircles, 'circularity', 'and', 0.8, 1.0) * 为了亚像素精度,提取区域的轮廓,然后拟合成XLD圆 gen_contour_region_xld (SmallHolesCircles, Contours, 'border') fit_circle_contour_xld (Contours, 'algebraic', -1, 0, 0, 3, 2, Row, Column, Radius, StartPhi, EndPhi, PointOrder)

这里有个很重要的细节:fit_circle_contour_xld的拟合算法(第二个参数)有'algebraic'、'geometric'、'huber'等好几种。

  • 'algebraic'是代数距离最小化,速度快,适合大多数规则圆。
  • 'geometric'是几何距离最小化,精度更高,但对异常点敏感。
  • 'huber'是带权重的鲁棒拟合,能有效抑制边缘毛刺的影响,是实测中最抗造的选择。

工业现场我通常直接用'huber'。它的思想是给偏离拟合圆的点设置一个误差权重,偏离太远的点会被降权,从而不影响整体拟合结果。应对倒角、毛刺、轻微缺损都很有效。

多目标场景还有一个常见问题:两个圆离得很近,连通域被粘连在一起。这种情况可以在connection之后加一步opening或watersheds分割。Halcon里有个很有用的算子叫opening_segmentation,专门处理粘连区域的分离,效果比单纯的开运算要好。

opening_segmentation (Region, RegionOpened, 5)

5. 从像素坐标到物理坐标:标定与测量精度控制

很多项目做完圆形查找不是画个圈看看就完了,而是要输出测量值、引导机械臂抓取,甚至是把像素坐标吐给下游PLC做定位。这时候你就必须面对一个绕不开的问题:像素坐标怎么换算成物理坐标。

最朴素的思路是像素当量法。用已知直径的标准件拍一张图,跑一遍圆形检测,得到像素直径,除以实际物理直径,就得到每个像素对应多少毫米。

* 已知标准件实际直径是10.00mm * 检测到的像素直径是2500像素 * 像素当量就是 PixelSize := 10.00 / 2500.0 * 单位:mm/像素

像素当量法简单粗暴,但只适合测量面与相机光轴完全垂直的场景。一旦镜头有透视、畸变,或者测量面有倾角,边缘位置的像素当量就变了,这时候必须上标定。

Halcon里做标定的标准流程是:

  1. 用标定板拍十几张不同位姿的图。
  2. find_calib_object算子提取标定板角点。
  3. calibrate_cameras算子解算出相机内参和畸变系数。
  4. 在运行时用change_radial_distortion_image对图像做畸变校正,或者用calibrate_camera_scara这类针对视觉引导的标定工具。

标定完之后的测量逻辑就变成:先在像素坐标系里找到圆心,然后用image_points_to_world_plane把像素坐标投影到世界坐标系的平面上,在世界坐标系里算半径和圆心距。

说句掏心窝的话:如果你做完标定之后发现测量结果还有0.1mm以上的跳动,先别怀疑算法,先去检查机械装配。这种精度漂移问题,十有八九是工件本身的位置没放平,或者夹具公差太大,而不是视觉定位算法的问题。

圆形定位做机械臂抓取时,除了圆心坐标,还有一个极其容易踩坑的点:机器人需要的是圆心在机器人基座坐标系里的三维坐标,不是相机视野里的二维坐标。这里面至少要过两次变换:相机坐标系到机械臂工具坐标系的标定(手眼标定),以及标定板平面到机械臂工作平面的对齐。Halcon里有hand_eye_calibration算子供你完成手眼标定,但标定过程涉及机械臂的示教运动,必须预留足够的时间来调试。

6. 从像素坐标到物理坐标的实用场景:Halcon里的实操经验

继续上面的坐标话题,分享一个特别实用的场景,也是圆形查找中最常见的真实需求:视觉引导机器人抓取圆形工件。很多刚接触视觉引导的工程师,以为找准圆心就万事大吉,但实际部署时却出现了各种“看起来算法没问题,但机器人就是抓不准”的怪现象。

这类问题的源头,往往出在坐标变换链路的顺序上。完整的链路是:像素坐标系 -> 图像坐标系 -> 相机坐标系 -> 机械臂基座坐标系。每一环都有对应矩阵,任何一个矩阵没有标定准确,最终抓取位置都偏。

我在项目里常用的做法是:先用Halcon的标定助手(Calibration Assistant)完成相机的内参标定和畸变校正,然后用calibrate_hand_eye完成手眼标定。标定完成后,把输出的齐次变换矩阵保存到文件里:

write_pose (PoseRobotToCam, 'robot_to_cam_pose.dat')

运行时直接读取,然后通过pose_to_hom_mat3d转换成矩阵,再和像素坐标换算出来的点做矩阵乘法,得到机器人能用的三维坐标。

有一个操作习惯建议养成:标定完成后,一定要用一个已知位置的基准点做一次全链路验证,把计算出来的机械臂坐标和实际示教坐标对比一下,偏差在允许范围内再批量使用。这个验证动作能帮你拦截掉大量后期调试的痛苦。

另外,如果在Halcon里处理圆形检测并集成到C#或C++程序里,强烈建议把整个视觉流程封装成一个类,对外只暴露一个“传入图像、返回圆心和半径列表”的接口。内部做到图像预处理、区域分割、轮廓拟合、坐标变换全链路封装。这样后续换相机、换光源、换工件尺寸,你只需要调整参数配置,不需要动主流程代码。这也是工程化落地和实验室跑通Demo的本质区别。

7. 参数与鲁棒性:圆检测里我踩过的那些坑

不管用哪条路线,圆形检测的坑基本都集中在几个地方。我把这些年反复踩过的坑集中列一下,你对照自己的项目排查一遍,能省不少无用功。

坑一:对比度不足导致边缘漂移。这是最常见的。如果你拍出来的圆边缘是一个渐变过渡带,而不是锐利的黑白交界,那么阈值分割时圆的边界就会随阈值变化而内外漂移,导致圆心和半径结果随亮度波动。解决思路有两个:一是光学上调整——换背光、调亮度、加偏振片,让边缘尽量锐利;二是算法上补偿——用亚像素边缘提取,比如edges_sub_pix配合fit_circle_contour_xld,精度能到1/10像素级,比纯像素级检测稳定得多。

坑二:圆心坐标出现系统性的偏移。如果同一个工件放十个位置,检测出的圆心相对工件的真实位置总往同一个方向偏零点几毫米,那基本可以判断是透视投影造成的视差问题。特别是相机没有正对工件平面、有个十几度的倾角时,圆的透视投影会变成椭圆,圆心会偏移。解决办法是尽量调整相机姿态让光轴垂直工件平面,如果机械结构不允许,就老老实实做标定和畸变校正,别想着靠算法扛过去。

坑三:图像里出现多个干扰圆。比如工件边上有个螺丝孔、有个沉头孔,或者背景里有圆形Logo,这些都会干扰Blob分析结果。我的习惯是先面积筛选,再圆度筛选,然后再加一个位置约束:如果知道目标圆大概在哪个区域,直接用reduce_domain把处理范围缩小到ROI内,干扰能少掉九成。

坑四:反光材质表面出现“假圆”。高光金属表面经常会把光源反射成一个亮斑,亮斑边缘看起来就是一个“圆”,而且灰度差还挺明显,阈值分割很容易把它误判成目标圆。这种场景建议改打光方式,用漫射光源或同轴光,减小镜面反射。如果打光没法改,就在算法上增加一个判断特征,比如检测圆内部区域的灰度均值是否符合预期,或者圆边缘的对比度方向是否符合预期(暗背景亮圆 vs 亮背景暗圆)。

坑五:一个圆被阴影切成两半。侧面打光常见的毛病,圆的一半是亮的,另一半沉在阴影里。阈值分割只能抠出半圆,拟合出来的圆心会偏向亮的那半边。这种坑的解法是:闭合运算先补洞,或者用connection之后把同圆的区域通过距离合并起来,再联合拟合。更推荐的做法是打光上做成无影效果,这比任何算法都可靠。

8. 从Halcon到C++/C#/Qt集成:把圆形检测部署到真正的软件里

最后说下工程部署。Halcon的算子再强大,最终也要集成到上位机软件里才能产线跑起来。我见过太多人HDevelop里调得飞起,一接到C#或C++就卡住了,问题几乎全出在三个地方。

第一,运行环境安装。Halcon的运行时(Runtime)必须在目标机器上正确安装,License也要对应配置。开发机上调试用的License是和HDevelop版本绑定的,部署到产线电脑上要用对应的Runtime License,不然程序起来就报错。

第二,图像数据的传递。Halcon里图像是用HObject表示的,从相机SDK拿到的图像数据往往是Byte数组,需要先封装成HObject。C#里的写法大概这样:

HObject image; HOperatorSet.GenImage1(out image, "byte", width, height, pointer);

这里的pointer要指向图像数据的内存首地址。相机SDK拿到的数据格式如果带通道对齐、行对齐之类的padding,要用gen_image1配合字节跳动的方式把数据按Halcon的期望排布好,不然图像就是花的。这一步细节非常多,强烈建议先用一张BMP图片跑通流程,再接相机。

第三,坐标数据回传的精度。Halcon里的HTuple转成C#的double时,要注意用ToString()double.Parse,不要直接强转,避免文化区域设置导致的小数点解析错误。这个看起来很低级,但我真的遇到过同事因为系统区域是德语环境,小数点逗号解析出问题,定位结果差出十万八千里。

Qt集成的话,核心是图像显示控件的接法。Halcon提供了HalconCppQHalconWindow的封装,可以在Qt界面里嵌入Halcon的显示窗口。关键的步骤是:在Qt的窗口初始化时获取Halcon窗口的句柄,然后用HDevWindowStack或是OpenWindow去绑定显示。直接在VS里用面向对象方式封装Halcon、通过类实例调用算子的方式编排任务,代码会清爽很多。

// 初始化窗口 HTuple hv_WindowHandle; OpenWindow(0, 0, width, height, 0, "visible", "", &hv_WindowHandle); // 显示图像 DispObj(image, hv_WindowHandle);

9. 性能优化:几毫秒找完圆是这么做到的

产线节拍是硬指标,尤其是视觉引导场景,经常要求单次检测在100毫秒以内,有些高速装配线甚至要求30毫秒以内。圆形检测虽然不算重算法,但在分辨率和干扰都拉满的时候,性能优化依然是绕不开的话题。

第一,该裁剪时果断裁剪。用reduce_domain把检测区域限制在ROI里,既减少计算量,也减少干扰。比如只需要检测画面中央的一个圆,就别让算法去处理四角的杂物。这一步优化通常能把耗时砍掉一半以上。

gen_rectangle1 (RoiRect, Row1, Col1, Row2, Col2) reduce_domain (GrayImage, RoiRect, ImageReduced)

第二,金字塔参数帮大忙。Halcon很多算子都支持金字塔层数设置,在边缘提取阶段,可以先用低分辨率大尺度找圆的大概位置,再用原分辨率精确定位。这个思想在Halcon里体现为gauss_pyramid配合粗精两级检测。很多时候粗检测加上精检测的两级时间,比单次全分辨率检测还快,因为这个方案大幅降低了候选区域的数量。

第三,多用亚像素轮廓,少用区域操作。区域操作的像素级别计算往往比XLD轮廓计算要重。处理圆时,尽量走sub_pix路线,找边缘轮廓再拟合,不要在大面积Region上做各种形态学处理。

第四,多线程按相机或按工位拆分。如果一套软件同时管两个相机、四个工位,就把每个相机、每个工位的检测流程放到独立线程里。Halcon的算子大部分是线程安全的,只要你不在多线程里共享同一个HObject做写入,就没问题。我用过四个线程同时跑四路圆形检测,CPU占用依旧健康,单路耗时几乎不增加。

10. 写在最后的工程经验

这么多年用Halcon做圆形查找,最大的体会是:这活儿看着容易,做扎实了靠的是对“图像从哪里来、要到哪里去”的完整理解。很多人卡在Halcon的算子细节里出不来,其实算法之外的光学设计、机械结构、坐标标定、工程封装,占了一个视觉项目成败的七成。

我自己最常用的一套组合拳是:优先背光成像,阈值分割粗定位,卡尺法精拟合,通过标定矩阵输出物理坐标,最后把整个链路封装成接口干净的功能类。这套组合在电子元件、五金加工、汽车零部件项目里反复验证过,稳定性和精度都够用。

如果你现在手头就有一张圆形的图,建议先把上面第2节的代码跑通,再去对比第4节的卡尺法精定位,感受一下两种方式的精度差异。只有亲手把圆心坐标从“就这么回事”变成“一直在那里”,你才算真正把Halcon圆形检测这一课学扎实了。

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

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

立即咨询