尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

水表读数识别实战:基于YOLOv8与CNN的两阶段深度学习方案

水表读数识别实战:基于YOLOv8与CNN的两阶段深度学习方案 简介这是一份基于深度学习方法实现水表识别项目的完整代码包采用“定位网络识别网络”两阶段方案先定位水表表盘或数字区域再对读数进行识别。资源适合深度学习初学者、计算机视觉爱好者及有智能抄表、OCR数字识别需求的研究者既可以作为课设、毕设的参考实现也能迁移到类似仪表读数场景。压缩包共55个文件大小144KB主要包含18个Python源码文件、13个pyc字节码文件、14张带坐标的水表样本图片以及少量XML配置、Markdown说明和工程配置。Python源码覆盖数据提供、图像预处理、模型定义、训练器与测试等模块如WM_config.py、WM_data_provider.py、TensorflowUtils.py、wm_train.py等结构清晰样例图片可用于训练或验证定位模型。资源已有128人学习下载。通过阅读源码和配置能够理解水表识别任务的数据组织方式、定位与识别网络的搭建思路及训练流程同时获得一套可运行的工程骨架方便在此基础上替换数据集、调整参数或扩展其他仪表识别功能。1. 水表识别为什么要拆成两个网络定位管「在哪」识别管「是什么」水表读数识别第一直觉是拿通用 OCR 直接跑。真做过就知道这路走不通水表装在楼道、井坑和管道间拍摄角度随意玻璃反光读数窗口只占整张图几个百分点。端到端检测直接框数字小目标和遮挡会让召回率掉得很难看通用 OCR 又会被表盘铭牌带偏。这个项目也常被用作深度学习毕设里的图像识别选题但能跑通 demo 和能扛住现场是两个难度。把任务拆成「一个定位网络 一个识别网络」就顺理成章定位网络从整图里把读数窗口找出来识别网络把窗口里的数字读明白。两个网络独立训练、独立调参、独立评估任何一段出问题都能单独替换。这也是这个深度学习项目标题里的核心结构适合刚入方向、手里只有现场拍摄图像和少量标注预算的团队。下面从架构选型、数据准备、训练参数一直讲到部署验证和踩坑按我实际做过的方案来。2. 先把架构立住定位网络和识别网络各自该怎么选选型这件事很多教程一句话带过但在这个项目里它决定了后面所有调试成本。下面按定位、识别、衔接三块讲清楚每块都会给出我的取舍理由。2.1 定位网络选型为什么默认 YOLO 系检测器定位网络的任务只有一个在整张水表图上框出读数窗口也就是数字轮所在的矩形区域。这是个单类目标检测问题类别少、目标数量固定为一却有真正的难点——目标占比小几百像素宽的图中窗口可能只有几十像素高玻璃反光、污损、水雾又让边缘不稳定。框架选型我直接排掉两头。Faster R-CNN 这类两阶段检测器精度确实高但训练部署链路重现场换硬件、转 ONNX、做量化都要多折腾一轮为单类检测不值。SSD 和 EfficientDet 的工程生态这几年收缩明显新工具链支持弱。真正稳妥的是 YOLO 系尤其 Ultralytics 的 YOLOv8训练脚本几行起导出 ONNX 和 TensorRT 有现成接口。社区里搜「深度学习实战项目案例」类的文章基本都跑在它上面踩坑很容易找到答案。YOLOv8 相对 v5 对这个任务最大的价值是 anchor-free 检测头。水表窗口宽高比受表型影响能从 3:1 到 8:1anchor-free 对这种跨度更省心不用费劲调 anchor 参数。模型规格我习惯先上 YOLOv8n约 3.1M 参数跑通全流程验证数据没毛病再换 YOLOv8s 提精度。别一上来就上 x单类小目标语义简单容量大在小数据集上更容易过拟合部署体积和推理耗时都吃亏。imgsz 默认 640 够用窗口占比实在太小再试 960 或 1280代价是训练和推理时间变长。有个易被忽视的点定位网络虽然只出一类目标但不能只看 mAP。它的框质量直接决定识别网络的输入质量——框太松带进表盘刻度框太紧切掉半截数字识别跟着错。3.3 节我会给具体的验收方法。提示训练分辨率最好和部署分辨率保持一致训练 640 部署 960 这种错配是定位性能掉一截的隐形原因。2.2 识别网络选型固定几何分割 逐位分类还是序列识别识别网络拿到的是定位网络裁剪出来的窗口图。水表的读数窗口结构高度固定一串数字轮横向排列黑底白字或白底黑字几个整数位加几个红字小数位位与位之间有隔板或分隔线。这种强结构决定了完全不用走通用不定长 OCR 的路子。主推方案是固定几何分割加逐位分类透视校正后数字位的横向位置基本稳定按比例或按投影把每个数字位切出来送进一个小分类网络判 0-9。切块后共享同一个分类器实现简单数据量要求低每个位数百张样本就够。现场排查也方便——哪一位错了单独看那一张 cell 就知道是分割歪了还是模型没学会。这也是水表离线识别方案里最常被采用的路线。序列识别方案CRNN 加 CTC我也试过适合数字粘连、位数不固定的场景比如燃气表、转速表。水表最后一位经常处于半翻转状态rolling digitCTC 的时序对齐会把中间态糊过去反而没有逐位分类直接可控。除非要通吃几十种位数、字号都不同的表否则序列模型不划算。还有朋友问要不要上 Transformer 或预训练 OCR我的回答是不要水表数字只有 10 类、字体简单、位置固定一个几十万参数的小 CNN 就能到 99.5% 的逐位准确率大模型只增加推理延迟和部署体积。2.3 两段管线怎么咬合裁剪、透视校正与输入归一化两个网络不是独立跑完就完事中间那段数据处理决定识别网络能不能吃饱。定位框输出之后水表拍照的透视变形还在框出来是斜的、梯形的直接裁剪送识别网络数字位的相对位置全乱固定几何分割立刻失效。我固定插三步透视校正、旋转摆正、尺寸归一。透视校正用 OpenCV 的 getPerspectiveTransform把窗口四个角点映射成一个正矩形。注意定位网络若输出旋转框要用旋转框角点若输出轴对齐框遇到俯拍角度大的图比如井坑里朝下拍得先估计整体旋转角再校正。这一步很多人漏结果识别网络看到的数字永远是歪的然后回头死调识别模型方向就错了。校正后按高度统一缩放比如高 64 像素、宽按原宽高比等比例保证数字笔画不被压扁。输入用灰度还是三通道取决于识别网络设计我推荐灰度能滤掉表盘颜色噪声也更方便压光照差异。裁剪还有个容易被忽略的点定位框和数字边界之间留不留边距。框紧贴数字时透视校正后边缘会混入隔板阴影框留 2-3 像素边距数字整体更居中识别更稳。但这个边距必须在训练识别网络的数据里一致存在否则推理时输入分布漂移。「定位输出 → 裁剪 → 校正 → resize」必须封装成同一段代码训练和部署共用这是整个项目里最值回票价的一条工程纪律。3. 定位网络落地用 YOLOv8 训练水表读数窗口检测器定位网络是整条管线的前门门没守住后面全白搭。这一章从数据讲到训练再到验收每一步都给可直接用的配置。3.1 数据与标注从现场拍照到可训练的数据集数据是这个项目里最花时间、也最决定成败的一环。我的经验是最少收集 1000 张真实拍摄的水表图覆盖不同楼道、不同表型、不同时间段的光照。纯靠网上找图不够——水表安装环境的背景噪点、表盘磨损、玻璃纹路只有现场数据才带得出来。预算有限时优先保证角度和光照的多样性比单纯堆数量更重要。标注用 labelImg 或任何支持 YOLO 格式的工具都行类别就一个 counter_window。这里有一条血泪经验标注框要贴住读数窗口的黑色边框内侧也就是数字本身所在的矩形别把整个表盘、玻璃外框也框进去。标注不一致比标注少更坑——模型会学到「大概的窗口」定位框时松时紧识别网络跟着遭殃。建议两个人标、一个人复核开工前定一套统一的贴边规则。增强参数也要按水表场景调YOLO 自带 mosaic、旋转、亮度扰动但水表要加大亮度扰动和模糊模拟现场有水雾和轻微失焦减弱翻转增强。水表窗口里的数字没有镜像对称性水平翻转虽然翻倍数据却可能让模型把数字位顺序学反。我一般关掉 fliplrflipud 只保留竖直方向的小扰动。3.2 最小可跑的训练脚本配置、命令与参数说明环境上不用折腾conda 建一个 Python 3.10 环境pip install ultralytics torch 就够这正是很多人问的深度学习环境配置里最简单的场景之一。数据准备好后先建 dataset.yaml 指向训练集和验证集# water_meter.yaml path: ./wm_dataset train: images/train val: images/val names: 0: counter_window训练脚本如下有 NVIDIA GPU 直接跑没有就用 CPU 训小模型只是慢# train_loc.py from ultralytics import YOLO model YOLO(yolov8n.pt) # 用 COCO 预训练权重做迁移学习 model.train( datawater_meter.yaml, epochs120, imgsz640, # 窗口占比小就提到 960 batch16, patience15, # 15 个 epoch 不涨就早停 device0, workers4, hsv_h0.02, # 水表偏灰色调扰动要小 hsv_s0.3, # 饱和度扰动可以稍大 hsv_v0.4, # 亮度扰动是重点现场光照差异大 degrees5, # 小角度旋转别开太大 fliplr0.0, # 关掉水平翻转保持数字顺序 projectruns/wm_loc, nameexp1, )几个参数为什么这么设hsv_v 开到 0.4因为楼道灯光和闪光灯补光会造成同一表位的亮度差两个量级亮度增强是泛化关键degrees 只给 5 度透视校正环节会处理大角度训练时旋转过大反而让检测框学歪fliplr 必须关水表数字位有固定左右顺序水平翻转等于给模型灌错误先验。patience 15 够用水表数据量不大80-100 epoch 基本收敛早停能省不少跑批时间。训练完看 runs/wm_loc/exp1/weights 下的 best.pt 和 last.pt。best 用于验证和部署last 用于中途继续训练别用错。3.3 定位结果的验收置信度、IoU 与框的贴边程度模型训完别急着接识别网络。先在验证集跑一遍评估重点看三个东西mAP0.5、置信度分布、框的贴边程度。mAP0.5 对单类检测来说超过 0.95 才算及格。低于这个数先别调模型回头查标注——绝大多数情况是标注框不齐。置信度分布看直方图正常应该双峰正样本集中在 0.8 以上背景误检集中在 0.2 以下。如果正样本大量落在 0.5-0.7说明训练样本里窗口形态太杂或者增强参数太激进优先排查数据而不是加训练轮数。贴边程度没有现成指标我习惯把检测框画在原图上人眼抽查几十张看框是否包住完整数字又不带多余表盘。再用脚本统计框的宽高比分布水表窗口宽高比通常在 3:1 到 8:1如果出现大量大于 10:1 的框多半是把多位数区域连在一起回查标注。定位网络验收通过的标准是它在验证集和现场样张上都能稳定给出紧贴窗口的框置信度不低于 0.85。4. 识别网络落地把窗口裁剪图变成数字读数窗口拿到了接下来这一章解决「读出来」的问题。识别网络本身不难难在建模方式、数据切分和后处理。4.1 识别任务的三种建模方式与选型理由窗口裁剪图到手后怎么把一串数字读出来常见有三种建模方式。第一种是把整个窗口当作一个整体类别枚举所有可能的读数组合。水表常见是 5 个整数位加 3 个小数位组合空间 10 的 8 次方根本枚举不完直接排除。第二种是固定几何分割加逐位分类也就是我推荐的主方案。窗口结构固定数字位间距在透视校正后基本等宽按位置比例或按投影切出每个数字的 cell每个 cell 交给一个小分类网络判 0-9。红字位单独设类也可以但更稳的做法是红黑分开处理识别网络只判 0-9红字由后处理根据颜色特征或固定位置关系区分。第三种是 CRNN 加 CTC 的序列识别前面说过它适合不定长场景水表位与位有明确分隔用它是杀鸡用牛刀且半数字问题处理更麻烦。除非你们要同时通吃十几种不同位数和字号的水表否则不用上序列模型。定了逐位分类路线后识别网络没必要设计得多复杂。数字字符结构简单、类别只有 10 个3-4 层卷积加两层全连接就够也不需要上 Halcon 那套商业机器视觉工具链开源方案完全能打。模型复杂度上来了反而在小样本上过拟合——每个数字位的训练样本通常只有几百到几千张。4.2 训练识别网络PyTorch 分类器的完整脚本推荐的数据组织方式定位网络跑出窗口框后按 2.3 节的流程做透视校正再按位切割每个 cell 存成 28x28 灰度图文件名带标签比如5_0000123.jpg表示标签 5。这样数据检查、后续扩展都很方便。分类网络用 PyTorch 写一个极简但够用的结构# train_rec.py import torch import torch.nn as nn class DigitCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 28 - 14 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 14 - 7 nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), # 7 - 1 ) self.classifier nn.Linear(128, num_classes) def forward(self, x): x self.features(x).flatten(1) return self.classifier(x)数据加载的核心逻辑读图、转灰度、resize 到 28x28、归一化到 [0,1]。cell 切割建议用投影法而不是纯按像素比例切——先做二值化再统计每一列的黑色像素数数字之间的空白列会把位与位自然分开比按窗口宽度除以位数更抗畸变。这个切法在透视校正不完美时尤其有用。# 训练循环关键片段 model DigitCNN(10) opt torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss(weightclass_weights) # 类别不平衡时加权 for epoch in range(30): for x, y in dataloader: # x: [batch, 1, 28, 28], y: [batch] opt.zero_grad() loss criterion(model(x), y) loss.backward() opt.step()优化器用 Adam初始 lr 1e-3batch 64epochs 30 左右就够。类别不均衡要留意整数位的 0-9 分布大概均匀但如果把红字位也当普通位训练样本量会少一截此时给红字位样本加权或者红字位单独训一个小分类器。class_weights 按样本量倒数归一化即可。这个网络在干净数据上很容易训到 99.5% 以上的逐位准确率但别高兴太早——逐位 99.5% 不等于整表读数准确率8 位连乘下来整表只有 96% 左右对计量场景不够后处理必须兜底。4.3 后处理半数字、概率校准与最终读数拼接逐位分类输出的是 10 维概率向量后处理做三件事置信度判断、半数字处理、读数拼接。置信度判断最简单某一位的最大概率低于阈值我常用 0.7就标记为 low_conf整条读数进人工复核队列。计量数据出错比多复核几次严重得多别硬猜。半数字是水表识别的老大难。机械水表最后一位数字轮永远在缓慢翻转拍到的可能是「5」和「6」之间的任何中间态。分类器在这种输入上表现为双峰概率5 和 6 各占 0.45 左右。我的处理逻辑top1 和 top2 的概率差小于 0.15说明拍摄瞬间数字正在跳变取概率更高者作为读数同时标记为 transitional交给上层做多帧确认。多帧策略很简单连拍三张两次以上结果一致才提交否则人工确认。注意0.15 这个阈值先拿验证集校准再按现场漏报情况微调别拍脑袋用固定值。读数拼接要把几何位映射回数值透视校正时记录每个 cell 的原始位置序号整数位按顺序拼成字符串红字位作为小数部分。注意方向——部分水表读数窗口的数字是从右往左进位拼接前用一张已知读数的样张验证位的左右顺序别把 12345 拼成 54321。这步出错时往往整条管线指标都好看唯独读数全错最容易被忽略。5. 水表识别避坑指南5 个真实踩坑记录和排查思路这个项目每个环节都有坑下面五条是我自己翻过车、也帮别人排查过的典型问题按「现象 → 原因 → 解决」写清楚。5.1 玻璃反光把定位框带偏识别跟着翻车现象训练集 mAP 0.97现场样张里定位框有时飘到表盘外侧或只圈住半个窗口识别网络拿到残缺窗口后读数随机错。原因反光在玻璃上形成高亮光斑亮度梯度比数字边缘还显著检测器被高亮区域吸引。训练数据里反光样本太少模型没见过「窗口在光斑后面」的形态。解决收集现场反光样本加入训练必要时在增强里加随机高光斑模拟——在图上随机位置叠加一个椭圆高亮。另一个廉价手段是定位前先做高光抑制把亮度超过阈值的区域局部压暗再送定位网络框漂移率能明显下降。5.2 最后一位半数字被读成「两边都不是」现象最后一位数字错得毫无规律今天 4 明天 5概率输出总是双峰两个最高类别都在 0.4 上下。原因机械水表最后一位轮盘一直在转拍摄瞬间停在非整数字位置分类器从没见过这种中间态。这是物理结构决定的问题不是模型能力问题再怎么加数据也消不掉双峰。解决按 4.3 节标记为 transitional不强行输出单一数字。落地配合多帧策略连拍三张取多数一致的结果或按业务规则两次读数差值小时按上一轮读数方向做圆整。5.3 红字小数位和黑字整数位互相混淆现象小数位被读成整数位读数整体差一个数量级但逐位准确率看着还挺高。原因红字位和黑字位在图像上除了颜色没有明显边界固定几何分割容易切歪红字位。如果识别网络用灰度输入红黑信息被抹掉模型只能靠形状猜红字和部分黑字形状相近就混淆。解决定位网络框出窗口后在窗口内部用颜色分量单独检测红字区域——红色通道减去绿色通道的差值图做阈值分割得到红字起始位置再据此调整几何分割边界。识别网络用灰度输入也没关系后处理按颜色决定小数点位置而不是依赖分割位置。5.4 训练数据指标 99%换现场就崩光照域差现象验证集逐位准确率 99.6%上到现场第一个楼道就掉到 85%且每个位置的错误分布都不一样。原因训练数据来自晴天白天拍摄现场是昏暗楼道加闪光灯补光色温、曝光方向全变了。这是典型的域偏移水表玻璃反光在不同光照角度下差异尤其大。解决把现场不同光照条件的数据单独分一个验证集不再用原来的划分评估。然后做两件事一是增强里把亮度、对比度、色温扰动拉大二是收集现场代表性坏样本做针对性微调不用重训。如果现场光照实在多变识别前加自适应直方图均衡CLAHE对灰度归一化能压掉一大部分光照差异。5.5 定位框贴得挺准识别还是错裁剪和 resize 的隐性偏移现象定位框肉眼看着没问题调试过程中识别准确率忽高忽低同一个样本有时对有时错。原因调试时裁剪框有时来自模型输出、有时来自人工标注两者和后续 resize 的方式不一致。比如模型框带 1-2 像素白边、人工标注不带resize 到 28x28 后数字的相对缩放和位移全变了分类器的输入分布被暗中改变。解决把「定位输出 → 裁剪 → 透视校正 → resize」写成固定函数调试和部署用同一套代码任何一步改参数都重新跑验证集。这是典型的黑匣子排查识别网络没问题问题在它没见过的数据处理路径上。把管线固化成版本号每次实验记录用的是哪个版本的预处理后悔药就在版本日志里。6. 部署验证与进阶把两个网络串成可交付的读数管线6.1 ONNX 导出与离线推理部署两个网络训好后我统一转 ONNX 再部署。现场环境不装深度学习框架全家桶只装 ONNX Runtime依赖干净、启动快CPU 跑小模型够用。yolo export modelruns/wm_loc/exp1/weights/best.pt formatonnx imgsz640import torch model DigitCNN(10) model.load_state_dict(torch.load(digit_cnn.pt)) model.eval() torch.onnx.export(model, torch.randn(1, 1, 28, 28), digit_cnn.onnx, input_names[img], output_names[logits], dynamic_axes{img: {0: batch}, logits: {0: batch}})ONNX Runtime 推理时定位网络输出是 1xNx6 的检测结果按 conf 大于 0.5、IoU 0.45 做 NMS识别网络输出 10 维 logits 接 softmax 拿概率。单张图串行推理在 CPU 上大约几十到一两百毫秒水表抄表场景完全够不用上 GPU。6.2 用「读数误差」而不是分类准确率验收逐位准确率会骗人8 位数字逐位 99.5%整表正确率也只有 96%。抄表业务真正关心的是读数误差——输出读数和人工读数的差值。验收时按误差分桶统计完全一致、差 0.1、差 1、差 10、乱码。差 10 级别的错误多半是位序拼接问题差 0.1 级别的多在半数字位上。先定一个可接受的读数误差率比如 0.5%再反推逐位准确率目标别反过来被模型指标带跑。6.3 进阶技巧多帧投票、自检和视角校正管线稳定后加三道保险。多帧投票最有效现场连拍 3-5 张取众数半数字位随时间走动多次采样天然消掉跳变。自检靠几何约束窗口宽高比、读数位数都要落在合理区间不满足就触发重拍或人工介入。大角度俯拍时还可以用表盘外圈椭圆做仿射变换先把视角拉正再送两个网络定位和识别都受益。我现在的习惯是每到一个新现场先采 50 张图跑一遍完整管线看读数误差分布再决定要不要微调模型。模型不是训完就交付的现场反馈才是迭代入口。希望帮到你。本文还有配套的精品资源点击获取
返回列表