变压器红外测温过热点检测:VOC转YOLO与YOLOv8训练全流程 简介面向电力设备智能运维与计算机视觉研究者的变压器红外测温过热点检测数据集聚焦非接触式测温场景下的设备故障隐患识别包含600余张实际拍摄的变压器及套管红外图像其中200余张已由专业工具标注为过热点样本配套VOC格式XML标签可直接用于训练YOLO、Faster R-CNN等目标检测模型。压缩包共1344个文件含672张JPG红外图像与672个XML标签文件整体仅28.58MB轻量易下载便于学生、科研人员快速开展缺陷检测实验。已有794人学习使用。标签与图像一一对应方便划分训练集与验证集针对过热点温度差异小、普通模型易漏检的问题可结合翻转、裁剪等数据增强手段提升鲁棒性并借助迁移学习优化检测精度是红外测温与智能诊断结合的高价值实践资源。1. 变压器红外测温过热点检测600 多张图里为什么 200 张过热图最值钱变压器红外测温过热点检测落到日常运维里就是一句话拿着红外热像仪围着主变拍一圈回机房对着屏幕找哪个部位温度异常。过去这活儿靠老师傅眼睛看现在靠数据训练出来的检测模型。这套图像数据一共 600 多张测温图其中 200 多张是过热图像配套 VOC 标签每个过热点都用矩形框标好了位置和类别。对做电力巡检算法的人来说它最有价值的点不是总量大而是这 200 多张过热图把正样本、背景样本和边界情况都覆盖到了。用它跑通一条完整链路——读标签、转格式、训练 YOLO 检测器、验证集评估完全够用而且规模小、迭代快特别适合先验证方案再放量。下面按我拿到这类数据后的实际处理顺序把这套流程和踩过的坑一起讲清楚。2. 先读懂这批 VOC 标签过热点在红外图里到底怎么标、标得对不对我拿到任何一批标注数据第一步都是做数据体检而不是急着训练。目标只有三个确认类别是不是单一、框的尺寸分布合不合理、有多少张图是没有任何目标的负样本。这三个问题用一段脚本就能全查完但前提是先把 VOC 的结构看明白。2.1 过热点检测为什么用单类矩形框标注就够这批数据的标签结构很简单object 的 name 通常只有一个类别比如 hotspot。为什么不必做成实例分割因为过热点在红外图里的语义是“局部高温区域”在热像图上表现为高亮斑块边界天生是模糊的。它不像猫狗有清晰轮廓受环境温度、风速、拍摄距离影响同一个热区的边缘能差出十几个像素。这时候硬上分割标注不同标注员之间的一致性很难保证训练出来的模型反而学进噪声矩形框只要框住热区的主要高亮范围就能给检测器提供正确的监督信号。工程上检测框的作用是“提醒我去看这里”精确到像素不是必须。这里要顺带和另一类常见分析区分开变压器局部放电典型图谱处理的是放电产生的波形和谱图属于传感信号分析而过热点检测是纯视觉目标检测输入是二维热像图输出是框。两者数据形态、算法栈完全不同不要混用。那 400 来张没有 bndbox 的正常图是浪费吗恰恰相反。只有正样本的检测器会在“看到什么都没得框”的场景里乱出框把阳光直射造成的局部高亮、油位计反射误判成过热点。正常图提供了关键的背景监督。训练时模型会从这些图里裁负样本块学会区分“主变本体正常的样子”和“真正的热区”这一步决定了模型上现场后的误报率。2.2 VOC XML 逐字段拆解从 filename 到 bndbox随便打开一张图的标注文件结构是固定的annotation folderthermal_images/folder filenameIR_PM01_20240712_143502.jpg/filename pathE:/thermal/IR_PM01_20240712_143502.jpg/path source databasetransformer_thermal/database /source size width640/width height480/height depth3/depth /size segmented0/segmented object namehotspot/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin378/xmin ymin210/ymin xmax431/xmax ymax236/ymax /bndbox /object /annotation逐项说明。folder 是图像来源目录filename 是图像文件名path 有些工具会写绝对路径跨机器迁移时基本失效所以转换脚本只用 folder 和 filename 拼相对路径。size 里的 width、height 必须和实际图像像素一致depth3 表示三通道伪彩色 JPG如果原图是 16 位灰度 TIFF很多导出工具会写 depth1后面转格式时按 depth 做分支最稳妥。object 节点是核心。name 是类别名bndbox 里四个值是矩形框左上角、右下角的像素坐标。VOC 规范里坐标按 0 起始还是 1 起始不同标注工具实现不一致这是老坑用 LabelImg 导出的经常是 1 起始而多数训练框架读 VOC 时按 0 起始处理。单独一张图无所谓但整个数据集如果混用了两种工具产出的标签转换后所有框会整体偏移 1 到 2 个像素。truncated 表示目标是否被图像边缘截断difficult 表示目标是否难以辨认这两个字段对过热点检测影响不大但转换脚本别把它俩丢掉后面统计时有用。2.3 用一段 Python 把 VOC 标签全量读一遍类别数、热区尺寸、宽高比分布体检脚本长这样import os import xml.etree.ElementTree as ET from collections import Counter voc_dir VOC/Annotations # 存放所有 XML 的目录 cls_counter Counter() # 类别 - 目标个数 box_sizes [] # 每个框的 (宽, 高) aspect_ratios [] # 宽高比 empty_imgs 0 # 无目标图片数 for xml_name in os.listdir(voc_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_name)) root tree.getroot() obj_nodes root.findall(object) if not obj_nodes: empty_imgs 1 for obj in obj_nodes: cls_counter[obj.findtext(name)] 1 b obj.find(bndbox) xmin int(b.findtext(xmin)) ymin int(b.findtext(ymin)) xmax int(b.findtext(xmax)) ymax int(b.findtext(ymax)) w xmax - xmin h ymax - ymin box_sizes.append((w, h)) aspect_ratios.append(round(w / h, 2)) print(类别计数:, dict(cls_counter)) print(无目标图片数:, empty_imgs) print(框宽范围:, min(b[0] for b in box_sizes), -, max(b[0] for b in box_sizes)) print(框高范围:, min(b[1] for b in box_sizes), -, max(b[1] for b in box_sizes)) print(宽高比 TOP10:, Counter(aspect_ratios).most_common(10))逻辑说明用 xml.etree.ElementTree 依次解析每个 XML遍历所有 object统计类别计数、无目标图片数、框宽高范围和宽高比分布。跑完就能回答三个问题类别是否只有 hotspot 一个如果有拼写不一致的类别名训练前要先合并过热框是偏扁还是接近正方形决定后续 anchor 设计YOLOv8 虽然自适应学习但极端宽高比时早期收敛明显变慢框的尺寸有没有接近整张图宽高的异常值万一有 bug 或误标这里立刻暴露。参数说明os.listdir 会读出目录里所有文件必须用 endswith(.xml) 过滤解析用 ET.parse 而不是 ET.fromstring容错更好遇到损坏的 XML 会有明确报错行号方便定位是哪个文件。文件路径含中文时Python 3 默认按 UTF-8 处理Windows 下如果路径是 GBK 编码建议在读取前统一转码。体检脚本跑一遍再动转换脚本这是我处理所有 VOC 数据的固定顺序能省掉后面大量排查时间。3. 让这批数据直接开训VOC 转 YOLO 格式与数据集划分脚本看完标签只是第一步。当前主流检测 pipeline 默认吃 YOLO 格式的 txt 标注。虽然 YOLOv8 内置了 VOC 解析入口但转成 txt 之后跑起来更顺数据也能复用到以后任何一个框架里。转换本身不复杂真正的坑在坐标换算和数据集划分上。3.1 先说清格式差异VOC 的绝对坐标 vs YOLO 的归一化中心坐标维度VOC XMLYOLO txt坐标形式左上角 xmin/ymin 右下角 xmax/ymax中心点 xc/yc 宽高 w/h取值范围像素绝对坐标全部归一化到 0~1多目标多个 object 节点每行一个目标空图没有 object 节点生成空的 txt 文件YOLO 每行从左到右是 class_id、x_c、y_c、宽、高且中心坐标和宽高都要除以图像宽高。换算公式是x_c(xminxmax)/2/widthy_c(yminymax)/2/heightw(xmax-xmin)/widthh(ymax-ymin)/height。最容易错的点是有人把 xmax 直接当成框宽来归一化结果所有框都宽了一倍。另一个隐蔽错误是把 width 和 height 搞混导致框垂直方向被拉伸。3.2 VOC 转 YOLO 最小脚本下面这段脚本我每次都用逻辑上没有任何多余的步骤import os import xml.etree.ElementTree as ET voc_ann_path VOC/Annotations # XML 目录 jpeg_path VOC/JPEGImages # 图像目录仅用于二次确认 yolo_label_path labels # 输出的 YOLO 标签目录 os.makedirs(yolo_label_path, exist_okTrue) # 类别映射VOC 里的 name - YOLO 类别 id class_map {hotspot: 0} for xml_name in os.listdir(voc_ann_path): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(voc_ann_path, xml_name)) root tree.getroot() img_width int(root.findtext(size/width)) img_height int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue # 跳过不关心的类别 b obj.find(bndbox) xmin int(b.findtext(xmin)) ymin int(b.findtext(ymin)) xmax int(b.findtext(xmax)) ymax int(b.findtext(ymax)) x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) yolo_name os.path.splitext(xml_name)[0] .txt with open(os.path.join(yolo_label_path, yolo_name), w, encodingutf-8) as f: f.write(\n.join(lines))逻辑说明对每个 XML先取图像宽高再遍历 object 计算归一化中心坐标和宽高写入同名 txt。尺寸是从 XML 的 size 节点读的不重新读图这样 600 多张图转换只在毫秒级。但我建议先用前面第 2 章的体检脚本确认 size 字段可信再整批转换如果不放心可以抽 10 张用 PIL 读图交叉验证一次确认后再批量执行。参数说明坐标保留六位小数足够 YOLO 训练用多留无意义class_map 的 id 必须和后面训练 yaml 里的 names 顺序一致否则模型学到的类别对不上。无目标图片会生成空 txt 文件千万别删——YOLO 训练时如果找不到某张图的标签文件会直接报错或静默丢图空 txt 才是它认识的“这张图没有目标”。3.3 按设备划分数据集别让同一台主变同时出现在训练和验证集划分训练集、验证集时最忌讳直接对图片随机切分。如果同一台变压器在不同负荷时段拍的图有的过热、有的正常随机切分后训练集和验证集会出现“近亲”图——同一设备、同一角度、相似的散热结构。模型等于在背诵设备而不是学习过热语义验证集指标虚高一换设备就现原形。这个经验其实和作物病害图像数据集划分的道理一样只要数据来自同一个采样源就必须在更粗的粒度上划分否则验证集就失去意义。做法是按文件名里的设备编码分组把整个组切进同一个集合import os import random from collections import defaultdict # 约定文件名形如 IR_PM01_20240712_143502.jpg # 取前两段 IR_PM01 作为设备分组 key def device_key(filename): parts filename.split(_) return f{parts[0]}_{parts[1]} files [f for f in os.listdir(VOC/JPEGImages) if f.endswith(.jpg)] groups defaultdict(list) for f in files: groups[device_key(f)].append(f) group_names list(groups.keys()) random.seed(42) random.shuffle(group_names) train_files, val_files [], [] for i, g in enumerate(group_names): if i int(0.8 * len(group_names)): train_files groups[g] else: val_files groups[g] print(f训练集 {len(train_files)} 张验证集 {len(val_files)} 张)逻辑说明按设备分组后同一设备的所有图片只进入一个集合验证集数据在设备维度上对模型完全陌生。600 多张图推测来自几十个设备或拍摄场次按 8:2 比例切分比较合适。如果某台设备恰好贡献了过热图的很大比例要把它的整组留在训练集避免正样本骤降。参数说明random.seed(42) 固定随机种子保证每次划分结果可复现划分后还要按设备做一次去重检查确认训练集和验证集的设备编码集合不相交。这一步做扎实后面验证集的每个指标才值得信。3.4 转换后的自查用可视化脚本确认 bndbox 与热区位置一致转换完别急着训练先抽几张图画框看看否则后面翻车了很难定位是转换的问题还是训练的问题。import cv2 img_path VOC/JPEGImages/IR_PM01_20240712_143502.jpg label_path labels/IR_PM01_20240712_143502.txt img cv2.imread(img_path) for line in open(label_path): cid, xc, yc, w, h map(float, line.split()) x1 int((xc - w / 2) * img.shape[1]) y1 int((yc - h / 2) * img.shape[0]) x2 int((xc w / 2) * img.shape[1]) y2 int((yc h / 2) * img.shape[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(check_annotations.jpg, img)逻辑说明把 txt 里的归一化坐标还原回像素叠加到原图上保存肉眼确认框是否正好框住热区的高亮区域。这一步能发现两类问题一类是坐标系统差比如 x、y 没有分别按 width、height 归一化框会整体偏移另一类是原始标注质量差框根本不在热区上。第二类图我建议直接剔除出训练集而不是让模型硬学错误标注。600 多张图全部可视化不现实随机抽 20 张过热的、10 张正常的基本就能覆盖常见问题。4. 用 YOLOv8 训练过热点检测器关键参数与红外图预处理数据格式就绪后下一步是训练。过热点检测和通用目标检测最大的区别在输入图像的性质红外图不是自然图像它的颜色、亮度与温度物理量强相关预处理和数据增强都要按这个特殊性来调。4.1 红外图预处理灰度、CLAHE 还是保留原伪彩色红外热像仪输出常见两种形态16 位原始温度阵列像素值对应绝对温度8 位伪彩色图比如铁红、彩虹、白热调色板。标题里这套 600 多张测温图如果是 JPG 格式大概率是伪彩色或灰度 JPG温度信息已经被压缩成 8 位显示值。预处理有三条路直接保留伪彩色图训练让模型学颜色纹理。优点是热区在铁红调色板下视觉上很明显收敛快。缺点是泛化差——不同品牌热像仪、不同调色板颜色映射完全不同换台相机就失效。转灰度训练只保留亮度模式。高温区在灰度图里就是亮区符合“温度高亮度高”的物理直觉跨相机鲁棒性最好。缺点是会丢掉部分对比度信息。灰度加 CLAHE对比度受限的自适应直方图均衡增强热区边缘让过热点更突出。我的习惯是优先试灰度加 CLAHE。像素级实现很直接import cv2 def preprocess_thermal(img_path, modegray_clahe): img cv2.imread(img_path) if mode gray: return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if mode gray_clahe: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) return clahe.apply(gray) if mode pseudo_color: return img raise ValueError(funknown mode: {mode})参数说明clipLimit2.0 控制对比度增强强度太大会把正常区域也增强成假目标太小则热区边缘不清晰tileGridSize(8,8) 是 CLAHE 处理局部对比度用的网格尺寸对 640x480 图像这个值够用。别忘了 YOLO 训练时输入是三通道灰度图要用 cv2.cvtColor 复制成三通道或者训练配置里把 channels 改成 1二选一别混。4.2 训练命令与超参数从 nano 模型起步数据量只有 600 多张模型直接上 YOLOv8m 或 x 很容易过拟合。我一般从 yolov8n 起步先看能到多少基线再决定要不要换大模型。from ultralytics import YOLO # 预训练权重第一次运行会自动下载 model YOLO(yolov8n.pt) model.train( datatransformer_thermal.yaml, imgsz640, epochs200, batch16, lr00.01, mosaic0.5, hsv_h0.0, hsv_s0.0, hsv_v0.2, patience30, seed42, )配套的数据配置 transformer_thermal.yamlpath: /data/transformer_thermal train: images/train val: images/val test: images/test nc: 1 names: 0: hotspot参数说明imgsz 从 640 起调。原始图如果是 320x240 左右的小图640 输入意味着上采样热区边缘会变糊这时候降到 512 常常指标反而更好如果原始图分辨率高可以提到 960 或 1280对小热区更友好。epochs 给 200 配 patience30 早停实际跑多少取决于验证集何时不再涨600 张图通常几十轮就收敛不必死等。mosaic0.5 是 YOLO 系最强的数据增强但要注意它把四张图拼在一起如果恰好拼进两个过热区模型可能会学到“模糊的一团”而不是单个热区出现这种情况就降到 0.3。hsv_h 和 hsv_s 我直接设为 0色相和饱和度扰动在自然图像里是利器在红外伪彩图里是灾难——铁红调色板下高温区偏白偏黄把色相一扭模型会以为冷区域也是热区。hsv_v 保留 0.2模拟不同天气、不同拍摄距离造成的亮度变化。4.3 类别不平衡200 张过热图怎么防止过拟合600 多张图里只有 200 多张带框正样本框数不多这是小样本检测的典型场景。要注意的是这里的类别不平衡和图像分类的不平衡不一样正常图不是要删掉的冗余而是用来提供背景负样本。所以策略不是砍数据而是给过热图加量一是过采样训练时让模型每个 epoch 多看到几次过热图。Ultralytics 的 data.yaml 里对同一张图可以重复写路径或者用 repeat 参数控制简单有效。二是离线增强把 200 多张过热图翻转、旋转、调亮度扩到 300 到 400 张。但要记得增强必须和标签同步变换水平翻转后 bndbox 的 xmin、xmax 要换算成新坐标旋转 5 度后框会歪处理起来麻烦。我的建议是用在线的 mosaic、flip 让框架自己做离线增强只做水平翻转和亮度扰动这两种最容易同步的。三是验证集别留太多过热图。600 多张里切 20 到 30 张过热图进验证集够评估用了剩下的全进训练集。正样本本来就少验证集留多了训练集就更不够。4.4 评估指标mAP、precision/recall 还是误报率训练完看验证集的 mAP0.5 和 mAP0.5:0.95。这里有个认知偏差要注意mAP0.5:0.95 对红外小目标非常苛刻框稍微偏一点IoU 上不去分数就掉。而过热点检测落地时框不需要像素级精准因为热区边界本来模糊所以我的参考顺序是 mAP0.5 为主mAP0.5:0.95 作为稳健性参考。更实际的是看误报率。在验证集里单独统计正常设备图上的误检数量如果平均每张正常图出 0.5 个框现场巡检人员就得反复跑现场复核这系统没法用。出现高误报时优先调高置信度阈值比如从默认 0.25 提到 0.4 到 0.5代价是漏掉弱热区但弱热区可以交给下一层的温度阈值兜底。5. 训练前后的避坑记录五个值得回放的翻车现场下面五条都是我在类似小样本检测任务里实际踩过的坑。每条按现象、原因、解决的顺序写遇到同款问题直接对照。5.1 训练 loss 很低验证 mAP 也很高一换设备立刻掉一半现象训练集和验证集指标都很好模型下现场后换一批没见过的设备mAP 直接崩。原因数据集划分时按图片随机切分同一台主变不同时间拍的图一部分在训练集、一部分在验证集。模型记住了设备本身的散热结构和拍摄角度相当于“背诵”设备而不是学习过热语义。这在样本量只有几百张时特别严重。解决按设备或拍摄场次分组划分保证训练集和验证集的设备集合完全不相交。划分后写个小脚本检查设备编码是否有重叠这一步值得花十分钟。5.2 换一台热像仪或换个调色板模型全漏检现象训练集用的是 A 品牌热像仪的铁红调色板图换 B 品牌或换彩虹调色板原来能检出的过热点全部漏检。原因模型学到的是铁红调色板的颜色映射关系——高温区偏白偏红而不是“温度异常区域”这个物理概念。不同调色板下同一温度呈现的颜色完全不同色彩分布一变模型就认不出来。解决最稳的是把输入转灰度让模型只学亮度模式同时训练集里尽量多混几种调色板的图。如果手上只有单一调色板数据推理阶段对输入先做灰度化再送模型能明显改善跨相机泛化。5.3 套管顶部、引线接头的小热区全部漏检现象箱体中下部的大热区检得很好套管顶部、引线接头这类小面积热区几乎全漏。原因过热点检测的难点恰恰在这里——主变整体在图里占很大面积而套管顶部热区只有几十个像素。模型下采样后小目标特征在特征图里只占很小区域很容易被当成背景。解决一是增大输入分辨率imgsz 从 640 提到 960 或 1280小目标像素占比变大二是对小目标热区做切图把套管区域单独裁剪成训练图提升小目标样本权重三是关注 anchor 配置对着第 2 章体检脚本统计的框尺寸分布确保 anchor 覆盖到小框。5.4 晴天下午和阴天早晨拍的图表现差异大现象同一台设备、同一负荷状态白天和清早拍的图检测结果一个准一个不准。原因红外图本质是辐射温度的可视化环境温度、风速、拍摄距离都会影响整图的亮度和动态范围。背景温度高时过热点与周围对比度下降热区就不明显模型对绝对亮度敏感自然不稳定。解决训练时保留 hsv_v 亮度扰动模拟不同环境温度下的整体偏移或者对输入做局部对比度归一化CLAHE 比全局直方图均衡更稳。更彻底的办法是收集不同季节、不同负荷率的数据补进训练集这是治本。5.5 600 张图训练每次跑出的验证指标波动正负 0.05现象同一份代码、同一个数据集跑两次训练验证集 mAP 波动接近正负 0.05。原因600 多张图里真正带框的只有 200 多张验证集里的过热图可能只有 20 到 30 张。验证集太小一次随机采样把难易程度不同的图抽进去指标就跟着抖。解决固定 seed保证每次训练划分一致对外汇报指标时用 5 折交叉验证取均值而不是单次划分的单点结果。验证集太小这件事本身没法完全消除但至少要知道这个数字的置信区间有多大别把一个波动值当精确结论。6. 落地前的最后一步检测框加温度校验让结论可复核检测器输出一个过热框现场运维要的不是框而是结论。二次校验的标准做法是利用热像图本身的温度信息。如果原始文件是 16 位辐射图厂商 SDK 或 raw 转温度函数能给出像素灰度到温度的标定关系这时候取检测框内像素最大值反查温度再对照带电设备红外诊断规范里的温升判据来定结论如果手里只有 8 位 JPG绝对温度已经丢失退而求其次用框内区域与同一张图背景区域的灰度差做相对温升特征设经验阈值。相对判断往往比绝对判断更稳因为同批数据里相机设置、环境温度可能不同绝对值的可比性反而不如相对值。还有一个值得做的校准在小验证集上画出 precision-recall 曲线找到误报率可接受的置信度阈值。现场对误报很敏感正常设备图不能乱出框否则巡检人员每次都要白跑一趟复核。按我的经验默认 conf0.25 在单类小目标数据上容易偏低调到 0.4 到 0.5 能把绝大多数背景误检压掉代价只是漏掉几个弱热区——漏掉的弱热区本来就该靠温度阈值兜住两段式判断各有分工。我最终验收这套流程的习惯不是看单张图 mAP而是挑 30 张设备完全不在训练集里的新照片看模型出框后人工需要修正多少次。平均每张修正不超过一个框才算这套数据加这套训练流程真正能省人。这个标准比任何指标都直观希望帮到你。本文还有配套的精品资源点击获取