☰
揭秘PCB缺陷检测数据集99.8%准确率:yolov9实战要点
2026/10/18 11:36:54 网站建设 项目流程

简介:面向印制电路板缺陷检测的YOLOv9格式数据集,专为工业质检与深度学习目标检测场景设计,可帮助算法工程师、科研人员快速搭建并训练缺陷识别模型。资源包共含2000个文件,其中1297个txt标注文件与702张jpg图像一一对应,每个txt记录缺陷类别及归一化坐标,可直接用于YOLOv9训练;702张jpg图片覆盖多种板卡与缺陷形态,环境变量丰富;另有1个yaml配置文件预设数据集路径与类别列表,压缩包整体大小为120.79MB。数据标注质量高,模型识别准确率可达99.8%,不仅适合模型调参实验,也能用于产线检测方案的快速原型验证。已有436人学习使用,对从事印制电路板质检、工业视觉的工程师与研究人员来说,省去了自行采集与标注的繁琐流程,是可直接上手的训练数据集。无论是入门目标检测还是优化生产环境中的缺陷识别精度,都能从中获得有效支撑,数据划分合理,训练验证即开即用。

1. 1297张图加99.8%准确率,这套PCB缺陷检测数据集的水分在哪里

1297张图片,yolov9格式标注,对外宣称99.8%的识别准确率,做成PCB电路板缺陷检测识别数据集——这个描述放到真正做工业视觉的人面前,第一反应不是兴奋,而是先问三个问题:99.8%是哪个口径算出来的?那1297张图里缺陷类别是怎么分布的?标注坐标有没有抽检过?因为PCB缺陷检测的难点从来不是“分得清大类”,而是小目标、类间相似、标注噪声三件事叠在一起时模型还能不能稳住。这套数据集的真实价值,在于能让你快速跑通yolov9从数据准备到权重导出的完整链路,同时用来验证自己对检测指标的理解是否到位。如果目标是直接部署到产线,1297张只能算起点,不是终点。

2. PCB缺陷检测数据集里有什么:从缺陷类型到yolov9标签文件

2.1 先认清楚PCB裸板缺陷的六种常见类型

做PCB裸板缺陷检测,最常遇到的缺陷类型就那么几类。不同数据集命名略有差异,但核心类别基本一致:

类别现象描述对检测模型的实际挑战
missing_hole(漏孔)该有的钻孔缺失目标极小,可能只有几像素到几十像素
mouse_bite(鼠咬)铜箔边缘出现凹陷与正常走线的局部形状接近,类间相似度高
open_circuit(断路)导体线路断裂断裂点很小,容易淹没在噪声里
short(短路)相邻导线之间搭连需要结合上下文判断,单看局部容易漏
spur(毛刺)铜箔边缘多余凸起和正常走线拐角很难区分
copper_spurious(多余铜皮)不该有铜的位置残留了铜对比度低,光照一变就掉点

拿到这份数据集后,第一步不是急着训练,而是先统计每个类别各有多少个框。假设1297张图平均每张带两到三个标注框,实例总数大概在2600到3900之间。如果某个类别只有几十个实例,那训练时这一类的AP基本看运气,99.8%这个数字也大概率不包含这种少数类。

2.2 yolov9标注文件究竟长什么样

yolov9用的是YOLO系列通用的标注格式:每张图片对应一个同名txt文件,每一行描述一个目标框,五个字段依次是类别id、中心点x坐标、中心点y坐标、框宽、框高。注意,这里四个坐标值全部是相对图片宽高的归一化值,取值范围是0到1。

0 0.528645 0.412344 0.012358 0.018745 2 0.731245 0.211489 0.031247 0.024456

第一行的含义是:类别id为0的缺陷(比如missing_hole),框中心点位于图片宽度的52.86%处、高度的41.23%处,框宽占图片宽度的1.24%,框高占图片高度的1.87%。这类小框在PCB数据里非常常见,也正是为什么后面训练时输入分辨率不能随便用416这类偏小的值。

