tianwanglaozi 发表于 6 天前

【瑞萨AI挑战赛】 部署仪表识别算法方案(二)

    一、方案背景
1.1 行业痛点
   在化工、电力、能源、智能制造等工业场景中,差压表、真空表、不锈钢压力表、普通压力表等各类指针式仪表仍被大量使用,这类仪表具备结构简单、成本低廉、抗干扰性强的优势,但传统读数方式依赖人工巡检,存在诸多痛点:人工巡检效率低下,无法实现24小时不间断监测;偏远点位、高危工况、密闭空间下人工巡检难度大、安全风险高;人工读数存在主观误差,数据无法实时上传至工业物联网平台,难以实现数据溯源、异常预警和远程管控;部分老旧厂区改造受限,无法直接更换智能数显仪表,亟需轻量化、低成本的边缘端AI改造方案。



1.2 方案核心目标
本方案基于瑞萨RA8P1 Titan开发板,依托板载硬件NPU算力,复用RT-Thread Studio官方YOLO-Fastest轻量化AI视觉例程框架,打造边缘端实时、多类型仪表兼容、高精度读数、低功耗部署的AI识别系统,无需改造原有仪表,通过视觉采集+AI推理实现指针、刻度自动检测与数值精准计算,替代人工巡检,适配各类工业指针式仪表的自动化监测需求,同时满足工业现场小体积、低功耗、高稳定性的部署要求。

1.3 方案适配仪表类型
• 差压表:量程0~0.1MPa、0~0.16MPa,用于监测两路流体压力差值,常见于管道、暖通、化工流体监测场景
• 真空表:量程-0.1~0MPa,用于测量负压、真空环境参数,适用于真空设备、负压罐、制药等场景
• 不锈钢压力表:量程0~1.6MPa、0~2.5MPa,通用型工业压力表,耐腐蚀、适配恶劣工况
• 可扩展适配其他量程指针式压力表、水压表、气压表,仅需微调模型与参数

二、方案整体架构与核心优势
2.1 整体技术架构
   本方案采用“硬件采集+边缘AI推理+数据计算+结果输出”的一体化边缘端架构,全程在瑞萨RA8开发板本地完成,无需依赖云端算力,数据实时处理、低延迟、高可靠,整体流程分为四大模块:
1. 硬件层:RA8P1 Titan开发板(核心算力)+ OV5640摄像头(图像采集)+ RGB GUI+RS485/CAN总线输出
2. 驱动与系统层:RT-Thread实时操作系统,搭载摄像头驱动、NPU驱动、OpenCV Lite视觉组件
3. AI算法层:轻量化YOLO-Fastest目标检测模型,针对仪表指针、刻度做专项训练,适配RA8 NPU硬件加速
4. 应用层:图像预处理、目标检测解析、多类型仪表自动识别、数值插值计算、结果串口/网络输出。

三、硬件平台选型与搭建
3.1 核心硬件清单


硬件名称

型号规格

核心作用


核心开发板

瑞萨RA8P1 Titan开发板

搭载ARM Cortex-M85内核,集成硬件NPU,提供AI推理算力,运行RT-Thread系统,是整个方案的核心控制单元


图像采集模块

OV5640高清摄像头模块

500万像素,支持640*480分辨率图像采集,色彩还原度高、对焦稳定,适配工业室内外光线环境,采集仪表表盘图像


存储模块

Micro SD卡(≥8GB)

存放转换后的RA8 NPU专用模型文件(.nb格式),存储临时图像数据与日志

显示屏等附件
RGB显示屏,Type-C数据线、串口模块、固定支架

用于GUI显示,程序烧录、调试日志查看、摄像头与开发板固定安装




3.2 硬件连接与部署要求
• 摄像头模块通过DVP接口直连RA8P1开发板,确保摄像头正对仪表表盘,距离保持15-25cm,避免倾斜、遮挡、强光直射或逆光,保证表盘完整、清晰入镜
• SD卡插入开发板卡槽,提前将转换好的AI模型文件存入SD卡根目录,确保路径与代码一致
• 开发板采用5V低压供电,功耗极低,可通过工业电源或USB接口供电,适合长期不间断运行

