资讯详情

资讯详情

变电站设备缺陷检测:8307张VOC+YOLO双格式标注数据集实战

简介面向电力系统智能巡检场景的变电站缺陷图像数据集含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张VOCYOLO双格式标注图覆盖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, funknown_{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(parentsTrue, exist_okTrue) (lbl_dst / split).mkdir(parentsTrue, exist_okTrue) 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_id0是insulator_breakdata.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以内先看趋势imgsz640batch按显存调整预训练权重用通用权重。完整命令用YOLO CLI很短yolo detect train datadefect.yaml modelyolov8n.pt epochs150 imgsz640 batch16 device0 workers4 patience20 projectdefect_runs namebase参数说明patience20表示验证指标20轮不涨就早停电力缺陷样本少时能省时间。workers4不是越大越好数据读取太快而GPU没那么强时反而会让CPU占满。batch16在显存不大时可降到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是OverheatYOLO标签里可能是overheat。这个坑不会报错但会让classes.index找不到元素导致跳过缺陷或类别id整体后移。解决方式统一小写后再匹配匹配前用strip去掉首尾空格。第三个坑是困难样本处理。有些标注工具会把难例标记为difficult或者ignore转换脚本直接取所有object最后TXT里多了困难样本导致训练时把背景当缺陷有些脚本只取第一个object又漏标。建议转换时保留一个开关默认跳过difficult1的样本而不是无脑全转。# 在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张图的数据集并不算大真正决定模型上限的不是图片数量而是每张图的标注边界是否一致。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →