简介:面向目标检测方向的学习者、PCB制造与质检领域的开发人员,数据包内含可直接投入训练的PCB缺陷数据集,适合课程设计、毕业课题或工业质检算法验证。数据集包含1000张真实场景高质量图片,图片涵盖多种缺陷类型与拍摄角度,贴近真实产线环境;采用LabelImg标注,并提供VOC、COCO、YOLO三种格式标签(xml/json/txt),标签分别存放于对应文件夹,免去手工格式转换,适配YOLO系列模型快速训练与评估。压缩包内共约2000个文件,以xml与txt标签文件为主体,另有HTML环境配置与训练教程、Python数据划分脚本、YAML模型配置文件等,整包约275.87MB。配套内容包括Windows和Linux双平台下的YOLO环境搭建、案例训练教程,以及训练集、验证集、测试集划分脚本,能帮助读者从数据准备到模型训练形成完整闭环。目前已有713人学习下载,整体目录清晰,既适合初学者入门,也便于中高级开发者按需调整数据与训练流程。
1. PCB缺陷检测为什么值得用YOLO:从一张板到一套数据集
一块PCB板上,开路、短路、缺孔这类缺陷往往只有几百微米,人工镜检一天下来眼睛发花,AOI设备扫出来一堆可疑点再让人复判,效率瓶颈就卡在这里。而YOLO目标检测在工业质检里已经被反复验证:速度快、部署直接、小目标也能调。问题是,做YOLO训练不是拿到图就能跑的,标签格式、划分方式、训练参数,每一项都能让人卡上好几天。这个标题里给的方案,恰好是把这件事做成一套交钥匙工程:1000张PCB缺陷图,VOC、COCO、YOLO三种格式标签都配齐,外加划分脚本和训练教程,你拿到手不用再处理格式转换这类脏活,可以专心调模型。适合刚接手AOI视觉算法的工程师、做PCB质检设备选型的技术负责人,以及想用目标检测做工业小目标识别的从业者。下面我按拿到这套东西之后的实际路径来拆:先讲三种格式怎么互转,再讲划分和训练,最后落在一线踩过的坑上。
2. VOC、COCO、YOLO三种标签格式:转换脚本与坐标换算的血泪坑
标题里写了三种格式标签都给你,很多人第一反应是“不就是改个后缀吗”,实际不是。三种格式的存储结构、坐标表达、类别映射完全不同,混着用轻则训练报错,重则模型训出来框全偏。这一章先把三种格式的底摸清,再给两个我常用的转换脚本。
2.1 三种格式分别存了什么
VOC格式是每张图对应一个XML文件,框用绝对像素坐标 (xmin, ymin, xmax, ymax) 表示,类别是字符串,比如<name>short</name>。COCO格式是全局一个JSON文件,所有标注塞在annotations数组里,框用绝对像素的 (x, y, w, h) 表示,类别用数字ID映射到categories。YOLO格式是每张图一个TXT文件,每行一个框,写法是“类别序号 cx cy w h”,全部是0到1的归一化值。
三种格式对同一批框,存的却是三套不同的数学表达。转换时最容易翻车的点就在坐标换算和类别映射。
| 格式 | 存储方式 | 框坐标 | 归一化 | 类别 |
|---|---|---|---|---|
| VOC | 每张图一个XML | (xmin, ymin, xmax, ymax) | 无 | 字符串 |
| COCO | 全局一个JSON | (x, y, w, h) | 无 | 数字ID从1开始 |
| YOLO | 每张图一个TXT | (cx, cy, w, h) | 有 | 数字序号从0开始 |
2.2 从VOC转成YOLO TXT:坐标换算脚本
最常见的落地路径:先用LabelImg标注出VOC格式,再用脚本转成YOLO的TXT去训练。我一般维护一个classes.txt来确定类别顺序,因为YOLO的类别序号完全靠这个TXT决定。
import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path("labels/voc") # 原始XML目录 out_dir = Path("labels/yolo") # 输出TXT目录 out_dir.mkdir(parents=True, exist_ok=True) # 类别顺序就是YOLO的类别ID,改顺序必须同步改 data.yaml classes = ["open", "short", "missing_hole", "spur", "mouse_bite"] for xml_file in voc_dir.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.findtext("./size/width")) img_h = int(root.findtext("./size/height")) lines = [] for obj in root.iter("object"): name = obj.findtext("name") if name not in classes: continue cls_id = classes.index(name) bndbox = obj.find("bndbox") xmin = int(bndbox.findtext("xmin")) ymin = int(bndbox.findtext("ymin")) xmax = int(bndbox.findtext("xmax")) ymax = int(bndbox.findtext("ymax")) # 中心点坐标 = (min + max) / 2,除以整图宽高完成归一化 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") out = out_dir / (xml_file.stem + ".txt") out.write_text("\n".join(lines) + "\n", encoding="utf-8")逻辑说明:中心点坐标是(xmin + xmax) / 2,然后分别除以整图的宽或高。注意这里除的是整图尺寸,不是框自己的宽高——我见过有人把 x_center 除以框宽,结果标签全飞出去,这是转格式里最低级的翻车。
参数说明:
classes列表的顺序就是YOLO训练时用的类别ID,一旦定下来就不能变,后续data.yaml里的 names 必须和这里的顺序一致。- 如果XML里有
<difficult>1</difficult>的难例,上面代码没做过滤,我建议加一个判断只转difficult=0的框,否则训练时会把模棱两可的标注当硬标签,损失函数震荡得更厉害。 - 每行保留6位小数,对PCB这种几千像素大图足够,但对只有10像素宽的缺陷框,建议改成
.9f保留9位,防止归一化后精度丢失导致框错位。
2.3 从YOLO TXT回拼COCO JSON:评估阶段用得上
训练用YOLO格式,评估却想跑官方COCO mAP脚本,这时就需要把TXT拼成JSON。注意COCO的类别ID从1开始,YOLO从0开始,这个偏移量必须补上。
import json from pathlib import Path from PIL import Image sets = ["train", "val"] label_dir = Path("labels/yolo") img_dir = Path("images") # 类别顺序和 data.yaml、训练时保持一致 categories = ["open", "short", "missing_hole", "spur", "mouse_bite"] images, annotations = [], [] ann_id = 1 for split in sets: for img_path in (img_dir / split).glob("*.jpg"): img_id = len(images) + 1 w, h = Image.open(img_path).size images.append({ "id": img_id, "file_name": str(img_path), "width": w, "height": h, }) txt = label_dir / split / img_path.with_suffix(".txt").name if not txt.exists(): continue for line in txt.read_text().strip().splitlines(): cat_id, cx, cy, bw, bh = line.split() # YOLO存的是归一化中心点,先转回绝对像素左上角 x = (float(cx) - float(bw) / 2) * w y = (float(cy) - float(bh) / 2) * h annotations.append({ "id": ann_id, "image_id": img_id, "category_id": int(cat_id) + 1, # COCO从1开始,YOLO从0开始 "bbox": [x, y, float(bw) * w, float(bh) * h], "area": float(bw) * float(bh) * w * h, "iscrowd": 0, }) ann_id += 1 coco = { "images": images, "annotations": annotations, "categories": [{"id": i + 1, "name": c} for i, c in enumerate(categories)], } Path("coco_annotations.json").write_text(json.dumps(coco), encoding="utf-8")逻辑说明:YOLO行里存的是归一化后的中心点坐标和宽高,回拼COCO时要先除以归一化系数还原成绝对像素,再转成左上角坐标。area字段是给COCO做小/中/大目标分组统计用的,不填后面分析小目标性能时找不到依据。
参数说明:
category_id必须 +1,很多人在这一步翻车,训练结果看起来没问题,一跑评估脚本全部报类别不匹配。- TXT里如果出现负坐标或超出图片边界的框,COCO评估脚本会报错甚至静默跳过。建议加一道清洗逻辑:把 x, y 限制在 [0, w] 和 [0, h] 内,宽高最小为1。
- 这里只取了 train 和 val,如果要做测试集,记得把 test 也加进
sets列表里,否则测试集没有标注信息。
2.4 三种格式配齐的真正价值
数据集作者把三种格式都给你,不是为了让你选一种用,而是让你在一条流水线里同时用上:VOC格式在LabelImg里做人工复核最方便,YOLO格式直接喂给训练器,COCO格式用来跑官方评估脚本横向对比不同模型。三种格式的边界清楚了,后面出了诡异问题你才能判断是标注坏了还是训练坏了。
3. 划分脚本:训练集验证集按板子分,不按图片分
标题里的“划分脚本”值得单独拿出来讲。很多人拿到数据集第一步就是train_test_split随机切,但对PCB缺陷数据,这样做会埋下大雷。
3.1 为什么不能直接随机打乱
1000张图里缺陷不是均匀分布的。假设短路类只有80张,缺孔类有400张,随机划分后验证集里很可能一张短路都没有,mAP指标虚高,到了实际产线一碰到短路就漏检。更隐蔽的问题是同源污染:如果你的图片命名带着板子编号,比如panel_001_short.jpg、panel_001_open.jpg,随机切分会把同一块板的不同缺陷图分到训练和验证两边,验证集里全是训练集见过的板子,评估成绩自然好看,但一换新板子就崩。
3.2 按文件名前缀分组的划分脚本
我常用的做法是:先按板子编号分组,再对组做随机,保证同一块板的图永远在同一侧。
import random from pathlib import Path random.seed(42) # 固定种子,复现划分结果 img_dir = Path("images/all") # 未划分的原图目录 train_out = Path("images/train") val_out = Path("images/val") train_out.mkdir(parents=True, exist_ok=True) val_out.mkdir(parents=True, exist_ok=True) samples = list(img_dir.glob("*.jpg")) # 按文件名前两段分桶,假设格式是 panel_001_short.jpg groups = {} for p in samples: parts = p.stem.split("_") prefix = parts[0] + "_" + parts[1] groups.setdefault(prefix, []).append(p) groups = list(groups.values()) random.shuffle(groups) val_ratio = 0.2 val_groups = groups[: int(len(groups) * val_ratio)] val_files = {p for g in val_groups for p in g} for p in samples: dst = val_out if p in val_files else train_out p.rename(dst / p.name)逻辑说明:核心是“先分组再随机”,不是对单张图随机。这样保住了同源数据不串集这条底线。如果你的文件名不是这个格式,改一下prefix的取值逻辑就行,关键是找到能代表“同一物理板”的标识。
参数说明:
val_ratio=0.2对1000张图来说验证集有200张,够稳定。如果你的类别极度不平衡,建议改成先按类别抽比例再合并,或者直接用分层抽样。random.seed(42)必须固定,否则每次运行划分结果不同,训练复现性直接归零。- 这个脚本只移动了图片,标签还没跟着走,具体对齐方式见下一节。
3.3 标签跟着图片走:划分脚本的边界
最容易犯的错是只移动图片,标签留在原目录,训练时YOLO报 warning 找不到对应txt,或者读到空文件。需要同步移动VOC和YOLO的标签。
import shutil from pathlib import Path # 移动图片后,把对应标签也移过去 for split in ["train", "val"]: src_img_dir = Path("images") / split for img in src_img_dir.glob("*.jpg"): for name in ["voc", "yolo"]: ext = ".xml" if name == "voc" else ".txt" src_label = Path("labels") / name / img.with_suffix(ext).name dst_dir = Path("labels") / name / split dst_dir.mkdir(parents=True, exist_ok=True) if src_label.exists(): shutil.move(str(src_label), str(dst_dir / src_label.name))逻辑说明:VOC和YOLO是按单张图存储的标签文件,必须跟着图片走。COCO是全局JSON,不参与移动,它应当在整个划分完成后再重新生成,直接指向images/train和images/val目录。
提示:如果你的划分脚本是p.rename()移动图片,而标签目录是另一棵目录树,务必按同样的规则移动标签。先删掉原来松散的文件再重新划分,比增量移动更省心。
3.4 划分完要做的三分钟自检
一个简单有效的自检:分别统计 train 和 val 的图片数量,然后按类别统计每个集合里包含该缺陷的图片数。如果 val 里某个类别的图片数为0,就得重新划分。我一般直接跑一个小脚本循环读TXT统计,一分钟出结果。这个自检不要省,我在这上面翻过一次车——验证集里没有开路缺陷,模型对开路全漏检,mAP还挺漂亮,一上线立刻露馅。
4. 训练流程:从data.yaml到YOLO参数调优的落地配置
环境准备好之后,下一步就是训练。这一章不聊算法原理,直接给一套能在PCB缺陷数据上跑通的配置,并把每个关键参数的取舍讲清楚。
4.1 目录结构与data.yaml的三个必填字段
拿到手的资源解压后,先整理成统一的目录结构才能喂给YOLO。我一般把数据集中到一个根目录下:
pcb_defect/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml然后编辑data.yaml:
# data.yaml path: ./pcb_defect train: images/train val: images/val names: 0: open 1: short 2: missing_hole 3: spur 4: mouse_bite参数说明:
path是项目根目录,train和val是相对于path的子目录。不写path时,train和val必须写绝对路径。names的索引顺序必须和TXT标签里的类别序号完全一致。如果VOC转YOLO时classes顺序是["short", "open", ...],这里的names也必须同步,否则类别标签错配,模型训练完mAP看着高,实际检出的全是错类。- 不要用中文做类别名,一些绘图和评估脚本对Unicode支持不稳定,报错时很难排查。
4.2 关键训练参数:imgsz、batch、mosaic、close_mosaic
训练命令推荐这样写:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ epochs=300 \ imgsz=1024 \ batch=16 \ device=0 \ patience=50 \ mosaic=1.0 \ close_mosaic=10 \ workers=8逐项说参数取舍:
model=yolov8s.pt:在预训练权重基础上微调。1000张图不足以从头训练,用COCO预训练权重做迁移学习收敛速度快很多,这是这个场景最常见的做法。imgsz=1024:PCB缺陷多是小目标,640在工业场景不够用,1024能把短路断口、缺孔边缘放大。显存不够就降到960,或用yolov8n减小模型体量。batch=16:如果训练过程中loss变成 nan 或剧烈震荡,优先调小 batch,而不是调学习率。PCB图背景单一,batch 太大容易过早过拟合。mosaic=1.0、close_mosaic=10:前90%的训练轮次开启马赛克增强,最后10轮关闭。这是让模型在最后阶段看真实分布、稳定输出的常用配置,不要自己乱改。patience=50:50轮不涨就停。PCB数据量小,没必要硬跑满300轮。
4.3 训练过程中的可观测信号:loss只看相对变化
训练日志里会输出cls_loss、box_loss、dfl_loss三部分。网上关于“yolo损失函数”的讨论很多,但实操里你只需要关注相对变化:
- 前10轮
box_loss从30左右快速降到个位数,说明模型在正常学习。 - 前50轮
box_loss几乎不动,说明学习率太小、标签噪声太大,或者数据增强太猛把真实标注盖住了。 - 某个 epoch 突然
cls_loss变 nan,优先查TXT里有没有全零文件或非法值,比如归一化坐标超过1。这些脏标签在locate问题图片时用脚本一查就能找出来,不要重启训练硬碰运气。
4.4 推理时的置信度阈值:产线取舍
训练完用 best.pt 预测:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=images/test \ conf=0.25 \ iou=0.45 \ imgsz=1024conf的选取要看产线逻辑。PCB缺陷属于“背景干净、目标极小”的场景,conf可以调到 0.35-0.45,因为PCB缺陷框本来就少,宁可少输出假阳也避免大量误报。但如果做的是终检,漏检的代价远大于误报,这时候把conf降到 0.15,让模型把可疑点全报出来,靠后面人工复判兜底。这个取舍直接由产线节拍决定,没有绝对正确的值。
5. PCB检测避坑:五个让YOLO模型翻车的高频问题与排查
这几条是我在PCB缺陷检测训练里反复遇到的真实故障,按“现象 → 原因 → 解决”写,方便你直接对着排查。
5.1 训练中途BN层崩溃,loss 直接变 nan
现象:训练十几轮后 loss 突然变成 nan,日志里还能看到 BatchNorm 相关报错,有人叫它“BN崩溃”,也有人叫“模型微调崩了”。
原因:YOLO预训练权重里的BN统计量是从COCO学来的,PCB图像和COCO的分布差异极大,加上 batch 太小,BN 的统计量在训练早期剧烈震荡后崩了。
解决:最常见做法是把 batch 调到 16 以上,让BN统计量更稳定;如果显存不够就换yolov8n。另外确认学习率没超出默认范围,不要为了加速收敛一上来就把 lr 调高一倍,PCB小数据集的BN层经不起这个折腾。
5.2 混淆矩阵总和对不上图片数
现象:训练完打开confusion_matrix.png,把预测框和标签框相加,数量比图片里的缺陷总数多出一截,有人误以为模型出了问题。
原因:YOLO的混淆矩阵是按“框”算的,不是按“图”算的。NMS合并了一部分重复框,还漏检了一些框,加上每个图可以有多个缺陷框,矩阵里的各项对不上原始图片数量是正常的。
解决:看混淆矩阵时别管总数,只看对角线之外的错误分布——哪些类别互相误检、哪个类召回率最低。对角线数值占比越大,模型区分力越好。
5.3 标签精度丢失,预测框比GT框小一圈
现象:训练完画出预测结果,框比人工标注的GT框明显小一圈,位置倒是准的。
原因:YOLO TXT里小框的归一化宽高值太小。比如框宽2像素、图像宽4096像素,归一化后约0.000488,如果脚本只保留6位小数,某些场景下会变成0.0005,误差被放大到原图好几个像素。
解决:转换脚本里坐标统一保留9位小数;更省事的做法是转一次定稿,之后所有修改都在YOLO格式上做,不要反复在VOC和YOLO之间来回转,折腾一次就丢一层精度。PCB小缺陷就是这个问题的重灾区。
5.4 验证集和训练集同源,mAP好看现场漏检
现象:训练时 mAP50 高到0.9以上,拿到产线新拍的照片上一测,漏检严重,同一个板位反复检测不过。
原因:划分脚本按单张图片随机切分,同一块板的不同缺陷图被分到了 train 和 val 两侧,模型相当于带着答案考试,新板子自然露馅。
解决:用第3章的方法,按板子编号分组后再划分。划分完做个类别统计自检,确认 val 里每个类别的图片数都不是0。
5.5 低频缺陷类别完全检不出
现象:1000张图里短路缺陷只有30张,训练完混淆矩阵显示短路这一类 precision 和 recall 都是0。
原因:Mosaic增强虽然增加了样本多样性,但短路这类极小缺陷在拼接过程中被缩放得更小,模型根本学不到有效特征。正样本过少,训练器会把这类当噪声。
解决:先做类别频率统计,对低频类单独做复制粘贴增强——把短路缺陷的区域从原图裁下来,随机贴到干净的PCB背景上,生成额外样本。也可以按第6章的迁移学习方式,用预训练模型在小样本上更容易稳定起步。
6. 1000张图的迁移学习:冻结策略与验证集上的置信度校准
1000张图做PCB缺陷检测,最大的顾虑是样本不够。我的习惯是:不从头训练,直接加载COCO预训练权重做迁移学习,并且冻结部分主干层,让模型在保留通用特征提取能力的同时,只微调检测头。
6.1 冻结主干层的训练命令
Ultralytics 框架里用freeze参数指定冻结的层数:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ epochs=200 \ imgsz=1024 \ batch=16 \ freeze=10 \ patience=30freeze=10表示前10层不参与梯度更新。PCB缺陷的边缘、纹理等浅层特征和通用目标有差异,冻结过多会导致模型学不到PCB特有的表征;冻结太少又起不到防过拟合作用。我一般先试freeze=10,如果过拟合严重(训练loss降、验证loss不降),再往上加。
6.2 在验证集上做一次置信度校准
训练结束后,不要只盯着总mAP,按类别拆开看。用 YOLO 自带的验证接口快速出结果:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(data="data.yaml", imgsz=1024) print(results.box.map) # mAP50-95 print(results.box.map50) # mAP@0.5 print(results.box.conf) # 每个类别的置信度列表重点看每个类别的置信度分布,而不是总平均。PCB缺陷类别如果 mAP50 低于0.7,我不建议直接上线,因为现场的光照、板面颜色变化会让分数再降一档。把 conf 阈值在验证集上扫一遍,找到 precision 和 recall 的平衡点,再拿这个阈值去推理,比凭感觉填 0.25 靠谱得多。
我现在的习惯是:拿到任何新PCB数据集,先跑一次完整训练拿 baseline 和混淆矩阵,再谈改网络结构。没有这个基准,后面所有调参都是空中楼阁。希望这篇能帮你在PCB缺陷检测上少踩几个坑。
本文还有配套的精品资源,点击获取