ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于YOLOv8的晶圆缺陷检测系统:从传统视觉到深度学习实战

基于YOLOv8的晶圆缺陷检测系统:从传统视觉到深度学习实战

晶圆缺陷检测这个方向,在半导体制造的品质管控里一直是块硬骨头。我最早接触这类需求,是被产线上的人工目检效率逼着入门的——一片晶圆上几百个 Die,靠人眼在显微镜下逐个排查,漏检率不低,而且人员疲劳导致的误判很致命,往往同一缺陷不同班次的人给出的结论都不一样。后来尝试了传统机器视觉方案,阈值分割、模板匹配在简单背景下还能用,一旦遇到划痕和颗粒混合、光照不均匀的情况,参数怎么调都打架,今天调好明天换一批产线样品又不行了。再往后就是转向 YOLOv8,把检测任务当成目标检测来做,整个思路才顺过来。

这篇文章就围绕"基于YOLOv8的晶圆体缺陷检测系统"来展开,覆盖 YOLO 数据集的制作、模型训练、UI界面封装和 Python 源码组织这几块。适合刚入行想落地工业视觉检测的同学参考,也适合已经有传统视觉基础、想换深度学习方案的朋友。我会把整个链路拆开讲,重点说清楚每一步的取舍逻辑,以及那些只跑通流程根本遇不到的坑。

1. 晶圆缺陷检测:为什么要从传统视觉转向 YOLOv8

1.1 传统视觉方案在晶圆场景的真正瓶颈

很多做工业视觉的老工程师一听到"缺陷检测",第一反应还是阈值分割加形态学处理,背景亮一点、缺陷暗一点就能提取出来,再不行就上模板匹配。这套思路在 PCB 裸板、金属表面划痕这类对比度明显的场景确实有用,但放到晶圆上就会出现三个很难绕开的问题。

第一是缺陷种类太杂。晶圆上常见的缺陷就包括颗粒污染、划痕、边缘崩缺、药液残留污渍、气泡等,同一类缺陷的表现形态差异也很大,划痕可以是连续的也可以是断续的,颗粒可能是一颗大的也可能是一簇密集的小点。传统算法只能针对某一类缺陷单独写规则,规则之间还会互相干扰,处理 A 缺陷的滤波核会把 B 缺陷给抹掉。

第二是对比度太低。很多晶圆缺陷是微观层面的,比如亚微米级的微裂纹、极浅的刮擦,在图像上只产生几个像素的灰度变化,甚至比背景纹理本身的波动还小。阈值怎么选都覆盖不了全部情况,选了低阈值会带回一堆背景噪声,选了高阈值就直接把缺陷漏掉了。

第三是环境敏感。光照角度稍微偏一点、相机增益调了一点,灰度分布就整体偏移,固定阈值的稳定性很差。生产现场为了追溯到不同工艺阶段的问题,光源和相机位置还经常调整,每次调整都意味着算法参数要重新标定一轮。

这些问题不是调参能解决的,本质上是传统算法在"用人工定义的规则去猜缺陷长什么样",而缺陷的形态组合几乎是无限的,规则永远覆盖不全。

1.2 YOLOv8 作为检测框架的适配优势

YOLOv8 属于单阶段目标检测器,它做的不是"找异常区域",而是"直接回归出目标的位置和类别"。通俗点说,你把带缺陷的晶圆图片给它看,告诉它缺陷在哪里、是哪一类,它自己会去学习什么样的视觉模式对应什么样的缺陷。这种数据驱动的方式,天然避开了人工规则覆盖不全的问题。

从工程落地的角度,YOLOv8 还有几个非常实用的点。

一是速度和精度的平衡很理想。工业检测通常要考虑产线节拍,一帧图不能处理太慢,YOLOv8 的 n/s 系列在 CPU 或普通 GPU 上都能跑到实时,这比两阶段检测器亲和得多。

二是框架封装得很干净。ultralytics 官方包把训练、验证、推理都封装成一两行代码,数据格式、评估指标、可视化都内置好了,对于一个需要快速交付的检测系统来说,省掉的都是实打实的开发时间。

三是模型结构上有改进。YOLOv8 是 anchor-free 的,不再依赖预设的锚框尺寸去匹配目标,模型对被检目标的尺寸变化适应更好。同时主干网络使用了 C2f 模块和 PAN-FPN 结构,多尺度特征融合比之前几代更充分,这对晶圆这类既要看大划痕又要看小颗粒的场景比较友好。

