在机器视觉项目里,找圆、测圆心距是出镜率非常高的需求。不管是PCB板上的定位孔、精密零件上的装配孔,还是手机中框上的螺丝孔位,很多项目最后的判断逻辑就一句话:这几个圆的圆心相距多少,合不合格。Halcon处理这类任务非常成熟,但很多刚上手的朋友会把阈值分割、找圆、测距三步写完就跑,结果一到现场就是各种误检、漏检、坐标漂移。这篇文章把“多圆检测+圆心距离计算”这条链路完整拆开,从图像预处理到轮廓拟合,再到距离计算和结果可视化,把每一步为什么这么做、参数怎么调、坑在哪里都讲清楚,最后附一份可以直接改参数用的完整代码。
1. 先搞清楚需求本质,才知道该选哪条路
1.1 “找多个圆”这个需求背后到底在问什么
很多刚接触Halcon的朋友,上来就问“有没有一个算子能直接找出所有圆”。实话实说,Halcon确实提供了Hough圆变换系列算子,但工程里真正的主力方案从来不是拿一个算子套上去就完事,而是把“找圆”这个需求拆成一系列基础操作的组合。为什么?因为“找圆”听起来是一个动作,本质上其实是四件事:把目标从背景里分离出来、把粘连或靠近的目标区分开、判断哪些目标真的是圆、最后把最精确的圆心坐标和半径算出来。每一件事都有专门的算子和对应的坑。
比如“判断哪些目标真的是圆”,在二值图层面看的是区域形状特征,在轮廓层面看的则是拟合误差。这两种判断的口径是不一样的。区域圆度高不代表轮廓拟合出来就一定准,反过来,轮廓拟合残差小也不代表这个轮廓一定对应一个圆形物体。这种细节,就是工程经验和书本教程之间的差距。
1.2 圆心距离计算的几种业务含义
圆心距离不是算出来就完事,它在不同项目里代表的业务含义完全不同。最常见的是位置判定:两个圆心距离在公差范围内就算OK,超出就NG,比如装配孔间距、铆钉孔位尺寸。第二种是偏移量计算:比如用一个标准圆作基准,另一个圆的圆心相对基准的偏差,就是这个零件的偏移量。第三种稍微隐蔽一点,是通过圆心距离推倒其他参数,像齿轮的中心距、轴承滚珠的分布均匀性,本质上都是在算一组圆心之间的几何关系。
所以在写代码之前,先想清楚一个问题:你要算的距离,是两两之间都算,还是只算相邻的?是固定号位之间算,还是找到基准圆再和其他圆算?这个决定了代码里循环怎么组织,也决定了后续如果检测到的圆数量有变化,程序该怎么处理。这篇博文后面给的完整代码,默认计算所有检出圆的“两两间距”,这是最通用、信息量最大的做法,实际项目里可以根据业务再加判断条件。
1.3 工业现场的干扰因素
实验室里拍的图干干净净,现场可完全不是。我在现场踩过的坑主要有三类。第一类是光照变化,同一个工位早上和下午的环境光不一样,或者照明光源老化,导致同一块区域的灰度值整体偏移;第二类是反光和阴影,金属件表面反光会让圆的一部分变亮甚至过曝,阈值分割时圆就“缺了一块”;第三类是油污和粉尘,这些东西会以斑点、划痕的形式出现在图像里,和真正的圆目标混在一起。
这些干扰因素不是等到出了问题再处理,而是在设计方案的阶段就要考虑进去。比如光源的选择、相机的曝光参数、镜头的工作距离,这些成像端的因素对后续Halcon算法的影响,比算法本身还要大。如果你的图像采集端就没做好,后续用再高级的算法也是事倍功半。
2. Halcon找圆的方案选型,别一上来就写代码
2.1 三条技术路线对比
Halcon里找圆,正经路线有三条。
第一条是“阈值分割+连通域分析+轮廓拟合”,这也是我强烈推荐的主力方案。思路是先通过灰度阈值把目标和背景分开,再用连通域分析把一个个独立区域分离出来,然后根据形状特征筛掉不像圆的区域,最后提取轮廓做最小二乘圆拟合,得到亚像素精度的圆心和半径。这条路线的好处是每个环节都可控、可调试,中间任何一个步骤出问题都能单独查看和修正。
第二条是“Hough圆变换”,对应的算子是hough_circle_trans和hough_circle_find。这个方案的思路是在参数空间里投票找圆,对边缘断裂、部分遮挡有一定抵抗力,不需要目标必须是完整的连通区域。但代价是参数多、计算量大,而且在圆比较多的时候,投票的峰值之间容易互相干扰,容易出现假圆或者圆位置偏移。我一般只在目标圆边缘严重不完整、阈值分割根本得不到好区域的时候才考虑它。
第三条是“亚像素边缘提取+圆弧段筛选”,用edges_sub_pix提取亚像素边缘,再用segment_contours_xld把边缘切成圆弧段,然后对圆弧段做圆拟合。这条路线的精度最高,但实现的复杂度也最高,因为实际图像里边缘弧段非常多,筛选哪些弧段属于同一个圆、哪些是噪声,需要写不少逻辑。适合对精度要求极高、且圆形目标有明确边缘特征的场景,比如镜片、钢球。
2.2 主力方案:为什么是阈值分割+轮廓拟合
重点说说为什么阈值分割这条路在工程里最常用。核心原因是它的“可控性”。阈值分割虽然听起来简单,但每一步的中间结果都是一张图或一个区域,你可以肉眼确认“分割对不对”“筛选有没有误杀”“拟合准不准”。出了问题,一眼就能定位到是哪一步的哪个参数不对。Hough方案就不一样了,参数一多,调起来像蒙着眼睛找开关,而且出错了很难说是哪个参数引起的。
另外,阈值分割+轮廓拟合在速度上也非常有优势。threshold、connection、select_shape这些都是经过高度优化的基础算子,在普通工业PC上处理一张百万像素级别的图像,整个流程跑下来一般也就几毫秒到十几毫秒。再加上Halcon本身支持多线程和GPU加速,完全能满足产线上的节拍要求。
还有一点很实际:阈值分割这条链路的学习曲线比较平缓。新手看中间结果就能理解每一步在干什么,带新人也好带。团队协作的时候,代码里每一步的意图也更容易写清楚,后期维护成本低。
2.3 什么时候该换备选方案
主力方案不是万能的。我遇到过几种必须换方案的情况。第一种是目标和背景的灰度高度接近,怎么调阈值都分不开,这时候要考虑的不是换滤镜,而是重新审视成像端的光源和相机设置。第二种是圆形目标表面有文字、划痕、高光,导致分割出来的区域不是实心的,而是带孔洞或者缺口很多,轮廓拟合时边缘点会被干扰。第三种是图像中有大量和圆目标灰度相近的其他物体,形状筛选怎么调都会误检。
遇到这三种情况,我一般会先尝试在主力方案内部做加强,比如用形态学闭运算填补孔洞、用动态阈值代替固定阈值、增加灰度特征作为筛选条件。只有这些都试过还不行,才考虑上Hough或者亚像素边缘路线。很多新手容易犯的错是一上来就上“高级方法”,其实基础方法被逼到极限之前,往往还有很大的调优空间。
2.4 方案选型速查表
| 场景特征 | 推荐方案 | 关键算子 |
|---|---|---|
| 目标与背景灰度差异明显,圆完整 | 阈值分割+轮廓拟合 | threshold、connection、fit_circle_contour_xld |
| 边缘断裂严重、目标不完整 | Hough圆变换 | hough_circle_trans、hough_circle_find |
| 精度要求极高,边缘清晰 | 亚像素边缘+圆弧拟合 | edges_sub_pix、segment_contours_xld |
| 光照不均、灰度整体漂移 | 动态阈值 | dyn_threshold、local_threshold |
| 圆目标表面有纹理/孔洞 | 形态学增强+轮廓拟合 | fill_up、closing_circle |
这张表是经验总结,不是金科玉律,实际项目里经常是几种方案混着用。比如先用动态阈值把区域分出来,再用亚像素边缘拟合提高精度。方案选型的原则永远只有一个:用最简单、最容易调试的方式满足精度和速度要求。
3. 完整代码:从读取图像到输出圆心距
3.1 代码整体结构
下面这份代码就是我前面说的主力方案的完整实现。设计场景是:一张图像里有多个圆形目标(比背景暗),目标是检测出所有圆并计算两两圆心之间的距离。代码在HDevelop里可以直接跑,图像路径换成你自己的就行。整体分五个阶段:图像读取与预处理、阈值分割与区域筛选、轮廓提取与圆拟合、圆心标记与可视化、圆心距离计算。
* 关闭窗口并清空 dev_close_window () dev_update_window ('off') * 1. 读取图像 read_image (Image, '你的图像路径.png') * 2. 彩色转灰度(如果原图是灰度图,这步可以省略) rgb1_to_gray (Image, GrayImage) * 3. 中值滤波,抑制噪声,同时尽量保留边缘 median_image (GrayImage, ImageMedian, 'circle', 3, 'mirrored') * 4. 阈值分割:目标比背景暗,所以阈值范围取较小值 threshold (ImageMedian, Regions, 0, 120) * 5. 连通域分析,把每个独立目标分开 connection (Regions, ConnectedRegions) * 6. 形状筛选:先按面积筛掉小噪点和太大的干扰物 select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 800, 50000) * 7. 再用圆度筛掉形状不规则的区域 select_shape (SelectedRegions, SelectedRegions, 'circularity', 'and', 0.6, 1.0) * 8. 填充区域内部的孔洞,让轮廓更完整 fill_up (SelectedRegions, RegionFillUp) * 9. 提取区域的亚像素轮廓 gen_contour_region_xld (RegionFillUp, Contours, 'border') * 10. 对每个轮廓拟合圆,得到圆心坐标和半径 fit_circle_contour_xld (Contours, 'algebraic', -1, 0, 0, 2, 0, Row, Column, Radius, StartPhi, EndPhi, PointOrder) * 11. 显示原图 dev_open_window_fit_image (GrayImage, 0, 0, -1, -1) dev_display (GrayImage) * 12. 在图像上绘制检出圆和圆心十字 gen_circle (Circles, Row, Column, Radius) dev_set_color ('green') dev_set_line_width (2) dev_display (Circles) for i := 0 to |Row| - 1 by 1 gen_cross_contour_xld (Cross, Row[i], Column[i], 30, 0.785398) dev_set_color ('blue') dev_display (Cross) endfor * 13. 计算所有检出圆两两之间的圆心距离,并画连线 if (|Row| >= 2) for i := 0 to |Row| - 2 by 1 for j := i + 1 to |Row| - 1 by 1 distance_pp (Row[i], Column[i], Row[j], Column[j], Distance) * 在两点之间画一条连线 gen_contour_polygon_xld (Line, [Row[i], Row[j]], [Column[i], Column[j]]) dev_set_color ('red') dev_display (Line) endfor endfor endif dev_update_window ('on')3.2 图像预处理:滤波这一步别小看
代码里第3步我加了中值滤波。很多人觉得滤波会损失边缘精度,能省就省,但实际工业图像里,尤其是CMOS相机在低照度下的图像,噪声点对后续阈值分割的影响非常大。一个小噪点可能就几百个像素,你后面用面积筛选是能筛掉,但如果噪点恰好在圆的边缘附近,它会让轮廓多一个毛刺,拟合圆的时候就会把圆心带偏几个像素。这种感觉就像量桌子的边长,尺子旁边粘了一粒沙子,量出来的数能准吗?
中值滤波的优势在于,它在抑制椒盐噪声的同时,对边缘的模糊程度比均值滤波小得多。参数选择上,'circle', 3表示3x3圆形邻域,这是兼顾去噪和边缘保留的常见选择。如果你的图像噪声很大,可以调到5,但超过5就要小心边缘被磨圆了,圆形目标的边缘一旦被磨圆,拟合出来的圆半径会偏小。
灰度化那步,rgb1_to_gray只有在输入是彩色图像时才算数。如果读进来就是灰度图,可以直接跳过。判别方法很简单,read_image之后用get_image_type看一下,如果返回'byte'且通道数为1,就说明是单通道灰度图。
3.3 阈值分割与区域筛选:这一段的重点是“知道自己在筛什么”
第4步threshold (ImageMedian, Regions, 0, 120),意思是把灰度在0到120之间的像素都选出来。这个120怎么来的?最直接的办法是用灰度直方图工具看一眼目标和背景的灰度分布区间,取两者之间的谷值作为阈值。如果你的目标是固定背景、固定光源,阈值是可以硬编码的。但如果现场光照有波动,建议在代码里用min_max_gray动态算出目标区域的灰度上界,或者干脆用auto_threshold做自动阈值,后面第6章会讲到动态方案。
第5步connection把连在一起的像素区域拆成一个个独立区域。这一步有个隐藏问题:如果两个圆靠得非常近,像素都连在一起了,connection是分不开的,得用腐蚀或分水岭先分离。这个在第5章的圆粘连问题里细说。
第6步和第7步是两个select_shape。第一个用面积筛掉太小的噪点,第二个用圆度筛掉形状不规则的区域。这里有个实操经验:筛选条件宁可宽松一点,也不要太苛刻。你筛得太狠,确实能把干扰都去掉,但也可能把有缺陷的圆给误杀。比如一个圆上有划痕,圆度会从0.95掉到0.7,你把圆度阈值设成0.8,这个圆就没了。所以先放宽条件把圆的都留下来,后续再通过拟合残差之类的指标做二次确认,是更稳妥的做法。
3.4 轮廓提取与圆拟合:亚像素精度从哪里来
第8步fill_up很关键,它把区域内部的孔洞填掉。为什么要填?因为如果圆的表面印了字符或二维码,阈值分割后会在区域内部留下孔洞。如果不填,提取轮廓的时候,这些内部孔洞也会生成轮廓,拟合圆的时候这些轮廓捣乱,结果自然不对。
第9步gen_contour_region_xld把筛选后的每个区域转换成亚像素轮廓。这里插一句,普通像素级的区域边界是锯齿状的,但XLD轮廓是经过亚像素插值平滑的,这就是后面拟合圆心坐标精度能达到亚像素的基础。
第10步fit_circle_contour_xld是整个流程的核心算子,参数看起来多,但常用参数就前几个。第一个参数Contours,就是上一步提取的轮廓;第二个参数'algebraic'是拟合算法,用代数距离做最小二乘拟合,速度快、绝大多数情况够用;-1表示使用轮廓上的全部点进行拟合;后面的0, 0, 2, 0在不需要严格闭合和裁剪时保持默认就行。输出的Row、Column就是每个拟合圆的圆心行列坐标,Radius是对应的半径,单位是像素。
3.5 圆心距离计算与可视化:看清楚结果比算出结果更重要
代码第13步用了两层循环计算所有检出圆之间的两两距离。distance_pp是Halcon里计算两点欧氏距离的算子,输入两个点的行列坐标,输出距离值。这里要注意,如果后续要把距离值和公差比较判定OK/NG,不能只看一个距离,一定要明确是哪两个圆之间的距离。所以实际项目里,我的习惯是先把所有圆的圆心坐标按位置排序编号,再按编号计算指定距离,比如“1号圆心到2号圆心”,而不是笼统地算所有两两距离。
可视化部分,我用绿色圆环显示检出圆的位置和大小,用蓝色十字标记圆心,用红色连线表示距离测量。这里有个很多人踩过的坑:gen_circle生成的是区域,dev_display显示的时候会覆盖在原图上。如果图像窗口不刷新,多次运行程序会看到图形叠加,看起来像是检测错了。所以代码开头和结尾的dev_update_window ('off'/'on')是有讲究的。dev_update_window ('off')的意思是程序执行过程中不要实时刷新窗口,等全部算完再一次性显示,这样能避免中间结果显示造成闪烁和误解。
4. 参数调整经验:每个阈值背后的逻辑
4.1 threshold阈值:灰度直方图是最好用的参考
阈值分割的核心是选对灰度范围。很多人喜欢盯着代码里的数字猜,其实最科学的办法是打开灰度直方图。在HDevelop里,选中图像变量,菜单栏的“灰度直方图”功能能直接画出灰度分布曲线。正常情况下你会看到两个明显的峰,一个对应背景,一个对应目标,两个峰之间的谷底就是理想阈值。
但如果图像只有一个大峰,没有明显的双峰结构,说明目标和背景的灰度太接近了。这时候硬调阈值没有意义,返回到成像端想办法才是正解。比如加偏振片消除反光、调整光源角度增加目标与背景的对比度。这一点希望各位记住:软件算法解决不了成像端的物理问题,好的图像质量是算法发挥效用的前提。
4.2 select_shape筛选:特征组合比单一特征更稳
select_shape可选的形状特征有几十种,但找圆项目里实用的就那几个:'area'面积、'circularity'圆度、'convexity'凸度、'width'和'height'外接矩形宽高、'outer_radius'外接圆半径。一个比较微妙的知识点:圆度是4*π*面积/周长²算出来的,最大值为1,表示完美的圆。但圆的边缘只要有一点锯齿,周长就会变大,圆度就会明显下降。所以实心圆和边缘光滑的圆,圆度能到0.95以上,但如果圆边缘有毛刺或缺口,掉到0.5也是有可能的。
我建议的筛选策略是按“面积→圆度→辅助特征”的顺序逐级筛选。面积是第一道粗筛,把噪点和明显不是目标的区域先干掉;圆度是第二道,把形状明显不对的去掉;如果这两个还不放心,加一个'convexity'或外接矩形宽高比来兜底。特征组合的核心思路是:每个特征都放宽,但多个特征叠加起来,筛选精度反而更高。
4.3 fit_circle拟合:算法选择和鲁棒性设置
fit_circle_contour_xld的拟合算法选择,影响的是“抗干扰能力”和“精度”的平衡。默认的'algebraic'是纯代数拟合,速度快,但如果有少量离群点(比如轮廓上粘了一点噪声),结果会被拉偏。'geometric'是几何拟合,精度更高,但速度慢一些。'ahuber'和'atukey'是带鲁棒统计思想的代数拟合,它们会自动降低离群点的权重,在轮廓有局部缺陷时表现得非常稳。
实操里的经验是:如果圆轮廓比较干净,用'algebraic';如果轮廓边缘有局部缺陷、毛刺、划痕,优先试'ahuber'。鲁棒拟合的ClippingFactor参数控制离群点剔除的强度,默认值2,调小一点剔除更激进,但要小心别把正常的边缘点也当成离群点了。这个参数我调的时候一般是从2往1.5试,观察拟合出的圆是否稳定跳变。
还有一个细节,Radius范围如果已知,可以直接在圆拟合前用select_shape设置'outer_radius'和'inner_radius'先粗筛一轮,减少后续拟合的干扰。圆的半径单位是像素,做视觉的人要养成一个习惯:像素值换算成物理单位(毫米)需要标定。如果你的项目只需要判断相对位置是否合格,那像素值就够了;如果你的需求是给机械手提供抓取坐标,那必须做相机标定,把像素坐标转换到世界坐标。
5. 现场实战中常见的问题与排查实录
5.1 漏检:圆明明在那里,程序就是找不到
漏检最常见的原因是阈值分割没把圆完整分割出来。表现是圆的一部分和背景融在一起,分割出来的区域有缺口甚至断成两半。排查步骤我一般是这样的:第一步,直接dev_display显示阈值分割后的Regions,肉眼确认圆区域是否完整;第二步,如果区域不完整,看是灰度阈值的问题还是图像本身的问题。在图像上取几个圆的内部点、边缘点、外部点,用get_grayval分别查灰度值,对比一下差异。如果圆内和圆外的灰度值很接近,那就是成像端对比度不够,先调光源,别硬调阈值。
还有一种漏检原因是圆太小,面积筛选阈值设高了。我见过不少新手把'area'设成5000,结果图像上的圆只有3000像素,直接被筛掉了。所以筛选条件一定要对着中间结果核,不要闭着眼睛预估。
5.2 误检:把非圆区域当成了圆
误检的原因主要是图像里有和圆灰度接近、形状也比较规则的干扰物。比如PCB板上的焊盘、文字外框、方形的定位块,这些都可能通过面积和圆度筛选。我的处理办法是在select_shape里加一个外接矩形宽高比的判断,'anisometry'(长短轴比)或'width'/'height'的比值,圆的宽高比应该非常接近1,而细长物体这个值会明显偏离1。
还有一个更稳妥的办法:既然每个圆都配了一个Radius,在输出结果前,检查一下各个圆的半径是否在合理范围内。比如项目里已知圆直径大概是3mm,标定后对应的像素半径在某个区间,超出这个区间的拟合结果直接丢掉。这个方法本质上是用业务的先验知识来兜底,比纯靠图像特征筛选靠谱得多。
5.3 圆心坐标不稳定/抖动:静态场景下重复精度差
如果在同一个图像反复跑程序,圆心每次都不一样,那问题基本出在拟合的输入轮廓上。轮廓不稳定,圆心自然不稳定。怎么查?把每次拟合前的Contours输出看一下,是不是轮廓的位置和形状每次都略有不同。如果是,那就是阈值分割的边缘不稳定。解决办法是:一方面加强平滑,比如前面提的中值滤波、均值滤波,可以有效减少边缘的随机抖动;另一方面可以考虑用鲁棒拟合算法,它对边缘离群点的抗干扰能力更强。
如果图像是动态采集的,每次拍摄目标本身有轻微移动,圆心坐标有变化是正常的。这时候要评估的是“重复精度”,也就是在同一个位置拍很多张,计算每次测出的圆心坐标的标准差,这个值能反映整个测量系统的稳定性。一般要求标准差小于测量公差的十分之一,才算稳定的系统。
5.4 两个圆靠太近:区域粘连导致无法分离
圆与圆之间距离很近,甚至在图像上边缘都贴在一起时,connection会把它俩当成一个区域,后面的拟合自然就错了。网上教程里最常用的方法是形态学开运算,用一个半径适当的圆形结构元素腐蚀再膨胀,把两个粘连的圆先分开。但开运算的问题是,如果两个圆重叠太多,腐蚀到一定程度会把目标本身也削弱掉,并不是万能药。
更精细的解法是分水岭算法。Halcon里的watersheds算子可以基于灰度图做分水岭分割,把灰度图像里的“山谷”找出来,从而分割开灰度上产粘连的目标。实际使用中,我会先对图像做一次距离变换distance_transform,把二值区域变成距离图,再对这个距离图做分水岭,这样对轻微粘连的分割效果很好。不过分水岭对噪声比较敏感,用之前最好先做平滑。
注意:不管用哪种分离方法,分离完之后一定要重新检查区域个数是否等于预期圆数,没对上就直接输出错误信息,别带着错误数据往下走。这种“数据质量门禁”的思想,是工业视觉项目里避免批量不良品流出的一道保险。
6. 进阶扩展:应对更复杂的情况
6.1 光照不均:固定阈值失效时的动态方案
固定阈值的最大敌人是光照波动。同一个工位,早上和下午环境光不同,或者工件位置稍微偏了导致反光区域变化,固定阈值就可能失效。处理方案是用dyn_threshold或local_threshold做动态阈值分割。dyn_threshold的原理是先对原图做一个大尺度的均值滤波,得到一个“背景估计图”,然后把原图和背景图逐像素比较,差异超过一定阈值的像素就是目标。这样一来,即使整体灰度偏移,只要目标和局部背景的相对差异稳定,就能分割出来。
参数方面,dyn_threshold的Offset就是那个“一定阈值”,调大则目标区域变小、抗噪变强但边缘向里缩,调小则目标区域变大、细节更全但噪声更敏感。理论上动态阈值比固定阈值稳,但也不是万能的,如果目标本身的灰度就和背景没有稳定差异,任何阈值方案都白搭。
6.2 批量处理:一个文件夹几百张图怎么跑
实测项目里不可能一张一张处理,都是批量跑。Halcon里用list_files列出文件夹里的所有图片,再用循环逐张处理,代码结构大致是这样的:
list_files ('图片文件夹路径', 'files', ImageFiles) tuple_regexp_select (ImageFiles, '\\.(png|bmp|jpg)$', ImageFiles) Count := |ImageFiles| for Index := 0 to Count - 1 by 1 read_image (Image, ImageFiles[Index]) * 这里是前面写的检测流程 * 把圆心坐标、半径、距离结果保存到文本或变量 endfor注意tuple_regexp_select是筛选特定后缀名的文件,避免把非图片文件也读进来。还有一个实操细节:批量跑的时候,代码里所有需要用户交互的算子比如disp_message的窗口模式最好都关掉,或者固定窗口显示,不然几百张图弹几百次窗口,程序跑得又慢又容易崩。结果一般用write_tuple或fwrite_string输出到文本,方便后续汇总分析。
6.3 从“测出来”到“测得准”:重复精度验证
最后分享一点比较容易被忽视的经验。算法跑通了、结果输出了,这不是终点。一个测量类项目上线之前,必须做重复精度验证。具体做法是:在同一个工件上重复采集至少30张图像,图像之间保持完全不变的状态,然后用程序批量跑这30张图,记录每次测出的圆心坐标和圆心距离,计算这些测量值的标准差。这个标准差就是系统的重复精度。
为什么这一步很重要?因为测量类项目客户关心的不是你算法多漂亮,而是你这个系统能不能稳定地、可靠地区分合格品和不合格品。如果重复精度已经占到了公差的一半,那这个系统基本没法用,因为测量误差本身就太大了。出现这种情况,优先排查成像端的震动、光源闪烁、相机场效应等问题,这些问题解决了,算法端的调整才有意义。
结尾:一些实战碎碎念
写到这里,整个多圆检测和圆心距离计算的流程就完整了。最后分享两个我踩过多次的坑。第一个是处理前一定要用dev_update_window ('off')关掉窗口实时刷新,不然批量跑图像的时候,HDevelop的界面刷新会把CPU占用拉高,导致整个程序执行时间翻倍。第二个是拟合圆之前,一定要把区域内部孔洞填充干净,不然一个圆的内部纹理可能在轮廓拟合时生成十几条额外的短轮廓,整个程序就会把这些短轮廓也当成圆去拟,输出一堆乱七八糟的圆心。
如果你正在做类似的项目,建议先把这份代码原样跑通,然后用你自己的图像去替换,观察每一步的中间结果。相信我,只要你能把每个中间结果都看明白,这个需求你就已经掌握了七八成。剩下的那些边界情况,等你在现场遇到再说,现场踩过的坑,印象永远是文档里学不到的。