资讯动态

芯片表面缺陷检测实战:Mask RCNN与UNet双模型解析

发布时间:2026/10/5 9:26:53 来源:尧图企业网站定制
简介面向芯片制造质检与计算机视觉研究人员提供基于Mask RCNN与UNet双架构的芯片表面缺陷检测实现方案支持bump凸起、dent凹陷、dot点状三类缺陷的像素级分割与定位适配半导体产线质检场景。压缩包共73个文件、约4.03MB含22个Python脚本模型定义、训练、评估、9个Jupyter Notebook数据检查、模型训练、结果分析、12个CSV数据文件及配套配置目录按mrcnn_6、UNet模块组织便于对照学习与二次开发。目前已有119人学习。资源内含可运行算法代码及完整实战链路涵盖数据预处理、模型训练、结果评估到应用部署附ReadMe说明与性能分析脚本适合工业缺陷检测方向的学生、算法工程师与智能制造从业者参考。1. 芯片表面缺陷检测Mask RCNN UNet 双模型实战资源做芯片外观检测的人基本都遇到过同一个场景产线上 AOI 机台拍回来的图bump 凸起、dent 凹陷、dot 点状缺陷混在一起传统图像处理用阈值分割或者形态学操作调一轮参数勉强能挡住一种缺陷换一批料就失灵。这两年深度学习给了一条出路但真正落地时大家的第一反应往往是——实例分割模型会不会太重、像素级分割到底能做到什么程度。我拆解这份《基于 Mask RCNN UNet 实现的芯片表面缺陷检测算法》实战资源时核心就盯着两件事一是它能不能把 bump、dent、dot 从「框级检测」推进到「像素级分割」二是这一整套从数据预处理、模型训练到结果评估的流程是不是能直接拿回自己手头的质检项目复用。这套资源里既有 Mask RCNN 的完整训练推理代码也有 UNet 的独立实现还带 TensorBoard 日志、结果分析和模型评估脚本可以说覆盖了一个缺陷检测项目的完整生命周期。适合正在做半导体外观质检、工业视觉缺陷检测或者想入门实例分割在制造业落地的人来参考。2. Mask RCNN 检测头chips.py 与 config.py 里的参数陷阱2.1 从检测框到像素掩码Mask RCNN 为什么适合芯片缺陷Mask RCNN 是在 Faster RCNN 基础上加了一条 mask 分支它在每个 RoI 上额外输出一个二值掩码所以天然支持像素级定位。芯片表面的 bump、dent、dot 最麻烦的一点是尺寸小、形状不好界定bump 像小鼓包dent 是下凹dot 是典型小斑点用滑动窗口或者单纯的目标检测只能告诉你「这里有异常」但说不清异常蔓延到哪里。Mask RCNN 的 mask 分支可以做到逐像素判定这对后续的缺陷面积统计和工艺分析特别关键。这份资源里mrcnn_6目录下的结构和原版 Mask RCNN 实现基本一致核心在于chips.py定义了针对芯片表面缺陷的数据集类。你要理解这个项目先要弄懂它把 Mask RCNN 怎么落到芯片缺陷这个场景上。常见做法是原版 Matterport 的 Mask RCNN 实现在 COCO 预训练权重基础上做迁移学习训练时冻结 backbone 的前几层只微调后面几个阶段和检测头。# chips.py 中关键的数据集加载逻辑基于项目文件还原的常见用法 class ChipsDataset(utils.Dataset): def load_chips(self, dataset_dir, subset): # 添加三类缺陷bump凸起、dent凹陷、dot点状 self.add_class(chip, 1, bump) self.add_class(chip, 2, dent) self.add_class(chip, 3, dot) # 遍历图片目录为每张图加载对应的 JSON 标注 for filename in os.listdir(image_dir): image_path os.path.join(image_dir, filename) annotations json.load(open(annotation_path)) self.add_image( chip, image_idfilename, pathimage_path, annotationsannotations )这段代码定义了三类缺陷的类别映射这是让 Mask RCNN 能区分 bump、dent、dot 的基础。load_chips遍历图片目录把每张图的路径和标注文件关联起来形成训练集索引。注意add_class的参数类别 ID 从 1 开始背景类默认是 0这一点在后面配置NUM_CLASSES时非常重要容易多算或者少算一个类别。2.2 训练参数怎么设这些配置不改模型基本白训config.py是整个 Mask RCNN 训练的命门。我在拆这个项目时对照它的ChipsConfig类发现几个直接影响收敛的参数值得拿出来细讲。# config.py 中针对芯片缺陷场景的核心配置 class ChipsConfig(Config): NAME chips GPU_COUNT 1 IMAGES_PER_GPU 2 NUM_CLASSES 1 3 # 背景 bump/dent/dot 共 4 类 STEPS_PER_EPOCH 500 VALIDATION_STEPS 50 IMAGE_MIN_DIM 512 IMAGE_MAX_DIM 1024 DETECTION_MIN_CONFIDENCE 0.85首先要理解NUM_CLASSES 1 3这个数字——1 是背景3 是三类缺陷。很多人在这写错写成 3 就会发现训练时 loss 爆掉或者类别错位。IMAGE_MIN_DIM和IMAGE_MAX_DIM决定了输入图片的尺度芯片表面缺陷普遍偏小原图如果特别大比如 4000×4000 的晶圆图直接缩放会把你本来就小的缺陷缩没了。这里我一般会先把原始图按 ROI 区域裁剪再送进网络而不是整体缩放。还有一个容易被忽略的是DETECTION_MIN_CONFIDENCE这个值设高了会漏检设低了会出现大量假阳。在芯片表面这种要求严格的场景我一般会先把 0.85 跑一轮 validation看 PR 曲线再调。这份资源的model_eval6.py里应该有评估逻辑别只看训练 loss要结合 precision/recall 来分析。2.3 用 run_6.py 跑训练从命令行到 TensorBoard 曲线run_6.py是训练入口整个流程是加载数据集 → 构建模型 → 加载预训练权重 → 执行训练。这里有个容易翻车的地方加载 COCO 预训练权重后最后的检测头维度不一样原版实现会自动跳过不匹配的层但打印日志里那几行 not loading 很多人不看导致训练了很久其实分类层是随机初始化的。# 训练 Mask RCNN 的命令行基于项目 run_6.py 逻辑的常见启动方式 python run_6.py train --dataset ./data/dataset --weights coco # 带续训参数的版本 python run_6.py train --dataset ./data/dataset --weights last训练的产出要盯三个指标loss总体下降趋势、loss_rpn_bbox是否在合理范围、loss_mrcnn_mask是否收敛。芯片缺陷这种小目标loss_mrcnn_bbox往往下降得比 mask loss 快如果你的 mask loss 跌不下去通常是标注精度不够或者类别混淆严重。TensorBoard 日志在tensorboard目录项目里配了run_6_6这类带 tensorboard 配置的入口我在实际跑的时候见过不少人不看曲线只等它训练完结果 epoch 数设得完全不合理。按我的经验500 步一个 epoch、验证 50 步这个配置在中小规模芯片数据集上大约 50 到 80 个 epoch 能收敛具体看你训练集大小。3. UNet 分割分支U 型网络如何补上 Mask RCNN 的边界短板3.1 UNet 在芯片缺陷场景的真实定位Mask RCNN 擅长把每个缺陷实例区分开但实例分割本质上是「先检测、再分割」多个缺陷挨得近的时候mask 分支容易把两个贴合在一起的 bump 切成一个。UNet 的优势是滑动窗口式的全局语义分割它不区分实例但对每个像素属于哪类缺陷判定得更稳。这份资源里UNet目录下有model.py和utils.py是标准的 UNet 实现。我理解它的定位是和 Mask RCNN 形成互补Mask RCNN 负责实例级检测和数量统计UNet 负责全图的像素级类别判定两个模型的结果可以做交叉验证。实际项目中两种方案各有适用场景如果产线只关心「有没有缺陷、缺陷数量多少」Mask RCNN 就够了如果工艺端需要输出缺陷面积占比、缺陷区域的精确轮廓UNet 这条线更值得跑通。# UNet/model.py 中 UNet 编码器-解码器结构基于项目文件的常见实现还原 def unet_model(input_shape(512, 512, 3)): inputs Input(shapeinput_shape) # 编码器路径逐层下采样 conv1, pool1 conv_block(inputs, 64) conv2, pool2 conv_block(pool1, 128) conv3, pool3 conv_block(pool2, 256) conv4, pool4 conv_block(pool3, 512) # 中间层 conv5, _ conv_block(pool4, 1024) # 解码器路径上采样并拼接跳跃连接 up6 concatenate([UpSampling2D(size(2, 2))(conv5), conv4], axis-1) conv6, _ conv_block(up6, 512) up7 concatenate([UpSampling2D(size(2, 2))(conv6), conv3], axis-1) conv7, _ conv_block(up7, 256) up8 concatenate([UpSampling2D(size(2, 2))(conv7), conv2], axis-1) conv8, _ conv_block(up8, 128) up9 concatenate([UpSampling2D(size(2, 2))(conv8), conv1], axis-1) conv9, _ conv_block(up9, 64) # 输出三层分别对应 bump/dent/dot outputs Conv2D(3, (1, 1), activationsigmoid)(conv9) model Model(inputs, outputs) return model这段 UNet 的关键在跳跃连接。编码器每层下采样后图像分辨率减半细节信息不断丢失解码器上采样时把同分辨率的编码器特征拼接回来相当于保留了浅层的边缘纹理信息。对芯片缺陷这种需要边界精度的任务跳跃连接比单纯的上采样恢复要可靠得多。输出层用的是 sigmoid 而不是 softmax因为一个像素位置理论上只属于一类缺陷但实际标注时可能出现重叠用 sigmoid 给每个类别独立判定的空间训练更稳定。3.2 三通道与六通道训练train_3channels 和 train_6channels 的差异项目里有train_3channels_no_null.ipynb和train_6channels_no_null.ipynb两个训练笔记本这个设计值得重点说。三通道输入就是原始 RGB 图六通道则是把缺陷掩码或者多尺度特征叠加进去。我在多个工业视觉项目里用过类似策略常见做法是通道 1 到 3 为原始图像通道 4 到 6 可以是边缘响应图、梯度图或者前一版模型的预测概率图。# 打开训练笔记本时需要确认的数据路径结构 project_root/ ├── data/ │ ├── train_images/ # 原始芯片图像 │ ├── train_masks/ # 像素级标注掩码 │ ├── val_images/ │ └── val_masks/ └── UNet/ ├── train_3channels_no_null.ipynb └── train_6channels_no_null.ipynb为什么搞两个版本直接原因是有些芯片工厂能提供的标注只有缺陷区域二值图没有逐类的精细标注这时三通道版本配上单通道掩码也能训练一个「缺陷 vs 背景」的二分类分割模型。而如果你的需求是要区分 bump、dent、dot 的类别就必须用六通道版本或者把掩码做成三通道 one-hot 形式。我一般习惯是先用三通道版本把基础分割能力跑通确认 loss 和 visual 效果再切到六通道看是否有收益避免一开始就在复杂模型上浪费时间。train_6channels_no_null.ipynb里的「no_null」很关键——它过滤掉了 background 完全空白、没有缺陷的图像。我在实际项目中遇到过一个经典翻车场景训练集里无缺陷图占 70% 以上模型学到的全部是「输出全黑」因为把所有像素判为背景的 loss 已经很低了。后面会专门讲这块的处理。3.3 performance.ipynb怎么判断 UNet 分割质量项目里的performance.ipynb是用来做结果分析的我打开看时发现它包含按类别计算的 IoU 和 Pixel Accuracy。UNet 训练完不能只看 loss损失函数值低不代表分割边界好。尤其是芯片表面的 dot 缺陷可能只占几十个像素单独算 IoU 可能只有 0.3但看起来已经不错了。正确做法是分别统计每类缺陷的 IoU缺陷类别像素占比典型值Mask RCNN mask IoUUNet IoU可接受下限bump5%~15%0.70~0.800.65~0.750.60dent3%~10%0.65~0.780.60~0.720.55dot1%0.30~0.500.35~0.550.30dot 类别的 IoU 天然偏低这是小目标分割的物理限制不是模型坏了。我一般在评估时看两个维度整体 mIoU 是否达到 0.6 以上以及性能最差类别的 IoU 是否在可接受范围。如果 dent 的 IoU 远低于其他类别优先检查标注是否有系统性偏移而不是马上调损失函数。4. 数据预处理与类不均衡95% 的背景像素是最大的敌人4.1 data_inspect.ipynb训练前必须做的统计分析data_inspect.ipynb这个文件容易被忽略但它是整个项目中价值被低估的资产。很多人拿到缺陷检测项目就直接开训结果模型学不到缺陷特征。正确顺序是先用这个 notebook 统计每张图像中缺陷像素占全图比例、各类缺陷出现的频率、缺陷尺寸的分布。# data_inspect.ipynb 核心统计逻辑项目内已包含此处展示关键部分 import numpy as np from PIL import Image def compute_mask_ratio(mask_path): mask np.array(Image.open(mask_path).convert(L)) total_pixels mask.shape[0] * mask.shape[1] foreground np.sum(mask 0) return foreground / total_pixels # 统计整个训练集的缺陷占比 ratios [] for mask_file in train_mask_files: ratios.append(compute_mask_ratio(mask_file)) print(f缺陷像素平均占比: {np.mean(ratios):.4f}) print(f缺陷像素最大占比: {np.max(ratios):.4f}) print(f缺陷像素最小占比: {np.min(ratios):.4f})我拆解项目数据时发现芯片表面缺陷像素占比通常在 1% 到 5% 之间dot 类缺陷甚至可能低于 0.5%。这意味着如果用原始的像素级交叉熵损失模型把所有像素预测为背景损失已经能降到很低。我见过有人训练完第一轮就发现准确率 98%异常兴奋结果模型输出的掩码全是黑的——准确率是虚高的因为背景占了 98% 以上。这里的关键是在做训练之前先跑一遍统计分析。如果缺陷像素平均占比低于 5%必须做针对性处理否则后面调什么都白搭。4.2 裁剪、过采样与类别权重三种有效对策三种处理方式各有适用场景。第一种是裁剪策略把大图切块让每个 patch 中缺陷占比提升。常见做法是把 1024×1024 的图切成 512×512 的四块只保留包含缺陷的块参与训练。第二种是过采样对包含 dot 这类稀有缺陷的图像做畸变增强后反复进入训练集。第三种是修改损失函数给稀有类别更高的权重。# 常见做法在 UNet 训练时使用加权损失处理类不均衡 def weighted_binary_crossentropy(y_true, y_pred): # 给罕见缺陷类别更高权重背景权重压低 weights np.array([1.0, 2.0, 1.5, 1.0]) # 背景/bump/dent/dot weights_tensor tf.convert_to_tensor(weights, dtypetf.float32) # 按真实标签类别给每个像素分配权重 weight_map tf.reduce_sum(y_true * weights_tensor, axis-1) bce tf.keras.losses.binary_crossentropy(y_true, y_pred) weighted_bce bce * weight_map return tf.reduce_mean(weighted_bce)这段代码把损失函数改造成了像素级加权版本。核心逻辑是真实标签中属于 dot 类的像素计算损失时乘上 2.0 的权重背景像素只有 1.0。这是我从实际项目中总结出来的——把 dot 类权重设高能明显改善小目标检出率但不要设太高比如 5 倍以上否则模型会过度敏感把噪声也当成缺陷。batch size 设到 4 或 8 时每个 batch 里都可能出现过采样后的 dot 样本训练稳定性更好。4.3 训练顺序与内存管理双模型训练的资源配置建议同时跑 Mask RCNN 和 UNet显存分配是现实问题。我一般建议两个模型分开训练而不是同时跑Mask RCNN 本身在 512×512 输入下、batch size 为 2 时大约需要 8GB 显存。UNet 相对轻量同等分辨率下 6GB 就够。如果你的机器只有一张 11GB 显存的卡先后跑完两个模型后挑一个效果好的做部署是更现实的做法。项目里run_6.py和 UNet 训练是独立的model_eval6.py是 Mask RCNN 的推理评估results_analyze.ipynb则用于对比 mask 预测结果和真实标注。我在跑的时候养成一个习惯每次训练前先nvidia-smi看一眼显存占用如果别人在跑任务就用CUDA_VISIBLE_DEVICES0限定单卡避免显存溢出导致训练中断。5. 避坑指南Mask RCNN UNet 芯片缺陷检测的五条血泪经验5.1 训练 loss 不降先怀疑标注别急着调网络现象训练前 5 个 epochloss 从 1.8 降到 1.4 后彻底不动了再加 epoch 也没有起色。原因这类问题大概率出在标注上——标注掩码和原图尺寸不匹配、JSON 标注坐标有负数或超出图像边界、或者是背景像素全部为 255 而不是 0 导致 mask 语义反了。我一开始跑这个项目时也踩过最后检查发现是读取标注时某个类别 ID 从 0 开始和背景冲突了。解决先写个可视化脚本把原图和 mask 叠加输出确认每张训练图都能看到缺陷轮廓覆盖在正确位置。再检查标注里的坐标范围Mask RCNN 的标注逻辑是 polygonal 格式坐标要归一化到 0~1UNet 则是 mask 直接是像素坐标。用data_inspect.ipynb里的代码逐项验证坏掉的标注直接删掉或重新标注比调任何参数都有效。5.2 输出全黑或全白类不均衡的典型症状现象模型训练完成后推理所有图片输出要么全黑背景要么全白前景完全不符合输入图像。原因前面提到的背景像素占比过高模型学到的最优策略就是把全部像素判为背景。全白的情况通常在 mask 读取顺序搞反时出现比如标注里黑底白字代码读成了白底黑字。解决用加权损失改造损失函数或者做数据裁剪。另外要在训练时用「no_null」策略过滤全背景图。这个项目既然叫train_3channels_no_null就说明开发者已经意识到这个问题你用 notebook 训练时别把过滤逻辑删掉那行判断代码是整个训练前的关键屏障。5.3 Mask RCNN 的 mask 和 box 对不上现象检测出来的 bounding box 位置正确但 mask 明显比 box 小一圈或者偏向一侧看起来像是错位。原因Mask RCNN 的 mask 分支在固定大小的 RoI 上进行分割如果训练图像中缺陷正好贴边裁剪时会截断部分缺陷轮廓推理时DETECTION_MIN_CONFIDENCE设得太高也会把低分的部分 mask 滤掉。解决检查config.py中的ROI_POSITIVE_RATIO这个值决定了 RoI 中正样本比例一般为 0.33。如果偏低模型容易在 mask 分支上欠拟合。另一个方法是在推理时不直接采用置信度过滤后的 mask而是输出所有 mask 的原始概率图后再做阈值处理。我在实际项目中用后一种方式mask 错位问题基本消失。5.4 验证集效果好测试集翻车现象model_eval6.py跑出的验证集准确率很好一换到产线新拍回来的图上误检率直接飙升。原因数据分布偏移。芯片缺陷检测项目很容易绕进这个坑训练集用的是实验室高倍镜拍摄图测试时用的是产线 AOI 机台图光照角度、分辨率、芯片材质反光都不一样。解决训练时做数据增强重点加亮度扰动和弹性形变。我在评估时发现验证集和测试集的 PR 曲线差距超过 10 个点时会手动把测试集图片返回训练集做 10 个 epoch 的微调。这个项目里的model_eval6.py自带保存评估结果的功能建议每次跑完都把 PR 曲线图存档做到每次调参都有对比依据而不是凭感觉。5.5 TensorBoard 日志丢失或无法启动现象训练时显示 TensorBoard 路径无效或者训练完想看曲线发现日志目录为空。原因常见是路径层级问题。项目里 tensorboard 目录和训练入口不在同一层启动 TensorBoard 时用了相对路径结果指向了错误的目录。或者训练代码里TensorBoard回调设了update_freqepoch但训练还没等到第一个 epoch 就被手动中断。解决启动前就手动创建日志目录并打印绝对路径训练脚本里用os.makedirs(log_dir, exist_okTrue)避免第一轮因目录不存在报错。启动时指定绝对路径tensorboard --logdir ./tensorboard/chips_logs --port 6006训练中断后想继续用run_6.py里--weights last参数加载上一次保存的权重TensorBoard 日志也建议按日期建子目录方便对比不同版本。6. 结果评估与部署验证从 mIoU 到产线可用的四个关键动作模型训练完只是第一步真正麻烦的是怎么让评估结果说服自己和客户。我拆这个项目最后拿出来的是一组完整的评估链路从模型输出到指标统计再到和标注做像素级偏差分析任何一个环节漏了最后的部署都会出问题。第一步是统一评估口径。results_analyze.ipynb里有按类别统计 IoU 的逻辑但你需要在代码里确认是只算缺陷类别的 mIoU还是连背景一起算。背景占比高的时候背景 IoU 会把总体指标拉得很虚高所以我习惯只看 bump、dent、dot 三类的平均 IoU代码里把背景的贡献排除掉# 评估时剔除背景类只看三类缺陷的 IoU基于 results_analyze.ipynb 的常见做法 iou_per_class [] for cls_id in [1, 2, 3]: # 只统计 bump/dent/dot intersection np.sum((pred cls_id) (true cls_id)) union np.sum((pred cls_id) | (true cls_id)) iou intersection / (union 1e-6) iou_per_class.append(iou) defect_miou np.mean(iou_per_class) print(f缺陷类别 mIoU: {defect_miou:.4f})第二步是设定部署阈值。Mask RCNN 里DETECTION_MIN_CONFIDENCE在训练配置里设的 0.85 是训练时参考值推理时应该单独调。我的习惯是先输出所有检测框的置信度和 mask画 PR 曲线找到 P 和 R 的平衡点。芯片质检宁可误检回去复看也不能漏检导致不良品流出所以阈值我会往低调 5~10 个点。第三步是把分割结果转成实际物理尺寸。芯片表面的 dot 缺陷如果你只在像素图上看到它很难让工艺人员信服。假设相机分辨率和芯片实际尺寸已知你可以给 mask 区域做连通域分析输出每个缺陷的实际面积平方微米。这一步用 OpenCV 的cv2.connectedComponents就能实现但要注意同一类缺陷在 Mask RCNN 里可能输出多个 mask 碎片UNet 里则需要先用形态学闭运算把相邻像素连上再统计。第四步才是部署。这个项目的代码是标准的 Keras/TensorFlow 实现导出到生产环境时有两条路一是直接保存 H5 权重加载跑 CPU/GPU 推理二是转成 TensorRT 或者 ONNX 做加速。芯片质检产线如果节拍要求每秒处理一张图TensorRT 的优化效果会非常明显。这块虽然在资源里没有现成代码但按 TensorFlow 2.x 的标准导出流程就能衔接上导出时固定输入尺寸 512×512 或 1024×1024语义分割模型对动态输入尺寸支持不稳定固定大小反而省心。这个项目最让我觉得靠谱的一点是它不止给了模型代码还给了results_analyze.ipynb和data_inspect.ipynb意味着你做数据分析和结果评估时不用自己重新造轮子。从那以后我每次拿到一个缺陷检测项目都强制自己走一遍这个流程先跑数据统计分析再做三通道 UNet 打底跑通 Mask RCNN 验证实例分割最后用统一的评估脚本对比两套模型的效果再决定部署哪条线。这个顺序帮我避开了很多无效训练的时间浪费希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