ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MaskRCNN+UNet双模型芯片表面缺陷检测:像素级分割实战

MaskRCNN+UNet双模型芯片表面缺陷检测:像素级分割实战 简介面向半导体制造与质量控制领域的工程技术人员该资源提供了一套基于Mask RCNN与UNet的芯片表面缺陷检测算法实现支持bump、dent、dot三类缺陷的像素级识别与分割主要用于替代传统人工目检提升产线检测效率与精度适合有一定深度学习基础并希望快速搭建检测方案的开发者和研究者。压缩包共含73个文件大小约4.03MB以Python脚本、Jupyter Notebook、CSV数据文件及pyc编译文件为主另有XML配置与ReadMe说明py文件承载模型定义、数据加载与训练流程ipynb用于交互式调试和结果可视化分析目录结构清晰便于按模块查阅。目前已有119人浏览学习。项目覆盖从数据预处理、模型构建与训练到结果评估与应用部署的完整流程MRCNN与UNet两套实现相互对照配套的notebook便于复现与二次开发能帮助读者理解实例分割与语义分割在工业视觉检测中的落地思路并掌握缺陷数据组织、模型参数调优与评估指标选择等关键工程细节。1. 芯片表面缺陷检测为什么把 MaskRCNN 和 UNet 叠起来用在芯片封装基板的 AOI 工位上最让视觉算法头疼的是 bump 表面镜面反射产生的假缺陷。传统灰度阈值一遍扫过去高光区全部报异常复判员一天要手动点掉几千个误检。这个标题给出的路线是用 MaskRCNN 和 UNet 双模型做像素级缺陷检测——UNet 先把疑似缺陷区域整体分割出来做语义粗筛MaskRCNN 再对每个缺陷实例做边界精化和类型判断最终输出 bump、dent、dot 三种缺陷的像素级掩码。方案适合正在被样本不平衡和边界精度折磨的 AOI 算法工程师也适合想从目标检测切到像素级分割的视觉团队。下面直接讲两条网络怎么分工、参数怎么配以及五个容易翻车的坑。2. 三类缺陷的标注口径与数据准备bump、dent、dot 怎么标才不算白标做像素级缺陷检测标注质量直接决定模型输出上界。很多人上来就用矩形框标训练完再做后处理切割结果边界永远对不齐。芯片表面缺陷的难点不在“能不能看到”而在缺陷和刻痕、划伤、氧化区域在灰度上高度相似标注口径不一致模型学到的就是噪声。2.1 成像差异决定了标注边界先说三种缺陷在图像上长什么样。bump 是焊球或凸点典型尺寸在 30 到 120 像素之间成像时中央高光、边缘带暗环真正的缺陷边界是暗环外缘而不是亮球本身。dent 是凹陷压痕尺寸约 15 到 80 像素整体比周围暗有阴影渐变边界常常是一圈模糊的灰度过渡带。dot 是异物颗粒或污点尺寸最小常见只有 3 到 30 像素灰度突变明显但容易被当作噪声滤掉。缺陷类型典型尺寸像素成像特征标注口径bump 凸点/焊球30 - 120中央高光边缘暗环标到暗环外缘不标高光球体dent 凹陷/压痕15 - 80整体偏暗有阴影渐变标完整凹陷畸变区域含阴影带dot 异物/颗粒3 - 30灰度突变与背景色差大按灰度突变轮廓小点外扩 1 - 2 像素标注工具推荐用 polygon 模式不要用矩形框。bump 和 dent 如果用矩形框标MaskRCNN 的掩码分支会强行拟合出一个矩形边界精度直接报废。dot 这类小点polygon 标两点或四点就行但外扩比例要固定否则模型会学出两种不一致的“真实边界”。2.2 采集、ROI 裁剪与切块喂给网络前先做的事数据采集阶段就要固定相机高度和光源角度。芯片表面是镜面加漫反射的混合体光源角度差 5 度同一个 bump 的暗环宽度能差出 3 个像素这对像素级分割是致命的。产线上通常先做 chip 定位把每颗芯片的 ROI 裁出来再切块送入网络。直接送整张晶圆图会导致两类问题一是缺陷占比太小损失函数被背景主导二是 GPU 显存撑不住高分辨率输入。import numpy as np def crop_chip_roi(image, chip_box, pad_ratio0.08): x0, y0, x1, y1 chip_box pad_x int((x1 - x0) * pad_ratio) pad_y int((y1 - y0) * pad_ratio) x0, y0 max(0, x0 - pad_x), max(0, y0 - pad_y) x1, y1 min(image.shape[1], x1 pad_x), min(image.shape[0], y1 pad_y) return image[y0:y1, x0:x1] def make_tiles(image, tile_size512, stride256): h, w image.shape[:2] tiles, boxes [], [] for y in range(0, h - tile_size 1, stride): for x in range(0, w - tile_size 1, stride): tiles.append(image[y:y tile_size, x:x tile_size]) boxes.append((x, y, x tile_size, y tile_size)) return tiles, boxespad_ratio 取 0.08 是因为缺陷通常不会正正好好落在芯片边缘留出外围上下文才能让 UNet 的编码器看到足够大的感受野。stride 取 256与 tile_size 512 形成一半的 overlap目的是避免缺陷恰好被切在 tile 边缘。推理时重叠区域的预测结果需要做均值融合或最大置信度融合这一步对边界稳定性非常重要。2.3 类别不平衡与小缺陷增强别让 dot 淹掉 bump产线上 dot 的出现频率往往是 bump 的 5 倍以上直接按文件随机采样一个 batch 里可能全是 dot。常见做法是按类别过采样bump 和 dent 的样本在训练集中至少占总样本的四分之一不能满足时用复制粘贴的方式合成缺陷样本。把无缺陷芯片图像当背景把一张图中标注好的 bump 区域切出来做随机旋转、缩放、亮度扰动后贴上去同时更新对应掩码。这种合成增强需要注意光源方向要一致否则模型会把不真实的高光当成特征。增强参数上我一般用轻度旋转正负 15 度、加性高斯噪声scale 0 到 8、乘法亮度扰动0.85 到 1.15。务必不要开 ElasticTransform 这类形变增强焊点形状被扭曲后标注边界就失去了物理意义。亮度扰动幅度控制在正负 15% 以内超过这个范围镜面反射区域会被过度增强UNet 输出概率图会变得非常碎。3. UNet 分支怎么搭语义粗分割与低阈值召回标题里把 MaskRCNN 放在 UNet 前面但实际落地时两条网络是串行配合的。MaskRCNN 做实例分割没问题但单独用在小缺陷密集的芯片表面上RPN 容易漏检尤其是 dot 这种十几个像素的小目标。UNet 的作用不是最终输出而是先用低阈值把“疑似区域”全部圈出来给 MaskRCNN 的 RPN 一个先验范围。3.1 为什么让 UNet 先“圈地”UNet 的结构优势是 U 形跳连编码器从高分辨率开始逐级下采样解码器再逐级上采样并融合各层特征。这个特点对小目标特别友好小缺陷的语义信息在深层位置信息在浅层跳连能把两路拼在一起。相比直接用 MaskRCNN 在整张图上让 RPN 搜区域UNet 先出一个概率图相当于告诉 RPN“缺陷大概率分布在这几个区域”候选区域数量可以减少一半以上误检也随之下降。这里要纠正一个常见误用有人把 UNet 当成最终分割网络追求输出边界和实际缺陷严丝合缝。UNet 的输出边界天生偏平滑达不到像素级精度。所以训练 UNet 时不要把阈值定在 0.50.3 甚至 0.25 都可以目的是高召回漏掉一个不该漏的区域比多报一个区域更麻烦。3.2 编码器选择与注意力改进编码器用预训练 ResNet34 就够不需要上 EfficientNet-B7 这类重网络。芯片切块后的图像尺寸是 512 见方缺陷尺寸又小重网络深层特征会丢失太多细节同时推理速度也扛不住产线节拍。解码器通道数取 256最后一层输出三个二值通道分别对应 bump、dent、dot背景由“三个通道都低于阈值”表达。我给 UNet 加了一个轻量改进在解码器最后一层之前接 CBAM 注意力模块。CBAM 先做通道注意力再把空间注意力作用在通道加权后的特征上能有效抑制镜面反射区域在特征图上的高响应。加了 CBAM 之后UNet 在 dent 这类弱对比缺陷上的召回率提升最明显原因是 dent 的灰度差异不大但空间位置信息有规律注意力机制能放大这一部分响应。import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, channels, reduction8): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.fc nn.Sequential( nn.Conv2d(channels, channels // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1), ) self.spatial nn.Conv2d(2, 1, kernel_size7, padding3) def forward(self, x): b, c, h, w x.size() avg self.fc(self.avg_pool(x)).expand_as(x) mx self.fc(self.max_pool(x)).expand_as(x) attn torch.sigmoid(avg mx) x x * attn cat torch.cat([x.mean(dim1, keepdimTrue), x.max(dim1, keepdimTrue).values], dim1) return x * torch.sigmoid(self.spatial(cat))CBAM 的 reduction 取 8空间注意力卷积核取 7这两个是改动成本低、效果容易复现的参数。通道数太少时 reduction 拉大反而损失表征能力512 通道情况下 8 是安全起点。3.3 损失函数与训练参数Dice 和 Focal 各管一段芯片缺陷分割的类别不平衡非常严重向背景类的像素占比经常超过 99%。单独用 BCE 会让模型倾向于把所有像素都预测为背景召回率极低。我一般用 Dice Loss 加 Focal Loss 的组合Dice 管整体区域重叠Focal 管难分像素尤其是 dot 这种小目标。class DiceFocalLoss(nn.Module): def __init__(self, alpha0.7, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): pred pred.clamp(1e-6, 1 - 1e-6) inter (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) dice 1 - (2 * inter 1) / (union 1) focal -(target * (1 - pred) ** self.gamma * pred.log()).mean(dim(2, 3)) return 0.6 * dice.mean() 0.4 * focal.mean()Focal 的 gamma 设 2.0alpha 设 0.7给少数类更高的权重。pred 和 target 都是 B×3×H×W 的多通道格式分别对应 bump、dent、dot 三个类别背景不参与 loss 计算。整个组合的意义是Dice 保证预测区域和标注区域的重叠度Focal 保证少数类不被背景淹没。训练时 batch size 8初始学习率 1e-3AdamW 优化器60 个 epoch每 10 个 epoch 验证一次。BN 层在小 batch 下统计量容易漂移如果 batch size 只能开到 2 或 4就把编码器里的 BN 换成 GroupNorm别硬扛。4. MaskRCNN 分支与两路掩码融合像素级结果怎么落UNet 给了疑似区域但分不清哪些区域属于同一个缺陷也分不清相邻 bump 的具体边界。这一步交给 MaskRCNN。芯片表面的 bump 经常成排出现彼此间距只有几个像素目标检测框会大量重叠只有用 MaskRCNN 的实例分割才能把每个焊球单独拎出来。4.1 MaskRCNN 在芯片场景里不负责“发现”只负责“分实例”MaskRCNN 的 RPN 仍然会在整图上滑窗但因为 UNet 概率图已经提前标记了疑似区域实际推理时可以做一个加速把 UNet 概率图 binarize 后取连通域的外接矩形作为 RPN 的 proposal 候选框输入。这样 RPN 不需要从零开始搜误检数量明显下降。MaskRCNN 真正干的重活是 mask head——在每个候选框内做逐像素分类输出每个实例的轮廓。训练 MaskRCNN 时 backone 用 ResNet50FPNFPN 的多尺度特征对芯片场景特别重要。bump 和 dent 尺寸跨度有 4 倍FPN 把不同层级的特征都保留下来小目标不至于在高层特征图上消失。这里有个参数要注意ROI Align 输出的 mask 尺寸默认是 28×28对 dot 这种 10 像素级别的小目标28×28 的输出太粗糙建议在配置里显式调大到 56×56。4.2 微调策略与关键参数MaskRCNN 不建议从头训练直接用 COCO 预训练权重微调。芯片表面图像和自然图像差异不小但预训练模型学到的边缘纹理基础特征仍然有效。微调分两步走先冻结 backbone只训练 RPN 和两个 head跑 20 个 epoch 左右然后解冻所有层用更低的学习率联合训练。参数推荐值说明输入尺寸768×768可和 UNet 不同大图保小目标细节ROI Align 尺寸56×56提升 dot 掩码精度anchor scales[8, 16, 32, 64, 128]覆盖 3 像素到 120 像素缺陷NMS 阈值0.3防止相邻 bump 框合并检测置信度阈值0.5低于此值判为弱候选image_per_batch1768×768 输入下显存紧张anchor scales 上限取 128 而不是更大是因为切块后的图像里单个缺陷不会超过图像尺寸的三分之一。NMS 阈值取 0.3 比默认的 0.5 更严格原因是 bump 之间的框重叠率极高宽松 NMS 会把两个焊球框成一个实例。训练时如果显存不够保持 image_per_batch1加上梯度累积效果等同。4.3 推理融合与后处理低阈值 UNet 高阈值 MaskRCNN推理时两条网络各出一份掩码直接取交集会漏掉很多像素直接取并集又会有碎片。我用的策略是“UNet 低阈值保召回MaskRCNN 高阈值保精度”然后按下面的规则融合。import cv2 import numpy as np def fuse_masks(unet_prob, rcnn_mask, thr_u0.3, thr_r0.5, min_area20): u_bin (unet_prob thr_u).astype(np.uint8) r_bin (rcnn_mask thr_r).astype(np.uint8) only_u cv2.bitwise_and(u_bin, cv2.bitwise_not(r_bin)) only_r cv2.bitwise_and(r_bin, cv2.bitwise_not(u_bin)) final cv2.bitwise_or(r_bin, only_u) num, labels, stats, _ cv2.connectedComponentsWithStats(final, connectivity8) keep np.where(stats[1:, cv2.CC_STAT_AREA] min_area)[0] 1 result np.zeros_like(labels, dtypenp.uint8) for idx in keep: result[labels idx] 1 return result, only_u, only_r融合规则的核心是MaskRCNN 标出的实例区域全部保留UNet 独有区域也保留但 UNet 独有区域会被标记为弱置信候选后续复判工具里用不同颜色显示。这样做的好处是——bump 边缘的暗环有时会被 MaskRCNN 的 mask head 裁掉UNet 能补回来而 UNet 过平滑漏掉的实例MaskRCNN 能兜住。min_area 设 20 像素小于这个面积的连通域大概率是镜面反射的碎片噪声。后处理输出用单通道 uint8 掩码存储1 表示 bump2 表示 dent3 表示 dot0 表示无缺陷。实际工程里可以按类各做一次融合最后拼成一张标签图。存储时用 PNG缺陷区域直接 ROI 编码避免整张高分辨率图反复读写拖慢产线节拍。5. 芯片表面缺陷检测的五个翻车点现象、原因与排查双模型方案最怕的就是“离线指标好看上线就翻车”。以下五条是芯片缺陷检测项目里最常踩的坑每一条都是现象、原因、解决三个步骤排下来的。5.1 高光反射把 bump 误报成缺陷现象UNet 和 MaskRCNN 在无缺陷芯片上大量报 bump特别是芯片中央的镜面区域报检率高达 30% 以上。原因bump 是中央高光加暗环镜面反射区域也有高光灰度特征高度相似单靠图像内容分不开。解决采集端加偏振片消除镜面反射这是最有效的做法算法端对图像做局部灰度归一化把每个 chip 的亮度直方图对齐到固定范围不要让模型靠绝对亮度判断缺陷。我见过不加偏振片只靠算法硬扛的项目换了三条产线怎么调阈值都压不住误检。5.2 UNet 低置信度区域抖动现象同一个缺陷在不同批次的推理结果里像素级边界忽大忽小有时多出 10 个像素有时少 10 个像素。原因训练时增强的随机性太大特别是亮度扰动幅度过猛导致 UNet 对边界附近像素的置信度不稳定。解决把亮度扰动乘法因子控制在 0.85 到 1.15不要开到 0.5 到 1.5训练时固定验证集和随机种子让每个 epoch 的验证结果可比再加 EMA 权重平滑用滑动平均后的权重做推理。EMA 的 decay 取 0.99 即可边界抖动能明显缓解。5.3 MaskRCNN 把相邻 bump 合并成一个实例现象两个间距 5 到 10 像素的 bumpMaskRCNN 输出一个相连的掩码复判时被当作一个大缺陷。原因NMS 阈值太高相邻候选框被合并另一个可能是 mask head 输出的边界过度膨胀两个实例的掩码天然接在一起。解决NMS 阈值从默认 0.5 降到 0.3增加相邻实例的抑制对 mask head 的 loss 加一个惩罚项两个实例掩码的重叠率超过 0.2 时额外加 loss。做好这一步成排 bump 的实例数才能和人工复判对上。5.4 小于十个像素的 dot 漏检现象dot 尺寸在 3 到 8 像素之间时MaskRCNN 几乎全部漏检UNet 偶尔能框出来但边界完全不可用。原因特征图经过 FPN 多次下采样后小缺陷的响应被相邻像素“稀释”ROI Align 在 28×28 输出下也几乎没有有效像素。解决把输入分辨率从 512 提到 768ROI Align 输出调到 56×56再给 FPN 增加一个更高分辨率的 P2 层。代价是显存和推理时间但 dot 的召回率通常能从 50% 涨到 80% 以上。5.5 离线验证好但换产线失效现象测试集 mIoU 有 0.6模型部署到另一条产线后误检率直接翻倍。原因两条产线的相机型号、光源角度、chip 在画面中的位置都不一致模型学到的其实是上一套采集环境的分布。解决做跨产线验证用产线 B 的 200 张图做盲测不参与任何训练上线前把采图参数统一包括相机增益、曝光时间、光源亮度甚至 ROI 裁剪的 pad 比例。像素级模型对采集一致性非常敏感这一点比检测框模型严重得多。6. 从验证到上线的最后一环像素级指标与模型迭代上线前要同时看两套指标缺一不可。第一套是像素级 mIoU直接度量掩码和标注的逐像素重叠程度公式是每个类别的交并比取平均。第二套是实例级 mAP0.5判断“是不是把每个缺陷作为一个实例找出来了”。很多项目只盯 mAP结果像素边界对不齐产线复判工具根本没法用。def compute_miou(gt, pred, num_classes3): miou 0.0 for c in range(1, num_classes 1): inter ((gt c) (pred c)).sum() union ((gt c) | (pred c)).sum() miou inter / (union 1e-6) return miou / num_classes我给验收定的底线是像素级 mIoU 大于 0.55实例级 mAP0.5 大于 0.4单颗芯片总推理时间在 80ms 以内GPU 条件下。低于这个线产线复判压力会非常大。进阶验证可以加一个轮廓距离指标用 Hausdorff 95% 分位数看预测边界和标注边界的最坏偏差。这个指标对像素级项目比 mIoU 更直观偏差小于 3 个像素才算拖焊点边缘这类精细结构也能用。模型上线后要建一个滚动标注循环每周从产线误检和漏检里抽 100 张人工修正后混入训练集。我第一个版本只盯着 mAP 调参上线两周后被产线退回原因是边界精度不达标。后来把像素级指标纳入验收问题才真正暴露出来。芯片表面缺陷检测的难点从来不是跑通模型而是把边界精度、小目标召回和产线一致性同时守住。希望帮到你。本文还有配套的精品资源点击获取
返回列表