当然,YOLOv8 也不是万能钥匙,小目标漏检的问题它一样存在,这个后面我会单独讲。但至少从整体思路上,它把"晶圆缺陷检测"从一个无限写规则的问题,变成了一个"整理数据、训练模型、调整参数"的标准目标检测问题,复杂度降了一大截。

2. 数据集:标注、格式转换与样本扩充

2.1 缺陷类别怎么定、标注时注意什么

做目标检测任务,第一步不是急着标注,而是先把类别定义清楚。类别定义太粗,比如把所有缺陷都归成一类"defect",模型确实能训出来,但实际产线上你想根据缺陷类型追溯工艺环节,这种粗分类就没法用了。类别太细也不行,比如把划痕再按方向分成横划痕、竖划痕,标注成本翻倍,模型还可能因为类间差异太小而学混。

我在做这个项目时最终定了五个类别:particle(颗粒污染)、scratch(划痕)、stain(污渍)、edge_chip(边缘崩缺)、bubble(气泡)。这五个类别基本覆盖了晶圆从切割、研磨到光刻、镀膜等主要工艺环节会出现的典型缺陷,而且每一类的工艺归因意义都比较明确。

标注的时候有几个细节需要提醒。第一,框一定要贴紧缺陷的边缘,尤其是划痕这种细长目标,宁可框得稍微保守一点,也不要为了"确保包含全部缺陷"而把背景大片框进去,不然模型会学到"背景也是缺陷的一部分",推理时误检率会很高。第二,边缘崩缺这类缺陷通常贴图边缘,标注时要允许框超出图像边界一些,很多标注工具支持这种半出界框,处理的时候要做裁剪。第三,一个图里有多个缺陷就每个都标,哪怕某个缺陷已经小到只有十来个像素,也要标,因为小目标样本本来就稀缺,漏标等于变相教模型"这些不用管"。

我习惯用开源标注工具配合手动调整。第一次标注时,建议两个人各标一遍再对结果,主要目的是统一标准:有的人习惯框大一点,有的人习惯框紧一点,标准和模型推理结果直接相关,越早统一越省事。

2.2 从标注文件转成 YOLO 格式

YOLOv8 使用的标注格式和很多标注工具默认格式不一样。它要求每一张图片对应一个同名的 txt 文件,文件里每一行代表一个目标:

class_id x_center y_center width height

注意,四个坐标值全部是相对于图片宽高的归一化值,范围在 0 到 1 之间。很多第一次接触 YOLO 的人在这里就栽了,把像素坐标直接填进去,训练时模型完全学不到有效信息。

如果用的是 LabelImg 这类工具,导出一般是 Pascal VOC 的 XML 格式,需要写脚本转换。核心转换逻辑不复杂:

import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, class_names, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size").find("width").text) img_h = int(root.find("size").find("height").text) lines = [] for obj in root.iter("object"): class_name = obj.find("name").text if class_name not in class_names: continue class_id = class_names.index(class_name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") txt_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines))

转换完之后一定要做一次可视化校验,把 txt 标注画回原图上,看看框的位置有没有偏差。尤其是当你做了图像缩放、裁剪、旋转等预处理之后,对应标注也必须同步变换,这块很容易错,一旦坐标没对齐,训练出来的模型就是在垃圾数据上找规律,效果必然一塌糊涂。

2.3 数据划分与样本增强策略

数据划分不是简单按比例随机切就完事,得先考虑数据之间的相关性。晶圆图像经常是同一片晶圆切成多个 patch 得到的,如果同一个晶圆的不同 patch 既进了训练集又进了验证集,那么验证指标会虚高,因为模型已经"见过"同一个晶圆的纹理背景了,换一片没见过的晶圆再测就露馅。正确做法是按晶圆本身分组,整片晶圆的所有 patch 一起划入同一个集合。

划分比例上,我比较推荐 7:2:1 或 8:1:1,训练集尽量多,验证集用来调超参数,测试集一次性留到最后做最终评测。缺陷检测的数据天然不均衡,颗粒污染可能几百个样本,气泡可能只有几十个,这种情况我会做类别平衡采样,让每个 batch 里尽可能覆盖各个类别,避免模型只学会了样本量大的那几类。

增强策略要结合缺陷特征来定。翻转、旋转、缩放是最基础的,基本都能用。亮度扰动和对比度扰动强烈建议加上,因为晶圆图像受打光影响大,训练时多模拟几种光照条件,模型在线上的泛化能力会好很多。马赛克增强(Mosaic)可以开,但要注意一个小问题:晶圆缺陷很多是小目标,马赛克把四张图拼成一张之后,小目标分辨率进一步被压缩,学到了也容易过拟合。我的做法是开着 Mosaic 跑前 30 个 epoch,后面关掉用普通增强微调,兼顾了小目标的拟合和大目标的多样性。

