ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

deepPCB数据集详解:PCB缺陷检测的VOC/YOLO双格式训练实战

deepPCB数据集详解:PCB缺陷检测的VOC/YOLO双格式训练实战

做PCB缺陷检测的同行,应该都对deepPCB这套公开数据集不陌生。它算是工业视觉里最容易被拿来做算法验证的资源之一:1500张640×640的裸板图像,标注了缺孔、鼠咬、开路、短路、毛刺、余铜6类典型缺陷,覆盖了蚀刻、钻孔、沉铜这些工序常见的异常形态。标题里的"VOC+YOLO格式1500张6类别",是说这份数据集已经把原始标注加工成了Pascal VOC的XML和YOLO的txt两套格式,省去了自己写转换脚本的流程。我之前在负责的质检项目里也倒腾过这套数据,从最开始手动解析txt,到后来直接用转换好的版本训YOLO,中间踩过的坑不算少。这篇文章就把这套数据的底细、拿到手之后的验证方法、三种格式之间的换算逻辑,以及实际训练时的参数和坑位都梳理一遍,给正在做工业缺陷检测或者刚接触YOLO目标检测的朋友做个参考。

1. deepPCB数据集来龙去脉:1500张裸板图里的6类致命缺陷

1.1 为什么PCB缺陷检测是工业视觉绕不开的考题

电路板是几乎所有电子设备的基础骨架,铜箔线路经过蚀刻、钻孔、电镀这些工序之后,一旦出现断路、短路、缺口,轻则功能异常,重则整块板报废。以前很多产线靠人工在放大镜下目检,一个人盯几个小时,眼睛疲劳之后小缺陷的漏检率直线上升,而且不同人之间的判定标准还不统一。AOI(自动光学检测)设备就是在这个背景下把相机、光源和检测算法组合起来,用图像方式替代一部分人工目检。

这里面的核心难度就两个字:找小。PCB裸板上的缺陷尺寸非常小,很多只有十几到几十个像素;同时铜箔在光照下反光,颜色和背景接近,对比度并不高。所以这个场景天然适合考察一个检测模型的极限能力,而不只是"能不能框住一个大目标"。deepPCB被大家反复使用,原因也在这里:它是从真实采集环境里来的,拍摄条件相对统一,标注也经过了人工整理,拿来验证算法、横向对比模型非常方便。

1.2 六类缺陷对照:名字、成因与检测难度

不同发布版本对类别顺序的处理可能不一样,但六类缺陷的名字基本是固定的。为了方便对照,我把每个类别的中文叫法、直观表现和检测难点列成一张表:

类别名中文叫法直观表现检测难点
missing_hole缺孔焊盘或过孔位置没有钻出孔与正常焊盘差异小,需要看中心区域
mouse_bite鼠咬铜线边缘出现凹坑、缺口目标小,像噪声,边界不规则
open_circuit开路铜线断裂,导电路径中断断缝可能只有几个像素宽
short短路两条本不该相连的线路搭在一起搭接点形态多变,颜色近似
spur毛刺线路边缘多出细长铜刺长宽比畸形,容易和正常走线混淆
copper余铜板面残留的铜块、铜渣位置随机,形状不一,反光强

训练时最重要的一件事,就是先确认你手里的类别id顺序和这六类名字一一对应。有的转换版本会把类别按字母序重新编号,也有人按原始txt里的出现顺序来编,顺序错了后面全乱。

1.3 1500张图的规模到底够干什么

1500张,对深度学习目标检测来说不算多,但也不算少。deepPCB里大部分图像包含多个缺陷框,有效标注框的数量是远超1500的,所以拿来训练一个单类别多尺度的检测器完全够用;但如果你想训出一个直接部署到产线的模型,这个量级通常只是起点。

我的建议是把这套数据集当作三件事来用。第一,做算法选型的试验场,快速对比不同尺寸的YOLO模型在"小目标加类别不平衡"场景下的表现;第二,当迁移学习的底料,先在deepPCB上把检测头训稳,再用自己产线攒的几百张图做微调;第三,当数据质量的教科书,通过它养成"拿到数据先验证再训练"的习惯,这个习惯能让你以后处理任何数据集都少踩坑。

2. 拿到VOC+YOLO双格式数据集后,先做这三步核验

有人觉得数据集到手就能直接开训,我劝你慢一点。标注错误有时候比模型结构错误更致命,而且这套数据被反复转换过格式,任何一个环节坐标系理解错了,框就会整体错位。拿到手先花半小时做下面三件事,能省下后面好几个小时的排错时间。

2.1 先看目录和文件数量,把三种格式对账

