
简介面向电力设备视觉检测方向的科研人员、工程师与目标检测学习者这份文档型资料围绕一套4271张红外图像、14类电气设备标注数据集展开重点服务变压器及相关电力部件的识别与定位任务。资源以1个docx文档交付约1006KB集中说明Pascal VOC与YOLO双格式的组织方式、jpg图片与xml/txt标注对应关系以及ACB、CT、Connection、LA、LBS、MCCB、PT、VCB等类别划分和总框数11896的标注概况。文档还交代labelImg矩形框标注规则、类别框数分布及精度不保证等使用说明便于读者在训练前核对数据规格、理解标签含义并评估适用性。已有101人学习适合需要快速掌握该数据集结构与检测场景的读者查阅。1. 4271 张红外图值多少工时一个巡检识别项目的起步账前年冬天帮一个做变电站巡检的团队看模型效果他们卡在一个很实在的地方现场热像仪每天产出上千张图人眼一眼能从热像里挑出过热的接头模型换个站就崩。查下来问题不在骨干网络而在训练集只有三百多张自拍图、类别只标了两种。这类场景的解法往往落在数据侧——一份标注齐全、类别覆盖够广的红外图像数据集比换三个检测头都实在。这个电力场景电气设备红外图像变压器检测数据集4271 张图、14 类设备、11896 个矩形框VOC 与 YOLO 双格式并存补的正是这块短板。它适合两类人正在做电力设备目标检测、需要现成标注先把 baseline 跑起来的算法同学手上有红外巡检业务、想验证自动识别可行性的工程团队。2. 数据集拆解4271 张图、14 类标签与 11896 个框的分布拿到一个数据集我第一件事不是急着写 train.py而是把包解开、数一遍文件、画一张类别分布图。数完心里就有底这活儿是能训出东西还是会在长尾上翻车。这份数据集的骨架很规整图片、VOC 标注、YOLO 标注三份文件数量完全对齐都是 4271说明没有半途而废的标注残留这对后续写转换脚本省了很多事。2.1 文件组织jpg、xml、txt 三件套怎么摆压缩包解开后是典型的 Pascal VOC 布局外加一份 YOLO 格式的平行目录。里面不含 ImageSets 那种划分路径的 txt也就是说训练集/验证集的名单得自己生成这一点在动手前要清楚别指望拿现成的 train.txt。ir_power_dataset/ ├── JPEGImages/ # 4271 张 .jpg红外伪彩或灰度渲染图 │ ├── 000001.jpg │ └── ... ├── Annotations/ # 4271 个 VOC .xml与 jpg 同名 │ ├── 000001.xml │ └── ... ├── labels/ # 4271 个 YOLO .txt与 jpg 同名 │ ├── 000001.txt │ └── ... └── classes.txt # 14 行类别名行号即 class_id从 0 起判断一个数据集能不能直接喂给训练脚本我一般跑三行命令核对ls JPEGImages | wc -l、ls Annotations | wc -l、ls labels | wc -l三个数必须相等再用comm -3比一次文件名列表确认没有孤儿文件。红外数据集常见的一个毛病是同一设备同一角度连拍多张文件名带(1)、(2)后缀转换脚本里如果按stem拼路径就会找不到对应的 xml。这份数据集名字规整但检查这一步我从不省血泪经验是一次漏检让整个验证集指标虚高 6 个点。2.2 14 个类别与长尾分布谁是大头谁是稀有物种14 个类别的框数差了两个数量级这是这份数据最需要提前知道的信息。按框数排下来class_id类别名含义框数占比0ACB空气断路器940.79%1CT电流互感器7906.64%2Conection连接器拼写变体4713.96%3Connection连接器396133.30%4LA避雷器2672.24%5LBS负荷开关5484.61%6MCCB塑壳断路器160013.45%7MOF计量/互感器类2732.29%8PF电力附件560.47%9PT电压互感器6615.56%10VCB真空断路器1441.21%11body柜身3803.19%12connection连接器小写195216.41%13core铁芯6995.88%—合计—11896100%表格里最扎眼的是Conection、Connection、connection三个条目——语义上都是连接器/接线端子却因为标注时的拼写和大小写差异被拆成了三个类。PF只有 56 个框ACB94 个这两个类在 640 输入尺寸下如果目标本身还小训到后期 AP 大概率是 0 或者个位数。我的判断是先按原始 14 类跑一版当基线记录每类 AP再决定要不要合并成 810 类而不是一上来就改标签那样出了问题分不清是数据的问题还是改造的问题。2.3 标注文件长什么样xml 与 txt 的字段对照VOC 与 YOLO 表达的是同一个框只是坐标系不同。VOC 存绝对像素的左上、右下角点YOLO 存归一化的中心点与宽高。理解这一层写转换脚本时就不会在/w和/h上搞反。annotation filename000123.jpg/filename size width640/widthheight480/heightdepth3/depth /size object nameConnection/name bndbox xmin210/xminymin96/yminxmax388/xmaxymax302/ymax /bndbox /object /annotation# 000123.txt —— 一行一个目标class_id cx cy w h全部归一化到 0~1 3 0.467188 0.414583 0.278125 0.429167对应的换算关系是cx(xminxmax)/2/width、w(xmax-xmin)/width纵轴同理。这里有个容易忽略的点xml 里的size字段不一定和 jpg 实际尺寸一致有的标注工具在图片缩放后没回写尺寸直接信 xml 会让框整体偏移。稳妥做法是转换时用cv2.imread(...).shape读实际尺寸和 xml 记录的尺寸做一次比对不一致就报警并统一按实际尺寸换算。3. 跑通第一步VOC 转 YOLO 的转换脚本与训练配置数据集同时给了两套格式理论上 YOLO 训练只吃 txtVOC 那份是给 TensorFlow/Paddle 系或 MMDetection 用的。但我不建议直接拿包里的 txt 开训——class_id 的分配顺序你得自己确认一遍一旦和data.yaml里 names 的顺序对不上训练照样跑loss 照样降最后预测出来避雷器的位置标着柜身这种错误没有报错提示纯纯的黑匣子。3.1 类别映射别用 sorted() 偷懒定 class_id自己写转换脚本时第一步是把类别顺序钉死成一份列表转 xml 和生成 yaml 都用这一份绝不在两个地方各写一遍。用sorted(set(names))自动推导看着聪明实际会引入大小写排序问题——大写字母全部排在小写前面body和core会被挤到中间去和包内 txt 的编号完全错位。# classes.py —— 全项目唯一的类别定义来源 CLASSES [ ACB, CT, Conection, Connection, LA, LBS, MCCB, MOF, PF, PT, VCB, body, connection, core, ] NAME2ID {name: idx for idx, name in enumerate(CLASSES)} ID2NAME {idx: name for name, idx in NAME2ID.items()}这份列表的顺序是照抄数据集说明里给出的顺序ACB是 0 号core是 13 号。如果决定合并那三个连接器类别只需要在这一个文件里改其他地方全部引用NAME2ID改完跑一次全量转换即可不用满项目找散落的数字。参数上唯一要盯的是enumerate的起点YOLO 的 class_id 从 0 开始而有些老脚本按 VOC 习惯从 1 开始差一位就是全盘错。3.2 转换脚本越界裁剪与退化框过滤真正容易出问题的地方在坐标上。红外图像边缘经常只拍到半个设备标注的人手一抖就把 xmax 标到了图片宽度之外也有框被标成xmin xmax的一条线。这两种情况不处理写进 txt 后就是越界的归一化值训练时要么被框架静默丢掉要么在计算 loss 时炸出 NaN。# voc2yolo.py import os import xml.etree.ElementTree as ET import cv2 from classes import NAME2ID def convert_one(xml_path, txt_path, img_path): h, w cv2.imread(img_path).shape[:2] # 以图片实际尺寸为准 root ET.parse(xml_path).getroot() lines, dropped [], 0 for obj in root.iter(object): name (obj.find(name).text or ).strip() if name not in NAME2ID: # 不在白名单里的类别直接跳过 dropped 1 continue b obj.find(bndbox) x1, y1 float(b.find(xmin).text), float(b.find(ymin).text) x2, y2 float(b.find(xmax).text), float(b.find(ymax).text) # 越界裁剪先夹到图像范围内 x1, x2 max(0.0, min(x1, w)), max(0.0, min(x2, w)) y1, y2 max(0.0, min(y1, h)), max(0.0, min(y2, h)) bw, bh x2 - x1, y2 - y1 if bw 2 or bh 2: # 退化框宽或高不足 2 像素 dropped 1 continue cx, cy (x1 x2) / 2 / w, (y1 y2) / 2 / h lines.append(f{NAME2ID[name]} {cx:.6f} {cy:.6f} {bw/w:.6f} {bh/h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) return len(lines), dropped if __name__ __main__: root_dir /data/ir_power_dataset total_box, total_drop, empty_img 0, 0, 0 for fn in os.listdir(f{root_dir}/Annotations): stem os.path.splitext(fn)[0] n, d convert_one( f{root_dir}/Annotations/{fn}, f{root_dir}/labels/{stem}.txt, f{root_dir}/JPEGImages/{stem}.jpg, ) total_box n; total_drop d empty_img (n 0) print(f写入框 {total_box}丢弃 {total_drop}空标注图 {empty_img})这段脚本干了三件事按白名单过滤类别、把越界坐标夹回图内、丢掉宽高不足 2 像素的退化框。跑完打印三个数字正常情况是写入框数接近 11896、丢弃数为个位数如果丢弃数上百多半是某批图的标注尺寸和图片尺寸对不上需要回去查那批文件。empty_img也不能忽略标注工具过滤之后留下空 txt 的图仍会被加载虽然不算负样本错误但会让一个 batch 的有效梯度变少我一般把空 txt 对应的图一起从训练名单里剔掉。想省事直接用包里现成的 txt 也行那就跳过这一步但至少抽样把labels/里的 class_id 分布统计一次和上面的表对得上才放心。3.3 划分与 data.yaml一次写对后面都省事数据集没带 ImageSets切分得自己做。我固定用 8:1:1并且按类别分层抽样保证PF、ACB这类小类在验证集里至少有十几个框否则算出来的 mAP 波动大得没法看。# split.py —— 分层切分保证小类在验证集也有样本 import random, shutil from pathlib import Path from collections import defaultdict random.seed(42) root Path(/data/ir_power_dataset) stems [p.stem for p in (root / labels).glob(*.txt)] # 用每个文件里出现的类别作为分层键 bucket defaultdict(list) for s in stems: ids {line.split()[0] for line in open(root / labels / f{s}.txt) if line.strip()} bucket[tuple(sorted(ids))].append(s) train, val, test [], [], [] for key, items in bucket.items(): random.shuffle(items) n len(items) train items[: int(n * 0.8)] val items[int(n * 0.8): int(n * 0.9)] test items[int(n * 0.9):] for split, items in [(train, train), (val, val), (test, test)]: for sub in (images, labels): (root / splits / split / sub).mkdir(parentsTrue, exist_okTrue) for s in items: shutil.copy(root / JPEGImages / f{s}.jpg, root / splits / split / images / f{s}.jpg) shutil.copy(root / labels / f{s}.txt, root / splits / split / labels / f{s}.txt) print(len(train), len(val), len(test))分层键用的是这张图里出现了哪些类别的组合而不是单一主类别效果比随机切分稳。代价是组合数量可能很碎样本少的组合分出来只有一两张这没关系小类本来就要靠训练时的采样策略补切分只能保证不全落到训练集里。# data.yaml path: /data/ir_power_dataset/splits train: train/images val: val/images test: test/images names: 0: ACB 1: CT 2: Conection 3: Connection 4: LA 5: LBS 6: MCCB 7: MOF 8: PF 9: PT 10: VCB 11: body 12: connection 13: corenames 的编号必须和classes.py逐行一致包括大小写。检查方法很土但有效随便挑一张图把它的 txt 第一行读出来拿 class_id 去 yaml 里查名字再回头看 xml 里对应 object 的 name能对上就说明整条链路没错位。# 先在 640 分辨率上跑一版基线别急着堆 epochs yolo detect train modelyolov8s.pt datadata.yaml imgsz640 epochs100 \ batch16 workers8 device0 patience30 close_mosaic10参数取值说明modelyolov8s.pt小模型先验证数据可用性跑通再换 m/limgsz640红外小目标多显存允许可上 960 试一版batch168G 显存的安全值OOM 就降到 8patience30连续 30 轮没提升就早停避免过拟合小类close_mosaic10最后 10 轮关掉 mosaic让框回归收敛得更准第一版训练我只关心两件事loss 曲线是不是平滑下降以及val上的每类 AP 分布和框数分布是否一致。如果Connection的 AP 上到 0.9 而PF停在 0.01那是样本量的问题不是模型的问题别在那调学习率白费时间。4. 训练链路适配红外图像的数据加载与增强策略把数据塞进 YOLO 默认管线能跑但跑出来的模型往往在真实红外图上表现不稳定原因多半出在增强上。可见光的增强配方搬到红外场景有些操作是在帮倒忙——热像图的颜色是伪彩映射出来的不是物体本色HSV 扰动一上模型学到的可能是配色规律而不是设备形状。4.1 红外图像的三个脾气低对比度、伪彩失真、噪点第一是动态范围窄。红外相机的原始数据是温度映射成 8 位图之后柜体和背景的灰度差可能只有十几个灰阶肉眼看着一片糊模型的浅层卷积很难提出边缘。第二是伪彩。同一台设备在不同色带设置下可以是红黄也可以是蓝紫若训练集全是一种配色换台设备就废。第三是噪声热像仪的非均匀性和随机噪点在低对比度区域特别明显容易被误检成小目标。对应的策略我一般这么定用 CLAHE 提升局部对比度、随机灰度化打散伪彩依赖、亮度/对比度扰动控制在 ±0.2 以内、不用HueSaturationValue 这类强色彩增强。翻转可以留水平翻转垂直翻转要慎用——电力柜里的断路器、仪表安装是有明确上下方向的翻过来就成了现实中不存在的样本。4.2 自定义 Dataset把增强控制在合理范围如果要用 Ultralytics 之外的框架Dataset 得自己写。核心是三段读图统一成三通道、读 txt 组 boxes、走 albumentations 管线并保证 boxes 跟着变换。# ir_dataset.py import cv2 import numpy as np import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf A.Compose([ A.LongestMaxSize(max_size640), A.PadIfNeeded(640, 640, border_modecv2.BORDER_CONSTANT, value0), A.HorizontalFlip(p0.5), # 垂直翻转对柜内设备不合理关掉 A.RandomBrightnessContrast(0.2, 0.2, p0.5), # 模拟曝光差异幅度收窄 A.CLAHE(clip_limit2.0, tile_grid_size(8, 8), p0.3), # 拉开局部对比度 A.GaussNoise(var_limit(5.0, 20.0), p0.2), # 贴合热像仪噪点 A.ToGray(p0.1), # 随机去色降低伪彩依赖 ToTensorV2(), ], bbox_paramsA.BboxParams(formatyolo, label_fields[cls], min_visibility0.3)) class IRDataset(Dataset): def __init__(self, img_dir, lbl_dir, tf): self.img_dir, self.lbl_dir, self.tf img_dir, lbl_dir, tf self.names sorted(p.stem for p in img_dir.glob(*.jpg)) def __len__(self): return len(self.names) def __getitem__(self, idx): stem self.names[idx] img cv2.imread(str(self.img_dir / f{stem}.jpg)) # 伪彩图按 BGR 读 if img is None: raise FileNotFoundError(stem) cls, boxes [], [] for line in open(self.lbl_dir / f{stem}.txt): if not line.strip(): continue c, cx, cy, w, h line.split() cls.append(int(c)); boxes.append([float(cx), float(cy), float(w), float(h)]) out self.tf(imageimg, bboxesboxes, clscls) return { image: out[image], boxes: torch.tensor(out[bboxes], dtypetorch.float32).clamp(0, 1), labels: torch.tensor(out[cls], dtypetorch.long), }几个参数的取舍值得说明。min_visibility0.3是裁剪/翻转后框只剩不到三成面积就丢弃避免产生一堆几乎看不见的残框污染标签ToGray(p0.1)概率不高目的是让模型偶尔见到无颜色信息的输入学会靠形状判别CLAHE的clip_limit2.0是个保守值调到 4 以上会把噪声也一起放大热像图的噪点被强化后很像是小目标反而拉低精度。如果直接用 Ultralytics 训练等价做法是在命令行加hsv_h0.0 hsv_s0.0 hsv_v0.3 degrees0 fliplr0.5 flipud0.0把色彩相关的增强全关掉。提示红外数据集如果混了灰度和伪彩两种渲染务必确认读图时通道数一致。灰度图用cv2.imread默认读成三通道没问题但用cv2.IMREAD_GRAYSCALE读进来再送进需要 3 通道的骨干网络会直接报维度错误。4.3 评估怎么读每类 AP、混淆矩阵和漏检图只看一个 mAP50 是自欺欺人。这份数据 14 类里框数从 56 到 3961总 mAP 会被Connection和connection两个大类拉到 0.8 以上看着漂亮实际PF、ACB全军覆没。我通常按这个顺序看先拉每类 AP 表和框数表并排放找框数不少但 AP 极低的类这通常是标注质量问题再看混淆矩阵重点看那三个连接器类别之间是否互相串——串得厉害就是拼写分裂导致的最后抽 20 张验证集原图做可视化专门找漏检红外图里设备密集堆叠的区域漏检率明显更高这类样本要回流到训练集。验证命令跑完之后runs/detect/val/下会生成confusion_matrix.png、PR_curve.png和各分类的val_batchX_pred.jpg。我习惯把预测图按文件名排序后快速翻一遍翻到哪个类标错颜色就记下来比盯着数字有用。5. 避坑排查标签拼写、坐标越界与验证集泄漏的 5 个坑这一节是整篇里我最想让人先看的部分。上面那些步骤照着做基本不会出岔子真正耗时间的是跑起来之后指标不对劲、又找不到原因的场面。下面五条都是这类数据集上真实会撞见的按现象 → 原因 → 解决记下来。5.1 标签层面的三个坑坑一三个连接器类别互相抢框混淆矩阵一片红。现象是训练能收敛但Conection、Connection、connection三类的 AP 都在 0.4 上下徘徊混淆矩阵里它们三个互相大量错分同一张图上的接线端子一会儿被预测成 2 号一会儿成 12 号。原因是标注阶段不同人对同一个部件的写法不统一本质上是一个类被拆成了三份每份样本量都不够模型学不到稳定特征。解决办法是合并成一个类在classes.py里把这三项合成单个connector重跑转换脚本同时注意合并后body/core的编号会前移data.yaml的 names 必须跟着改两处一起改别只改一边。坑二训练到一半 loss 变 NaN或者某些框完全学不到。现象是前几轮 loss 正常下降十几轮之后突然炸成 NaN或者日志里出现大量被忽略的框。原因是原始 xml 里存在越界坐标和零面积框转换时没裁干净归一化后出现大于 1 的值或宽度为 0 的行。解决办法就是 3.2 节那段脚本里的截断逻辑转换完再补一道校验把所有 txt 读一遍统计cx1 or cy1 or w0 or h0的行数正常应该是 0不为 0 就说明那批 xml 有问题回去看是哪个标注批次。坑三某张图上有目标但 txt 是空的训练时框全丢。现象是验证时发现明明有设备的图模型一个框都不出。原因通常是标注工具里画了框但没保存或者类别名写成了表里没有的拼写比如把MCCB写成MCCB带了尾部空格转换脚本按白名单过滤时就静默跳过了。解决办法是在转换脚本里加一行日志把未命中白名单的类别名连同文件名打印出来跑一次全量转换就能把这个名单收齐另外strip()一定要加尾部空格是最隐蔽的一种。5.2 训练与评估层面的两个坑坑四验证集指标高得离谱换到现场就崩。现象是 val 上 mAP50 轻松上 0.9拿去跑另一批现场图连大类都认不全。原因是随机切分把同一台设备、同一角度、甚至是连拍的近似帧同时分进了训练集和验证集模型等于在背答案。解决方法是切分时按采集批次/设备编号分组同一组只进一个 split。如果文件名里没有批次信息就用图像相似度聚类把近邻帧归为一组再切。这一步多花半天能省掉现场调试的两周。坑五小类别 AP 长期为 0调参调不动。现象是PF56 个框、ACB94 个框从第一轮到最后一轮 AP 都是 0其他类正常。原因是这两类样本量太少且目标在图中往往偏小标准的随机采样下每个 batch 里几乎见不到它们。解决办法分三步先确认这两类的框确实存在且坐标正确用可视化脚本画出来看再开启类别加权采样或过采样小类图片最后考虑提高输入分辨率到 960让小平米目标在特征图上多占几个像素。如果三步做完 AP 还是 0接受它——56 个框本来就不足以支撑一个独立类别合并进语义相近的类更实际。注意这五条里坑一和坑四最容易被忽略因为它们的共同特征是训练过程完全正常、没有任何报错。前者错在标签后者骗在切分两者都不会在终端里给你一行红字。6. 二次开发类别重划、半自动预标注与难例回流把基线跑通只是起点真正落到业务里14 类这个粒度大概率要动。我一般的做法分三层推进每一步都用指标验证不凭感觉。第一层是类别重划。按业务需求把 14 类压成设备大类 关键部件比如断路器ACB/MCCB/VCB 合并、互感器CT/PT/MOF 合并、连接器三个拼写合并、柜身、铁芯、避雷器、负荷开关。合并后类别数从 14 降到 8 左右每个类的框数都在 300 以上PF这种 56 框的稀有类要么并入邻近类要么单独留出来做二阶段分类。改造方式是只动classes.py里的映射表加一个RAW2MERGED字典转换脚本里多查一次表原始 xml 一个字都不改随时可以回退。改造项改造前改造后验证方式类别粒度14 类8 类每类 AP 是否整体上移小类是否脱离 0输入分辨率640960小目标召回率、显存占用与推理耗时数据量4271 张4271 预标注复核新增样本中人工修正比例是否逐步下降第二层是半自动预标注。用训好的模型对未标注的现场图做推理置信度阈值卡在 0.4 左右把预测框直接写成 YOLO txt再人工过一遍只改错的不画新的。这一步能省掉标注成本的大头但有个细节要注意预标注生成的 txt 里混着模型的系统性偏差如果全部无条件接受下一轮训练只是在放大误差。我的经验是每天抽 10% 的预标注结果做人工复核并统计修正率修正率降到 5% 以下再考虑扩大自动接受范围。# prelabel.py —— 用已有权重给新图打预标注低置信度直接丢不留给人工纠结 from ultralytics import YOLO from pathlib import Path model YOLO(runs/detect/train/weights/best.pt) src, dst Path(/data/new_ir), Path(/data/new_labels) dst.mkdir(parentsTrue, exist_okTrue) for r in model.predict(sourcestr(src), imgsz960, conf0.4, iou0.5, streamTrue): stem Path(r.path).stem lines [] for b in r.boxes: c, x, y, w, h int(b.cls), *b.xywhn[0].tolist() lines.append(f{c} {x:.6f} {y:.6f} {w:.6f} {h:.6f}) (dst / f{stem}.txt).write_text(\n.join(lines))conf0.4是刻意压低的值宁可多给人看几个框也别漏掉真目标——漏掉的框人工根本发现不了多出来的框一眼就能删。imgsz960要和训练时保持一致尺寸不匹配会让小目标直接消失。第三层是难例回流。把验证集和现场测试里漏检、误检的图单独存一个目录每隔一轮训练就补进去同时在data.yaml里加一组hard路径做专项评估。这个循环跑上三五轮模型在现场的稳定性提升通常比换更大的骨干网络明显。一个教训我早期做这类改造时习惯一次性把类别、增强、分辨率全改完结果指标掉了也不知道是哪一项的责任只能全量回滚重来。从那以后我每次改数据集只动一个变量先跑 20 轮小实验看趋势确认有效再进全量训练改动记录写在classes.py的注释里隔两个月回来也能看懂自己当时干了什么。希望帮到你。本文还有配套的精品资源点击获取