做PCB基板视觉检测这行久了,提起“基板瑕疵”四个字,几乎所有上过产线的工程师都会下意识皱眉。板材底色不均匀、铜箔反光、油墨颜色深浅各异,焊盘边上一点点锡膏残留就可能造成后道虚焊,而这些在普通照明下肉眼根本没法稳定判断。所以工业生产里靠得住的方案,基本都是机器视觉。今天要聊的这个项目,就是用C#和Halcon搭一套PCB基板瑕疵检测系统,把多维度缺陷识别和流程化管控整合到一个源码工程里。它解决的是产线上“有没有缺陷、是什么缺陷、该不该拦截”这一连串问题,适合刚准备切入机器视觉的C#开发,也适合已经有视觉基础、想补全上层控制的现场工程师。
1. 整体设计:为什么PCB瑕疵检测系统偏要用C#和Halcon
1.1 技术选型背后的关键原因
很多团队在搭视觉系统时,会先在OpenCV、VisionPro、Halcon之间纠结。我的建议比较直接:如果目标是稳定交付、快速上线,而不是发论文,C#加Halcon是目前性价比很高的组合。Halcon的算子库对工业视觉场景覆盖度极高,模板匹配、亚像素边缘定位、形态学处理、Blob分析都是现成的,参数调整起来也直观;C#则负责Halcon不太擅长的部分,比如窗体交互、数据库读写、网络通信、多线程调度和产线上下料逻辑。两者通过HalconDotNet绑定,相当于让“调度大脑”和“视觉内核”各自干擅长的事。
有人会问,OpenCV免费,为什么还要用Halcon?OpenCV确实免费,但工业现场讲究的是稳定复现和快速调试。Halcon对光照变化、旋转缩放、亚像素精度这些场景做了大量工程化封装,一个算子搞定的事情,OpenCV可能要组合几个函数还要自己调参数。更重要的是,Halcon在开发时可以随时调整流程,同一套算法逻辑可以快速验证,我的经验是,这类项目越早上线、越早发现问题,成本就越低,虽然Halcon有授权费用,但省下的调试时间往往更值钱。
1.2 系统模块怎么拆才不容易乱
PCB基板瑕疵检测不是一个孤立的算法程序,而是一套软硬件协同系统。一套典型的源码工程,从逻辑上可以拆成五个模块:
- 图像采集模块:负责控制相机、触发信号、采图、图像缓存,这块经常要和运动控制卡或PLC配合。
- 视觉检测模块:图像预处理、模板定位、缺陷分割、特征分类,核心算法全部在这里。
- 流程管控模块:状态机、任务队列、结果判定、OK/NG分流、机械手臂或传送带联动。
- 数据管理模块:保存检测图片、缺陷图片、SN码、检测时间,方便追溯和复检。
- 配置调试模块:参数界面、模型选择、阈值调整、日志查看。
我见过不少新手一上来就堆代码,结果采集、算法、控制全揉在一个类里,最后改一个判定阈值都要重新编译。所以源码工程从一开始就要按模块分文件夹,程序集之间通过接口调用,视觉输出结果统一封装成一个类,这样后面扩展新缺陷类型时,根本不需要动控制层。
1.3 多维度缺陷识别的含义
所谓“多维度缺陷识别”,并不是指多装几个相机,而是对同一个PCB基板,在不同区域、不同特征维度上分别做检查。比如一块裸板,要看线路有没有缺口、短路、残铜;一块贴片后的板,要看焊盘有没有少锡、桥连、偏移、缺件、极性反。不同缺陷的特征差异极大,有的是灰度差,有的是几何形状差,有的必须依赖模板匹配。因此系统里不能只有一个“找异常”的函数,而应该是多条视觉流水线,每条流水线负责一类维度的检测,再汇总成最终的判定结果。
2. 多维度缺陷识别:从图像到判定结果
2.1 先搞清楚PCB基板到底要检测哪些瑕疵
在做算法前,一定要和工艺人员确认缺陷清单。不是所有产品都要做同样的检测,不同PCB类型、不同工序,重点完全不一样。常见缺陷大致可以分成四类:
| 缺陷类型 | 典型表现 | 检测思路 |
|---|---|---|
| 线路缺陷 | 断路、短路、残铜、缺口 | 模板对比 + 差影 + 连通域分析 |
| 焊盘缺陷 | 少锡、锡膏桥连、焊盘偏移 | 区域定位 + 灰阶特征分析 |
| 元件缺陷 | 缺件、偏移、极性反 | 模板匹配 + 灰度相关 |
| 表面缺陷 | 划痕、污染、氧化、异色 | 光照校正 + 灰度统计分析 |
注意,基板在裸板阶段和成品阶段,检测重点不一样。裸板更多关注线路完整性和焊盘表面质量,PCBA阶段则重点看元件贴装和锡膏状态。因此在写源码时,建议做成策略模式,不同产品类型对应不同的检测流程,避免一套代码硬套所有板子。
坦率讲,基板瑕疵检测最难的往往不是复杂缺陷,而是“良品看起来也不太一致”。PCB基材的纹理、油墨厚度、铜箔反射率都存在批次波动,如果算法阈值写死,换一批板子就误报满天飞。所以我在设计源码时,检测模块开头一定会加一个光照自适应校正,先用参考图像做灰度归一化,再用动态阈值分析,这个做法后面会详细讲。
2.2 图像预处理与模板定位:先让图像“对齐”
PCB在运动过程中不可能每次都停在同一个位置,角度也会有几度的偏差。如果直接拿当前图和模板图做差影,边缘错位会被误判成缺陷。因此视觉流程的第一步是模板定位,把当前图像通过旋转平移校正到模板相同的位置。
在Halcon里,模板定位通常用形状匹配。我经常用这样的流程:
- 建立模板:选取一块包含Mark点或者特征明显区域的图像,使用create_shape_model算子生成模板。
- 在线匹配:使用find_shape_model在实时图像里找到模板所在的位置和角度。
- 计算变换矩阵:用vector_angle_to_rigid将模板中心和实时图像中心对齐。
- 图像校正:用affine_trans_image把当前图变换到模板坐标系。
参数上,MinScore我一般先用0.5起步,如果现场漏检或定位失败率偏高,就往下调,但最低通常不低于0.3,否则误匹配会很多。角度范围要根据产线上的相机安装偏差和夹具重复精度来设,如果机构稳定性好,AngleExtent给±20度足够,没必要做全360度,因为搜索范围越大,耗时越长。
2.3 缺陷分割:差影法与形态学分析结合
图像对齐之后,最经典的缺陷检测方法是“差影法”,也就是把当前图和标准模板图对应像素做差值,差异大的区域就是疑似缺陷。Halcon里可以用abs_diff_image或者直接对两幅灰度图做差。
但这只是第一步。原始差分图一般包含很多噪声,比如基材纹理差异、光源微弱抖动、板材边缘反光,所以还要继续做灰度阈值分割和形态学处理:
- threshold提取灰度差超过阈值的区域。
- connection把连成一片的像素组合成独立区域。
- select_shape按面积、宽度、高度、圆度等几何特征过滤掉正常纹理和小噪声。
- area_holes、opening_rectangle1等算子进一步处理内部孔洞和边缘毛刺。
我在这里踩过一个很典型的坑:直接用全局阈值去分割差影图,结果基板边缘有一道半月形反光,灰度差甚至比真实缺陷还高,导致误判。后来改成对检测区域先生成ROI,只在焊盘区域、线路区域内部做差影,并且加入面积下限过滤,误报立刻降下来。这个经验很重要:PCB检测的第一原则是“只在该检查的区域检查”,不要全局扫描。
2.4 多维度判定策略:按区域和缺陷权重综合打分
缺陷找出来后,还要判定这个缺陷算不算“NG”。不同区域、不同类型的缺陷,严重程度不一样。我在源码里会维护一个缺陷规则表,大致包含:
- 缺陷类型。
- 缺陷所在ROI编号。
- 面积阈值范围、数量上限。
- 严重等级(致命/主要/次要)。
- 该区域该缺陷是否允许出现。
比如焊盘中央如果出现一个面积大于0.05平方毫米的孤立暗斑,很可能就是少锡,判致命;如果同一个区域出现一条宽度小于0.03毫米的细线,可能是轻微划伤,判次要,允许复判。这样系统输出的不只是OK/NG,而是带缺陷类型、坐标、面积、等级的完整结果,后续在界面上还能用标签高亮显示缺陷位置。
3. 流程化管控程序:C#如何把视觉结果变成产线动作
3.1 状态机设计:让程序知道当前该干什么
工业产线程序最忌讳没有状态管理。相机没触发就去处理图像,或者结果还没出来就发OK信号,都会造成严重问题。我习惯用一个状态枚举来管理整个流程:
public enum WorkState { Idle, // 空闲等待 WaitingForTrg, // 等待外部触发 GrabImage, // 正在抓图 Inspecting, // 视觉处理中 ResultReady, // 结果已产生 Reset, // 复位 Error // 异常停机 }每个状态定义好允许进入下一个状态的条件,比如只有Inspecting执行完成才能进入ResultReady,只有ResultReady状态下才能向PLC发送OK/NG信号。这样程序不会乱跳,调试时也容易定位问题。
在C#里,我会用一个后台线程或者Task持续跑状态机循环,UI线程只负责显示状态和结果,避免界面卡死。
3.2 和PLC通信怎么设计才稳定
流程化管控必须和产线设备联动。通常的做法是PLC给相机发触发信号后,上位机收到采集完成通知,开始视觉处理,处理完把结果写回到PLC,PLC根据结果控制机械臂把不良品推到复判线。
通信方式上,最常见的两种是TCP/IP和串口。TCP适合数据量大的上位机交互,串口简单但抗干扰不如以太网。不管用哪种,我强烈建议加上心跳检测和握手协议,不要只发一个字符串就完事。
举例来说,PLC发给上位机的报文可以这样规划:
帧头(0xAA55) + 命令字 + 数据长度 + 数据区 + CRC校验上位机收到报文后先做CRC校验,再解析命令。发送结果时也一样。这样现场即使有干扰,也不至于误动作。我在调试过程中遇到过一次很玄学的问题:上位机偶尔会收到长度错误的帧,后来排查是网线接头屏蔽层接触不良,加了CRC之后至少能稳定拦截坏帧,不会让产线乱跑。
3.3 数据保存与追溯:每块板都能查得到
PCB检测系统一定要能追溯。我的做法是:检测完成的每一块板,都在数据库里写入一条记录,包含SN码、检测时间、整体判定结果、缺陷列表、对应图片路径。图片不直接存二进制大对象,而是把原图和缩略图存成文件,数据库里只存路径,这样数据库体积不会爆炸,查询速度也快。
对于NG板,我会保存完整的原始图像和检测标注图像,标注图用Halcon的reduce_domain和draw_region功能画出缺陷框。这样后续复判人员打开界面,直接看标注图就知道大概是什么问题,不用重新跑算法。
4. 源码实现关键节点与参数调试记录
4.1 C#调用Halcon的几种姿势
在源码工程里,C#调用Halcon主要有两种方式:一是直接使用HalconDotNet提供的算子封装类,二是在HDevelop里写好脚本,再用HDevEngine加载脚本执行。
第一种方式灵活,适合逻辑复杂、需要和C#代码交互的场景。示意代码如下:
using HalconDotNet; public class PcbInspector : IDisposable { private HTuple _modelId; public PcbInspector(string modelPath) { HOperatorSet.ReadShapeModel(modelPath, out _modelId); } public bool FindPcbAndAlign(HObject image, out HTuple row, out HTuple column, out HTuple angle) { HTuple score; // 参数依次为:待搜索图像、模板ID、起始角度、角度范围、最小分数、匹配个数、最大重叠、亚像素模式、金字塔层数、贪婪度 HOperatorSet.FindShapeModel(image, _modelId, -0.2, 0.4, 0.6, 1, 0.5, "least_squares", 0, 0.9, out row, out column, out angle, out score); return score.Length > 0; } public void Dispose() { if (_modelId != null && _modelId.Length > 0) { HOperatorSet.ClearShapeModel(_modelId); } } }第二种方式适合算法流程相对固定、平时主要通过参数调整的场景。把HDevelop脚本保存成后缀为hdvp的过程文件,程序里用HDevEngine执行,代码更简洁,但调试脚本时还是要在HDevelop里做。我个人的习惯是,核心算法先放到HDevelop里跑通,导出成C#代码或者脚本,外面的流程控制再用C#写,这样两边都清晰。
4.2 二值化阈值和动态阈值的实际选择
缺陷分割的阈值选择是调试里最花时间的环节。PCB基板的反光差异和纹理噪声很容易让固定阈值失效。我常用的做法是先用灰度直方图观察目标缺陷和背景的灰度分布,确定大致的范围,然后再用动态阈值算子local_threshold替代全局固定阈值,它会根据每个像素邻域的灰度均值和标准差自适应确定阈值,对光照不均匀有很强的容忍度。
举个实际的例子:检测焊盘少锡时,正常焊盘区域灰度值大概在180到220之间,少锡区域由于金属光泽消失通常会变暗,灰度掉到120以下。但基板本身某些油墨区域也可能只有110,如果全局定阈值140,会把油墨误判成少锡。把检测范围限定在焊盘ROI,再用动态阈值对比邻域均值,就能明显减少这种问题。
这类参数没有一次性到位的捷径,我一般准备二三十张不同批次的样品图,调完参数后反复跑一遍,统计漏检率和误检率。如果误检率偏高,优先过滤面积较小的孤立点;如果漏检率高,优先降低动态阈值的对比度系数,同时检查模板定位是否稳定。
4.3 内存释放与线程安全:容易踩的大坑
C#调用Halcon时,HObject类型实现了IDisposable,但它不是托管资源,Dispose之后底层native内存才会释放。在循环采图的产线程序里,如果每次检测后不释放HObject,内存会持续增长,跑几个小时甚至一天后直接崩溃。我之前遇到过现场系统连续运行8小时后卡死,打开任务管理器发现内存已经占了9GB,原因就是图像对象没有及时释放。
线程安全也是一个大坑。HalconDotNet的HSystem等静态对象在多线程同时访问时并不安全,不建议多个线程同时调用同一个HDevEngine或者共享同一个HObject。我的做法是每一个工作线程创建自己的视觉处理实例,互不共用。要共享数据,就用ConcurrentQueue或者加锁传递结果对象,而不是直接共享HObject。
private readonly object _resultLock = new object(); private List<DefectInfo> _latestDefects; public void SetResult(List<DefectInfo> defects) { lock (_resultLock) { _latestDefects = defects; } }5. 现场调试避坑与常见问题排查
5.1 误判和漏检到底怎么查
现场调试时,最多的问题就是误判和漏检。误判是“明明没问题,系统说有缺陷”,漏检是“明明有缺陷,系统放过它了”。这两种问题排查思路完全不一样,我整理了一张速查表:
| 现象 | 常见原因 | 处理建议 |
|---|---|---|
| 固定区域频繁误报 | 光源不稳定、探头污染 | 清洁镜头,检查光源是否有频闪,亮度降到临界值 |
| 产品批次换型后误报暴增 | 模板和当前板灰度差异大 | 重新采样建立模板,增加光照归一化步骤 |
| 同一块板复测结果不稳定 | 定位不稳定 | 检查夹具和相机固定,增大重叠度,提高匹配分数 |
| 细线缺陷漏检 | 面积过滤条件太宽 | 降低面积下限,或者加入基于细长比的过滤条件 |
| 焊盘桥连漏检 | 差影法不易发现连接过渡区 | 增加直方图阈值判断或单独跑区域灰度统计 |
| 程序运行越久越卡 | 图像对象未释放 | 检查所有HObject是否Dispose,用内存监控确认 |
少锡和桥连的检测有时候要靠多幅图像配合。比如锡膏桥连,从顶部看可能只是灰度略有变化,但如果用3D激光或者增加一个低角度环形光,就能看到连锡的形状。不过源码方案里如果只能用一个相机,我会尽量在算法里多提取特征,比如焊盘区域内暗区延伸到焊盘边缘的长度和方向,做约束判断。
5.2 现场最容易被忽视的几个细节
一是相机触发。很多人用软触发,靠上位机循环读图,结果节拍一快就漏拍。做产线项目,最好使用硬件触发,相机收到外部信号后开始曝光,上位机只负责等帧,这样稳定性高得多。
二是光源老化问题。LED光源用几个月后亮度会衰减,如果不做补偿,灰度值整体下降,原本正常的板子会开始大面积误判。我的做法是在程序里定期采集一张标准白色参考板的图像,自动统计灰度均值,再对检测图像做比例校正,可以延后光源老化带来的影响。
三是配置文件管理。检测参数不要硬编码在源码里,做成XML或JSON配置文件,根据产品型号加载不同的参数集。我在实际项目里遇到过给A产品调好的参数,切到B产品时忘记切换模板,结果一套算法把B产品的好板全部判NG。改成配置化之后,换型时直接选择产品型号,模板、阈值、ROI全部自动加载,这个问题就不会出现。
5.3 经验小结:这套系统后续还能怎么扩展
做了一段时间的PCB基板检测后,我发现很多瑕疵单靠传统图像处理是有天花板的,比如基板纹路本身的随机性、颜色渐变的油墨字符,都会让差影法表现不稳。现在比较流行的思路是传统视觉结果作为触发初筛,再用深度学习做二次分类,把初筛出来的可疑区域图片截取出来,训练一个小型分类网络,比如用Halcon的Anomaly Detection或接口调用深度学习模型,误检还能再降一个量级。
不过,我也要泼一点冷水:深度学习不是万能药,它需要大量标注数据,而且换产品后大概率需要重新收集样本。如果产线产品单一、节拍要求高,传统模板匹配加差影法反而是最可控的。上不上深度学习,要等运行数据积累到一定数量后再判断,不要一开始就把系统做得太复杂。
我个人调试这类系统最大的一个体会是,算法只是整个系统的一半,另一半是现场对“稳定”二字的理解。再精妙的检测逻辑,光源抖动一下、焦距偏一点、板子放斜一点,都会让结果失效。所以源码工程里的模块划分、参数配置、日志记录这些看起来不起眼的部分,恰恰是保证系统长期平稳运行的关键。先把基础流程做成规范,再慢慢优化算法,这条路走起来最稳。