基于YOLOv8的晶圆缺陷检测系统:从传统视觉到深度学习实战
晶圆缺陷检测这个方向在半导体制造的品质管控里一直是块硬骨头。我最早接触这类需求是被产线上的人工目检效率逼着入门的——一片晶圆上几百个 Die靠人眼在显微镜下逐个排查漏检率不低而且人员疲劳导致的误判很致命往往同一缺陷不同班次的人给出的结论都不一样。后来尝试了传统机器视觉方案阈值分割、模板匹配在简单背景下还能用一旦遇到划痕和颗粒混合、光照不均匀的情况参数怎么调都打架今天调好明天换一批产线样品又不行了。再往后就是转向 YOLOv8把检测任务当成目标检测来做整个思路才顺过来。这篇文章就围绕基于YOLOv8的晶圆体缺陷检测系统来展开覆盖 YOLO 数据集的制作、模型训练、UI界面封装和 Python 源码组织这几块。适合刚入行想落地工业视觉检测的同学参考也适合已经有传统视觉基础、想换深度学习方案的朋友。我会把整个链路拆开讲重点说清楚每一步的取舍逻辑以及那些只跑通流程根本遇不到的坑。1. 晶圆缺陷检测为什么要从传统视觉转向 YOLOv81.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.pt3.1 选哪个规格的 YOLOv8 模型YOLOv8 官方提供了 n、s、m、l、x 五种规格区别主要体现在深度和宽度上参数量和推理时间依次增加精度也随之提升。型号选择不是越大越好关键看部署场景和硬件条件。模型规格参数量约推理速度适用场景YOLOv8n3.2M最快CPU 或低算力边缘设备快速原型验证YOLOv8s11.2M快普通 GPU批量离线检测YOLOv8m25.9M中等精度要求较高的离线分析YOLOv8l43.7M较慢高精度场景需要较好 GPUYOLOv8x68.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( datawafer_defect.yaml, epochs120, imgsz640, batch16, lr00.01, patience20, device0, workers8, )几个关键参数我展开说一下。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_threshold0.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( sourceself.image_path, confself.conf_threshold, iou0.45, imgsz640, verboseFalse ) 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 上帧率能有明显提升。这一步对生产部署很实用代码改动也不大就是导出、加载、前处理对齐几个环节要仔细一点。还有一个很容易被忽视的点是持续学习和模型维护。晶圆工艺会更新新工艺可能产生新形态缺陷旧模型就会性能退化。我现在的做法是把系统做成支持增量训练的形式定期用最近的人工复检结果和误检样本重新微调模型并保留一个模型版本管理方便随时回滚到表现稳定的版本。这个机制虽然不如算法本身那么显性但它决定了一套检测系统能不能长期稳定运行。最后再分享一个小技巧因为晶圆图片文件名往往带有批次、工艺、设备等信息我在做结果保存时会把文件名解析出来一并写进检测报告。这样后续一旦某个批次的缺陷率异常可以直接按文件名关联回溯到当时的工艺条件。这个功能实现很简单但在实际追溯质量问题的时候价值非常大。