ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

配电变压器检测图像数据集:3000幅VOC标注助你快速跑通目标检测

配电变压器检测图像数据集:3000幅VOC标注助你快速跑通目标检测 简介配电变压器检测图像数据集面向计算机视觉与电力巡检自动化方向的研究者提供约3000幅配网航拍图像及对应的VOC格式标注可用于训练YOLO、Faster R-CNN等目标检测模型帮助识别与定位配电变压器。资源包内共2000个XML标签文件压缩包大小282.42MB每个XML均记录变压器边界框坐标与类别信息便于直接构建训练集与验证集。该数据集贴近真实工业场景适合电力设备智能监测、故障预警等应用研究。目前已有599人学习下载。通过该资源可掌握VOC标注解析、目标检测模型训练与评估流程为自动巡检系统开发提供真实数据支撑也是验证检测算法性能的实用素材。1. 配电变压器检测图像数据集3000幅VOC标签图到底能解决什么做电网巡检目标检测时真正让人头疼的往往不是模型结构而是没有贴合场景的训练数据。配电变压器在巡检画面里占比小、外观相近、背景干扰多通用目标检测数据集很难直接派上用场。这个配电变压器检测图像数据集正是冲着这个缺口来的3000幅图像全部用VOC标签格式标注每一台变压器的位置、类别都在XML框里给出适合用来验证检测算法、做迁移学习起点也适合小团队先在本地把流程跑通再决定要不要继续投入采集和标注。这里有个反直觉的结论3000幅图不算多但比不少动辄几万张的通用数据集更适合电力场景落地。原因很简单通用数据集里的“变压器”要么数量太少要么来自各种非配电场景迁移后反而拉低精度。而这份数据把配电变压器单独拎出来目标明确标注格式又是工程界最熟悉的VOC拿来即用省掉大量洗数据和重标工作。适合的人也很清楚做输电巡检算法验证的工程师、刚接触目标检测的学生以及需要快速产出配电设备识别Demo的团队。2. 数据集构成与VOC标注细节读不懂XML就没法谈训练很多人在拿到VOC格式数据后第一反应是直接扔给训练脚本结果不是路径读不到就是类别名对不上。VOC格式本身不复杂但它有几个约定必须在动手前确认清楚否则后面每一步都会被带偏。2.1 目录与文件对应关系JPEG、XML与类别列表怎么配套这类配电变压器检测数据集最常见的目录布局是沿用Pascal VOC那套约定。图片放在JPEGImages目录下同名XML放在Annotations目录下ImageSets/Main目录里放train.txt、val.txt这类训练验证划分文件。拿到手的第一步不是去看图片长什么样而是先核对这三部分是否一一对应。建议先跑一段最基础的核对脚本把有图无标签、有标签无图、XML无法解析这三种情况全部筛出来。我之前接手过一批数据表面上看图片和XML数量一致结果里面有十几张图对应的XML文件内容是空壳只有annotation根节点没有任何object子节点。这类文件在训练时不会报错但会让模型学到不少空背景干扰还挺隐蔽。import os import xml.etree.ElementTree as ET img_dir JPEGImages xml_dir Annotations for xml_name in os.listdir(xml_dir): stem os.path.splitext(xml_name)[0] img_path os.path.join(img_dir, stem .jpg) xml_path os.path.join(xml_dir, xml_name) if not os.path.exists(img_path): print(f[缺失图片] {xml_path}) continue try: tree ET.parse(xml_path) objs tree.getroot().findall(object) if len(objs) 0: print(f[空标注] {xml_path}) except ET.ParseError: print(f[XML解析失败] {xml_path})这段脚本的逻辑很简单先按文件名前缀匹配图片和XML再检查XML是否能解析、是否包含object节点。参数方面img_dir和xml_dir按实际路径改就行图片后缀如果不是.jpg把第9行的后缀同步替换。跑完如果输出为空说明目录配套没问题可以进入下一步。多数人在这一步会偷懒跳过但这几分钟排查能避免后面训练到一半才发现数据文件损坏的尴尬。2.2 配电变压器场景的常见类别设计与标注粒度VOC标注里最重要的就是object节点下的name字段它决定了模型最终能识别什么。配电变压器检测数据集的类别设计通常不会只写一个笼统的“变压器”而是拆成更细的部件或状态比如变压器本体、套管、油位表、散热器、锈蚀区域等。拆得细的好处是模型不仅能定位设备还能辅助判断设备状态这对巡检场景很有价值。但类别粒度也直接决定了标注难度。如果一份标注里“变压器本体”和“散热器”经常重叠训练时模型容易在两个类之间摇摆。我在实际项目中一般会把目标分为三类来评估设备整体类、设备部件类、缺陷状态类。设备整体类适合做区域定位部件类和缺陷类适合做细粒度诊断。拿到这份3000幅的数据后建议先把所有XML里的name去重做一次类别频次统计看看每类样本量是否均衡。import os import xml.etree.ElementTree as ET from collections import Counter xml_dir Annotations counter Counter() for xml_name in os.listdir(xml_dir): xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) for obj in tree.getroot().findall(object): name obj.findtext(name) if name: counter[name] 1 for cat, cnt in counter.most_common(): print(f{cat}: {cnt})这段代码的核心是counts。输出后如果发现某个类别只有几十个样本而另一个类别有两三千个就要提前想好处理策略加权采样、复制粘贴增强或者干脆在初版模型里先合并该类。类别频次统计是关键模型loss设计、召回率评估、样本增强策略都以它为基础。很多人直接跳过这步就开始训练等到验证集里某个类AP为0才回头查已经浪费不少时间。2.3 为什么选择VOC标注而不是直接给COCO或YOLO格式拿到数据集时可能会产生一个疑问既然现在主流检测框架都支持COCO或YOLO的txt格式为什么还要用VOC这类相对传统的格式我个人的理解是VOC格式相当于一个“原始存储格式”它信息更完整转换成本低而且最容易人工检查和修改。VOC的XML里包含图片尺寸、目标类别、坐标、是否截断、是否难例等元信息这些信息在转换时可以按需取舍。而COCO的JSON体积大、手工编辑麻烦YOLO的txt是归一化坐标肉眼很难直接看出标注位置对不对。所以数据集的制作者把VOC作为交付格式是合理的使用者拿到后按自己的训练框架转换即可这也是为什么网上大量预处理脚本都以VOC为输入。把VOC当中间格式还有个额外好处如果后续要做类别合并或删除直接改XML比改JSON直观得多。这个环节的教训是不要因为VOC格式“老”就认为它过时。凡是能稳定描述目标框、能被各种脚本解析、能追溯原始信息的格式就是好格式。越接近原始保存状态的格式越不容易在转换链路上把信息搞丢。3. 训练前必做的转换VOC转YOLO与数据集切分VOC格式虽然便于人工检查却不能被大多数训练脚本直接消费。实际训练前我一般会把VOC转成YOLO的txt格式偶尔需要COCO JSON时再做一次转换。这一章给出转换脚本和切分方法重点说明坐标计算和数据划分这两个容易出错的环节。3.1 VOC转YOLO txt归一化坐标计算是整个流程的核心YOLO系列训练框架需要的是每个目标一行文本格式为“类别ID 中心点x 中心点y 宽度 高度”且四个坐标值都必须归一化到0到1之间。转换的核心是把XML里的绝对像素坐标换算成相对整张图的归一化坐标换算公式是中心点x(xminxmax)/2/图宽框宽(xmax−xmin)/图宽。import os import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, classes): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) out_lines [] for obj in root.findall(object): name obj.findtext(name) if name not in classes: continue cls_id classes.index(name) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) 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]做个截断更保险 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) out_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_path Path(out_dir) / (Path(xml_path).stem .txt) out_path.write_text(\n.join(out_lines), encodingutf-8) xml_dir Annotations out_dir labels classes [transformer, bushing, oil_gauge, radiator] os.makedirs(out_dir, exist_okTrue) for xml_name in os.listdir(xml_dir): if xml_name.endswith(.xml): voc_to_yolo(os.path.join(xml_dir, xml_name), out_dir, classes)这段脚本里的classes列表必须和训练配置里的类别顺序完全一致否则会出现类别ID错位。比如训练配置里把bushing排在第一个而脚本里把transformer排在第一个模型就会把套管当成变压器学习。这个错误不会报错只会让精度变得很奇怪。坐标截断那三行是防止目标刚好压在图片边缘时归一化值略大于1导致后处理出错。文本里的:.6f是保留六位小数对小目标检测来说精度足够没必要保留更多位。3.2 按类别均衡划分训练集与验证集随机切分容易翻车很多人在切分数据集时不假思索地随机抽取但配电变压器数据集往往类别分布不均。如果随机切分时把某个小类的样本全部划进训练集验证集里这一类的AP永远算不出来甚至报“找不到该类别”的错误。我习惯用分层抽样保证每个类在训练集和验证集中都有一定比例。import os import random from collections import defaultdict random.seed(42) xml_dir Annotations label_to_files defaultdict(list) for xml_name in os.listdir(xml_dir): xml_path os.path.join(xml_dir, xml_name) tree ET.parse(xml_path) names set() for obj in tree.getroot().findall(object): name obj.findtext(name) if name: names.add(name) key tuple(sorted(names)) label_to_files[key].append(os.path.splitext(xml_name)[0]) train_files [] val_files [] for key, file_list in label_to_files.items(): random.shuffle(file_list) val_count max(1, int(len(file_list) * 0.15)) val_files.extend(file_list[:val_count]) train_files.extend(file_list[val_count:]) with open(train.txt, w) as f: f.write(\n.join(train_files)) with open(val.txt, w) as f: f.write(\n.join(val_files))这段脚本按“该类组合”分组后按比例切分比单纯按单类切分更稳。val_count取max(1, 15%)是为了防止某个类组合只有一两张时验证集直接为空。固定random.seed(42)很关键否则每次运行切分结果都不一样后续实验对比就无法保证公平。train.txt和val.txt里写的是文件名前缀训练框架加载时通常会自动拼接图片和标注路径。3.3 转换结果自检把标注框画回图像看一眼转换脚本写完输出文件看起来也生成了但坐标方向、类别名有没有错位光看txt文本很难发现。我的做法是挑20到30张图把转换后的txt标注框画回原图保存为可视化结果人工快速过一遍。import cv2 def draw_yolo_label(img_path, txt_path, classes, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_center float(parts[1]) * w y_center float(parts[2]) * h bw float(parts[3]) * w bh float(parts[4]) * h x1 int(x_center - bw / 2) y1 int(y_center - bh / 2) x2 int(x_center bw / 2) y2 int(y_center bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(out_path, img) draw_yolo_label(JPEGImages/sample.jpg, labels/sample.txt, [transformer, bushing, oil_gauge, radiator], check/sample.jpg)自检时重点看三处框是不是明显偏移、框是否小于目标实际范围、类别文本是否和目标实物匹配。如果一个框整体往左上方偏了半个身位多半是归一化公式用错如果框的宽或高明显不对多半是宽高计算时把坐标顺序搞反了。这步自检虽然机械但能省下后面训练时的排障时间。4. 用3000幅数据把检测模型跑通最小训练流程与参数调整数据和转换都准备好了接下来是训练环节。很多人一上来就想调大模型、堆参数但小数据集更看重的是流程能不能平稳跑通、指标能不能反映真实水平。这一章按最小可行方案来写先跑通再优化。4.1 训练数据与类别配置文件先让框架认识你的数据以YOLO系检测框架为例训练前需要准备两个文件一个是数据配置文件写清楚训练集路径、验证集路径和类别数量另一个是模型配置文件写清楚结构或选择预训练权重。数据配置文件的格式在不同版本里略有差异但核心字段一致。常见写法是train: /data/train.txt val: /data/val.txt nc: 4 names: [transformer, bushing, oil_gauge, radiator]train和val字段指向的就是上一章切分生成的txt文件nc是类别数names列表必须和转换脚本里的classes顺序一致。这里最容易犯的错是names顺序和classes不一致或者nc填错训练时不报错但预测结果全乱。我一般在写完配置文件后会先打印一行“类别顺序校验”把两个来源的顺序打出来对比一下确认无误再跑训练。4.2 模型选择与训练参数图像尺寸、batch、学习率怎么定3000幅图的数据量不建议一开始就上最大最重的模型。常见做法是先选一个基础版本作为基线比如输入尺寸640预训练权重用检测模型自带的那套。这样跑一轮的速度快显存占用也能被大多数显卡接受。表里是我在这种数据集上经常用的初始参数。参数推荐起点调整建议输入图像尺寸640x640目标太小就升到960否则保持640batch size16显存不够降到8但学习率也对应降低初始学习率0.01收敛不稳就降到0.001训练轮数100看验证集mAP不再上升就提前停数据增强mosaic、随机翻转小目标多时关闭重mosaic避免切碎目标冻结训练前50轮冻结骨干数据少时能防止过拟合且加速收敛图像尺寸的选择直接决定小目标能不能被检出。配电变压器在巡检图中往往只占200x200像素甚至更小如果输入尺寸降到416目标区域可能只剩几十个像素特征被压缩得很严重。所以我一般从640起步如果验证集显示小目标AP明显偏低再尝试输入960代价是训练时间变长。batch size和学习率需要联动调整batch越大学习率可以适当调高但3000张图的数据量本身不大不用刻意追求大batch。运行训练的命令比较直接核心是把数据配置文件和预训练权重传进去。epoch先设到100打开早停机制关注验证集指标。训练过程中不要频繁改参数每次只改一个变量否则出了问题根本不知道是哪一步导致的。4.3 训练后验收别只看总mAP要看每个类别的AP分布训练结束时框架会输出一张指标表和一系列曲线图很多人只看最后的mAP数字觉得差不多就收工了。但在配电变压器检测场景里总mAP高很可能是因为“变压器本体”这个大头类做得好而“油位表”“锈蚀区域”等小类可能AP接近0。所以验收时我会把每个类别的AP单独列出来看。import numpy as np # 假设从验证结果文件里读出了各类别AP ap_dict { transformer: 0.92, bushing: 0.78, oil_gauge: 0.31, radiator: 0.55, } for cat, ap in ap_dict.items(): print(f{cat}: {ap:.2f}) if ap 0.5: print(f - {cat} 类别偏弱需要补数据或调权重)如果发现某类AP明显低于平均优先排查的不是模型而是数据该类样本数是否太少、标注框是否偏差大、该类别在验证集里的样本是不是多为极端角度。类别AP分布是数据质量最直接的镜子先把数据问题解决再谈模型优化。5. 配电变压器检测数据集训练避坑记录四个高频翻车现场与解决训练这类数据集时我踩过不少坑也帮别人排查过不少奇怪现象。下面四条是最常遇到的按“现象、原因、解决”的思路写清楚供直接对照。5.1 训练Loss到一半突然变NaNXML里混进了空类别名或非法坐标现象训练到某个epoch时loss变成NaN之后指标全部失效重启训练后可能在同一个epoch附近再次崩溃。原因转换脚本遇到某个XML中object的name字段为空或者bndbox坐标写成0到图片宽之外的异常值。归一化后可能出现负值或无穷值模型反向传播时梯度就炸了。解决转换前对XML做全量清洗遇到空name直接跳过该目标遇到xmax小于xmin的非法框先打印出来人工检查。for obj in root.findall(object): name obj.findtext(name) if not name or name : print(f[跳过空类别] {xml_path}) continue xmin float(box.findtext(xmin)) xmax float(box.findtext(xmax)) if xmax xmin: print(f[非法框] {xml_path}: xmax{xmax}, xmin{xmin}) continue这个习惯能用一分钟代码避免几小时的排障。每一批新拿到的数据都值得先做一次这样的异常框检查尤其是由外包标注团队生成的数据坐标手滑填错并不罕见。5.2 推理框整体偏移目标被框住但位置不准坐标归一化时用错了图宽高现象模型训练的loss正常验证mAP也不算差但画出来的框总是偏左或偏上半个身位看起来像所有框统一平移了一段距离。原因VOC的XML尺寸字段和实际图片尺寸不一致。有些数据集在标注时用了缩略图XML里存的size是缩略图的宽高但实际训练加载的是原图导致归一化坐标被错误还原。解决转换前先读取实际图片尺寸以实际尺寸为准做归一化不信任XML里的size。import cv2 img cv2.imread(img_path) actual_h, actual_w img.shape[:2] xml_w int(size.find(width).text) xml_h int(size.find(height).text) if actual_w ! xml_w or actual_h ! xml_h: print(f[尺寸不一致] {xml_name}: XML{xml_w}x{xml_h}, 实际{actual_w}x{actual_h})这个问题隐蔽在训练指标上基本看不出来只能靠可视化检查发现。如果发现所有框统一偏移第一时间就去对比图片实际尺寸和XML尺寸大概率能直接命中。5.3 少样本类别AP一直为0类别不平衡被验证集放大现象训练结束后某个类别的AP在验证集上始终是0但该类在训练集里确实存在。原因该类样本总量太少比如只有30个框其中大部分在训练集验证集只有两三个预测时只要稍微偏差一点IoU不达标就被判为漏检AP自然趋近0。解决先把切分改为分层抽样确保验证集里有该类样本再考虑对少样本类别做复制粘贴增强或过采样最后才考虑调整损失函数权重。顺序不能反数据层面能解决的问题不要绕到损失函数里去硬调。5.4 两次训练结果差异巨大验证指标忽高忽低切分种子和随机种子没固定现象同样一份数据参数完全一样第一次训练mAP0.75第二次变成0.68且验证集曲线波动剧烈。原因切分数据集时没有固定随机种子两次训练用了不同的训练验证划分或者训练框架没固定全局随机种子数据加载顺序变化导致结果波动。解决切分时固定seed训练命令里固定框架的随机种子。实验对比的commit里也要记录seed值否则后续想复现结果都没办法。这也是实验规范性的底线数据量越小随机性影响越大3000张图的数据尤其明显。6. 在3000幅数据之上再进一步难例挖掘与数据闭环把基础流程跑通之后接下来的提升点不在模型结构上而在数据闭环上。我习惯的做法是每次训练后把验证集里预测置信度不高但真实存在的目标以及置信度很高却完全预测错的目标单独导出成难例清单按批次人工过一遍。这个习惯看起来原始但对小数据集非常有效。值得投入的下一步工作是围绕这些难例做定向补充。比如验证集里发现大量“逆光环境下的变压器本体”漏检那就收集几十张同类图像补充到训练集发现“套管被散热器遮挡”导致误判就针对这类遮挡角度做复制粘贴增强。3000张图只是一个起点真正决定检测上限的是你能否把现场反馈源源不断地变成新的标注样本形成迭代闭环。我在做这类项目时最后的验收标准永远不是训练集上的精度而是拿一批从未见过的现场照片让模型跑一遍统计漏检和误检。每次跑完把失败样本截下来按原因分类建文件夹两三个月后回看会发现早期那些让人头疼的场景样本已经被数据闭环慢慢补上了。数据驱动不是一句口号而是每天多花十分钟把错题收集起来再把它变成下一轮训练的数据。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进