【瑞萨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]