
简介面向目标检测学习与指针仪表识别任务的数据集适合机器学习研究者、算法开发者及需要开展仪表定位与读数提取实验的学生使用。包内以783张jpg仪表图像为主体另附2个json文件压缩包约950.3MB共785个文件图像可用于训练与测试json通常承载标注或说明信息便于按训练集、测试集组织实验。标题标注包含XML标注文件实际包内文件类型以jpg与json为主使用前建议先核对目录结构与标注格式。已有303人学习说明该数据在相关任务中具备一定参考价值。读者可据此搭建指针仪表检测流程训练模型学习仪表位置、指针方向与读数区域并用独立测试图片检验泛化能力为自动化仪表读数、能源管理等应用提供数据基础。1. 指针仪表检测先解决数据问题一份带 XML 的训练集与测试集做指针仪表目标检测的团队十有八九在数据上出过问题表盘玻璃反光被框进去、同一块表在训练集和测试集各出现一次、XML 里的 xmax 刚好等于图片宽度。这份指针仪表数据集把训练集、测试集和 XML 标注文件分开存放适合直接做表盘定位与表计分类也适合新手把 VOC 转 YOLO 的链路跑通。它不是只给几张样图的演示包而是能支撑一次完整训练和验证的数据组织方式做巡检、读数或表计状态识别的可以把它当标注底稿再决定扩样方向。2. 目标检测标签转换从 XML 到 YOLO 的坐标换算与训练集、测试集骨架拿到这份指针仪表数据集第一件事不是直接开训练而是把 XML 和图片的对应关系验一遍。数据集已经给了训练集、测试集和 XML但 XML 只是标注的中间态目标检测训练入口多数吃的是归一化后的 txt 标签。下面按我平时拆包的顺序走先读一张 XML 确认字段再批量转 YOLO然后检查训练集和测试集有没有重叠最后把目录和 data.yaml 固定下来。2.1 先读一张 XML字段、尺寸与类别名批量转换前先抽一张 XML 读出来不要一上来就写循环。常见 VOC 风格的字段就那几层filename 对应图片名size 里的 width/height 是归一化分母object/name 是类别名bndbox 里是左上角和右下角像素坐标。先把一张读对后面批量转换才有的放矢。字段含义转换时要注意filename对应图片文件名必须和 images 下的文件名一致包括大小写size/width、height原图像素尺寸归一化分母不能用缩放后的尺寸object/name类别名要和 data.yaml 的 names 索引对齐bndbox/xmin、ymin、xmax、ymax像素框VOC 常见左上/右下注意边界闭区间from pathlib import Path import xml.etree.ElementTree as ET def read_voc_xml(xml_path): root ET.parse(xml_path).getroot() size root.find(size) w int(size.findtext(width)) h int(size.findtext(height)) objs [] for obj in root.findall(object): name obj.findtext(name).strip() box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) objs.append((name, xmin, ymin, xmax, ymax)) return w, h, objs if __name__ __main__: # 先换成你包里的真实路径只读一张 w, h, objs read_voc_xml(raw/train/annotations/0001.xml) print(尺寸:, w, h) print(目标:, objs)这段脚本只做读取不写文件目的是把尺寸和类别名先看清楚。参数上重点看三个地方xml_path 要指向训练集里真实存在的标注w、h 必须来自 XML 的 size不要用图片查看器里的显示尺寸代替name 读出来后要 strip避免尾部空格把类别名变成两个。如果 size 缺失不要猜直接用图片读宽高再把同一套尺寸写回转换逻辑。2.2 坐标换算VOC 的左上右下到 YOLO 的中心宽高YOLO 标签一行是class_id cx cy w h其中 cx、cy、w、h 都是相对图像宽高的归一化值。换算本身不复杂cx 等于 xmin 和 xmax 的中点除以宽cy 等于 ymin 和 ymax 的中点除以高w 和 h 是框的宽高除以图像宽高。容易出问题的是边界xmax 可能等于图片宽度像素索引又从 0 开始不裁就会得到大于 1 的归一化值。from pathlib import Path import xml.etree.ElementTree as ET # 类别顺序一旦定下就不要随意改label 第一列靠它映射 CLASSES [gauge] def voc_to_yolo(xml_path, out_dir, classesCLASSES): root ET.parse(xml_path).getroot() size root.find(size) w float(size.findtext(width)) h float(size.findtext(height)) lines [] for obj in root.findall(object): name obj.findtext(name).strip() if name not in classes: # 遇到未知类直接报错别静默跳过 raise ValueError(f未知类别 {name}文件 {xml_path}) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 裁到像素边界避免 xmax 等于 w 时归一化越界 xmin max(0.0, min(xmin, w - 1)) ymin max(0.0, min(ymin, h - 1)) xmax max(0.0, min(xmax, w - 1)) ymax max(0.0, min(ymax, h - 1)) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{classes.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path out_dir / f{xml_path.stem}.txt out_path.write_text(\n.join(lines), encodingutf-8) if __name__ __main__: out_dir Path(gauge_dataset/train/labels) out_dir.mkdir(parentsTrue, exist_okTrue) for xml_path in Path(raw/train/annotations).glob(*.xml): voc_to_yolo(xml_path, out_dir)这段逻辑的重点是“先裁再归一化”不是直接套公式。参数上CLASSES的顺序就是 label 第一列必须和后面的 data.yaml 完全一致w - 1和h - 1是像素索引的保守裁法如果你的标注工具导出的 xmax 是连续边界、允许等于 w那就统一裁到[0, w]但不要一半文件按闭区间、一半按连续边界处理。另一个参数是无效框处理我一般会先统计被跳过的数量超过 1% 就停下来看源数据而不是让脚本一声不吭地丢掉。提示转换完抽 20 张图把 txt 画回图上目检重点看贴边表盘和密集小表别只看 loss。2.3 训练集与测试集拆分先查重叠再谈比例数据集自带 train/test 不代表一定干净。现场采集常见连拍同一块表在几秒内拍五六张文件名不同、画面几乎一样如果随机划分训练集和测试集就会共享同一块表的纹理验证指标会虚高。先做文件名交集检查再做分组划分顺序不能反。from pathlib import Path train_stems {p.stem for p in Path(gauge_dataset/train/images).glob(*.jpg)} test_stems {p.stem for p in Path(gauge_dataset/test/images).glob(*.jpg)} print(训练集数量:, len(train_stems)) print(测试集数量:, len(test_stems)) print(重叠数量:, len(train_stems test_stems))这段只做集合交集逻辑很直接训练集和测试集的图片主文件名如果出现同一个 stem说明至少有一对图片同名后面必须人工确认是不是同一张图或同一块表。参数上要注意扩展名jpg、jpeg、png 要分别统计或者统一转成小写后再取 stem如果包里的图片命名带_left、_right这类后缀先确认它们是不是同一张原图切出来的别把切片当独立样本。如果需要在扩样后重新划分我一般按“表编号”或“拍摄场景”分组而不是按图片随机抽。下面这个脚本用文件名前两段做组键再用组键的哈希决定去训练集还是测试集保证同一组不会跨集。from pathlib import Path import hashlib def group_key(stem): # 文件名形如 gauge_001_20240501_01取前两段做表编号 parts stem.split(_) return _.join(parts[:2]) if len(parts) 2 else stem def split_by_group(xml_dir, test_ratio0.2): groups {} for xml_path in Path(xml_dir).glob(*.xml): key group_key(xml_path.stem) groups.setdefault(key, []).append(xml_path.stem) train, test [], [] for key in sorted(groups): # 用哈希而不是随机数保证每次划分结果一致 bucket test if int(hashlib.md5(key.encode()).hexdigest(), 16) % 100 test_ratio * 100 else train bucket.extend(groups[key]) return train, test if __name__ __main__: train, test split_by_group(raw/train/annotations, test_ratio0.2) print(训练组样本:, len(train)) print(测试组样本:, len(test))这段的逻辑是“先聚组、再分桶”group_key决定什么算同一块表。参数上test_ratio0.2是测试集比例样本少的时候不要低于 0.15否则验证指标抖动很大hashlib.md5只是拿来做稳定分桶不涉及安全用途。文件名没有表编号时换成感知哈希或拍摄时间分组核心原则只有一个同一块表、同一场景的连拍图不能跨集。2.4 目录骨架与 data.yaml让路径和类别索引一次对齐转换和划分都验完后再把文件放进训练入口认的目录。常见结构是 images 和 labels 分开放train 和 test 各一套。不要一边训练一边改目录路径错一次后面排查成本很高。mkdir -p gauge_dataset/{train,test}/{images,labels}这条命令只建骨架不搬文件。参数上gauge_dataset是数据集根目录train和test分别对应训练与验证实际拷贝时用cp或mv把图片和同名 txt 成对放进去图片和标签的主文件名必须一致比如0001.jpg对应0001.txt。如果后面还要加类别先在这个骨架上加不要直接改训练命令里的路径。path: ./gauge_dataset train: train/images val: test/images names: 0: gauge这份 yaml 的逻辑是把根目录、训练图片相对路径、验证图片相对路径和类别索引一次写清。参数上path用绝对路径最稳相对路径容易受启动目录影响train和val是相对path的路径不要重复写根目录names的索引就是 label 第一列0 对应 gauge如果 XML 里还有 pointer、scale 这类 name先统计全量类别再按顺序补不要先写nc再回头改标签。3. 指针表盘训练配置分辨率、增强与验证指标的取舍表盘这类目标有几个固定约束接近正方形、边缘有刻度、反光多、尺度跨度大。训练配置没有万能表但可以先守住两条小目标召回不能太差验证集不能只靠 mAP 好看。下面这套起点适合单类表盘检测改参数时先动分辨率和增强再动学习率。3.1 输入分辨率与长宽比表盘不是行人表盘外接框接近 1:1训练入口默认 letterbox 到正方形形变风险不大真正的矛盾在尺度。一张巡检图里表盘可能只占几十分之一640 输入下小表只剩二三十像素经过 backbone 下采样后特征很弱。分辨率不是越大越好显存和过拟合都要看但小表占比高时640 往往不够。场景imgsz理由风险表盘占画面 1/3 以上640速度快显存低远处小表容易漏表盘占画面 1/10 左右960召回和速度比较平衡显存上涨batch 要降远距离巡检图、多表同框1280 或切图保留刻度和表盘边缘训练慢小样本易过拟合参数上imgsz不是固定值要按验证集里表盘像素面积统计来定先算每张图标注框的宽高按 10%、25%、50% 分位数看低于 40 像素的样本占比超过两成就把输入拉到 960 以上或者切图训练。切图时注意重叠保留表盘被切成两半对检测帮助不大。3.2 小目标与密集刻度检测框只到表盘不到指针这里要先把任务边界说清目标检测框的是表盘外接矩形不是指针也不是刻度。下游要读数常见做法是两阶段先检表盘再裁图做关键点、语义分割或传统读数算法。把指针也塞进检测类别标注成本高、框不稳定训练出来容易抖。小目标密集时先看数据分布不要急着加 anchor。表盘尺度跨度大anchor 只能缓解一部分真正有效的是提高输入分辨率、切图、补小目标样本。验证集里单独统计小、中、大目标的 recall比只看总 mAP 更能发现问题如果小目标 recall 明显低先补数据再调imgsz最后才考虑模型结构。3.3 数据增强的边界旋转、透视与亮度表盘增强有两个容易过界的地方旋转和透视。现场安装确实有倾斜但 90 度旋转会破坏表盘语义指针方向也失去意义透视过强会把圆表拉成椭圆模型学到的是形变噪声。亮度、对比度可以放开一点表盘玻璃反光、逆光、欠曝都是现场常态。参数建议起点原因degrees5~10模拟轻微安装倾斜别做 90 度perspective0~0.0005轻微侧视可以过大破坏圆表形状hsv_h0.015轻微色偏不要大幅改色相hsv_s0.3~0.5表盘颜色差异hsv_v0.2~0.4逆光、欠曝、反光mosaic0.3~0.5增加小目标但会拼出不自然表盘close_mosaic10~20最后阶段关掉稳定收敛参数上mosaic不是越高越好。表盘是规则圆形四图拼接后容易出现半截表盘、比例异常的表盘训练后期关掉mosaic能让模型回到真实分布。degrees超过 15 就要谨慎除非你的现场确实有横装、竖装混用并且标注框能覆盖旋转后的表盘。3.4 训练参数与验证指标先看 recall再看 mAP下面这条命令以常见的 YOLO 训练入口为例把数据配置、输入尺寸和增强参数串起来。模型选小的先跑通不要一上来上大模型表盘检测的数据量通常不大小模型加高分辨率往往比大模型加 640 更实用。yolo detect train \ modelyolo11n.pt \ datagauge_dataset/data.yaml \ imgsz960 \ epochs120 \ batch8 \ patience30 \ lr00.01 \ degrees8 \ perspective0.0005 \ hsv_h0.015 \ hsv_s0.4 \ hsv_v0.3 \ mosaic0.4 \ close_mosaic15 \ projectruns_gauge \ nameexp01这条命令的逻辑是data指到上一步的 yamlimgsz统一训练和验证输入增强参数只作用在训练集project和name固定输出目录方便对比实验。参数上batch8是 960 输入下的保守值显存不够就降到 4 或开梯度累积patience30表示验证指标 30 轮不升就早停样本少时不要设太小lr00.01是常见起点微调预训练模型可以降到 0.001 到 0.005。训练完先看验证集的 recall 和混淆矩阵再看 mAP50、mAP50-95表盘漏检比框稍微偏一点更致命recall 不达标先回去查小目标样本和标注边界。4. 避坑与排查从 XML 到训练链路的五条问题记录这条链路我跑过多次真正费时间的不是模型而是标注和路径。下面五条都是实际拆包时容易遇到的每条按“现象、原因、解决”记方便对照排查。4.1 标注与转换层面的三个问题现象转换脚本跑完部分标签的 cx 接近 1训练时提示框越界或者画回图上框贴边缺失。原因VOC 的 xmax 有时等于图片宽度像素索引从 0 开始归一化前没裁也有脚本先归一化再裁顺序反了。解决像素坐标先裁到[0, w-1]归一化后再裁到[0,1]转换完抽 20 张画框目检贴边表盘重点看。现象训练 loss 正常降验证 mAP 长期为 0 或极低混淆矩阵只有背景。原因XML 里的 name 是Gauge、gauge带空格或者写成了meterdata.yaml 里只写gauge脚本按类别索引映射时错位或跳过。解决先全量统计 name 的 set 和频次统一小写、去空格固定 classes 列表顺序转换时遇到未知类直接报错不要静默 continue。现象测试集 mAP50 很高换一批现场图掉得厉害。原因同一块表的连拍图被分到训练集和测试集文件名不同但画面几乎一样验证指标虚高。解决按表编号或拍摄场景分组划分训练前跑文件名交集和缩略图相似度检查同一组连拍只进一个集合。4.2 训练与评估层面的两个陷阱现象标注框整体转了 90 度或者框和表盘明显错位。原因手机拍摄带 EXIF Orientation图片查看器按显示方向展示XML 也按显示方向标转换脚本按原始像素读方向没统一。解决统一用ImageOps.exif_transpose重存图片确认 XML 的 width/height 与重存后的图片一致或者转换前按 EXIF 交换宽高和坐标二选一不要混用。现象大表盘 recall 很高远处小表盘只有一半左右验证集里小框断断续续。原因640 输入下小表盘只剩二三十像素下采样后特征太弱mosaic 又让尺度分布更乱。解决提高imgsz到 960 或 1280或切图训练单独统计小目标样本比例和 recall先补小目标数据再考虑 anchor 和模型结构。5. 验收与进阶用中心点偏移反推检测框质量mAP 是排序指标能告诉你整体好不好但表盘检测还要看框稳不稳。下游读数阶段表盘中心偏一点裁图就会把指针切掉宽高比被拉扁圆表会变成椭圆。我一般额外统计两个量中心点偏移和宽高比误差。中心偏移用归一化坐标算欧氏距离0.02 以内说明表盘中心基本没飘宽高比误差超过 15%就要回去看标注框是不是把玻璃反光或支架也框进去了。import numpy as np def center_offset(pred, gt): # pred/gt: [cx, cy, w, h]归一化坐标 dx pred[0] - gt[0] dy pred[1] - gt[1] return (dx ** 2 dy ** 2) ** 0.5 def aspect_error(pred, gt): pred_ratio pred[2] / max(pred[3], 1e-6) gt_ratio gt[2] / max(gt[3], 1e-6) return abs(pred_ratio - gt_ratio) / max(gt_ratio, 1e-6) offsets [center_offset(p, g) for p, g in zip(preds, gts)] ratios [aspect_error(p, g) for p, g in zip(preds, gts)] print(中心偏移中位数:, round(float(np.median(offsets)), 4)) print(偏移超过 0.05 的比例:, round(float(np.mean(np.array(offsets) 0.05)), 4)) print(宽高比误差中位数:, round(float(np.median(ratios)), 4))这段统计的逻辑是把检测框和真值框都当成归一化中心宽高逐对算中心距离和宽高比相对误差。参数上0.05 是报警线不是合格线表盘任务里中心偏移中位数最好压到 0.02 以下宽高比误差中位数控制在 0.1 左右。统计时只取匹配上的框没匹配上的先算漏检不要混进偏移统计里掩盖问题。从那以后我每次拿到新的表计数据都先抽 20 张画框、跑一遍中心偏移统计再开训练这一步比调参更能提前发现问题。希望帮到你。本文还有配套的精品资源点击获取