还要提醒一句,样本量不是越多越好,质量比数量重要。如果同一类缺陷全是高相似度的重复样本,增强再多也只是在同样的模式上打转。理想情况是尽可能收集不同工艺批次、不同光照、不同分辨率的样本,让数据集覆盖真实分布的多样性。

3. 模型训练:从 yaml 配置到 best.pt

3.1 选哪个规格的 YOLOv8 模型

YOLOv8 官方提供了 n、s、m、l、x 五种规格,区别主要体现在深度和宽度上,参数量和推理时间依次增加,精度也随之提升。型号选择不是越大越好,关键看部署场景和硬件条件。

模型规格参数量(约)推理速度适用场景
YOLOv8n3.2M最快CPU 或低算力边缘设备,快速原型验证
YOLOv8s11.2M快普通 GPU,批量离线检测
YOLOv8m25.9M中等精度要求较高的离线分析
YOLOv8l43.7M较慢高精度场景,需要较好 GPU
YOLOv8x68.2M最慢追求极致精度的研究实验

晶圆缺陷检测的现场部署环境通常不太理想,很多产线工控机只有 CPU 或者入门级 GPU,在线检测还有实时性要求。我在这类项目上一般直接用 YOLOv8s,如果对速度要求特别苛刻就降到 YOLOv8n,先跑通流程再考虑用蒸馏方式把大模型的知识迁移到小模型。好高骛远一上来选 x,训练跑不动、部署也带不动,最后反而耽误事。

3.2 data.yaml 与关键训练参数

数据准备好之后,需要写一个 data.yaml 文件告诉模型训练什么,内容大概长这样:

path: ./wafer_defect train: images/train val: images/val test: images/test nc: 5 names: ["particle", "scratch", "stain", "edge_chip", "bubble"]

注意 path 字段是数据集根目录的相对路径或绝对路径,train、val 指向各自的图片目录,模型会自动去同目录下的 labels 文件夹找对应的 txt 标注。names 的类别顺序必须和标注文件里的 class_id 完全对应,这是最容易出错的地方,一旦顺序乱了,所有标签都会错位。

训练命令用官方包就很简洁:

from ultralytics import YOLO model = YOLO("yolov8s.yaml") results = model.train( data="wafer_defect.yaml", epochs=120, imgsz=640, batch=16, lr0=0.01, patience=20, device=0, workers=8, )

几个关键参数我展开说一下。

epochs 设为 100 到 150 比较合适,缺陷数据集通常不大,几十个 epoch 就够收敛,设少了对小样本类别可能欠拟合,设多了容易过拟合。imgsz 是训练输入尺寸,默认 640,但如果你的缺陷里面有大量小于 16×16 像素的小目标,640 会把它们压得特别小,建议直接提到 960 或 1280,代价是训练时间和推理时间都会明显上涨。batch 大小要看 GPU 显存,显存不够就减小 batch,同时适当降低学习率。lr0 初始学习率 0.01 是官方默认,配合 AdamW 优化器通常表现稳定,如果发现 loss 震荡得厉害,就把 lr0 降到 0.005。patience 是早停耐心值,连续多少个 epoch 验证集指标没提升就自动停止,这个参数一定要开,它能帮你省下大量无意义的重复训练时间。

还有一个容易被忽略的参数是 workers,数据加载线程数。默认值可能只有 2,而晶圆图像分辨率通常比较大,数据加载往往会成为训练瓶颈。在 Windows 上开太高会报 worker 相关错误,Linux 上开到 cpu 核心数附近即可。

3.3 如何看训练曲线和评估指标

训练跑完,官方包会在 runs/detect/train 目录下生成一堆结果文件。很多人只看 mAP 就完事,我觉得至少要花十分钟把 training 的 loss 曲线和验证指标曲线都过一遍,这些曲线的形态能告诉你很多书上不写的信息。

loss 曲线主要看三个:box_loss(边框回归损失)、cls_loss(分类损失)、dfll_loss(分布焦点损失)。正常情况下三者都会随着训练迭代稳步下降然后趋于平稳。如果 box_loss 一直在降但 cls_loss 不降,大概率是类别定义有问题,个别类之间视觉差异太小,模型分不开,这时候回去整理数据比继续调参有效。如果训练 loss 和验证 loss 之间差距越拉越大,说明过拟合了,增加增强力度或者减小模型规格都可以尝试。

