Halcon焊球检测实战:ball.hdev例程拆解与产线优化指南

发布时间:2026/10/19 13:53:59
Halcon焊球检测实战:ball.hdev例程拆解与产线优化指南
1. 从一张焊球照片说起这个例程到底在解决什么问题球焊也就是业内常说的BGA焊球检测是电子制造里非常典型的一类视觉任务。芯片封装完成后底部会有一排排细小的焊球这些焊球负责把芯片和电路板电气连接起来。焊球如果出现缺失、偏移、直径异常、形状畸变轻则导致接触不良重则整块板子报废。人工目检在几百个焊球的密度下基本不可行所以这类任务天然适合交给机器视觉来做。Halcon自带的ball.hdev例程就是针对球焊检验的一个经典入门案例。它做的事情用一句话概括从一张焊球阵列的灰度图像中把每一个焊球分割出来然后对每个焊球做形态学分析判断它是否合格。这个例程之所以经典是因为它把图像分割—特征提取—缺陷判定这条完整的检测链路用很短的代码串了起来而且用的算子都是Halcon里最基础、最常用的那批。我第一次接触这个例程的时候觉得它太简单了不就是阈值分割加连通域分析吗。但真正把它往实际产线上搬的时候才发现里面藏着不少门道。比如光照不均匀怎么办、焊球粘连怎么拆、阈值怎么自适应、判定标准怎么定这些问题例程本身不会告诉你答案得靠自己一点点调。这篇文章我就把这个例程从头到尾拆一遍把每个算子的意图、参数的含义、以及实际落地时容易踩的坑都讲清楚。适合读这篇的人有两类一类是刚学Halcon想通过一个完整例程理解机器视觉检测流程的新手另一类是有一定基础但想把例程改造成能上产线的实际项目的工程师。不管你是哪一类我都建议你先把Halcon装好打开这个例程一边看代码一边跟着我的思路走效果会比干读文字好得多。2. 拆解ball.hdev的完整检测链路2.1 图像读取与预处理阶段做了什么例程开头第一步是读图。Halcon里读图用read_image这个没什么好说的但要注意图像格式。ball.hdev用的是一张8位灰度图焊球在图像里表现为比背景亮的圆形区域。读进来之后例程通常会先做一步get_image_size拿到图像宽高这个信息在后面做区域裁剪或者坐标换算时会用到。预处理阶段例程一般不会做太复杂的操作因为这张示例图的质量本身比较好。但在实际项目里预处理往往是决定成败的关键一步。常见的手段包括用mean_image做均值滤波去噪用gauss_filter做高斯平滑或者用emphasize做锐化增强边缘。选择哪种取决于你的图像噪声类型和焊球边缘的清晰程度。这里有个经验如果焊球边缘本身就比较锐利不要过度滤波否则会把边缘糊掉反而影响后续的阈值分割。我一般会先用gray_histo看一下灰度分布如果背景和焊球的灰度峰分得很开那基本不需要滤波如果两个峰有重叠才考虑用滤波或者局部阈值来改善。2.2 阈值分割为什么用动态阈值而不是固定阈值这是这个例程里第一个值得深挖的点。例程用的是dyn_threshold也就是动态阈值分割而不是简单的threshold固定阈值。为什么固定阈值的逻辑是灰度值大于某个数的像素就算焊球。但实际图像里光照往往是不均匀的图像中心亮、边缘暗如果用同一个阈值中心区域可能把背景也误判成焊球边缘区域可能把焊球漏掉。动态阈值的思想是先对原图做一次平滑比如用mean_image得到一个背景估计图然后比较原图和背景估计图的差异差异超过某个偏移量的像素才被判为目标。dyn_threshold的典型调用形式是这样的mean_image(Image, ImageMean, 59, 59) dyn_threshold(Image, ImageMean, RegionDyn, 15, light)这里的59, 59是均值滤波的窗口大小15是偏移量light表示提取比背景亮的区域。窗口大小的选择很关键窗口要明显大于焊球的直径这样均值图才能反映背景而不是焊球本身。如果窗口太小均值图会把焊球也平滑进去导致分割失败。偏移量则决定了分割的灵敏度值越小越敏感但噪声也越容易被误判。我实测下来的经验是窗口大小取焊球直径的2到3倍比较稳妥偏移量则要根据图像的对比度来调一般从10开始试逐步加减。2.3 连通域分析与区域筛选的逻辑阈值分割之后得到的是一个可能包含噪声碎片的区域。接下来例程用connection把区域拆成一个个独立的连通域每个连通域理论上对应一个焊球。但实际图像里噪声、反光、灰尘都可能形成小的连通域所以必须做筛选。筛选的依据通常是面积。例程里会用select_shape选择面积在某个范围内的区域。面积太小的基本是噪声面积太大的可能是多个焊球粘连在一起或者是背景误判。这一步的参数设定直接决定了检测的召回率和准确率。connection(RegionDyn, ConnectedRegions) select_shape(ConnectedRegions, SelectedRegions, area, and, 100, 5000)这里的100和5000是面积的下限和上限具体数值要根据你的图像分辨率和焊球实际像素面积来定。我的做法是先不做筛选把所有连通域的面积用area_center算出来打印出来看看分布找到正常焊球面积的集中区间再据此设定上下限。这样比拍脑袋定参数靠谱得多。2.4 焊球质量判定的特征维度筛选出候选焊球之后例程会对每个焊球做进一步的特征分析判断它是否合格。常用的特征维度包括面积反映焊球的大小面积异常说明焊球偏大或偏小。圆度用circularity算子计算值越接近1说明越接近正圆。焊球如果变形圆度会下降。直径可以通过面积反算也可以用smallest_circle得到最小外接圆直径。中心坐标用area_center得到用于判断焊球是否偏移。例程里通常会把这些特征算出来然后和预设的标准做比较。合格与否的判定逻辑本质上就是一组阈值的组合。比如圆度大于0.85、面积在某个区间内、中心偏移小于某个像素数才算合格。这里要提醒一点判定标准不是拍脑袋定的而是要结合工艺要求。比如某个封装标准要求焊球直径公差是正负10%那你的像素判定阈值就要换算成对应的像素范围。换算的时候要考虑镜头的标定系数也就是一个像素对应多少毫米。3. 把例程改成能上产线的检测程序需要补哪些东西3.1 光照不均匀时的动态阈值参数整定例程用的示例图光照比较均匀所以dyn_threshold的参数很好调。但产线上的光源往往做不到完美均匀尤其是环形光源打光时图像中心亮、四周暗是常态。这种情况下mean_image的窗口大小和dyn_threshold的偏移量都需要重新整定。我的做法是分区域整定。先把图像分成几个区域分别看每个区域的灰度分布如果差异很大说明光照不均匀严重。这时候有两个思路一是改用local_threshold或者var_threshold这类局部自适应阈值算子二是先做光照校正比如用illuminate算子或者自己算一个背景图做除法校正把光照拉平之后再统一阈值。illuminate的用法大致是这样illuminate(Image, ImageIlluminated, 30, 30, 0.5)它通过拟合一个低频的光照背景然后把原图除以这个背景达到匀光的效果。参数需要根据实际光照梯度来调梯度大就加大滤波尺寸。3.2 焊球粘连时的分水岭分割方案当两个焊球靠得很近阈值分割后它们会连成一个连通域connection拆不开。这时候面积筛选会把它们当成一个超大区域直接排除掉导致漏检。解决办法是引入分水岭分割。分水岭的思路是先对区域做距离变换得到每个像素到区域边界的距离焊球中心距离最大两个焊球之间的连接处距离最小形成一个山谷。然后在这个距离图上找分水岭就能把粘连的焊球切开。Halcon里对应的算子是distance_transform加watersheds或者更简单地用partition_dynamic按宽度切分。我一般先用distance_transform看距离图的形态如果两个焊球之间的鞍点很明显用分水岭效果就很好。distance_transform(ConnectedRegions, DistanceImage, octagonal, true, 100, 100) watersheds(DistanceImage, Basins, Watersheds)分水岭之后得到的区域可能还需要合并或筛选因为分水岭本身会产生过分割。这一步的调参比较费时间但一旦调好对密集焊球阵列的检测效果提升非常明显。3.3 判定阈值的标定从像素到实际尺寸前面提到判定标准要结合工艺要求这就涉及像素到实际尺寸的换算。换算的前提是相机标定也就是知道一个像素对应多少毫米。标定的方法有很多最简单的是用一个已知尺寸的标准件比如标准量块或者标定板拍一张图量出它在图像里占多少像素然后算比例。假设标定结果是1像素等于0.02毫米工艺要求焊球直径是0.5毫米正负0.05毫米那么换算成像素就是25像素正负2.5像素。你的面积阈值就要根据这个直径范围来反算。面积等于π乘以半径的平方直径25像素对应半径12.5像素面积约490平方像素。上下限就按直径公差反算出来。这一步看起来简单但实际做的时候要注意镜头的畸变。如果用的是广角镜头图像边缘的像素当量和中心不一样直接用一个统一系数换算会有误差。严谨的做法是做畸变校正或者只在图像中心区域做检测。3.4 多焊球批量处理的性能优化一张BGA图像里可能有几百甚至上千个焊球如果对每个焊球都单独调用一堆算子速度会很慢。优化的思路是尽量用Halcon的批量算子也就是一次处理整个区域集合而不是循环处理单个区域。比如select_shape本身就是批量算子一次就能筛选所有区域。area_center也是批量的一次返回所有区域的面积和中心。真正需要循环的往往是那些没有批量版本的算子或者需要根据单个区域做特殊处理的场景。另外一个优化点是减少图像数据的重复拷贝。Halcon的算子大多是原地操作或者生成新对象如果代码里频繁做copy_obj或者concat_obj会拖慢速度。我一般会先规划好数据流尽量让每个中间结果只被消费一次。如果检测节拍要求很高还可以考虑用Halcon的并行处理能力比如par_start开多线程把图像分成几块并行处理。不过并行会带来线程同步的开销是否划算要看具体任务。4. 实际调试中容易踩的几个坑4.1 阈值调好了但换一张图就失效这是新手最常遇到的问题。在示例图上把参数调得完美换一张实际产线的图检测结果一塌糊涂。根本原因是参数过拟合到了示例图的特定光照和对比度上。解决办法是建立参数鲁棒性测试。准备一批覆盖各种工况的图像不同光照、不同批次、不同焊球状态用同一套参数跑看通过率。如果通过率波动很大说明参数不够鲁棒需要引入自适应机制。比如用图像的平均灰度或者对比度来自动调整阈值偏移量而不是写死一个数。4.2 圆度计算对边缘噪声过于敏感circularity算子的计算依赖区域的边缘如果边缘有毛刺或者锯齿圆度值会明显偏低导致合格焊球被误判为不合格。这个问题在低分辨率图像或者边缘对比度不高的情况下特别明显。缓解办法有两个一是分割后先做一次形态学开运算把边缘毛刺去掉二是用smallest_circle或者smallest_rectangle2这类基于拟合的算子来评估形状它们对边缘噪声的敏感度比circularity低。opening_circle(SelectedRegions, RegionOpening, 2.5)开运算的半径要小于焊球半径否则会把小焊球整个腐蚀掉。一般取焊球半径的十分之一左右比较安全。4.3 区域排序不稳定导致结果对不上如果检测结果需要和固定的焊球位置对应比如第几行第几列的焊球就必须对区域做排序。Halcon里用sort_region可以按行或按列排序但排序的稳定性依赖区域中心的计算精度。如果两个焊球中心坐标很接近排序可能会颠倒。更稳妥的做法是用sort_region先按行排再在每一行内按列排形成稳定的二维索引。或者干脆用模板匹配的方式预先定义好每个焊球的参考位置检测时按最近邻匹配这样即使个别焊球缺失也不会影响其他焊球的索引。4.4 内存泄漏与对象释放Halcon的对象图像、区域、轮廓是占用内存的如果在一个长时间运行的检测程序里不断生成新对象而不释放内存会持续增长最终导致程序崩溃。虽然Halcon有自动垃圾回收机制但在循环里频繁创建对象时手动释放仍然是个好习惯。clear_obj(ImageMean) clear_obj(RegionDyn)在每轮循环结束时把不再使用的对象清掉可以显著降低内存峰值。这个习惯在调试阶段可能感觉不到差别但程序跑几个小时之后差别就出来了。5. 从ball.hdev延伸出去还能做哪些检测5.1 焊球缺失与多余检测例程本身主要关注焊球的形状和尺寸但实际产线上焊球缺失和多余也是常见缺陷。缺失检测的思路是预先知道标准焊球阵列的位置检测时看每个预期位置是否有焊球。如果没有就是缺失。多余检测则相反看是否有预期位置之外的焊球出现。实现上可以用一个标准的焊球位置模板和实际检测到的焊球位置做比对。比对可以用最近邻距离也可以用find_shape_model做模板匹配。模板匹配的好处是对位置偏移不敏感但需要提前训练模板。5.2 焊球共面性检测共面性是指所有焊球的底部是否在同一平面上这对BGA封装的焊接质量影响很大。用2D图像很难直接测共面性通常需要3D视觉或者激光位移传感器。但如果用2D图像可以通过焊球的反光模式间接推断比如共面性差的焊球反光区域会偏移或者变形。这个方向已经超出了ball.hdev的范围但如果你在做BGA检测项目共面性迟早要面对。我的建议是如果工艺对共面性要求高直接上3D方案不要试图用2D图像硬凑精度和可靠性都跟不上。5.3 焊球表面缺陷检测焊球表面的氧化、污染、裂纹等缺陷在灰度图上表现为局部灰度异常或者纹理变化。检测这类缺陷阈值分割就不够用了需要用到纹理分析或者频域分析。Halcon里可以用texture_laws做纹理滤波或者用fft_image转到频域看异常频率成分。这类检测的难度比尺寸检测高一个量级因为缺陷的形态千变万化很难用一套固定的规则覆盖。实际项目里通常会结合深度学习来做用Halcon的深度学习模块训练一个分类或者分割网络。不过那是另一个话题了这里不展开。6. 一些调试心得和参数整定的个人经验调这个例程的过程中我最大的体会是不要一上来就追求全自动。先把例程跑通理解每个算子的输入输出然后拿实际图像一步步替换每替换一步就验证一次结果。这样出问题的时候你能快速定位是哪一步引入的。参数整定方面我的习惯是先粗后细。比如dyn_threshold的偏移量先以10为步长从5试到50看哪个区间分割效果最好然后再以1为步长细化。窗口大小也是类似先试几个量级差异明显的值再细化。这样比一上来就微调效率高得多。还有一个技巧是善用Halcon的变量检查窗口。每跑完一步就在变量窗口里看看中间结果长什么样区域是不是符合预期灰度分布是不是合理。很多时候问题在中间结果里就已经暴露了不需要等到最后判定阶段才发现。最后说一个关于代码组织的心得。例程的代码是线性的从头到尾一气呵成。但实际项目里我建议把检测流程拆成几个函数预处理、分割、特征提取、判定、结果输出。每个函数只做一件事输入输出明确。这样不仅便于调试也便于后续替换某个环节的算法比如把阈值分割换成深度学习分割其他部分不用动。焊球检测这个任务看起来简单但要做到高召回、低误报、稳定运行需要抠的细节非常多。ball.hdev是一个很好的起点它给了你一条完整的链路但链路上的每个环节都需要你根据实际场景去打磨。我到现在也不敢说把这个例程吃透了每次遇到新的图像总会有新的问题冒出来。但正是这种不断遇到问题、解决问题的过程才是机器视觉这个方向最有意思的地方。