正常的双格式版本目录结构大概是这样的:

deepPCB_voc_yolo/ ├─ VOC/ │ ├─ JPEGImages/ # 1500张jpg │ ├─ Annotations/ # 1500个xml │ └─ ImageSets/ │ └─ Main/ │ ├─ train.txt │ └─ val.txt └─ YOLO/ ├─ images/ │ ├─ train/ │ └─ val/ └─ labels/ ├─ train/ └─ val/

对账的方法就是数数:JPEGImages下应该正好是1500张jpg,Annotations下是1500个xml,YOLO的train和val加起来也应该是1500。ImageSets里的train.txt、val.txt文件名要能和图片一一对上,YOLO目录里的划分通常和VOC的划分保持一致。任何一个数字对不上,宁可花时间重新整理,也不要带着残缺的数据集直接开训。

2.2 画框可视化:最容易暴露坐标系问题的环节

格式转换最大的坑是坐标系。原始标注的坐标原点在左上角还是左下角、转换时y轴有没有翻转、XML里存的是整数还是浮点数,这些细节一旦错,训练出来的模型指标会很奇怪:loss看起来正常,但预测框要么上下颠倒,要么整体偏移。

我每次拿到新标注都会先跑一个画框脚本,随机抽10到20张图,把XML或YOLO标注直接画在图上,人工确认框有没有贴着缺陷边缘。这一步没什么技术含量,但价值极高。给你一段能直接跑的OpenCV脚本:

import os, random import cv2 import xml.etree.ElementTree as ET img_dir = "deepPCB_voc_yolo/VOC/JPEGImages" ann_dir = "deepPCB_voc_yolo/VOC/Annotations" out_dir = "check_vis" os.makedirs(out_dir, exist_ok=True) for img_name in random.sample(os.listdir(img_dir), 20): xml_path = os.path.join(ann_dir, img_name.replace(".jpg", ".xml")) img = cv2.imread(os.path.join(img_dir, img_name)) tree = ET.parse(xml_path) for obj in tree.getroot().findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(float(bbox.find("xmin").text)) ymin = int(float(bbox.find("ymin").text)) xmax = int(float(bbox.find("xmax").text)) ymax = int(float(bbox.find("ymax").text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(ymin - 5, 15)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(os.path.join(out_dir, img_name), img)

如果框的位置都合理,再随机抽20个YOLO的txt标注,按第3节的公式转回左上角、右下角坐标,同样画一轮。两边都确认过,训练前最大的隐患就排除了。

2.3 标注统计三件套:类别数量、框面积、越界空文件

可视化确认了"方向对",还得确认"分布正常"。我习惯写一个统计脚本,一次性输出几个关键数字:

import glob from collections import Counter cls_counter = Counter() empty_files = [] area_ratios = [] bad_lines = 0 for label_path in glob.glob("deepPCB_voc_yolo/YOLO/labels/train/*.txt"): lines = [ln.strip() for ln in open(label_path) if ln.strip() and not ln.startswith("#")] if not lines: empty_files.append(label_path) for ln in lines: parts = ln.split() if len(parts) != 5: bad_lines += 1 continue cls_id = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) if not (0.0 <= xc <= 1.0 and 0.0 <= yc <= 1.0 and 0.0 <= bw <= 1.0 and 0.0 <= bh <= 1.0): bad_lines += 1 cls_counter[cls_id] += 1 area_ratios.append(bw * bh) print("类别数量:", cls_counter) print("空标注文件:", len(empty_files)) print("标注行异常数:", bad_lines) print("平均框面积占比:", sum(area_ratios) / len(area_ratios))

这几个数字分别回答了三类问题:类别是否齐全、类别是否极度不平衡;有没有图片漏标注;有没有把坐标写到[0,1]范围之外的坏数据。deepPCB的标注质量整体是靠谱的,但转换过程偶尔会产生一行被截断、类名被替换之类的低级错误,统计一遍心里才有底。

3. 三种标注格式深度拆解:坐标系与归一化的换算关系

如果你打算把这份数据集的格式逻辑吃透,或者以后自己手里也有原始txt标注需要转换,这一节建议仔细看。三种格式改来改去,核心就一个东西:怎么用一个矩形框去描述一个缺陷,以及这个矩形框在不同格式里的描述方式有什么不同。

3.1 原始TXT解析:先搞清楚坐标系的约定

deepPCB原始标注一般是txt文本,每一行对应一个缺陷框,常见写法是:

short 165 320 188 342 open_circuit 400 210 430 225

前一个字符串是类别名,后面四个数字是矩形框的坐标。麻烦在于,不同发布版本对坐标原点的约定并不完全统一:有的以图像左上角为原点,x向右增大,y向下增大;有的论文表述里习惯用左下角为原点。把y轴方向搞反,是格式转换里最典型的错误。

所以不管是拿到别人的txt,还是自己写解析器,第一件事永远是拿一张图出来画框验证。我做转换脚本的时候会故意画一遍原坐标、再画一遍y轴翻转后的坐标,对比哪一组跟缺陷边缘吻合,确定坐标约定之后才继续往下走。

3.2 VOC XML的字段结构:bndbox是核心

Pascal VOC是目标检测领域的老牌格式,XML文件里记录图像路径、尺寸、通道数,以及每个目标框的类别和坐标。一份典型的标注长这样:

<annotation> <folder>JPEGImages</folder> <filename>img_0123.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>short</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>165</xmin> <ymin>320</ymin> <xmax>188</xmax> <ymax>342</ymax> </bndbox> </object> </annotation>

bndbox里这四个值用的是绝对像素坐标,左上角xmin、ymin和右下角xmax、ymax,要求xmin小于xmax、ymin小于ymax。VOC格式的优势是人和机器都能读,而且单个文件里自带图像尺寸,换机器迁移也方便。缺点就是冗余字段多,读起来费劲,训练框架通常不直接吃XML。

3.3 YOLO五元组:归一化中心点加宽高

YOLO系列使用的txt标注只有一行五个数:类别id、归一化中心点x、归一化中心点y、归一化宽度、归一化高度。换算公式很简单:

x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height

反过来,从YOLO坐标还原成像素坐标:

xmin = (x_center - box_w / 2) * width ymin = (y_center - box_h / 2) * height xmax = (x_center + box_w / 2) * width ymax = (y_center + box_h / 2) * height

为什么要归一化?因为不同分辨率的图可以用同一套标注通用,模型训练时做的尺寸缩放、多尺度训练都不用跟着改标签。这也是YOLO系训练框架只认这种txt、不认XML的原因。

3.4 同一条标注在三种格式间的等价性验证

转换脚本写完不能只跑一遍了事。我习惯抽几十条标注做一次"三角验证":从原始txt读坐标,转成XML,再从XML转回YOLO,最后对比两条YOLO五元组,误差应该几乎为零。如果误差明显,多半是转换中用了int截断,或者归一化时除错了尺寸。这个验证逻辑可以写成一个函数:

def verify_roundtrip(yolo_line, img_w=640, img_h=640): cls_id, xc, yc, bw, bh = map(float, yolo_line.split()) # 还原绝对坐标 xmin = (xc - bw / 2) * img_w ymin = (yc - bh / 2) * img_h xmax = (xc + bw / 2) * img_w ymax = (yc + bh / 2) * img_h # 再转回归一化 xc2 = (xmin + xmax) / 2 / img_w yc2 = (ymin + ymax) / 2 / img_h bw2 = (xmax - xmin) / img_w bh2 = (ymax - ymin) / img_h return (abs(xc - xc2) < 1e-6 and abs(yc - yc2) < 1e-6 and abs(bw - bw2) < 1e-6 and abs(bh - bh2) < 1e-6)

这段代码本身很简单,但把三种格式的数学关系吃透了。对格式转换比较纠结的同行,我的建议是:格式只是载体,坐标的数学关系才是根本。只要脑子里有"左上角/右下角、绝对/归一化、宽高/中心点"这几个维度,任何格式之间切换都不会出错。

4. 直接开跑:YOLO训练配置与关键参数解读

前面把数据验证和格式逻辑都过完了,现在可以真正开训了。用YOLO官方Python库训练时,需要准备一个data.yaml、一个合理的目录组织方式,以及几个关键超参数的选择。

4.1 目录落地与data.yaml:类别顺序错一个字符就全乱

YOLO训练库默认的目录约定是images与labels同级,同名的jpg和txt一一对应。你拿到手的双格式数据集里,YOLO目录本身就应该符合这个约定,直接确认train和val的划分是否合理就行。假设数据放在/data/deepPCB_yolo,配一个data.yaml:

path: /data/deepPCB_yolo train: images/train val: images/val nc: 6 names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short 4: spur 5: copper

这里最需要注意的就是names的顺序必须和txt标注里的类别id完全一致。很多转换版本喜欢把类别重新编号,如果你习惯按字母序理解类别,很容易把short和spur搞混。启动训练的第一时间,训练库会扫描标注文件,并打印每个类别的图片数和标签数,留意那几行日志,如果发现某个类别数量为0或者和预期相差巨大,说明data.yaml的names顺序有问题,赶紧停下来改。

4.2 训练命令与超参数:为什么这么设

训练命令可以这样写:

pip install ultralytics yolo train data=/data/deepPCB_yolo/data.yaml \ model=yolo11n.pt \ epochs=120 \ batch=16 \ imgsz=640 \ device=0 \ workers=8

几个参数的逻辑说一下。imgsz设成640,因为数据集原生分辨率就是640×640,不需要额外缩放,避免了小缺陷在降采样之后直接消失。model用预训练权重而不是从零开始,是因为检测骨干网络在COCO这种大规模数据上学到的纹理和边缘特征,对小目标识别帮助很大,尤其在你只有1500张图的情况下,迁移学习几乎是必须的。batch大小主要看显存,16是一个常见起点,如果显存不够可以降到8,影响不会太大。epochs我习惯给到120并开启早停,训练库默认会在验证指标连续多轮不涨时自动停下来,不用一直空跑。

用Python方式也是一样的:

from ultralytics import YOLO model = YOLO("yolo11n.pt") model.train(data="/data/deepPCB_yolo/data.yaml", epochs=120, batch=16, imgsz=640, workers=8, patience=20)

4.3 训练日志和指标怎么读:别只盯mAP50

训练结束后,训练库会在runs/detect/xxx目录下输出一堆结果文件。我一般按这个顺序看:先看results.png里的loss曲线,如果box_loss和cls_loss稳步下降,验证loss没有在后期大幅反弹,说明训练过程健康;然后看confusion_matrix.png混淆矩阵,重点观察"短路被判成余铜""开路被判成鼠咬"这类容易互相混淆的类别对;接着看PR曲线里每个类别的AP值,尤其是少数类的AP是不是明显偏低。

mAP50和mAP50-95要一起看。mAP50只要求预测框和真实框的IOU超过0.5就算命中,对小缺陷来说标准比较宽松;mAP50-95对框的位置精度要求高得多,更能反映模型在实际使用中框得准不准。deepPCB这类数据上,训练得当的轻量模型mAP50可以到0.9上下,mAP50-95通常在0.55到0.7之间,这个区间供你对照参考。指标提不上去的时候,先去翻失败样本图,而不是急着换更大的模型,这个习惯能省很多时间。

5. 实测避坑清单:这6类缺陷为什么容易互相混淆

用这套数据训练,我相信第一步大家都会遇到类似的问题:明明训练正常,val的mAP也不低,但推理到真实PCB图片上,缺陷就是会漏。下面四类问题是我自己实测遇到的,按发生频率从高到低排列。

5.1 小目标怎么数:框面积比例与漏检的直接关系

deepPCB里缺陷框的尺寸普遍很小。拿短路举例,两条线之间的搭接往往只有几十个像素见方;开路的断缝更极端,可能只有几个像素宽。假如一张640×640的图里有一个20×20的缺陷框,它占整张图的面积只有0.1%左右。模型在多次降采样的特征图上,这个小目标可能只剩几个像素,检测分支根本看不见它。

针对这个问题的实操手段有三个:一是保持imgsz不小于640,不要为了加速把训练尺寸降到416;二是打开mosaic增强,让模型在拼接图里接触大量不同尺度的小目标;三是如果框架支持多层检测头,优先选带浅层高分辨率特征图的配置,小目标召回会明显提升。这些方向上,nano级别的轻量模型就已经能跑出不错的效果,不一定要上大模型。

5.2 类别不均衡的实操对策:上采样和离线增强

六类缺陷在数据里并不是均匀分布的。开路和短路通常数量多,毛刺、鼠咬这类相对少一些。类别不均衡会导致模型对少数类的学习不充分,表现为毛刺、鼠咬的AP明显低于开路、短路。

处理办法我试下来最有效的有两种。一种是离线复制粘贴增强:把鼠咬、毛刺这类小目标从原图里抠出来,按规则贴到空白区域或正常板图上,同时补齐标注,把少数类的样本量补上去;另一种是训练时做多尺度随机裁剪采样,让模型每次迭代看到不同数量的框,避免少数类样本被大批量重复。直接改损失函数权重在部分YOLO版本里支持得不太好,框架不支持的话建议优先走离线增强路线。

5.3 鼠咬与开路、毛刺与余铜:边界模糊类别的取舍

这四类缺陷之间的边界本来就不清晰。鼠咬是线路边缘的凹坑,凹坑深到一定程度线路就断了,从检测角度看就是一个开路;毛刺是线路边缘多出来的铜刺,铜刺脱落或残留成大块,就成了余铜。标注人员对同一张图的理解不同,框出来的类别就可能不同,这种标注噪声会直接压低AP的上限。

如果你最终的目标是产线缺陷拦截,而不是发论文刷指标,我建议把业务上可以合并的类别合并掉,比如把鼠咬和开路统一成"线路异常",把毛刺和余铜统一成"多余铜"。类别少了,边界清晰了,模型判断会更稳定,业务上更容易解释。反过来,如果你需要细粒度分类来做返修工艺指导,那就要接受一部分混淆误判,并用人工复核兜底。

5.4 空图与背景图:防误检的另一半功课

deepPCB这套数据几乎每张图都有缺陷框,训练出来的模型在"无缺陷的干净板子"上反而容易瞎报。在真实产线上,绝大多数板子是良品,模型误检率哪怕只有百分之几,放大到每天几万块板子都是灾难。

我后来在自己项目里做了一个改动:从产线采集大量无缺陷板图,作为负样本混进训练集,并把它们处理成空标注。YOLO框架对没有标注文件的图片也可以训练,会把它当作纯背景来抑制误检。如果你手头有自己产线的正常板图,强烈建议在deepPCB数据之外补一批负样本,这个操作对降低误检的作用,比换任何更大的模型都直接。

6. 从mAP到漏检率:产线部署前还要跨过的三道坎

拿到转换好的数据集,也训出了不错的模型,这离产线真正能用还有一段距离。这一段主要说推理时的参数、模型的导出,以及实验室指标和产线指标之间的真实差距。

6.1 增强配方:旋转必须跟着标注转,颜色扰动模拟不同光源

训练阶段的增强,直接影响模型的泛化能力。deepPCB的图像是在相对统一的光源下拍的,而产线不同机台、不同批次的板子,光源色温和角度都会有差异。我建议用HSV颜色增强来模拟这种差异:饱和度、明度各做20%左右的随机扰动,色调也做小幅扰动,让模型学会无视颜色波动。

旋转增强要特别小心。PCB缺陷在裸板上的形态和方向有一定随机性,90度、180度、270度的旋转是安全的,因为这相当于换了一个拍摄朝向;但任意的15度、30度旋转会让框变成倾斜,训练框架只能按外接正框处理,反而引入噪声。凡是做旋转增强,一定要确认标注框同步旋转,否则模型学到的就是错位框。

6.2 推理设置与模型导出:速度与精度的平衡

训练完的模型通常要导出成ONNX之类的中性格式,再转换到推理引擎部署。导出时注意几个点:输入分辨率固定成640,避免动态尺寸在边缘设备上带来性能波动;推理的置信度阈值我一般从0.25起步,在产线场景里挑一个"误检率可接受、漏检率最低"的平衡点;NMS的IoU阈值保持0.45左右,不必为了框更多而调高,PCB缺陷本身尺寸小,框多了反而难判。

如果对速度有要求,可以试试半精度或整型量化。轻量模型在量化和不量化之间的精度差距通常不大,但速度能提升好几倍。我实测中跑640分辨率,量化后的单张推理时间能压到几毫秒,配合多机台并行完全够用。

6.3 实验室指标和产线指标:mAP不是业务指标

最后聊一个容易被忽略的问题。实验室里看mAP,产线上看的是每千块板的漏检数和误判数。mAP到了0.9看起来很高,但如果缺陷平均一块板上只有0.1个,千块板里漏掉几十块都是完全可能的。业务方给你的验收指标往往不是mAP,而是"指定误检率下的漏检率上限"。

所以我的建议是:模型训练完之后,不要急着看指标收工,而是从验收集里专门挑一批难样本——不同批次板材、不同光源条件、不同缺陷形态的图——做一次人工复核。把模型判断错误的图按"漏检"和"误检"两类分别存放,每隔一段时间回灌到训练集做增量微调。这个过程是循环的,数据集、模型、业务指标三者形成闭环之后,才能真正谈"上线"。

最后分享一个自己的习惯。每次拿到任何标注数据,不管是公开的还是自己标定的,我都先跑一遍统计脚本和可视化脚本,把类别分布、框的位置、坐标合法性都看一眼再进训练流程。这个习惯帮我避开了好几次"训了一整天才发现标注坐标系错了"的惨剧。deepPCB这套双格式数据集本身质量不错,但再好的数据,在进入你的训练流程之前都值得花半小时验证一遍。格式转换的脚本我也会留着,以后采集了自己的PCB数据,同样走一遍转换、核验、训练的流程,这套流水线确实越用越顺手。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进