ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

芯片表面缺陷检测:Mask R-CNN与UNet像素级分割实战

芯片表面缺陷检测:Mask R-CNN与UNet像素级分割实战 简介本资源面向半导体质量控制与深度学习缺陷检测方向的研究者及工程师提供一套基于Mask RCNN与UNet融合的芯片表面缺陷检测完整项目实战。算法可对bump凸起、dent凹陷、dot点状等缺陷实现像素级实例分割与精确定位兼顾检测精度与处理速度适配工业实时性要求。压缩包共73个文件约4.03MB以22个py源码、21个pyc编译文件、12个csv数据表、9个ipynb实验笔记及3个xml配置为主涵盖数据预处理、模型训练、结果评估与应用部署等关键环节目录按UNet、mrcnn、chips等模块清晰组织。已有118人学习关注。读者可获取完整可复现的算法实现、训练与评估脚本、可视化分析笔记及项目结构参考便于快速理解双模型融合思路并迁移至自身检测任务对推动智能制造与工业4.0实践具有参考价值。1. 芯片表面缺陷检测为什么难从 Mask R-CNN 加 UNet 的像素级方案说起芯片表面的 bump、dent、dot 这三类缺陷做过的都知道难的不是「有没有缺陷」而是「缺陷到底占几个像素、边界在哪」。bump 是凸起dent 是凹陷dot 是点状污染尺寸经常只有几个像素到几十个像素在整张 wafer 或 die 图上占比极小。你拿一个普通分类网络去跑它告诉你「这张图有缺陷」但产线要的是「缺陷在第几行第几列、面积多大、属于哪一类」这就把问题从图像分类直接推到了实例分割加语义分割的层面。这也是为什么「Mask R-CNN UNet」这个组合在芯片表面缺陷检测里反复被拿出来用。Mask R-CNN 负责「找到缺陷在哪、是什么类别、给出粗略 mask」UNet 负责「把 mask 边缘修干净、把像素级轮廓抠准」。两者不是简单叠加而是分工一个做实例级定位一个做像素级精修。这套方案适合谁适合手里已经有几百到几千张带标注的芯片表面图、想从「能检出」走到「能测量」的团队。新手能跟着后面的步骤把最小闭环跑通熟手能直接看参数边界和踩坑记录。2. Mask R-CNN 与 UNet 的分工为什么不是二选一2.1 两类缺陷检测任务的区别先把任务拆清楚。芯片表面缺陷检测里其实混着两种任务一种是实例分割要求区分「这是第 1 个 dot、那是第 2 个 dot」每个缺陷是独立实例另一种是语义分割只要求区分「这个像素是不是缺陷」不关心它属于哪个实例。bump 和 dent 往往面积较大、边界连续实例分割更合适dot 经常密集出现、互相挨着实例分割容易粘连语义分割反而更稳。Mask R-CNN 本质是 Faster R-CNN 加一个 mask 分支输出的是每个候选框对应的二值 mask天然做实例分割。UNet 是编码器-解码器结构靠跳跃连接把浅层高分辨率特征和深层语义特征拼起来天然做语义分割对细小目标和边界特别敏感。所以常见做法是Mask R-CNN 出实例框和粗 mask把每个实例区域裁出来送进 UNet 做像素级精修或者把 Mask R-CNN 的 mask 当先验UNet 在整图上做语义分割后与实例结果融合。2.2 选型理由与数据前提为什么不直接上一个更强的分割网络因为芯片缺陷数据有两个现实约束样本少、标注贵。像素级标注一张芯片图熟练标注员也要几分钟到十几分钟dot 这种小目标还得放大到像素级去描。Mask R-CNN 用 COCO 预训练权重迁移UNet 用 ImageNet 预训练编码器都能在几百张图上收敛。纯 Transformer 分割方案在小数据上容易过拟合训练成本也高。数据前提要说清楚图像分辨率建议不低于 1024×1024缺陷最小边不少于 3 个像素否则标注本身就不稳定。标注格式用 COCO 的 polygon 或 mask PNG 都行关键是每个缺陷实例要有独立 id。如果只有语义 mask 没有实例 idMask R-CNN 那一路就得先做连通域拆分这一步后面避坑章会讲。2.3 整体流程的最小闭环整个流程分四步数据准备与标注转换、Mask R-CNN 训练出实例检测、UNet 训练做像素精修、推理时融合两路输出并做后处理测量。下面先给一个数据转换的最小脚本把 COCO 标注转成 UNet 能吃的 mask 图。import json import numpy as np from PIL import Image, ImageDraw def coco_to_unet_masks(coco_json, out_dir, num_classes4): # num_classes: 0 背景, 1 bump, 2 dent, 3 dot with open(coco_json, r) as f: data json.load(f) img_id_to_info {img[id]: img for img in data[images]} # 每张图生成一张单通道 mask像素值即类别 for img_id, info in img_id_to_info.items(): h, w info[height], info[width] mask Image.new(L, (w, h), 0) draw ImageDraw.Draw(mask) for ann in data[annotations]: if ann[image_id] ! img_id: continue cat ann[category_id] # 需与 num_classes 对齐 for seg in ann[segmentation]: pts [(seg[i], seg[i1]) for i in range(0, len(seg), 2)] draw.polygon(pts, fillcat) mask.save(f{out_dir}/{img_id:06d}.png) coco_to_unet_masks(annotations.json, ./masks, num_classes4)这段脚本的逻辑是读 COCO json按 image_id 分组把每个 polygon 用ImageDraw.polygon填成对应类别值背景保持 0。参数上num_classes必须和你的类别定义严格一致category_id如果从 1 开始正好对应 1/2/3如果从 0 开始要加偏移。注意 polygon 坐标是浮点draw.polygon会做取整小目标上可能丢一两个像素dot 类缺陷建议改用 mask PNG 直接读别走 polygon。3. 用 Mask R-CNN 跑通实例检测配置、训练与调参3.1 环境与骨干网络选择环境上PyTorch 1.10 以上加 torchvision 的 detection 模块就够不需要额外装 detectron2除非你要用它的高级数据增强。骨干网络我一般选 ResNet-50 FPN芯片图纹理重复度高ResNet-50 够用ResNet-101 在小数据上反而更容易过拟合。输入尺寸按短边 800、长边不超过 1333 来这是 torchvision 的默认芯片图如果本身是 2048 以上先缩到 1024 左右再送进去不然显存吃不消。3.2 训练脚本与关键参数下面是一个基于 torchvision 的最小训练循环重点看参数注释。import torch from torchvision.models.detection import maskrcnn_resnet50_fpn from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor def build_model(num_classes4): model maskrcnn_resnet50_fpn(weightsCOCO_V1) # 替换 box 分类头 in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) # 替换 mask 头 in_features_mask model.roi_heads.mask_predictor.conv5_mask.in_channels model.roi_heads.mask_predictor MaskRCNNPredictor(in_features_mask, 256, num_classes) return model model build_model(4).cuda() params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.SGD(params, lr0.005, momentum0.9, weight_decay0.0005) lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) for epoch in range(10): model.train() for images, targets in dataloader: images [img.cuda() for img in images] targets [{k: v.cuda() for k, v in t.items()} for t in targets] loss_dict model(images, targets) losses sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() lr_scheduler.step()逻辑说明weightsCOCO_V1加载预训练权重这是小数据能收敛的关键。FastRCNNPredictor和MaskRCNNPredictor换成自己的类别数注意num_classes要包含背景所以 3 类缺陷写 4。学习率 0.005 是 SGD 在 batch size 2 到 4 时的常用值如果你用 Adam改成 1e-4。StepLR每 3 个 epoch 降一次10 个 epoch 对几百张图基本够。参数上最容易翻车的是 anchor 尺寸。默认 anchor 是 32 到 512芯片 dot 缺陷可能只有 5 个像素默认 anchor 根本覆盖不到。常见做法是改anchor_generator的 sizes加一档 8 或 16同时把rpn_pre_nms_top_n_train从 2000 提到 4000让更多小候选框活下来。3.3 训练监控与早停判断训练时盯三个量loss_classifier、loss_mask、loss_objectness。如果loss_objectness一直不降说明 RPN 没学好多半是 anchor 不匹配或学习率太大。loss_mask降得比loss_classifier慢是正常的mask 分支收敛本来就慢。验证时用 COCO 的 mAP0.5 和 mAP0.75 两个指标芯片缺陷上 mAP0.5 到 0.7 以上算可用mAP0.75 能到 0.5 就说明边界质量不错。如果验证 mAP 连续 3 个 epoch 不涨就停别硬跑小数据上过拟合来得很快。4. UNet 做像素级精修从粗 mask 到边界可测量4.1 UNet 在芯片缺陷上的结构改动标准 UNet 编码器是 5 层下采样芯片缺陷小下采样太深会把 dot 直接采没。我一般把编码器改成 4 层最深层特征图保持 1/8 分辨率而不是 1/32。跳跃连接保留但浅层那几跳加一个 1×1 卷积做通道压缩减少显存。输出层用num_classes通道加 softmax做多类语义分割。4.2 训练脚本与损失函数选择import torch.nn as nn import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes4, decoder_channels(256, 128, 64, 32) # 4 层解码对应 1/8 最深 ).cuda() # 类别不均衡背景远多于缺陷用 Dice CE 组合 ce nn.CrossEntropyLoss(weighttorch.tensor([0.1, 1.0, 1.0, 2.0]).cuda()) dice smp.losses.DiceLoss(modemulticlass) for images, masks in unet_loader: images, masks images.cuda(), masks.cuda() preds model(images) loss ce(preds, masks) dice(preds, masks) loss.backward() optimizer.step() optimizer.zero_grad()逻辑说明用segmentation_models_pytorch省去手写 UNetdecoder_channels只给 4 个值强制解码器只到 1/8 分辨率保住小目标。损失用 CE 加 DiceCE 的 weight 里背景给 0.1dot 给 2.0因为 dot 像素最少、最难分。Dice 负责整体区域重叠CE 负责逐像素分类两者互补。参数上encoder_weightsimagenet必须开小数据上从头训 UNet 基本没戏。学习率用 1e-3 配 Adambatch size 4 到 8。如果显存不够把输入裁成 512×512 的 patch 训练推理时滑窗拼回去重叠区取平均。4.3 两路结果融合与后处理测量推理时先跑 Mask R-CNN 得到实例框和粗 mask再跑 UNet 得到整图语义 mask。融合策略对每个 Mask R-CNN 实例取其框内 UNet 语义 mask 中对应类别的像素与粗 mask 做交集得到精修 mask。然后对精修 mask 做连通域分析算面积、周长、圆度输出测量结果。import cv2 import numpy as np def refine_and_measure(instance_mask, unet_mask, cls_id, min_area4): # instance_mask: 0/1, unet_mask: 类别值 inter ((instance_mask 0) (unet_mask cls_id)).astype(np.uint8) num, labels, stats, _ cv2.connectedComponentsWithStats(inter, 8) results [] for i in range(1, num): area stats[i, cv2.CC_STAT_AREA] if area min_area: # 过滤噪点 continue cnts, _ cv2.findContours((labels i).astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) peri cv2.arcLength(cnts[0], True) circularity 4 * np.pi * area / (peri ** 2 1e-6) results.append({area: area, perimeter: peri, circularity: circularity}) return results逻辑说明交集保证实例归属和像素类别一致连通域分析把粘连的缺陷拆开min_area4过滤掉标注噪声和压缩伪影。圆度用来区分 bump偏圆和 dent偏不规则实际阈值按你的数据统计定我一般圆度大于 0.7 判 bump。5. 避坑与排查芯片缺陷检测里最容易翻车的五件事5.1 小目标全被 RPN 过滤掉现象训练 loss 正常降但验证时 dot 类几乎检不出mAP 接近 0。原因默认 anchor 最小 32 像素dot 只有几个像素RPN 在第一阶段就把它们当背景筛掉了。解决改anchor_generator.sizes加 8 和 16 两档aspect_ratios保持默认同时把rpn_pre_nms_top_n_train提到 4000、rpn_post_nms_top_n_train提到 2000给足候选框。5.2 UNet 把背景纹理学成缺陷现象UNet 在无缺陷区域也输出大片高置信度 mask假阳性爆炸。原因芯片表面有周期性纹理和反光背景和缺陷在浅层特征上相似加上 CE 权重没调好模型倾向于多预测缺陷。解决CE 里背景权重降到 0.1 以下加 Dice 或 Focal 损失数据增强里加随机亮度、对比度扰动让模型不依赖固定纹理后处理用连通域面积和圆度双重过滤。5.3 标注 polygon 导致 mask 边界锯齿现象精修后的 mask 边界呈锯齿状面积测量波动大。原因COCO polygon 标注本身是折线转 mask 时取整丢像素小目标上尤其明显。解决dot 类缺陷改用 mask PNG 标注或者标注时把 polygon 点加密转换脚本里用ImageDraw.polygon后做一次形态学闭运算填掉一像素的缝。5.4 训练集和验证集同图不同裁切导致指标虚高现象验证 mAP 很高上线后一塌糊涂。原因数据划分时同一张原图的不同 patch 被分到了训练和验证集模型其实见过验证集内容。解决按原图 id 划分同一张图的所有 patch 只能进一个集合如果原图数量太少用 wafer 或批次 id 做分组划分。5.5 推理时两路模型输入分辨率不一致现象融合后 mask 和实例框错位测量坐标对不上。原因Mask R-CNN 内部会把图缩到 800 短边UNet 用的是原图或 512 patch两路坐标系不同。解决统一在预处理阶段把图缩到同一尺寸记录缩放比例推理完把 mask 和框都映射回原图坐标再做融合或者干脆两路都用原图裁 patchMask R-CNN 也走 patch 推理。6. 把方案推到产线阈值标定、增量训练与一个验证习惯方案能跑通不等于能上线。产线上最实际的两个问题是阈值怎么定和模型怎么持续更新。阈值标定别拍脑袋拿一批已知结果的图画 precision-recall 曲线按产线能接受的漏检率反推置信度阈值。比如漏检率要求低于 1%就在 recall0.99 的位置取阈值再看那个点的 precision 够不够。bump、dent、dot 三类阈值分开标dot 最难通常阈值要低一些。增量训练上芯片产线的缺陷分布会随工艺漂移固定模型几个月就退化。我一般每积累 200 到 300 张新标注图就做一次增量训练用旧模型权重初始化学习率降到原来的十分之一只训 3 到 5 个 epoch。注意增量数据里要混入 20% 到 30% 的旧数据否则会灾难性遗忘把之前学好的类别忘掉。验证习惯上我坚持留一个「黄金集」50 到 100 张覆盖所有缺陷类型和典型误检的图每次模型更新都跑一遍记录每类的漏检和过检数量。这个集不参与训练也不轻易换。有一次我图省事跳过了黄金集验证直接上线结果新模型把 dent 和反光条纹搞混产线误报了一整天血泪经验。后来不管多急黄金集必跑。还有一个具体技巧测量结果别只输出面积把圆度、长宽比、灰度均值一起存下来用这些特征做一个简单的规则层能挡掉相当一部分模型误检。规则层不用复杂几个 if-else 就够但它给了你一个不用重训模型就能快速调整的后悔药。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进