四、软件开发环境与配置
4.1 软件环境准备
   方案完全基于RT-Thread生态开发,环境配置与官方例程保持一致,无需额外搭建复杂AI环境,具体软件清单:
• RT-Thread Studio,瑞萨e2 studio IDE,FSP
• RA8P1 Titan Board SDK:通过RT-Thread Studio包管理器导入,包含板级驱动、NPU组件、视觉组件、摄像头驱动,是开发的核心基础
• NPU模型转换工具:RT-Thread官方提供的npu_toolkit,用于将训练好的YOLO-Fastest模型转换为RA8 NPU可执行的.nb格式
• 辅助工具:OpenCV(用于数据集标注、图像预处理)、串口调试助手(波特率115200,用于查看识别结果)
4.2 工程组件配置(关键步骤)
在RT-Thread Studio中新建基于RA8P1 Titan的工程,需启用以下核心组件:
1. 启用OV5640摄像头驱动:配置分辨率为640*480,确保图像采集正常
2. 启用NPU驱动组件:开启RA8 NPU硬件加速功能,配置模型加载路径为SD卡根目录
3. 启用OpenCV Lite轻量级视觉组件:用于图像缩放、归一化、格式转换等预处理操作
4. 启用文件系统组件:支持SD卡文件读取,加载AI模型文件

五、AI模型训练与转换
5.1 模型选型逻辑
参考RT-Thread官方例程的YOLO-Fastest轻量化模型,该模型是专为边缘端MCU、嵌入式NPU设计的超轻量目标检测算法,相比YOLOv3、YOLOv5体积缩小80%以上,推理速度提升3倍,完美适配RA8 NPU的算力限制,无需外接算力模块即可实现实时推理,是工业边缘仪表识别的最优选型。


5.2 数据集制作与标注
针对多类型仪表,定制专属数据集,确保模型识别精准:
• 标注目标:分为两类目标,分别是pointer(指针)、scale(刻度),无需标注复杂特征,聚焦指针端点与核心刻度点位
YOLO在整个面板检测出指针仪表用分类,网络判断是哪种指针仪表。再用YOLO检测关键刻度、指针头、指针尾根据角度估计读数。
• 数据集要求:采集不同光线、角度、量程的仪表图片≥300张,覆盖差压表、真空表、不锈钢压力表,按8:2比例划分训练集与测试集
• 标注格式:采用YOLO标准格式,适配模型训练,标注工具选用LabelImg,操作简单、适配嵌入式模型训练







5.3 模型训练与参数微调
• 基于YOLO-Fastest官方开源代码,修改配置文件:类别数改为2(指针+刻度),调整锚框参数适配仪表小目标检测,输入分辨率设置为320*320(兼顾精度与推理速度)
• 训练完成后导出PyTorch格式模型(.pt),后续用于NPU格式转换
5.4 模型转换(RA8 NPU专用)
普通深度学习模型无法直接在RA8 NPU运行,必须通过官方工具转换:
1. 第一步:将.pt模型转换为ONNX中间格式,保证模型结构完整、无冗余算子
2. 第二步:通过npu_toolkit工具将ONNX模型转换为RA8 NPU专用的.nb格式,配置输入尺寸为1*3*320*320,适配NPU输入要求
3. 转换完成后,将.nb模型文件存入SD卡根目录,文件名与代码中加载路径保持一致

六、核心代码框架与业务逻辑(适配RT-Thread)
6.1 工程目录结构(标准化设计)
官方YOLO-Fastest例程目录结构,新增仪表业务逻辑模块,便于开发者理解与二次开发,具体结构:


Plain Text
ra8_meter_recognize/
├── inc/                      # 头文件目录
│   ├── meter_common.h      # 通用枚举、结构体定义(仪表类型、检测结果等)
│   ├── meter_detect.h      # YOLO目标检测接口声明
│   ├── meter_calculate.h   # 多类型仪表数值计算接口
│   └── hardware_init.h       # 硬件初始化(NPU、摄像头)声明
├── src/                      # 源码目录
│   ├── main.c                # 主程序入口,循环采集与识别逻辑
│   ├── meter_detect.c      # YOLO-Fastest推理、结果解析
│   ├── meter_calculate.c   # 仪表类型识别、压力值计算
│   └── hardware_init.c       # NPU、摄像头、SD卡初始化
├── model/                  # 模型存放目录(临时)
│   └── yolofastest_meter.nb# 转换后的RA8 NPU专用模型
└── SConscript               # RT-Thread编译脚本,适配工程编译


6.2 核心业务流程
系统上电后,全程自动运行,无需人工干预,核心流程如下:
1. 硬件初始化:系统启动后自动初始化NPU、OV5640摄像头、SD卡文件系统,加载AI模型,硬件初始化失败会通过串口输出报错信息
2. 图像实时采集:摄像头按1秒1帧的频率采集仪表表盘图像,分辨率640*480,保证图像清晰、表盘完整
3. 图像预处理:通过OpenCV Lite将图像缩放至320*320,完成归一化处理,适配YOLO-Fastest模型输入要求
4. NPU AI推理:将预处理后的图像输入NPU,运行YOLO-Fastest模型,检测出指针(pointer)和刻度(scale)目标,输出目标坐标、置信度
5. 目标筛选与校验:过滤置信度低于0.7的低精度目标,避免误检测,保留有效指针与刻度数据
6. 仪表类型自动识别:根据检测到的刻度数值范围,自动判断是差压表、真空表还是不锈钢压力表,匹配对应量程参数
7. 数值精准计算:提取指针与相邻刻度的坐标,通过线性插值算法计算精准压力值,排除表盘畸变、角度偏差带来的误差
8. 结果输出:通过串口输出仪表类型、量程范围、识别压力值,识别失败会输出报错提示,可扩展接入物联网模块上传数据
6.3 关键代码模块说明
• 硬件初始化模块:封装NPU初始化、模型加载、摄像头初始化函数,通过INIT_APP_EXPORT实现开机自动初始化,无需主函数调用
• YOLO检测模块:复用官方例程推理逻辑,修改结果解析代码,适配指针、刻度两类目标,提取目标坐标与置信度
• 数值计算模块:核心业务模块,实现仪表类型判断、刻度排序、线性插值计算,支持负压真空表、正压压力表的双向计算,兼容多量程适配
• 主程序模块:实现循环采集、推理、计算、输出的闭环逻辑,定时清理缓存,避免内存泄漏,保证系统长期稳定运行
   通用定义(inc/meter_common.h)
<font color="#000000">#ifndef __METER_COMMON_H__
#define __METER_COMMON_H__

#include <rtthread.h>
#include <cv_core.h>

// 仪表类型枚举
typedef enum {
    METER_TYPE_DIFFERENTIAL = 0,// 差压表
    METER_TYPE_VACUUM = 1,         // 真空表
    METER_TYPE_STAINLESS = 2,      // 不锈钢压力表
    METER_TYPE_UNKNOWN = 3         // 未知类型
} meter_type_t;

// 检测目标结构体
typedef struct {
    char name;      // 目标名称(scale_0.08/pointer)
    float x;            // 归一化X坐标(0~1)
    float y;            // 归一化Y坐标(0~1)
    float score;      // 置信度(0~1)
} detect_obj_t;

// 仪表识别结果
typedef struct {
    meter_type_t type;// 仪表类型
    float pressure;   // 压力值(MPa)
    float range_min;    // 量程最小值
    float range_max;    // 量程最大值
    rt_bool_t valid;    // 结果是否有效
} meter_result_t;

#endif // __METER_COMMON_H__</font>初始化src/hardware_init.c
<font color="#000000">#include "hardware_init.h"
#include "npu/npu.h"
#include "camera/ov5640.h"

#define CAMERA_W 640
#define CAMERA_H 480

/**
* @brief 初始化NPU硬件
*/
rt_err_t npu_meter_init(void) {
    if (npu_init() != RT_EOK) {
      rt_kprintf("NPU init failed!\n");
      return -RT_ERROR;
    }
   
    // 加载多类型仪表检测模型
    if (npu_load_model(0, "/sdcard/yolofastest_meter.nb") != RT_EOK) {
      rt_kprintf("Load meter model failed!\n");
      return -RT_ERROR;
    }
   
    rt_kprintf("NPU init success!\n");
    return RT_EOK;
}

/**
* @brief 初始化摄像头
*/
rt_err_t camera_meter_init(void) {
    if (ov5640_init(CAMERA_W, CAMERA_H) != RT_EOK) {
      rt_kprintf("Camera init failed!\n");
      return -RT_ERROR;
    }
   
    rt_kprintf("Camera init success!\n");
    return RT_EOK;
}

/**
* @brief 硬件总初始化
*/
rt_err_t hardware_meter_init(void) {
    if (npu_meter_init() != RT_EOK) return -RT_ERROR;
    if (camera_meter_init() != RT_EOK) return -RT_ERROR;
    return RT_EOK;
}

INIT_APP_EXPORT(hardware_meter_init);</font>   YOLO 检测实现(src/meter_detect.c)

<font color="#000000">#include "meter_detect.h"
#include "npu/npu.h"

#define YOLO_INPUT_W 320
#define YOLO_INPUT_H 320
#define MAX_DETECT_OBJ 20// 最大检测目标数

/**
* @brief 解析YOLO输出结果
*/
static void parse_yolo_output(float *output, detect_obj_t *obj_list, rt_uint8_t *obj_count) {
    *obj_count = 0;
    float threshold = 0.5;// 置信度阈值
   
    // 遍历输出特征图,解析scale/pointer目标
    for (int i = 0; i < 1000 && *obj_count < MAX_DETECT_OBJ; i++) {
      float score = output;
      if (score < threshold) continue;
      
      // 解析目标类型和坐标
      char *obj_name = (output > output) ? "pointer" : "scale";
      float x = output / YOLO_INPUT_W;
      float y = output / YOLO_INPUT_H;
      
      // 处理scale的数值(如scale_0.08)
      if (strcmp(obj_name, "scale") == 0) {
            float scale_val = output * 1.0;// 从模型输出解析刻度值
            sprintf(obj_list[*obj_count].name, "scale_%.2f", scale_val);
      } else {
            strcpy(obj_list[*obj_count].name, obj_name);
      }
      
      obj_list[*obj_count].x = x;
      obj_list[*obj_count].y = y;
      obj_list[*obj_count].score = score;
      (*obj_count)++;
    }
}

/**
* @brief 仪表目标检测
*/
rt_err_t meter_detect(cv_mat_t *frame, detect_obj_t *obj_list, rt_uint8_t *obj_count) {
    if (frame == RT_NULL) return -RT_ERROR;
   
    // 1. 图像预处理:缩放+归一化
    cv_mat_t *input = cv_resize(frame, YOLO_INPUT_W, YOLO_INPUT_H);
    cv_normalize(input, input, 0, 1, CV_NORM_MINMAX);
   
    // 2. NPU推理
    npu_input_tensor(0, 0, input->data);
    npu_run_model(0);
   
    // 3. 解析结果
    float *output = npu_output_tensor(0, 0);
    parse_yolo_output(output, obj_list, obj_count);
   
    cv_mat_release(input);
    return RT_EOK;
}</font>

多类型仪表读数计算(src/meter_calculate.c)
   <font color="#000000">#include "meter_calculate.h"
#include <string.h>
#include <stdlib.h>

/**
* @brief 识别仪表类型
*/
static meter_type_t recognize_meter_type(detect_obj_t *obj_list, rt_uint8_t obj_count) {
    // 根据刻度范围判断仪表类型
    float max_scale = 0.0, min_scale = 100.0;
   
    for (int i = 0; i < obj_count; i++) {
      if (strstr(obj_list.name, "scale_") == RT_NULL) continue;
      
      // 提取刻度值
      float scale_val = atof(obj_list.name + 6);
      if (scale_val > max_scale) max_scale = scale_val;
      if (scale_val < min_scale) min_scale = scale_val;
    }
   
    // 判断类型
    if (max_scale <= 0.16 && min_scale >= 0) return METER_TYPE_DIFFERENTIAL;// 差压表
    if (max_scale <= 0 && min_scale >= -0.1) return METER_TYPE_VACUUM;      // 真空表
    if (max_scale <= 1.6 && min_scale >= 0) return METER_TYPE_STAINLESS;      // 不锈钢压力表
   
    return METER_TYPE_UNKNOWN;
}

/**
* @brief 计算仪表压力值
*/
meter_result_t calculate_meter_pressure(detect_obj_t *obj_list, rt_uint8_t obj_count) {
    meter_result_t result = {0};
    result.valid = RT_FALSE;
   
    // 1. 识别仪表类型
    result.type = recognize_meter_type(obj_list, obj_count);
    if (result.type == METER_TYPE_UNKNOWN) return result;
   
    // 2. 设置量程
    switch (result.type) {
      case METER_TYPE_DIFFERENTIAL:
            result.range_min = 0.0;
            result.range_max = 0.16;
            break;
      case METER_TYPE_VACUUM:
            result.range_min = -0.1;
            result.range_max = 0.0;
            break;
      case METER_TYPE_STAINLESS:
            result.range_min = 0.0;
            result.range_max = 1.6;
            break;
      default:
            return result;
    }
   
    // 3. 提取指针和刻度坐标
    float pointer_x = -1.0;
    float scale_vals = {0};// 刻度值
    float scale_xs = {0};    // 刻度X坐标
    int scale_count = 0;
   
    for (int i = 0; i < obj_count; i++) {
      if (strcmp(obj_list.name, "pointer") == 0 && obj_list.score > 0.7) {
            pointer_x = obj_list.x;
      } else if (strstr(obj_list.name, "scale_") != RT_NULL) {
            scale_vals = atof(obj_list.name + 6);
            scale_xs = obj_list.x;
            scale_count++;
      }
    }
   
    // 4. 线性插值计算压力值
    if (pointer_x < 0 || scale_count < 2) return result;
   
    // 找到指针相邻的两个刻度
    float scale1_val = 0, scale2_val = 0;
    float scale1_x = 0, scale2_x = 0;
    for (int i = 0; i < scale_count - 1; i++) {
      if (pointer_x >= scale_xs && pointer_x <= scale_xs) {
            scale1_val = scale_vals;
            scale1_x = scale_xs;
            scale2_val = scale_vals;
            scale2_x = scale_xs;
            break;
      }
    }
   
    // 计算最终值
    result.pressure = scale1_val + (scale2_val - scale1_val) *
                      (pointer_x - scale1_x) / (scale2_x - scale1_x);
    result.valid = RT_TRUE;
   
    return result;
}</font>   



视频:https://www.bilibili.com/video/BV1J4w3zHEEe?buvid=XY75B537C416E17A953A00A33EDB883CA28EB&from_spmid=dt.dt.video.0&is_story_h5=false&mid=Pw2Hq3t6IhfKj%2FPD%2Bao96g%3D%3D&plat_id=116&share_from=ugc&share_medium=android&share_plat=android&share_session_id=7eddabca-161b-4a6a-b035-4116a3887e19&share_source=WEIXIN&share_tag=s_i&spmid=united.player-video-detail.0.0×tamp=1773591349&unique_k=2Wfxw5c&up_id=526937168



页: [1]
查看完整版本: 【瑞萨AI挑战赛】 部署仪表识别算法方案(二)