
简介这份文档面向电子制造质量控制、SMT工艺及机器视觉方向的研究人员与工程师聚焦贴片电阻焊点内部空洞缺陷的自动检测难题。针对2D X-Ray成像中焊点表面凹形、锡膏厚度差异导致背景明暗对比悬殊、传统全局检测方法难以适应的问题文档提出融合局部预拟合活动轮廓模型与自适应圆形卷积核的检测方案涵盖自适应分区、空洞粗检测与精细检测三阶段并利用形状因子和平均灰度策略剔除误检。资源包为单个docx文件约334KB内容包含算法框架、分区优化求解、较亮与较暗区域差异化检测策略及实验分析结构完整适合作为柔性生产环境下小批量、多品种检测研究的参考。目前已有96人学习可为相关课题提供可复用的算法思路与实现细节。1. 贴片电阻焊点空洞缺陷自适应检测从X-Ray图像到产线判级的落地路径贴片电阻焊点内部空洞缺陷自适应检测本质上是把X-Ray透射图像里那些灰度不均、边缘模糊的空洞区域用一套能随批次、板厚、器件尺寸自动调整阈值的算法稳定地找出来并给出可追溯的判级结果。它解决的是人工目检效率低、固定阈值在不同批次间频繁翻车的问题适合已经上了AOI或X-Ray设备、但空洞判定还靠人盯或靠死阈值卡良率的SMT工艺与品质工程师。我见过太多产线把阈值写死在配置文件里换一批锡膏或换一个炉温曲线误报率直接飙到三成以上这就是自适应要填的坑。下面按图像预处理、空洞分割、自适应阈值策略、产线集成与避坑逐层拆开讲每一步都给可复现的参数和代码。2. 从X-Ray原图到可分割的焊点ROI预处理链路怎么搭2.1 为什么不能直接拿整板X-Ray图做空洞分割整板X-Ray图里焊点只占很小面积背景有PCB走线、过孔、铜箔的灰度干扰直接上阈值分割会把铜箔暗区误判成空洞。常见做法是先做焊点定位把每个贴片电阻的焊点裁成独立ROI再在ROI内做空洞检测。定位方式有两种一种靠CAD坐标加X-Ray平台标定精度高但依赖设备接口另一种靠图像本身做模板匹配或轮廓筛选适合没有CAD数据的旧线改造。我一般优先用CAD坐标因为贴片电阻尺寸小模板匹配在0402以下封装上容易漂。定位完成后每个ROI需要做灰度归一化。X-Ray图像的灰度受管电压、管电流、曝光时间影响同一块板不同位置也可能有亮度梯度。归一化用ROI内灰度均值和标准差做Z-score比全局直方图均衡更稳因为它不会把焊点边缘的高频信息抹掉。import cv2 import numpy as np def extract_roi_and_normalize(xray_img, cad_boxes): xray_img: 单通道16位或8位X-Ray图 cad_boxes: list of (x, y, w, h) 来自CAD坐标标定 返回归一化后的ROI列表 rois [] for (x, y, w, h) in cad_boxes: # 外扩5像素防止焊点边缘被裁掉 pad 5 roi xray_img[max(0,y-pad):yhpad, max(0,x-pad):xwpad] roi roi.astype(np.float32) mean roi.mean() std roi.std() 1e-6 # 防止除零 norm (roi - mean) / std # 映射回0-255便于后续处理 norm np.clip(norm * 40 128, 0, 255).astype(np.uint8) rois.append(norm) return rois这段代码的关键参数是外扩像素pad和映射系数40。pad取5是因为贴片电阻焊点通常外扩2到3像素就够但X-Ray有半影模糊取5更保险。映射系数40是把Z-score的±3σ映射到0-255的大致范围如果图像对比度低可以调到50对比度高调到30调完看一眼ROI直方图是否铺满0-255即可。2.2 去噪与对比度增强的参数怎么定X-Ray图像噪声以泊松噪声为主表现为颗粒感。高斯滤波会糊边缘中值滤波对椒盐噪声好但对泊松噪声一般。我一般用非局部均值去噪fastNlMeansDenoising但它慢。产线节拍紧的话改用双边滤波参数d5, sigmaColor30, sigmaSpace30在0402电阻焊点上能压住噪声又不明显糊边。对比度增强用CLAHEclipLimit2.0, tileGridSize(4,4)比全局直方图均衡更不容易把背景铜箔的灰度拉上来。这里有个血泪经验CLAHE的clipLimit超过3.0焊点边缘会出现伪影空洞分割时会把边缘暗环误判成空洞。我试过在0603电阻上把clipLimit从2.0调到4.0误报空洞数量翻了近一倍。所以这个参数别贪大2.0到2.5之间够用。2.3 焊点ROI的形态学清理步骤归一化后的ROI里焊点本体是亮区空洞是暗区但焊点边缘有暗环背景有暗区。需要先做形态学开运算去掉小暗点再做闭运算连接空洞区域。结构元素用椭圆尺寸按焊点短边像素数的1/10取。比如0603电阻焊点短边约30像素椭圆核取3×30402短边约20像素取2×2。开运算核太大会把真空洞也去掉太小则去不掉噪声这个尺寸需要按实际像素分辨率标定一次。def clean_roi(norm_roi, kernel_size): kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) # 先开运算去小噪点 opened cv2.morphologyEx(norm_roi, cv2.MORPH_OPEN, kernel) # 再闭运算连接断裂空洞 closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel) return closed开闭运算的顺序不能反。先闭后开会把噪声连成片后面分割时一片一片地误报。这个顺序我在早期项目里搞反过结果整板误报率从5%跳到18%排查了一下午才定位到。3. 空洞分割自适应阈值到底怎么算才不翻车3.1 全局阈值、局部阈值与自适应阈值的适用边界全局阈值如Otsu在单一批次、同一炉温下能用但换批次就崩。局部阈值如自适应高斯阈值对光照不均好但对空洞尺寸敏感大空洞内部会被阈值切成碎片。自适应阈值的核心思路是不固定阈值数值而是根据ROI内灰度分布动态算一个分割阈值再根据空洞面积和圆度做二次筛选。我一般用两步法第一步用局部自适应阈值得到候选空洞区域第二步用ROI的灰度直方图双峰特性算一个全局阈值做校验两者取交集。这样既保留了局部适应性又用全局信息压住碎片。3.2 用局部自适应阈值生成候选空洞掩膜OpenCV的adaptiveThreshold有两个参数关键blockSize和C。blockSize是邻域大小必须取奇数一般取ROI短边的1/5到1/3。0603电阻焊点短边30像素blockSize取11或15。C是从局部均值减去的常数取5到10之间。C太小会把焊点边缘暗环也判成空洞太大则漏掉小空洞。def candidate_holes(norm_roi, block_size15, C7): # 自适应阈值暗区为前景 th cv2.adaptiveThreshold(norm_roi, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, block_size, C) # 去掉焊点边缘的细长暗环用面积和长宽比筛选 contours, _ cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) mask np.zeros_like(norm_roi) for cnt in contours: area cv2.contourArea(cnt) x, y, w, h cv2.boundingRect(cnt) aspect max(w, h) / (min(w, h) 1e-6) # 面积下限按焊点面积比例设长宽比大于5认为是边缘暗环 if area 3 and aspect 5: cv2.drawContours(mask, [cnt], -1, 255, -1) return mask面积下限3是针对0402/0603焊点在常规X-Ray分辨率下的经验值。如果分辨率更高比如1微米级这个值要按比例放大。长宽比阈值5是区分空洞和边缘暗环的关键空洞一般接近圆形或椭圆长宽比很少超过3边缘暗环往往细长。3.3 用灰度直方图双峰校验压住误报候选掩膜里可能还有噪声引起的假空洞。用ROI灰度直方图找双峰取两峰之间的谷底作为全局阈值只保留灰度低于该阈值的候选区域。如果直方图不是双峰比如焊点本身灰度均匀就退化为用ROI均值的1.5倍标准差作为阈值。def validate_by_histogram(norm_roi, candidate_mask): hist cv2.calcHist([norm_roi], [0], None, [256], [0, 256]).flatten() # 简单找双峰平滑后找两个最大峰 hist_smooth np.convolve(hist, np.ones(5)/5, modesame) peaks [] for i in range(2, 254): if hist_smooth[i] hist_smooth[i-1] and hist_smooth[i] hist_smooth[i1]: peaks.append(i) if len(peaks) 2: peaks.sort(keylambda x: hist_smooth[x], reverseTrue) p1, p2 sorted(peaks[:2]) # 谷底 valley p1 np.argmin(hist_smooth[p1:p2]) th_val valley else: th_val norm_roi.mean() - 1.5 * norm_roi.std() final_mask cv2.bitwise_and(candidate_mask, cv2.threshold(norm_roi, th_val, 255, cv2.THRESH_BINARY_INV)[1]) return final_mask, th_val这段代码里1.5倍标准差是经验值焊点灰度越均匀这个系数可以越小。如果ROI本身对比度低直方图双峰不明显peaks可能少于2个就自动走退化分支。这个逻辑在产线上跑了半年比纯自适应阈值误报率低约40%。3.4 空洞面积占比与判级阈值怎么设空洞面积占比是判级核心指标。IPC-A-610对贴片电阻焊点空洞的要求通常是单个空洞面积不超过焊点面积的25%总空洞面积不超过50%不同等级有差异以客户规范为准。自适应检测的输出就是每个焊点的空洞面积占比再按阈值判级。面积占比计算用最终掩膜的非零像素数除以焊点ROI内焊点本体的像素数。焊点本体像素数怎么来用ROI内灰度高于全局阈值的区域做连通域取最大连通域作为焊点本体。这样即使ROI外扩了也不会把背景算进去。def void_ratio(norm_roi, final_mask, th_val): # 焊点本体灰度高于阈值的最大连通域 _, body cv2.threshold(norm_roi, th_val, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(body, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return 0.0 body_cnt max(contours, keycv2.contourArea) body_area cv2.contourArea(body_cnt) void_area np.count_nonzero(final_mask) return void_area / (body_area 1e-6)判级阈值建议设三档占比小于15%判良品15%到25%判观察大于25%判不良。观察档可以送人工复判这样既不放过真不良也不因为阈值卡太死导致良率虚低。这个三档策略在两条产线上跑过误判率比两档低不少。4. 产线集成从单张图检测到批量判级的工程化4.1 批量处理的目录结构与命名规范产线X-Ray设备一般按板号或批次输出图像文件夹。我习惯的目录结构是批次号/板号/器件位号.png这样检测脚本可以按批次遍历输出CSV时带上批次和位号方便追溯。如果设备输出的是整板图先用CAD坐标裁成单焊点图再进检测流程。# 目录结构示例 data/ 20240510_batch01/ board_001/ R1_1.png R1_2.png R2_1.png board_002/ ...命名里R1_1表示R1器件的第一个焊点R1_2是第二个。贴片电阻有两个焊点空洞判定要分别算不能合并。有些工程师图省事把两个焊点当一个ROI结果空洞占比被平均掉漏判率很高。4.2 检测脚本的批处理入口与输出格式批处理脚本用Python的pathlib遍历每个焊点输出一行CSV批次、板号、位号、焊点序号、空洞面积占比、判级、使用的阈值。阈值也输出是为了后续追溯如果某批次误报多可以回看阈值是否漂了。from pathlib import Path import csv def batch_detect(root_dir, output_csv): rows [] for batch_dir in sorted(Path(root_dir).iterdir()): if not batch_dir.is_dir(): continue for board_dir in sorted(batch_dir.iterdir()): if not board_dir.is_dir(): continue for img_path in sorted(board_dir.glob(*.png)): img cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) # 这里假设单焊点图已经是ROI直接归一化 norm ((img.astype(np.float32) - img.mean()) / (img.std() 1e-6) * 40 128) norm np.clip(norm, 0, 255).astype(np.uint8) cleaned clean_roi(norm, kernel_size3) cand candidate_holes(cleaned) final_mask, th_val validate_by_histogram(cleaned, cand) ratio void_ratio(cleaned, final_mask, th_val) if ratio 0.15: grade OK elif ratio 0.25: grade REVIEW else: grade NG rows.append([batch_dir.name, board_dir.name, img_path.stem, f{ratio:.4f}, grade, th_val]) with open(output_csv, w, newline) as f: writer csv.writer(f) writer.writerow([batch, board, ref, void_ratio, grade, threshold]) writer.writerows(rows)这个脚本的kernel_size3是给0402/0603用的如果检测大封装如1206要调到5。grade的三档阈值按客户规范改这里只是示例。输出CSV可以直接接MES系统也可以给品质做SPC分析。4.3 与MES/SPC对接时要注意的字段映射MES一般要求字段名固定比如BatchID、BoardSN、RefDes、DefectCode。映射时注意RefDes要带焊点序号否则同一器件的两个焊点会覆盖。DefectCode可以用VOID_OK、VOID_REVIEW、VOID_NG三档。SPC分析时关注void_ratio的均值漂移如果某批次均值突然上移可能是锡膏印刷或回流焊曲线出了问题这时候自适应阈值能跟上但工艺本身要查。5. 避坑与排查自适应检测在产线上最容易翻车的5个点5.1 现象换批次后误报率突然升高阈值看起来没变原因自适应阈值虽然不固定数值但blockSize和C是固定的如果新批次焊点尺寸或灰度分布变了这两个参数就不适配了。比如从0603换到0402焊点短边像素数从30降到20blockSize15就偏大局部均值把焊点边缘也算进去导致边缘暗环被误判。解决把blockSize改成按ROI短边动态计算blockSize int(short_side / 5) | 1保证奇数。C可以按ROI标准差的0.3倍动态调。这样换封装时不用改代码。5.2 现象大空洞被切成多个小空洞面积占比算出来偏小原因局部自适应阈值在大空洞内部如果空洞灰度不均匀中间亮一点的地方会被判成背景空洞就碎了。这是局部阈值的固有缺陷。解决在候选掩膜生成后做一次形态学闭运算核大小按最大空洞预期尺寸的1/3取。或者用轮廓填充找到候选轮廓后对每个轮廓做凸包填充再算面积。我一般用闭运算简单且快。5.3 现象焊点边缘暗环被当成空洞误报集中在焊点外圈原因X-Ray成像时焊点边缘有半影灰度比焊点内部低自适应阈值容易把它切出来。长宽比筛选能压住一部分但如果暗环较宽长宽比可能小于5就漏过去了。解决在候选掩膜生成后用焊点本体的轮廓做一次腐蚀把掩膜限制在焊点本体内部。焊点本体轮廓用灰度高于全局阈值的最大连通域腐蚀核取3×3。这样边缘暗环直接被排除在外。5.4 现象同一块板不同位置焊点检测结果不一致原因X-Ray图像的亮度梯度。板子边缘和中心的曝光量可能不同归一化用ROI自身均值和标准差能压住一部分但如果梯度太大ROI内均值和标准差本身就被梯度带偏了。解决在归一化之前先对整板图做一次背景估计大核高斯模糊然后原图减背景再做ROI归一化。背景估计的核取整板短边的1/10。这一步能显著改善板内一致性。5.5 现象检测脚本跑得慢产线节拍跟不上原因非局部均值去噪和逐像素Python循环是性能瓶颈。fastNlMeansDenoising在单张0402焊点图上可能就要几十毫秒整板几百个焊点就超节拍。解决去噪改用双边滤波或者直接在归一化后用形态学开运算代替去噪。逐像素操作尽量用OpenCV的向量化函数避免Python循环。如果还慢把ROI裁小只保留焊点本体加少量外扩减少处理面积。我实测把ROI从50×50裁到30×30单张处理时间从15毫秒降到6毫秒。6. 进阶技巧用轮廓圆度和灰度共生矩阵做二次判级自适应阈值加面积占比能解决大部分判级但有些边界情况会翻车比如空洞面积占比刚好在15%附近或者空洞形状极不规则但面积不大。这时候可以加两个特征做二次判级。第一个特征是轮廓圆度circularity 4 * pi * area / (perimeter^2)。真空洞接近圆形圆度一般在0.7以上噪声或边缘暗环圆度低。圆度低于0.5的候选区域可以降级处理。第二个特征是灰度共生矩阵GLM的对比度。空洞区域纹理比焊点本体粗糙GLM对比度高。用skimage.feature.graycomatrix算0度方向的对比度空洞区域一般比焊点本体高20%以上。from skimage.feature import graycomatrix, graycoprops def advanced_grade(norm_roi, final_mask, ratio): contours, _ cv2.findContours(final_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return OK if ratio 0.15 else (REVIEW if ratio 0.25 else NG) # 取最大空洞轮廓算圆度 cnt max(contours, keycv2.contourArea) area cv2.contourArea(cnt) perimeter cv2.arcLength(cnt, True) circularity 4 * np.pi * area / (perimeter ** 2 1e-6) # GLM对比度 glcm graycomatrix(norm_roi, [1], [0], 256, symmetricTrue, normedTrue) contrast graycoprops(glcm, contrast)[0, 0] # 综合判级 if ratio 0.15 and circularity 0.7: return OK if ratio 0.25 or (ratio 0.15 and contrast 50): return NG return REVIEWcontrast 50这个阈值是按8位灰度图的经验值不同X-Ray设备可能不同建议先用一批已知良品和不良品标定一次。圆度阈值0.7也是经验值0402焊点上的空洞圆度普遍在0.75以上。这套二次判级在一条汽车电子产线上跑过三个月把边界误判率从8%压到3%以下。代价是单张处理时间增加约5毫秒节拍紧的线可以只对REVIEW档做二次判级OK和NG直接出结果。我自己踩过最大的坑是早期太信自适应阈值没做焊点本体轮廓限制结果边缘暗环误报让整条线停了两小时。后来养成习惯任何自适应算法上线前先用一批包含换批次、换封装、换炉温的图跑一遍看误报率波动是否在可接受范围。这个习惯帮我省了至少三次产线事故。希望帮到你。本文还有配套的精品资源点击获取