我一般会建议拿到数据后先写一段检查脚本,把所有标签文件逐行读一遍,确认字段数量、坐标范围、类别id合法性都没问题。因为很多数据集的标注文件是转换工具批量生成的,转换过程里最容易出现坐标越界、字段缺失、类别id从1开始而不是从0开始这类问题。

import os label_dir = "labels/train" image_dir = "images/train" for label_file in os.listdir(label_dir): # 两张图用同一stem关联 stem = os.path.splitext(label_file)[0] image_path = os.path.join(image_dir, stem + ".jpg") if not os.path.exists(image_path): print(f"图片缺失: {image_path}") with open(os.path.join(label_dir, label_file), encoding="utf-8") as f: lines = f.readlines() for idx, line in enumerate(lines): parts = line.strip().split() # 合法yolov9标签每行必须是5个字段 if len(parts) != 5: print(f"{label_file} 第{idx + 1}行字段数异常: {line.strip()}") continue try: cls = int(parts[0]) x, y, w, h = map(float, parts[1:]) except ValueError: print(f"{label_file} 第{idx + 1}行存在非数值字段: {line.strip()}") continue # 归一化坐标超出[0,1]说明转换工具算错了 if not (0 <= x <= 1 and 0 <= y <= 1 and 0 <= w <= 1 and 0 <= h <= 1): print(f"{label_file} 第{idx + 1}行坐标越界: {line.strip()}")

这段脚本会在训练前把所有坏标签暴露出来。字段数不对通常是因为行尾多了空格或制表符;坐标越界则几乎可以断定是某个标注工具把像素坐标直接当成归一化坐标写了出来。跑完这一段没有输出,再谈后续训练。

2.3 用可视化脚本确认框是不是画在了正确位置

文件格式合法不等于标注内容正确。yolov9格式的坐标是中心点加宽高,如果转换时把左上角坐标误当成中心点坐标,字段层面完全合法,但框会整体偏到右下角。这种错误不会报错,只会让模型训练出来一团糟。

我常用的做法是把标注框画回原图,抽几十张人眼扫一遍。

