水位读数不准、现场环境恶劣、模型识别偶尔抽风——这些都是做水尺视觉识别项目时绕不开的坎。我最近基于 YOLOv9m 完成了一套水位计水位识别方案,从数据采集、标注、训练一路推到嵌入式设备部署,中间踩了不少坑也总结了不少经验。这套方案的核心思路很直接:用目标检测模型定位水尺区域、识别刻度数字,再通过几何换算推算出实际水位值。相比传统的人工读尺和液位计方案,它不需要改造水尺本体,不依赖供电通讯线路改造,适合存量站点快速智能化升级,也适合新建的无人值守水位站。无论你是做水利信息化开发的工程师,还是高校里做视觉检测方向研究的同学,这篇实践记录应该都能提供一些可复现的参考。
1. 方案核心思路与选型拆解
1.1 水位识别的问题本质
水位计在水尺上的读数,本质上是一个“小目标密集检测+几何定位”问题。水尺上的数字通常只有几百个像素高,在整幅画面中占比很小,而且刻度密集排列、背景水渍泥污混杂。这意味着模型不仅要识别出“数字是什么”,还要精确回答“数字在哪里”。
两年前我在一个泵站项目里试过传统方案:用颜色分割找刻度线、用霍夫直线检测定位数字边缘,结果一到雨天就破功。镜头上的雨滴、水面的波纹、水尺上的青苔都会让边缘检测结果面目全非。那次经历让我彻底转向了基于深度学习的目标检测路线。
目标检测模型对光照变化、轻微遮挡、复杂背景的鲁棒性远好于传统图像处理方法。而YOLO系列模型又是工程落地里最成熟的选择——推理速度快、部署资料多、社区生态完善。到了YOLOv9这一代,backbone和neck的结构经过重构,小目标检测能力相比v5和v8有了明显提升,正好契合水尺数字这种小目标密集场景。
1.2 YOLOv9m的选型理由
在YOLOv9系列里,有t、s、m、c、e几个规格。我最终选了m版本,没有选更轻的s,也没有选更重的c/e,原因很简单:算力和精度的平衡点。
先看精度需求。水尺数字的检测如果精度不足,后续水位推算必然连锁出错。s模型在精度上虽然能用,但在夜间和雨天场景下margin偏小,一旦光照剧烈变化就容易漏检。再看算力约束。我部署的目标平台讲嵌入式设备,显存和算力都有限。c和e模型虽然AP更高,但推理延迟和显存占用对边缘设备不够友好。m版本在两者之间取了一个很舒服的位置:COCO指标比s高出一截,推理耗时又不会让嵌入式平台捉襟见肘。
另一个实际考虑是模型体积。m权重约50MB出头,在传输、存储、加载上都方便。传统水位站点的网络条件不一定好,模型太大光是OTA升级到现场设备都费劲。50MB这个量级通过4G网络也能比较顺畅地推送。
1.3 总体检测流程设计
整个系统不是训练一个模型就直接出水位,而是拆成两级检测加一步几何换算。
第一级是水尺区域定位。先在整帧图像里找到水尺的外接框,把检测范围限制在ROI内。这一步的价值在两方面:一是排除水尺周边大量无关背景的干扰,二是为第二级检测缩小搜索范围,减少算力消耗。
第二级是刻度数字检测。在ROI内识别水尺上的每个数字,输出类别和坐标。这里我用了0~9十个数字类别,加上一个“E”符号类别用于零位辅助定位。数字类别直接作为读数,省去再接一套OCR模型的麻烦。
最后是水位推算。拿到数字框的坐标和类别后,结合水面线位置、刻度间距先验值,按比例换算得到水位读数。第三步才是整个项目真正的门槛,后面我会专门展开讲。
提示:三个环节的误差是逐级累积的。第一级的框偏了,第二级裁出来的图就偏,数字坐标也跟着偏,最终读数自然不准。所以每一级的质量都要盯紧,不能只管模型MAP好看。
2. 数据采集与标注规范
2.1 数据采集:长期无人值守场景下的样本覆盖
目标检测项目里有一句话叫“有多少数据就有多少能力”,在水尺场景下尤其成立。如果训练集全是晴天的清晰照片,部署到现场遇到雨夜就会直接翻车。我按以下维度控制了数据分布。
时间段上,白天、黄昏、夜晚、凌晨都要覆盖。白天数据好采,夜晚数据必须有补光条件才能采到可用画面。如果摄像头本身带红外夜视,夜晚数据的可用性会大幅提高。天气上,晴天、阴天、雨天、雾天尽量都收。雨天特别关键,雨滴造成的反光、水尺湿滑后的镜面效应、水面线的模糊,都是模型必须见过的模式。
水位状态上,从低水位到高水位覆盖水尺的全量程。这个点容易忽略,因为某段水位可能整个月都不会出现,只能靠长期挂机累积。我建议有条件的站点做视频录像,按水位高低分桶存档,隔一段时间抽帧补充训练集。设备端我一般设置为1~2秒抽帧一次,一天的录像大约能积累几千帧有效样本。
拍摄角度上,正规安装的相机基本都是正对水尺,但现场总会有安装偏差。训练集里加入小角度侧视、仰视的样本,能让模型对视角变化更宽容。
2.2 标注类别和框规则
标注方案直接影响模型的识别上限,这里没有捷径。我使用的类别体系如下:
| 类别 | 含义 | 说明 |
|---|---|---|
| water-gauge | 整个水尺外框 | 第一级检测,定位ROI |
| digit-0~digit-9 | 水尺上的单个数字 | 第二级检测,类别即读数 |
| mark-E | 水尺顶部“E”符号 | 辅助定位零位 |
| water-line | 水面线与水尺的交界位置 | 辅助水位推算 |
数字拆分到10个独立类别而不是统一做一个“数字”类别,这个决策经历过实测验证。统一数字类别虽然能让框更聚焦数字本身,但读数时还得再走一遍OCR,多一个步骤就多一层误差。直接分类到0~9,模型输出的类名就是数字值,省事又直观。
标注框的规则上,我总结了几条容易踩雷的点:
- 数字框必须贴紧数字主体,不要把旁边的刻度线、刻度间格包进去。框内杂质越多,类别特征越被稀释。
- 框的尺寸比例要一致。水尺数字是竖向排列,框比例统一在0.8~1.2之间,避免模型学到错误的比例先验。
- 被水面线截断、被水渍覆盖一半的数字不要删除。这种“半残样本”恰恰是真实场景的常态,模型必须见过才能稳。
2.3 数据增强:别让模型学习错误的物理规律
数据增强是提高泛化能力的常规手段,但不能盲目照搬通用配置。水位场景有一个特殊性:水尺是物理物体,数字不会上下颠倒,也不会左右翻转。默认增强里的水平翻转如果开着,会生成“数字镜像”的样本——这种样本在语义上是错的,会干扰模型学习正确的方向特征。
我实测过一组对比:默认增强配置下验证集mAP50在0.85附近,调整后关闭水平翻转、限制旋转角度,mAP50提升到0.93左右。增强配置如下:
- Mosaic:开启,训练后期(最后10个epoch)关闭,让模型在接近真实分布的图像上精修。
- 水平翻转:关闭。
- 旋转:0~±5°,模拟相机安装的角度偏差。
- HSV扰动:亮度±20%,饱和度±10%,模拟不同天气和光线。
- 随机擦除:开启,模拟泥污和污渍遮挡。
这套组合既保留了常见的泛化手段,又避免了破坏物理语义的错误增强。调参的时候多想一下“这个增强在真实场景里会不会出现”,能省掉很多无效试错。
3. 模型训练与调参实践
3.1 依赖环境与数据组织
环境方面没有特别冷门的需求,基础配置如下:
Python 3.8+ PyTorch 1.10+(推荐2.0以上版本) torchvision opencv-python numpy pyyaml代码基于官方开源仓库进行二次开发。数据按YOLO格式组织:
dataset/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ └── data.yamldata.yaml里面写类别数、类别名称、训练集和验证集路径。数据划分上我固定8:2,并且固定随机种子,保证每次训练的数据划分一致,便于对比实验。
3.2 关键训练参数及理由
我用的训练配置如下,可以用作参考起点:
task: detect mode: train model: yolov9m.yaml data: data.yaml hyp: hyp.scratch-low.yaml epochs: 200 batch: 16 imgsz: 640 device: 0 workers: 8 optimizer: SGD lr0: 0.01 weight_decay: 0.0005 warmup_epochs: 3 close_mosaic: 10逐个说明关键参数的选择逻辑。
imgsz=640是速度和精度的折中点。水尺数字属于小目标,如果用纯GPU服务器训练且推理端算力充足,可以尝试1024或1280,小目标召回率会有明显提升。但嵌入式设备推理大分辨率图像会让耗时成倍增加,所以我建议先从640起步,后续根据设备性能逐步上调。
batch=16在单卡12GB显存下能跑起来,显存吃紧就降到8,同时适当提高学习率。注意batch减半后loss曲线走势会有变化,不要直接照搬原学习率。
epochs=200在数据量2000张以内足够收敛。数据量更大时可以降到150左右,数据量更少时建议先靠数据增强撑规模,而不是盲目加epoch,否则过拟合会来得很早。
close_mosaic=10这个细节容易被忽略。Mosaic增强在前中期能提升模型的鲁棒性,但也让模型在拼接图的“假分布”上学习。最后10个epoch关闭,相当于给模型一段时间回到真实数据分布做精修,收敛更稳。
3.3 训练监控与结果评估
训练期间重点盯验证集上的box_loss和cls_loss,两条曲线本该平稳下降。如果验证损失在后期反弹,说明过拟合已经开始,果断早停,不要硬跑完所有epoch。
目标检测还有一个绕不开的度量对:mAP50和mAP50-95。mAP50看识别是否准确,mAP50-95则更苛刻,要求框的位置定位足够精细。水位识别场景两者都要看,因为水位推算极度依赖框位置的准确性。我在最终模型上mAP50约为0.95,mAP50-95约为0.78,实际部署中读数稳定性已经可以接受。
超参数搜索这块,工业项目里我一般不做大规模调参。训练一轮基线模型,再根据失败样本手动微调一两轮,效果足够。时间成本上更划算,也容易控制。
4. 水位推算逻辑与部署实践
4.1 从检测框到水位读数:几何换算详解
模型检测完数字,业务系统要的是一串带单位的数值。这个换算过程是项目最容易出错的地方,我把具体步骤拆开讲。
先在检测结果里找当前水面线下方最近的那个数字框。这里水面线位置有两种获取方式:一是单独训练一个water-line类别,二是从标定好的底部基准推算。我项目里是两者结合,优先用模型检测的水面线,缺失时退回底部推算。
再用相邻数字框做比例内插,公式如下:
水位 = 下方数字值 + (水面线纵坐标 - 下方数字中心纵坐标) / 相邻数字中心像素距 × 刻度间距举例:水尺刻度每2cm一格里,数字为84、86。检测到84框中心纵坐标320像素,86框中心纵坐标280像素,水面线纵坐标300像素。
相邻数字中心像素距 = 320 - 280 = 40 像素 比例 = (320 - 300) / 40 = 0.5 水位 = 84 + 0.5 × 2 = 85.0 cm公式本身不复杂,但三个前提缺一不可:检测框确实配对到相邻刻度数字;框中心点能代表刻度位置;相机光轴与水尺面接近垂直。任何一条不满足,误差都会线性放大。
4.2 相机畸变和零点标定
工业相机出图不带畸变校正时,画面边缘的坐标偏移能有几个像素。水尺这种线条密集的场景,几个像素误差换算成水位读数就是好几厘米,完全不可接受。所以部署前必须做一次完整的相机标定。
畸变标定用棋盘格加OpenCV的cv2.calibrateCamera即可。标定完拿到内参和畸变系数,推理前对整帧做cv2.undistort。边缘变形不严重时,也可以只在检测到的水尺ROI区域内做校正,能省一些嵌入式设备上的算力。
零点标定是把视觉坐标映射到真实高程。最稳妥的做法是在安装现场用水准仪测量水尺零点对应的高程,然后对比多组人工读数和视觉读数,得到偏移量。后续每次视觉读数加偏移量就是上报水位。这一步做完一定要人工复核,一次马虎就会污染整条数据链路。
4.3 多帧滤波:压掉读数跳变
水位是缓慢变化的物理量,但单帧检测偶尔会跳。水波导致水面线抖动、飞鸟短暂遮挡、反光造成的漏检,都会让单帧读数瞬间偏差巨大。
部署时我加了两层滤波。第一层是滑动窗口内取中位数,窗口通常10帧。中位数相比均值的好处是天生抵抗离群值——哪怕一帧给你读了个离谱的数,也不会拖垮整窗口。第二层是变化速率限制。设定每秒允许的最大变化量,如果当前帧读数与前值差超出阈值,判断为异常帧,沿用上一帧有效值。这两层叠加之后,输出读数非常平滑,实际交付时基本不需要再做二次平滑。
4.4 推理优化与部署架构
模型导出与部署链路:PyTorch训练完毕 → 导出ONNX → TensorRT转engine(FP16)→ 嵌入式设备推理。转换过程用onnx模型先跑一遍基准,确认输出与PyTorch一致再进TensorRT,这一步能提前拦下大量算子兼容问题。
在嵌入式设备上,YOLOv9m的FP16推理单帧约15~25ms,加上预处理和前后处理,整体在30ms以内。水位站常规的采样频率是1~5秒一次,这个性能余量很充足,甚至能并发处理多路摄像头。
部署架构上我把所有环节收敛在单进程内:拉流、预处理、检测、水位推算、上报。不做跨进程通信,减少消息丢失和延迟。读数和现场抓拍图通过HTTP POST或者MQTT上报平台,本地保留最近一段时间的抓拍图,方便后续人工复核和模型迭代。
5. 典型问题与排查实录
5.1 水位读数频繁跳变
这是上线初期最常被反馈的问题。排查时按顺序看三处:
先看数字框是否稳定。如果同一个数字的框位置在相邻帧之间来回漂,说明模型对目标定位不自信。改善手段是提升推理分辨率、补充该场景的样本。
再看水面线检测是否稳定。水面波动大的时候,水面线类别的框会上下抖,多帧滤波能压住大部分抖动。如果滤波之后还跳,就得增加水面线类别的样本量,提高模型对这个目标本身的稳定度。
最后看反光。强反光会让部分数字漏检,导致参与计算的数字对时有时无。这时候优先调整补光角度,尽量避免正对水尺的强反射。
5.2 夜间低光照检测下降
夜间补光不均匀时,水尺部分数字过暗,检测率明显下跌。两个有效手段:一是在训练集中加入真实夜晚数据,这是最直接的;二是在预处理环节加自适应直方图均衡化(CLAHE),拉高局部对比度。两个手段结合,夜间mAP能比裸模型提升约5个百分点。
5.3 水尺被泥污部分遮挡
长期无人值守的站点,水尺长满苔藓甚至被淤泥覆盖,是必然会发生的事情。模型再怎么训练也读不出被完全覆盖的数字,这时候靠的是“插值自救+主动告警”。
插值自救:只要还能检测到部分数字,就能结合刻度等间距先验外推缺失位置。比如只识别到80和88,中间缺失82、84、86,也依然能用水面线和这两个已知数字推算水位。
主动告警:检测到的数字数量低于阈值时,上报“水尺遮挡/需清洁”事件。这个功能让运维从“等故障被发现”变成“主动收到提醒”,价值很大。
5.4 ONNX导出与部署的坑
训练环境到部署环境的版本不一致是导出问题的第一来源。经验做法是:导出时锁定torch和onnx版本,导出后立刻用onnxruntime验证输出一致性,再转TensorRT。另外TensorRT engine不跨版本兼容,升级TensorRT必须重新生成engine,不能图省事拿旧文件复用。
6. 实操总结与个人体会
这套方案从算法验证到现场落地,我最大的感触是:模型训练只是其中一环,真正决定项目成败的是数据质量、标注规范、现场标定和推算逻辑的闭环。很多团队把精力全投到刷模型精度上,结果部署到现场,读数依然不准。原因往往不是模型不行,而是零点没标好、相机畸变没校正、水尺安装角度太随意。
给准备上马类似项目的团队一个建议:优先把现场安装规范化。相机和水尺的相对位置固定、角度垂直、避免正对强光源,这些做到位,后续算法工作量和数据需求会明显下降。
后续我想在这个方案上继续扩展的方向有两个:一是把单帧检测升级为端到端的语义分割,直接输出水面线像素位置,减少中间环节;二是接入时序信息,用水位变化的连续性做异常检测和趋势预测。这些扩展都建立在当前这条稳定链路之上,先把基础打牢,后面才有条件谈优化。
最后分享一个实操中非常管用的小习惯:每完成一次现场调优,就把当天的抓拍图、标注版本、模型版本、参数配置统一打标签归档。几个月后回头迭代模型时你会发现,有这套完整记录,能省下大量重新摸索的时间。