ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

水表识别实战:从Circle定位到极坐标展开的完整方案

水表识别实战:从Circle定位到极坐标展开的完整方案 简介面向计算机视觉与OpenCV实践者的水表数字识别项目资源聚焦水表刻度与数字的自动识别流程涵盖图像灰度化、二值化、滤波去噪、Canny边缘检测、轮廓提取以及基于Tesseract的OCR字符识别等核心环节适合有一定Python基础并希望接触实际图像处理项目的开发者。压缩包仅1.32MB共13个文件其中包括7张水表样本图片、4个XML配置文件、1个主程序Circle.py和项目元文件代码与图片分离、结构清晰便于直接运行并分析识别效果。已有593人学习下载。资源还原了一个较完整的水表读数识别方案从预处理到指针刻度定位再到数字识别均有可借鉴的OpenCV写法并可通过调整参数应对光照不均、背景噪声等常见干扰帮助读者掌握从图像输入到结果输出的整套实现思路。1. 水表识别项目在真实场景里翻车多半是没先把 Circle 这张牌打出去做水表识别的人很容易先入为主不就是识别几个数字吗拿现成的 OCR 或者 MNIST 那套手写数字识别模型一换本地一跑完事。真把几十张现场照片丢进去才发现表盘反光、拍摄角度歪、数字挤成一团识别率直接掉到六成以下。水表识别真正的难点不在“数字分类”本身而在“数字是从一个圆形表盘上被抠出来的”。标题里的 Circle指的是这套方案里最容易被省略却最关键的工序——先定位圆形表盘区域再把弧形排列的数字展开成水平直线最后才交给数字识别模型去判断。这条路线适合三类人做智能抄表改造的做机械表离线拍照补录的以及想把手写数字识别那套经验迁移到工业表计场景的团队。下面直接按数据、训练、避坑、上线四个环节拆开讲每一段都能落到代码上。2. 数据准备与标注规范先把 Circle 区域做成干净的裁剪样本2.1 样本采集离线识别场景下的数据构成智能水表分两类一类是自带信号输出的电子表另一类是大量还在运行的机械表。后者没有通信模块最常见的技术改造方案是加摄像头或人工持机拍照离线状态下完成识别。这种“水表离线识别方案”对数据的要求和在线抄表不太一样离线数据没有实时标签流可以回灌必须一开始就按“一次采集、多次回放”的方式建库。我在实际项目里的采集样本结构是核心的正面正对表盘拍摄数字区占画面比例尽量大用可调焦距或手机贴近拍刻意拍一批侧面视角倾斜 30 度左右的样本用于对抗透视畸变逆光、窗边侧光、手电筒直射各拍一组把反光和阴影问题提前暴露出来表盘内部如果有流水计红色小指针或轮式低位数字也要单独拍几段视频方便后续做截断离线场景里拍摄质量不稳定是确定会发生的事。所以采集时不要只图“好看”要把最脏的、最糊的、对不上焦的照片也留下来。这些样本不会直接进训练集而是放进一个小众验证集里当“压力测试集”用来判断模型到底能被推到什么程度。2.2 圆盘区域定位圆心计算与极坐标展开脚本拿到表盘照片后先把变形的数字变成线性的。我们以“Circle”为核心的第一步不是直接做检测而是把整个圆形表盘区域定位出来并展开。常见做法是用 OpenCV 的霍夫圆检测但在表盘边缘被遮挡或者反光成一整片的时候霍夫圆检测很容易翻车。我一般会选择半自动标定人工在表盘边缘点三个点根据三点求外接圆把圆心和半径算出来稳定得多。import cv2 import numpy as np def circle_from_3_points(p1, p2, p3): # 三点求外接圆返回 (cx, cy, radius) x1, y1 p1 x2, y2 p2 x3, y3 p3 a np.array([ [2 * (x2 - x1), 2 * (y2 - y1)], [2 * (x3 - x1), 2 * (y3 - y1)] ]) b np.array([ x2**2 y2**2 - x1**2 - y1**2, x3**2 y3**2 - x1**2 - y1**2 ]) cx, cy np.linalg.solve(a, b) radius np.sqrt((cx - x1) ** 2 (cy - y1) ** 2) return int(cx), int(cy), int(radius) def unwrap_table(img, cx, cy, radius): # 极坐标展开把圆形表盘变成矩形图数字从弧线变直线 dsize (radius * 2, radius) unwrapped cv2.warpPolar( img, dsizedsize, center(cx, cy), maxRadiusradius, flagscv2.INTER_LINEAR cv2.WARP_POLAR_LINEAR ) return unwrapped这段代码的逻辑分两步先由人工点选的三个边缘点解出圆方程得到圆心和半径再调用warpPolar把圆环区域展开成矩形。展开后数字原本沿圆弧排列现在变成水平排列后续做字符切分时按列切就行不需要处理“数字是歪的”这种几何问题。radius * 2的宽度和radius的高度是常用设置保持半径方向分辨率不丢失数字高度大约对应 32 像素左右正好适合后续 CNN 模型的输入尺寸。如果表盘数字本身很小可以把这个尺寸乘以 2 再展开识别率和推理耗时会线性变化需要自己权衡。2.3 标注规范单字符标注比整体识别更稳数字识别的标注方式有两种主流选择一种是直接把整段数字框出来做序列识别CRNN 路线另一种是把数字一位一位切开做单字符分类。我在机械水表项目里更倾向后者原因在于水表数字是固定在拨轮上的位与位之间天然有刻度线单字切分后的样本干净、好标注、也方便做投票纠错。目录结构可以参考这个样例datasets/ real/ # 真实表盘展开图 0/ 1/ 2/ 3/ 4/ 5/ 6/ 7/ 8/ 9/ reject/ synth/ # 合成表盘数字 0/ 1/ 2/ 3/ 4/ 5/ 6/ 7/ 8/ 9/ reject/ test/ # 压力测试集含反光、倾斜、污损标注时注意两类特例数字只露出一半的视觉上看像 3 又像 8不要硬标成 3 或 8而是放进reject类表盘玻璃上有水垢或霉斑的数字本身清晰但背景脏也应该进reject。新增这一类“拒识类”是表计识别与普通 OCR 最大的差别它在部署阶段能拦掉大量低质量样本比强行提高模型阈值更可靠。3. 数字识别模型训练选型依据与三个必调参数3.1 从手写数字识别到水表数字为什么不能直接套 MNIST很多团队第一步想到的是跑通 MNIST 手写数字识别那套流程因为数据是现成的模型也只有几层网络训练几分钟就能出结果。把训练好的模型直接跑在展开后的表盘图上效果却常常惨不忍睹。原因在于两类数据的分布相差太远水表数字等宽、字体固定、黑底白字还会带上玻璃反光和拨轮的阴影而 MNIST 是白底黑字的手写体笔画粗细和位置都不固定。CNN 学到的底层特征看起来很接近但实际决策边界完全不同。正确的做法有两个来源混合训练一是真实表盘展开图数量少但价值高二是用印刷字体加随机背景、透视变换、亮度扰动生成的合成图。合成数据可以把数量做到几万张真实数据哪怕只有三五百张也能把模型从“见过这种分布”拉回到“见过真实分布”。3.2 最小训练脚本PyTorch 实现网络结构不需要太重。ResNet 级别的深度对单字符识别来说是浪费一个小型 CNN 配上合理的输入尺寸就能跑出不错的效果。下面这版是项目里实际用的简化版import torch import torch.nn as nn import torch.nn.functional as F class DigitNet(nn.Module): def __init__(self, num_classes11): # 0-9 reject super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))模型的核心设计是三层卷积加两层池化最后用全局平均池化替代了全连接层这样不管输入尺寸漂移多少classifier层的参数量都不变。训练参数方面我习惯用 Adam 优化器初始学习率 1e-3每 15 个 epoch 衰减为原来的 0.5并把输入图像固定到 32×16 或 48×24。展开后的数字高度大约等于宽度的一半按这个比例 resize不要把图强行压成正方形否则数字会被纵向拉扁。3.3 精度不够时的三个必调参数第一输入分辨率与字符宽度。展开图数字往往只有 20 来个像素宽强行缩到 28×28 会丢失笔画细节。先用代码统计每张展开图里数字连通域的宽高分布再决定缩放目标。数字宽度在 12 到 20 像素之间就选 48×24如果低于 10 像素先放大再缩放不要直接进模型。第二BatchNorm 的推理一致性。训练时 BatchNorm 会统计当前 batch 的均值和方差部署推理时若忘记切换到model.eval()会沿用训练时的统计模式。fake 的统计结果在遇到反光样本时表现差异极大。这个坑在分类任务上不容易暴露因为数字识别的准确率本来不是百分之百几个点的回退很容易被当作模型能力不足而误判。第三数据增强中的反向对比。真实表盘白字黑底和黑字白底都有合成数据如果只生成一种底模型在另一种底上的表现会退化到随机水平。我习惯做随机反色增强把输入像素值乘上 -1 再归一化相当于在训练过程中让模型同时看到正片和负片。增强强度不需要太高随机对三成图片做反色就足够。4. 避坑指南水表数字识别项目里最常见的五处踩坑记录4.1 反光导致二值化大翻车现象用固定阈值把表盘图转成黑白图反光区域的数字和背景糊成一团识别准确率暴跌。原因水表玻璃是曲面灯光或自然光会在玻璃表面形成大面积白色高光让局部像素分布直接饱和。固定阈值只能处理光照均匀的理想图片一遇到反射就成了玄学。解决不要在全局做二值化把“二值化”从预处理阶段去掉只做局部对比度归一化。用 OpenCV 的createCLAHE对灰度图做限制对比度自适应直方图均衡化再进模型。神经网络自己有能力学习光照不变性前提是你不要用二值化把信息提前丢掉。4.2 红色水流计数字被当成读数现象模型输出一串带七八位的结果其中最后两三位总是跳动或者计算错误仔细核对发现多读了红色指针旁边的数字。原因机械水表表盘上除了黑色主读数字还有红色数字或红色指针区域。展开整个圆盘后红色数字也进入了检测区域模型没有语义概念会把所有数字都当成读数。解决在展开图上按高度范围裁剪只保留主读数字的带状区域。这个带状区域的位置可以通过固定比例拿到例如圆环展开后主读数字通常位于上三分之一的区域内。裁剪完再送模型比靠模型自己去分辨颜色可靠得多。4.3 展开后数字宽度不均现象字符切分时有时候把两个数字切在一起有时候把一个数字切成两半导致识别结果长度不稳定。原因机械表拨轮数字即使在静止时相邻数字之间也可能存在半格位状态——一个字符已经完全转出另一个字符只露出一半。数字的实际宽度并不等于字体设计宽度。解决不要依赖固定宽度切分而是用连通域分析找到每个字符的左右边界再以中位数字宽度作为先验把细碎的连通域合并把过宽的连通域按等距分裂。这一步处理完之后再进模型识别结果的稳定性会提高很多。4.4 合成数据分布太干净一换光照就失灵现象模型在测试集上准确率 98%到客户现场换成室内日光灯准确率掉到 85%。原因合成数据只用了几种系统字体背景是纯色或者简单渐变没有模拟玻璃厚度、弧度、灰尘和水垢。模型学到的是“干净环境下的数字模板”而不是“真实表盘上的数字”。解决每张合成图都叠加随机透视变换、运动模糊、高斯噪声并随机生成一块半月形的高光区域模拟玻璃反光。做这一层的增强代码量不大但对真实场景的泛化能力提升非常明显。4.5 置信度很高但结果是错的现象模型对某一位数字输出 0.94 的高置信度人工复核却发现数字识别错误。原因数字“0”和“6”、“3”和“8”在一半显示状态下视觉特征的边界非常模糊。模型在高置信度下也可能犯错因为它学习到的训练样本里这类边界本来就不一致。解决处理方案分两层。第一设置一个足够高的拒识阈值比如 0.92 以下一律返回“无法识别”宁可漏报也不误报。第二部署时连续抓拍多帧逐位投票。机械表拨轮每秒都在微动连续多帧取众数后单帧异常值的影响会被压到最低。5. 上线前的最后一步用三帧投票把准确率钉到可交付离线识别场景里最怕的不是识别得慢而是批量识别结果里混着几十个错数等人工复核时已经找不到是哪块表。我最后的习惯是按“对齐-推理-投票”三层结构收尾。对齐是为了让多帧画面处在同一个坐标系下。使用第 2 节里的人工圆心标定方法把每一帧展开图的起点对齐到同一根角度参考线通常选刻度 0 作为起始位置。如果每帧图都是随便拍的数字会来回偏移投票就失去意义。from collections import Counter def vote_results(frame_results, topk3): # frame_results: list of list, 每一帧是 [digit, digit, digit, ...] final [] for pos in zip(*frame_results): counter Counter(pos) digit, votes counter.most_common(1)[0] # 如果最高票数不占多数说明这几帧不一致标记为拒识 if votes topk: final.append(?) else: final.append(str(digit)) return .join(final)代码的逻辑是针对每一个数字位把连续几帧的识别结果做一次投票表决。只有同一结果出现至少 3 次才把它当作最终读数否则输出?等待人工确认。topk参数实际项目里设为 3 或者 4配合拒识类一起使用可以做到 300 个表位里只挑出两三个低置信度的表号让人工复核。这个方案跑通后我很长时间没有换过底层模型结构因为真正提升交付质量的不是把单帧准确率从 98% 推到 99%而是把取样次数拉高、把错误判定统一交给投票机制处理。水表识别和 MNIST 那类手写数字识别最大的不同就在这里现实场景允许你多测几次却很难接受一个查不出来的错数。希望你也能在自己的照片采集和标注环节提前把这一步安排进去会省去大量深夜对数据的麻烦。本文还有配套的精品资源点击获取
返回列表