简介:面向电力巡检与目标检测研究者,提供一份输电线路螺栓销钉缺失检测数据集。资源采用Pascal VOC与YOLO两种标注格式,配套jpg图片与对应xml、txt文件,涵盖DefectInsulator、DefectPin、NormalInsulator、NormalPin四个类别,各类别标注框数分别为248、656、1070、789,总计2763个矩形框,可支撑绝缘子缺陷、销钉缺失等场景下的模型训练、测试与算法评估。
资源压缩包共2000个文件,以1999个XML标注文件为主体,另含使用说明TXT文档,整体约143MB。VOC格式便于通过labelImg二次标注及格式转换,YOLO格式则可直接接入主流目标检测框架,目录结构清晰,可按需取用。数据集标注规则统一,均为矩形框标注,类别分布明确,可用作缺陷检测方向的基准数据或训练增广素材。
目前已有1306人学习下载,适合电力缺陷检测方向的学生、算法工程师及科研人员参考,用于模型效果对比、数据增广或迁移学习实验。
1. 这个数据集是干什么的:2504张图教会YOLO看住输电线路上的小螺栓
输电线路巡检里,螺栓销钉缺失是最让人头疼的一类缺陷——它不像断股、污闪那样显眼,往往就是塔头上一颗不到几十像素的小零件消失,等巡线员肉眼发现时,可能已经过去了几个巡视周期。我见过一个真实案例:某220kV线路因为销钉脱落导致螺栓松动,最后掉线短路,整段线路被迫停电检修。这种问题靠人工看照片效率太低,靠通用目标检测模型又几乎不可能,因为你需要的是专门针对电力场景、专门标注过“缺失”这个语义的数据集。这套VOC+YOLO格式、2504张、4类别的数据集,就是拿来干这件事的。它把图片、标注、格式都打包好,解压后可以直接训练YOLO,省掉从零做标注的周期。适合正在做电力巡检AI落地、手里缺高质量小目标样本的工程师,也适合刚接触YOLO想快速跑通一个垂直场景的入门者。
2. 为什么螺栓销钉缺失检测让通用目标检测模型集体翻车
2.1 小目标、类内差异与复杂背景:先看清问题再选数据
电力场景下的螺栓销钉检测,难点不在“认不认得出来”,而在“在什么条件下认得出来”。输电线路的照片通常由无人机或直升机拍摄,拍摄距离远、角度变化大,一颗M16螺栓在2000万像素原图里可能只占几十个像素,折算成训练时的640分辨率输入,可能只剩几个像素。小目标问题不是“尺寸小”这么简单,它直接影响了特征提取:浅层特征图上的信息本来就少,再经过卷积和下采样,核心纹理可能被背景吞掉。这也是为什么很多人在COCO上跑得风生水起的YOLO,拿到输电线路图片上mAP直接掉到0.3以下。
另一个难点是类内差异。同一根销钉,在不同塔型、不同光照、不同锈蚀程度下,外观差异极大;而“缺失”这个类别本身又是一个语义概念——不是让你检测一个物体,而是让你检测“某个位置原本应该有但现在没有”的状态。数据集的4个类别就需要把正常、松动、缺失、异物等情况分开,让模型学习“有和无”的边界。这种任务对标注的规范性要求极高,如果标注框是随手拉的,模型学到的东西就是乱的。
再说背景。输电线路塔头上有绝缘子、导线、金具、铁塔结构件,颜色从灰到红到蓝都有,纹理密集。通用检测模型训练时见过大量自然图像(人、车、动物),对金属结构件的响应往往会被背景干扰。这个时候,一套专门拍摄、专门裁剪、专门标注的电力场景数据集,比在通用数据集上预训练再微调更可靠。你可以把预训练当作起点,但真正让模型学会“看住螺栓”的,是训练时每张图上那一小块上下文。
2.2 通用数据集(比如COCO)在这里帮不上忙,Pascal VOC和YOLO格式才是这份数据集的便利点
有人会问:我直接用COCO预训练权重不行吗?行,但也只是初始化权重。COCO的80个类别里没有“销钉”“螺栓”“缺失”,模型在COCO上学到的特征表示可以迁移,但检测头需要重新训练。更重要的是,COCO里的目标普遍偏大,一个“人”占图幅比例远大于一个“销钉”,模型对尺度不敏感的话,在小目标上会明显失焦。你微调之后仍然需要针对小目标做多尺度训练、调整锚框,这些都要建立在数据质量过关的基础上。
而这份数据集直接用Pascal VOC格式和YOLO格式双保险给出标注。Pascal VOC格式是以XML文件存放标注信息,每个XML对应一张图,里面记录object的name、bndbox坐标;YOLO格式则是每张图一个txt,每行是“类别id x_center y_center width height”,坐标按图像宽高归一化。这两种格式是目标检测最通用的中间格式,PyTorch的YOLOv5/v8、MMDetection、Detectron2都能直接吃。拿到手之后,你基本不需要再做格式转换,最多按自己的划分策略重新split一下。这也是这份数据集的便利点:省掉从标注软件导出、清洗、转格式的一大堆流程,直接进训练。
不过,格式直接可用不代表没有坑。VOC的坐标是左上角和右下角的绝对像素值,YOLO是中心点加宽高的相对比例,两者转换时很容易因为取整误差导致框偏移,尤其对小目标,几个像素的偏移就能让一个本来正确的标注变成噪声。第三节我会写一个转换脚本,顺带说清楚这里面的边界问题。
3. 把.7z解压成能直接喂给YOLO的训练集:格式解析与转换
3.1 解压7z并核对VOC/YOLO双目录结构
拿到.7z压缩包,第一步是解压。7z格式在Linux下用p7zip,Windows下用7-Zip,macOS用p7zip或The Unarchiver。命令行解压是通用的:
# Linux / macOS 下解压(需要安装 p7zip) 7z x dianli_luoshuan_que_shi.7z -o./dataset # Windows 下可以用 7-Zip 图形界面,或者用 7z.exe # 7z.exe x dianli_luoshuan_que_shi.7z -o./dataset参数说明:x是解压并保留完整目录结构;-o指定输出目录,注意-o后面不能有空格,直接接路径。解压完成后,进去看一眼目录结构。常见做法是里面同时有JPEGImages/、Annotations/(VOC的XML)、labels/(YOLO的txt),可能还有ImageSets/Main/存放train.txt、val.txt。如果没有划分文件,你需要自己划分。
我一般会先用tree命令看两层目录:
tree -L 2 dataset/逻辑说明:这一步不是走形式。很多数据集在打包的时候会在外层套好几个文件夹,甚至带中文名,直接把你自己的脚本路径带偏。看到实际目录后再写后续处理,避免“命令写对了,路径不对”的尴尬。
3.2 VOC标注到YOLO的坐标转换:公式与Python脚本
如果你的数据集是VOC和YOLO双份,那训练YOLO时直接用YOLO txt就行。但有些数据集只有VOC,或者你拿到的是别人发的老版本,就需要自己转换。转换公式很简单:
- x_center = (xmin + xmax) / 2.0 / width
- y_center = (ymin + ymax) / 2.0 / height
- w = (xmax - xmin) / width
- h = (ymax - ymin) / height
注意所有坐标必须是浮点数,不能取整。看一个Python脚本:
import xml.etree.ElementTree as ET import os def voc2yolo(xml_file, classes): tree = ET.parse(xml_file) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('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) # 裁剪到图像边界,防止越界 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: 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 # 归一化后值域在 (0, 1),但有些框可能在边界上导致为0 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines classes = ['bolt', 'pin', 'missing', 'foreign_body'] # 按数据集实际情况改 xml_file = 'sample.xml' yolo_lines = voc2yolo(xml_file, classes) print("\n".join(yolo_lines))逻辑说明:脚本先解析XML里的size拿到图像宽高,再遍历object。这里有两个关键处理:一是坐标裁剪,因为标注软件偶尔会允许框超出图像边界,如果不裁剪,YOLO训练时会报“box out of bounds”错误;二是无效框过滤,如果裁剪后宽或高为0,这种框是纯噪声,留着只会干扰损失计算。归一化后的值理论上不会超过1,但我仍然做了min/max钳制,这是血泪经验——某些标注鼠标一抖,坐标直接飞到几千,不钳制的话训练时损失函数会爆炸。
3.3 按train/val/test划分并生成数据集描述文件
VOC格式一般会提供ImageSets/Main/train.txt和val.txt,但很多开源数据集并不可靠,可能只有总文件列表。保险做法是自己重新划分。我一般按7:2:1或8:1:1划分,并且保证同一张图不要同时出现在训练和验证里——如果数据集中有连拍的相似图,需要按序列分组,防止评估指标虚高。
import os import random from collections import defaultdict image_dir = 'dataset/JPEGImages' label_dir = 'dataset/labels' output_dir = 'dataset/split' # 按图片所属序列分组,这里假设文件名前缀为序列号 images = os.listdir(image_dir) groups = defaultdict(list) for img in images: seq = img.split('_')[0] # 按实际情况改 groups[seq].append(img) random.seed(42) all_seq = list(groups.keys()) random.shuffle(all_seq) train_seq = all_seq[:int(0.8 * len(all_seq))] val_seq = all_seq[int(0.8 * len(all_seq)):int(0.9 * len(all_seq))] test_seq = all_seq[int(0.9 * len(all_seq)):] def write_list(seqs, out_path): with open(out_path, 'w') as f: for seq in seqs: for img in groups[seq]: name = os.path.splitext(img)[0] f.write(name + '\n') os.makedirs(output_dir, exist_ok=True) write_list(train_seq, os.path.join(output_dir, 'train.txt')) write_list(val_seq, os.path.join(output_dir, 'val.txt')) write_list(test_seq, os.path.join(output_dir, 'test.txt'))逻辑说明:这个划分脚本的关键在于按序列分组。输电线路无人机拍摄时,同一个塔位经常连续拍几十张,如果不分组直接随机划分,训练和验证集里会出现同一塔的不同角度照片,模型实际上已经“见过”这个场景了,验证结果虚高,部署到新线路上就翻车。按前缀分组是一种保守做法,如果文件名没有明确的序列前缀,也可以按拍摄时间或者文件大小聚簇,但最简单的方法还是看原数据集的目录层级。
另外,YOLO训练不一定需要这个train.txt列表,但很多老工具链(比如YOLOv3格式)都需要。我习惯生成一份,至少可以做人工抽查。.7z包里如果自带了划分文件,你可以对比一下我的划分和原划分的差异,原则上以原包为准,自己划分只是为了复现时的可控性。
4. 用YOLOv8在这个数据集上训练:从yaml配置到损失函数设置
4.1 写一个data.yaml把类别名和路径对应起来
YOLOv8(还有v5)训练前需要一个data.yaml,里面写图片路径和类别名。格式如下:
# dataset.yaml path: ./dataset # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 test: images/test # 可选 nc: 4 names: ['bolt', 'pin', 'missing', 'foreign_body']逻辑说明:path是根目录,train和val是相对于path的路径。这里要注意,YOLOv8会用path + train去拼接图片目录,所以如果你的数据和标签分别在JPEGImages和labels里,需要先把图片和标签组织成标准结构:每个子目录下有images/和labels/,且文件一一对应(同名不同后缀)。这个结构是YOLO默认约定的,不按这个结构,训练时读不到标签。
如果你的图片和标签不在同一路径下,可以用软链接或直接改路径映射。简单做法是用一个Python脚本整理:
mkdir -p dataset/images/train dataset/labels/train # 按 split/train.txt 中的名字,将图片和标签分别复制到对应目录实际项目中我更喜欢软链接而不是复制,避免浪费几GB空间。具体命令因系统而异,不展开了。
4.2 模型选n/s/m还是x?先看锚框和小目标
YOLOv8有n/s/m/l/x五档模型。在这个场景下,我一般推荐从yolov8s开始,除非你验证资源非常有限。理由如下:
n模型太轻,对几十像素的小目标,特征提取能力不够,训练完往往mAP50-95上不去。s是性价比最高的点,在640分辨率下,显存占用约4-6GB,训练速度也快。m和l精度更高,但参数量上去了,小目标性能提升有限,而且部署到边缘盒子(比如Jetson)时帧率压力大。
如果你用的是YOLOv5系列,还可以用--multi-scale开启多尺度训练,让模型在0.5~1.5倍尺度间随机缩放输入。对螺栓销钉这种小目标,多尺度训练可以说是必选项。YOLOv8默认并没有完全关闭多尺度,但效果有限,建议在训练时显式设置scale=0.5(v8用超参数控制)或者手动开启。
训练命令的典型写法:
yolo detect train \ data=dataset.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ optimizer=AdamW \ patience=20 \ seed=42 \ degrees=0 \ translate=0.1 \ scale=0.5 \ fliplr=0.5 \ mosaic=1.0参数说明:model=yolov8s.pt表示从预训练权重开始微调,而不是从随机权重开始,这是YOLO训练的基本操作。epochs=100对这个小数据集来说偏多,一般配上patience=20早停,也就是连续20轮验证集没有提升就停。imgsz=640是输入分辨率,如果原图很大,我建议先用imgsz=1024训练一轮,再看小目标召回率有没有变化。batch=16按显存调整,显存不够就减半。lr0初始学习率,AdamW下0.01是个相对稳的值。scale=0.5是多尺度缩放的范围,这对小目标至关重要,尤其当你的原始图片分辨率超过2000像素时,训练时随机缩放到原来的一半,模型能学到更丰富的尺度特征。
4.3 YOLO的损失函数:从分类到回归,理解这几个参数
YOLOv8的损失函数主要由三部分构成:分类损失(BCEWithLogitsLoss)、定位损失(CIoU或DFL)、目标性损失(有无目标)。对小目标检测而言,影响最大的是定位损失和分类损失的平衡。YOLOv8里没有直接暴露这三个损失权重,但通过box和cls的超参数可以间接控制。比如默认的box=7.5,cls=0.5,这个比例对普通目标检测是合理的,但对小目标,框定位的精度往往比分类更敏感。我试过把box=10.0,cls=0.4,在销钉缺失这类任务上mAP50-95能提高1~2个百分点。
另外需要特别关注mosaic数据增强。mosaic=1.0表示100%的批次使用Mosaic增强,也就是把4张图拼成一张。这个增强对增加小目标样本非常有效,因为拼接后的图里,每个目标相对图像的占比变得更小。但Mosaic在一些数据集上会导致过拟合——模型学到的是“拼接痕迹”。所以我一般在最后20个epoch把mosaic=0.0关闭,让模型在真实分布上微调。这个操作在YOLOv5里叫--mosaic 0,在YOLOv8里需要在训练过程中动态调整超参数,或者你直接训练两段:先带Mosaic训练80轮,再关掉Mosaic训练20轮。
4.4 验证:mAP50、mAP50-95和PR曲线怎么看
训练结束后,用yolo detect val跑验证:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=dataset.yaml \ imgsz=640输出会给出mAP50、mAP50-95。对螺栓销钉检测,我更关注的是mAP50,因为类别区分度不高时,IoU阈值越高越难。如果你的mAP50在0.9以上,mAP50-95在0.6以上,这个模型基本能用。如果mAP50-95远低于0.5,说明预测框位置不够准,回到数据集检查标注框质量,而不是急着调参。
PR曲线也要看一眼。runs/detect/train/PR_curve.png会展示每个类别的查准率-召回率曲线。对“缺失”这类类别,召回率往往比准确率更重要——漏检一个销钉缺失可能造成停电事故,而误检顶多多发一条工单。所以如果你发现“missing”类别的召回率低于0.8,宁可通过降低置信度阈值来提高召回,然后在人工复核环节兜底。
5. 避坑:VOC转YOLO、小目标训练和.7z解压的五个雷
5.1 现象:训练时类别错乱,销钉变成了螺栓
原因:VOC的XML里name是字符串(“bolt”“pin”),YOLO格式要求整数id,如果你的classes列表顺序和XML里的字符串不严格对应,或者转换脚本里用了classes.index(name)但name因为大小写不一致匹配不上,就会跳过或错位。常见的是把“Bolt”和“bolt”当成两个类别,或者读到的name带空格。
解决:转换前打印所有XML里出现过的name去重列表,人工确认和classes一致。加一行校验:
if name not in classes: print(f"warning: {name} not in classes, skip") continue不要静默跳过,否则训练集少了一类,mAP还显得不错但实际模型是个瘸腿。
5.2 现象:mAP很高但实测漏检,问题在标注框偏了
原因:VOC标注的xmin、ymin从1开始还是从0开始?不同标注工具有差异。如果原始标注是1-based,转换时直接不处理坐标,所有框整体偏移一个像素。对几百像素的大目标无所谓,对几十像素的小目标,框偏移就导致特征区域不对,模型学不到正确外观。
解决:训练前用随机抽样代码把标注框画到图上,人工看10~20张。如果发现所有框都向左上角偏移半个到一个像素,在转换公式里减1:
xmin = float(box.find('xmin').text) - 1 ymin = float(box.find('ymin').text) - 15.3 现象:小目标完全检不出来,多尺度训练没生效
原因:可能你把imgsz设成了640,而原图都是4000×3000,直接resize后小目标变成2~3个像素,特征炸掉。还有一种情况是数据集的标注里本身就过滤了很多极小目标,训练时模型没见过真正的“小”。
解决:先用原图看一下目标像素尺寸分布。写个脚本统计标注框的宽度分布,如果中位数小于20像素,建议用imgsz=1024甚至1280训练,代价是显存和速度。或者使用SAHI这类切分推理方案,推理时把大图切成多个小块再检测。后者在真实项目中非常有效,我后面还会提。
5.4 现象:解压后图片少了,文件名中文乱码
原因:.7z包里的文件名如果不是UTF-8编码,在Windows下解压可能变成乱码,甚至无法解压。另外有些包使用中文文件名,训练时YOLO的datasets类对非ASCII路径支持不好。
解决:Linux下用ls确认文件名,如果是乱码,用convmv做编码转换,或者干脆重命名所有文件:
# 将所有图片重命名为纯数字前缀 python -c "import os for i,f in enumerate(sorted(os.listdir('JPEGImages'))): ext=f.split('.')[-1] os.rename(f'JPEGImages/{f}', f'JPEGImages/{i:05d}.{ext}') "同时把labels目录按同样的映射改名。这一步虽然粗暴,但能避免之后所有框架层面的编码问题。
5.5 现象:验证集图片与训练集重叠,指标虚高
原因:前面提到过,如果直接随机划分,同一塔位的连续照片会被分到两个集合里,模型记住场景而不是识别销钉。
解决:按序列分组划分。如果看不到文件名里的序列信息,可以用感知哈希算法计算相似图片簇,但最可靠的方法还是回到原始拍摄任务,问数据提供方哪些照片属于同一场景。数据集里可能没有提供分组信息,那你就只能相信文件名规律。我自己的习惯是如果发现验证集mAP特别高(比如0.98以上)但在新场景上掉到0.6,先怀疑数据划分泄漏。
6. 一个验证数据集质量的技巧:随机抽图叠加标注框并统计类别分布
拿到任何数据集,别急着开训,先花10分钟做一次可视化体检。我的习惯是写一个简单的Python脚本,从训练集里随机抽16张图,把VOC或YOLO标注框画出来,然后拼成一张大图。这样能直观看到框是否贴合目标、有没有漏掉难例、有没有标签和图像不匹配的情况。
import cv2 import random import glob img_files = glob.glob('dataset/images/train/*.jpg') random.shuffle(img_files) selected = img_files[:16] for i, img_path in enumerate(selected): img = cv2.imread(img_path) h, w = img.shape[:2] label_path = img_path.replace('images', 'labels').replace('.jpg', '.txt') # 读取YOLO格式并画框 with open(label_path) as f: for line in f: parts = line.split() cls = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f'check_{i}.jpg', img)逻辑说明:这个脚本把每张图的标注框原样画出来。你肉眼观察的时候,重点看三类问题:框是否比目标大一圈、框是否明显偏离目标中心、是否存在大量目标没被框住。如果框普遍偏大,VOC转YOLO时可能错把外接矩形扩到了背景;如果框偏移,检查1-based还是0-based问题。
同时顺手统计类别分布:
for f in dataset/labels/train/*.txt; do cat $f; done | awk '{print $1}' | sort | uniq -c如果四个类别样本数偏差超过10倍,比如“missing”只有几十张,训练时这个类别大概率学不好。这时你需要考虑类别重采样,或者用--cls损失权重调节。很多时候,你会发现所谓4类别数据集真正有效的其实只有2~3类,剩下的是凑数。这不是数据集不负责,而是缺陷样本本身稀缺。遇到这种情况,我的做法是放弃均衡,把注意力放在召回率上,接受一定的误检。
这也是我为什么一直强调“先验证再训练”的原因。跑一次训练再发现这些问题,至少浪费半天。现在,这份数据集尽管只有2504张,但只要你把上面几步检查做到位,它足够支撑一个能上线的检测模型。电力场景的检测从来不是拼数据集大小,而是拼你对小目标问题的理解和对噪音的容忍度。希望你少踩我踩过的这些坑,训练顺利。
本文还有配套的精品资源,点击获取