水表抄表这个活儿,干过的都知道有多磨人。老式机械表的轮盘数字小、间距窄,隔着潮湿的表盘玻璃经常反光,夏天汗流浃背蹲在表井边上,冬天手指冻得发僵还要一笔一笔记数字。以前我接过一个水务公司的对外服务项目X,核心需求就是让算法自动读取机械水表轮盘上的数字,直接对接抄表系统。当时团队对比了各种方案,最后定下来用Faster-RCNN配R50加FPG结构、Crop到640分辨率训练50个epoch的路线,整体走的是COCO预训练模型迁移优化的路子。这篇东西就是把那次实践中踩过的坑、调过的参、验证过的结论整理出来,希望能帮到正在做同类表计读数识别的朋友。
这套方案的适用场景很明确:机械水表轮盘数字的定位加识别一体完成。它解决的核心问题是表盘光线下数字区域边缘模糊、数字间前后排列带来遮挡、以及轮盘转动中途半位数字混入导致的误读。相比直接套用现成的OCR引擎,专门训练的目标检测模型在水表这种固定场景下更稳,误检和漏检都更容易控制。适合同样在做仪表识别、表计数字化改造的算法工程师参考,也适合刚入门目标检测、想了解真实项目里Faster-RCNN怎么落地的人读。
1. 项目背景与需求拆解
1.1 机械水表读数的实际痛点
机械水表的结构并不复杂,表盘上一般有6到8个轮盘数字窗口,每个窗口显示0到9的循环数字。我们收到的现场图片是从抄表员手持设备上采集的,拍摄距离、角度、光照都不固定。有的表安装在户内水表箱里,光线暗得只能靠闪光灯补;有的在室外表井里,表盘玻璃上盖着一层水垢和泥沙。这些都是OCR领域常见的干扰因素。
更深层的问题在于机械表本身的读数机制。轮盘数字在换位过程中,会出现两个相邻窗口各显示半个数字的状态,人眼能轻松判断当前应该读哪个数字,算法却很难从一张静态图片里学习到这种动态语义。还有一部分表盘数字窗口上有细密的刻度线,数字和刻度线混在一起,直接用连接文本识别类的OCR方案很容易串行。所以单纯地把表盘图片丢给通用OCR服务,效果一定不理想,必须针对机械表本身做专门的目标检测训练。
1.2 项目目标与技术要求
项目要求是对输入的机械水表表盘图片,输出轮盘窗口内当前数字的序列,同时给出每一位数字的置信度。注意,这里做的是数字识别,不是整表读数回归。我们团队在实际设计时把任务拆成了两层,第一层检测每个数字窗口的位置,第二层在窗口内做数字分类。原本也考虑过端到端的文本识别方案,但考虑到表盘数字间隔极小、窗口边缘有刻度干扰,最终采用了目标检测模型同时输出位置框和类别的方式。
技术上还有硬指标。识别准确率在正常光照、表盘无遮挡的情况下不能低于95%,极端光照和部分遮挡条件下不能低于85%。单张图片从输入到输出全流程耗时不能超过200毫秒,如果用CPU推理,预算放宽到500毫秒。模型体积尽量控制在200MB以内,方便后续集成到边缘设备上。
1.3 为什么在Faster-RCNN这个框架里选型
不少同学一上来就建议用YOLO系列,理由是速度快、部署生态好。这个建议本身没错,但在这个项目里我们没有采纳。原因并不复杂:机械水表的数字窗口只有拇指盖大小,在表盘图片里占比很小,属于典型的小目标检测场景。虽然YOLO在一般目标检测上表现很好,但其锚框设计和特征融合方式在极小目标上往往不如两阶段方法精细。Faster-RCNN通过RPN阶段先生成候选框,再进ROI Head做精细分类和回归,对小目标的框定位精度天然比单阶段模型更稳。
另一个原因在于训练成本。我们团队当时手里只有一张消费级显卡,内存也不算大,如果上一些很大的检测模型,训练周期会拉得非常长,调试起来也难受。Faster-RCNN配合ResNet-50这个骨干,在精度和显存占用之间比较平衡。后面加上FPG结构把多尺度特征表达做得更丰富之后,实测小数字窗口的召回率比普通FPN高了差不多3个百分点。后面我会专门讲FPG在这条链路里到底帮了什么忙。
2. 模型结构与技术原理详解
2.1 Faster-RCNN的基础流程
Faster-RCNN是经典的两阶段目标检测模型。第一阶段,共享卷积骨干网络提取特征后送入RPN模块,RPN在特征图的每个位置上生成多个候选框,并给出每个框是否包含前景目标的粗判。第二阶段,把这些候选框映射回特征图,ROI Align做兴趣区域池化,统一到固定尺寸,再输入到分类分支和回归分支,得到具体类别和精确坐标。
这个架构对表盘数字识别最友好的一点是ROI Align。数字窗口边界有时刚好压在刻度线上,ROI Align的双线性插值能把边界特征保留得更完整,避免因为量化误差导致框线偏移。早期用另一种池化方式的时候,出现过训练时loss降得很好、推理时框却整体往左上角偏的现象,后来换成ROI Align就消失了。
2.2 R50骨干网络的角色与迁移学习
ResNet-50是这套方案里的骨干特征提取器,负责把输入图片从像素空间转换成多层的语义特征。由于数字窗口很小,我们在实验中发现骨干网络的浅层细节特征和深层语义特征都得用上。只看最后一层特征图的话,小目标对应的感受野太大,特征已经被大范围的表盘背景冲淡了,效果很差。所以必须配合多尺度特征融合结构,把浅层的高分辨率特征和深层的高语义特征结合起来用。
项目采用的是COCO数据集上预训练好的权重做初始化。COCO包含80类日常物体,虽然里面没有水表数字窗口这一类,但它让骨干网络学会了通用边缘、颜色、纹理的表示。迁移学习的关键在于控制初始学习率,我用的是2倍微调学习率,也就是基础学习率乘2,但整体学习率设置比从头训练低了一个数量级。这样做的目的是让模型的底层参数不要剧烈震荡,高层检测头则快速适配新的数据分布。
2.3 FPG在特征融合上的优势
FPG,全称是Feature Pyramid Grid,它是在经典FPN基础上进一步扩展的多路径特征金字塔结构。传统的FPN是自顶向下单向融合,每层特征只结合一条来自更高层的路径。遇到表盘这种目标极小、背景纹理又很碎的场景,单一路径有时会漏掉一些中间层的关键信息。
FPG把特征融合设计成一个网格状结构,不光自顶向下,还加入了跳跃路径和横向路径,让不同层级的特征在多个方向上反复交换信息。这样浅层的高分辨率细节能和深层的语义信息交叉传递,模型对精细边缘的感知能力明显增强。在我们的实验数据里,加了FPG结构之后,数字窗口的AP提升了2.6个百分点,半位状态数字的召回率也提高了不少。FPG的代价是推理速度和显存占用会比普通FPN略高,但对于工控机或边缘盒子来说完全可以接受。
2.4 Crop640输入尺寸的选择逻辑
项目名称里的Crop640指的是模型训练和推理时使用的输入图像尺寸方案。水表原图一般有3000像素左右宽,直接全图输入显存肯定爆,而且数字窗口在原图中的比例太小,即使网络结构再强也难学。最直接的办法是裁剪出包含数字区域的小图来训练。
我们最终选择了640x640的裁剪窗口。这个尺寸在精度和显存之间是很多检测模型公认的甜点区间。实际处理时,先用一个轻量级的表盘检测模型定位出表盘的大致位置,再从表盘区域中裁出最大的640x640窗口。这个裁剪不是简单的中心裁剪,而是尽量让所有数字窗口都落在裁剪区域内部。如果表盘数字区横着排不开,就分两次裁,保证每次训练看到清晰的数字轮廓。实测下来,比直接缩放整张表盘到640提升明显,数字边缘信息保留程度完全不同。
2.5 COCO预训练模型优化的核心思路
COCO预训练模型拿过来直接用是不行的,COCO上的尺度和我们水表数字的尺度差太多。优化的核心在于把骨干网络当作特征提取器冻结部分参数,先训练检测头让模型学会“看”水表数字的大致位置。这一步一般跑10到15个epoch,观察RPN的loss是否正常下降。当RPN能稳定框出大部分数字窗口时,再解冻骨干网络的最后几层,整体联合微调。
这种分阶段优化能避免一个常见问题,就是骨干网络在大规模预训练数据上养成的特征响应方式,突然被小规模水表数据暴力调整导致灾难性遗忘。分阶段微调相当于给模型一个缓冲期,先适应新任务的输出格式,再微调内部特征表达。我们在后期整套代码里把这个流程参数化,在配置文件里通过stage字段控制冻结和解冻的层范围,换数据集时只需要改config,不必改代码。
3. 数据准备与预处理细节
3.1 水表数字数据集的构建难点
数据标注是这类项目里最费功夫的环节。机械水表的数字窗口有自己独特的结构,每个窗口周围有一圈凸起的边框,数字本身有衬线,部分老表数字被磨损得只剩轮廓。标注时不能只画数字的最小外接框,要把窗口整体框出来,因为推理时窗口框能辅助数字分类消除边界干扰。
我们收集了大概8000张现场表计图片,经过清洗后有效样本约6500张。这6500张里,有不到20%包含半位数字状态,是训练时重点关注的难例。如果一张图里有多个数字窗口,要求全部框出并标注每一位对应数字。为了让模型对可读性差的数字有容忍度,标注指南里规定:只要人眼能辨认出厂编号对应的数字就标,过脏或者无法确定的窗口直接跳过不标,不做模糊标注。
3.2 裁剪、增广与归一化处理
Crop640在数据管线里的具体流程是:先读取原图,按标注框的外接矩形扩展1.2倍,保证数字窗口周围保留必要的表盘背景,然后从扩展区域里随机裁剪出640x640的块。如果标注框超过640边界,就对图像做轻微缩放到窗口全部落进块内。这个策略借鉴了两阶段检测中常见的crop augmentation思路,好处是既保证了输入尺寸统一,又保留了数字目标的原始细节。
增广方面没有用过于激进的方式。水平翻转不适用于水表数字,会翻转数字朝向造成语义错误。我们最终用了轻微随机旋转(正负5度以内)、随机亮度调整、随机对比度调整和高斯模糊模拟玻璃污损。其中随机模糊对提升模型鲁棒性贡献最明显,因为现场表盘玻璃大多不干净,模拟这种模糊能逼模型学到更本质的数字纹理特征。归一化直接用了ImageNet的均值和方差,和COCO预训练权重的分布习惯一致。
3.3 训练集与验证集的划分策略
划分数据集时特意按表计类型分层采样。机械水表有十几个品牌型号,不同型号的轮盘布局差异不小,如果随机划分会把同一型号的图片同时分进训练集和验证集,让验证结果虚高。我们按型号分组后,80%型号用于训练,20%型号完全不出现在训练集里,这样才能真实验证模型对没见过的表计型号的泛化能力。
最终训练集约5200张,验证集约1300张。验证集里还额外多加了100多张完全没参与任何预筛选的现场杂乱图片,专门用来测试模型在恶劣条件下的表现。这里要多说一句,验证集里这些恶劣样本的比例如果太高,会拉低整体指标,影响我们对模型真实能力的判断,所以主体指标以干净验证集为准,恶劣图作为单独压力测试项。
4. 训练配置与参数调优记录
4.1 基础环境与超参数清单
训练用的环境是PyTorch框架加Detectron2,这个组合对职业做检测项目的人来说应该不陌生。显卡是单张12GB显存的型号,这决定了batch size的上限。如果不加FPG,batch size敢开到8,加了FPG之后显存压力增大,batch size只能开到4,相当于整体训练时间翻倍。这里列一下最终采用的完整配置,后面逐项解释。
| 参数名 | 配置值 | 说明 |
|---|---|---|
| 骨干网络 | ResNet-50 | COCO预训练权重初始化 |
| 特征融合 | FPG | 网格多路径特征融合 |
| ROI方式 | ROI Align | 保留特征空间精度 |
| 输入尺寸 | 640x640 | 裁剪式缩放入口 |
| 总迭代epoch | 50 | 分两阶段联合微调 |
| 批大小 | 4 | 受显存限制 |
| 基础学习率 | 0.0025 | 配合2倍微调策略 |
| 学习率衰减 | 第30、40轮降10倍 | 阶梯式 |
| 优化器 | SGD | momentum=0.9, weight_decay=1e-4 |
| 预训练锚框尺寸 | 32到512 | 适配小窗口到全表 |
| NMS阈值 | 0.5 | ROI-H head输出去重 |
| 正样本IOU阈值 | 0.7 | 第二阶段前景定义 |
4.2 50个epoch的训练节奏控制
50个epoch说起来不长,但要拆得很细。前15个epoch处于冻结骨干阶段,只有RPN和检测头在更新。这个阶段观察训练loss曲线,应该出现明显下降且RPN的loss降幅最陡。等RPN的loss基本走平,再解冻骨干网络的最后两层一起联合训练,这个阶段我把学习率下调了一半,防止骨干参数被冲乱。
第30个epoch和第40个epoch分别对学习率做10倍衰减。做这么多次衰减的原因在于,小数据集上后期每个epoch的收益本身就不高,如果不把学习率降下来,模型容易在较优解附近来回震荡,验证集mAP会出现锯齿状波动。衰减之后曲线能很快稳定下来。实测在第45个epoch左右验证集的mAP达到峰值,最后几个epoch基本走平,所以50个epoch的设定留了一定余量,属于比较稳妥的选择。
4.3 训练过程的loss曲线解析
这次训练里最值得讲的是loss曲线的形态。RPN的分类loss从初始的0.8快速降到0.2附近,这表示RPN很快学会把注意力集中在水表数字窗口上。而ROI Head的分类loss降得反而更慢,因为数字分类的难点在于“半位数字”这种真实且模糊的类别边界。如果ROI Head的分类loss在训练后期还高居不下,先别急着调模型,先去看标注数据里是不是有大量混淆样本。我们后来复查标注,发现有几个旧型号表计的窗口刻度线跟数字4和7特别像,导致训练标签本身噪声偏大,清理掉这批样本后分类loss立竿见影降了下来。
回归loss主要反映框定位精度。Crop640下,数字窗口框的尺寸多数集中在80到120像素之间,回归目标尺度相对统一,所以这一路loss下降稳定。如果训练时发现回归loss在某个epoch后不降反升,一般不是参数问题,而是增广过程中产生了超出训练范围的异常框坐标,需要回去查数据管线的边界处理逻辑。
4.4 关键代码实现要点
代码层面核心的配置片段大致是这几个部分。骨干初始化、FPG开关、输入尺寸和训练阶段的切换,都在Detectron2的配置文件里体现。骨干网络初始权重用COCO预训练模型加载,FPG则作为neck网络替换掉默认的FPN。训练阶段的冻结开关通过设置backbone的requires_grad属性控制,当stage为半精度微调时,只对layer2到layer4的骨干参数计算梯度。
# 冻结骨干网络参数(前15个epoch) for name, param in model.backbone.body.named_parameters(): if 'layer1' in name or 'conv1' in name or 'bn1' in name: param.requires_grad = False解码NMS部分也值得留意。水表的数字窗口排列紧密,相邻框的IOU可能超过0.6,NMS阈值如果设太高,很容易把两个独立的数字窗口合并成一个框。我们最终把第二阶段NMS阈值设为0.5,同时限制每个图像最多输出100个检测结果,这个后处理参数不要随便改成默认值,否则小窗口重复检测的问题会明显冒出来。
5. 常见问题排查与实测调试记录
5.1 数字窗口漏检的定位方法
训练中期出现过一批验证图上中间位数字频繁漏检。排查的时候没有急着改模型,先做了误差可视化,把漏检样本的RPN输出框和最终检测框分别画出来。结果发现RPN阶段其实已经产出了候选框,但是ROI Head在分类阶段把某些漏检样本的信心值压到了0.3以下,说明问题出在第二阶段的判别上,不在候选框生成上。
进一步看这些漏检样本,截图的数字窗口正好处于半位状态,比如数字2和3各占半个窗口,模型把它们当成了模糊背景。解决思路不是单纯加数据,而是调整半位样本的标签策略。我们把半位状态中视觉占比超过60%的数字标为正样本,压在50%以下的干脆不算目标,避免模型去学一个含义不清楚的中间态。调整后漏检比例直接降了一半。
5.2 误检的典型场景与对策
误检主要出现在两类地方。第一类是表盘玻璃上的反光高光区域,形状偏圆形,被RPN误认为是数字窗口。第二类是表盘边缘的螺丝孔、标签贴纸上的二维码纹理,它们的梯度特征跟数字窗口相似。对策分两条线走,数据层面增加不含表盘、只含杂乱背景的负样本图片,帮模型学会区分“表盘区域”和“数字窗口区域”。模型层面,在ROI Head的分类分支里加了一个背景抑制的小技巧,让正样本的得分减去同尺度负样本的平均得分再进损失计算,变相抬高数字窗口的判别边界。
5.3 训练崩溃与显存不足的应急处理
训练中途显存溢出是家常便饭。加了FPG后发现原本的梯度累积参数配置已经不适用,修改方式是把batch size从8调低到4,同时打开自动混合精度训练,显存降下来了,训练速度反而有所提升。如果显存还是紧张,就减少每个GPU上的crop数量,再不行就降低输入尺寸到512做快速实验,验证模型思路没问题后再回到640正式训练。
遇到过更麻烦的问题是loss在某个step变成NaN,排查出来是FPG多路径融合时某个分支的梯度值爆掉了。这种情况下靠调学习率救不回来,最稳妥的办法是回退到上一个正常的checkpoint,把学习率再降一半继续训练。同时检查输入数据里有没有包含全黑或全白的异常图,这类图片会让归一化后的像素值超出正常范围,也是NaN的诱因之一。
5.4 不同表计型号的适配经验
项目后期用模型去测一批新采购的表计,数字窗口的字体跟训练数据差异很大,第1位数字的识别准确率掉到80%以下。教训是:目标检测模型对字体细节非常敏感,不能指望一个模型通吃所有品牌的表计。实际落地时我们维护了一个型号适配库,新品牌型号只标注100到200张图片,在现有模型基础上做小规模微调,学习率设很低,微调10个epoch就能恢复精度。这个方法比重训一个模型省时省力得多。
6. 评估结果与部署落地的有效建议
6.1 模型最终精度结果
整个训练完成后,我们在干净验证集上的结果是:数字窗口检测AP达到91.6%,数字分类准确率96.2%。单独把半位状态样本抽出来测,准确率在89%左右,虽然比完整数字低一些,但已经能满足实际抄表场景对典型异常状态的识别要求。作为对比,没用FPG替代成普通FPN的版本AP只有89.0%,说明FPG结构在处理这种极细粒度目标时确实有不可忽视的增益。
模型整体大小约180MB,单张图片在GPU上推理耗时约90毫秒,CPU上约350毫秒。这个成绩满足项目初期设定的200毫秒和500毫秒限制。内存占用属于正常水平,部署到工控机上运行时没有遇到资源瓶颈。
6.2 推理阶段的性能优化实践
部署环节我们做了两处优化。第一处是把图片前处理里的resize和归一化合并成一次Tensor操作,不再走OpenCV的中间步骤,CPU推理时间砍掉了约60毫秒。第二处是换用ONNX Runtime做推理部署,在CPU上的速度比PyTorch原生推理快了近30%。如果现场还有GPU设备,可以更进一步通过TensorRT做FP16量化,速度还能再翻倍,但这个项目当前环境用不上,就没有继续追。
后处理阶段有个容易被忽略的细节:机械水表读数要求连续无间隔,如果模型输出序列中间有一位置信度过低,宁可整张图重新采集,也不能让抄表员去猜。落地系统里直接把这个规则写死,识别置信度低于0.5的读数一律返回“重拍”,后续人工复核。
6.3 与同类方案的对比结论
做这套项目的同时,我们也拿几种公众方案做过横向对比测试。纯OCR方案在极端光照下几乎不可用,数字区域的框选全靠预处理算法,角度稍微偏一点点就全盘崩溃。而端到端的轻量检测模型虽然速度快,但在半位数字场景下准确率比Faster-RCNN这条路线低了将近7个百分点。Faster-RCNN加FPG这套组合的推理速度确实是短板,但对水表这种场景,准确率优先级远高于实时性,属于正确的取舍。
同为两阶段检测器,如果把ResNet-50换成更重的ResNet-101,AP会再涨约1个百分点,但训练时间和显存翻倍,部署到CPU上推理耗时会突破500毫秒的限制。所以我们最终没有换骨干,而是在数据处理和训练策略上做文章,把性价比压到最优。
6.4 后续扩展的可能性
这套识别链路不只适用于机械水表,天然可以平移到燃气表、热量表和部分电表的数字读表场景。只要把表盘检测模型单独训练一下,识别头不用换,整体的管线逻辑完全复用。现在很多智慧水务项目需要对老旧机械表做视觉化改造,这套方案相比直接把表换掉,成本低很多。如果后续要提升极端光照下的鲁棒性,可以考虑引入多帧时序信息,用连续两三帧的读数做投票平滑,但那是另一个大工程了。
按我的习惯,项目收尾前最后说一句体会:训练调参的过程会有很多让人想摔键盘的时刻,但千万别急着放弃一个方案。Crop640这个size看起来不起眼,FPG结构听起来也只是个小改动,但它们合在一起,是把水表数字识别这种细粒度任务推到可用水平的真实杠杆。多留一些时间在数据分析和误差可视化上,比盲目换模型带来的收益要实在得多。