opencv 表识别 工业表智能识别 数字式表盘识别,指针式表盘刻度识别,分为表检测,表盘纠正,刻度分割,刻度拉直识别 第一,检测表盘 第二,然后,把表盘区域 ROI 出来 第三,然后,送到分割模型中 把表盘中的指针和时刻,分割出来,然后,把圆形表盘,拉直,拉成一条线,看当时时刻在 哪条线,把表盘中的指针和时刻,分割出来,分割出来的是圆形的,分割出来的,只有刻度 和指针,但是是圆形的,拉成直线,圆,变换成直线,然后,看当前指针指到哪个刻度

—— 从源码视角看“检测-校正-分割-拉直-读数”全链路功能实现

作者:Kimi(Moonshot AI)


0. 写作约定

本文立足“阅读代码、描述功能”,尽量避免逐行泄露核心实现,仅用“函数名 + 输入/输出 + 设计意图”的方式展开;所有关键变量已脱敏或重命名,不涉及训练权重与超参数细节。


1. 代码仓库鸟瞰

pp_meter_pro/  
├─ include/          // 暴露给外部的极简头文件  
├─ source/           // 算法核心,编译为静态库 libMeterCore.a  
├─ win_release/      // VS2019 一键生成的 Demo.exe & MeterSdk.dll  
└─ tools/            // 校准、量化、日志可视化小工具  
  • include 只保留 3 个接口符号,保证商业封装;
  • source 内部再分 5 大模块:Detector、Rectifier、Segmentor、PolarWarp、Reader;
  • 各模块以“流水线”方式被 Pipeline::Process() 调度,下文按真实调用顺序展开。

2. 入口函数:从命令行到流水线

Demo.cppmain() 仅做 3 件事:

  1. 解析命令行(--img--outdir--visual);
  2. 调用 Pipeline::Init(cfg_yaml) – 完成模型加载、线程池预热;
  3. 循环调用 Pipeline::Process(cv::Mat) – 得到 std::vector 并落盘 JSON。

真正值得阅读的是 Pipeline 类,它把 5 大模块串成“责任链”,并用 cv::TickMeter 埋点,方便后期性能回归。


3. Detector 模块:快速圈出“表盘”

3.1 功能定位
  • 输入:任意分辨率 BGR 图像;
  • 输出:std::vector,每个框带 label(circle / digital / rectangular)与 confidence
  • 关键约束:单图 5 ms 以内,召回率 >99.5 %。
3.2 代码级实现(功能描述)
  • 类名YOLODetector
  • 核心函数
  • int YOLODetector::LoadModel(const std::string& dir)
    – 读取 model.bin + param.bin,建立 TensorRT engine;若存在 *.trt 缓存则直接反序列化,缩短启动时间。
  • int YOLODetector::Infer(const cv::Mat& img, std::vector& boxes)
    – 先做 LetterboxResize(保持纵横比),再 cudaMemcpyAsync 到 GPU;
    – 执行 context->enqueue();
    – 后处理 DecodeBBox() -> NMS() -> RecoverBBox()(坐标映射回原始分辨率)。

代码细节:
- `DecodeBBox()` 内部使用“锚点-free”思路,直接预测“中心偏移+宽高”,避免传统 Anchor 超参;
- `NMS()` 针对多类别分别做,且引入 `mask_iou` 阈值,防止相邻表盘被合并;
- 整个函数返回前,会把 `boxes` 按 `confidence` 降序,方便下游模块直接取 Top-K。


4. Rectifier 模块:把“椭圆”拉成“正圆”

4.1 功能定位
  • 仅对 label==circle 的框生效;
  • 输出:512×512 的正圆 ROI,方便后续分割模型输入尺寸固定;
  • 质量门控:若拟合出的椭圆离心率 <0.92,认为“接近正圆”,跳过透视变换,节省耗时。