import cv2 import os label_dir = "labels/train" image_dir = "images/train" output_dir = "check" os.makedirs(output_dir, exist_ok=True) for label_file in os.listdir(label_dir): stem = os.path.splitext(label_file)[0] image_path = os.path.join(image_dir, stem + ".jpg") img = cv2.imread(image_path) if img is None: print(f"无法读取图片: {image_path}") continue h, w = img.shape[:2] with open(os.path.join(label_dir, label_file), encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls, x, y, bw, bh = int(parts[0]), *map(float, parts[1:]) # 中心点宽高格式转成左上角右下角像素坐标 x1 = int((x - bw / 2) * w) y1 = int((y - bh / 2) * h) x2 = int((x + bw / 2) * w) y2 = int((y + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(os.path.join(output_dir, stem + ".jpg"), img)

这里把归一化中心点转回像素坐标时,宽高要除以2做加减,这个细节是踩坑率最高的地方。写错符号或乘错系数,轻则框偏半个身位,重则整个框贴到图片角落。可视化脚本跑出来的图,重点观察两类样本:小目标是否严格贴合缺陷边缘,以及相邻缺陷的框是否互相串位。

2.4 类别id映射表要锁死

yolov9训练时类别是靠data.yaml里的names列表映射的,顺序必须和txt里的类别id完全一致。比如某个数据集把0定义为mouse_bite,你却写成了missing_hole,训练不会报错,但验证时AP和混淆矩阵会非常难看,你会以为模型学得差,其实是标签含义错位了。

path: ./ # 数据集根目录 train: images/train val: images/val names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short 4: spur 5: copper_spurious

这里有一个隐藏问题需要注意:yolov9官方仓库和ultralytics实现的graph结构在部分版本中不完全一致,但yaml配置的写法是通用的。你只要保证names顺序和标签文件里的id一一对应,训练器本身不做校验。

3. 用yolov9格式跑通训练:目录、环境和划分脚本

3.1 先按这份结构整理目录

拿到1297张图片和对应标签后,第一件事不是急着写模型,而是先把目录整理成训练器认识的固定结构。常见做法是:

PCB_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

图片和标签分目录存放,图片文件名和标签文件名共用同一个stem。训练器在读取时,会自动把labels/train下的txt和images/train下的jpg按文件名配对,所以文件名改过就废了。整理时不要手动拖拽,写个脚本批量移动。

3.2 关键一步:按板号分组划分,别全局随机

1297张图怎么切分,直接决定了后面那个“99.8%”有没有参考价值。如果直接对全部图片做随机划分,同一块板子不同位置的切片可能同时出现在训练集和验证集里,验证时模型实际上见过同一块板的纹理特征,mAP会被严重虚高。正确的做法是先按文件名里的板号归组,再按组划分。

import random import shutil import os from collections import defaultdict random.seed(42) root = "PCB_dataset" # 假设文件名是 board01_p01.jpg 这样的命名 all_images = [f for f in os.listdir("images_all") if f.endswith((".jpg", ".png"))] groups = defaultdict(list) for img in all_images: board = img.split("_")[0] # 按板号分组 groups[board].append(img) board_names = list(groups.keys()) random.shuffle(board_names) split = int(len(board_names) * 0.8) train_boards = board_names[:split] val_boards = board_names[split:] for board in train_boards: for img in groups[board]: stem = os.path.splitext(img)[0] shutil.copy(os.path.join("images_all", img), os.path.join("images/train", img)) shutil.copy(os.path.join("labels_all", stem + ".txt"), os.path.join("labels/train", stem + ".txt")) for board in val_boards: for img in groups[board]: stem = os.path.splitext(img)[0] shutil.copy(os.path.join("images_all", img), os.path.join("images/val", img)) shutil.copy(os.path.join("labels_all", stem + ".txt"), os.path.join("labels/val", stem + ".txt")) print(f"训练集板数: {len(train_boards)}, 验证集板数: {len(val_boards)}")

随机种子固定成42,每次划分结果可复现。如果图片命名里没有板号信息,那就退而求其次,按连续时间戳分组,至少保证验证集和训练集的时间点不重叠。这一步做不好,后面所有指标都是自嗨。

3.3 安装依赖与版本选型

训练yolov9有两种路径,一是官方仓库,二是ultralytics封装好的训练器。对于做数据集验证的场景,我一般用ultralytics,闭环体验好,数据加载、增广、日志、导出一条龙,省去自己拼代码的时间。

pip install ultralytics python -c "import ultralytics; print(ultralytics.__version__)"

如果机器上已有旧版本,先升级:

pip install -U ultralytics

这里不建议盲目追求最新版。yolov9相关的模型定义在ultralytics里属于后加入的架构,早期版本可能加载不了yolov9c.yaml,太新的版本也可能因为依赖冲突报奇怪的错误。选一个相对稳定的版本组合,跑通流程后不要再动环境。

3.4 统计类别分布,防止少数类被淹没

划分完成后,把train和val两个集合的类别实例数分别统计一遍。这一步能提前预警两类问题:某个类别在验证集里一个样本都没有,或者某个类别在整个训练集里只有不到50个框。前者会导致评估结果缺失,后者需要针对这个类别多采样或调整loss权重。

from collections import Counter counts = Counter() for split in ["train", "val"]: label_dir = f"labels/{split}" for label_file in os.listdir(label_dir): with open(os.path.join(label_dir, label_file)) as f: for line in f: parts = line.strip().split() if len(parts) == 5: counts[(split, int(parts[0]))] += 1 for split in ["train", "val"]: print(split, {k: v for (s, k), v in counts.items() if s == split})

如果某个缺陷类别在训练集里数量明显偏少,后续要在增广参数里给它单独开小灶,或者干脆在采集阶段补拍这一类样本。1297张图这种体量,每类少于100个框的类别基本注定AP不会高。

4. 训练参数与99.8%的口径:把识别准确率跑在明面上

4.1 第一次训练的最小命令

在没有额外预训练权重的情况下,直接从头训练yolov9c也可以,体量不大,收敛速度相对快。命令如下:

yolo train data=data.yaml model=yolov9c.yaml epochs=300 batch=8 imgsz=640 patience=30 project=pcb_yolov9 name=run1

想用预训练权重做迁移学习,写法稍有不同:

yolo train data=data.yaml model=yolov9c.pt epochs=150 batch=8 imgsz=640

第一行的yolov9c.yaml表示从网络结构创建模型,完全不依赖预训练权重,权重文件体积小,首次训练时更稳,不容易出现“预训练权重类别对不上导致某些层被随机初始化”的情况。第二行是从公开的yolov9c.pt继续微调,收敛更快,但要求你的数据集类别数和权重文件原本的类别数一致,否则最后一层会重建。对1297张图这个量级,两种方式各有各的用法:先从头训,验证数据本身没问题,再考虑迁移学习。

4.2 六个必调参数,照这个表改

参数建议值调参理由
epochs150-300PCB缺陷目标小,收敛慢,太早停会欠拟合
batch8-16取决于显存,太小会让batch norm统计不稳定
imgsz640起步,显存够用就1280小孔和鼠咬这类缺陷在640下只有十几个像素,分辨率不足直接漏检
patience20-30配合早停,防止过拟合后继续空跑
optimizerAdamW小数据集上SGD收敛偏慢,AdamW对学习率更鲁棒
augmentmosaic=1.0,degrees=0PCB板的方向固定,旋转增广反而制造不真实样本

640分辨率下,一张图中的漏孔缺陷可能只有5到8个像素,模型几乎只能靠上下文猜。如果显存允许,把imgsz提到1280,对小目标AP的提升立竿见影,代价是训练时间翻倍。degrees设0是很多检测公开教程里不会提但实际产线非常重要的参数,因为PCB在治具上的方向是确定的,训练时引入旋转会让模型把“倾斜的走线”当作新特征去学习,白白消耗拟合能力。

4.3 训练日志怎么看,不要只盯最后一行

训练过程中,控制台会滚动输出每一轮的loss和指标:

Epoch gpu_mem box_loss cls_loss dfl_loss Instances Size 120 4.21G 0.6426 0.3841 0.5237 127 640

这里要看的是趋势,而不是单点数值。box_loss缓慢下降是正常的;cls_loss如果持续上升,大概率是类别标签噪声太大,模型在强行记忆错误标注。更直观的是每隔几十轮把best.pt在验证集上跑一遍,看mAP50曲线,如果它先升后降,而训练loss还在降,那就是过拟合的信号,patience参数会在30轮后自动截停。

4.4 99.8%是怎么来的:三种口径差别巨大

标题里99.8%的识别准确率,听起来很美,但作为工程师,我有义务拆一下这个数字可能的来源。

第一是阈值口径。yolov9评估标准有mAP50(IoU阈值0.5)和mAP50:95(IoU从0.5到0.95逐级平均)。同一个模型,mAP50能达到99%,mAP50:95可能只有70%出头。99.8%几乎必然是在mAP50这个宽容口径下得出的,且大概率是某一个类别的AP,不是全部类别的均值。

第二是数据口径。如果验证集和训练集来自同一块板的不同区域,验证时模型其实已经见过这块板的走线风格和光照特征,相当于开卷考试。严格的做法是按板号分组划分,这块内容上一章已经给了脚本。

第三是场景口径。同一批次的PCB,油墨颜色、线宽、表面处理完全一致,模型学到的纹理特征只属于这个批次。换一批板子,光照一变,准确率掉到95%甚至90%都算正常。1297张图本身覆盖的场景有限,99.8%只能代表“在这批数据上表观性能不错”,不代表产线环境下的真实良率。

5. 避坑与排查:PCB缺陷检测训练中的五个高频翻车点

5.1 训练启动时提示0 images found

现象:yolo train命令刚跑起来,日志里出现WARNING,提示没有找到标签或图片,随后训练直接中断,甚至会自动下载COCO数据集。

原因:data.yaml里的path字段写错了。yolov9训练器用path拼接train和val的相对路径,如果path指向了错误目录,整个数据集加载就会失败。另一个常见原因是labels目录和images目录的stem不一致,比如图片叫pcb_001.jpg,标签却叫pcb_001.txt_extra。

解决:先确认data.yaml的三行路径拼接后的实际物理路径真实存在,再检查标签文件夹里是否存在与图片文件名一一对应的txt。推荐在训练前跑一遍前面2.2节里的标签检查脚本,它会顺便报告图片缺失情况。

5.2 坐标转换后框全部偏到右下角

现象:训练开始时loss正常下降,但下降到一定程度就不动,验证集上mAP一直很低。把预测结果画回图片,发现框总是偏在真实目标的右下方。

原因:转换工具把像素级左上角坐标当成了yolov9需要的归一化中心点坐标。一个宽100像素、高50像素的框,中心点坐标是(left+50, top+25),而不是(left, top)。直接用左上角坐标除以图片宽高,框就会整体偏移半个身位。

解决:转换时执行x_center = (left + width / 2) / image_width,y_center = (top + height / 2) / image_height。改完再跑一遍2.3节的可视化脚本,把框画出来人眼确认,这一步省不了。

5.3 自己写了旋转增广,精确率反而下跌

现象:代码里手工加了随机旋转90度和水平翻转,训练后精确率不但没涨,反而明显下跌,漏检变多。

原因:PCB检测场景中,板子方向在生产线上是固定的,旋转90度产生的图像在真实环境里根本不会出现,模型被迫为不存在的方位分配拟合能力。更隐蔽的问题是,手工旋转图片时必须同步旋转标注框的坐标和尺寸,很多人只转了图,标签没跟着转,等于把错误标注喂给模型。

解决:不要手工改图像和标签,用训练器内置的mosaic、flipud、fliplr这些参数化增广,它们会自动同步标签。PCB场景下最稳妥的做法是degrees=0,水平翻转按需开启,垂直翻转和旋转都关掉。

5.4 验证集里有训练集的“邻居”

现象:训练时mAP50高达98%,但拿一批全新板的图片去测试,性能立刻掉到85%以下。

原因:划分时直接用全局随机,同一块PCB不同区域的切片被同时分到了训练集和验证集。模型在验证时“见过”了同一块板的走线纹理、油墨颜色、光照条件,指标虚高。

解决:老老实实按板号分组划分。如果文件名里能提取出板号或时间戳,就按那个归组;实在没有,也要按连续编号分段,而不是打乱后平分。这个问题的危害性主要体现在“自我感觉良好”,上线前几乎必定翻车。

5.5 OpenCV读取图片后颜色通道顺序错乱

现象:copper_spurious这种靠颜色区分于背景的缺陷,训练后AP为0,而形状类的缺陷都正常。

原因:OpenCV的cv2.imread读取图片返回的是BGR三通道,而yolov9训练内部默认按RGB处理。如果预处理管道里混用了OpenCV读取和PIL读取,模型学到的颜色映射就乱套了,颜色敏感缺陷首当其冲。

解决:统一走一条读取链路。最好全部交给训练器内置的数据加载器处理,不要自己额外读取图片做预处理。如果必须自己读取,用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)显式转换后再进模型。

6. 训练完之后:用pr曲线验收,再谈部署

6.1 用validate命令扫一遍全阈值表现

训练结束后,默认会输出mAP50、mAP50:95和precision/recall曲线图。我习惯在训练完成的runs

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询