
简介本资源为面向PCB缺陷检测任务的图像数据集适合从事工业质检、深度学习目标检测的开发者与研究人员使用可用于训练与验证缺陷识别模型。数据集覆盖六类常见PCB缺陷包括Missing_hole、Mouse_bite、Open_circuit、Short、Spur、Spurious_copper标注精确、类别分布均匀有助于拟合效果较好的检测模型。压缩包共1386个文件由693张jpg图像与693个xml标注文件组成整体约907.52MB标签同时提供VOC与YOLO两种格式方便直接接入不同检测框架。目前已有2904人学习下载作者为DeepLearning_。需要说明的是受上传文件大小限制当前包内为部分样本完整数据需另行联系作者获取且仅建议通过CSDN官方渠道下载以获得完整最新版本与后续更新。1. PCB缺陷数据集2700张VOC与YOLO双格式标签到底怎么用手上拿到一个标注好的PCB缺陷数据集2700张图同时带Pascal VOC的XML和YOLO的TXT两套标签这件事本身就省掉了一大半标注成本。做过PCB表面缺陷检测的人都知道最难的不是选模型而是凑出一批类别一致、框位准确、格式能直接喂进训练脚本的图。这个数据集解决的正是这个卡点它把VOC那种人可读、工具链兼容性好的XML和YOLO那种直接进训练器的归一化TXT放在一起让你在数据转换、格式校验、训练配置之间少走弯路。适合谁一是刚接触PCB缺陷检测、想跑通第一个baseline的工程师二是手里有产线图但标注格式混乱、需要统一到YOLO训练流的团队三是想验证自己写的转换脚本对不对、拿现成双格式做交叉校验的人。下面按“先搞懂两套标签的差异再动手转换和训练最后讲坑”的顺序讲透。2. VOC与YOLO标签的差异坐标、类别与目录结构2.1 两套标签的坐标体系为什么不能直接混用Pascal VOC的XML存的是绝对像素坐标xmin、ymin、xmax、ymax四个值直接对应原图像素位置读出来就能画框。YOLO的TXT存的是归一化中心点坐标加宽高格式是class_id x_center y_center width height五个值都在0到1之间且是相对于图像宽高的比例。这个差异决定了你不能把XML里的坐标直接塞进YOLO训练器也不能把TXT里的比例值当像素用。常见做法是写一个转换脚本读XML拿到图像宽高把绝对坐标换算成归一化值。这里有个容易翻车的点如果XML里记录的图像尺寸和实际图片尺寸不一致换算出来的框会整体偏移。我一般会在转换前先遍历一遍所有XML核对size里的宽高和cv2.imread读出来的shape是否一致不一致的直接标记出来人工确认。2.2 目录结构怎么摆才能让训练脚本直接认VOC的标准目录是Annotations放XMLJPEGImages放原图ImageSets/Main放训练验证划分文件。YOLO的标准目录是images放图labels放TXT且图片和标签同名不同后缀放在两个平行目录下。这个数据集既然同时含两种格式落地时建议按下面这样组织避免训练时路径找不到pcb_dataset/ ├── images/ # 所有原图jpg或png │ ├── 000001.jpg │ └── ... ├── labels/ # YOLO格式TXT与images同名 │ ├── 000001.txt │ └── ... ├── annotations_voc/ # VOC格式XML保留原始 │ ├── 000001.xml │ └── ... ├── train.txt # 训练集图片路径列表 ├── val.txt # 验证集图片路径列表 └── data.yaml # YOLO训练配置train.txt和val.txt里每行写一张图的绝对路径或相对路径YOLO训练器靠这个列表找图再自动去labels目录找同名TXT。data.yaml里写类别数和类别名路径指向这两个列表文件。这个结构的好处是VOC的XML原封不动留着方便你回头用其他框架或做可视化核对YOLO的TXT直接进训练流互不干扰。2.3 类别映射表必须先定死再动手PCB缺陷常见类别有短路、开路、鼠咬、余铜、缺铜、针孔、划痕等不同数据集类别命名可能不一样。转换前必须先把类别名和类别ID的映射写死比如short: 0, open: 1, mouse_bite: 2, spur: 3, copper: 4, pin_hole: 5, scratch: 6。这个映射一旦定了XML转TXT、训练配置、推理后处理都要用同一套。我见过有人转换时按字母序排训练时按另一个顺序写data.yaml结果模型学出来的类别全乱排查半天才发现是映射不一致。建议把映射单独写成一个classes.txt每行一个类别名行号就是类别ID转换脚本和训练配置都从这个文件读。3. 从VOC XML转YOLO TXT转换脚本与四个边界坑3.1 转换脚本的完整实现与逐行说明下面这个脚本读annotations_voc下的XML输出到labels目录同时生成train.txt和val.txt。用Python写依赖xml.etree.ElementTree和os不需要额外装库。import os import xml.etree.ElementTree as ET import random # 类别映射行号即类别ID classes [short, open, mouse_bite, spur, copper, pin_hole, scratch] class_to_id {name: i for i, name in enumerate(classes)} def convert_xml_to_yolo(xml_path, img_w, img_h): 读一个XML返回YOLO格式的行列表 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in class_to_id: print(f跳过未知类别 {cls_name} in {xml_path}) continue cls_id class_to_id[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 裁剪到图像边界内防止越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) if xmax xmin or ymax ymin: print(f跳过无效框 in {xml_path}) continue 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 def main(): voc_dir annotations_voc img_dir images out_dir labels os.makedirs(out_dir, exist_okTrue) all_names [] for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] xml_path os.path.join(voc_dir, xml_file) # 从XML里读图像尺寸和实际图核对 tree ET.parse(xml_path) size tree.getroot().find(size) xml_w int(size.find(width).text) xml_h int(size.find(height).text) img_path os.path.join(img_dir, stem .jpg) if not os.path.exists(img_path): print(f图片缺失 {img_path}) continue lines convert_xml_to_yolo(xml_path, xml_w, xml_h) with open(os.path.join(out_dir, stem .txt), w) as f: f.write(\n.join(lines)) all_names.append(stem) # 按8:2划分训练验证 random.shuffle(all_names) split int(len(all_names) * 0.8) with open(train.txt, w) as f: for n in all_names[:split]: f.write(os.path.abspath(os.path.join(img_dir, n .jpg)) \n) with open(val.txt, w) as f: for n in all_names[split:]: f.write(os.path.abspath(os.path.join(img_dir, n .jpg)) \n) print(f转换完成共{len(all_names)}张训练{split}张验证{len(all_names)-split}张) if __name__ __main__: main()逻辑说明脚本先定义类别映射然后逐个读XML取object下的类别名和bndbox坐标做边界裁剪后换算成归一化中心点加宽高写入同名TXT。最后打乱所有样本名按8:2写train.txt和val.txt。参数说明classes列表的顺序就是类别ID必须和data.yaml里的names顺序完全一致x_center等值保留6位小数YOLO训练器对精度不敏感但保留足够位数避免累积误差边界裁剪用max(0, min(...))防止标注框超出图像范围导致归一化值大于1或小于0。3.2 边界坑一XML里的尺寸和实际图片对不上现象是转换后框整体偏移或者框大小明显不对。原因通常是XML的size里写的宽高和实际图片不一致可能是标注工具导出时用了缩略图尺寸或者图片被重新保存过。解决方法是转换前先跑一遍核对脚本把不一致的样本列出来。我一般会在转换脚本里加一句判断如果xml_w ! actual_w or xml_h ! actual_h就打印警告并跳过人工确认后再决定用哪个尺寸。更稳妥的做法是直接用cv2.imread读实际图片拿宽高忽略XML里的size但前提是图片本身没有被错误缩放。3.3 边界坑二类别名有空格或大小写不一致现象是转换时提示未知类别或者同一类被拆成两个ID。原因是XML里name标签的内容可能带前后空格或者有的写Short有的写short。解决方法是读取时用.strip()去空格并统一转小写再查映射表。如果数据集里确实存在大小写混用建议在映射表里同时登记大小写变体或者在转换前批量把XML里的类别名规范化。这个坑不处理训练时会出现类别数比预期多、某些类样本极少的情况。3.4 边界坑三空标签文件和纯背景图现象是某些TXT是空的训练时YOLO会报错或直接跳过。原因是原图里没有缺陷XML里没有object节点。YOLO训练器对空标签文件的处理方式因版本而异有的版本会当成负样本有的会直接报错。稳妥做法是保留空TXT但在train.txt里也保留对应图片路径让训练器自己决定。如果训练器不支持空标签就把这些图从列表里剔除或者单独作为背景图处理。我一般会统计空标签数量如果占比超过10%说明数据集里正常样本太多需要考虑是否单独加一类“无缺陷”或者调整采样策略。3.5 边界坑四坐标越界和零面积框现象是转换后归一化值出现负数或大于1或者宽高为0。原因是标注时框拉到了图像外面或者xmin等于xmax。解决方法是转换时做裁剪和有效性判断xmax xmin或ymax ymin的直接跳过并记录。裁剪后如果框面积小于几个像素也建议跳过这种框对训练没有正贡献反而引入噪声。裁剪逻辑写在转换脚本里比训练时再处理更省事。4. 用这份数据集跑通YOLO训练配置、命令与验证4.1 data.yaml的写法与三个必调参数YOLO训练靠data.yaml告诉训练器去哪找图、有几类、类名叫什么。基于上面的目录结构data.yaml这样写path: /absolute/path/to/pcb_dataset train: train.txt val: val.txt nc: 7 names: 0: short 1: open 2: mouse_bite 3: spur 4: copper 5: pin_hole 6: scratchpath是数据集根目录train和val是相对path的列表文件路径。nc是类别数必须和names的条目数一致。names的顺序必须和转换脚本里的classes列表完全一致否则类别ID对不上。三个必调参数一是nc写错直接导致类别数不匹配报错二是names的顺序错了模型学出来的类别全乱三是path写相对路径时容易因为工作目录不同找不到文件建议写绝对路径。4.2 训练命令与关键超参设置假设用YOLOv8训练命令如下yolo detect train \ data/absolute/path/to/pcb_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectpcb_runs \ nameexp1model选yolov8n.pt是轻量版适合先跑通流程epochs设100PCB缺陷数据集2700张不算大100轮通常够收敛imgsz设640如果原图分辨率很高可以设更大但显存占用会增加batch设16显存不够就降到8lr0初始学习率0.01是常用值如果loss震荡明显可以降到0.001patience设2020轮验证指标不提升就早停省时间。训练过程中重点看mAP50和mAP50-95两个指标前者是IoU阈值0.5时的平均精度后者是0.5到0.95多个阈值下的平均后者更能反映框的定位质量。4.3 训练完怎么验证标签转换没出错训练结束后用验证集跑一次推理把预测框和真实框画在同一张图上对比。如果预测框和真实框位置基本重合说明标签转换没问题如果整体偏移回去查XML尺寸和实际图片是否一致如果类别标错查data.yaml的names顺序和转换脚本的classes是否一致。另一个验证方法是直接用YOLO的验证命令输出每类的precision和recall如果某一类recall极低可能是该类样本太少或者标签有问题。我一般会抽10张图人工核对重点看小目标缺陷的框有没有漏标或错标。yolo detect val \ modelpcb_runs/exp1/weights/best.pt \ data/absolute/path/to/pcb_dataset/data.yaml \ imgsz640这条命令输出每类的指标如果某类instances数量为0说明验证集里没有该类样本需要调整划分比例或补充数据。5. 避坑与排查PCB缺陷数据集训练中的五个血泪经验5.1 现象训练loss正常下降但mAP一直很低原因通常是标签格式有问题模型学的是错误的目标。排查步骤先抽几张图把TXT里的框画出来和原图对比看框的位置和类别对不对。如果框位置对但类别错查data.yaml的names顺序如果框位置整体偏移查XML尺寸和实际图片是否一致如果框大小明显不对查归一化时用的宽高是不是搞反了。这个坑我踩过当时是width和height在换算时写反了框全部横向拉伸mAP卡在0.1上不去排查了一下午。5.2 现象训练报错“Label class x is out of bounds”原因是TXT里的类别ID超过了nc定义的范围。比如data.yaml里nc: 7但某个TXT里出现了7或更大的ID。排查方法是遍历所有TXT找出最大类别ID和nc对比。常见原因是转换脚本的classes列表和data.yaml的names不一致或者XML里出现了映射表之外的类别名转换时没跳过而是给了默认ID。解决方法是统一映射表转换时遇到未知类别直接跳过并记录不要给默认值。5.3 现象某些图片训练时被跳过提示“image not found”原因是train.txt或val.txt里的路径写错了或者图片扩展名和列表里写的不一致。比如列表里写.jpg但实际图片是.png。排查方法是逐行读列表文件用os.path.exists检查每个路径。解决方法是生成列表时用os.path.splitext动态获取扩展名不要硬编码。另外注意路径里的空格和特殊字符YOLO训练器对含空格的路径支持不好建议数据集路径不要带空格。5.4 现象小目标缺陷检测效果差漏检多PCB缺陷里针孔、划痕这类目标通常很小640的输入尺寸下可能只有几个像素。原因是下采样后特征丢失。解决方向有三个一是提高imgsz到1024或1280代价是显存和训练时间增加二是用带P2层的模型结构P2层对应更高分辨率的特征图对小目标更友好三是数据增强里加mosaic和copy_paste增加小目标的出现频率。我一般先试提高imgsz如果显存不够再换模型结构。另外检查标签里小目标的框有没有被漏标2700张里如果小目标样本占比低模型学不好也正常。5.5 现象验证集指标波动大每次训练结果差很多原因是验证集太小或者划分不均匀。2700张按8:2划分验证集540张如果某些类别在验证集里只有几张指标波动就会很大。解决方法是做分层抽样保证每个类别在训练集和验证集里的比例接近。另一个原因是随机种子没固定每次划分不一样。建议在划分前固定random.seed(42)并且把划分结果保存下来后续训练都用同一套划分方便对比不同模型的效果。6. 进阶技巧用双格式标签做交叉校验与主动学习这份数据集同时带VOC和YOLO两套标签最大的价值不是“多一种格式”而是可以拿来做交叉校验。我一般会写一个校验脚本把XML转成YOLO格式后和数据集自带的YOLO TXT逐行对比如果某个框的坐标差异超过阈值就标记出来人工确认。这个做法能发现标注时的手误比如XML里改了框但TXT没同步更新。校验脚本的核心逻辑是读XML转成YOLO行列表读TXT行列表按类别ID和坐标排序后逐行比对差异超过0.01的就记录。def cross_check(xml_lines, txt_lines, threshold0.01): 对比XML转换结果和自带TXT返回差异列表 diffs [] xml_sorted sorted(xml_lines, keylambda x: (x.split()[0], x.split()[1])) txt_sorted sorted(txt_lines, keylambda x: (x.split()[0], x.split()[1])) if len(xml_sorted) ! len(txt_sorted): diffs.append(f框数量不一致: xml{len(xml_sorted)}, txt{len(txt_sorted)}) return diffs for xl, tl in zip(xml_sorted, txt_sorted): x_parts [float(v) for v in xl.split()] t_parts [float(v) for v in tl.split()] if x_parts[0] ! t_parts[0]: diffs.append(f类别不一致: {xl} vs {tl}) continue for i in range(1, 5): if abs(x_parts[i] - t_parts[i]) threshold: diffs.append(f坐标差异: {xl} vs {tl}) break return diffs这个校验跑一遍2700张大概几分钟能筛出大部分标注问题。另一个进阶用法是主动学习先用这份数据训练一个baseline模型然后拿模型去推理未标注的产线图把置信度中等比如0.3到0.6的预测框导出来人工修正后加入训练集。这样每轮迭代都能用少量人工标注换来明显的指标提升。我自己的习惯是每轮主动学习加200到300张新图训练一轮后看mAP变化如果提升小于1个点就停止说明当前数据分布已经覆盖得差不多了。这套流程跑下来比一次性标几千张再训练要省力得多而且模型对产线实际分布的适应更好。希望帮到你。本文还有配套的精品资源点击获取