☰
PCB元件检测数据集实战:VOC/COCO/YOLO标签转换与YOLOv8训练指南
2026/9/27 1:06:02 网站建设 项目流程

简介:面向目标检测入门与进阶用户的PCB电路板元件数据集,采用真实场景高质量图片构建,场景覆盖多种板面布局与元件形态,使用LabelImg标注且框体质量较高,可服务于PCB缺陷检测、元件识别与高校课程实训。压缩包共2000个文件,约125.03MB;其中1000个xml文件对应VOC格式,990个txt文件适配YOLO系列训练,同时提供coco格式标注文件及配套yaml配置文件,三种标签分目录存放,无需额外转换即可对接YOLOv5、YOLOv8等常用检测框架。附赠Linux与Windows双平台的YOLO环境搭建与训练教程html文档,以及3个Python脚本,可实现训练集、验证集、测试集划分或生成ImageSets下的txt索引,便于按项目需求灵活组织与迭代数据。目前已有1400余人学习下载,适合课程作业、毕业设计或PCB元件检测项目起步参考,可显著省去标注整理与格式转换的重复劳动。

1. 一千张PCB元件图,为什么很多人卡在第一步而不是训练

一份YOLO目标检测PCB电路板元件数据集,含1000张图片,还把VOC、COCO、YOLO三种格式的标签都准备好了,听起来是把数据丢进训练脚本就能跑。但真正做过PCB元件检测的人都知道,第一步往往卡在标签格式统一上:VOC的XML、COCO的JSON、YOLO的TXT,三套坐标体系和类别索引规则完全不一样,直接混用会导致模型连损失函数都收敛不了。这篇文章按这套数据集的常见结构,把标签转换、数据集划分、YOLOv8训练和反复出现的翻车点拆开讲清楚。适合手里有一批PCB图片、想按VOC/COCO/YOLO标准整理数据,或者准备跑YOLOv8训练自己的电路板元件检测模型的人。

2. 从VOC到COCO再到YOLO:三套标签体系怎么对齐不踩雷

拿到这类数据集,第一件事不是急着训练,而是把三套标签格式的差异摸清。很多新手犯的错是以为VOC和COCO只是文件后缀不同,实际上它们的坐标基准、类别索引、文件组织方式完全不同。这一章先讲清差异,再给一份可复用的转换脚本,最后说三个我自己踩过的坑。

2.1 三种格式到底差在哪:XML、JSON、TXT各自为政

VOC格式沿用了Pascal VOC比赛的标准,一个图片对应一个XML文件,放在Annotations目录下。XML里用<object><name>记录类别名,用<bndbox>记录目标框,坐标是xmin, ymin, xmax, ymax这种左上右下绝对像素值。它的好处是能看懂,坏处是训练时没人直接用——大多数框架都要求把类别名映射成数字ID。

COCO格式把所有标注塞进一个JSON文件,内部按images、annotations、categories三个数组组织。关键点是:category_id从1开始计数,目标框是[x, y, width, height],同样使用绝对像素值。很多人在这一步翻车,就是因为把COCO的类别ID直接当YOLO的类别序号用。

YOLO格式是一张图对应一个TXT文件,每一行表示一个目标,格式是class_id cx cy w h,其中cx, cy是中心点坐标,w, h是宽高,全部除以图片宽高做了归一化,取值在0到1之间。这里class_id从0开始计数。也就是说,同一个“电阻”类别,在COCO里可能是category_id=1,在YOLO里就是class_id=0。

维度VOC XMLCOCO JSONYOLO TXT
坐标基准绝对像素绝对像素归一化到0~1
框的表示xmin, ymin, xmax, ymaxx, y, width, heightcx, cy, width, height
类别表示字符串名称从1开始的数字ID从0开始的数字序号
文件组织每图一个XML全部标注在一个JSON每图一个TXT
是否直接训练不直接部分框架可用YOLO系列直接读

这三套格式本质上是同一批标注的不同投影,转换的核心工作就是把“坐标表示”和“类别索引”换算清楚。换算错了,训练出的模型mAP会一直趴在0附近,看起来像是模型问题,其实是数据问题。

