尧图精选

基于YOLO的电表读数识别:检测与分类的两步解法

🕒 发布时间:2026/10/2 3:01:25 📁 来源:尧图网络
简介目标检测是计算机视觉中的基础任务旨在从图像中定位并识别多个对象。基于深度学习的目标检测模型通过卷积神经网络提取特征并利用边界框回归与分类损失进行优化能够在复杂场景下实现精准定位。这一技术在工业自动化领域具有重要价值例如通过智能摄像头自动识别仪表盘读数替代人工抄表。在电力巡检、能耗采集等场景中环境光照多变、表计类型复杂直接使用端到端OCR往往效果不佳。基于YOLO的电表读数识别系统将任务拆解为检测与分类两步先用YOLO定位表盘中的每个数字和小数点区域再通过分类器识别数字并拼接成最终读数。文章从数据准备、模型训练到RK3588边缘设备部署系统介绍了落地实践中的关键技术与常见问题。1. 基于YOLO的电表读数识别系统从表盘像素到一行读数的两步解法基于YOLO的电表读数识别系统解决的不是“认字”而是“在混乱现场把读数区域先找出来再交给分类器去认字”。我第一次做这个方向时直接端到端 OCR表盘一有反光就翻车后来改成 YOLO 检测数字框 分类器识别数字项目才真正跑起来。这套系统适合做电力巡检、能耗采集、老旧表计数字化改造的工程师尤其是要部署到 RK3588 这类边缘板卡、用摄像头对着现场表计抓拍转读数的场景。你只要把数据集格式、训练参数和推理后处理这三段捋顺就能把一张带表盘的图像变成一行可靠读数。这里不讨论理论架子直接按落地路径讲。2. 抄表识别的技术路线为什么先检测区域再做数字分类2.1 端到端 OCR 的老路适合文本行不适合被遮挡的表盘直接用端到端 OCR 读电表在干净的大数字表盘上能跑通但换成老旧机械表、数码管表、红外相机拍摄的夜间图错误率就明显上去了。很多项目一上来堆 PaddleOCR 或者传统 OCR 方案最后都会卡在三个地方。第一个原因是 OCR 的检测单元是文本行而表盘上数字之间压得很近中间还有表号、商标、红色小数位。文本行切分经常把整数和小数点切成两段或者把一个数字框进两个检测框里。第二个原因是表盘反光和水雾会让字符边缘在预处理二值化后糊成一团文本框漏检、误检反复出现。第三个原因是表计种类差异很大同样是电流表有的带指针有的是七段数码管有的字轮式机械表。OCR 模型在文本行检测上训练得越充分遇到表盘这种“非均匀文本”反而越容易按照自己的惯性去裁行。所以常见做法是把问题拆成“检测 分类”两段YOLO 只负责从整张图里把每个数字字符、小数点符号的外接框找出来类别就定义成 0 到 9 加 dp找到框之后对框内的小图再去做分类和读数拼接。这个方案的失败模式非常清晰数字漏检就去调检测置信度或者补数据数字错认就去处理分类不平衡不会像 OCR 一样把问题全堆在黑匣子里。2.2 YOLO 损失函数与模型选型小数字目标如何影响损失权重在电表读数这个场景里目标普遍偏小尤其老旧表计数字高度在 640×640 输入下可能只有十几个像素。YOLO 系列对这类小目标的损失主要由三部分组成分类损失 L_cls、边界框回归损失 L_box、边界框分布损失 L_dfl。整体可以简化看成L_total λ_cls × L_cls λ_box × L_box λ_dfl × L_dfl其中 box_loss 在 Ultralytics 版本的 YOLOv8 里走的是 CIoU 和 DFL 的组合。对电表数字来说框高度小哪怕一两个像素的偏移IoU 变化都很明显所以 CIoU 对预测框的宽高非常敏感。这就是为什么很多人在训练时发现其他损失都下去了box_loss 还在轻微震荡不一定是模型有问题可能只是小目标回归的固有小步抖动。因此模型选型上有一条经验不要盲目加骨干网络宽度优先提高输入分辨率。把 imgsz 从 640 提到 960 或 1280数字在特征图上的尺寸变大损失函数能更稳定地优化比从 n 系列强行换到 x 系列更划算。很多人问“YOLO 和 Transformer 结合”是不是更好RT-DETR 这类方案在精度上确实有优势但部署到 RK3588 时Transformer 的算子支持不如纯 CNN 的 YOLO 成熟。做演示可以量产边缘设备我还是优先选 YOLOv8n 或 YOLOv5s 这类模型。下面这个表是我在类似项目里做选型对比时的常用判断维度方案表盘数字定位能力部署到 RK3588落地难度YOLOv8n/s好成熟算子齐全可以直接转 RKNN中YOLOv5s好五系列老牌部署资料多可以直接转 RKNN中RT-DETR精度上限更高部分算子要绕过或替换偏高端到端 OCR文本行强表盘一般模型偏重偏高2.3 一条实用流水线检测、排序、组数的三段式结构实际系统的处理流程我一般固定成四步。第一步是整图输入YOLO 输出所有数字框和小数点框第二步先按检测框中心点的 y 坐标做行聚类把同一块表盘上的字符分成一行避免把上下两行数字拼在一起第三步每一行内部按中心点 x 坐标排序遇到 dp 类别就在拼接串里插入小数点第四步按表计精度规则补零得到最终读数。这里最容易翻车的是排序。如果直接按检测框的左边界 x1 排序遇到字符宽度不均、数字“1”特别窄的情况很容易把这个窄字符排到前一格读数整体错位。按中心点 x 坐标排序再配合 y 方向的行聚类基本能避免这种错位。我在第一个版本里就是栽在这个排序上肉眼看起来每个框都框对了读出来的数却和实际差一位。后面会在第 5 章的推理代码里给出具体的排序写法。3. 电表训练数据准备从公开数据集到 VOC 转 YOLO 格式3.1 现场数据太难凑公开数据集、合成表盘和自采怎么搭班子电表读数识别项目里数据往往比模型更值钱。公开能用的电力相关数据集不算多FIRC 电力红外数据集是其中比较常见的它以 VOC 和 YOLO 格式提供电力设备红外图像但主要标注的是变压器、绝缘子、刀闸这类设备本体不是表盘数字级。如果你做的是红外夜视电表识别可以用它训练一个“设备检测”模型筛掉背景里的发热物体但读数部分还得靠自己的标注数据。常见做法有三条路同时走。第一是现场采集固定摄像头对着表盘连续录视频抽关键帧把同一种表在不同时间段、不同光照下的画面都拿下来。第二是伪标注加速先用预训练 YOLO 对视频帧检测候选数字框人工只负责修正这样比从零画框快很多。第三是合成数据找 20 张干净表盘背景图把 0 到 9 字符按不同字体、字号、旋转角度、模糊程度贴上去再把贴完的图贴回表盘背景做光照合成。合成数据能解决类别数量不平衡问题比如数字 0 和 1 最多数字 9 很少合成时专门多生成 9。数据量我给一个相对保守的底线10 个数字类加上 dp 小数点类每个类别至少 1000 个实例整体不低于 8000 个目标框。低于这个数量训练出来的模型很难在验证集上稳定复现自己的混淆矩阵。3.2 VOC 转 YOLO 的 Python 脚本坐标归一化与四个边界坑VOC 格式的 XML 里存的是真实像素坐标YOLO 训练需要的是归一化后的中心点坐标和宽高。转换脚本并不复杂核心逻辑如下# voc2yolo.py import os import glob import xml.etree.ElementTree as ET def parse_voc(xml_path, classes): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) out_lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) 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 min(max(x1, 0), img_w) x2 min(max(x2, 0), img_w) y1 min(max(y1, 0), img_h) y2 min(max(y2, 0), img_h) if x2 - x1 1 or y2 - y1 1: continue line ( f{cls_id} f{(x1 x2) / 2 / img_w:.5f} f{(y1 y2) / 2 / img_h:.5f} f{(x2 - x1) / img_w:.5f} f{(y2 - y1) / img_h:.5f} ) out_lines.append(line) return out_lines if __name__ __main__: classes [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, dp] for xml_path in glob.glob(voc_annotations/*.xml): lines parse_voc(xml_path, classes) txt_name os.path.basename(xml_path).replace(.xml, .txt) txt_path os.path.join(yolo_labels, txt_name) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))这里要解释几个参数。classes的索引顺序必须和后续训练配置文件里的 names 完全一致否则类别错位是静默发生的训练看不出报错但推理结果全错。坐标公式里center_x (xmin xmax) / 2 / image_widthcenter_y 同理用图片高度width 和 height 分别归一化不要用同一个分母。最后过滤掉宽高小于 1 像素的退化框这种框会在计算 CIoU 时产生无效梯度。四个边界坑里最常踩的是“标签文件命名不对”。YOLO 要求标签文件名和图片文件名保持同名图片是frame_001.jpg标签就必须是frame_001.txt不是 XML 同名。其次是空标签文件有些图里没有表盘数字也要生成一个空 txt否则训练时会跳过这张图影响批次稳定性。第三是类别索引从 0 开始很多人拿 VOC 的标签名转换成数组下标时忘了减 1。第四个坑是坐标越界上面代码里已经做了裁剪但裁剪逻辑必须放在计算归一化之前否则越界值算出来的中心点完全错误。3.3 标注策略0123456789 还是整表区域小数点要不要单独建类标注策略决定了整个系统的上限。第一种策略只标注表盘读数区域YOLO 只负责框出“这一块有数字”再交给 OCR 识别。这种方式标注量小但 OCR 那层仍然会丢精度我在项目里基本弃用。第二种是 10 类数字直接标在字符框上每个框只包含一个数字这是最常用的方案。第三种在 10 类数字之上再加一个 dp 小数点类推荐使用因为小数点决定整数位和小数位的拼接位置没有 dp 类排序后只能靠猜。标注时的细节决定了训练手感。一个数字框里绝对不能框进两个字符数字“6”和“9”在旧表盘里长得很像标注时不要按主观印象分而是按实际视觉形态标。小数点单独建类后它在拼接逻辑里只负责在字符串里插入一个点不需要再做额外分类。如果现场表计类型多同一个数字在不同表盘上的字体差异很大最好按表计类型分组做标注尽量保证每个类别下的字体多样性。另一个要注意的增强参数是水平翻转数字是文本符号水平翻转会把 6 变成 9、7 变成 1如果打开水平翻转又保持标签不变模型会被互相矛盾的标签逼到不收敛。这个后悔药我吃过一次关闭水平翻转后 mAP 明显回升。4. yolo训练自己的数据集环境配置、训练命令与常见问题排查4.1 yolo预训练模型下载与环境配置一次装好不乱动环境配置建议用独立 conda 环境不要直接装在系统全局 Python 里。不同项目之间 torch 版本冲突会浪费大量时间。最小安装路径如下conda create -n meter python3.10 -y conda activate meter pip install torch torchvision --index-url 你的CUDA对应的PyTorch官方源 pip install ultralytics这里需要说明两点。torch 版本必须和显卡驱动、CUDA 版本匹配没有 GPU 的机器也能跑但训练速度会慢很多建议先用 CPU 跑通 5 个 epoch 验证数据没问题再换 GPU 正式训练。不要尝试从源码编译 ultralytics官方 release 包已经够用源码编译只会引入 OpenCV 和 torch 的版本纠缠。第一次运行下面这段代码会自动触发 yolo预训练模型下载from ultralytics import YOLO model YOLO(yolov8n.pt) # 也可以换成 yolov8s.pt 或 yolov5s.pt model.predict(test.jpg, saveTrue)如果项目压缩包里有best.pt或last.pt直接加载就可以不一定非要重新下载预训练模型。预训练权重只在你从零开始训练时提供初始特征在电表数字这种特殊字符识别场景里迁移过来的特征主要体现在边缘纹理和形状轮廓上。4.2 dataset.yaml 与训练命令yolo损失函数的三个可调位置数据配置文件名可以自定义内容如下# meter.yaml path: /data/meter train: images/train val: images/val nc: 11 names: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, dp]训练命令我一般这样写yolo detect train \ datameter.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ device0 \ patience30 \ cos_lrTrue \ lr00.005 \ box7.5 \ project./runs/meter参数含义按优先级解释。imgsz对电表数字影响最大画面里数字占比低时优先提到 960 或 1280这比换大模型更直接。batch受显存限制但不要因为 batch 小就把nbs相关机制忽略掉Ultralytics 默认nbs64小 batch 训练时它会自动做学习率归一化这是 yolo损失函数更新过程中比较隐蔽的一层保护。box7.5控制边界框回归损失权重数字框小CIoU 对偏移敏感建议不要低于默认值。patience30表示验证集 30 个 epoch 没有提升就早停电表数据量不大时这个值比默认 100 更实用。关于 yolo损失函数训练输出里的train/box_loss、train/cls_loss、train/dfl_loss是三条独立曲线。如果cls_loss降得很快但box_loss反复震荡通常是数字目标太小如果val_loss在后期回升而train_loss还在下降说明过拟合此时优先检查验证集是否与训练集来自同一段视频。很多人看到 box_loss 回升就急着调学习率其实更该看验证集上的 val_box_loss 变化趋势。4.3 训练高频坑排查BN 崩溃、混淆矩阵总合不唯一、置信度漂移第一坑训练到中途 train loss 变成 NaN输出全空。现象是损失曲线直接消失日志里出现 nan后续所有 epoch 都无效。原因通常是数据里有坏图片、标签坐标出现 NaN或者学习率过高导致 BN 统计量崩掉这就是俗称的 BN 崩溃。解决方法是先删掉损坏图片检查标签里是否有非数字坐标然后把 lr0 从 0.01 降到 0.005有条件时把 batch 调大。BN 崩溃后不要从第 0 个 epoch 重跑用崩溃前一个 epoch 的last.pt恢复训练能把损失夺回来这是最省时间的做法。第二坑验证时混淆矩阵总合不唯一也就是说混淆矩阵里各行列的计数加起来和验证集标注数量对不上。原因是 YOLO 评估时会做 NMS 去重一个 GT 框可能匹配多个预测框而置信度低于阈值的预测会被归到背景类另外训练完临时改了 names 顺序也会造成类别错位。解决方法是以 mAP 和每类 TP 数为准不要用混淆矩阵的总和做精确校验评估命令固定写datameter.yaml modelbest.pt val不要后续手动改 names。第三坑训练完的模型在验证集上 AP 很高一到现场置信度普遍虚高但读数错得离谱。原因是验证集和训练集来自同一段视频模型只学了光照和背景。解决方法是把不同时段、不同角度的图片单独划成验证集不参与训练推理时把 conf 阈值从默认 0.25 拉到 0.5 以上再用验证集看一眼 precision不要只看 final mAP。第四坑数字分类把“7”读成“1”把“0”读成“8”。原因是数字像素太小或者缺少特定字体的样本。解决方法要么提高输入分辨率要么在标注时单独给窄字体的“7”和“1”增加样本让分类器见更多字形。5. 把模型部署到 RK3588 边缘设备导出、推理与后处理顺序5.1 导出 ONNX 再转 RKNN为什么要先关掉增强再做 half 量化RK3588 的 NPU 对 INT8 支持最好也兼容 FP16但直接把best.pt扔上去是不行的必须先导出 ONNX再转成 RKNN 格式。导出命令如下yolo export modelbest.pt formatonnx imgsz640 halfTrue simplifyTrue这里halfTrue生成 FP16 权重simplifyTrue会化简掉一批多余的 reshape 和 transpose 节点。imgsz必须和训练时保持一致否则导出后的网络输入尺寸变了后处理的解码位置会对不上。实际到 RK3588 上大部分工具链会默认走 INT8 量化量化过程需要几张校准图。校准图不要用训练集里的图要在部署位置上现场拍几十张覆盖不同曝光和角度。通用转换代码大概是下面这个样子from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588 ) rknn.load_onnx(modelbest.onnx) rknn.build(do_quantizationTrue, datasetcalib.txt) rknn.export_rknn(best.rknn)注意mean_values和std_values只是示意YOLO 预处理里像素归一化通常在 0 到 1 之间但不同工具链对 RGB 和 BGR 顺序的处理不一样。校准阶段先跑一张图验证颜色通道别把整批数据都校准完才发现输入顺序反了。这个错误在边缘部署上特别隐蔽表现为模型什么都检不到或者置信度极低。5.2 视频流推理与数字排序代码把 YOLO 框拼成完整读数部署推理的核心逻辑在于把检测框变成字符串而不是在模型调用上。下面是基于 ultralytics 的代码换成 RKNN 接口后逻辑完全一样from ultralytics import YOLO import cv2 model YOLO(best.pt) def build_reading(row): row.sort(keylambda t: t[0]) # 行内按中心 x 坐标排序 s for cx, cls in row: if cls 10: # dp 小数点类 s . else: s str(cls) return s def frame_to_reading(frame, conf0.6, iou0.45): results model.predict(frame, confconf, iouiou, verboseFalse)[0] items [] for box in results.boxes: cls int(box.cls.data) x1, y1, x2, y2 box.xyxy[0].tolist() cx (x1 x2) / 2 cy (y1 y2) / 2 items.append((cx, cy, cls)) items.sort(keylambda r: r[1]) # 先按 y 坐标分行 reading last_y None row [] for cx, cy, cls in items: if last_y is None or abs(cy - last_y) 20: if row: reading build_reading(row) row [] last_y cy row.append((cx, cls)) if row: reading build_reading(row) return reading cap cv2.VideoCapture(rtsp://camera/stream) while True: ok, frame cap.read() if not ok: break text frame_to_reading(frame) cv2.putText(frame, text, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow(meter, frame) if cv2.waitKey(1) 27: break这段代码有两个关键点。第一先按 y 坐标聚类分行再按 x 坐标排序。电表尤其是机械表高位数字和低位数字不在同一条水平线上直接用 x 全局排序会把两行数字交叉拼起来。第二dp 类别在拼接时插入小数点如果表盘没有小数位最好在标注时统一不标 dp 类避免模型偶尔把表号里的装饰点检成小数点。这种后处理逻辑是整个系统里最需要单测的部分。我会把每一帧的检测框和拼接结果同时保存成带框图片方便回查。排序算法看着简单但一旦出现大写数字和小写数字混排、dp 框落在两个数字中间的情况靠临时调试很费劲。5.3 边缘部署误检率高的常见原因阈值、NMS 与镜头安装边缘部署监控误检率高很多时候不是模型训练问题而是推理参数和镜头安装问题。先说阈值。实验阶段习惯用 conf0.25 来多检一些框但现场电表读数追求的是低误检我一般把 conf 提到 0.6。然后开启agnostic_nmsTrueYOLO 默认 NMS 是类内抑制0 和 8 这两个类别如果框重叠类间 NMS 不会互相压制导致同一个位置输出两个框读数拼接时就多了一位。开启全局 NMS 后重叠框只保留最高置信度误检能降一半。镜头安装有一个土规则表盘宽度至少占画面四分之一正对表盘偏转角不要超过 30 度。偏转太大时YOLO 还能检出框但数字分类置信度会明显下降尤其是机械表的字轮在斜视角下会被压扁。与其用一个广角镜头同时拍四块表不如每个表单独用中等焦距镜头这样后处理也简单。6. 一个验证读数正确性的土办法连续帧取众数别信单帧结果6.1 连续帧取众数与原始图回放部署完成后我最常用也最推荐的验证方法不是看 mAP而是连续帧投票。现场摄像头对着一个表位连拍 10 帧把每一帧的读数结果记录下来同一表位的最终读数取众数。10 帧里至少 9 帧一致才上报否则标记为人工复核。这个方法成本低但能挡住大部分偶发误检。回放时把原始图和检测框一起存复盘时直接看是哪一帧哪一位出了错而不是对着曲线猜。6.2 回灌困难样本与模型再更新经过投票复核标记出来的错误帧不要丢裁剪后重新标注回灌到训练集里。积累 200 到 300 张困难样本误检率通常能明显下降。我还习惯在推理后端加一层规则防火墙数字框宽高比超出正常范围的直接丢弃。规则会误伤但能拦住极个别极端误检。训练时我也总结了一个固定习惯验证集里永远放一段不同时间、不同角度的现场视频只信这条曲线的规律不围观训练集上的现象。这套流程走下来电表读数识别系统才算真正从“能识别”变成“能交付”。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →