ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

YOLO水位表读数识别实战:689张数据集处理与训练全流程解析

YOLO水位表读数识别实战:689张数据集处理与训练全流程解析 简介面向水位检测仪器读数识别的 YOLO 系列目标检测数据集已按训练集、验证集、测试集划分完毕并内置 data.yaml 配置文件兼容 yolov5、v7、v8、v9、v10、v11 等主流版本可直接进入训练流程适合正在学习目标检测、准备算法实验或毕设项目的读者快速上手省去从零采集图像和人工标注的准备工作。压缩包共包含 2000 个文件整体约 34.8MB其中 621 张 JPG 原图、689 个 TXT 标签和 689 个 XML 标签TXT 为 YOLO 格式类别索引与归一化中心点、宽高信息齐全XML 为 VOC 格式两种标签分别存放便于在两种标注体系间对照或迁移另附 1 个 YAML 数据集配置文件可直接被 YOLO 系列框架读取。内容以水位检测仪器上的读数显示为检测目标贴合工业监测、仪表读数识别等实际场景适合验证模型在读数识别任务中的检测精度与泛化能力。目前已有 231 人浏览学习是开展 YOLO 系列算法训练、模型验证与读数识别预研时可复用的数据集资源。1. 689张带标签图像与YOLO水位表读数识别的最小可行起点很多做水利信息化、智慧灌区和工业现场抄表的人拿到这类“水位检测仪器读数数据集”的第一反应是赶紧拿来训练一个YOLO模型把表盘上的数字自动读出来。这个zip的价值正在于此——689张图像不是几千上万的通用数据集但胜在领域单一、标签齐全包含号码、测量、最大值这些与读数强相关的标注维度。它的适用场景很明确你想低成本验证“基于yolo算法的表计读数识别”这个方向可不可行先把这套数据跑通再叠加自己的现场图像做增量训练。适合正在做目标检测入门、工业视觉和物联网抄表方案的工程师也适合刚接触数据集落地的新手——因为图像规模不大整个流程从解压到训练出可用模型一两天就能走完。2. 拿到zip先做数据体检解压、盘点、标签语义确认2.1 解压前先看包文件嵌套与中文路径的坑压缩包这层最容易出问题。不要直接双击解压先看一下包内结构。命令行里用unzip -l列目录Windows 下用 7-Zip 的“打开内部压缩包”也是一样的效果。我见过很多人在这一步翻车解压后产生两级同名目录yolo算法-水位检测仪器读数数据集又套一层同名目录YOLO训练脚本里写相对路径就全乱掉还有一部分图像文件名带中文Python 的cv2.imread在 Windows 默认编码下读不出中文路径报错却指向下一行代码。# 先看压缩包顶层结构别急着解压 unzip -l yolo算法-水位检测仪器读数数据集-689张图像带标签-号码-测量-最大值.zip | head -30 # 解压到纯英文路径避免中文目录导致OpenCV读取失败 mkdir -p /data/water_meter_dataset unzip yolo算法-水位检测仪器读数数据集-689张图像带标签-号码-测量-最大值.zip -d /data/water_meter_dataset这段命令的思路是用unzip -l观察包内是否有顶层文件夹head -30只看前30条就够了核心是确认图像和标签是不是在同一个根目录下解压时把目标目录设置成/data/water_meter_dataset这种纯英文路径是在提前消除编码隐患。如果包内确实有一层外层目录解压后你得到的就是/data/water_meter_dataset/水位检测仪器读数数据集/...后面所有脚本记得多带一层即可。2.2 图像与标签的数目盘点用几条命令查清家底解压之后先别训练先回答三个问题图像有多少张、标签有多少个、两者能不能一一对上。689张图像是标题宣称的数据量实际到手可能多几张少几张也可能图像689张但标签只有670份——漏标、空标签在打包过程中很常见。这里用 find 和 wc 组合做一次快速体检。# 统计图像文件数量jpg/png都要算 find /data/water_meter_dataset -type f \( -name *.jpg -o -name *.png \) | wc -l # 统计标签文件数量常见扩展名是xml/txt/json find /data/water_meter_dataset -type f \( -name *.xml -o -name *.txt -o -name *.json \) | wc -l # 列出前10个标签文件名看清到底是xml还是txt find /data/water_meter_dataset -type f -name *.xml | head -10习惯上先把图像和标签各自的数量列出来再比对文件名集合。数量对不上时不要急着怀疑数据集有问题先看是不是有非图像文件混在标签目录里。更稳妥的做法是直接用 Python 做一次文件名求差集把“有图无标”和“有标无图”的文件名各自列出来。数据集的坑多集中在文件层这一步花十分钟后面少踩三个坑。来看看一套合格的数据集内部长什么样下面这张核查表可以对照着手动检查。检查项期望结果异常处理方法图像格式jpg或png统一混有bmp/tif时用Pillow批量转成jpg注意质量参数标签格式全部同一种格式混用xml和txt时按格式分别解析不要强行合并文件名对应图像与标签同名后缀不同用Python对文件名取差集单独移出缺失文件图像分辨率不低于640x640过小的图直接删掉或标注后作为增强数据标签内容每份至少一个目标空标签文件单独归类后面用脚本处理2.3 标签语义确认“号码”“测量”“最大值”到底标在哪文件名里那串“号码-测量-最大值”不能猜。把标签文件打开看几个样本确认这三个词是类别名、属性字段、还是只存在于文件名里的描述信息。下面这段 Python 代码用标准库解析 XML 标签打印出每个目标的类别和边界框坐标。import xml.etree.ElementTree as ET from pathlib import Path # 以解压后的目录为例手动指定一个xml样本 xml_path Path(/data/water_meter_dataset/labels/000001.xml) if xml_path.exists(): tree ET.parse(xml_path) root tree.getroot() # 图像尺寸在annotation里记录后用于归一化计算 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) print(f图像尺寸: {img_w}x{img_h}) # 遍历object节点输出类别名和像素坐标 for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) print(f类别: {name}, 框: ({xmin:.1f}, {ymin:.1f}) - ({xmax:.1f}, {ymax:.1f}))这段代码的逻辑很直白先读size拿到原图宽高这是后面计算归一化坐标的必要输入再遍历object节点把类别名和边界框打出来。跑完几个样本后你会发现“测量”和“最大值”很可能不是检测类别而是表计的量程信息“号码”则可能是仪表编号文本也可能是一张图里多个表盘实例的编号。如果是前者那“测量/最大值”只是附带的业务元数据不需要训练如果是后者这几个词会出现在类别列表里就必须在 yaml 里全部定义。判断标准就一条去找标签文件里name字段的实际取值只有一个类别就写一个nc1有三个就写nc3其他都是多余的。3. 从原始标签到YOLO训练格式格式转换与数据集划分3.1 为什么YOLO非要txt归一化坐标背后的逻辑VOC 格式的 XML 标签在公开数据集里最常见但 YOLO 训练器只认纯文本的 txt 标签每行一个目标格式为class_id x_center y_center width height。最关键的是坐标必须是归一化的也就是说所有值都要除以图像宽高落在 0 到 1 之间。这样做的原因是 YOLO 在训练时会自动把输入图缩放到固定尺寸例如 640x640归一化坐标可以保证无论输入大圖还是小图标注框都能正确映射到缩放后的特征图上。而 XML 里的坐标是像素值直接拿绝对值去算换一张分辨率不同的图像就全错位了。这就是每份 YOLO 数据集必须有一个“格式转换”环节的原因。另一个容易被忽略的点是很多公开的 xml 标签文件根本没有标签节点也就是只有object的空文件这种情况不能直接转为空白 txt 了事需要单独处理。3.2 转换脚本批量把XML转成YOLO格式txt并清洗空标签下面的脚本按最常见的 VOC XML 格式写假设标签文件在labels_xml目录图像在images目录。转换时除了归一化坐标还会过滤掉difficult1的模糊目标并跳过解析失败的标签文件。import xml.etree.ElementTree as ET from pathlib import Path # 类别映射以实际标注为准这里给一个示例假设只有读数字符区 label_map { meter_reading: 0, # 表盘读数区域 meter_number: 1, # 仪表编号 } xml_dir Path(/data/water_meter_dataset/labels_xml) txt_dir Path(/data/water_meter_dataset/labels_txt) txt_dir.mkdir(exist_okTrue) for xml_file in xml_dir.glob(*.xml): try: tree ET.parse(xml_file) root tree.getroot() except ET.ParseError: # 标签文件损坏时跳过并打印出文件名方便排查 print(f解析失败: {xml_file.name}) continue size root.find(size) if size is None: continue img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): # 跳过difficult标注避免模糊样本干扰训练 diff obj.find(difficult) if diff is not None and int(diff.text) 1: continue name obj.find(name).text if name not in label_map: print(f未知类别: {name} 在 {xml_file.name}) continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化坐标并剔除宽高为0的异常标注 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 if w 0 or h 0: continue lines.append(f{label_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) # 文本文件与图像同名写在txt_dir下 txt_path txt_dir / f{xml_file.stem}.txt if lines: txt_path.write_text(\n.join(lines) \n) else: # 没有有效标签的空文件也要写出来YOLO用它作为背景负样本 txt_path.write_text()转换脚本有三个关键设计一是label_map必须提前从上一章的标签语义确认中拿到类别名不对应会在转换阶段就报“未知类别”而不是等到训练才炸二是空 txt 文件是合法的在 YOLO 里它表示“这张图没有需要检测的目标”训练时会被当作背景图参与 loss 计算不用删除三是w 0 or h 0的过滤操作这种异常标注在人工标注数据里真实存在不筛掉会让 loss 变成 NaN。脚本跑完后再用find labels_txt -name *.txt | wc -l统计一次和图像数量比对差距应控制在极小范围内。3.3 划train/val并生成data.yaml分组比随机更可靠数据集划分最忌讳粗暴随机。水位表计数据通常涉及多台设备多次拍摄如果同一台设备的 3 张图像分别落在训练集和验证集验证就变成“开卷考试”模型看到过同一场景的不同角度得分虚高。正确的做法是按设备 ID 或拍摄编号分组整组划入训练或验证。文件名里如果带编号可以解析出来作为分组键。import random import shutil from pathlib import Path img_dir Path(/data/water_meter_dataset/images) txt_dir Path(/data/water_meter_dataset/labels_txt) train_img Path(/data/water_meter_dataset/images/train) train_txt Path(/data/water_meter_dataset/labels_txt/train) val_img Path(/data/water_meter_dataset/images/val) val_txt Path(/data/water_meter_dataset/labels_txt/val) for d in [train_img, train_txt, val_img, val_txt]: d.mkdir(parentsTrue, exist_okTrue) # 假设文件名格式为 device_001_frame01.jpg按设备分组 files list(img_dir.glob(*.jpg)) groups {} for f in files: device_id f.stem.split(_)[1] # 提取001作为分组键 groups.setdefault(device_id, []).append(f) all_files [] for device_id, imgs in groups.items(): random.shuffle(imgs) # 每台设备80%的图像进训练20%进验证 split_idx int(len(imgs) * 0.8) all_files.append((imgs[:split_idx], train)) all_files.append((imgs[split_idx:], val)) # 按组移动文件标签与图像同步迁移 for subset, split in all_files: for img_path in subset: txt_path txt_dir / f{img_path.stem}.txt if split train: shutil.copy(img_path, train_img) shutil.copy(txt_path, train_txt) else: shutil.copy(img_path, val_img) shutil.copy(txt_path, val_txt)按设备分组的逻辑是split(_)[1]提取出设备编号整组一起划分保证同一台表计只出现在一个集合里。这种做法的代价是可能造成某个类别在训练集和验证集数量不均衡——比如设备 003 恰好全是“测量值”类别而它全分到了验证集。所以分组后要看一眼两个集合里各类别的数量偏差大就调一下分组粒度。最后一步是写data.yaml这是 YOLO 训练器读取数据配置的入口。# data.yaml path: /data/water_meter_dataset train: images/train val: images/val nc: 2 names: 0: meter_reading 1: meter_numberpath是数据集根目录train和val是相对路径nc必须和names里的数量严格一致。这里最容易犯的错是nc写成目标的种类数而不是类别的最大编号加一如果你的类别编号是 0 和 1那nc2没错但如果标签里有类别 5nc至少要写 6否则训练器直接报 class id out of range。4. 用YOLOv8跑通训练选模型、调参、盯损失曲线4.1 模型选型与最小训练命令689 张图像属于典型的小规模专项数据集模型选型不需要一上来就上 YOLOv8x。我一般会用yolov8s起步它在精度和训练速度之间最平衡如果你只想验证流程yolov8n更快精度略低但足以判断数据集是否靠谱。安装依赖和启动训练的命令如下# 安装ultralytics库自动带上yolo命令行工具 pip install ultralytics # 用yolov8s预训练权重启动训练 yolo detect train datadata.yaml modelyolov8s.pt epochs120 imgsz640 batch16 device0这里解释一下modelyolov8s.pt的含义它是 COCO 数据集上预训练好的模型内部带着 coco80 那 80 个类别的输出头但训练器在加载时会自动把输出层替换成你data.yaml里nc2的结构所以类别不一致不需要你手动改任何代码。预训练权重复用的是前几层提取通用特征的能力对这个小数据集特别重要——从零训练 689 张图基本必过拟合。如果你在 PyCharm 这类 IDE 里操作直接把这两条命令放进终端窗口跑就行和命令行没有区别只是注意工作目录要切换到data.yaml所在的文件夹。4.2 必调参数imgsz、batch、epochs与增强开关小数据集训练参数不能照搬 COCO 的大路配置。imgsz 影响最直接水位表盘在画面里通常占比较大640 足够但如果你的目标是识别表盘上的小字读数数字区域可能只有几十个像素建议把 imgsz 提到 960代价是显存占用约翻倍。batch 默认是自动模式8GB 显存我建议手动固定到 16。epochs 对这种规模设 120 比较合适开启早停后它会自己停止。参数建议值说明modelyolov8s.pt小数据集用s起步验证完毕后可换m提高精度imgsz640数字很小、需要抠细节时调到960batch168G显存以下建议不超过16OOM时降到8epochs120配合早停实际通常在80轮左右收敛patience20验证集指标连续20轮不升即停止mosaic0.5表盘数字密集时mosaic会把目标切成碎片建议调低cos_lrTrue余弦退火调度后半段收敛更平稳这里重点说 mosaic。YOLOv8 默认开启 mosaic它把四张图拼成一张训练能显著提升泛化但水位表是强结构目标拼接后经常把读数字符切成一半让模型学到“数字断开了也算一个目标”。这类现场经验很少写进文档只能靠损失曲线观察——如果训练集的 cls_loss 一直降不下来试着把mosaic0.5或直接关掉。4.3 盯住yolo损失函数什么情况算健康什么情况算翻车训练目录下会自动生成results.png和results.csv里面有三个 loss 曲线对应 yolo 损失函数的三个组成部分box_loss 负责框的位置和大小回归cls_loss 负责类别分类dfl_loss 是分布焦点损失负责边框的精细调整。健康训练的曲线特征很典型前 20 个 epoch 三条 loss 直线下降之后转入缓慢下降期验证集曲线与训练集保持同步如果验证集的 box_loss 在某个 epoch 开始掉头向上训练集还在降那就是过拟合的信号。# 查看训练输出目录results.png就是损失曲线图 ls runs/detect/train/ # 打开结果csv用任意工具看最后几行 tail -5 runs/detect/train/results.csv看曲线的时候还要配合两个指标mAP50 和 mAP50-95。mAP50 达到 0.7 以上说明框基本打准了mAP50-95 更能反映框的精细度——如果前者高后者低说明框的位置“差不多但不精确”这在读数字任务里是致命的因为数字识别需要精确的裁剪区域。另外训练结束后打开confusion_matrix.png看一下“测量值”和“编号”这两个类别有没有互相混淆。这类业务相关的类别人眼里分得很清模型却容易搞混一旦混淆矩阵里出现明显的非对角线亮块要么增加对应样本要么考虑合并类别。5. 水位读数数据集的五个踩坑点现象、原因、解决5.1 现场图mAP不低却框不准问题出在成像差异现象验证集上 mAP50 有 0.85看起来很不错把模型部署到现场去测同一个表盘就是框不准数字要么框大要么框小。 原因数据集的图像大多是近距离、顺光拍摄的现场是隔着防护网、逆光甚至带玻璃反光拍的成像差异比想象中大得多。 解决不要只在数据集上磨参数。从现场截几十张孤立的图按 YOLO 格式手工标一版混入原数据集重新训练。这个增量训练的成本很低但效果立竿见影。推理时还得检查预处理确保预测命令里imgsz和训练时一致YOLO 用的 letterbox 会补边而不是拉伸如果被人为改成 resize框的坐标就会偏移。5.2 class id out of range类别编号没对齐现象训练刚开始就弹AssertionError: class id out of range。 原因标签 txt 里出现了大于等于nc的类别编号常见于类别从 1 开始编号但 yaml 里nc1只留了一个位置或者数据集中混入了其他数据集的标签文件。 解决先用一条命令扫出标签里的最大类别编号# 统计所有txt标签里最大类别编号确保小于nc awk {if($1max) max$1} END {print max class id:, max} labels_txt/*.txt如果最大值是 2但data.yaml里nc2那就是编号 2 溢出有效编号只能是 0 和 1。要么改 yaml 把nc3要么重新写标签把类别映射到 0 和 1。别嫌这个检查麻烦在打包传输的数据集里混入一两个不匹配文件是常有的事。5.3 模型只认竖装表盘数据增强和划分的双重责任现象训练集里 90% 的表盘是竖装横装样本只有几十张结果模型对横装表盘漏检率极高。 原因数据集天然不平衡加上默认增强里旋转角度很小横装模式特征没被补出来。 解决划分数据集时就要按朝向分组保证验证集里也有横装样本如果横装样本实在少90 度旋转复制一份加进训练集。但要注意旋转后的标签坐标要跟着变换不能直接把图像转了而标签不转否则就是白白给模型喂错误信息。手工实现 90 度旋转的数据增强脚本不复杂但计算坐标时要小心宽高互换。5.4 EXIF旋转让标签错位看图软件和训练器各读各的现象训练时 loss 正常但可视化预测时发现框整体偏转 90 度而且只发生在部分图像上。 原因手机或部分工业相机拍的照片带 EXIF 方向信息看图软件会自动旋转显示但 OpenCV 的imread默认忽略 EXIF训练器拿到的实际图像和标注工具的“所见”不一致。 解决在标注阶段就把方向固话。先批量把图像转正再开始标注如果数据集已经打包好写一个预处理脚本对带 EXIF 方向标记的图像执行旋转并去除方向标记字段from PIL import Image, ExifTags for img_path in image_list: img Image.open(img_path) exif img.getexif() orientation exif.get(274) # 274是方向标记的标签id if orientation 6: img img.rotate(-90, expandTrue) elif orientation 8: img img.rotate(90, expandTrue) img.save(img_path)这个坑隐蔽在“部分图像”上只影响少数文件但一粒老鼠屎就能把训练集搅浑。处理完后再跑一次训练错位问题就消失了。5.5 XML空标签与负样本漏标文件到底怎么处理现象标签文件数量对得上但训练日志里显示某几张图的 cls_loss 异常高打开一看xml 标签文件内容为空或只有根节点没有任何object。 原因打包数据集时把一些漏标的图像也放了进来xml 文件被创建但没写入内容。 解决用 3.2 的转换脚本时空 xml 会被跳过去而对应的 txt 文件为空。空 txt 在 YOLO 里合法会被当负样本参与训练防止模型在无目标区域乱框这不一定是坏事。但你得分清是“故意放的背景图”还是“漏标的真样本”——如果是后者这张图里的表盘就被模型学成了背景推理时遇到相似表盘会漏检。最稳妥的办法是转换后人工抽查几十个空 txt 文件确认对应图像确实没有目标。6. 最后一公里检测框如何变成真实读数6.1 先排序再识别把检测框拼成读数串模型输出的是一个个检测框但它不会告诉你读数是多少。要把框变成数字常规做法是先用 YOLO 检测出读数字符区域再按 x 坐标从左到右排序裁剪出每张数字图送入 OCR 模块识别。这个流程比端到端的读数识别更灵活训练成本也低得多。from ultralytics import YOLO from PIL import Image model YOLO(runs/detect/train/weights/best.pt) img Image.open(现场图.jpg) results model.predict(img, conf0.35, imgsz640) boxes results[0].boxes.xyxy.cpu().numpy() clss results[0].boxes.cls.cpu().numpy().astype(int) names model.names # 只取读数区域的框按x坐标排序 reading_boxes [] for b, c in zip(boxes, clss): if names[c] meter_reading: reading_boxes.append(b) reading_boxes.sort(keylambda x: x[0]) # 从左到右 # 逐个裁剪并送OCR这里简化为保存裁剪图 for i, b in enumerate(reading_boxes): xmin, ymin, xmax, ymax [int(v) for v in b] crop img.crop((xmin, ymin, xmax, ymax)) crop.save(fcrops/digit_{i}.jpg)排序这一步看着简单实际上 Python 的sort默认按元组第一个元素排正好符合读数从左到右的顺序。但如果表盘是多行数字比如测量值在上、最大值在下光按 x 排序会把两行混在一起。这时候要加一个按 y 坐标聚类分行的逻辑先在sort(keylambda x: (x[1] // 50, x[0]))这种写法里分行再组串。6.2 用“最大值”做量程校验与多帧投票检测和识别都跑通之后还要利用标签里“最大值”这个信息给结果兜底。每个表盘都有量程比如最大量程 10 米那么识别出的读数理论范围是 0 到 10。如果输出的读数是 12.5一定是识别错了——可能是小数点位置错可能是数字 1 被误读成 7。常见的兜底策略是读数超过量程的 1.05 倍时重新推理一次连续两次超限就丢弃该帧等下帧再试。多帧识别后取中位数作为最终读数能有效过滤掉偶发误检。这类数据集的真正价值不在于训练出一个 100% 完美的模型而在于帮你把一个完整的数据处理链路跑通数据集解压、标签清洗、格式转换、训练调参、误差兜底。我在现场做过类似的项目模型在办公室演示环境里跑得很漂亮一上工地每隔半小时错一张最后发现是太阳斜射造成表盘玻璃反光。后来在程序里加了“读数超过量程就连续重试 3 帧”的规则问题才彻底消失。这类血泪经验很难靠数据集自带文档避开只能在实际部署中一点点补——希望帮你在这个方向少踩几个坑。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表