2.2 转换脚本:从XML一步拿到COCO和YOLO标签

常见做法是写一个Python脚本,先解析VOC的XML,再分别输出COCO的JSON文件和YOLO的TXT文件。以下脚本可以直接放到数据集根目录运行,只需要按实际情况改CLASS_NAMES列表。

import xml.etree.ElementTree as ET import json import os from glob import glob # 类别清单,顺序一旦确定就不要更改 CLASS_NAMES = ["resistor", "capacitor", "diode", "transistor", "inductor", "connector"] def parse_voc(xml_path): """解析单个VOC XML文件,返回文件名、图片尺寸和目标框列表""" tree = ET.parse(xml_path) root = tree.getroot() filename = root.find("filename").text size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) objects = [] for obj in root.findall("object"): name = obj.find("name").text bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) objects.append({"name": name, "bbox": [xmin, ymin, xmax, ymax]}) return filename, img_w, img_h, objects def voc_to_coco(xml_dir, json_path, img_dir): """把整个VOC标注目录转成COCO格式的JSON文件""" images, annotations, categories = [], [], [] cat_id_map = {name: i + 1 for i, name in enumerate(CLASS_NAMES)} categories = [{"id": i + 1, "name": name} for i, name in enumerate(CLASS_NAMES)] ann_id = 1 for img_id, xml_path in enumerate(sorted(glob(os.path.join(xml_dir, "*.xml")))): filename, w, h, objs = parse_voc(xml_path) images.append({ "id": img_id + 1, "file_name": filename, "width": w, "height": h }) for obj in objs: xmin, ymin, xmax, ymax = obj["bbox"] bw = xmax - xmin bh = ymax - ymin annotations.append({ "id": ann_id, "image_id": img_id + 1, "category_id": cat_id_map[obj["name"]], "bbox": [xmin, ymin, bw, bh], "area": bw * bh, "iscrowd": 0 }) ann_id += 1 with open(json_path, "w") as f: json.dump({"images": images, "annotations": annotations, "categories": categories}, f) def voc_to_yolo(xml_dir, yolo_dir, class_names): """把每个VOC XML转成对应的YOLO TXT标签""" os.makedirs(yolo_dir, exist_ok=True) for xml_path in glob(os.path.join(xml_dir, "*.xml")): filename, w, h, objs = parse_voc(xml_path) txt_path = os.path.join(yolo_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") lines = [] for obj in objs: cls_id = class_names.index(obj["name"]) xmin, ymin, xmax, ymax = obj["bbox"] cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines)) # 按需调用 voc_to_coco("Annotations", "annotations/instances_train.json", "JPEGImages") voc_to_yolo("Annotations", "labels", CLASS_NAMES)

这段代码里,parse_voc负责从XML里把文件名、宽高、目标框全部读出来,这样后续转两种格式时不用重复解析。voc_to_coco里的cat_id_map把类别名映射成从1开始的ID,这是COCO官方评估工具pycocotools的硬性要求,ID从0开始会导致评估时报错或者mAP算出来是0。

voc_to_yolo里有一个容易忽略的细节:TXT文件名用的是XML文件的basename,而不是XML里<filename>标签的内容。这样写成TXT才能和图片文件名严格对应,因为后面划分数据集、训练读图时都是按文件名配对找标签的。

2.3 转换脚本里最容易抄错的三个参数

第一个坑是类别列表的顺序。CLASS_NAMES这个列表的顺序就是YOLO里class_id的隐式定义,一旦训练中途发现某个类别序号对不上,全部TXT都要重新生成。更稳妥的做法是单独维护一份classes.txt,训练时也用这份文件生成names配置,保证训练和验证用的是同一套顺序。

第二个坑是宽高方向和整除。VOC的xmin, xmax对应水平方向,ymin, ymax对应垂直方向,转成YOLO时cx用xmin + xmax除以2,cy用ymin + ymax除以2,width用xmax - xmin除以图片宽,height用ymax - ymin除以图片高。方向写反了坐标不会报错,但mAP会低得离谱。

第三个坑是COCO的area字段。虽然YOLO训练时用不到area,但COCO的评估脚本会依赖它计算小目标、中目标、大目标的AP。area要用width * height算出来,不能随手填0。如果这个数据集以后要和其他COCO格式的数据合并,或者要用pycocotools做标准评估,area缺失会直接导致评估失败。我一般会在转换完随机抽5张图做一次可视化,把框画回原图确认位置没错,再进入下一步划分。

3. 划分脚本怎么分才不出幺蛾子:分层抽样与划分后校验

数据集划分看起来是最没技术含量的一步,很多人直接random.shuffle一把梭。对1000张图这种规模,随机划分很可能把某个类别的所有样本都分进训练集,验证集里这个类别一个都看不到,训练结果自然没法看。这一章讲为什么要分层、脚本怎么写、划分完怎么校验。

3.1 为什么随手shuffle不值得信:小数据集的类别分布陷阱

PCB元件数据集的类别分布通常不是均匀的。以常见板卡为例,电阻和电容可能各占几百个目标,而变压器、晶振这种大元件可能只有几十个目标。如果按图片随机划分,那些样本量少的类别被分到验证集的概率无法保证,甚至可能整类消失。

更隐蔽的问题是“单图多类别”带来的相关性。一张PCB图上往往同时有电容、电阻、连接器,如果直接用图片ID做随机划分,训练集和验证集可能包含来自同一块板子的高度相似图片,验证指标会虚高。分层抽样做不到完全消除这种相关性,但至少能保证每个类别在三个集合里都有样本。

做法是把每张图的“类别存在向量”作为分层标签,比如某张图同时含电阻和电容,它的向量就是[1, 1, 0, 0, 0, 0]。StratifiedShuffleSplit按这个向量做分层,每层内部再随机抽,这样既保证了类别覆盖,又保留了随机性。

3.2 分层划分脚本:一次拆出train、val、test三个集合

下面这段脚本按照“类别存在向量”做两层划分:第一层把全部数据拆成80%的训练集和20%的临时集,第二层把临时集对半拆成验证集和测试集,最终得到800张训练、100张验证、100张测试。比例可以根据自己的数据量调整。

import os import numpy as np from glob import glob from sklearn.model_selection import StratifiedShuffleSplit IMG_DIR = "images" LABEL_DIR = "labels" NUM_CLASSES = 6 X = np.array(sorted(glob(os.path.join(IMG_DIR, "*.jpg")))) def label_vector(txt_path): """读YOLO格式TXT,统计这张图包含哪些类别""" vec = np.zeros(NUM_CLASSES) if not os.path.exists(txt_path): return vec with open(txt_path) as f: for line in f: parts = line.strip().split() if parts: vec[int(parts[0])] = 1 return vec y = np.array([label_vector(p.replace(IMG_DIR, LABEL_DIR).replace(".jpg", ".txt")) for p in X]) # 第一层:80%训练 / 20%临时 sss1 = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, temp_idx = next(sss1.split(X, y)) # 第二层:临时集对半拆成验证和测试 temp_X, temp_y = X[temp_idx], y[temp_idx] sss2 = StratifiedShuffleSplit(n_splits=1, test_size=0.5, random_state=42) val_idx, test_idx = next(sss2.split(temp_X, temp_y)) val_imgs = temp_X[val_idx] test_imgs = temp_X[test_idx] train_imgs = X[train_idx] def write_list(imgs, list_path): """把图片路径写入txt文件,训练时可直接读取""" with open(list_path, "w") as f: for p in imgs: f.write(p + "\n") os.makedirs("splits", exist_ok=True) write_list(train_imgs, "splits/train.txt") write_list(val_imgs, "splits/val.txt") write_list(test_imgs, "splits/test.txt")

这段脚本有两个关键参数。test_size=0.2是第一层划分的临时集比例,random_state=42决定了随机种子,固定成同一个值才能保证每次运行得到完全相同的划分结果。很多人训练复现不了,就是因为这次用42下次用7。

label_vector函数读取YOLO格式的TXT,每行第一个数字是类别ID,这个函数只关心“有没有”,不关心“有几个”,因为分层抽样按图片粒度做。如果某个类别的目标总数特别少,比如整个数据集只有30个变压器,分层按“类别存在向量”依然可能遗漏,这时候可以把稀有类别单独抽出来,先保证测试集里包含它们,再把剩余数据按比例划分。

3.3 划分完先过一遍校验:图片、标签、路径一个都不能少

划分完不要急着训练,先跑一遍完整性校验。常见的情况有三种:训练集里某张图没有对应的TXT标签;TXT里写了越界的坐标值;图片路径里的目录名和实际目录对不上。这些错误在训练阶段会以“image not found”或者loss异常的形式暴露出来,但定位起来费时,不如在划分后直接拦截。

def check_split(img_list, label_list): missing_img = [p for p in img_list if not os.path.exists(p)] missing_lbl = [p for p in label_list if not os.path.exists(p)] bad_lines = [] for lbl in label_list: for line in open(lbl): parts = line.strip().split() if len(parts) != 5: bad_lines.append((lbl, "字段数不对: " + line.strip())) continue for v in parts[1:]: if not (0.0 <= float(v) <= 1.0): bad_lines.append((lbl, "坐标越界: " + line.strip())) break return missing_img, missing_lbl, bad_lines train_imgs = [line.strip() for line in open("splits/train.txt")] train_lbls = [p.replace(IMG_DIR, LABEL_DIR).replace(".jpg", ".txt") for p in train_imgs] missing_img, missing_lbl, bad_lines = check_split(train_imgs, train_lbls)
校验项通过标准失败时的常见原因
图片文件存在每个路径都能找到jpgimages目录路径写错、文件名大小写不一致
标签文件存在每个jpg都有同名txt转换脚本漏跑、目录名不一致
坐标范围cx, cy, w, h都在0到1之间VOC转YOLO时未除以图片宽高
字段数量每行恰好5个字段空行、多余空格、类别ID超范围

这段校验脚本跑完,如果有问题,先回到转换环节修,不要带着脏数据进训练。我在实际项目里吃过这个亏:验证集只有100张图,里面有2张缺标签,训练时loss曲线看起来完全正常,但验证mAP忽高忽低,最后花了半天才发现是划分阶段丢了一个标签文件。

4. 用YOLOv8把PCB元件检测跑通:数据YAML、训练命令与产物解读

标签转好了,数据也划分完了,接下来就是把YOLOv8训练跑起来。这一章不讲算法原理,只讲怎么把数据集喂给YOLOv8、超参怎么定、训练完看哪些文件。训练这事有一定玄学成分,但大部分问题都能从产物里找到线索。

4.1 目录结构怎么摆:images和labels必须严格同名

YOLO系列对数据集目录结构要求很明确:图片和对应的TXT标签必须同名,只是后缀不同。常见做法是建立images/train、images/val、images/test和labels/train、labels/val、labels/test六个目录。训练时YOLO会根据图片路径自动去labels目录找同名TXT。

用一段bash脚本把划分好的图片和标签移动到对应目录,注意图片和标签要同步移动:

mkdir -p images/train images/val images/test mkdir -p labels/train labels/val labels/test while read p; do mv "$p" images/train/ mv "${p%.jpg}.txt" labels/train/ done < splits/train.txt while read p; do mv "$p" images/val/ mv "${p%.jpg}.txt" labels/val/ done < splits/val.txt while read p; do mv "$p" images/test/ mv "${p%.jpg}.txt" labels/test/ done < splits/test.txt

移动完成后,YOLO要求的数据集配置文件pcb.yaml如下:

path: ./pcb_dataset train: images/train val: images/val test: images/test nc: 6 names: 0: resistor 1: capacitor 2: diode 3: transistor 4: inductor 5: connector

path是数据集根目录,train、val、test是相对path的目录路径。nc是类别数量,names里的类别顺序必须和第2章转换脚本里的CLASS_NAMES做成同一份。我见过有人训练时用一份names,验证时又换了顺序,结果mAP一直在0.5左右上不去,就是这个原因。