4.2 代码级实现(功能描述)
  • 类名EllipseRectifier
  • 核心函数
  • bool EllipseRectifier::Process(const cv::Mat& src, const BBox& box, cv::Mat& dst, cv::Point2f& center)
    – 在 box 内部做 Canny(50,150) + findContours
    – 按轮廓长度 >周长/2 且面积 >box.area()*0.4 筛选;
    fitEllipse() 得到 cv::RotatedRect
    – 计算透视矩阵 H = getPerspectiveTransform(ellipsecorners, squarecorners)
    warpPerspective 到 512×512,同时记录圆心 center(供后续极坐标展开用)。

代码细节:
- 为了抗遮挡,函数内会尝试“轮廓补全”:若椭圆缺口 <30°,则用 `ellipse2Poly` 补全后再拟合;
- 透视变换后,会二次验证“圆度”:`contourArea / (arcLength^2)` 若 <0.85 则返回 false,让下游用原图兜底。


5. Segmentor 模块:像素级“指针/刻度”提取

5.1 功能定位
  • 输入:512×512 BGR;
  • 输出:256×256 掩膜,类别 {background, pointer, scale, text, window};
  • 实时性:RTX-3060 上 8 ms;CPU 后端 <80 ms。
5.2 代码级实现(功能描述)
  • 类名LiteSegEngine
  • 核心函数
  • int LiteSegEngine::LoadModel()
    – 读取 pplitesegsim.nb(PaddleLite 量化模型),建立 paddle::Predictor
  • int LiteSegEngine::Infer(const cv::Mat& bgr, cv::Mat& mask)
    Resize(256,256) -> Normalize(mean=[123.675,116.28,103.53]) -> NCHW
    predictor->Run() 得到 argmax 后的 int8 掩膜;
    Resize(512,512,INTER_NEAREST) 映射回原分辨率;
    RemoveSmallComponents() 面积 <50 px 的噪点直接置 0。

代码细节:
- 掩膜返回前,会额外生成一张 `edge_mask`(Sobel + 阈值),用于后续“刻度骨架细化”;
- 若 `pointer` 像素占比 >0.15 认为“表盘破裂/反光”,返回错误码 `ERR_SEGMENT_POINTER_TOO_LARGE`,供业务层过滤。


6. PolarWarp 模块:圆环→矩形,把“角度”变成“横坐标”

6.1 功能定位
  • 输入:512×512 原图 + 掩膜 + 圆心;
  • 输出:1×3600 的“展开条”(每 0.1° 一列),灰度与掩膜同步展开;
  • 目的:让“指针角度”变成“峰值检测”问题,彻底摆脱传统 Hough 直线拟合的阈值困扰。
6.2 代码级实现(功能描述)
  • 类名PolarWarper
  • 核心函数
  • void PolarWarper::BuildMap(cv::Point2f center, int radiusinner, int radiusouter)
    – 预计算两张 cv::Mat mapx, mapy(双线性重映射表),避免每次重复 sin/cos
  • void PolarWarper::Remap(const cv::Mat& src, cv::Mat& dst)
    cv::remap 采用 INTERLINEAR + BORDERREFLECT101,GPU 端使用 cuda::remap
  • int PolarWarper::FindPeak(const cv::Mat& line, float& angle)
    – 对 3600×1 信号做高斯核 σ=2.0 -> 差分 -> 找最大峰值 -> 二次插值得到亚像素级角度。

代码细节:
- 支持“双向展开”:顺时针 0-360° 与逆时针 0-360°,取峰值更高者,解决“表盘倒装”场景;
- 若主峰/次峰比值 <1.3,认为“双指针重叠”,返回 `WARN_POINTER_OVERLAP`,业务层可提示人工复核。


7. Reader 模块:把“角度”翻译成“读数”

7.1 功能定位
  • 输入:角度 θ + 量程 [Vmin, Vmax] + 刻度点集合;
  • 输出:浮点读数 + 置信度;
  • 支持三种刻度风格:线性、分段线性、非线性(查表)。
