简介:面向电力系统智能巡检场景的变电站缺陷图像数据集,含8307张现场拍摄的高清jpg图像,提供Pascal VOC xml与YOLO txt双格式标注,覆盖避雷器破损、变压器渗油、开关柜异物、隔离开关锈蚀、绝缘子污闪、小动物入侵等17类运维高频典型缺陷。资源包共4个文件,包括txt说明、Python脚本、inscode配置及gitignore,整体仅3KB,其中使用前必读.txt明确列出文件组织方式、类别映射表及常见问题处理建议。已有260人学习下载。所有标注边界框完整准确,txt文件仅含class_id及归一化坐标,可直接接入YOLO系列、Faster R-CNN、SSD等主流框架训练验证;xml包含完整图像尺寸与object结构,适配OpenCV、LabelImg、Roboflow等工具链,便于二次处理与模型迭代。
1. 变电站设备缺陷图像数据集,8307张双格式标注图能解决的那件事
如果你正在做变电站设备缺陷检测,第一反应通常是到处找图像数据。看到“变电站设备缺陷图像数据集,含8307张VOC+YOLO双格式标注图,覆盖17类典型故障类型”这个标题,先别把它当普通图库:这是一份已经标注好、可以喂给目标检测模型训练的数据包。它把电力巡检场景里最常见的一类麻烦——图片有了但标注格式混乱、类别定义不统一——提前消掉了。适合谁用?做电力视觉检测的算法工程师、刚转过来做工业缺陷识别的开发者、以及要给团队搭一套检测Demo的负责人。8307张图不算超大,但对17类缺陷来说,只要能正确划分训练集,足够把基线模型跑起来,而且双格式意味着你不需要再纠结是接VOC接口还是YOLO接口。
2. 双格式的底牌:VOC与YOLO标注在坐标系和组织方式上的差异
2.1 VOC的XML与YOLO的TXT各自怎么描述一个目标框
VOC格式用的是XML,核心是object节点,每个object里有name和bndbox四个坐标:xmin、ymin、xmax、ymax,单位是像素。YOLO训练用txt文本,每一行是一个目标,格式是class_id x_center y_center width height,坐标都归一化到0到1之间,也就是相对图像宽高的比例。两种格式描述的是同一个目标框,但数学模型不同:VOC直接给人读,YOLO给模型读。互相转换的公式是固定套路:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height
大多数标注工具导出时已经做了这些,但如果你拿到原始XML自己转,最容易出错的是除以图像宽高而不是除以边界框的宽高,或者把归一化后的width和height当成xmin、ymin存。我一般会写一个不依赖标注工具的校验函数,哪怕只是打印一个框的原始XML和转换后的五元组对一眼。
import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_path): tree = ET.parse(xml_path) root = tree.getroot() img_w, img_h = Image.open(img_path).size objs = [] for obj in root.findall("object"): name = obj.find("name").text bnd = obj.find("bndbox") xmin = int(float(bnd.find("xmin").text)) ymin = int(float(bnd.find("ymin").text)) xmax = int(float(bnd.find("xmax").text)) ymax = int(float(bnd.find("ymax").text)) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / float(img_w) h = (ymax - ymin) / float(img_h) objs.append((name, x_center, y_center, w, h)) return objs逻辑说明:这段代码先从XML里拿类别名和像素坐标,再用PIL读取真实图片尺寸做归一化。关键在于 img_w 和 img_h 来自原始图片而不是某个“标准宽度”,因为很多变电站巡检图片是无人机或机器人拍的,分辨率经常不一致。如果假设全数据集都是同一个分辨率,转换出来的坐标会整体漂移,训练时loss降不下去,这类问题用肉眼很难发现。
参数说明:int(float(...))是为了处理某些标注工具把坐标写成了带小数的字符串,直接int可能报错,先转float再转int更稳。如果要保留浮点精度,也可以不转int,直接用float值做换算。
2.2 8307张图和一个双格式数据集想让你省掉的四件事
双格式存在的直接好处是省掉了格式对接:你要用检测框架的VOC接口,直接用Annotations里的XML;要用YOLO系训练,直接用labels里的TXT。但要把它当成工程资产而不是压缩包,还要验证四件事。
第一,图片数量和标注文件数量是否一致。8307张图理论上应该有8307个XML和8307个TXT,如果某个XML里没有任何object,有些工具不会生成TXT,导致YOLO结构里少文件,做数据划分时列表不齐。第二,图片文件名和标签文件名的主干必须完全一致,包括大小写和后缀。第三,17类的类别清单是固定的,YOLO训练用的data.yaml里names顺序必须和TXT里的class_id一一对应。第四,检查有没有损坏图片,比如EXIF异常或分辨率为零,这类图在前处理时容易导致batch直接崩。
写个统计脚本排查很快:
from pathlib import Path imgs = {p.stem for p in Path("JPEGImages").glob("*.jpg")} xmls = {p.stem for p in Path("Annotations").glob("*.xml")} txts = {p.stem for p in Path("labels").glob("*.txt")} print("images:", len(imgs)) print("xml missing:", len(imgs - xmls)) print("txt missing:", len(imgs - txts)) print("all aligned:", imgs == xmls == txts)逻辑说明:三个集合按stem比较,能快速找到缺失方向。用集合而不是列表,是为了避免文件顺序差异导致的误判。imgs - xmls 表示有图但没有XML的样本;imgs - txts 表示有图但没有YOLO标签的样本。如果两边都缺失,说明这张图本身就没有标注。
参数说明:glob("*.jpg") 只匹配jpg后缀。如果数据包里还有png或bmp,需要把三种后缀都扫一遍,或者直接用 Path("JPEGImages").iterdir() 再过滤文件后缀。这个问题在工业数据包里很常见,图片格式不统一不是玄学,是常态。
2.3 类别分布决定评价指标而不是反过来
17类里往往有的类别出现几百次,有的只出现几十次。训练前要统计每个类别的框数量。如果某个类只出现在很少的图里,再经过train/val划分,验证集可能一张都没有,导致mAP里该类被跳过或直接报class not present。
先统计每个class_id在labels里出现多少次:
from collections import Counter from pathlib import Path # 按数据集实际定义填,这里只是示例 cls_map = { 0: "insulator_break", 1: "overheat", 2: "foreign_object", 3: "oil_leak", } counter = Counter() for txt in Path("labels").glob("*.txt"): for line in txt.read_text().splitlines(): if line.strip(): cls_id = int(line.split()[0]) counter[cls_id] += 1 for cls_id, cnt in sorted(counter.items()): print(cls_map.get(cls_id, f"unknown_{cls_id}"), cnt)逻辑说明:标签文件里每行第一列就是class_id,读出来计数即可。这个统计至少能回答两个问题:类别是否严重不平衡,以及每个类别是否有足够的框参与训练。如果某个类只有几十个框,那它在验证集里的表现将非常不稳定,涨跌可能只是随机性,而不是模型真实能力。
参数说明:cls_map必须从数据集的data.yaml或classes.txt读取,不能靠猜。有些数据包类别顺序按英文字母排,有些按采集顺序排,排序方式不同会让class_id完全错位。读到unknown时,要先停下检查,而不是继续训练。
3. 把数据集跑成缺陷检测模型:最小可复现的训练流程
3.1 从双格式到YOLO目录结构:图像与标注的同步划分
常见做法是把数据集目录整理成YOLO风格:
dataset/ images/train images/val labels/train labels/val data.yaml如果原始包已经是这种结构,基本能直接训。如果只有JPEGImages、Annotations、labels三个平铺目录,就要先划分。划分时最容易翻车的是只切图片,不切标签文件,或者切的时候两个目录的顺序不一致。我一般用Python脚本一次性生成划分并复制文件,而不是shell里两次glob。
import random import shutil from pathlib import Path src_img = Path("JPEGImages") src_lbl = Path("labels") img_dst = Path("dataset/images") lbl_dst = Path("dataset/labels") random.seed(42) all_imgs = list(src_img.glob("*.jpg")) random.shuffle(all_imgs) val_n = int(len(all_imgs) * 0.15) for i, img in enumerate(all_imgs): split = "val" if i < val_n else "train" stem = img.stem lbl = src_lbl / f"{stem}.txt" if not lbl.exists(): print("missing label:", stem) continue (img_dst / split).mkdir(parents=True, exist_ok=True) (lbl_dst / split).mkdir(parents=True, exist_ok=True) shutil.copy(img, img_dst / split / img.name) shutil.copy(lbl, lbl_dst / split / lbl.name)逻辑说明:按stem前缀去找标签能避免jpg和png混用时的名称错位;random.seed固定后每次划分结果一致,方便复现。val_n取15%是为了训练有足够样本,又留了验证曲线。若某类缺陷特别少,要考虑按类别分层划分,否则稀有类可能集中在train或val里。
参数说明:先用mkdir之后再copy,避免目标目录不存在时报错。用copy而非move是因为原始数据集要做保留,move万一走到一半报错,原始包就残缺,后悔药都没有。如果你的硬件显存小,可以只移动标签文件,图片用软链方式指向JPEGImages,节省一半磁盘空间。
3.2 data.yaml与训练参数:17类names对齐是第一优先级
无论用什么开源YOLO训练框架,都会要求一个data.yaml或等效的类名文件。把17类写入names时,顺序必须和labels里class_id的数字对应。千万不要按中文名排序,很多新手在这里翻车:labels里class_id=0是insulator_break,data.yaml里names[0]写成了overheat,训练不报错,但val曲线和实际检测结果全部错位。
path: dataset train: images/train val: images/val nc: 17 names: 0: insulator_break 1: overheat 2: foreign_object # 剩余14项按数据集的完整清单填参数说明:path字段写相对路径时,要保证执行训练命令的终端目录在dataset的上一级,不然会拼出错误路径。train和val也可以是绝对路径,但换机器后要改,推荐用相对路径。names里的顺序直接决定类别输出,标注边界时也要对着这份清单看。
训练脚本我习惯把epochs留在150以内先看趋势,imgsz=640,batch按显存调整,预训练权重用通用权重。完整命令用YOLO CLI很短:
yolo detect train data=defect.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 device=0 workers=4 patience=20 project=defect_runs name=base参数说明:patience=20表示验证指标20轮不涨就早停,电力缺陷样本少时能省时间。workers=4不是越大越好,数据读取太快而GPU没那么强时,反而会让CPU占满。batch=16在显存不大时可降到8。数据增强里的mosaic默认开着,对绝缘子这类小目标效果不错;如果训练后期发现val指标震荡,再逐步关掉mosaic。
3.3 训练前用一张图自检标注加载
我第一次拿到新的标注集时,不会直接train,而是先写一段可视化脚本,抽取train里每个类别各一张,画出ground truth框,确认框的位置和缺陷位置重合。YOLO框架自己有可视化工具,但如果你用VOC接口训练,就要自己写。
import cv2 from pathlib import Path def draw_yolo(img_path, txt_path, cls_colors): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cid, xc, yc, bw, bh = map(float, line.split()) 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), cls_colors[int(cid)], 2) return img逻辑说明:画框时要先把归一化坐标乘以宽高再取整。直接画在归一化坐标系上会画到图片外。这个自检脚本跑15分钟,能过滤掉百分之八十的坐标错位问题,比训练到一半看loss曲线有效。
参数说明:cls_colors是每个类别一个BGR三元组的列表,颜色不重复。这样能一眼看出类别错位,比如绝缘子缺陷框是红的、发热缺陷框是绿的,如果发热缺陷跑到了绝缘子上,说明class_id和names没对齐。
4. VOC与YOLO互转:转换脚本的每个参数和三个隐蔽坑
4.1 XML转TXT:从读取到写入的最小脚本
很多数据集给了VOC标注,但你要用YOLO训练,所以转换必须自己做。标题说双格式,意思是两边都有,但实际用的时候总会遇到需要从一边补全另一边的情况。常见做法是先用XML转YOLO,再反向校验。
import xml.etree.ElementTree as ET from pathlib import Path # 顺序必须与data.yaml的names一致 classes = ["insulator_break", "overheat", "foreign_object", "oil_leak"] def convert_xml_to_yolo(xml_path, lbl_path): tree = ET.parse(str(xml_path)) 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: print("unknown class:", name) continue cls_id = classes.index(name) bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) x = (xmin + xmax) / 2 / img_w y = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x:.6f} {y:.6f} {w:.6f} {h:.6f}") lbl_path.write_text("\n".join(lines) + "\n")逻辑说明:classes列表的顺序直接决定所有类别id,这个顺序必须与data.yaml里的names保持一致。size节点是XML里自带的图像原始尺寸,比用PIL读图更可靠,前提是标注工具生成XML时把size写对。遇到某个缺陷类别没在classes里,直接continue会漏标;更严谨的做法是打印警告,并在最后统计遗漏数量。
参数说明:保留6位小数足够满足归一化精度。行尾的换行符在YOLO训练中不是必须,但部分脚本按行读时会习惯性strip,保留可以避免最后一行解析异常。如果一个XML里有多个目标,lines里会有多行,这也和TXT的每行一个目标规则对齐。
4.2 TXT转XML:归一化坐标回绝对坐标时的边界处理
反过来用的时候,常见于要把YOLO预测结果或者经过清洗的TXT重新导回VOC接口。逆转换不能简单乘回图片宽高,还要做clip,避免坐标越界和宽高为负。
def yolo_to_voc(txt_path, img_w, img_h): boxes = [] with open(txt_path) as f: for line in f: parts = line.split() if len(parts) < 5: continue cls_id = int(parts[0]) xc = float(parts[1]) yc = float(parts[2]) bw = float(parts[3]) bh = float(parts[4]) xmin = (xc - bw / 2) * img_w ymin = (yc - bh / 2) * img_h xmax = (xc + bw / 2) * img_w ymax = (yc + bh / 2) * img_h xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) boxes.append((cls_id, xmin, ymin, xmax, ymax)) return boxes逻辑说明:归一化坐标在极端情况下会等于1.0,乘回宽高后右边界等于图片宽度,很多VOC解析脚本再取整数时会越界,所以用img_w-1做上界。只做这一步就能避免画框时OpenCV报坐标超图像尺寸的错误。同样,底部用img_h-1防止坐标等于高度值。
参数说明:过滤len(parts)<5是为了跳过空行和损坏的标签行,防止一行只有类别id时把后续坐标错位。cls_id如果来自错误类别索引,这里不会报错,所以转换前仍然要确认TXT是和数据集的17类定义一致。
4.3 互转时最容易栽的三个具体坑
第一个坑是图片尺寸取错。有的数据包里jpg图片和XML里的size.width不一致,原因是标注工具对原图做了压缩但没更新XML。转换后所有框都偏小或偏移。排查方法:随机拿5个XML的size和PIL实际读出的尺寸对比,不一致就先修正再转换。
第二个坑是类名大小写不一致。比如XML里的name是Overheat,YOLO标签里可能是overheat。这个坑不会报错,但会让classes.index找不到元素,导致跳过缺陷或类别id整体后移。解决方式:统一小写后再匹配,匹配前用strip去掉首尾空格。
第三个坑是困难样本处理。有些标注工具会把难例标记为difficult或者ignore,转换脚本直接取所有object,最后TXT里多了困难样本,导致训练时把背景当缺陷;有些脚本只取第一个object,又漏标。建议转换时保留一个开关,默认跳过difficult=1的样本,而不是无脑全转。
# 在4.1的循环里增加判定 difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue逻辑说明:跳过困难样本能减少训练噪声,但如果标注规范里刻意把遮挡严重的样本标成difficult,这里跳过没问题;如果整个数据集没有difficult字段,这个判定也不会生效,兼容性更好。
5. 17类故障类型标注的边界问题:避坑清单
5.1 拿到17类先做类别清单与边界规则确认
标题说覆盖17类典型故障类型,但17类具体是哪17类、框怎么画,直接决定了模型效果。我的做法是把labels里所有txt的class_id枚举出来,和data.yaml的names做一次diff。常见构成通常包括绝缘子破损、发热、异物、渗漏这几类,但不同数据集抽样标准不同。绝对不要从名字猜边界,而是要看同类别5到10个标注框,归纳出标注者习惯。例如“发热缺陷”可能只框高温区域,也可能把整个设备本体框进去,这两种规则训练出来的模型语义完全不一样。
拿到双格式数据的好处就在这里:VOC的XML里带有类别名,YOLO的TXT里只有id,对照着看能快速确认每个id对应的真实语义。如果怀疑某个类别标注边界不统一,就把该类的XML全部画出来,按缺陷位置排序,能很快看出哪些框是异常大或异常小的。
5.2 踩坑1:类别索引漂移让训练标签错位
现象:训练loss正常下降,但验证mAP在几个类上极低,可视化画框时发现缺陷框全部画到了另一个目标上。
原因:VOC的XML里object name是字符串,转换脚本用classes列表索引赋值时,某个类别名拼写不一致,导致原本的类别1变成了类别2,整个id序列从那一行之后漂移。
解决:不要依赖肉眼核对,写一个校验函数,读取TXT后反向生成一个临时VOC文件,和原XML对比类别名与坐标差,超过两个像素就报警。如果能直接利用数据集里的VOC格式做对照,转换后每个框都应该在允许误差范围内。
5.3 踩坑2:双格式不同步,训练时标签少了一块
现象:YOLO训练过程中提示No labels found或者val集里某个类完全没有目标。
原因:原始包确实双格式都有,但某一次清洗只删了XML没删TXT,或者相反;还有可能是随机划分脚本对两个目录用了不同的文件列表顺序。
解决:把JPEGImages、Annotations、labels三个目录分别做stem集合,三方交集才是有效样本。划分前先跑一次三方对齐,缺任何一个文件就把这张图从训练集剔除,不要留半截数据。
5.4 踩坑3:小目标漏标是这类数据集的第一杀手
现象:训练完在真实巡检图上测试,绝缘子破损基本能测到,但远处的接线板缺陷和小区域发热点全部漏检。
原因:8307张图里有大量远距离拍摄,缺陷目标在640分辨率下只有20×20像素左右。标注者如果只框明显大缺陷,小目标标签数量太少,模型学不到。
解决:训练时把imgsz从640提高到896或1280,并打开多尺度训练;同时按目标像素面积统计框大小,把小于32×32的目标单独筛出来,在训练集里做几次过采样。如果数据集本身标注不够,只能后面补充。
5.5 踩坑4:背景纹理被当成缺陷约束了泛化
现象:训练集mAP不错,但在另一台相机或变电站角度下,误检率飙升,比如把绝缘子阴影当作破损。
原因:17类缺陷中有些类别和背景强相关,模型学到的不一定是缺陷本身,而是设备区域的纹理底色。
解决:做测试时不能只用同源的val集,至少留一组不同时间段或不同角度拍摄的图做外部验证。这属于数据集使用的延伸,但决定项目值不值得继续投入。
6. 把双格式用好的最后一个技巧:类别名映射与可视化验证
6.1 一个同时用上VOC和YOLO的验证脚本
当你训练完,输出是YOLO格式的预测结果,包含class_id、坐标和置信度。为了看清每个缺陷的语义,把class_id映射回VOC的name再画在图上。这个技巧不复杂,但很多人不做,导致分析预测结果时只看到红框,不知道是什么缺陷。
def map_pred_to_name(pred_txt, names, img_w, img_h): result = [] for line in pred_txt.splitlines(): parts = line.split() cls_id = int(parts[0].split("_")[-1]) # 视输出格式而定 conf = float(parts[4]) x1, y1, x2, y2 = [float(x) for x in parts[1:5]] result.append({"name": names[cls_id], "conf": conf, "box": [x1, y1, x2, y2]}) return result参数说明:不同YOLO框架输出的txt列序不同,有的把class_id放在第6列,所以要先看输出文件的表头再写解析代码。names直接从data.yaml读取,不要手写。我的习惯是每次训练结束当晚就生成一张拼接对比图,左边是ground truth的VOC标注,右边是模型预测,类别名各写在框上方。这张图比mAP数字更能暴露问题。如果发现某个类别的标注框本身存在系统性偏大,比如把整个设备都框进去了,就会把它单独拆出来重新讨论标注规则。8307张图的数据集并不算大,真正决定模型上限的不是图片数量,而是每张图的标注边界是否一致。希望帮到你。
本文还有配套的精品资源,点击获取