评估指标我主要看 mAP50 和 mAP50-95 两个数。mAP50 是 IoU 阈值 0.5 下的平均精度,更贴近"框大致准不准";mAP50-95 则是在多个 IoU 阈值下取平均,对框的定位精度更苛刻。晶圆缺陷检测里,缺陷虽然小,但定位精度直接影响后续工艺归因和缺陷判定,所以 mAP50-95 尽量往 0.7 以上拉,不要只看 mAP50 好看就觉得万事大吉。

启动训练之前我还建议先把验证集的 PR 曲线看清楚。如果某个类别的 PR 曲线面积明显小于其他类,说明这个类难检,你就能有针对性地补充该类别的样本,或者单独调整该类别的置信度阈值。这些诊断在项目早期做,比训练完上线才发现某个类别几乎完全不检要省事得多。

4. UI 界面与推理部署:把模型封装成可用系统

4.1 界面选型:PySide6 还是 Tkinter

模型训练好了,只停在命令行里敲 predict 对现场人员来说毫无意义。产线上的操作员根本不懂什么是置信度,他们要的是一个能选图、点按钮、看检测结果的界面。这一步要把模型包进一个可交互的 UI 系统。

Python 做桌面界面,选项无非是 Tkinter、PySide6/PyQt5、wxPython 这几种。我在这类项目里更倾向 PySide6,原因有三个:第一,它对标 Qt 的成熟控件体系,表格、文件选择框、图片显示区这些都可以直接用现成组件拼出来,不用自己拿 Tkinter 的 Canvas 一点点画;第二,信号槽机制做界面的响应逻辑非常清晰,处理完一帧图然后刷新显示,天然就是事件驱动;第三,界面视觉效果比 Tkinter 好看不少,在产线上给操作员用,界面明确一点、结果展示直观一点,信任度就高一分。

Tkinter 也不是不能用,Python 内置无额外依赖,做一个简单原型只要几十行代码。但如果你的系统还要带参数配置、表格展示、批量处理这些功能,Tkinter 后面会越写越痛苦。我的建议是:图省事学习交互可以用 Tkinter,正式上线给用户用就上 PySide6。

4.2 推理线程与模型加载

UI 系统里最容易犯的错误是把模型推理放在主线程里。模型加载一次就要几百毫秒到几秒,推理一张高分辨率晶圆图又需要几百毫秒,放在主线程里,界面会直接卡死,表现为窗口无响应、转圈,操作员憋不住就会强制关进程。正确做法是把模型加载和推理放到工作线程。

PySide6 里的标准做法是自定义一个 QThread 子类,重写 run 方法,用 Signal 把结果传回主线程。

from PySide6.QtCore import QThread, Signal from ultralytics import YOLO import numpy as np class DetectThread(QThread): result_ready = Signal(object) progress = Signal(str) def __init__(self, model_path, image_path, conf_threshold=0.25): super().__init__() self.model_path = model_path self.image_path = image_path self.conf_threshold = conf_threshold def run(self): model = YOLO(self.model_path) self.progress.emit("正在推理...") results = model.predict( source=self.image_path, conf=self.conf_threshold, iou=0.45, imgsz=640, verbose=False ) rgb = results[0].plot() # 绘制检测框后的图像 self.result_ready.emit(rgb)

主界面里只需要实例化线程,连接信号和槽函数,点击按钮时启动线程,线程发出 result_ready 信号后,在槽函数里把图像显示到 QLabel 上。这样界面始终流畅,操作员在检测过程中还可以继续操作其他功能。

另一个实践细节是模型加载不要每次推理都做。YOLO 模型文件几十 MB,加载一次启动时完成,之后在多个线程之间复用同一个模型实例。我一般会在程序启动时先做一个初始化,把模型对象存成全局单例,工作线程里直接拿这个实例做 predict,这样响应时间能小半秒以上。

4.3 检测结果的显示与报告导出

界面光显示画了框的图片还不够,实际生产环境里操作员往往需要知道每一项缺陷的类型、数量和置信度,最好还能生成可归档的检测报告,用于批次追溯和质量分析。

我的实现里,检测结果除了显示在预览区,还会实时填充到一个 QTableWidget 表格中,每一行是"序号、缺陷类别、置信度、x、y、w、h"。点击某一行,预览区对应位置高亮显示,操作员可以快速复核可疑缺陷。数据拿的是 results[0].boxes 里的数据,比如:

for i, box in enumerate(results[0].boxes): cls_id = int(box.cls[0]) conf = float(box.conf[0]) x_center, y_center, w, h = box.xywh[0].tolist() # 添加到界面表格

