简介:面向输电线路巡检场景的螺栓销钉缺失检测图像数据集,包含大量高清图片,并配套VOC格式的详细标注文件,全面覆盖正常与销钉缺失两类典型状态。整套压缩包共两千个文件,包含约一千二百张图片与等量标注文档,大小不足一百兆,便于下载与快速部署。图片以输电线路大目标为主,清晰度高,使用标注工具逐框完成边界框绘制,坐标和类别信息可直接被主流检测框架读取。基于此数据训练的目标检测模型在实测中平均精度达到百分之九十三点七,能够有效识别销钉缺失隐患,为电力设施自动化巡检提供可靠支撑,显著降低人工登塔检查的安全风险。目前已有千余人学习下载,适合视觉入门者、电力智能化研究人员以及算法开发工程师将其作为基准数据集使用。
1. 输电线路螺栓销钉缺失检测图像数据集:1209张大目标VOC值得先看
做输电线路巡检缺陷检测的人,绕不开“输电线路螺栓销钉缺失检测图像数据集(1209张,大目标,VOC)”这类资源。它的任务很具体:从无人机或人工拍摄的电力杆塔图像中,判断螺栓销钉是否缺失。销钉缺失看着是小毛病,实则是金具松动、断线掉串的前兆,巡检中一旦漏掉,后果就不是一张罚单能解决的。
这个数据集最反直觉的地方在“大目标”三个字。很多人一听螺栓销钉,第一反应是“小目标检测,得上YOLO-seg或者专门的小目标分支”,但在这个数据集里,拍摄距离近、目标占比大,常规检测器完全能胜任。VOC格式意味着拿到手先要做格式转换和数据划分,再喂给模型。这篇笔记就围绕“这是什么、怎么转格式、参数怎么给、坑在哪”展开,适合刚接触电力视觉、准备用公开数据集训练第一个缺陷检测模型的工程师。
2. 拆解VOC标注与“大目标”:图像集交给模型前的第一道准备
2.1 VOC数据集的标准三件套:JPEGImages、Annotations 与 ImageSets
Pascal VOC格式是目标检测数据集最常见的组织方式,这个数据集如果按标准VOC组织,拿到手解压后会是这样的目录结构:
dataset_root/ ├── JPEGImages/ │ ├── IMG_0001.jpg │ ├── IMG_0002.jpg │ └── ... ├── Annotations/ │ ├── IMG_0001.xml │ ├── IMG_0002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txtJPEGImages放原始图像,Annotations放一一对应的XML标注文件,ImageSets/Main里是训练、验证、测试集的文件名清单。标题里写的“1209张”对应JPEGImages下的图片数量,正常情况下Annotations里同样有1209个XML,ImageSets里的txt每行一个不带扩展名的文件名。
我拿到任何VOC数据集的第一件事不是看图片,而是先检查三件套是否齐全。很多公开数据集搬运时丢过文件,ImageSets里写了某个文件名,但JPEGImages里根本没有这张图,训练跑到一半报错,回头查才发现是数据不全。先跑一个脚本把三个目录的文件名集合比对一下,能省后面一整天的排错时间。
检查完目录完整性,下一步是看ImageSets里给好的划分能不能直接用。有些数据集作者按自己的习惯划分训练验证集,比例未必合理,更常见的情况是只有train.txt和val.txt,没有test.txt。这时不要迷信作者划分,我会在转格式阶段自己重新划分,毕竟训练集和验证集的质量,直接决定你后面调参时看指标准不准。
2.2 一条销钉缺失标注在XML里长什么样
VOC格式的核心是XML标注文件,它不像YOLO的txt那样只有一行类别和坐标,而是把图像尺寸、文件名、目标类别、边界框全部结构化地写在XML里。一个典型的销钉缺失标注XML长这样:
<annotation verified="yes"> <folder>JPEGImages</folder> <filename>IMG_1587.jpg</filename> <source> <database>bolt_pin_dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>missing_pin</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>1124</xmin> <ymin>566</ymin> <xmax>1486</xmax> <ymax>844</ymax> </bndbox> </object> </annotation>这个XML里有三部分信息最要紧。size节点记录原始图像的宽、高、通道数,后面转YOLO格式做坐标归一化全靠它,如果这个值和图像实际尺寸对不上,转换出来的框全是偏的。object节点下的name是类别名,bndbox是边界框的绝对像素坐标,xmin/ymin是左上角,xmax/ymax是右下角。difficult字段表示这个目标是否难以辨认,值为1的样本在训练时应该被忽略或降权,但很多人转换格式时直接丢掉这个字段,等于把难例也当正常样本喂给模型。
这里要特别提醒一点:不同数据集的类别命名习惯不一样。同一个“螺栓销钉缺失”问题,有的数据集叫missing_pin,有的叫bolt_missing,还有的叫defect_pin。类别名不会影响模型训练的本质,但会影响你写配置文件时对类别的理解。拿到数据集后先统计一遍所有XML里出现过的name值,确认到底有哪几个类别,再决定后续怎么组织训练。这个数据集通常涉及“有销钉”和“销钉缺失”两类状态,具体怎么命名,以实际XML里的统计结果为准。
2.3 用面积占比判定“大目标”,并决定要不要上高分辨率
标题里的“大目标”不是玄学,是可量化的。判断一个目标是大还是小,最直接的方式是计算边界框面积占整张图像面积的比例。写几行代码就能算:
import xml.etree.ElementTree as ET tree = ET.parse("Annotations/IMG_1587.xml") root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) img_area = w * h for obj in root.iter("object"): if obj.find("name").text != "missing_pin": continue 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) box_area = (xmax - xmin) * (ymax - ymin) print(f"{root.findtext('filename')} 目标面积占比: {box_area / img_area:.3f}")按我自己的经验阈值划分,面积占比大于0.04算大目标,0.01到0.04算中等目标,小于0.01就要认真对待了。如果这个数据集里多数缺失销钉的框占图像面积在5%以上,那它确实配得上“大目标”三个字。
“大目标”这个属性直接影响两个决策。第一,模型选型不用为了小目标专门加检测头或上超高分辨率输入,YOLOv8、RTDETR这类主流检测器直接能用。第二,输入图像分辨率不需要无脑拉到1280以上,大目标即使缩放到640分辨率,特征依然保留得比较完整,训练速度还能快不少。很多人在大目标数据集上反而把训练搞得很慢,就是因为默认用了小目标场景的套路,白白浪费显存和时长。
3. 把VOC转成YOLO训练输入:完整脚本与三类划分注意
3.1 先做两类自查:缺图缺标注、类别名不一致
转格式之前,先跑一遍数据自查。第一个自查是文件完整性,比对JPEGImages和Annotations的文件名差集:
from pathlib import Path img_stems = {p.stem for p in Path("JPEGImages").glob("*.jpg")} xml_stems = {p.stem for p in Path("Annotations").glob("*.xml")} print("缺XML的图片:", img_stems - xml_stems) print("缺图片的XML:", xml_stems - img_stems)这个脚本输出空集合才算正常。只要有差集,后面转格式或训练时就会在某个节点报“找不到文件”。缺XML的图片可以直接从训练列表里剔除;缺图片的XML说明标注信息损坏,更得查清楚来源。
第二个自查是类别统计。把所有XML里出现过的name枚举一遍,确认类别数量和你预期一致:
from collections import Counter import xml.etree.ElementTree as ET from pathlib import Path counter = Counter() for xml_file in Path("Annotations").glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.iter("object"): counter[obj.find("name").text] += 1 print(counter)如果统计结果里多出意想不到的类别,或者某个类别只有个位数样本,现在发现还来得及调整策略。比如有的数据集会把“销钉正常”和“销钉缺失”作为两类分别标注,你的模型就要做成二分类检测;如果只标了“缺失”一类,那么正常销钉区域就是背景,训练时模型靠的是“看到正常销钉不输出”来学习,这对正负样本比例的要求更高。这里不存在标准答案,只有基于实际标注的应对方案。
3.2 不依赖第三方标注库的VOC转YOLO脚本:关键参数说明
YOLO系列训练需要的是txt格式标注,每行一个目标,格式是类别ID x_center y_center width height,坐标全部是相对于图像宽高的比例值。写一个不依赖任何第三方标注库的转换脚本,用Python标准库就能搞定:
import xml.etree.ElementTree as ET from pathlib import Path # 类别列表,顺序决定了YOLO的类别ID,必须和训练用的yaml保持一致 CLASSES = ["missing_pin", "normal_pin"] def convert_voc_to_yolo(xml_file: Path, out_dir: Path) -> None: root = ET.parse(xml_file).getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) filename = root.findtext("filename") out_path = out_dir / (Path(filename).stem + ".txt") lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in CLASSES: continue cls_id = CLASSES.index(cls_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) # 归一化到 [0, 1],并裁剪越界框 x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) bw = min(bw, 1.0 - x_center) bh = min(bh, 1.0 - y_center) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path.write_text("\n".join(lines), encoding="utf-8") if __name__ == "__main__": out_dir = Path("labels") out_dir.mkdir(exist_ok=True) for xml_file in Path("Annotations").glob("*.xml"): convert_voc_to_yolo(xml_file, out_dir) print(f"转换完成,输出目录: {out_dir}")这段脚本里两个地方最容易出问题。第一,CLASSES列表的顺序就是YOLO的类别ID,脚本里missing_pin排在0号位、normal_pin排在1号位,训练时data.yaml的names列表必须一字不差地按同样顺序写,否则模型会把类别标签学串,训练过程loss正常但评价指标一塌糊涂。第二,越界框裁剪不是可选项。VOC标注偶尔会出现框超出图像边界的情况,不裁剪直接训练,YOLO在计算损失时会产生负坐标或宽度超过1的框,轻则警告,重则训练不稳定。
3.3 按目标分布划分数据:而不是简单shuffle
很多人转完格式,直接一个random.shuffle把图片分成训练集和验证集完事。这种方式在这个数据集上容易翻车:如果“销钉缺失”样本在1209张里只占一小部分,随机划分很可能把大部分缺失样本都分进训练集,验证集里几乎看不到缺失类别,模型训练出来验证mAP虚高,一到真实场景就露馅。
更好的做法是按目标类别分布分层划分。先统计每张图是否包含缺失样本,再在两个分组内分别做划分:
import random from pathlib import Path import xml.etree.ElementTree as ET random.seed(42) train_ratio = 0.8 def has_missing(xml_file: Path) -> bool: root = ET.parse(xml_file).getroot() for obj in root.iter("object"): if obj.find("name").text == "missing_pin": return True return False missing_files = [] normal_files = [] for xml_file in Path("Annotations").glob("*.xml"): if has_missing(xml_file): missing_files.append(xml_file.stem) else: normal_files.append(xml_file.stem) train_list = [] val_list = [] for group in (missing_files, normal_files): random.shuffle(group) split_idx = int(len(group) * train_ratio) train_list.extend(group[:split_idx]) val_list.extend(group[split_idx:]) # 写入带.jpg扩展名的列表文件,YOLO直接可用 Path("train.txt").write_text("\n".join(f"{name}.jpg" for name in train_list)) Path("val.txt").write_text("\n".join(f"{name}.jpg" for name in val_list)) print(f"训练集: {len(train_list)} 张, 验证集: {len(val_list)} 张")分层划分的意义在于,验证集里的缺失样本比例和训练集一致,模型在验证集上的表现才真实反映它对缺失类别的识别能力。另外,这里写出的txt文件每行直接带.jpg后缀,YOLO读取时能直接定位图片。VOC自带的ImageSets/Main里的txt通常没有扩展名,不能直接拿来做YOLO训练列表,这也是个常见的衔接断层。
4. 用YOLOv8跑通销钉缺失检测:参数怎么给才不白训
4.1 数据配置文件:先搞定yaml才不会训练时报错
格式转换完成后,训练前还要写一个data.yaml。YOLOv8的数据配置非常简单,但路径和类别顺序写错的人不在少数:
path: /data/bolt_pin_dataset train: train.txt val: val.txt names: 0: missing_pin 1: normal_pinpath是数据集根目录的绝对路径,train和val填的是相对于path的txt文件路径。这里有一个细节:如果train.txt里存的是绝对路径,YOLO会直接按绝对路径找图,path字段只作为兜底;如果存的是相对路径,path就必须写对,否则报错找不到图片。
names的顺序是最大隐患。上一节转换脚本里CLASSES列表怎么排,这里names就得怎么排。missing_pin是0、normal_pin是1,训练脚本会把这个顺序内嵌进模型输出层。一旦顺序写反,模型训练的loss正常,但预测时输出的类别语义完全颠倒,这是最隐蔽的坑之一。
4.2 训练命令与参数表:imgsz、batch、epochs怎么给
数据配置没问题后,先用最小命令跑通训练闭环。我一般会先用YOLOv8m而不是最大的YOLOv8x,因为第一轮的目的是验证数据链路是否通畅,不是追精度:
yolo detect train \ model=yolov8m.pt \ data=bolt_pin.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/bolt_pin \ name=exp1几个关键参数的给法如下:
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8m.pt | 第一轮用m量级跑通,不要一上来就x量级 |
| imgsz | 640 | 大目标场景先跑通,有需要再试960或1280 |
| batch | 显存允许范围内越大越好 | 16不够稳就降到8,配合梯度累积 |
| epochs | 100 | 先看前50轮的曲线趋势,再决定是否加长 |
| device | 0 | 单卡默认,多卡用0,1,2,3 |
很多人拿到“大目标”数据集后第一反应是把imgsz拉到1280,觉得分辨率越高精度越好。这里我建议反着来:第一轮就用640跑通,让训练时间短一点、迭代试错快一点。大目标图像缩到640后,螺栓销钉的特征依然清晰,不会因为下采样丢掉关键纹理。如果640训练后mAP确实不满意,再上960,通常都能看到明显的精度提升,而1280只是在960基础上做边际收益的取舍,还要掂量显存占用和推理延时的代价。
batch对训练稳定性的影响比很多人以为的大。batch太小,BN层的统计量抖动厉害,loss曲线会像锯齿;batch太大,显存不够只能砍imgsz,反而得不偿失。我的习惯是先看GPU显存,12GB左右的卡用batch 8配imgsz 640,24GB的卡可以上batch 16。显存不够时,优先降batch而不是降imgsz,因为imgsz对精度的影响更直接。
4.3 训练中观察什么:loss走向与验证集指标
训练过程中不要只盯着终端进度条,要看三个维度的信号。第一是train/box_loss,它应该在前期快速下降,中后期进入平台期,如果出现明显反弹,多半是学习率设置过冲。第二是train/cls_loss,它的走向比box_loss更敏感,一旦突然飙升,先检查是不是类别标签顺序出了问题。第三是val/mAP50-95,这个指标会震荡,但整体趋势必须向上,如果50轮过去mAP50还在0.5以下卡住,问题大概率出在数据层面而不是训练参数。
我自己踩过的教训是:第一轮训练跑完,如果mAP50有0.6以上,先别急着调参,直接去看bad case。YOLO训练结束后会在runs/bolt_pin/exp1/下生成混淆矩阵和验证集预测图,花十分钟翻一遍预测图,比调三天参数都管用。你要确认的是漏检的到底是不是那些目标极小、被遮挡严重的样本,如果是,这就不是参数问题,而是数据本身覆盖不够。
5. 常见问题与排查:4个让销钉检测翻车的坑
5.1 现象:训练loss正常下降,验证mAP卡在低位
训练过程看着一切正常,box_loss和cls_loss都在降,但验证集mAP就是上不去,在0.3到0.5之间反复横跳。这种情况我第一反应是去看每个类别的单独AP,而不是盯着整体mAP。打印出混淆矩阵后,十有八九是“缺失销钉”这个类的AP很低,而正常销钉的AP很高。
原因通常是类别样本极不平衡。1209张图里,如果“销钉缺失”的标注框数量只有正常销钉的十分之一,模型会把大部分学习能力分配给学生量更大的类别,缺失类被当成离群点忽略掉。解决思路有两个:一是在损失函数层面给缺失类提高权重,YOLOv8里可以通过cls参数调节类别损失占比,但效果有限;二是更实用的做法,在数据层面做增强偏置,对包含缺失销钉的图片施加更强的随机裁剪、旋转、亮度扰动,让模型在每个epoch里看到的缺失样本形态更多样。
5.2 现象:同一螺栓在相邻两帧一帧检出、一帧漏检
用视频帧序列做推理时,同一个螺栓销钉在上帧被检出、下帧就漏了,单看每一帧的置信度都说得过去,但时序上断断续续。这个问题不在于模型单帧能力差,而在于训练数据的多样性覆盖不足。
巡检视频相邻帧之间的差异主要是光照角度和微小的拍摄抖动,模型对亮度变化和轻微模糊的鲁棒性不够。解决方法是调整训练时的数据增强参数,重点加强HSV扰动。YOLOv8训练命令里加上hsv_h=0.02 hsv_s=0.8 hsv_v=0.5这类参数,模拟不同光照下的色相、饱和度、亮度变化。推理侧的补救措施是连续帧投票:把同一塔位连续N帧的检测结果按IoU做时序匹配,单个目标在超过一半帧里被检出就保留,否则丢弃。这个方法能把单帧漏检率降下来一个量级,代价是要做帧间跟踪的逻辑。
5.3 现象:把绝缘子、均压环这些金具识别成销钉
模型对真正的销钉检测得不错,但频繁把绝缘子串、均压环、碗头挂板这些纹理接近的电力金具框出来标成“缺失销钉”,误报率高到没法上线。这种误检通常是两个原因叠加。
第一,标注框的边界不干净。如果XML里有些bbox把销钉周围的背景也圈进去了,模型学到的特征就包含金具纹理和背景的混合模式,推理时遇到相似纹理就容易误触发。第二,训练数据里缺少“难负样本”。正常销钉被标注成normal_pin类,但绝缘子、均压环这些从未出现在任何标注框里的物体,模型根本没有见过它们作为“非目标”的样子。解决思路是收集一批没有任何销钉的杆塔图像,整张图不标注任何目标,作为纯背景图加入训练集。这一步加几十张图,往往比调任何参数都管用。
5.4 现象:VOC转YOLO后,框的中心点对不上
转换完格式,用可视化脚本把txt标注画回图像上,发现框的位置整体偏移,有时甚至跑到目标旁边的空地上。这个问题九成出在图像尺寸上,XML里的size和实际图像解码后的尺寸不一致。
有些数据集的图像经过预处理缩放,但XML里的width和height还是原始值;还有些手机或无人机拍摄的JPG带EXIF旋转信息,用OpenCV读取时方向会被自动矫正,而标注坐标是在旋转前列的。解决方法是转换脚本里加一步校验,用cv2.imread读取实际图像尺寸,和XML的size做比对:
import cv2 img = cv2.imread(str(img_path)) h_actual, w_actual = img.shape[:2] if h_actual != h or w_actual != w: scale_x = w_actual / w scale_y = h_actual / h # 所有x坐标乘以scale_x,所有y坐标乘以scale_y,再继续归一化这一步校验放在转换脚本的最前面,能在源头上拦住一类很隐蔽的错误。不要相信我,也别相信XML里的记录,以实际解码后的图像尺寸为准。
6. 结果验收与数据扩展:别让模型只在验证集上好看
6.1 验收要看漏检率与误报率,mAP只是第一步
训练结束后,很多人把val/mAP50-95当成绩单,但工程上线要的是另外两个数:漏检率和误报率。mAP是排名指标,它衡量的是模型在全部置信度阈值下的平均表现;而现场部署时你会固定一个置信度阈值,真正决定能不能用的是“缺失销钉到底漏了多少、正常金具被误报了多少”。
我习惯在验证集上单独统计这两个数:设conf=0.25跑一遍预测,对每个真实标注框,看有没有IoU大于0.5的预测框命中;对每个预测框,看它有没有对应的真实框。漏检率用“没被命中的缺失目标数除以缺失目标总数”,误报率用“每100张图产生的无匹配预测框数”。这两个数字能直接换算成现场巡检验收时的人工复核成本。
6.2 用少量相似场景数据做外推验证
最后一个建议:数据扩展时,优先找不同拍摄距离、不同天气、不同角度下的杆塔图像,而不是继续收集和现有数据高度雷同的图片。我做过的项目里最亏的一次,就是训练集和验证集都是同一批塔、同一个相机角度拍的,模型在验证集上mAP漂亮得不行,拿去给另一批杆塔做巡检立刻露馅,原因就是训练数据没覆盖“相机焦距变了”这个变量。
扩展数据最省力的路径是半自动标注:用当前模型对新图像做预标注,人工只负责修正错框补漏框,1209张规模的数据集,一两天就能翻一倍,而且新样本带来的多样性收益远比数量增长重要。这个方向值不值得投入,说到底不是看模型在现有验证集上的分数,而是看它在没见过的现场条件下能不能站住。希望帮到你。
本文还有配套的精品资源,点击获取