
做机器视觉这行十有八九都躲不过“找圆”这个需求。小到纽扣电池的极片定位大到风电轴承的滚子分拣圆形检测几乎是视觉引导、尺寸测量、缺陷判别的底层地基。但“用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里做标定的标准流程是用标定板拍十几张不同位姿的图。用find_calib_object算子提取标定板角点。用calibrate_cameras算子解算出相机内参和畸变系数。在运行时用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提供了HalconCpp和QHalconWindow的封装可以在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圆形检测这一课学扎实了。