尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

水表识别双网络实战:定位+识别与坐标标注全解析

发布时间:2026/9/26 13:50:38

资讯中心
01
ARTICLE

水表识别双网络实战:定位+识别与坐标标注全解析

水表识别双网络实战:定位+识别与坐标标注全解析
简介面向深度学习视觉应用场景项目以定位网络识别网络的两阶段方案实现水表数字自动读数。定位网络负责从复杂背景中框出表盘区域识别网络进一步提取数字序列两阶段解耦设计既降低训练难度也便于独立调优与替换模块。压缩包共55个文件整体仅144KB包含18个Python源码、13个pyc编译文件、14张JPG表盘样本并配有XML配置、README说明及辅助数据目录按base、seg、utils等功能模块划分覆盖数据预处理、模型搭建、训练与推理全流程可快速定位入口脚本。已有128人学习浏览适合计算机视觉初学者或算法工程师参考完整的双网络工程实现。下载后可直接阅读网络定义、训练配置与数据加载逻辑结合自带表盘图片跑通识别管线无需从零搭建环境也能为自制同类读数识别项目提供可复用的代码骨架。1. 水表识别项目拿到手先跑通双网络定位加识别为什么比单模型稳抄表照片里水表读数区域往往只占画面很小一块而且表盘角度歪、有反光、数字连在一起直接拿一个端到端模型去识别经常把边框上的铭牌数字也读进来。这份 WaterMeter-master 工程把问题拆成两步先用一个定位网络从整张照片里回归出水表读数区域给你四点坐标框再用第二个识别网络只对这个区域做数字读取。资源里自带带坐标标注的真实表盘图片数据集训练代码按定位、识别两条链路分开组织拿到手先跑test.py看一遍完整流程再考虑换自己的数据。适合做毕设、工业表计识别落地验证以及想复现双网络深度学习项目的同学。我把定位网络的坐标格式、数据提供器、两段训练的入口和常见翻车点都拆开讲一遍。2. 读懂坐标标注与数据提供器把四角框变成网络输入2.1 文件名自带坐标四点标注的解析方式打开newdatares文件夹看到的文件名长这样1271_0706140506449)(3,33 6,69 138,27 139,62 )[3,33 6,69 138,27 139,62 ].jpg一眼看过去像乱码其实它是「样本名 坐标」拼出来的。括号里每一组是两个数字四个点连起来就是一个四边形按顺序分别是左上、右上、右下、左下或顺时针取决于标注工具导出顺序。我一般先写个解析脚本把坐标抠出来import re def parse_wm_filename(filename): # 从文件名中提取两组四点坐标例如 # 1271_0706140506449)(3,33 6,69 138,27 139,62 )[3,33 6,69 138,27 139,62 ].jpg pattern r\)\(([\d,\s])\)\[([\d,\s])\] match re.search(pattern, filename) if not match: return None def to_points(s): nums list(map(int, re.split(r[,\s], s.strip()))) # 每两个数字为一个点: [x1, y1, x2, y2, x3, y3, x4, y4] return [(nums[i], nums[i 1]) for i in range(0, len(nums), 2)] return to_points(match.group(1)), to_points(match.group(2))逻辑说明这个正则把)(和)[中间的坐标串切出来然后按“两个数字一个点”还原成四个坐标点。文件名里出现了两组一样的坐标是因为标注工具同时输出了原始坐标和归一化后的坐标副本实际使用时只用一组避免差异。参数说明坐标是绝对像素还是归一化值取决于WM_config.py里的设置。默认在image_pre.py预处理阶段会统一除以图片宽高做归一化所以训练时网络回归的也是归一化坐标。如果你自己的数据集标注是普通x1,y1,x2,y2矩形框把这个函数改成按矩形顺序读取即可但后面模型输出层需要相应调整。2.2 WM_data_provider 与 D_data_provider两条数据管道的分工工程里有两个数据提供器很多人第一次看会疑惑为什么搞两套。因为定位网络和识别网络的输入完全不同文件服务对象输入内容输出WM_data_provider.py定位网络整张表盘图 四点框坐标归一化图片、坐标回归标签D_data_provider.py识别网络裁剪后的数字区域图数字类别标签或序列标签WM_data_provider负责从newdatares里读原始图片和坐标做随机扰动、缩放、归一化然后喂给定位网络训练。D_data_provider则是先按定位结果把数字区裁出来再对裁剪图做二次预处理喂给识别网络。这个拆分是合理的因为定位网络要看到整张图才能学到“表盘在哪”识别网络只需要盯着数字区如果两阶段都用同一套数据增强识别阶段反而会被背景噪声带偏。我用这类工程时的习惯是定位网络的数据提供器里做随机裁剪、亮度抖动和轻微旋转增强对拍摄角度的鲁棒性识别网络的数据提供器只做对比度归一化和尺寸统一因为数字区一旦裁好几何变化已经很小再做大幅度旋转反而破坏数字结构。2.3 image_pre.py 里做了什么预处理顺序和参数utils/image_pre.py和image_preprocess.py是图像预处理的核心主要干三件事缩放、归一化、去均值。def preprocess(image, target_size(224, 224)): # 1. 先缩放到固定尺寸注意这里用双线性插值保持数字边缘不出现明显锯齿 image cv2.resize(image, target_size, interpolationcv2.INTER_LINEAR) # 2. 转到 RGB 并转 float范围从 0-255 映射到 0-1 image cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 # 3. 去均值均值从训练集统计得到常见做法是 [0.485, 0.456, 0.406] mean np.array([0.485, 0.456, 0.406], dtypenp.float32) image - mean return image逻辑说明先缩放是为了让网络输入尺寸固定方便组 batch再归一化到 0-1 可以加速收敛去均值是让每个通道的输入分布中心归零这在小数据集上能明显减少过拟合。image_pre.py里还有对坐标做同步缩放的处理即图片 resize 时对应的四点坐标也要按同样比例缩放否则标签和图像就错位了。这个细节最容易漏很多新手直接只对图做 resize坐标还是原图尺寸训练出来的框永远对不上。3. 训练流程拆开看定位网络与识别网络的模型和训练入口3.1 base_model.py 和 base_train.py统一的训练骨架base文件夹下是base_model.py和base_train.py这两个文件是整个工程的骨架。定位网络wm_model和识别网络d_model都继承自base_model共用一套构图、保存、日志逻辑。class BaseModel(object): def __init__(self, config): self.config config self.input tf.placeholder(tf.float32, shape[None] config.input_size) self.label tf.placeholder(tf.float32, shape[None] config.label_size) self.is_training tf.placeholder(tf.bool) self.global_step tf.Variable(0, trainableFalse) def build_graph(self): # 子类必须实现搭建前向网络并计算 loss raise NotImplementedError def train_one_step(self, sess, batch_data): # 子类复用执行一步梯度更新并返回 loss _, loss_val sess.run( [self.train_op, self.loss], feed_dict{self.input: batch_data[image], self.label: batch_data[label], self.is_training: True} ) return loss_val逻辑说明BaseModel把网络搭建和训练更新分开子类只需要实现build_graph()定义自己的网络结构训练循环、模型保存、tensorboard 日志都从基类继承。这样做的好处是定位和识别两个网络虽然结构不同、loss 不同但训练流程完全一致改模型时不用动训练脚本。参数说明config.input_size和config.label_size在两个子网络的 config 里分别定义。定位网络label_size是 8四个点 × x/y识别网络label_size通常是数字类别数。如果识别网络是按每一位数字分类还要在D_config.py里定义数字位数比如digit_num 6表示最多识别 6 位读数。3.2 wm_train.py定位网络的训练细节wm_train.py是定位网络的训练入口整个训练命令大概是python wm_train.py --gpu 0 --batch_size 16 --learning_rate 0.001 \ --max_epoch 100 --data_dir ../newdatares训练核心逻辑是把图片和四点坐标组成 batch然后计算坐标回归 loss。定位网络的输出层是 8 个神经元对应归一化后的四个点坐标loss 一般用 L2 或 smooth L1。# 定位网络前向输出: 得到 (batch_size, 8) 的坐标回归结果 pred_coords self.model.build_graph(self.input) # 常见做法是用 smooth L1 loss比纯 L2 对离群点更鲁棒 delta tf.abs(pred_coords - self.label) loss tf.reduce_mean(tf.where(delta 1.0, 0.5 * delta ** 2, delta - 0.5))逻辑说明tf.where实现了 smooth L1小于 1 的误差用平方放大大于 1 的误差用线性衰减这样个别标注偏差很大的样本不会主导梯度。坐标回归和分类不同它没有“类别”概念输出的是连续值所以必须用回归 loss而不是 softmax 交叉熵。参数说明注意--learning_rate 0.001搭配 Adam 优化器时我一般会在 60 个 epoch 后降到 0.0003否则后期 loss 会在一个平台期震荡。如果你的数据量只有几百张batch_size建议设 8 或 16太大容易过拟合到训练集上的拍摄角度分布。3.3 d_train.py识别网络的训练细节识别网络单独用d_train.py训练它的输入不是原始图而是定位网络裁剪出来的数字区。工程里的D_data_provider已经封装好了这个过程先用训练好的定位网络对每张原图出框抠出数字区再让识别网络学习数字分类。这里有一个关键选择识别网络是输出单个数字分类还是输出一整个数字序列。wm目录下同时存在wm_trainer和d_trainer说明这个工程把两个阶段完全分离了。# 识别网络的数据提供器: 按定位框裁剪并统一高度 crop image[int(y1):int(y3), int(x1):int(x3)] crop cv2.resize(crop, (config.crop_width, config.crop_height)) # 如果表盘数字是6位标签做成 one-hot 序列 label np.zeros((config.digit_num, config.num_classes)) for i, digit in enumerate(digits): label[i, digit] 1.0逻辑说明裁剪时我按y1到y3、x1到x3取矩形因为四点标注虽然是任意四边形但在实际表盘照片里数字区近似矩形直接取外接矩形即可。识别网络有两种常见做法一是把裁剪图整体当作一个多位数图片用 CRNN 或 LSTM 做序列识别二是按位切分每一位训练一个分类器。这份工程里的d_model.py更接近按位分类所以标签是digit_num × num_classes的二维 one-hot。3.4 test.py一次推理怎么串联两个网络test.py放在工程根目录是整个项目跑通的最后一步。它做的事可以概括为四句话加载定位模型 → 对输入图片预测坐标框 → 按框裁剪 → 加载识别模型读出数字。# 加载两个训练好的模型 wm_model WMModel(config) d_model DModel(config) # 第一步: 定位网络输出四点坐标 coords sess.run(wm_model.output, feed_dict{wm_model.input: img}) # coords 是归一化坐标, 乘回原图宽高 x1, y1, x2, y2, x3, y3, x4, y4 coords[0] * np.array([W, H, W, H, W, H, W, H]) x1, x3 min(x1, x3), max(x1, x3) y1, y3 min(y1, y3), max(y1, y3) # 第二步: 裁剪数字区域, 送入识别网络 crop img[int(y1):int(y3), int(x1):int(x3)] result sess.run(d_model.output, feed_dict{d_model.input: crop})逻辑说明代码里最关键的一步是坐标还原——模型输出是归一化的 0~1 值必须乘回原图宽高才能画框。test.py里通常会顺手把预测框画在原图上保存下来这一步对排查问题非常重要。定位网络预测的四点顺序如果不一致裁剪出来的区域可能是斜的甚至镜像的所以我在 test 脚本里加了min/max取外接矩形宁可多裁一点背景也不要裁漏数字。4. 避坑指南坐标顺序、环境版本与推理输出的三个大坑4.1 坐标顺序错位导致框旋转现象定位网络训练 loss 能降到很低但把预测坐标画到原图上框是歪的甚至点与点之间交叉形成蝴蝶结形状。原因数据集文件名里的四点坐标可能是顺时针排列也可能是左上、右上、右下、左下排列而网络输出层固定按(x1,y1,x2,y2,x3,y3,x4,y4)的顺序回归。如果标注顺序和网络回归顺序不一致网络学到的是一个错乱的映射。解决先用画框脚本把训练集每个样本的标注可视化核对WM_data_provider读取坐标后重组数组的下标顺序。一般做法是在 provider 里统一转成“左上、右上、右下、左下”再喂给网络并且在训练前跑一次可视化确认。从那以后我每次拿到新数据集第一件事就是把 20 张图的标注框画出来人工过一遍坐标顺序错了后面全白做。4.2 TensorFlow 工程跑在 TF2 环境直接报错现象import tensorflow后报ModuleNotFoundError: No module named tensorflow.contrib或者Session、placeholder找不到。原因这个工程是 TensorFlow 1.x 时代写的代码里大量使用tf.placeholder、sess.run、tf.contrib这些 API 在 TF2 里被移除或改到tf.compat.v1。解决最常见做法是装 TF 1.14 或 1.15 直接跑通。如果必须用 TF2可以在所有脚本开头加一段兼容代码import tensorflow.compat.v1 as tf tf.disable_v2_behavior()但注意tf.contrib没有兼容层如果代码里用了tf.contrib.layers需要手动替换成tf.layers或tf.keras.layers的等价函数。我的建议是这个项目整个逻辑不依赖 TF2 新特性直接用 Python 3.6 TF 1.15 最省事别浪费时间在迁移 API 上。4.3 源码和 pyc 文件不一致改了代码不生效现象我改了wm_config.py里的图片尺寸参数但训练时输出日志显示还是旧尺寸折腾一个下午才发现问题。原因工程里同时存在.py和.pyc文件比如WM_config.pyc和WM_config.py。Python 在某种情况下会优先加载已有的字节码缓存尤其是脚本顶层直接 import 同一个模块时新旧.pyc混用会导致改了源码不生效。解决清理所有__pycache__和.pyc文件然后用python -B禁止生成字节码缓存运行find . -name *.pyc -delete python -B wm_train.py --data_dir ../newdatares这属于典型的“黑匣子”问题现象非常隐蔽不排查到缓存机制基本发现不了。4.4 数字粘连导致识别网络整体错一位现象识别网络训练完单张数字识别很准但整块表盘数字一读就错比如把1914.5读成914.5或19145。原因定位框裁出来以后多位数字在图中是连在一起的如果识别网络是按“整体多位数图片”训练分类需要固定输入宽度。表盘数字位数不同有的 5 位有的 6 位宽度不一致导致归一化时数字被拉伸变形网络就分不清 0 和 6、1 和 7。解决训练识别网络时我一般先统计训练集里数字区域的宽高比分布把D_config.py里的crop_width和crop_height按中位数设置并且做 padding 而不是简单 resize——在数字区域左右各补一定宽度的背景保持数字本身的长宽比。另一个很偷懒但有效的办法是识别网络输出多一位“空白”类别让网络自己学会在不足 6 位时输出空白而不是硬把 5 位数截成 5 个类别。5. 新数据集落地画框可视化加 test.py 的三步验证法拿到一份新表盘数据集我一般不会直接训练而是先强制走一遍三步验证因为双网络模型的错误会叠加——定位框歪了一点识别网络就跟着错最后你根本不知道问题是出在定位还是识别。第一步改test.py里的模型路径和图片路径先拿原资源里的测试图跑一遍确认定位框贴合数字区域边缘。如果框明显外扩把test.py里min/max取外接矩形的逻辑去掉改用四点本身画多边形框能更清楚看到定位网络的真实回归精度。第二步把新数据集的图片放进newdatares图片命名按工程要求的坐标格式改好这里我写了一个快捷脚本专门用来给新图生成带坐标的文件名import os, cv2 def rename_with_coords(image_path, coords): # coords 是四点列表 [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] base os.path.basename(image_path).split(.)[0] coord_str .join([f{x},{y} for x, y in coords]) # 生成如: 1271_0706140506449)(3,33 ... )[3,33 ... ].jpg 的格式 new_name f{base})({coord_str})[{coord_str}].jpg img cv2.imread(image_path) h, w img.shape[:2] # 如果标注是绝对像素, 后续归一化时就除以 w, h return new_name逻辑说明这样生成的文件名能被正则在WM_data_provider里解析不需要改读取代码。注意坐标一定要原图的绝对像素值不要先归一化再存因为 provider 内部会按图片尺寸重新归一化双重归一化会导致标签范围偏差。第三步训练完定位网络后把定位输出的裁剪图全部批量保存成crops/文件夹再人工抽查 50 张裁剪图的质量。如果裁剪图里数字完整、背景干净才去训练识别网络如果超过 10% 的裁剪图缺字或带了多余表盘刻度线回头调定位网络的回归 loss 和训练 epoch而不是硬着头皮训练识别网络。画框可视化是我调试这类工程永远绕不开的习惯核心就一句话——把网络眼里看到的东西原样画出来。我用的是最朴素的 matplotlib 方案import matplotlib.pyplot as plt def draw_pred(img, coords, save_path): # coords: [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] plt.imshow(img) poly plt.Polygon(coords, fillFalse, edgecolorred, linewidth2) plt.gca().add_patch(poly) plt.savefig(save_path, dpi120)这段代码会输出一个带红色四边形框的图片。从那以后我每次迭代模型都会强制把训练集、验证集、测试集各抽出 10 张画框对比标注歪了、坐标顺序错了、归一化乘错宽高三分钟就能看出来比盯 loss 曲线高效得多。这套“定位出框 → 裁剪检查 → 识别验证”的流程希望帮你在自己的水表识别项目上少走几趟弯路。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。