报告我一般导出成 CSV 和带框图像两种形式。CSV 方便后续做统计分析和工艺追溯,带框图像方便直接贴到检测记录里。批量检测多张图片时,可以再封装一层批量处理逻辑,把所有结果汇总成一个总的统计表。这个功能做出来之后,这套系统的价值就不只是"看缺陷"了,而是变成了一条可记录、可追溯的质量检测链路。

UI 那块的工程量其实不大,但很多细节特别影响体验:文件路径支持拖拽上传、检测结果带原图尺寸信息、异常图片能单独标记重查。这些功能在第一次写出可用的版本之后,再根据现场反馈逐步迭代加进去,比一开始憋一个巨大的 UI 需求要靠谱得多。

5. 实测效果与踩坑记录

5.1 光照和反光导致的误检

模型在测试集上指标不错,不代表线上就稳,第一个让我头疼的问题就是光照。晶圆本身有金属光泽,特定角度下会出现强反光,反光区域在图像里的形态和某些缺陷非常像,模型很容易把它们误检成 scratch 或 stain。测试集里的图片是我精心挑选的,光照条件比较统一,所以当时完全没暴露这个问题。

后来我的解决办法分两步走。第一步是数据层面,主动采集了不同光源角度、不同曝光时间下的正常晶圆样本,把它们标注成"无缺陷",作为负样本加入训练集。这一步非常关键,让模型真正见过"看起来像缺陷但实际不是"的样本,误检率降了一个量级。第二步是应用层面,在 UI 里加了一个置信度滑条,默认 0.25,现场操作员可以根据当前批次的误检情况实时调高或调低。虽然这治标不治本,但在排查问题期间能稳住产线。

这里也提醒一下,负样本不是随便拿正常图片堆进去就好,加入负样本后模型可能在正常图片上强行框出目标,所以要定期检查误检率,发现负样本过拟合就调整数量。

5.2 小目标漏检与 imgsz 选择

这个项目里最典型的痛点是小目标缺陷。晶圆上的颗粒污染很多只有十几个像素,在 640×640 的输入图里几乎只有几个像素大,YOLOv8 即便有增强的浅层特征图,对这种超小目标依然容易漏检。

试过两种思路。第一种是提升推理输入尺寸,把 imgsz 从 640 提到 960,小目标在特征图上的响应增大,mAP50 大概提升了三个点,但推理时间也从 120ms 涨到 250ms 左右,产线上还可以接受。第二种是切块检测,把一张高分辨率晶圆图切成若干 512×512 的 patch 分别检测,再把结果拼回去。这种方案对小目标更友好,但要处理边缘处的缺陷跨块问题,实现复杂一些,我短期只把它作为离线分析工具,没有放进实时检测流程。

另外,anchor-free 模型对小目标其实也有自身的限制:目标太小,在深层特征图上几乎没有响应,所以不能只依赖 FPN 的多尺度融合,需要结合输入分辨率和 patch 策略共同解决。这一点在项目规划阶段就应该想清楚,而不是等到漏检了才找方案,因为裁 patch 检测会对数据采集和标注流程提出额外要求,后期补会很被动。

5.3 后续优化方向

这套系统跑起来之后,可以延伸的优化方向其实很多。

如果对缺陷的精细形态有要求,比如要测量划痕的宽度、面积,可以把目标检测升级为实例分割,用 YOLOv8-seg 或者别的分割模型,训练数据需要把矩形框换成多边形轮廓,标注成本会高不少,但能得到像素级的缺陷区域,对工艺归因和缺陷面积统计都更有价值。

如果算力有限,可以走模型轻量化和加速的路线。把训练好的模型导出为 ONNX,再用 OpenVINO 或 TensorRT 做推理加速,在 CPU 上帧率能有明显提升。这一步对生产部署很实用,代码改动也不大,就是导出、加载、前处理对齐几个环节要仔细一点。

还有一个很容易被忽视的点是持续学习和模型维护。晶圆工艺会更新,新工艺可能产生新形态缺陷,旧模型就会性能退化。我现在的做法是把系统做成支持增量训练的形式,定期用最近的人工复检结果和误检样本重新微调模型,并保留一个模型版本管理,方便随时回滚到表现稳定的版本。这个机制虽然不如算法本身那么显性,但它决定了一套检测系统能不能长期稳定运行。

最后再分享一个小技巧:因为晶圆图片文件名往往带有批次、工艺、设备等信息,我在做结果保存时会把文件名解析出来,一并写进检测报告。这样后续一旦某个批次的缺陷率异常,可以直接按文件名关联回溯到当时的工艺条件。这个功能实现很简单,但在实际追溯质量问题的时候,价值非常大。

RELATED READING

延伸阅读

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