资讯详情 PCB缺陷数据集实战:VOC与YOLO双格式标签解析与YOLO训练避坑指南
📅 2026/10/10 23:40:44
简介本资源为面向PCB缺陷检测任务的图像数据集适合从事工业质检、深度学习目标检测的开发者与研究人员使用可用于训练与验证缺陷识别模型。数据集覆盖六类常见PCB缺陷包括Missing_hole、Mouse_bite、Open_circuit、Short、Spur、Spurious_copper标注精确、类别分布均匀有助于拟合效果较好的检测模型。压缩包共1386个文件由693张jpg图像与693个xml标注文件组成整体约907.52MB标签同时提供VOC与YOLO两种格式方便直接接入主流检测框架。目前已有2904人学习下载数据按类别编号组织便于快速划分训练集与验证集减少前期清洗与格式转换成本适合作为课程设计、科研实验或工程原型的训练数据基础。1. 拿到 PCB 缺陷数据集先别急着训练2700 张、双格式标签到底意味着什么刚入行做工业质检视觉那会儿我最容易犯的错就是看到「数据集」三个字先wget下来解压然后直接往 YOLO 里一丢开始跑。跑完一看 mAP 还行上线一测全是误报回头翻数据才发现标签格式根本没对齐。PCB 缺陷数据集 2700 张含 VOC 和 YOLO 两种格式标签这个标题里其实藏了三个关键信息规模是 2700 张属于中小规模够做迁移学习微调不够从零训大模型、任务类型是缺陷检测不是分类是带框的目标检测、标签给了 Pascal VOC 的 XML 和 YOLO 的 TXT 两套。对做 PCB 外观质检、AOI 复判、或者拿嘉立创 EDA 画完板子想做缺陷检测练手的工程师来说这套数据最大的价值不是「大」而是「双格式」——它省掉了你自己写 VOC 转 YOLO 脚本那一步但也正因为两套标签并存坐标系、类别映射、空标签文件这些坑会集中爆发。这一章先把这套数据能干什么、不能干什么说清楚后面几章再拆怎么用、怎么避坑。2. 先搞懂 VOC 与 YOLO 标签的坐标系差异为什么直接混用必翻车2.1 两套标签描述的是同一个框但原点、单位、归一化方式全不一样Pascal VOC 的标注文件是 XML一个缺陷框长这样bndbox里有xmin、ymin、xmax、ymax这四个值是像素绝对坐标原点在图片左上角x 向右、y 向下。YOLO 的 TXT 标签则是每行一个框class_id x_center y_center width height这五个值里后四个都是相对整图宽高的归一化值0~1 之间中心点坐标而不是角点。很多人第一次转格式时把xmin/ymin直接当中心点写进去训练 loss 能降但框全偏这就是典型的坐标系没对齐。我一般会先写一个校验脚本把两种标签各自画到图上对比确认转换逻辑没错再往下走。下面这段代码读一张图的 VOC XML 和对应 YOLO TXT把框画出来存成对比图跑几张就能肉眼看出对不对。import xml.etree.ElementTree as ET import cv2 import os def voc_to_xyxy(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) boxes [] for obj in root.findall(object): name obj.find(name).text bb obj.find(bndbox) xmin float(bb.find(xmin).text) ymin float(bb.find(ymin).text) xmax float(bb.find(xmax).text) ymax float(bb.find(ymax).text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes def yolo_to_xyxy(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid, xc, yc, bw, bh map(float, parts) xmin (xc - bw / 2) * img_w ymin (yc - bh / 2) * img_h xmax (xc bw / 2) * img_w ymax (yc bh / 2) * img_h boxes.append((int(cid), xmin, ymin, xmax, ymax)) return boxes # 用法分别读同一张图的 xml 和 txt画框对比 img cv2.imread(sample.jpg) h, w img.shape[:2] _, _, voc_boxes voc_to_xyxy(sample.xml) yolo_boxes yolo_to_xyxy(sample.txt, w, h) for name, x1, y1, x2, y2 in voc_boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) for cid, x1, y1, x2, y2 in yolo_boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 1) cv2.imwrite(compare.jpg, img)逻辑说明VOC 分支直接读像素坐标YOLO 分支先把归一化中心点还原成像素角点绿色是 VOC 框、红色是 YOLO 框如果两者基本重合说明这套数据的双格式是对齐的如果红框整体偏移或大小不对说明转换时宽高取反了或者用了错误的图片尺寸。参数上唯一要注意的是img_w、img_h必须用标注时对应的原图尺寸不能拿缩放后的图去还原否则归一化反算全错。2.2 类别映射表要先固定下来别让 class_id 在不同脚本里各说各话VOC 里类别是字符串比如missing_hole、mouse_bite、open_circuit、short、spur、spurious_copper这类 PCB 常见缺陷名YOLO 里是整数class_id。这套数据如果两种格式都给全了理论上 class_id 和类别名的对应关系是固定的但实际拿到的数据里经常出现「VOC 有 6 类、YOLO 只用了 4 个 id」或者「id 从 1 开始而不是 0」的情况。我的习惯是先扫一遍所有标签把类别集合和 id 集合都统计出来生成一份classes.txt和映射字典后续训练、推理、可视化全部引用同一份避免训练时 id0 是 missing_hole、推理时 id0 变成 mouse_bite 这种玄学问题。import os, glob from collections import Counter voc_names Counter() yolo_ids Counter() for xml in glob.glob(annotations/*.xml): root ET.parse(xml).getroot() for obj in root.findall(object): voc_names[obj.find(name).text] 1 for txt in glob.glob(labels/*.txt): with open(txt) as f: for line in f: if line.strip(): yolo_ids[int(line.split()[0])] 1 print(VOC 类别分布:, voc_names) print(YOLO id 分布:, yolo_ids)跑完这份统计你就能判断两套标签是否自洽。如果 VOC 有 6 类而 YOLO 只有 0~3 四个 id那要么是部分类别没转要么是转换脚本里做了类别合并必须搞清楚再训否则模型学到的类别空间和你的业务需求对不上。3. 用这套数据跑通 YOLO 训练的最小闭环从目录结构到第一轮收敛3.1 目录结构按 YOLO 官方约定摆别自创层级YOLO 系列v5/v8/v11 都类似对目录结构有约定images/train、images/val、labels/train、labels/val图片和标签同名不同后缀放在平行目录里。这套数据既然已经给了 YOLO 格式标签最省事的做法是直接按这个结构组织而不是把 VOC 的 XML 也塞进去。我一般会保留一份原始 VOC 目录不动另外生成一份纯 YOLO 结构用于训练这样出问题还能回溯。# 假设原始数据images/ 下所有 jpgvoc_xml/ 下所有 xmlyolo_txt/ 下所有 txt mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val # 按 8:2 划分固定随机种子保证可复现 python - EOF import os, random, shutil random.seed(42) imgs sorted([f for f in os.listdir(images) if f.endswith(.jpg)]) random.shuffle(imgs) split int(len(imgs) * 0.8) for i, img in enumerate(imgs): phase train if i split else val stem os.path.splitext(img)[0] shutil.copy(fimages/{img}, fdataset/images/{phase}/{img}) txt fyolo_txt/{stem}.txt if os.path.exists(txt): shutil.copy(txt, fdataset/labels/{phase}/{stem}.txt) else: open(fdataset/labels/{phase}/{stem}.txt, w).close() # 空标签负样本 EOF逻辑说明这段脚本做三件事——建目录、按 8:2 切分、把图片和对应 TXT 复制到对应 phase。关键点是没有标签的图片要生成空 TXTYOLO 会把空标签文件当作「这张图没有目标」的负样本这对 PCB 缺陷检测很重要因为大量正常板子就是负样本能显著降低误报。参数上random.seed(42)是为了让每次划分一致方便复现和对比实验比例 8:2 是中小数据集的常规选择2700 张里约 540 张做验证够看趋势但不适合做最终精度评估真要严谨还得再切一份 test。3.2 data.yaml 里三个参数决定训练能不能正常起步YOLO 训练靠一个data.yaml描述数据路径和类别这个文件写错是最常见的「训练启动即报错」来源。典型内容如下path: /abs/path/to/dataset train: images/train val: images/val nc: 6 names: [missing_hole, mouse_bite, open_circuit, short, spur, spurious_copper]path建议写绝对路径相对路径在不同工作目录下启动容易找不到nc必须和names长度一致且和标签里的最大 class_id1 一致如果标签里出现 id6 但 nc6合法 id 是 0~5训练会直接报索引越界names的顺序必须和 class_id 严格对应顺序错了模型能训但类别全乱。我一般会在训练前跑一遍nc与标签 id 最大值的断言校验几行代码省掉半小时排查。import yaml, glob cfg yaml.safe_load(open(data.yaml)) max_id -1 for txt in glob.glob(dataset/labels/train/*.txt): with open(txt) as f: for line in f: if line.strip(): max_id max(max_id, int(line.split()[0])) assert max_id cfg[nc], f标签最大 id {max_id} 超出 nc {cfg[nc]} assert len(cfg[names]) cfg[nc], names 长度与 nc 不一致 print(校验通过可以开训)3.3 第一轮训练命令与关键超参中小数据集别照搬大模型配置数据就绪后用 YOLOv8 起步的命令大致是这样yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/pcb \ nameexp1参数说明modelyolov8s.pt是拿 COCO 预训练权重做迁移学习2700 张从零训基本不可能收敛好迁移学习是必选项imgsz640是默认值PCB 缺陷往往很小如果缺陷在图上占比很小可以提到 1024 甚至 1280但显存和速度要权衡batch16在 8G 显存上跑 640 一般够跑 1024 就得降到 4 或 8lr00.01是 YOLO 默认初始学习率微调时如果发现 loss 震荡厉害可以降到 0.001patience20表示 20 轮验证指标不提升就早停中小数据集上这个值别设太大否则过拟合了还在跑。第一轮跑完重点看results.png里的train/box_loss和val/box_loss是否同步下降如果 train 降 val 不降甚至上升就是过拟合信号该加数据增强或减模型容量了。4. 从 VOC 补转 YOLO 或反向转换脚本、边界与四个必查项4.1 VOC 转 YOLO 的标准脚本与坐标裁剪虽然这套数据已经给了 YOLO 格式但实际项目里你经常需要把新标注的 VOC 补进来或者反过来把 YOLO 转回 VOC 给标注工具用。VOC 转 YOLO 的核心就是坐标归一化但有个容易忽略的边界xmax可能等于图片宽度、ymax等于高度归一化后中心点加半宽会略大于 1YOLO 虽然容忍轻微越界但最好裁到 [0,1]。import xml.etree.ElementTree as ET import os def voc2yolo(xml_path, out_path, class_map): root ET.parse(xml_path).getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue cid class_map[name] bb obj.find(bndbox) xmin max(0, float(bb.find(xmin).text)) ymin max(0, float(bb.find(ymin).text)) xmax min(w, float(bb.find(xmax).text)) ymax min(h, float(bb.find(ymax).text)) xc (xmin xmax) / 2 / w yc (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 裁到 [0,1]避免浮点误差越界 xc, yc min(max(xc, 0), 1), min(max(yc, 0), 1) bw, bh min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f{cid} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))逻辑说明先读原图宽高把像素角点裁到图片范围内再算归一化中心点和宽高最后再裁一次防止浮点误差。class_map是类别名到 id 的字典必须和训练时的names顺序一致。参数上保留 6 位小数是 YOLO 常见精度够用且文件不会太大。4.2 四个必查项空标签、越界框、重复框、类别名拼写转换或整理完数据别急着开训先跑一遍体检。我固定查四件事一是空标签文件数量负样本太多会导致正负失衡太少又可能漏标二是越界框归一化值超出 [0,1] 的框要修三是重复框同一位置多个几乎重合的框往往是标注重复会让模型学到冗余四是类别名拼写missing_hole和missinghole会被当成两类。下面这段脚本一次查完import glob, os from collections import Counter empty, oob, dup, names 0, 0, 0, Counter() for txt in glob.glob(dataset/labels/**/*.txt, recursiveTrue): boxes [] with open(txt) as f: for line in f: if not line.strip(): continue p line.split() cid, xc, yc, bw, bh int(p[0]), *map(float, p[1:]) names[cid] 1 if not all(0 v 1 for v in (xc, yc, bw, bh)): oob 1 boxes.append((cid, round(xc, 3), round(yc, 3), round(bw, 3), round(bh, 3))) if not boxes: empty 1 if len(boxes) ! len(set(boxes)): dup 1 print(f空标签文件: {empty}, 越界框: {oob}, 含重复框文件: {dup}) print(类别 id 分布:, names)跑完根据结果决定空标签占比超过 30% 要考虑是否负样本过多越界框必须修重复框建议人工抽查类别 id 分布严重不均某类只有几十个框要考虑过采样或类别加权。5. 训练与验证阶段最容易踩的坑从 loss 不降到验证集指标虚高5.1 现象loss 一直不降或剧烈震荡 → 原因学习率与 batch 不匹配 → 解决先小 lr 试跑PCB 缺陷数据集 2700 张属于中小规模很多人直接套用大模型训练配置lr00.01加batch64结果 loss 前几轮就炸。血泪经验是中小数据集微调先把lr0降到 0.001、batch降到 8 或 16跑 5 轮看 loss 是否平稳下降稳了再逐步加。如果 loss 震荡但整体下降可以加 warmupYOLO 默认有 3 轮 warmup或开cos_lr。5.2 现象验证集 mAP 很高但实际推理误报多 → 原因训练验证同分布、缺负样本 → 解决补正常板负样本如果验证集是从同一批数据里随机切的且负样本正常板很少模型会学到「每张图都有缺陷」的先验mAP 虚高。解决方法是往训练集里补一批纯正常板的空标签图片占比 10%~20%同时验证集也要包含负样本看模型在正常板上的误报率。5.3 现象小缺陷检测不到 → 原因输入分辨率不够或 anchor 不匹配 → 解决提高 imgsz 或换更小模型PCB 上的spur、mouse_bite这类缺陷在原图上可能只有十几个像素640 输入下缩到几个像素特征全丢了。常见做法是把imgsz提到 1024 或 1280或者用 YOLOv8n/s 这种小模型配合高分辨率别用大模型硬扛。如果显存不够可以切图推理把大图切成小块分别检测再合并。5.4 现象训练报「Label class xxx exceeds nc」→ 原因class_id 从 1 开始或类别数写少 → 解决统一从 0 开始并核对 nc这是最直接的报错原因通常是标注工具导出时 id 从 1 开始或者data.yaml里nc写少了。解决就是全量扫一遍标签最大 id把nc设成max_id1并把所有 id 统一减 1如果确实是从 1 开始。5.5 现象同一张图 VOC 和 YOLO 框对不上 → 原因图片尺寸记录不一致或转换时宽高取反 → 解决用原图尺寸重算并可视化抽查双格式数据最隐蔽的坑就是两套标签其实没对齐可能 XML 里记的宽高和实际图片不一致或者转换脚本把 w、h 写反了。解决方法是随机抽 20 张用第 2 章的对比脚本画出来看红绿框重合才算过关。6. 把 2700 张用到极致分层采样、难例回流与一个可复用的验证习惯数据量固定的时候提升效果靠的是「怎么用」而不是「换模型」。我一般会做三件事。第一是分层采样划分训练验证按缺陷类别做分层保证每个类别在训练和验证里都有足够样本而不是纯随机切导致某类验证集只有几个框。第二是难例回流第一轮训完把验证集里漏检和误报的图挑出来人工复核标签确认是标注问题就修是模型问题就加进下一轮训练集迭代两三轮通常能涨几个点。第三是固定一套验证流程每次改超参或换模型都在同一份验证集上跑记录 mAP50、mAP50-95、每类 AP 和误报率别只看一个总数。下面这个脚本用来按类别分层划分比纯随机更稳import glob, os, random, shutil from collections import defaultdict random.seed(42) # 统计每张图包含的类别 img_classes defaultdict(set) for txt in glob.glob(labels/*.txt): stem os.path.splitext(os.path.basename(txt))[0] with open(txt) as f: for line in f: if line.strip(): img_classes[stem].add(int(line.split()[0])) # 按主类别分组组内 8:2 切 groups defaultdict(list) for stem, classes in img_classes.items(): key min(classes) if classes else -1 # 空标签归为 -1 组 groups[key].append(stem) for key, stems in groups.items(): random.shuffle(stems) split int(len(stems) * 0.8) for i, stem in enumerate(stems): phase train if i split else val for ext in (.jpg, .png): src fimages/{stem}{ext} if os.path.exists(src): shutil.copy(src, fdataset/images/{phase}/{stem}{ext}) shutil.copy(flabels/{stem}.txt, fdataset/labels/{phase}/{stem}.txt)逻辑说明先统计每张图含哪些类别按最小类别 id 分组空标签单独一组组内再 8:2 切这样每个类别在训练和验证里的比例大致均衡。参数上key min(classes)是一种简化分组如果一张图含多类归到最小编号那组实际项目里也可以按类别做多标签分层但实现复杂些中小数据集用这个简化版够用。最后说个我自己的习惯每次拿到新数据集先花半小时做三件事——可视化 20 张标签、统计类别分布、跑一遍格式校验。这三件事做完后面训练出问题能快速定位是数据问题还是模型问题。PCB 缺陷检测这行数据质量比模型结构重要得多2700 张用好了比 10000 张脏数据强。希望帮到你。本文还有配套的精品资源点击获取