4.2 训练命令与关键超参:首次跑通用这些值

训练命令用ultralytics的YOLOv8:

yolo detect train \ data=pcb.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ patience=30 \ workers=4 \ seed=42

第一次把这个数据集跑通,我建议直接用yolov8n.pt预训练模型,这个权重文件在第一次运行时自动下载,不需要自己单独找。nano版本模型参数量小,1000张图的规模下不容易过拟合,显存占用也低。如果显存有余量,再试yolov8s.pt做对比。

几个超参的选择依据:epochs=120是PCB元件检测这类任务比较稳的轮数,数据集不复杂时一般到60到80轮就收敛了,120轮是给mosaic增强和低学习率阶段留余量。imgsz=640是速度和精度的平衡点,如果板卡图里的贴片电阻特别小,可以改成960,但显存占用会明显上涨。batch=16在8GB显存上比较稳妥,16GB显存可以上到32。patience=30的意思是验证集指标连续30轮不提升就提前停止,这个值不要调太小,否则模型还没收敛就被叫停。

4.3 训练完先看哪几个文件:不要在TensorBoard里泡太久

训练结束后,产物都在runs/detect/train目录下。先看weights/best.pt和weights/last.pt这两个权重文件,best.pt是按验证集指标选出的最优权重,后续预测和部署都用它;last.pt是最后一轮的权重,一般不直接使用。

再看results.csv,它记录了每一轮的loss、精度和mAP曲线,用Excel或pandas打开都能看。重点观察验证集loss,如果训练loss一直在降、验证loss在某个点开始反弹,就是过拟合信号,此时应该减少epochs或加强数据增强。confusion_matrix.png反映每个类别的误检情况,对角线越亮越好,某个非对角线格子特别亮说明两个类别持续混淆。最后看val_batch_pred.jpg这类带预测框的样例图,直观感受预测框是否贴合元件边界。

我习惯把训练命令里project和name参数固定下来,比如project=runs/pcb、name=exp1,这样多次实验的结果不会混在一起。

5. 训练PCB检测模型的避坑清单:五个反复出现的翻车现场

这一章全部来自实际跑PCB数据集时反复出现的坑,每条都按“现象 → 原因 → 解决”写,建议收藏起来对照排查。

5.1 训练启动就报“image not found”,中断了

现象:训练命令跑几秒后报错,提示某张图片找不到,训练直接中断。

原因:最常见的有两种。一种是VOC XML里<filename>字段和实际图片文件名不一致,比如XML里写的是IMG_0001.JPG,磁盘上实际是img_0001.jpg;另一种是划分脚本生成图片路径时用了绝对路径,移动文件后路径失效。

解决:在转换脚本里增加一步文件名清洗,统一改成小写jpg后缀,并以清洗后的文件名为准重新生成TXT标签。划分后先执行第3章的校验脚本,确认所有图片路径都能打开再进训练。

5.2 训练能跑,loss也下降,mAP一直趴在0附近

现象:训练过程没有任何报错,loss曲线正常下降,但每个类别的mAP都是0,或者徘徊在0.01附近。

原因:绝大多数情况是类别索引错位。COCO格式要求category_id从1开始,YOLO要求class_id从0开始。转换脚本里如果直接沿用了COCO的ID,YOLO的TXT里类别ID就会整体偏移,模型学到的根本不是真实的类别标签。

解决:检查YOLO的TXT文件,看第一列数字是否在0到nc-1范围内。随机挑一个类别多的文件,把每个类别的第一列数字打印出来人工核对一遍。更根治的办法是转换脚本统一维护一份类别名到ID的映射表,COCO和YOLO都从这份表生成。

5.3 贴片电阻、小电容这类小目标几乎检测不到

现象:训练完成后,大元件如连接器、电解电容检测效果不错,贴片电阻、贴片电容这种小目标漏检严重,mAP一塌糊涂。

