PCB电路板的缺陷检测做了这么多年,一直是视觉检测里又典型又磨人的一块。缺陷种类多、目标尺寸小、样本不均衡,传统图像处理做AOI经常误报率高得没法看。我最近把整个检测链路重做了一遍,基于YOLOv10从数据集整理、模型训练到UI界面封装,完整搭了一套PCB缺陷检测系统。这篇文章做个深度复盘,把里面涉及的技术取舍、训练细节、部署坑点和实测数据都拆开讲清楚。如果你正准备做工业视觉项目,或者想拿YOLO系列练一个真实落地场景,这篇应该能帮你省不少试错时间。
1. PCB缺陷检测的难点拆解:先想清楚要解决什么问题
很多人一上来就找数据集、训模型,忽略了最关键的环节——把检测需求本身拆清楚。PCB缺陷检测和通用目标检测不太一样,它有几个让模型训练很头疼的特点:缺陷目标极小、背景纹理复杂、类别之间形态相似、样本数量严重不平衡。如果不把这些搞清楚,后面调参就是无头苍蝇。
1.1 六类典型缺陷的视觉特征与检测难度
PCB缺陷数据集中最典型的六类缺陷分别是缺孔、鼠咬、开路、短路、毛刺和假铜。我先把它们的视觉特征和检测难度梳理一遍,这对后面理解模型行为非常重要。
| 缺陷类型 | 视觉特征 | 检测难点 |
|---|---|---|
| 缺孔 | 焊盘位置没有钻孔,圆形孔消失 | 目标太小,容易和正常反光混淆 |
| 鼠咬 | 线路边缘出现缺口,像被老鼠咬过 | 边缘缺陷,形状不规则,和毛刺很像 |
| 开路 | 导线中间断裂,信号中断 | 断口细微,需要较高分辨率才能看清 |
| 短路 | 两条导线之间意外连接 | 形态变化大,铜箔残留形式多样 |
| 毛刺 | 导线上多出一小段突出 | 细长形缺陷,长宽比极端 |
| 假铜 | 不该有铜箔的位置出现铜箔残留 | 位置随机,背景干扰强 |
这六类缺陷的共同点是尺寸都很小。拿开路和鼠咬来说,在很多数据集里占的图像面积可能只有几十个像素,已经属于小目标甚至极小目标范畴。而常见检测头本身对小目标的召回就偏弱,再加上PCB走线、焊盘、丝印这些高对比背景干扰,误判和漏检几乎是必然的。
1.2 传统视觉方案为什么越做越痛苦
我以前接触过用传统图像处理做PCB检测的产线方案,基本套路是:先做图像配准,再做差影法找差异,最后用连通域分析筛出缺陷区域。这套逻辑在标准化程度很高的批次上勉强能用,但一旦来了新板型、换了个光照环境、或者AOI成像的角度稍有变化,误报数直接起飞。产线工程师只能不断调阈值、加ROI区域、写特判逻辑,最后维护成本比开发成本还高。
深度学习检测方案的核心优势是:不需要为每类缺陷精心设计特征,而是让模型自己从大量标注样本里学习"缺陷在图像上到底长什么样"。检测流程简化为:
- 采集PCB板图像并标注缺陷位置和类别
- 训练目标检测模型,输出缺陷类别、置信度和边界框
- 在UI界面中加载模型,对单张图片、批量图片或摄像头实时画面进行推理
- 将检测结果叠加在原始图像上,生成缺陷统计和检测报告
1.3 系统需求盘点:模型、算力、交互三要素
动手之前我把需求列了一个清单,这是整个项目能不能收尾的基准线:
- 检测类别:六类典型PCB缺陷
- 检测方式:支持单张图片、批量图片、摄像头实时检测三种模式
- 运行环境:开发机上GPU训练模型,产线环境用普通工控机做推理
- 交互方式:图形化界面,操作人员不需要懂深度学习
- 输出要求:原图上叠加检测框,附带类别、置信度、检测数量统计和自动报告导出
这个需求清单直接决定了后面的选型方向。模型不能太重,否则工控机跑不动;UI必须轻量且稳定;数据集必须围绕这六类缺陷进行增强和平衡。把这些理清楚之后,才开始选模型框架。
2. YOLOv10的选型逻辑:无NMS设计到底改变了什么
选择YOLOv10而不是YOLOv8、RT-DETR或其他检测框架,并不是因为它更"新",而是因为它有几个点刚好打在PCB缺陷检测的痛点上。
2.1 从NMS到one-to-one:检测头的设计演进
传统YOLO系模型在推理阶段都依赖NMS(非极大值抑制)来过滤重复检测框。NMS的问题在于它的阈值不好调:阈值设高了,重叠目标被误删;阈值设低了,重复框堆在一起。工业场景里这很烦人,因为我们要给产线一个稳定的输出,不想因为一个置信度阈值的小差异导致检测结果忽多忽少。
YOLOv10改了这套逻辑,改用one-to-one的标签分配策略,让每个目标只匹配一个预测框,训练时就走端到端路线,推理时天然不需要NMS。这一步带来的直接收益是:
- 推理代码更简单,少了一个后处理环节,出错的点更少
- 延迟更低,每张图省掉NMS的时间,在批量处理时差距更明显
- 检测框输出更干净,重复框问题基本消失
我去掉NMS之后实测,单张推理时间大概能省下百分之十几,这对产线几百张图的批量检测来说是可感知的提速。
2.2 C2f、PSA与整体效率精度权衡
YOLOv10在骨干网络上保留了CSP风格的C2f结构,同时引入了部分自注意力模块PSA。PSA的意思不是全局都用注意力,而是只对部分特征通道做自注意力计算,避免全局注意力带来的巨量计算开销。这种"局部注意力"用在PCB这种目标小但背景纹理强的场景下,比单纯堆卷积更有效,模型能更集中地关注到细小的缺陷特征。
另一个值得说的是SCDown这种空间-通道解耦下采样。传统下采样直接卷积容易丢失小目标的细节信息,解耦操作把空间维度和通道维度的变化分开处理,信息损失更小。这对PCB线路边缘的细微缺口检测是有实际帮助的,我的训练曲线里,鼠咬和毛刺这两类的召回率在换用YOLOv10后有明显提升。
2.3 工业场景下的版本选型
YOLOv10提供了从n到x等多个尺寸版本。我实际测试时重点比较了nano和small两个版本,因为产线工控机通常没有强力GPU,需要平衡速度和精度。
| 模型版本 | 参数量 | 推理速度(工控机CPU) | 检测精度 | 适用场景 |
|---|---|---|---|---|
| yolov10n | 较小 | 快 | 偏低 | 实时摄像头巡检、低算力设备 |
| yolov10s | 适中 | 较快 | 较高 | 批量图片检测、大部分产线 |
| yolov10m | 较大 | 慢 | 高 | 离线高精度检测、有GPU环境 |
我最后选的是yolov10s作为主力,因为它在精度和速度之间最平衡,并且可以在GPU上训练后再导出ONNX模型部署到工控机上。选型的原则很简单:先跑通最轻的版本确认流程没问题,再逐步放大模型看精度增益是否划算。工业项目最忌讳一上来就用最大模型追求理论最高精度,部署时才发现算力撑不住。
3. 数据集构建与预处理:决定检测上限的地基
模型训练里有个很朴素的规律:数据决定上限,模型只是尽量逼近这个上限。PCB缺陷检测的数据集整理比训练本身更花时间,这块做扎实了后面调优能轻松一半。
3.1 数据来源与标注格式转换
我用的数据集来源于公开的PCB缺陷图像集结合自己补拍的一批测试板样本。公开部分包含六类缺陷的标注,格式是XML。YOLO系训练需要的格式是TXT,每行代表一个目标,分别是类别索引、归一化的中心点坐标和宽高,一共五个数:
class_id cx cy w h转换时需要逐个解析XML文件,读取物体的category和边界框坐标,再做归一化。这里有个容易踩的坑:XML里的边界框坐标可能是相对图像宽高的绝对值,也可能已经是相对值,得根据标签头判断清楚。我写转换脚本的时候因为没注意这一点,第一批数据全浪费了,坐标全部偏移出图像范围。转换脚本的核心逻辑长这样:
import os import xml.etree.ElementTree as ET def xml_to_yolo(xml_file, out_dir, class_names): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size').find('width').text) img_h = int(root.find('size').find('height').text) outputs = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h outputs.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") # 写入对应TXT文件转换后一定要做可视化校验,把TXT标注画回原图上检查边界框是否正确贴合缺陷位置。这一步不能省,工业数据集里错标、漏标会直接教坏模型。
3.2 数据增强策略的取舍
PCB图像的特点决定了数据增强不能乱来。因为缺陷和线路纹理高度耦合,旋转、缩放、裁剪这类几何增强还好,但颜色抖动需要谨慎:PCB板底色本身是固定色系的,过度改变颜色会让模型学到"颜色变了就不是PCV板"这种错误特征。
我的增强策略分成两档。训练阶段默认开启的光学增强包括轻度HSV变化、随机翻转、随机缩放,其中缩放范围控制在0.5到1.5倍之间。mosaic增强在训练前中期开启,它能把多张图拼接在一起,强制模型适应不同尺度目标混杂的场景。到了训练后期我会逐渐降低mosaic的概率,避免小目标在拼接过程中被裁掉。
实测下来,mosaic对提升开路、短路这类大范围缺陷的泛化很有帮助,但对鼠咬、毛刺这类极小目标反而不友好。解决办法是开启mosaic时设置较小的拼图粒度,并保证每个拼图块里的小目标不被裁剪出边界。
3.3 类别不均衡问题的处理
PCB缺陷数据集的类别分布非常不均衡。短路和毛刺样本通常较多,缺孔和鼠咬样本可能只有前者的三分之一甚至更少。模型对少样本类别容易欠拟合,推理时漏检率明显偏高。
我采用了两层处理。第一层是在数据层面做针对性的过采样,对缺孔、鼠咬这些类别重复采样,让参与训练的图片覆盖更多对应样本。第二层是在损失函数层面,给少样本类别更大的权重。YOLOv10的训练配置里可以手动调整每个类别的权重比例,我按照样本数量倒数归一化设置了分类损失权重,让模型在训练时更重视样本少的类别。两层配合下来,缺孔和鼠咬的召回率提升了十几个百分点。
4. 训练流程与调优实战:从收敛到稳定上量
数据集准备好后进入训练环节。这一部分我直接给出我自己验证过的训练方案,包括配置文件、命令和关键参数的解释,按步骤操作基本能跑通,但更重要的是理解每个参数背后的原因。
4.1 训练配置与启动
训练前先准备数据集描述文件,告诉模型图像路径和类别名称:
path: ./datasets/pcb # 数据集根目录 train: images/train # 训练图像目录 val: images/val # 验证图像目录 names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short 4: spur 5: spurious_copper我使用预训练权重启动训练而非从头开始训练。预训练权重已经在大规模数据上学习过基本特征表达,迁移到PCB场景时收敛速度更快、精度起点更高。训练命令:
yolo detect train data=pcb.yaml model=yolov10s.pt epochs=200 imgsz=640 batch=16 optimizer=AdamW lr0=0.001 patience=50参数里面的几个关键点:
- imgsz设为640是因为这个值在精度和显存占用之间比较均衡。PCB小目标多,我用过1280分辨率测试,确有小类别的召回提升,但训练时间翻了快四倍,产线推理也会拖慢。最终实际场景我选择了"训练用1280、部署用640"这种不对称方案,后面细讲。
- batch大小根据GPU显存来,我用的显卡16GB显存,batch=16正合适。显存越宽裕,batch可以越大,但注意过大的batch在小数据集上反而容易过拟合。
- lr0设成0.001,比默认的0.01更低,因为PCB数据集规模不算大,学习率太高一开始就会震荡,损失不降反升。
- patience=50表示连续50轮验证精度不提升就提前停,省时间。
4.2 训练过程的观察点
训练启动后,我一般不看总的损失曲线,而是看每类别的召回率变化。终端会输出每个类别的P、R、mAP50这些指标,重点关注少样本类别的指标是否稳步上升。
第一轮跑完200个epoch,我踩过最典型的坑就是过拟合。验证集上mAP50还能到0.85左右,但某个类别召回率特别低,比如鼠咬只有0.6出头。检查后发现训练样本里鼠咬样本本来就少,模型把这个类别的特征和毛刺搞混了。解决办法:
- 增加鼠咬样本的重复采样倍率
- 减少训练轮数到150,避免在小样本上反复拟合
- 开启更强的几何增强,让鼠咬缺口的形态变化更丰富
调整完再训练,鼠咬召回率提升到0.78以上。训练这种小目标缺陷模型,稀缺类别永远是优先关注的对象。
4.3 针对小缺陷的调优经验
PCB缺陷的目标尺度跨度极大。短路有时占据很大区域,蚊咬可能只有十几个像素。一套imgsz=640的配置很难两头都照顾好,我的调优经验总结如下:
提高输入分辨率是提升小目标召回最直接的手段。我把训练集分辨率提高到1280之后,缺孔和鼠咬的mAP50提升了近10个点。这个方案的问题是推理也想要1280时,工控机速度跟不上。最终方案是训练1280、推理640,用高分辨率训练让模型学到更精细的缺陷特征,再在推理时降回640。虽然理论上存在分辨率域差异,但实际效果比全程640好,这招对极小的缺陷目标很管用。
裁切子图训练。把2560×2560的大图裁成多个640×640的小图,每张小图只保留缺陷完整可见的区域,再喂给模型。这样等效放大了缺陷目标,模型在部署时直接对大图做滑窗检测或直接检测分辨率足够高的整图,效果比单纯缩放更好。缺点是训练样本数量变多,训练时间也会增加。
调整anchor参数。YOLOv10虽然是无NMS的设计,但anchor的具体尺寸对检测框的回归精度还是有影响。我统计了训练集中缺陷框的尺寸分布,发现大量目标宽高比集中在1:1到1:3之间,但也有一些线状缺陷长宽比超过1:5。于是我调整了anchors配置,让长条形的预置框更多一些。这个改动让毛刺和开路的定位精度提升明显。
这些经验没有太多理论玄学,全是拿数据砸出来的。每调一个参数,就跑一组实验对比指标,最后把有效策略组合在一起形成最终训练方案。
5. UI界面开发:把模型封装成可交付的工具
模型训完只是完成了第一步。产线工人不可能去跑命令行,也不可能看TensorBoard曲线,他们要的是一个界面简洁、操作直观的工具。UI并不是简单的"包一层壳",里面的线程管理、结果显示、批量处理都有不少细节。
5.1 界面框架与功能规划
界面框架我选的是PyQt5,生态成熟、控件丰富、打包成exe也方便。界面整体分成三大区域:
- 左侧操作面板:选择检测模式、加载模型文件、选择图片/视频/摄像头源
- 中间图像显示区:原始图像和检测结果叠加显示,缩放浏览细节
- 底部信息区:检测结果列表,包括缺陷类别、置信度、坐标信息,以及当前图像的缺陷统计
功能规划上我坚持一个原则:能一键完成的绝不让用户点两下。打开应用时自动加载默认模型权重,拖入图片自动检测,检测完自动生成报告。操作人员需要掌握的操作降到了最低。
5.2 多线程推理与交互体验的细节处理
推理放在主线程会导致界面卡死,这是很多新手做UI最容易犯的错。图片推理少则几百毫秒,多则一两秒,期间界面无响应看起来像崩溃了。我用QThread把推理放到后台线程,主线程只负责刷新界面:
class InferenceThread(QThread): result_ready = pyqtSignal(object, object) # 原图, 结果列表 def __init__(self, model, image): super().__init__() self.model = model self.image = image def run(self): results = self.model.predict(self.image) self.result_ready.emit(self.image, results) # 界面中调用 def start_detect(self, img): self.thread = InferenceThread(self.model, img) self.thread.result_ready.connect(self.show_result) self.thread.start()这里还有一个容易忽略的问题:PyQt5的界面刷新和模型推理如果共享同一个QThread对象,连续点击检测时会出现线程状态冲突。我每次点击都新建一个线程实例,并在启动前检查旧线程是否还在运行,避免资源竞争导致程序崩溃。
5.3 检测结果展示与批量导出
检测结果可视化是UI的核心价值。我给每个缺陷类别分配了固定颜色,边界框绘制用了抗锯齿画笔,置信度显示在框的上方。对不同类别还做了特殊区分:缺孔用红色,短路用橙色,鼠咬用黄色,深浅搭配让工人一眼能看出问题区域。
辅助的信息面板用表格展示当前图像的检测结果,包含序号、类别、置信度和检测框坐标。用户双击某一行,图像显示区会自动居中放大对应的检测框位置,这个功能在产线复核时特别好用,能让工人快速确认是不是真的缺陷还是误报。
批量检测功能方面,我支持选择一个文件夹,程序自动遍历所有图片,逐张推理并将结果保存到指定目录。保存内容包括:叠加检测框的标注图、CSV格式的检测明细、每张图的缺陷统计summary。批量处理时用线程池并行处理多张图,每张图一个线程,同时最多处理四张,兼顾了速度和工控机的性能限制。
UI做完之后整个系统才算闭环:图片进来,检测框出来,结果能追溯能导出。这才是能交给产线用的工具。
6. 模型导出部署与现场实测:数字背后的真实表现
训练和UI都完成后,最后一个关卡是部署。PyTorch模型文件不能直接拿给产线用,需要导出成推理性能更好的格式,还得针对现场环境做实测和优化。
6.1 模型导出与推理引擎选择
我用了两种导出方案。第一种是导出ONNX通用格式,适用于绝大多数运行环境,尤其CPU推理。导出命令非常简单:
yolo export model=best.pt format=onnx opset=12 simplify=True导出时加simplify=True可以做计算图简化和常量折叠,模型体积略有缩减。导出的ONNX文件配合onnxruntime在CPU上推理,速度比PyTorch原模型快不少。第二种是如果有NVIDIA GPU,可以进一步导出TensorRT格式,延迟能再降低一个量级,适合对实时性要求高的产线摄像头检测。
导出之后一定做输入输出维度的检查。我遇到过导出的ONNX输出名和预期不一致的情况,因为YOLOv10输出没有NMS后处理,输出张量直接是预测框的坐标和目标分数。用onnxruntime推理时,需要手动解析这个输出张量并提取检测框,不能直接套YOLOv8的解析代码。解析逻辑大概是:
import onnxruntime as ort import numpy as np ort_session = ort.InferenceSession('best.onnx') input_name = ort_session.get_inputs()[0].name preds = ort_session.run(None, {input_name: img_array})[0] # preds 形状为 [1, num_queries, 6] 或 [1, 84, 8400],需按模型实际输出解析这是我当时踩过最深的坑,因为网上教程大多沿用NMS版本模型的解析代码,直接照抄肯定报错。解决方案就是先打印输出张量的shape,再对着训练时的模型输出结构理解每一维的含义。
6.2 实测精度与速度
我用验证集跑了一轮最终指标,也记录了不同部署环境下的推理速度,给一个参考数据:
| 部署环境 | 推理耗时/张 | 备注 |
|---|---|---|
| GPU(训练机) | 约15ms | 速度最快 |
| CPU(工控机,ONNX) | 约320ms | 批量检测可接受 |
| CPU(工控机,原PyTorch) | 约680ms | 明显偏慢,不建议 |
验证集总的mAP50达到了0.89,其中短路和假铜这两类效果最好,缺孔和鼠咬相对差一些但也能接受。现场最关心的是漏检率,我把六类缺陷分别统计了召回率,报告里明确标注了每类缺陷的置信度阈值建议。操作人员在UI里可以调置信度阈值,追求低漏检就调低一点,代价是误报多一点;追求低误报就调高一点,工人复核压力小一些。实际使用中,我倾向于将置信度阈值设在0.25到0.35之间,既保证缺陷不遗漏,误报数量也在可接受范围内。
6.3 现场误检案例复盘与处理
实测阶段最容易暴露问题的地方不是纯技术指标,而是各种"看起来像缺陷但其实不是"的干扰情况。
第一个典型误检是丝印文字和焊盘反光被识别成假铜或者短路。PCB板上白色的丝印字符边缘锐利,在特定光照下会产生高光区域,模型容易误判。这个不能靠重新训练解决,因为在训练数据里这类负样本太少。对策是在数据集中加入了大量无缺陷的PCB板图作为负样本,让模型见过更多正常板的形态。加入负样本之后误检数量明显下降。
第二个问题是摄像头实拍图与数据集图像的噪声特征不同。数据集里的图片是经过良好光照的高清扫描图,而产线摄像头可能带轻微摩尔纹、压缩噪声或者反光不均。我实测时发现识别率整体下降,解决方案是对每个新拍摄环境采集几十张背景图加入训练集,做一个简单的domain adaptation。跑一次训练,部署到对应工控机上,问题就解决了。
第三个问题是当一块板上同时出现多个缺陷时,过于密集的检测框相互干扰。YOLOv10的无NMS设计在这里反而体现出优势,不存在NMS把相邻缺陷框吞掉的问题。但我还是发现当两个同类缺陷距离很近时,模型偶尔只输出一个框。这大概率是训练时边界框标注不够细致导致的。我的处理策略是重新检查这些密集区域的标注,确保每个缺陷边界框准确覆盖目标,避免框位偏移误导模型。
6.4 最终交付形态与使用反馈
整套系统交付后,我的交付物包括:训练好的best.pt模型和best.onnx模型文件、完整的Python源码、UI可执行程序和一份使用说明文档。说明文档里写了如何加载模型、如何切换检测模式、如何调整置信度阈值、如何批量检测并查看报告,产线工人照着操作即可。
从实际反馈来看,这套系统在正常规范操作的PCB测试板上,能将人工复检比例降低一半以上。当然它不是万能的,现场依然有少数极端案例需要人工确认,比如板面严重氧化、钻孔严重偏位导致特征模糊,这类情况模型输出不稳定是正常且合理的。产线的质量部门能够接受这种"AI辅助判定,人工最终确认"的工作模式。
最后说一个我个人在实操中体会最深的事情:做工业视觉项目,不要在模型结构上追求最花哨,要在数据质量和工程稳定性上多花时间。YOLOv10本身是成熟稳定的工具,真正决定项目成败的是你有没有把数据整理干净、有没有把边界情况想清楚、有没有把UI做得足够简单可靠。哪怕换一个缺陷检测场景,这套从数据、训练、调优、UI、部署整个链路的方法论依然可以直接复制。希望这篇复盘能帮你少走一些弯路,如果正在做类似项目,照着这个流程走一遍,大概率能顺利交付。