发电厂指针仪表数据集xml标注与yolov5训练全流程

📅 发布时间:2026/10/2 20:49:22
发电厂指针仪表数据集xml标注与yolov5训练全流程
简介这份资源是面向电力行业智能化改造与计算机视觉学习者的发电厂指针仪表数据集可用于目标检测模型的训练与验证帮助解决工业场景下仪表自动读数与定位问题。压缩包共2000个文件约84.72MB其中1214个xml标注文件对应785张jpg仪表图像另有1个txt说明文件xml可直接对接主流检测框架jpg覆盖多种表盘与拍摄角度。目前已有100人学习下载。数据集图像来自发电厂实际环境标注规范适合直接用于YOLOv5等网络训练读者可据此完成从数据加载、模型训练到读数提取的完整流程并参考作者博客中的效果展示与调参建议快速复现检测结果。对于研究工业仪表识别、希望积累真实场景数据的中高级开发者而言这份资源能节省采集与标注成本也可作为算法对比和系统集成的实践基础。1. 发电厂指针仪表数据集xml 标注为什么比想象中难做发电厂集控室里那些老式指针仪表压力表、温度表、电流表读数全靠人眼巡检。想上自动识别第一步不是选模型而是搞到一份能直接训练的发电厂指针仪表数据集而且标注格式是xml。很多人以为拍几张表盘照片、拉个框就完事真做起来才发现指针是细长目标表盘有反光刻度数字小到几十像素xml 里的 bbox 稍微偏一点后面角度回归就全歪。这份数据集要解决的核心问题是把「表盘位置 指针方向 量程刻度」这三件事在 xml 里标清楚让检测模型和读数算法能接得上。适合两类人一是做电力巡检视觉落地的工程师二是手里有 yolov5、yolov8 训练经验、想切到工业仪表场景的算法同学。下面按「数据怎么来、xml 怎么标、怎么转训练格式、坑在哪」一路讲透。2. 指针仪表 xml 标注体系从表盘到指针的字段设计2.1 为什么工业仪表场景优先选 xml 而不是纯 txtxml 格式在目标检测里不算新潮coco 用 jsonyolo 用 txt但工业仪表数据集偏偏常见 xml原因很实际。第一xml 天生带层级和属性一个object里可以同时塞name、bndbox、pose、truncated指针这种需要额外记录「指向角度」的目标用属性扩展比 txt 一行五个数灵活得多。第二发电厂项目验收时甲方往往要一份人能读的标注文件xml 打开就是结构化文本txt 得对着类别文件猜。第三很多标注工具labelImg 这类默认导出就是 pascal voc 的 xml现场标注员上手快。但 xml 也有代价文件体积大一个表盘一张图一个 xml几千张就是几千个小文件训练前必须转成模型吃的格式。所以我的做法是——xml 作为标注和归档的母格式训练时脚本转 yolo txt 或 coco json母格式不动转换脚本可重跑。这样标注返工不用重标只重跑转换。字段设计上我一般定这几类类别名标注对象bndbox 要求额外属性dial表盘外框贴紧表盘圆形外接矩形无pointer指针覆盖指针从轴心到针尖pose 记大致朝向scale刻度数字区域框住数字串无center指针轴心小方框边长约表盘 1/10无center这个类容易被忽略但读数算法要靠轴心和针尖连线算角度轴心标不准角度误差直接放大。我见过有人只标 pointer结果后处理阶段用 bbox 中心当轴心圆形表盘上偏几个像素量程换算就错一格。2.2 采集阶段发电厂现场拍照的四个硬约束数据集质量七成取决于采集。发电厂环境有几个绕不开的约束直接决定后面 xml 好不好标。光照。集控室有顶灯、有仪表自发光、有窗户侧光表盘玻璃还反光。我的经验是每个仪表至少采三种光照正常顶灯、关部分灯、手电侧照。反光严重的角度直接弃掉别指望标注能救回来。角度。指针仪表是圆的正对拍最理想但现场机柜排列不允许。俯仰角控制在 30 度以内偏航角尽量正对。超过这个范围表盘变椭圆刻度非线性压缩xml 框出来的指针角度和真实读数对不上。距离与分辨率。指针针尖宽度在图上至少要有 3 到 5 个像素否则检测框抖动大。按这个反推如果表盘直径 100mm相机到表盘 0.5m用 200 万像素相机拍表盘大概占 400 像素针尖约 4 像素勉强够。想稳一点表盘在图上占 600 像素以上。数量分布。别只拍一种表。压力表、温度表、电流表、电压表表盘颜色、指针粗细、刻度密度都不同。每类至少 200 张起步不同光照角度各占三分之一。总量 1000 到 3000 张是比较现实的起点再多边际收益递减。采集完做一次去重和模糊过滤。用拉普拉斯方差粗筛阈值我一般设 100低于这个的图先人工看一眼多半是糊的。import cv2 import os def blur_score(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return -1 # 拉普拉斯方差越小越模糊 return cv2.Laplacian(img, cv2.CV_64F).var() src_dir ./raw_images for name in os.listdir(src_dir): if not name.lower().endswith((.jpg, .png)): continue score blur_score(os.path.join(src_dir, name)) # 低于 100 标记为待复核 if score 100: print(fblur? {name} score{score:.1f})这段脚本遍历原始图算拉普拉斯方差。cv2.Laplacian的CV_64F是输出深度别用CV_8U否则负值被截断方差偏小。阈值 100 不是铁律光照暗的图整体方差低会误杀所以输出的是「待复核」而不是直接删。真正删图前一定人工过一遍这是血泪经验——我有次批量删了 200 张「模糊」图后来发现是傍晚低照度但指针清晰的样本补拍花了两天。2.3 xml 标注实操labelImg 配置与指针框的边界处理标注工具用 labelImg 就够它原生输出 pascal voc xml。安装和启动pip install labelImg labelImg ./images ./annotations/predefined_classes.txtpredefined_classes.txt里一行一个类名按前面表格写dial、pointer、scale、center。启动后切到 PascalVOC 模式左侧按钮标注完CtrlS存 xml。指针框的边界处理是难点。指针是细长斜目标框太紧训练时正样本少框太松把表盘背景框进来角度回归被污染。我的规则是框的短边至少覆盖指针宽度加 2 像素长边从轴心外沿到针尖外沿。斜指针的 bbox 会包含大量背景三角形区域这是正常的检测阶段只关心框的位置和类别角度靠关键点或后处理算。轴心center框我要求标注员放大到 400% 再画边长取表盘直径的十分之一左右。太小了训练时容易被 NMS 滤掉太大了和 dial 重叠严重。标完一批做一致性抽检随机抽 10% 的 xml用脚本把框画回图上人眼核对。这一步能抓出「框歪了」「类别选错」「漏标指针」三类高频错误。import xml.etree.ElementTree as ET import cv2 def draw_voc(xml_path, img_path, out_path): tree ET.parse(xml_path) root tree.getroot() img cv2.imread(img_path) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) x1 int(float(box.find(xmin).text)) y1 int(float(box.find(ymin).text)) x2 int(float(box.find(xmax).text)) y2 int(float(box.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, name, (x1, max(y1 - 5, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(out_path, img)ET.parse读 xmlfindall(object)拿所有目标坐标用float再转int因为有些工具会写小数。画框颜色固定绿色类别名写在框上方max(y1-5,10)防止贴顶时文字出画。抽检时把原图和画框图并排看一眼能看出问题。3. 从 xml 到可训练格式转换脚本与参数校验3.1 xml 转 yolo txt坐标归一化与类别映射yolov5、yolov8 吃的是每张图一个 txt每行class_id cx cy w h全部归一化到 0 到 1。转换脚本要处理三件事类别名到 id 的映射、坐标归一化、越界裁剪。import xml.etree.ElementTree as ET import os CLASSES [dial, pointer, scale, center] CLASS_TO_ID {c: i for i, c in enumerate(CLASSES)} def voc_to_yolo(xml_path, img_w, img_h, out_txt): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_TO_ID: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 裁剪到图像范围内 x1 max(0.0, min(x1, img_w)) x2 max(0.0, min(x2, img_w)) y1 max(0.0, min(y1, img_h)) y2 max(0.0, min(y2, img_h)) if x2 x1 or y2 y1: continue cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{CLASS_TO_ID[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines))CLASSES顺序必须和训练时data.yaml里的names完全一致错一位整个训练报废。裁剪那几步是防标注越界labelImg 偶尔会画出图外的框。x2 x1的判断过滤掉零面积框。归一化保留 6 位小数够用yolo 内部还会再处理。图像宽高从哪来别信 xml 里的size字段有些工具写的是原始尺寸图被裁过就对不上。老老实实用cv2.imread读实际尺寸或者提前把尺寸表存成 json 查。3.2 划分训练验证集别让同一块表盘跨集泄漏划分数据集有个隐蔽的坑同一个仪表的多张照片如果一部分进训练集、一部分进验证集验证指标会虚高因为模型见过这块表盘。正确做法是按仪表实例划分同一块表的所有照片只进一个集合。做法是采集时给每块表编号文件名带编号比如P001_01.jpg、P001_02.jpg。划分脚本按编号前缀分组再按 8:1:1 分训练、验证、测试。import os import random from collections import defaultdict def split_by_meter(img_dir, out_dir, ratios(0.8, 0.1, 0.1)): groups defaultdict(list) for name in os.listdir(img_dir): if not name.lower().endswith((.jpg, .png)): continue meter_id name.split(_)[0] groups[meter_id].append(name) meters list(groups.keys()) random.seed(42) random.shuffle(meters) n len(meters) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: meters[:n_train], val: meters[n_train:n_train n_val], test: meters[n_train n_val:], } for split, ids in splits.items(): os.makedirs(os.path.join(out_dir, split), exist_okTrue) for mid in ids: for fname in groups[mid]: print(f{fname} - {split})defaultdict(list)按前缀聚合random.seed(42)保证可复现。比例 8:1:1 是常规起点仪表数量少于 50 块时验证集可能只有几块表指标波动大这时候改成 7:1.5:1.5 或者做交叉验证。划分结果要落成文件清单训练脚本按清单读别靠目录扫描否则中间加删图会乱。3.3 转换后的三重校验数量、坐标、可视化转完不校验等于埋雷。我固定做三重校验。第一重数量对账。xml 里的 object 总数应该等于所有 txt 行数之和。差一个都要查。import xml.etree.ElementTree as ET import os def count_objects(xml_dir): total 0 for name in os.listdir(xml_dir): if not name.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, name)).getroot() total len(root.findall(object)) return total def count_lines(txt_dir): total 0 for name in os.listdir(txt_dir): if not name.endswith(.txt): continue with open(os.path.join(txt_dir, name)) as f: total sum(1 for line in f if line.strip()) return total print(xml objects:, count_objects(./annotations)) print(txt lines:, count_lines(./labels))第二重坐标范围。归一化后所有值应在 0 到 1 之间w、h大于 0。写个断言扫一遍越界的打印文件名和行号。第三重可视化。把 yolo txt 画回图上和 xml 画框图对比确认转换没丢目标、没偏位。这一步最直观也最容易发现类别映射错误——比如pointer画成了scale的颜色。提示三重校验脚本建议固化成check_dataset.py每次改标注或改转换逻辑后重跑别靠记忆。4. 指针仪表数据集避坑五条现场踩出来的记录4.1 坑一反光导致指针「消失」xml 标了但模型学不到现象训练 loss 正常下降但验证时反光表盘的指针检测框时有时无。原因标注时人眼能透过反光看出指针但像素上指针和反光融成一片模型没有可学特征。解决采集阶段就弃掉强反光样本标注阶段遇到反光遮挡指针超过三分之一的图直接标记为无效图移出数据集。别心疼留着只会拉低指标。4.2 坑二xml 的 size 字段和实际图像尺寸不一致现象转 yolo txt 后框整体偏移或缩放。原因图像被裁剪或缩放后xml 里的width、height没更新转换脚本用了 xml 的尺寸做归一化。解决转换时一律用cv2.imread读实际尺寸忽略 xml 的 size 字段。如果标注流程里确实会裁图裁完必须重新生成 xml 或至少更新 size。4.3 坑三指针框画成「最小外接矩形」导致角度信息丢失现象读数误差大尤其斜向指针。原因标注员用旋转框思维画了紧贴指针的斜矩形但 voc xml 只存水平外接矩形斜指针的框变成一个大正方形轴心到针尖的方向信息被抹平。解决明确要求画水平外接矩形角度信息靠center和pointer两个框的相对位置在后处理里算或者额外用关键点标注针尖和轴心。4.4 坑四类别不平衡scale 类样本远多于 pointer现象模型对刻度数字检测很准指针漏检多。原因一张表盘上刻度数字可能有十几个框指针只有一个类别数量差一个量级。解决训练时给pointer类加权或者在损失函数里用 focal loss。更根本的是标注阶段别把每个刻度数字都单独标把整段刻度标成一个scale区域减少冗余框。4.5 坑五训练验证集按图随机划分指标虚高现象验证 mAP 0.9上线实测掉到 0.6。原因同一块表的多张图被分到训练和验证两边模型记住了这块表的外观。解决按仪表实例分组划分同一块表只进一个集合。划分后检查一下训练集和验证集的仪表编号有没有交集有就重分。5. 让 xml 数据集真正跑起来增强策略与读数验证闭环数据集转完、校验过下一步是训练和验证闭环。指针仪表场景有几个针对性的增强技巧用对了能省一半标注量。几何增强要克制。常规检测任务爱用随机旋转、透视变换但指针仪表不行——旋转会改变指针角度透视会让圆形表盘变形读数标签全废。我一般只开水平翻转且只对对称表盘、小幅度平移缩放±10%、亮度对比度扰动。mosaic 增强可以用但mosaic0.5以下太高了表盘被拼得七零八落。亮度增强要覆盖现场光照。发电厂集控室白天晚上光照差好几档训练时用 HSV 空间的 V 通道扰动范围设 0.6 到 1.4模拟暗光和强光。这个比几何增强更值得花时间调。读数验证闭环。检测只是第一步最终要出读数。我的做法是检测框出来后取center框中心为轴心pointer框内做细化分割或直线拟合找针尖方向算角度再按量程线性映射成读数。验证时人工读 50 块表做真值和算法读数比误差超过一格量程的样本回查标注。这个闭环跑通才知道数据集到底够不够用。import cv2 import numpy as np def pointer_angle(pointer_crop, center_xy): # pointer_crop: 指针区域灰度图 # center_xy: 轴心在 crop 内的坐标 (x, y) gray cv2.cvtColor(pointer_crop, cv2.COLOR_BGR2GRAY) # 自适应阈值突出指针 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 15, 5) # 霍夫直线找主方向 lines cv2.HoughLinesP(binary, 1, np.pi / 180, threshold30, minLineLength20, maxLineGap5) if lines is None: return None cx, cy center_xy best None best_dist -1 for line in lines: x1, y1, x2, y2 line[0] # 取离轴心较远的端点作为针尖候选 d1 (x1 - cx) ** 2 (y1 - cy) ** 2 d2 (x2 - cx) ** 2 (y2 - cy) ** 2 tip (x1, y1) if d1 d2 else (x2, y2) dist max(d1, d2) if dist best_dist: best_dist dist best tip if best is None: return None dx best[0] - cx dy best[1] - cy # 转成 0-360 度y 轴向下需取反 angle (np.degrees(np.arctan2(-dy, dx)) 360) % 360 return angleadaptiveThreshold用THRESH_BINARY_INV让指针变白HoughLinesP找线段minLineLength20过滤短噪声。取离轴心最远的端点当针尖arctan2算角度时-dy是因为图像 y 轴向下。这个函数是简化版实际用要加角度有效性判断——比如指针角度落在刻度范围内才认。参数threshold、minLineLength按实际分辨率调表盘在图上越大这两个值越大。最后说个习惯。我做完每份指针仪表数据集都会留一份「黄金测试集」——50 到 100 张人工反复核对过标注和读数的图不参与训练每次改模型或改后处理都拿它跑一遍。这份集子是我的后悔药指标涨了跌了拿它一测就知道是真进步还是过拟合。数据集这活儿标注时多花一小时后面少熬三个通宵。希望帮到你。本文还有配套的精品资源点击获取