news 2026/9/26 13:50:32

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
水表识别双网络实战:定位+识别与坐标标注全解析

简介:面向深度学习视觉应用场景,项目以定位网络+识别网络的两阶段方案实现水表数字自动读数。定位网络负责从复杂背景中框出表盘区域,识别网络进一步提取数字序列;两阶段解耦设计既降低训练难度,也便于独立调优与替换模块。压缩包共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, interpolation=cv2.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], dtype=np.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, trainable=False) 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, fill=False, edgecolor='red', linewidth=2) plt.gca().add_patch(poly) plt.savefig(save_path, dpi=120)

这段代码会输出一个带红色四边形框的图片。从那以后我每次迭代模型,都会强制把训练集、验证集、测试集各抽出 10 张画框对比,标注歪了、坐标顺序错了、归一化乘错宽高,三分钟就能看出来,比盯 loss 曲线高效得多。这套“定位出框 → 裁剪检查 → 识别验证”的流程,希望帮你在自己的水表识别项目上少走几趟弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 13:50:28

Matlab符号积分int函数详解:从int(x^2,x,0,1)到定积分与数值积分对比

刚接触Matlab符号计算的同学&#xff0c;十有八九都遇到过这么一幕&#xff1a;在命令行里兴冲冲敲下 int(x^2, x, 0, 1) &#xff0c;结果回车之后弹出一行红色报错—— Undefined function or variable x 。明明照着教程写的&#xff0c;怎么就不认账&#xff1f;其实问题…

作者头像 李华
网站建设 2026/9/26 13:50:23

不占本地配置的AI获客系统:云端算力与四大核心能力解析

1. 先拆掉误解&#xff1a;AI获客系统到底把活儿干在了哪里 如果是销售团队或管理层第一次听到“企业AI获客系统”&#xff0c;普遍的第一反应通常不是“能带来多少客户”&#xff0c;而是“这东西是不是又要配一台高配服务器&#xff1f;会不会占我们本地电脑的内存&#xff1…

作者头像 李华
网站建设 2026/9/26 13:49:57

糖尿病预测毕设系统:JavaFX+Python双栈机器学习闭环

简介&#xff1a;本资源是一套基于机器学习的糖尿病预测系统完整实现&#xff0c;面向计算机、人工智能、电子信息等相关专业在校学生、教师及初级开发者&#xff0c;适用于课程设计、毕业设计、项目演示与算法实践学习。系统采用Java为主开发语言&#xff0c;结合JSP前端界面与…

作者头像 李华
网站建设 2026/9/26 13:49:52

机器学习检测恶意代码:基于smali opcode与3-gram的静态检测流水线解析

简介&#xff1a;这是一套面向恶意代码检测的机器学习源码项目&#xff0c;项目聚焦Android应用smali指令序列&#xff0c;通过提取3-gram操作码特征&#xff0c;并利用TF与TF-IDF两种加权方式构建高区分度特征集&#xff0c;随后训练二分类模型并输出预测结果、TPR/FPR指标及R…

作者头像 李华
网站建设 2026/9/26 13:49:00

Wand-Enhancer开源补丁:Windows游戏辅助工具注入与拦截技术解析

1. 从标题说起&#xff1a;这个工具到底解决什么问题Wand 这个名字&#xff0c;在游戏辅助和系统增强这个圈子里其实不算陌生。它本质上是一类运行在 Windows 平台上的辅助工具&#xff0c;核心能力是给用户提供游戏内的数值调整、界面增强、快捷操作等功能。而 WeMod 则是另一…

作者头像 李华