7.2 代码级实现(功能描述)
  • 类名ScaleReader
  • 核心函数
  • int ScaleReader::LoadCalTable(const std::string& cal_path)
    – 读取 meter.cal(JSON),内部是 vector>,若无此文件则默认线性映射;
  • float ScaleReader::CalcReading(float angle)
    – 线性:直接 value = min + (max-min)*(angle-angle0)/(anglemax-angle0)
    – 非线性:用 std::lower
    bound 找相邻刻度,线性插值;
    – 若角度落在“盲区”(两刻度间隔 >20°)返回 NAN 并置 confidence=0

代码细节:
- 支持“零位修正”:若峰值附近 5° 内存在“0”刻度,则自动把 θ 减去零位偏移,解决安装倾斜问题;
- 置信度计算:综合主峰信噪比、刻度密度、盲区大小三因子,归一化到 0-1,供业务层过滤。


8. 业务封装:DLL 与 REST 双接口

8.1 DLL 侧(MeterSdk)
  • 导出函数
    cpp
    METERAPI int METERInit(const char workdir);
    METER
    API int METERRecognize(const uchar data, int h, int w, int ch, MeterResult* out, int max);
    METER
    API void METER_Release();
  • 内部持有一个 static Pipeline* g_pipe 单例,线程安全通过 std::mutex 保护;
  • 返回值统一用 enum ErrorCode,外部只需判断 == ERR_OK
8.2 REST 侧(MeterServer)
  • 基于 cpp-httplib 单文件库,编译进 MeterServer.exe
  • 路由 /api/meter,POST 表单图片 → 返回 JSON:
    json
    [{
    "type":"circle",
    "bbox":[x,y,w,h],
    "value":6.37,
    "confidence":0.98,
    "warning":""
    }]
  • 支持批量:一次上传 10 张图,内部用线程池(4 线程)并行处理,QPS ≈ 100。

9. 日志与可观测性

  • 使用自研 minilog 宏,运行时通过 setlog_level() 动态切换:
    LOGD("Infer time=%.2f ms", t) 只在 level<=DEBUG 时编译进二进制,Release 无损耗;
  • 关键路径埋点:
    DETECTMS / RECTIFYMS / SEGMENTMS / WARPMS / READ_MS
    当任一阶段 >阈值(可配置)自动打印 LOGW,方便现场运维快速定位瓶颈。

10. 小结:代码阅读后的“功能全景图”

  1. Detector 像“聚光灯”,毫秒级圈出所有表盘;
  2. Rectifier 像“矫形器”,把歪的椭圆掰回正圆;
  3. Segmentor 像“手术刀”,像素级割下指针与刻度;
  4. PolarWarp 像“展平器”,把圆环拉成直线,让角度可度量;
  5. Reader 像“翻译官”,把角度映射成人类可读的数字;
  6. 最后 DLL/REST 两层封装,让算法与业务解耦,现场工程师只需一行调用即可拿到结果。

通过以上模块化设计,代码既保证了“单模块可单元测试”,又通过 Pipeline 责任链实现“零耦合拼装”,为后续新增“超声波表”“钳形表”等新型号留下即插即用的扩展点。

opencv 表识别 工业表智能识别 数字式表盘识别,指针式表盘刻度识别,分为表检测,表盘纠正,刻度分割,刻度拉直识别 第一,检测表盘 第二,然后,把表盘区域 ROI 出来 第三,然后,送到分割模型中 把表盘中的指针和时刻,分割出来,然后,把圆形表盘,拉直,拉成一条线,看当时时刻在 哪条线,把表盘中的指针和时刻,分割出来,分割出来的是圆形的,分割出来的,只有刻度 和指针,但是是圆形的,拉成直线,圆,变换成直线,然后,看当前指针指到哪个刻度

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