☰
芯片表面缺陷检测实战:Mask RCNN与UNet双模型解析
2026/10/5 9:26:49 网站建设 项目流程

简介:面向芯片制造质检与计算机视觉研究人员,提供基于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_id=filename, path=image_path, annotations=annotations )

这段代码定义了三类缺陷的类别映射,这是让 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(shape=input_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), activation='sigmoid')(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.60
dent3%~10%0.65~0.780.60~0.720.55
dot<1%0.30~0.500.35~0.550.30

dot 类别的 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, dtype=tf.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_DEVICES=0限定单卡,避免显存溢出导致训练中断。

5. 避坑指南:Mask RCNN + UNet 芯片缺陷检测的五条血泪经验

5.1 训练 loss 不降:先怀疑标注,别急着调网络

现象:训练前 5 个 epoch,loss 从 1.8 降到 1.4 后彻底不动了,再加 epoch 也没有起色。

原因:这类问题大概率出在标注上——标注掩码和原图尺寸不匹配、JSON 标注坐标有负数或超出图像边界、或者是背景像素全部为 255 而不是 0 导致 mask 语义反了。我一开始跑这个项目时也踩过,最后检查发现是读取标注时某个类别 ID 从 0 开始,和背景冲突了。

解决:先写个可视化脚本把原图和 mask 叠加输出,确认每张训练图都能看到缺陷轮廓覆盖在正确位置。再检查标注里的坐标范围,Mask RCNN 的标注逻辑是 polygonal 格式,坐标要归一化到 0~1,UNet 则是 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_freq='epoch',但训练还没等到第一个 epoch 就被手动中断。

解决:启动前就手动创建日志目录并打印绝对路径;训练脚本里用os.makedirs(log_dir, exist_ok=True)避免第一轮因目录不存在报错。启动时指定绝对路径:

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 验证实例分割,最后用统一的评估脚本对比两套模型的效果再决定部署哪条线。这个顺序帮我避开了很多无效训练的时间浪费,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询