
简介面向电路板PCB缺陷检测任务的数据集覆盖假铜、缺孔、鼠咬、开路、针孔、划痕、短路、毛刺八类常见缺陷采用 Pascal VOC 与 YOLO 双标注格式适合深度学习目标检测模型的训练与验证。图像与标注严格对应共 9666 张 jpg 图片配有 9666 个 xml 与 9666 个 txt 标注文件合计 51549 个矩形框额外提供 labelImg 工具的标注规则说明方便使用者按需清洗、扩展样本或进行二次标注。整个资源包仅 1 个 docx 文档压缩后大小约 3.38MB文档中详细描述了数据集构成、标注统计与获取方式并说明这批样本包含部分椒盐噪声增强图片可用来增强模型的抗噪能力。目前已有 228 人学习使用适合从事 PCB 缺陷视觉检测的研究者或算法工程师作为训练数据补充。1. 一个漏检问题背后差的不是模型是数据PCB质检产线上最头疼的一类事故是模型在实验室测出来 mAP 很高一上线就漏检开路和短路工程群里立刻炸锅。排查到最后问题往往不在模型结构而在训练数据标注格式转换错了、类别名大小写不一致、坐标越界被静默裁剪任何一条都足以让模型学歪。标题里这套「PCB缺陷检测数据集 VOCYOLO 格式 9666 张 8 类别」本质上就是一份工业视觉训练的原材料它的价值不在文件数量而在你能否把它正确拆解、转换、验证并喂给训练管线。这篇文章就围绕这套双格式数据集讲清楚格式之间的字段对应、转换脚本、训练前体检和那些最容易让工程师翻车的细节适合正在用 YOLO 做 PCB 外观检测、或者刚接触工业缺陷数据集准备跑通流程的开发者。2. PCB缺陷检测数据集拆开看9666张、8个类别到底覆盖了什么2.1 八类缺陷的形态差异与学习难度拿到数据集第一件事不是直接解压扔给训练脚本而是先弄清这 8 个类别各自的视觉形态。PCB 缺陷检测和通用目标检测有个显著差别缺陷之间形态高度相似类别边界模糊模型很容易把「鼠咬」学成「毛刺」。以下这 8 个类别是这类数据集里最常见的设计标注名缺陷形态视觉特征模型学习难度missing_hole缺孔焊盘或基板上应有孔位缺失中mouse_bite鼠咬边缘出现弧形凹陷像被咬掉一块高open_circuit开路铜箔走线断裂表现为亮线中断中short短路相邻走线之间异常相连中burr毛刺边缘伸出尖锐突起高pinhole针孔微小孔洞直径通常小于 1mm高copper多余铜大块铜箔残留低spurious_copper铜渣散落的小颗粒铜屑中难点集中在两类mouse_bite 和 burr 都出现在导线边缘形态上一个是内凹、一个是外凸但在低分辨率图像上两者都可能变成「边缘的异常突起」需要足够多的局部正样本帮助模型区分。pinhole 的问题不是语义而是尺度它占整张图的比例常在千分之几如果训练时直接 resize 到 640×640目标可能只剩几个像素学习难度陡然上升。这也是为什么很多 PCB 检测项目最后选择切 patch 训练。2.2 VOC与YOLO两种格式的字段对应关系标题里同时带 VOC 和 YOLO 两种格式意味着数据集里每个样本大概率同时存在 XML 标注和同名 TXT 标注也可能只提供其中一种需要自己做转换。理解两种格式的字段对应是后续一切操作的基础。VOC 格式的核心是 PASCAL VOC 的 XML 结构一张图对应一个 XML 文件记录图片来源、尺寸和所有目标框。目标框使用绝对像素坐标以左上角 (xmin, ymin) 和右下角 (xmax, ymax) 表示。YOLO 格式则是一个纯文本文件每行对应一个目标五个值依次是类别编号、归一化中心点 x、归一化中心点 y、归一化宽度、归一化高度所有坐标除以图片宽高后映射到 [0, 1] 区间。信息项VOC 格式XML 字段YOLO 格式TXT 字段类别名称object/name无名称只有类别编号框左上角 Xbndbox/xmin无直接对应框左上角 Ybndbox/ymin无直接对应框右下角 Xbndbox/xmax无直接对应框右下角 Ybndbox/ymax无直接对应中心点 X无(xminxmax)/2 / 图片宽中心点 Y无(yminymax)/2 / 图片高宽高无(xmax-xmin)/图片宽(ymax-ymin)/图片高图片原始尺寸size/width, size/height无训练时从图片读取工程上我一般会把 VOC 当成「可读的数据源」保留把 YOLO 格式作为「训练主格式」来生成。原因很直接XML 树状结构方便人查看和二次标注但每次训练都要重新解析且很多增强库对 TXT 的支持更直接YOLO 格式一行一个目标读取速度快也不会因为某个 XML 节点缺失而中断训练。3. 把VOC格式转成YOLO格式转换脚本与四个参数边界3.1 转换前的目录梳理与类别映射假设你拿到的是 VOC 原始结构通常长这样PCB/ ├── JPEGImages/ │ ├── 100.jpg │ └── 101.jpg ├── Annotations/ │ ├── 100.xml │ └── 101.xml └── ImageSets/ └── Main/ ├── train.txt └── val.txt转换前先固定类别顺序。YOLO 格式里类别只有编号编号对应的名字此时不写进 TXT而是另存到 labels.txt 里。类别顺序一旦确定并开始训练后续就不要改动否则之前训好的权重全部作废。我常用的映射方式按语义分组排列LABELS [ burr, # 0 毛刺 copper, # 1 多余铜 missing_hole, # 2 缺孔 mouse_bite, # 3 鼠咬 open_circuit, # 4 开路 pinhole, # 5 针孔 short, # 6 短路 spurious_copper, # 7 铜渣 ]这个顺序不是固定的但建议把容易混淆的两类尽量排在一起方便后续分析混淆矩阵时一眼看出相邻类别互相误判。3.2 读取XML并生成YOLO的Python脚本转换脚本本身不难真正值得费心思的是对异常情况的处理。我一般会写这样一个函数import os import xml.etree.ElementTree as ET LABELS [ burr, copper, missing_hole, mouse_bite, open_circuit, pinhole, short, spurious_copper ] def voc_to_yolo(xml_path, labels): 将单个VOC XML转为YOLO格式的行列表。 tree ET.parse(xml_path) root tree.getroot() # 优先从XML读取宽高但只作为参考后面还要和真实图片核对 size root.find(size) # text img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in labels: print(fWARNING: {os.path.basename(xml_path)} 包含未映射类别 {name}) continue cls_id labels.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) # 越界夹取防止归一化后出现大于1的值 xmin max(0.0, xmin) ymin max(0.0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines # 批量处理 ann_dir PCB/Annotations out_dir PCB/labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] lines voc_to_yolo(os.path.join(ann_dir, xml_file), LABELS) with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines))逻辑说明脚本遍历 Annotations 目录下所有 XML先把像素坐标夹取到图片范围内再换算成归一化中心点坐标和宽高最后写到与图片同名的 TXT。这里有个容易忽略的细节x_center 和 y_center 用的是中心点而 VOC 给出的是左上和右下中间有一步坐标转换w 和 h 则是直接用右下减左上不需要除以 2。参数说明max(0.0, xmin)这类夹取操作不是可选项很多标注工具允许框略微出界不夹取会导致归一化坐标出现负值或大于 1训练时框被静默丢弃或拉伸到错误位置。f{cls_id} {x_center:.6f}保留 6 位小数是行业惯例低于 4 位会损失小目标定位精度超过 6 位对浮点训练没有实质收益。3.3 转换完必做的三点核验转换完成后立刻核验不要直接进训练。第一点抽查 3 到 5 个 TXT确认行数和对应 XML 里的 object 数量一致head -3 PCB/labels/100.txt wc -l PCB/labels/100.txt第二点检查是否有空 TXT空文件对应两种情况原图确实没有缺陷背景图通常会被过滤掉或者 XML 解析时类别名没匹配上find PCB/labels -type f -empty | wc -l第三点全局扫描归一化坐标是否都在 [0, 1] 区间awk {for(i2;i5;i) if($i0||$i1) print FILENAME, $0} PCB/labels/*.txt如果 awk 有输出说明 XML 里的 size 和真实图片尺寸对不上或者目标框严重出界。这个时候宁愿花时间回溯标注也不要带着脏数据往下走。4. 训练前的数据体检坏图、越界框与类别偏斜怎么量化4.1 坏图和16位深灰度图的排查脚本很多 PCB 显微成像设备输出的不是常规 8 位三通道图而是 12 位或 16 位深度的灰度图。直接用cv2.imread默认参数去读返回的往往是 None 或者被截断成奇怪的效果。我给数据集做体检时的标准流程如下import os import glob import cv2 import numpy as np img_dir PCB/JPEGImages bad_images [] converted [] for img_path in glob.glob(os.path.join(img_dir, *)): img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img is None: bad_images.append(img_path) continue if img.dtype np.uint16: # 16位深度转8位右移8位比直接除255更合理 img_8 (img 8).astype(np.uint8) cv2.imwrite(img_path, img_8) converted.append(img_path) print(f坏图(无法读取): {len(bad_images)}) print(f16位深度已转8位: {len(converted)})逻辑说明IMREAD_UNCHANGED保留原始位深只有这样才能发现 16 位图并做转换。转换时右移 8 位而不是除以 255是因为 16 位图的像素范围是 0 到 65535右移 8 位等价于除以 256能把动态范围线性映射到 0 到 255直接除以 255 会导致高光溢出暗部压缩缺陷对比度反而下降。参数说明如果发现 16 位图比例很高建议在转换时加一个自适应对比度拉伸因为 PCB 显微图像往往整体偏暗缺陷区域的细节藏在低灰度段。常见做法是np.percentile(img, 1)和np.percentile(img, 99)作为拉伸下限和上限。接着检查 YOLO 格式标注本身有没有异常行import glob bad_lines 0 for txt_path in glob.glob(PCB/labels/*.txt): with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: bad_lines 1 print(f异常行: {txt_path} - {line.strip()}) print(f异常标注行数: {bad_lines})这个检查会揪出两类问题文件末尾多了空行、或某行少了字段。空行会让部分框架在读取时抛异常少字段会让目标框坐标全部错位属于必须零容忍的问题。4.2 类别分布与训练/验证集划分的两种策略对象检测的类别不平衡不会直接让训练崩溃但会让少数类在验证集上的指标起伏很大。先把类别分布统计出来import collections stats collections.Counter() for txt_path in glob.glob(PCB/labels/*.txt): with open(txt_path) as f: for line in f: cls_id int(line.strip().split()[0]) stats[LABELS[cls_id]] 1 for name, count in stats.most_common(): print(f{name}: {count} 个目标)假设结果里 pinhole 只有几百个目标框而 copper 有几千个这不代表数据集有问题而是说明训练时损失函数会被大类主导。接下来的处理方式有两条路一是对少数类使用更高的损失权重二是在划分训练集/验证集时做分层抽样。划分数据集时最不该做的事是纯随机打乱图片。PCB 图片通常以板卡为单位采集同一块板会有多个视角或多层版图像纯随机会把同一板卡的图同时分到训练集和验证集验证指标虚高上线就现原形。我一般会按文件名里的板卡前缀分组import random from glob import glob img_files glob(PCB/JPEGImages/*) groups {} for img in img_files: prefix os.path.basename(img).split(_)[0] groups.setdefault(prefix, []).append(img) group_names list(groups.keys()) random.shuffle(group_names) split_idx int(len(group_names) * 0.8) train_groups set(group_names[:split_idx]) val_files [] train_files [] for g, imgs in groups.items(): if g in train_groups: train_files.extend(imgs) else: val_files.extend(imgs)这样划分保证了同一块板的所有视角不会跨集合。参数说明分组比例按训练集 80%、验证集 20% 是常见起点如果你的在线模型已经上线、只是为了增量更新可以把验证集扩大到 30%优先保证线上稳定性。5. PCB缺陷数据集使用避坑5个高频翻车现场5.1 现象一类别名大小写不一致导致一部分标注静默丢失在转换脚本里if name not in labels分支触发了无数次却没人注意。某次训练前我统计类别分布发现 short 只有几十个标注检查 XML 才发现标注工具自动补全把Short和short混着写。原因是一组图由不同标注员完成命名习惯不统一。解决的办法是在转换前先跑一遍全量扫描把 XML 里出现的所有 object 名称打印出来归并同义大小写后再写入 LABELSgrep -h name PCB/Annotations/*.xml | sort | uniq -c | sort -nr转换后立刻对比每个类别的目标数量数量异常偏低的类别基本都有命名问题。5.2 现象二坐标越界被静默裁剪目标框悄悄变小标注框越界是常态尤其是紧贴图片边缘的缺陷。转换时如果没有夹取归一化坐标可能出现 1.0002 这样的值训练框架在加载数据时读到越界框有的直接报错有的把框截断到边界。截断后目标框面积变小与真实情况不符模型对边缘目标的定位精度一路下降。解决方法是转换脚本里统一做min/max夹取并在输出时统计越界比例。越界比例超过 5% 时说明标注工具校准有问题需要回溯标注轮次。5.3 现象三16位深灰度图直接加载失败一张好图被当坏图扔了PCB 显微图里最常见的坑。cv2.imread不带IMREAD_UNCHANGED读 16 位图会返回 None然后你的体检脚本就把这些图判为坏图删除等到训练完才发现少了一批关键样本其中可能包含大量 pinhole。解决方式在第 4 章的脚本里已经给了读入时带IMREAD_UNCHANGED判断 dtype 后转 8 位。特别提醒转换后要在原图路径上重新覆盖写入不要生成新文件否则容易和管理数据的同事扯不清楚。5.4 现象四按图片随机划分数据集把同一块板的多个视图切到两个集合这个问题最隐蔽因为训练指标完全正常mAP 甚至很好看直到线上批量验证才发现泛化不达标。原因就是同一块 PCB 的多个视图一个进了训练集、一个进了验证集验证时模型见到了训练时已经学过的板上特征。解决办法是第 4 章的分组划分按板卡 ID 或文件名前缀聚合后再切分。如果你手里有产线批次信息可以直接按批次划分效果更严格。5.5 现象五类别编号和labels名单错位模型收敛了但输出全是串类训练一切正常权重保存了推理时发现类别 ID 对应关系全错。常见原因是 LABELS 顺序在转换后调整过txt 里已经写死了编号后续跑脚本没重新生成。另一个高频场景是多个数据集合并训练A 数据集的 open_circuit 编号是 4B 数据集的 open_circuit 编号是 5模型被迫在一个输出头上学习两个编号同一个语义。解决习惯是训练目录下永远放一份 labels.txt和训练数据在同一级目录推理脚本从这里读类别名而不是硬编码。6. 验证数据集的真正价值按类别评估、小目标召回与小样本回灌6.1 按类别拆分的AP评估脚本训练完只看整体 mAP 是 PCB 缺陷检测的大忌因为 copper 大类很容易把指标拉高pinhole 这些小类差到没法用也看不出来。我习惯在评估阶段加一段按类别的统计把每个类别的 AP0.5、AP0.75 分别打印出来并按照目标框面积占比分桶比如把框面积小于整图面积 1% 的归为小目标单独统计。# 假设 dets 和 gts 是每张图片的检测结果与真值列表 # 类别名从项目目录下 labels.txt 读取 for cls_name in labels: cls_dets [d for d in all_dets if labels[d.cls_id] cls_name] cls_gts [g for g in all_gts if labels[g.cls_id] cls_name] ap_50 compute_ap(cls_dets, cls_gts, iou_thresh0.5) ap_75 compute_ap(cls_dets, cls_gts, iou_thresh0.75) print(f{cls_name}: AP0.5{ap_50:.3f} AP0.75{ap_75:.3f})AP0.5 反映框定位到大致位置的能力AP0.75 才是真正评估工业落地精度的指标。PCB 缺陷框普遍很小0.5 的 IoU 容差可能让一堆位置偏差 30% 的框都算对AP 虚高。建议在线下测试中同时报告两个阈值验收时以 AP0.75 为准。6.2 小目标漏检的两种补救手段切patch与漏检回灌评估完发现 pinhole 这类小缺陷召回率低直接调置信度阈值会引入大量误检不解决本质问题。常见做法的第一步是切 patch 训练把原图按 640×640 带重叠切块缺陷框保留与 patch 的交集并过滤过小的碎片相当于把小目标放大后再训练。patch 尺寸和重叠率要根据原图分辨率调整我一般选 25% 重叠率保证贴边缺陷至少完整出现在一块 patch 里。第二步是漏检回灌把验证集里被模型漏检的图抽出来人工复核确认是标注漏了还是缺陷形态过于罕见。如果是后者把这些样本按 1:3 的比例扩充进训练集比单纯增加训练轮次更有效。我现在的习惯是每次训练前先跑一遍数据体检再跑一遍评估脚本确认两个环节干净才进入调参阶段。这个习惯帮我把回归问题的定位时间从两天缩到半小时。希望帮到你。本文还有配套的精品资源点击获取