原因:PCB元件检测里很多目标在640分辨率下只有十几个像素,YOLO默认的特征图下采样倍数对小目标不够友好,目标信息在多次下采样后丢失了。这是小目标检测的经典问题,不是模型越改越差。

解决:第一步把imgsz从640提高到960,小目标在输入里占的像素比例变大,效果通常立竿见影。第二步检查数据增强,确保mosaic是开启的,它能把不同图的小目标拼到同一张图里,增加小目标样本数量。第三步如果还不够,就把测试图先切成四块分别推理再拼接,这种tiling方案对小目标密集场景是通用做法。

5.4 类别严重不平衡,损失函数被某几个类主导

现象:训练时loss下降得很快,但查看每个类别的PR曲线发现,样本少的类别AP普遍偏低,样本多的类别AP很高,整体mAP不够理想。

原因:PCB数据集里电阻、电容样本可能各占几千个目标,变压器类可能只有几十个目标。YOLOv8默认的损失函数对每个目标平等对待,多数类在梯度更新中占主导,少样本类很难学好。

解决:少样本类做针对性增强,包括随机旋转、亮度扰动、复制粘贴到其他板上。复制粘贴要注意别把元件贴到焊盘以外的位置。另一个办法是收集更多该类别样本,1000张图规模下,代价最小的还是从数据集切分阶段就把稀有类别的验证集样本单独保证,至少让评价指标能真实反映该类别的水平。

5.5 验证集loss在降,mAP却不涨甚至波动

现象:训练loss和验证loss都正常下降,但每隔几轮mAP出现大幅波动,整体没有稳步上升。

原因:常见的原因是训练和验证使用了不一致的类别顺序。比如训练时names用了一套顺序,验证时加载的数据集配置里names是另一套顺序,模型输出的类别ID和验证集标注的类别ID对不上。另一个原因是验证集太小,100张验证图在随机增强影响下指标波动大。

解决:训练和评估永远只用同一个pcb.yaml文件,不要手动改names。验证集如果只有100张图,波动大是正常的,可以把imgsz固定,关闭验证阶段的增强,减少随机因素。更彻底的办法是扩大验证集到200张,代价是训练集减少100张,这个取舍要看总体数据量。

6. 用val模式做一次毕业验收:混淆矩阵、漏检回看与置信度阈值

最后一次训练结束后,不要急着部署,先用验证集做一次完整的“毕业验收”。训练流程里验证集已经在发挥作用,但一次性指标不够,要把预测结果拉出来人工看一遍。

yolo detect val \ data=pcb.yaml \ model=runs/detect/train/weights/best.pt \ imgsz=640 \ conf=0.25 \ save_json=True

跑完后重点看两个东西。一是confusion_matrix_normalized.png,看哪些类别互相混淆。PCB场景里,贴片电阻和贴片电容外观接近,混淆常见,如果真的混得厉害,要考虑是否标注边界本身就不清晰。二是验证集里每张图的预测框,确认漏检和误检的具体形态。

预测图回看用predict模式:

yolo predict \ model=runs/detect/train/weights/best.pt \ source=images/test \ conf=0.30 \ save_txt=True \ save_conf=True \ project=runs/pcb_verify

把每个测试图的预测结果画到原图上,重点看三类:未检出的元件是否被周围密集元件遮挡、小目标是否被过滤、以及是否存在把一个元件框成两个的重复检测。阈值是最后一道调优手段,PCB元件检测我一般会把conf调到0.25到0.45之间跑三遍对比。工业场景我倾向于保守,宁可多几个低置信度候选框给人工复核,也别漏掉元件的真实位置。

我的习惯是每次训练完固定存一份best.pt加一份当时的pcb.yaml,权重和配置放同一个目录,这样几个月后再回看,还能精确还原当时的实验条件。这个习惯帮我解决过不少“明明当时指标很好现在复现不出来”的问题。数据格式对齐和划分校验是这类数据集项目里最不值得省时间的环节,前期花半天把这两步做扎实,后面训练省下的时间远不止半天。希望这篇笔记能帮你少踩几个坑。

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

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

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

立即咨询