
简介一套面向电路板PCB缺陷检测的工业视觉数据集适合目标检测方向的学习者、研究者及质检算法开发人员使用。数据覆盖8类常见缺陷缺孔、鼠咬、开路、针孔、划痕、短路、多余铜、毛刺共包含9666张JPEG图像及对应标注标注框总数超过5.1万个同时提供Pascal VOC格式的XML文件与YOLO格式的TXT文件其中部分图片为椒盐噪声增强样本更贴近真实产线质检场景可直接用于YOLO系列等模型的训练与验证。资源以单个docx文档形式打包大小3.38MB文档中整理了数据集格式说明、类别分布、各缺陷标注框数量及使用注意事项方便快速查阅。目前已有220人浏览学习适合需要标准PCB缺陷数据开展目标检测实验或工业项目验证的读者参考。1. 拿到一份9666张的PCB缺陷检测数据集先别急着开训做电路板PCB缺陷检测的人最缺的往往不是模型而是“能不能立刻开训的图”。一份标成“VOCYOLO格式、9666张、8个缺陷类别”的PCB数据集意味着省掉几周拍照和标注直接进入算法验证。它适合你用来做模型选型、跑通从训练到推理的完整流程也适合验证小目标检测方案。但产线落地前必须回归到自己的板子公开数据集的mAP不能直接当验收指标。下文的步骤都围绕这份数据集展开格式怎么拆、怎么转换、怎么训、坑在哪、怎么验证它到底值不值得用。2. 先别管模型把VOC和YOLO两份标注对齐2.1 拿到压缩包先做三件事而不是直接解压开训这种“双格式数据集”的生成路径一般是先按VOC格式标注XML文件再写脚本批量转换成YOLO格式txt文件。转完能跑通但两份标注是否严格同步没有任何保证。所以拿到手第一件事不是解压跑训练而是核对第一数数JPEGImages里的图片数量、labels里的txt数量是否和9666对得上多出来的空标注文件、少掉的图片都要列出来第二随机抽几个txt看每行的class_id最大值是否小于8大于等于8说明类别映射写错了第三用labelImg或直接叠加画框目检20张图看框的位置和缺陷本体是否吻合。这三步做完这份数据在你手里才算真正“解锁”。多数翻车现场都是因为跳过这三步直接训练到一半才发现类别ID错位。VOC格式的目录通常是JPEGImages/图片、Annotations/XML标注、ImageSets/Main/train.txt、val.txt而YOLO格式通常是images/和labels/。两份标注的图片完全同名靠文件名关联。听起来简单但同名文件经过多次压缩、解压、二次整理后存在两种常见错位一种是图片被替换过而XML没更新另一种是XML更新了而txt没重转。2.2 8个类别怎么分布决定了后面的增强策略常见PCB公共缺陷集一般标6类missing_hole漏孔、mouse_bite鼠牙痕、open_circuit开路、short_circuit短路、spur毛刺、copper铜渣。这份数据集标了8类额外两类通常是pin_hole针孔和scratch划痕也可能是别的命名。无论具体叫什么拿到手第一时间统计类别分布因为PCB缺陷天生不平衡——短路、毛刺往往几百例背景板占了大部分。class_id常见类别名形态特征检测难度0missing_hole焊盘中央应有的孔缺失低1mouse_bite铜箔边缘V形缺口中2open_circuit走线断裂中3short_circuit相邻线路桥连中4spur铜箔边缘突出毛刺高5copper残留铜渣中6pin_hole针孔状空洞高7scratch表面划痕高注意这里列的是常见名字你的数据集classes清单以labels里实际出现的为准不要对着这份表想当然。多数情况下类别清单写在数据集的README或classes.txt里把它和XML里的object name逐一对上。如果对不上以XML中的name为准反推txt而不是反过来因为txt每行只有一个数字错位后没有线索可查。2.3 两份标注为什么会“打架”不只是格式不同VOC的XML记录的是绝对坐标xmin、ymin、xmax、ymaxYOLO的txt记录的是归一化中心点坐标class_id、x_center、y_center、width、height。即使同一张图两份标注的数值也不一样这一点不难理解。难的是一份标注被人动过另一份没同步。典型场景是团队里有人发现某张图的漏孔没标上直接改了XML忘了跑转换脚本。这会导致YOLO格式里那个缺陷形同虚设模型训练时根本没学过这个“正样本”。反过来有人在txt里补了一行XML里没有但那行到底对应哪个框没有任何人能说清。推荐做法是把这份数据集当成“原料”而不是“成品”所有训练数据一律以自己转换出的YOLO格式为准。也就是先校验XML再从XML重新生成txt原来自带的txt只用来做对照参考。下文第3章给出的是这套转换脚本它会帮你把“双格式”这个卖点变成可控的预处理流水线。3. 把VOC转成YOLO格式坐标归一化转换脚本与三个边界坑3.1 写一个voc_to_yolo.py一次跑完所有XML下面这段脚本按“严格模式”设计遇到未知类别直接报错遇到非法框直接丢弃并在控制台点名。这样你不会静默地带着错误数据训完整轮回头找不出原因。# voc_to_yolo.py import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image CLASSES [ missing_hole, mouse_bite, open_circuit, short_circuit, spur, copper, pin_hole, scratch, ] def convert_one_xml(xml_path, img_root, label_out, classes): tree ET.parse(xml_path) root tree.getroot() # 用真实图片尺寸不要依赖XML里的size节点 img_path img_root / (xml_path.stem .jpg) if not img_path.exists(): img_path img_root / (xml_path.stem .png) with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): name obj.findtext(name) if name not in classes: # 严格模式未知类别直接抛异常 raise ValueError(funknown class: {name} in {xml_path.name}) cls_id classes.index(name) bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) # 边界收紧禁止坐标越界同时丢弃退化框 xmin max(0, min(xmin, w - 1)) xmax max(0, min(xmax, w - 1)) ymin max(0, min(ymin, h - 1)) ymax max(0, min(ymax, h - 1)) if xmax xmin or ymax ymin: print(finvalid bbox dropped: {xml_path.name}) continue x_center (xmin xmax) / 2 / w y_center (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) if lines: label_out.mkdir(parentsTrue, exist_okTrue) (label_out / (xml_path.stem .txt)).write_text(\n.join(lines)) def main(): voc_ann_dir Path(Annotations) img_dir Path(JPEGImages) yolo_label_dir Path(labels_txt) for xml_path in voc_ann_dir.glob(*.xml): convert_one_xml(xml_path, img_dir, yolo_label_dir, CLASSES) if __name__ __main__: main()逻辑上最关键的一处是每行顺序固定为class_id、x_center、y_center、width、height全部归一化因此结果与图片绝对分辨率无关模型训练时任意resize都不会破坏坐标对应关系。max(0, min(x, w-1))这层边界收紧很重要因为部分VOC标注存在坐标值比图片宽高多出几个像素的情况不收紧YOLO训练时容易报 “all points are not in boundaries”。参数说明CLASSES的顺序直接决定class_id所以这个列表必须和训练用的data.yaml里的names顺序一致改动任何一处都要连带改另一处。.jpg和.png的兜底逻辑是针对部分数据集混用格式设计的如果你的数据集是其他后缀如.bmp自己补一行。3.2 只信代码不信任XML用真实图像尺寸替代XML里的size节点转换脚本里有一句值得展开用Image.open(...)读真实宽高而不是取XML里size节点的值。原因是部分数据集的图片被批量压缩过比如原来2048×1536压成1024×768但XML里的size没有同步更新。如果按XML里写的老尺寸做归一化所有框的x_center、width全部偏大结果就是训练出的模型框总是偏右、偏下小目标尤其明显。这段血泪经验对应的现象是模型在验证集上mAP不低但框的位置肉眼可见地整体偏移。因为你用了一套错误的归一化坐标而YOLO训练时又把图片resize到固定尺寸两个误差叠在一起最终框的位置成了“大概对但总差一点”。处理这个问题的标准做法就一句话归一化的分母永远用图片本身的真实宽高。所以脚本里先找图、再读size、最后写txt三个步骤顺序不能反。如果你的图片全是300dpi的精密板卡几十像素的偏移就是致命问题。3.3 转换完如何自检三张图对比法脚本跑完后不要急着开训用下面这个简单的Python脚本抽三张图把XML的框和生成的txt框都画在同一张图上人工看是否重合# draw_check.py from PIL import Image, ImageDraw def draw_boxes(img_path, xml_boxes, txt_boxes, out_path): img Image.open(img_path).convert(RGB) draw ImageDraw.Draw(img) for box in xml_boxes: # 红色XML draw.rectangle(box, outlinered, width2) for box in txt_boxes: # 蓝色txt draw.rectangle(box, outlineblue, width2) img.save(out_path)逻辑说明红色框来自XML的绝对坐标蓝色框来自txt乘以图片宽高还原出的绝对坐标。两张框如果重合说明转换正确如果出现大面积蓝色框偏移说明你的分母用错了回去检查3.2里说的size问题。这一步不花多少时间相比训练几小时跑完后才发现数据错位性价比高太多。4. 训练YOLOv8前的数据划分与参数配置9666张怎么分才不翻车4.1 按文件名随机划分有个隐藏问题同板不同图被拆到训练集和验证集很多人习惯直接train_test_split按文件名随机分7:2:1这在PCB数据集上会踩一个隐性坑同一块板子拍的多个视野/不同光照会被拆进不同集合。模型在验证集上表现好未必是真学到了缺陷特征而是见过同一块板的“记忆”。真正上线时换了新板mAP立刻掉10个点以上。合理做法是先按文件名前缀分组把同一块板的图放进同一组再在组级别做划分。比如文件名格式是panel01_0001.jpg、panel01_0002.jpg那就以panel01为组键# split_by_group.py from pathlib import Path import random imgs sorted(Path(images).glob(*.jpg)) groups {} for p in imgs: key p.name.split(_)[0] # panel01_0001 - panel01 groups.setdefault(key, []).append(p) keys list(groups.keys()) random.seed(42) random.shuffle(keys) n len(keys) train_keys keys[: int(n*0.8)] val_keys keys[int(n*0.8): int(n*0.9)] test_keys keys[int(n*0.9):] def dump(key_list, dest): dest Path(dest) for key in key_list: for p in groups[key]: dest.mkdir(parentsTrue, exist_okTrue) shutil.copy(p, dest / p.name) dump(train_keys, images/train) dump(val_keys, images/val) dump(test_keys, images/test)这个脚本把“分组—划分—拷贝”一次性做完。参数说明冒号后面的int(n*0.8)取整后可能丢失少量尾组如果你名下有板子的图特别多还可以在分组后加一个“每个组最多取N张”的上限避免单块板占比过高。划分完先数一数train/val/test各自图片数确保三者规模差距在合理区间内。4.2 写data.yaml类别顺序、路径、imgsz三个必调项划分完成后训练配置的核心是data.yaml。YOLOv8的data.yaml有几处容易写错class_id从0开始连续编号names列表的顺序必须和txt的class_id完全一致train/val路径要写成相对路径或绝对路径不要写不存在的花括号变量。# pcb_data.yaml path: /data/pcb_dataset train: images/train val: images/val test: images/test nc: 8 names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short_circuit 4: spur 5: copper 6: pin_hole 7: scratch这里尤其注意path字段的含义它是所有相对路径的根目录train/val/test都基于它拼接。如果你的数据不在/data/pcb_dataset下请改成实际路径。nc必须与names长度一致多一个或少一个都会导致训练报错或类别错位。推荐在训练前先跑一次yolo detect train datapcb_data.yaml epochs0让YOLO加载数据并打印各类别分布这一步能提前暴露路径问题。4.3 训练命令与关键参数batch、imgsz、mosaic、损失函数权重PCB缺陷的特性是小目标多、背景纹理高度重复、类别不平衡严重。所以训练参数不能照搬coco默认配置。以YOLOv8为例我一般这样起训练yolo detect train \ modelyolov8n.pt \ datapcb_data.yaml \ imgsz1024 \ batch16 \ epochs200 \ patience30 \ close_mosaic10 \ optimizerAdamW \ lr00.001 \ device0参数说明逐一展开。imgsz1024是针对PCB小目标最重要的一个改动针孔、鼠牙这类缺陷在原图上往往只有20到40像素如果缩到640它们只占不到3个像素细节直接消失。显存吃紧的话可以降到768但不要低于640。close_mosaic10表示训练最后10轮关闭mosaic增强让模型在接近真实分布上收敛否则很容易出现“训练loss很低推理时框偏”的情况。optimizerAdamW配合lr00.001是YOLOv8在中小数据集上的稳定组合SGD在9666张这种规模下收敛慢参数也更难调。YOLOv8的损失函数分三部分box_loss边框回归、cls_loss分类、dfl_loss分布焦点损失对应的权重参数是box7.5、cls0.5、dfl1.5。PCB缺陷检测时如果发现分类错误多比如把scratch判成spur可以适当提高cls权重如果框不准优先调box权重。建议每次只调一个参数不要三个一起动否则出了问题你根本不知道是谁引起的。参数推荐值作用备注imgsz1024小目标可分辨的最小尺寸显存不够降到800/768batch16单卡显存允许的上限RTX 3090/4090可上32epochs200中小数据集收敛轮次配early stoppingpatience3030轮不提升则停防止过拟合close_mosaic10最后10轮关mosaic稳定框回归box7.5边框损失权重框不准优先调cls0.5分类损失权重分错类优先调batch选择上PCB图像纹理接近batch过大反而容易在早期收敛太快、陷入局部极值。通常16到32之间即可不要盲目上64。训练中如果发现验证集loss在epoch 40后反复震荡优先怀疑是mosaic增强过强导致数据分布和真实场景偏差太大可以加大close_mosaic的轮数到15或20。5. PCB缺陷检测的6个避坑点从标签错位到小目标漏检5.1 两份标注对不上训练正常但验证mAP玄学波动现象训练loss正常下降但验证mAP忽高忽低同一套参数跑两次结果差5个点以上。原因你用的是数据集自带的YOLO txt但XML在之后被改过一轮txt没有同步验证集里有一部分框是老版本标注模型怎么学都学不对。解决用第3章的转换脚本从XML重新生成txt再重跑验证。不要偷懒只重新生成验证集的txt训练集和验证集必须同源否则模型在“学一套、考另一套”mAP高反而说明它有记忆。5.2 少样本类别被多数类别“吃掉”mAP只有0.2现象训练完看per-class mAPshort_circuit等少数类只有0.2左右其他类0.8以上。原因类别样本数极不平衡比如missing_hole有3000样本short_circuit只有200样本模型梯度被多数类主导。解决先统计训练集每类图片数。对少数类做复制粘贴增强把某张图的缺陷区域随机贴到背景板上或提高mosaic概率让每个batch更容易同时包含少数类。若少样本类只有几十张靠增强不够考虑“半监督预标注人工修正”扩展数据。5.3 小目标在640分辨率下直接消失现象imgsz640训出来的模型其他缺陷都正常pin_hole和mouse_bite几乎全漏。原因这两类缺陷在原图上只有二三十像素resize到640后变成几个像素卷积下采样几层就没了。解决一是imgsz提升到1024以上二是推理时使用tiling切图把原图切成两块或四块分别检测再合并三是换带更浅检测头的模型比如YOLOv8n-P2在stride4的特征层上输出对小目标更友好。代价都一样——显存占用上升推理速度下降但PCB是静态质检场景帧率不是第一约束。5.4 负样本缺失导致过检率爆炸现象模型对完全没有缺陷的板子疯狂输出检测框通常集中在焊盘和丝印区域。原因数据集中所有图都至少带一个缺陷模型没见过“没有缺陷”的板子于是倾向于把任何纹理异常都当作缺陷。PCB板纹理密集误检率一高产线根本没法用。解决从无缺陷的板卡图里截一批背景块标注文件留空加入训练集做负样本。注意YOLO格式中空标注文件也应存在0字节txt否则会被默认忽略。负样本建议占总数10%到20%过检率会明显下降。5.5 标注框坐标越过图像边界训练直接报错或静默丢弃现象转换脚本跑完报“Height out of range”或训练日志中一张图的标注框数量变成0。原因VOC标注里的xmax写成了比图片宽度还大的值或ymin为负数YOLO训练严格要求坐标在[0, 1]区间内。解决转换时统一用真实图片尺寸做clamp并在丢弃时打印文件名见3.1脚本。批量处理完再看日志如果丢框超过总量1%说明原始标注质量有问题要回到标注阶段修而不是靠clamp硬拗。5.6 拿验证集当测试集反复调参等于给自己挖坑现象调参两轮后验证集mAP一直在涨你信心满满地拿到产线图一试立刻跌回原点。原因你在用验证集反复调参验证集的信息已经通过你的决策泄漏进了模型。越调越像“背答案”。解决从开局就切死一个test集训练和调参只用train和val每调完一轮才在test上测一次。再把test上的表现当作真实水平的估计。对PCB检测来说这个习惯能救你命因为产线实际板子和你手上的数据集永远有分布差异。6. 一张PR曲线验证数据集的成色召回率、误检与真实产线冒烟测试6.1 用val模式出PR曲线和混淆矩阵先看右下角再看左上角训练完不要只看总mAP跑一次验证并输出图表yolo detect val \ modelruns/detect/train/weights/best.pt \ datapcb_data.yaml \ plotsTrue参数说明plotsTrue会生成PR曲线、混淆矩阵和一批带预测框的样例图。PR曲线的纵轴precision、横轴recall曲线越靠近右上角模型越好。重点关注曲线右段高recall区是否突然掉到0——如果掉得早说明模型要把所有缺陷都抓出来时会产生大量误检这在PCB场景中会拖垮复判环节。混淆矩阵则看两类错误真实缺陷没被检出missing detection、背景被误判成缺陷false positive右下角如果有明显高亮块直接参考5.4补负样本。6.2 亲手拍50张自己板子的图比任何测试集都可信我拿到任何PCB数据集第一件事永远是画类别分布柱状图第二件事是抽20张图看标注质量这两步做完才考虑训练。训练完再拍50张自己产线的板卡图做冒烟测试这一步能让你快速判断这份公开数据集到底能不能用。如果在自己板子上漏检集中在某一两类缺陷优先补这些类别的真实样本如果掉点均匀大概率是光照和纹理差异问题先尝试对训练图做颜色增强、灰度扰动仍不行再考虑迁移学习。这个习惯帮我在不少项目上避免“公开数据集mAP虚高、产线实测翻车”的尴尬。你可能也会遇到类似的情况希望这篇笔记帮你少踩几个坑。本文还有配套的精品资源点击获取