简介:PCB板缺陷检测数据集源自北京大学开放资源,面向深度学习与机器视觉领域从事缺陷检测、图像分类和目标检测研究的学生、工程师与算法开发者,可用于PCB制造质量管控场景中的模型训练与算法验证。该数据集在原始真实缺陷样本基础上进行数据增强,形成含6930张图像的扩充版本,覆盖多余铜、缺口、毛刺等典型缺陷类别。压缩包共1346个文件,以jpg缺陷图像、xml标注文件与txt说明清单为主体,总容量约584MB,xml采用目标检测通用标注格式,可直接配合YOLO、Faster R-CNN等框架开展训练。目前已有2223人学习下载;增强后的样本量约为原始数据的数倍,图像尺寸与缺陷形态多样,既能支撑有监督检测模型训练,也便于验证增强策略对泛化性能的影响,省去自行采集和标注的高昂成本,是PCB缺陷检测课题可供直接上手的实用数据集。
1. PCB板缺陷检测数据集:693张原图扩成6930张,这条增强路径值不值得复现
PCB板缺陷检测数据集(某高校实验室693数据增强,6930)——这个标题核心是PCB缺陷检测里最现实的一条数据增强路径:693张原始缺陷图,通过几何和颜色变换扩成6930张,覆盖漏孔、鼠咬、断路、短路、毛刺、残铜六类常见缺陷。它解决的是工业视觉里最卡脖子的环节:缺陷样本少、标注贵、产线上还总出现新形态。这个数据集的工程价值不只是「有一批图可以训练」,而是把「几百张原图怎么扩到几千张、扩的时候怎么不弄坏标注」这件事做成了可复现的流程。适合谁:手里只有几百张缺陷图、想快速跑通YOLO系检测模型、需要一份基线数据做方案验证的从业者。读完之后你能判断这条路是否值得照搬到自己产线上。
2. 拆解这个PCB数据集:从六类缺陷到6930张增强副本的分布规律
拿到任何数据集的第一步不是急着写训练脚本,而是把目录结构、标注格式、类别分布彻底摸一遍。这个数据集的原始规模是693张,增强后是6930张,多出来的6237张都是副本。如果不先把副本的命名和生成规律搞清楚,后面划分训练验证集时很容易把同一张原图的多个副本同时塞进两个集合,训练时mAP虚高,一上真实产线立刻现原形。下面从缺陷定义、目录结构、增强手法三块拆开讲。
2.1 六类PCB缺陷的视觉特征与检测难点
六类缺陷各自长什么样、难在哪,直接决定你后面怎么调anchor、怎么设类别权重,所以先逐个说清楚。
漏孔(missing_hole):过孔位置没有打通,视觉上是一块完整的实心圆盘。这类目标很小,在640×640的板图上往往只占二三十个像素,和孔径环状纹理混在一起时很容易被模型当成背景。检测难点是小目标加低对比度,用YOLO默认anchor容易漏检。实际排障时我会把漏孔单独拎出来看AP曲线,而不是看总的mAP。
鼠咬(mouse_bite):焊盘边缘出现圆滑凹陷,像被咬掉一块。它和毛刺的方向正好相反:鼠咬是内凹,毛刺是外凸。难点在于它和正常布线拐角的梯度非常相似,拍摄角度稍微偏一点就容易漏。这类缺陷在深色板材上尤其吃亏,因为凹陷边缘的反光会制造大量干扰。
断路(open_circuit):走线中间断开,数据里常见断口只有1到5个像素宽。在深色板材上,断口的对比度很低,检测器经常把它当作正常走线的反光干扰。断口一旦靠近过孔或拐角,漏检概率会明显上升。这类缺陷的虚检也常见,板面上的划痕和丝印缺口经常被误报成断路。
短路(short):两条本不该连接的导线之间出现了桥接。它比断路好认,因为桥接区域往往是一块明显的铜区。真正的麻烦在于它和正常的丁字焊盘结构很像,模型区分这两者靠的是上下文而非单点特征,所以短路的误检往往集中在焊盘密集区。
毛刺(burr):走线边缘长出针状或三角状突起,多半是蚀刻不净造成。尺寸小、长宽比大,容易和丝印文字重叠。检测难点是长条形目标在检测框里占比很扁,IoU对定位偏差非常敏感,框偏几个像素就可能从真阳性变成假阳性。
残铜(spur):不连主线的孤立铜屑,位置随机。它和毛刺的本质区别是毛刺一定长在走线边缘,残铜可以孤悬在任意位置。难点是它和板面上的油墨斑、灰尘在视觉上没有本质区别,误检率偏高。我见过不少模型把白色丝印误判成残铜,这类问题靠数据增强解决不了,只能靠调conf阈值或者加背景负样本。
这六类里,漏孔和残铜是最容易拖后腿的两类:一个太小,一个太像噪声。后面训练时如果总mAP看着还行、细看又不行,基本都是这两类在拉低。
2.2 目录结构、VOC与YOLO两种标注格式
解压之后先打印前20个文件名,确认命名规律,再看目录结构。常见布局是四块:原图目录、标注目录、增强后图像目录、增强后标签目录。
PCB_Dataset_6930/ ├── images/ │ ├── train/ # 增强后训练图像 │ ├── val/ # 验证图像 │ └── test/ # 测试图像 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── originals/ # 693张增强前原图 ├── annotations/ # 原图对应的VOC XML标注 └── classes.txt # 六类名称,一行一个注意不同渠道打包的版本里文件名规律不一致,有的是纯序号,有的是「编号_类别_增强后缀」。不管哪种,训练前必须确认两件事:一是images和labels的文件名一一对应,二是classes.txt里类别顺序和标注里一致。顺序错位是最容易发生的静默错误,后面类别ID全乱。
原始标注是VOC格式,每个XML对应一张图,结构里包含图像尺寸和每个目标的名字、边界框。示例如下:
<annotation> <size> <width>640</width> <height>640</height> </size> <object> <name>missing_hole</name> <bndbox> <xmin>128</xmin> <ymin>96</ymin> <xmax>150</xmax> <ymax>118</ymax> </bndbox> </object> </annotation>YOLO训练用的是归一化txt标注,一行为一个目标:class_id x_center y_center width height,四个坐标都是0到1之间的小数。转换时最容易出错的是忘记把坐标除以图像宽高,归一化后的值一旦超过1,YOLO训练大概率报出「all bbox not valid」。另一个隐蔽问题是空标注,部分原图没有缺陷但XML文件仍然存在,转换脚本如果直接跳过,labels目录就会少文件,后面一统计就对不上。
2.3 从693到6930的增强手法与标注同步逻辑
从693到6930意味着每张原图生成约9个增强副本。常见组合是旋转90、180、270度,水平翻转,垂直翻转,再加亮度、对比度抖动。几何变换贡献了大部分倍数,颜色抖动负责模拟不同曝光条件。
| 增强方式 | 轮次倍数 | 是否影响标注框 | 模拟场景 |
|---|---|---|---|
| 水平/垂直翻转 | 2 | 坐标需要同步翻转 | 板材翻面、相机镜像 |
| 旋转90/180/270度 | 3 | 坐标旋转且宽高可能互换 | 板材朝向变化 |
| 亮度/对比度抖动 | 2 | 不影响坐标 | 不同曝光条件 |
| 高斯噪声 | 1~2 | 不影响坐标 | 相机传感器噪声 |
几何增强最坑的地方是标注同步。90度或270度旋转后,检测框的宽和高要互换;做任意角度旋转时,旋转后的框要用四个角点重新取最小外接矩形。如果直接拿原标注套上去,模型接收的就是错位监督,训练过程中loss能降但验证集一测就露馅。
下面这段代码是我常用的任意角度旋转带标注同步的写法,用OpenCV的旋转矩阵一次性把图像和四个角点都转过去:
# rotate_bbox.py import cv2 import numpy as np def rotate_image_bbox(image, boxes, angle, scale=1.0): h, w = image.shape[:2] # 绕图像中心旋转 M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, scale) rotated = cv2.warpAffine(image, M, (w, h), borderValue=(0, 0, 0)) new_boxes = [] for (x1, y1, x2, y2) in boxes: corners = np.array([ [x1, y1], [x2, y1], [x2, y2], [x1, y2] ], dtype=np.float32) # 关键一步:角点跟着旋转矩阵一起变换 rotated_corners = cv2.transform( corners.reshape(-1, 1, 2), M).reshape(-1, 2) nx1 = rotated_corners[:, 0].min() ny1 = rotated_corners[:, 1].min() nx2 = rotated_corners[:, 0].max() ny2 = rotated_corners[:, 1].max() # 裁到图像边界以内 nx1 = max(0, int(nx1)); ny1 = max(0, int(ny1)) nx2 = min(w, int(nx2)); ny2 = min(h, int(ny2)) if nx2 > nx1 and ny2 > ny1: new_boxes.append((nx1, ny1, nx2, ny2)) return rotated, np.array(new_boxes)这段代码里最值得注意的不是旋转本身,而是cv2.transform这一行。它让角点严格跟随图像坐标变换,不会出现「图转了、框没转」的错位。borderValue设成0会补黑边,如果原图深色背景就会融入,但如果你的板子是浅色底,记得改成255或者原图边缘均值。旋转后取最小外接矩形会让框轻微变大,这是正常现象,比框错位要好得多。
3. 数据预处理:把VOC标注转成YOLO格式并检查每一个细节
YOLO系框架虽然能读VOC,工程上我还是建议先把所有标注统一成YOLO txt格式。理由有三:txt是纯文本,git diff方便,改错一眼能看出;后续做mosaic增强或自定义采样逻辑时,归一化坐标比绝对坐标好处理;跨版本迁移时txt格式最稳定,不依赖特定XML解析库。把原始693张图先转好,增强副本沿用同一套转换逻辑,整个数据集的使用就顺了。
3.1 VOC转YOLO的转换脚本与逐行说明
转换脚本的核心是遍历所有XML,解析每个object的name和bndbox,再除以图像宽高得到归一化坐标。下面这个脚本是我常用的模板,干净够用:
# voc2yolo.py import xml.etree.ElementTree as ET import glob import os CLASSES = ['missing_hole', 'mouse_bite', 'open_circuit', 'short', 'burr', 'spur'] def convert(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CLASSES: continue # 跳过类别清单外的目标 cls_id = CLASSES.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) # 归一化到 0~1 x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / 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}") if not lines: return # 空标注不生成txt,避免后面误判 base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) if __name__ == '__main__': xml_files = glob.glob('annotations/*.xml') os.makedirs('labels_txt', exist_ok=True) for xml_file in xml_files: convert(xml_file, 'labels_txt') print(f"converted {len(xml_files)} xml files")脚本有两个容易翻车的点。第一是CLASSES顺序必须和后续训练配置一致,否则类别ID错位,模型会拿漏孔的标签去学短路的特征,而且训练时毫无报错。第二是x_center的计算,统一用(xmin+xmax)/2/img_w最稳,别写成xmin + w/2再单独归一化,容易漏除某个值。
跑完脚本后不要急着训练,先抽查三个文件:挑一张多目标的XML,转成txt后手工算一遍坐标,确认数值在0到1之间;再看类别ID和classes.txt的对应关系;最后数一下labels目录下的txt数量和images目录下的图片数量是否一致。缺txt通常意味着XML里没有object,需要回头确认是不是标注本身为空。
3.2 文件名、类别名与图片尺寸的三重检查
数据质量检查不能省,尤其是非官方渠道下载的压缩包更容易出现文件名截断或尺寸不一致。我按三个维度排查。
第一重是文件名对应。遍历images目录,逐个检查同名txt是否存在。第二重是类别合法性,检查每一行txt的第一个数字,如果超过5,说明转换脚本的CLASSES顺序错了。第三重是图片尺寸,PCB板图大多是640×640,但部分增强副本在保存时可能被resize过,尺寸不统一会导致训练时imgsz强制缩放,小目标进一步丢失。
# check_dataset.py import os from collections import Counter img_dir = 'images/train' label_dir = 'labels/train' cls_counter = Counter() for img_name in os.listdir(img_dir): base = os.path.splitext(img_name)[0] txt_path = os.path.join(label_dir, base + '.txt') if not os.path.exists(txt_path): print(f"[missing] {img_name}") continue with open(txt_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"[bad line] {base}: {line}") continue cls_id = int(parts[0]) cls_counter[cls_id] += 1 total_boxes = sum(cls_counter.values()) for cls_id, cnt in cls_counter.items(): print(f"class {cls_id}: {cnt} boxes, {cnt / total_boxes * 100:.1f}%")这段脚本输出的类别统计非常有用。六类缺陷的目标数量分布会直接影响训练策略,如果漏孔的目标占比明显低于其他类,后面要针对小目标类单独处理。而如果残铜占比高到离谱,比如超过40%,那大概率是标注重名或者XML解析重复,需要回头看原始标注,不要急着训练。
3.3 训练/验证/测试划分的两种取法
划分集合有两种思路,取决于你拿到的是分好的压缩包还是散装数据。如果已经分好,直接用;如果只有原始集合,自己划分时我建议按原始图像划分,再做增强。
第一种是「先划分后增强」:把增强前的693张原图按8:1:1随机分成train、val、test,然后分别对每个子集做同样的增强操作,得到对应的增强副本。好处是增强副本和原图同源同域,val和test不会混进同一张原图的不同副本,数据泄漏风险最小。
第二种是「先增强后划分」:把693张扩充到6930张再随机划分。速度更快,但如果随机种子没固定,train里出现某张原图的水平翻转副本,val里又出现同一张原图的垂直翻转副本,验证集等于开卷考试。这个数据集的坑就在这,因为10倍增强后副本数量大,随机划分很容易把同源副本散落到不同集合。
我一般只推荐第一种。原因只有一个:数据集的本质是「同一块板子的多个视角副本」,划分必须按原始图像维度进行,不能按增强副本维度进行。这个原则在任何一个做数据增强的目标检测项目里都成立。划分完还要用前面给的检查脚本核对三个目录的类别分布,确保val和test没有偏离train太多。
4. 训练YOLOv8缺陷检测模型:pcb.yaml配置、关键参数与收敛判断
数据准备完毕,接下来就是把6930张图喂进模型。这个步骤按实际工程标准走:先给配置,再给命令,最后讲怎么看训练日志判断要不要停。很多人在这一步把epoch拉得很长,结果只是浪费电,真正该关注的是mAP有没有进入平台期。
4.1 pcb.yaml配置与启动命令
YOLO训练需要的第一个文件是数据集配置。把下面的内容存成pcb.yaml:
# pcb.yaml path: /data/PCB_Dataset_6930 # 改成你的实际路径 train: images/train val: images/val test: images/test nc: 6 names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short 4: burr 5: spur一个细节:train和val字段既可以写目录路径,也可以写txt文件路径,txt里每行是一张图像。当数据集很大时我倾向于用txt方式,因为调整子集范围更灵活,不必移动文件。这里的path是根目录,YOLO会把它和train、val拼接成完整路径,所以根目录路径别写错。
启动训练用下面的命令,模型选择yolov8n是因为这个数据集规模不大,n版本性价比最高。显存小、训练快,作为基线足够。
yolo detect train \ data=pcb.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ seed=42 \ device=04.2 关键参数的经验值与调整逻辑
上面这些参数不是随便填的,每一个都可以根据你的显存和任务调整。我整理了一张经验表,覆盖常见调整场景:
| 参数 | 本数据集推荐值 | 什么时候改 | 改多大 |
|---|---|---|---|
| epochs | 120 | 最后mAP还在明显上升 | 放到180~200 |
| batch | 16 | 显存有余量且训练慢 | 32,注意OOM |
| imgsz | 640 | 想提升小目标召回 | 768,代价是显存和速度 |
| lr0 | 0.01 | loss震荡或变nan | 0.005 |
| patience | 30 | 训练波动大 | 50 |
| seed | 42 | 需要多次对比实验 | 固定同一值 |
关键解释一下几个点。imgsz设为640是因为原图就是640×640,强行用更大尺寸不会带来显著提升,只会增加显存压力和推理耗时。lr0在epoch比较多时可以用0.01起步,如果发现前20轮loss不降反升,多半是学习率偏大,降到0.005重开。patience是早停轮数,太小会在第60轮附近错过小目标类别AP还在爬升的机会,所以30是下限,不要低于20。
4.3 训练日志和验证指标如何对照分析
训练结束后,runs/detect/train目录下会有results.csv和weights/best.pt。results.csv每一行对应一个epoch,包含train loss、val loss、mAP50、mAP50-95等指标。我判断收敛的标准有三个。
第一,看mAP50是否进入平台期。如果最后20轮的mAP50波动不超过0.5%,模型基本收敛,再训练只是过拟合。第二,看precision和recall的相对高低。PCB缺陷检测更看重recall还是precision取决于质检流程:后段有人工复检的产线,recall优先,漏检比误检可怕;全自动筛选则precision优先,误检太多会淹没产线。第三,看每类别的AP分布。results.csv里不会单独列每类AP,需要额外跑一次验证命令拿per-class报告,这一步在第6章展开。
训练日志不是黑匣子,loss下降曲线能反映很多问题。train loss在下降但val loss在上升,典型过拟合,此时加数据增强比加epoch有用。train loss和val loss都下不去,可能是数据集本身标注噪声大,或者学习率不合适。如果遇到loss瞬间拉高又恢复,多半是某个batch里出现了裁剪过度的增强样本,可以在下一次训练时把mosaic关掉验证。
5. 跑PCB缺陷检测最容易踩的4个坑与排查记录
这一部分是我自己在这个数据上趟过的坑,按「现象→原因→解决」写,每一条都是真会遇到的场景,不是理论推导。
5.1 增强副本串进验证集,精度虚高却不自知
现象:训练日志显示验证集mAP50稳定在0.9以上,效果看起来很好。把best.pt部署到产线摄像头拍的实板上测试,mAP掉到0.5上下,差距巨大,当时第一反应是模型过拟合,折腾半天发现根本不是。
原因:数据划分时把增强副本当成了独立样本。同一张原图的水平翻转副本进训练集,垂直翻转副本进验证集,模型在训练阶段已经见过同一块板子的所有视角,验证成了开卷考试。
解决:严格按原始图像维度划分集合。如果拿到的数据集已经分好,先用脚本检查val和train里有没有同名前缀,也就是同一张原图派生出来的副本,有就说明划分不干净。我的习惯是训练前把原始693张图单独复制到一个目录,只在这份干净原图上做最终验证对比,比信任打包好的val目录靠谱得多。
5.2 旋转90°后标签没换宽高,预测框整体错位
现象:训练loss正常,验证时发现某类缺陷的预测框总是从左边偏到右边,偏移量差不多四分之一图宽。当时以为是anchor没调好,排查半天才发现是标注问题。
原因:增强脚本对图像做了旋转,但对标签只处理了中心坐标,没有交换框的宽和高。一个宽20像素、高40像素的框,旋转90度后应该变成宽40像素、高20像素,代码没处理这个交换,标签框就和图像内容错位。
解决:几何变换时把框的四个角点跟旋转矩阵一起变换,再重新取最小外接矩形,不要只改中心坐标。写完增强脚本后,随机挑5张增强图,把标注框渲染到图上人工看一眼,比写一百行自动化检查更直观。这个习惯帮我省了很多次翻车。
5.3 小目标类别漏检严重,AP被拉低
现象:总体mAP50在0.85,但漏孔类AP50只有0.5左右,残铜类只有0.45,其他四类都在0.9以上。看上去整体不错,细看完全不能用。
原因:六类缺陷的目标数量差距大,漏孔和残铜本身样本少、目标又小,模型把学习能力倾向到了数量多、面积大的类别上。6930张图听着多,摊到六类小目标上,每个类别的正样本可能只有几千个,其中漏孔类尤其吃亏。
解决:先看类别分布表,某类目标占比低于10%就单独给它做一轮增强,比如漏孔样本多做几次旋转和缩放。训练时也可以用weighted sampler,让每个batch见到小目标类的概率更高。不要一上来就调损失函数权重,box loss和cls loss的尺度不一样,调起来容易引发别的类精度暴跌。
5.4 训练后期loss变成nan
现象:训练到第50轮左右,train loss突然变成nan,接着val mAP归零,整个训练进程报废。当时第一反应是代码bug,翻遍脚本没找到问题。
原因:最常见的是学习率过高,加上mosaic增强把多张小图拼在一起,边界处的黑边参与计算后梯度爆炸。另一个容易被忽略的原因是数据集里有破损图像,增强脚本读图失败返回空数组,导致loss算不下去。
解决:第一步把lr0从0.01降到0.005,并确保warmup生效;第二步检查数据目录里有没有损坏的jpg,用脚本逐个打开图像测试能否解码;第三步在增强脚本里对读取失败做try-catch,出现一张坏图就跳过,而不是终止整个进程。这三步做完,nan问题基本不会再出现。
6. 验证与进阶:用混淆矩阵和693张干净原图反推模型漏检
训练完成不等于实验完成,真正该做的是搞清楚模型漏的是哪一类,以及漏在什么形态上。这一步能反推数据集的短板,也能帮你决定要不要补充某个类别的标注样本。
先在测试集上跑一次per-class验证:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') metrics = model.val(data='pcb.yaml', split='test', conf=0.25, iou=0.5) for i, name in model.names.items(): print(f"{name:<16} AP50={metrics.box.ap50[i]:.3f} " f"precision={metrics.box.p[i]:.3f} recall={metrics.box.r[i]:.3f}") matrix = model.val().confusion_matrix print(matrix)输出里能清楚看到哪一类AP50最低。结合类别分布表判断:如果残铜类目标数量少且AP低,是样本不足;如果目标数量多但AP低,是特征本身和背景太像,需要换模型结构或提高输入分辨率。这个判断决定了下一步投入方向,是继续标数据还是改网络。
进阶的验证习惯我特别强调:用原始693张干净图重新跑一次推理,不要只信增强后的测试集。做法是遍历originals目录,对每张原图做推理并统计每一类的recall。如果原图上的recall比增强后测试集低5个百分点以上,说明模型在依赖增强副本的某种角度特征,泛化能力不足。我每次训完模型都会做这一步,这比任何训练曲线都更能反映真实水平。
关于这个小数据集的投入判断,我可以给一个明确倾向:如果你的缺陷种类和这六类接近,值得先拿它跑通流程、验证模型选型;如果产线缺陷形态差异很大,不要盲目复用增强参数,而是只借鉴「按原图划分、同步标注、干净原图复检」这三条习惯。把这三条习惯搬到自己的数据上,比多训一百个epoch有用得多。我的教训是:刚接触这份数据时急着调参,浪费了很多轮训练时间,后来才想明白,数据集本身的质量边界才是真正决定精度的东西。希望这些经验能帮你在同类项目里少走弯路。
本文还有配套的精品资源,点击获取