本帖最后由 硬核王同学 于 2024-6-6 10:41 编辑
之前搞这个部署识别没搞通,部署环境的时候总是报错,最后发现是硬盘空间不够了!而且我直接在vm里面扩容导致数据损坏,Ubuntu直接开不开机了,实在没时间写程序跑模型,这次先跟着论坛大佬走一遍!
环境部署很详细,新手可以直接跟着我做。
一、初识开发板
这是幸狐的 LuckFox Pico Max 开发板,基于 瑞芯微 RV1106 芯片 。
到手开发板就是这个样子,一块板子,一个SC3336摄像头,一个RTC电池,后面的杜邦线是我自己配的,用于串口收发。
注意摄像头座子要反接,将接口拔起来,排线蓝色的一面朝向网口,再按下去。
详细产品介绍就不多做解释了,可以去官网查看:https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-quick-start
二、镜像烧录
LuckFox Pico Max自带的 SPI NAND FLASH,并在出厂时搭载了测试系统。但是我们也要学下镜像烧录,万一有什么问题,可以详细排查。
1)环境准备
-
安装驱动,下载RK驱动助手 DriverAssitant(去官网下载)。打开RK驱动助手 DriverAssitant 安装 USB 驱动程序,此过程无需连接,安装完成后重启电脑。
-
镜像下载(网盘下载),太大了10个G,挑专用的luckfox_pico_pro_max_image下载。专门为用户提供了适配 LuckFox Pico 和 LuckFox Pico Mini A 的 SD 卡固件以及 LuckFox Pico Mini B 和 LuckFox Pico Plus/Pro/Max 的 SPI FLASH 固件。
-
镜像烧录,下载和解压烧录工具(去官网下载)。
2)烧录详细步骤
-
按住 BOOT 键后连接电脑后,松开 BOOT 键,瑞芯微刷机工具就会显示 MaskRom 设备。
-
加载固件的存放目录,重载 env 文件,勾选所有项。
-
点击下载。
这样就成功了。
3)检查环境(这步可以跳过,一般都对)
如果烧录成功,此时开发板会闪红灯,并且可以实现SSH、Telnet 登录、串口调试、ADB 登录、文件传输,这里就不再过多赘述,我习惯使用ADB,所以一般用ADB。详情看这个:https://wiki.luckfox.com/zh/Luckfox-Pico/SSH-Telnet-Login
另外还要检查下摄像头,需要配置下网卡地址为 172.32.0.100,详情看这个:https://wiki.luckfox.com/zh/Luckfox-Pico/CSI-Camera
三、SDK 环境部署(PC端)
打开Ubuntu22.04,这个环境安装第一章讲过。
1)搭建编译环境
-
安装依赖环境:
sudo apt update
sudo apt-get install -y git ssh make gcc gcc-multilib g++-multilib module-assistant expect g++ gawk texinfo libssl-dev bison flex fakeroot cmake unzip gperf autoconf device-tree-compiler libncurses5-dev pkg-config bc python-is-python3 passwd openssl openssh-server openssh-client vim file cpio rsync
-
获取最新的 SDK :
git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git
git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git
2)SDK 目录说明
-
SDK目录结构
├── build.sh - project/build.sh ---- SDK编译脚本
├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
├── project ------------------------- 参考应用、编译配置以及脚本目录
├── output -------------------------- SDK编译后镜像文件存放目录
└── tools --------------------------- 烧录镜像打包工具以及烧录工具
-
镜像存放目录
output/
├── image
│ ├── download.bin ---------------- 烧录工具升级通讯的设备端程序,只会下载到板子内存
│ ├── env.img --------------------- 包含分区表和启动参数
│ ├── uboot.img ------------------- uboot镜像
│ ├── idblock.img ----------------- loader镜像
│ ├── boot.img -------------------- kernel镜像
│ ├── rootfs.img ------------------ kernel镜像
│ └── userdata.img ---------------- userdata镜像
└── out
├── app_out --------------------- 参考应用编译后的文件
├── media_out ------------------- media相关编译后的文件
├── rootfs_xxx ------------------ 文件系统打包目录
├── S20linkmount ---------------- 分区挂载脚本
├── sysdrv_out ------------------ sysdrv编译后的文件
└── userdata -------------------- userdata
3)编译镜像文件
制作镜像不是必要操作。但是下载luckfox-pico这个源码环境包是必须的,后边很多都依赖这里边的工具。
-
安装交叉编译工具链
cd tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/ source env_install_toolchain.sh
-
编译
cd luckfox-pico
./build.sh lanch //我烧录进SPI_flash ,选择8
./build.sh allsave
不知道有没有和我一样编译不成功的,后来排查了一下,没空间了,删了点东西,就可以了。
./build.sh clean
./build.sh lanch
./build.sh allsave
第一次编译需要花点时间,等待好几个小时,而且需要编译的空间很大,我的40G盘根本放不下,搞得Ubuntu直接崩溃了,还要重新装系统,太痛苦了!!!
编译成功后,生成的固件存放在 SDK目录/output/image 目录下,还是老样子,直接用这个目录就可以烧录镜像。
四、在开发板上跑RKNN模型
1)测试官方给的例子
先用官方给的例子测试一下能不能用 opencv-mobile 添加在摄像头采集图像,左上角添加 fps 并进行 rtsp 推流。
创建个文件夹,克隆下来。(要翻墙,翻不了的老铁自行找码云,或克隆其他大佬的)
mkdir workspace
cd workspace/
git clone https://github.com/luckfox-eng29/luckfox_pico_rtsp_opencv.git
编译下,使用绝对路径!
cd luckfox_pico_rtsp_opencv/
//export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path>
export LUCKFOX_SDK_PATH=/home/linux/Desktop/luckfox-pico
mkdir build
cd build
cmake ..
make && make install
将编译生成的luckfox_rtsp_opencv_demo/luckfox_rtsp_opencv 和 lib目录都上传到开发板上,我这里用的是Smaba+ADB。
一根数据线就可连接传输文件,很方便。
adb shell
ls
mkdir work
exit
adb push \\192.168.44.129\share\Desktop\workspace\luckfox_pico_rtsp_opencv\luckfox_rtsp_opencv_demo /work
adb push \\192.168.44.129\share\Desktop\workspace\luckfox_pico_rtsp_opencv\lib /work/luckfox_rtsp_opencv_demo
登录进板子里,先停止默认运行的rtsp,这个是用于传输视频流的。
adb shell
RkLunch-stop.sh
要先更改下权限,才可以运行,luckfox_rtsp_opencv是一个Linux可执行文件。
chmod 777 luckfox_rtsp_opencv
./luckfox_rtsp_opencv
最后用VLC拉流看视频即可,但有的时候板子的网卡没有自动打开,可以重启下。
2)测试大佬的例子
到这里我们就可以尝试使用上一章生成的RKNN模型进行推理测试,模型生成过程就不再过多赘述。
因为官方给的测试代码跑数字识别模型识别率会稍微低一些,所以先试着用大佬改好的代码跑一下。
大佬原帖:https://bbs.eeworld.com.cn/thread-1282745-1-1.html
先把大佬的代码克隆下来
git clone https://gitee.com/luyism/luckfox_rtsp_mnist.git
克隆完就有luckfox_rtsp_mnist文件了,直接进去看下
linux@linux-virtual-machine:~/Desktop/workspace$ cd luckfox_rtsp_mnist/
linux@linux-virtual-machine:~/Desktop/workspace/luckfox_rtsp_mnist$ ls
3rdparty common lib README_CN.md
build image_show.png luckfox_rtsp_mnist_dir README.md
CMakeLists.txt include model
和官方测试代码一样,都可以编译后上传到开发板上运行,这里的 luckfox_rtsp_mnist_dir 文件夹是可以直接运行的完整文件,那我们先用大佬的模型测试下识别率,看下效果
cd luckfox_rtsp_mnist/
//export LUCKFOX_SDK_PATH=<Your Luckfox-pico Sdk Path>
export LUCKFOX_SDK_PATH=/home/linux/Desktop/luckfox-pico
mkdir build
cd build
cmake ..
make && make install
提前把文件传到板子上
adb push \\192.168.44.129\share\Desktop\workspace\luckfox_rtsp_mnist\luckfox_rtsp_mnist_dir /work
先停止默认运行的rtsp,这个是用于传输视频流的。
adb shell
RkLunch-stop.sh
也是先更改下权限,再输入下面这个即可运行.其中luckfox_rtsp_mnist是可执行文件, model.rknn是对应的模型
chmod 777 luckfox_rtsp_mnist
./luckfox_rtsp_mnist ./model/model.rknn
这里手写的3识别不出来,8还可以,所以朋友们识别的时候把线图粗一些
3)跑通我们自己生成的RKNN模型
这里开始用我们自己生成的模型,可以跟大佬的模型做对比。
直接在开发板上,拷贝一份大佬的工程文件,用于给我们自己测试。
cp luckfox_rtsp_mnist_dir/ luckfox_mnist_test/ -r
cd luckfox_mnist_test/
直接运行,带上我们自己的mnist模型
./luckfox_rtsp_mnist ./model/mnist.rknn
从我这里运行,可以看出来,应该是模型初始化失败了,我又扒了下别人的模型运行,发现应该就是我自己的模型出问题了,所以我就滚去再生成模型去,此处再省略1W字......
和原模型作者合影留念
五、代码详解
1)官方例子
1.代码框架
-
3rdparty: 这个目录通常用于存放第三方库或依赖。在这里,它包含 allocator、librga 和 rknpu2 三个子目录。
-
build: 这个目录用于存放构建(编译)过程中生成的文件。
-
common: 这个目录包含项目中多个地方会用到的通用代码、库或资源。
-
include: 这个目录通常用于存放项目中用到的头文件。
-
lib: 这个目录通常用于存放库文件(如 .a 或 .so 文件)。
-
luckfox_rtsp_opencv_demo: 这个目录包含与项目演示或示例相关的代码或资源。
-
src: 这个目录通常包含源代码文件。
所以我们重点关注src这个文件就可以了,进入src文件夹,只有luckfox_mpi.cc main.cc这两个文件。
文件1: luckfox_mpi.cc
这个文件包含了一些与Rockchip多媒体处理接口(MPI)相关的函数实现。这些函数主要用于初始化和配置视频输入(VI)、视频处理子系统(VPSS)和视频编码(VENC)模块。
-
TEST_COMM_GetNowUs(): 这个函数用于获取当前的时间戳,以微秒为单位。
-
vi_dev_init(): 初始化视频输入设备。它首先检查设备是否已配置,如果没有,则进行配置。然后检查设备是否启用,如果没有,则启用它,并绑定到特定的管道。
-
vi_chn_init(): 初始化视频输入通道,设置缓冲区数量、内存类型、分辨率、像素格式等。
-
vpss_init(): 初始化视频处理子系统(VPSS),设置通道属性,如模式、动态范围、像素格式、分辨率等,并启动VPSS组。
-
venc_init(): 初始化视频编码器,设置编码类型、像素格式、配置文件、分辨率、帧宽度、帧高度、缓冲区数量、比特率等,并启动接收帧。
文件2: main.cc
这个文件是程序的主入口点,它使用OpenCV库来显示视频帧,并使用Rockchip的MPI来处理视频流。
-
包含了一系列标准C库和Rockchip特定的头文件,以及OpenCV的头文件。
-
main函数首先初始化ISP(图像信号处理器)、MPI系统、RTSP会话,并设置视频输入和VPSS。
-
使用vi_dev_init和vi_chn_init函数初始化视频输入设备和通道。
-
使用vpss_init函数初始化VPSS,并绑定VI到VPSS。
-
使用venc_init函数初始化视频编码器。
-
在一个无限循环中,程序从VPSS获取帧,使用OpenCV显示帧,并计算帧率。
-
编码H264视频流,并通过RTSP会话发送。
-
在循环结束后,程序释放资源并退出。
这两个文件共同工作,实现了一个视频捕获、处理和编码的流程,并通过RTSP协议流式传输视频数据。
2.代码解析
我们这里只需要了解main.cc文件即可。一共只有174行,我们只需要关注下几个重点的部分:
/*****************************************************************************
* | Author : Luckfox team
* | Function :
* | Info :
*
*----------------
* | This version: V1.0
* | Date : 2024-04-07
* | Info : Basic version
*
******************************************************************************/
/*头文件包含: 包含了标准C库头文件、Rockchip平台专用的头文件、OpenCV库的头文件。*/
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/poll.h>
#include <time.h>
#include <unistd.h>
#include <vector>
#include "rtsp_demo.h"
#include "luckfox_mpi.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
int main(int argc, char *argv[]) {
/*
全局变量定义:
s32Ret: 用于存储函数调用的返回值。
width 和 height: 视频帧的宽度和高度。
fps_text 和 fps: 用于显示帧率。
stFrame 和 h264_frame: 用于存储编码后的视频流数据和视频帧信息。
*/
RK_S32 s32Ret = 0;
int sX,sY,eX,eY;
int width = 2304;
int height = 1296;
char fps_text[16];
float fps = 0;
memset(fps_text,0,16);
//h264_frame
VENC_STREAM_S stFrame;
stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S));
VIDEO_FRAME_INFO_S h264_frame;
VIDEO_FRAME_INFO_S stVpssFrame;
/*
初始化ISP: SAMPLE_COMM_ISP_Init 和 SAMPLE_COMM_ISP_Run 用于初始化和运行图像信号处理器。
*/
// rkaiq init
RK_BOOL multi_sensor = RK_FALSE;
const char *iq_dir = "/etc/iqfiles";
rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL;
//hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2;
SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir);
SAMPLE_COMM_ISP_Run(0);
/*
初始化MPI系统: RK_MPI_SYS_Init 初始化Rockchip的多媒体处理接口。
*/
// rkmpi init
if (RK_MPI_SYS_Init() != RK_SUCCESS) {
RK_LOGE("rk mpi sys init fail!");
return -1;
}
/*
RTSP会话初始化: 创建RTSP演示处理程序和会话,设置视频编码格式,并同步时间戳。
*/
// rtsp init
rtsp_demo_handle g_rtsplive = NULL;
rtsp_session_handle g_rtsp_session;
g_rtsplive = create_rtsp_demo(554);
g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0");
rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime());
/*
视频输入设备初始化: vi_dev_init 和 vi_chn_init 初始化视频输入设备和通道。
*/
// vi init
vi_dev_init();
vi_chn_init(0, width, height);
/*
VPSS初始化: vpss_init 初始化视频处理子系统。
*/
// vpss init
vpss_init(0, width, height);
/*
绑定VI到VPSS: 使用RK_MPI_SYS_Bind将视频输入绑定到视频处理子系统。
*/
// bind vi to vpss
MPP_CHN_S stSrcChn, stvpssChn;
stSrcChn.enModId = RK_ID_VI;
stSrcChn.s32DevId = 0;
stSrcChn.s32ChnId = 0;
stvpssChn.enModId = RK_ID_VPSS;
stvpssChn.s32DevId = 0;
stvpssChn.s32ChnId = 0;
printf("====RK_MPI_SYS_Bind vi0 to vpss0====\n");
s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvpssChn);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("bind 0 ch venc failed");
return -1;
}
/*
视频编码器初始化: venc_init 初始化视频编码器,设置编码参数。
*/
// venc init
RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC;
venc_init(0, width, height, enCodecType);
while(1)
{
/*
获取VPSS帧:从VPSS中获取一个视频帧。0,0指定了组和通道的ID,&stVpssFrame是存储获取到帧的变量,-1是超时时间,表示无限等待直到获取帧。
*/
// get vpss frame
s32Ret = RK_MPI_VPSS_GetChnFrame(0,0, &stVpssFrame,-1);
/*
处理和显示帧:将获取到的视频帧的内存块地址转换为虚拟地址,然后使用OpenCV创建一个Mat对象,用于处理和显示。这里,将帧率信息显示在帧上,并更新帧的内容(例如,将处理后的图像数据复制回帧的内存空间)。
*/
if(s32Ret == RK_SUCCESS)
{
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
cv::Mat frame(height,width,CV_8UC3, data);
sprintf(fps_text,"fps = %.2f",fps);
cv::putText(frame,fps_text,
cv::Point(40, 40),
cv::FONT_HERSHEY_SIMPLEX,1,
cv::Scalar(0,255,0),2);
memcpy(data, frame.data, width * height * 3);
}
/*
编码H264视频帧:将处理后的帧发送给视频编码器进行编码。
*/
// send stream
// encode H264
RK_MPI_VENC_SendFrame(0, &stVpssFrame,-1);
/*
获取编码后的视频流:从视频编码器获取编码后的视频流
*/
// rtsp
s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1);
/*
RTSP传输:如果已经初始化了RTSP会话,将编码后的视频流通过RTSP传输出去。
*/
if(s32Ret == RK_SUCCESS)
{
if(g_rtsplive && g_rtsp_session)
{
//printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS);
rtsp_do_event(g_rtsplive);
}
/*
计算帧率:计算当前的帧率,TEST_COMM_GetNowUs 用于获取当前时间戳,然后根据前后帧的时间差来计算。
*/
RK_U64 nowUs = TEST_COMM_GetNowUs();
fps = (float) 1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS);
}
/*
释放资源:释放VPSS帧和编码流占用的资源,这是非常重要的,以避免内存泄漏。
*/
// release frame
s32Ret = RK_MPI_VPSS_ReleaseChnFrame(0, 0, &stVpssFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret);
}
s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
}
}
/*
清理阶段:
解除VI和VPSS的绑定。
停止并销毁VPSS组,停止ISP,停止编码器接收帧,销毁编码器通道。
释放分配的内存,退出MPI系统。
*/
RK_MPI_SYS_UnBind(&stSrcChn, &stvpssChn);
RK_MPI_VI_DisableChn(0, 0);
RK_MPI_VI_DisableDev(0);
RK_MPI_VPSS_StopGrp(0);
RK_MPI_VPSS_DestroyGrp(0);
SAMPLE_COMM_ISP_Stop(0);
RK_MPI_VENC_StopRecvFrame(0);
RK_MPI_VENC_DestroyChn(0);
free(stFrame.pstPack);
if (g_rtsplive)
rtsp_del_demo(g_rtsplive);
RK_MPI_SYS_Exit();
return 0;
}
最后我们总结下整体的代码逻辑:
-
初始化阶段:
-
设置视频帧的宽度和高度。
-
初始化帧率显示文本和计算变量。
-
初始化编码后视频流和视频帧信息的结构体。
-
初始化ISP模块。
-
初始化Rockchip MPI系统。
-
初始化RTSP会话和设置视频流参数。
-
初始化视频输入设备和VPSS。
-
将视频输入(VI)绑定到视频处理子系统(VPSS)。
-
初始化视频编码器,设置编码参数。
-
主循环阶段:
-
获取VPSS帧: 从VPSS获取视频帧。
-
处理和显示帧:
-
编码H264视频帧: 发送处理后的帧到视频编码器进行编码。
-
获取编码后的视频流: 从视频编码器获取编码后的视频流。
-
RTSP传输:
-
计算帧率: 根据当前时间和上一帧的时间戳计算帧率。
-
释放资源: 释放VPSS帧和编码流占用的资源。
-
清理阶段:
-
解除VI和VPSS的绑定。
-
停止并销毁VPSS组。
-
停止ISP模块。
-
停止视频编码器接收帧,销毁编码器通道。
-
释放分配的内存。
-
退出MPI系统。
基本上可以看成这张图:
2)大佬例子
1.代码框架
-
3rdparty: 此目录通常用于存放项目中使用的第三方库或依赖。
-
build: 此目录用于存放构建(编译)过程中生成的文件。
-
common: 此目录包含项目中多个地方会用到的通用代码、库或资源。
-
include: 此目录通常用于存放项目中用到的头文件。
-
lib: 此目录通常用于存放库文件(如 .a 或 .so 文件)。
-
luckfox_rtsp_mnist_dir: 这个目录与MNIST数据集的处理或识别有关,包含模型文件和库文件。
-
model: 此目录用于存放项目中使用的模型文件,这些文件可以是配置文件或数据模型。
-
src: 此目录通常包含源代码文件,是编写项目代码的地方。
可以看到比官方给的例子多了一个model文件夹,是因为我们需要用这个文件夹存放我们自己生成的RKNN模型,为后续的编译起到作用。
这部分修改可以查看CMakeLists.txt文件,跳转到最后。
set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir")
file(GLOB RKNN_FILES "${CMAKE_CURRENT_SOURCE_DIR}/model/model.rknn")
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX})
install(FILES ${RKNN_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/model)
这部分是大佬修改的,重新定义了项目的安装规则。
-
设置安装前缀:
set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir")
这条命令设置了项目安装的目标路径。CMAKE_INSTALL_PREFIX 是CMake中的一个变量,用于定义项目构建结果的安装根目录。这里,它被设置为 ${CMAKE_CURRENT_SOURCE_DIR}/luckfox_rtsp_mnist_dir,意味着安装路径将是项目源代码目录下名为 luckfox_rtsp_mnist_dir 的子目录。
-
收集文件:
file(GLOB RKNN_FILES "${CMAKE_CURRENT_SOURCE_DIR}/model/model.rknn")
这条命令使用 file(GLOB ...) 来收集与模式匹配的文件列表。GLOB 是 "global" 的缩写,用于查找所有与给定路径模式匹配的文件。这里,它查找位于 ${CMAKE_CURRENT_SOURCE_DIR}/model/ 目录下所有名为 model.rknn 的文件,并将这些文件的路径存储在变量 RKNN_FILES 中。
-
安装目标:
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX})
这条命令指定了项目构建生成的目标(可能是可执行文件或库文件)的安装规则。install() 函数定义了如何将构建目标安装到系统中。${PROJECT_NAME} 是在 project() 命令中定义的项目名称,这里假设它是一个构建目标。DESTINATION 指定了这些目标文件的安装目录,即上面设置的 CMAKE_INSTALL_PREFIX。
-
安装文件:
install(FILES ${RKNN_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/model)
这条命令同样使用 install() 函数,但它用于安装文件。${RKNN_FILES} 是上面通过 file(GLOB ...) 收集的文件列表,DESTINATION 指定了这些文件的安装目录,这里是 ${CMAKE_INSTALL_PREFIX}/model,意味着所有匹配的 model.rknn 文件将被安装到 luckfox_rtsp_mnist_dir/model 目录下。
2.代码解析
我们这里也只需要了解main.cc文件即可。一共只有723行,我们看下大佬如何修改的代码:
/*****************************************************************************
* | Author : Luckfox team
* | Modified By : knv luyism
* | Function :
* | Info :
*
*----------------
* | This version: V1.1
* | Date : 2024-05-23
* | Info : Basic version
* | Function Add: 1. Add the function of recognizing numbers in the image
* | 2. Add the function of displaying the recognized number and its probability
* | 3. Add the function of displaying the frame rate
*
******************************************************************************/
/*头文件包含: 包含了标准C库头文件、Rockchip平台专用的头文件、OpenCV库的头文件。*/
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/poll.h>
#include <time.h>
#include <unistd.h>
#include <vector>
#include "rtsp_demo.h"
#include "luckfox_mpi.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <rknn_api.h>
#define MODEL_WIDTH 28
#define MODEL_HEIGHT 28
#define CHANNEL_NUM 1
// 默认 RKNN_TENSOR_UINT8
rknn_tensor_type input_type = RKNN_TENSOR_UINT8;
// 默认 RKNN_TENSOR_NHWC
rknn_tensor_format input_layout = RKNN_TENSOR_NHWC;
// rknn_context ctx = 0;
// 定义一个结构体储存预测到的数字和其对应的概率
struct Prediction
{
int digit;
float probability;
};
// 定义全局变量简单队列用于存储预测到的数字和其对应的概率
std::vector<Prediction> predictions_queue;
// 定义一个结构体用于存储rknn的上下文
typedef struct {
rknn_context rknn_ctx;
rknn_tensor_mem* max_mem;
rknn_tensor_mem* net_mem;
rknn_input_output_num io_num;
rknn_tensor_attr* input_attrs;
rknn_tensor_attr* output_attrs;
rknn_tensor_mem* input_mems[3];
rknn_tensor_mem* output_mems[3];
int model_channel;
int model_width;
int model_height;
bool is_quant;
} rknn_app_context_t;
// 从文件中加载模型
static unsigned char *load_model(const char *filename, int *model_size)
{
// 打开指定的文件以读取二进制数据
FILE *fp = fopen(filename, "rb");
if (fp == nullptr)
{
printf("fopen %s fail!\n", filename);
return NULL;
}
fseek(fp, 0, SEEK_END);
int model_len = ftell(fp);
unsigned char *model = (unsigned char *)malloc(model_len); // 申请模型大小的内存,返回指针
fseek(fp, 0, SEEK_SET);
// 检查读取模型数据是否成功,如果读取失败,则打印错误信息并释放内存,然后返回空指针。
if (model_len != fread(model, 1, model_len, fp))
{
printf("fread %s fail!\n", filename);
free(model);
return NULL;
}
*model_size = model_len;
if (fp)
{
fclose(fp);
}
return model;
}
// 打印张量属性
static void dump_tensor_attr(rknn_tensor_attr *attr)
{
printf(" index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, "
"zp=%d, scale=%f\n",
attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3],
attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type),
get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale);
}
// 在图像中找到数字的轮廓,同时减小找到轮廓时的抖动
cv::Rect find_digit_contour(const cv::Mat &image) {
// 预处理图像
cv::Mat gray, blurred, edged;
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
cv::Canny(blurred, edged, 30, 150);
// 应用形态学操作
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::dilate(edged, edged, kernel);
cv::erode(edged, edged, kernel);
// 查找轮廓,声明一个变量来存储轮廓
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edged, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
if (contours.empty()) {
return cv::Rect();
}
// 找到最大的轮廓
auto largest_contour = std::max_element(contours.begin(), contours.end(),
[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {
return cv::contourArea(a) < cv::contourArea(b);
});
// **轮廓面积过滤**:在找到轮廓之后,可以排除那些面积过小的轮廓。这样可以减少不必要的小轮廓对整体结果的影响。
if (cv::contourArea(*largest_contour) < 10) {
return cv::Rect();
}
// **轮廓形状过滤**:除了面积外,还可以考虑其他形状特征,如轮廓宽高比。这样可以排除一些不规则的轮廓,从而提高准确性。
cv::Rect bounding_box = cv::boundingRect(*largest_contour);
float aspect_ratio = static_cast<float>(bounding_box.width) / bounding_box.height;
if (aspect_ratio < 0.2 || aspect_ratio > 3) {
return cv::Rect();
}
// **轮廓稳定性检测**:
// 通过比较当前帧和之前几帧的轮廓位置来判断轮廓的稳定性。
// 如果多帧之间的轮廓位置变化较小,则可以认为轮廓比较稳定,不需要进行过多的调整。
static std::vector<cv::Rect> prev_bounding_boxes;
if (prev_bounding_boxes.size() > 5) {
prev_bounding_boxes.erase(prev_bounding_boxes.begin());
}
prev_bounding_boxes.push_back(bounding_box);
if (prev_bounding_boxes.size() == 5) {
float avg_width = 0.0;
float avg_height = 0.0;
for (const auto& box : prev_bounding_boxes) {
avg_width += box.width;
avg_height += box.height;
}
avg_width /= prev_bounding_boxes.size();
avg_height /= prev_bounding_boxes.size();
float width_diff = std::abs(bounding_box.width - avg_width) / avg_width;
float height_diff = std::abs(bounding_box.height - avg_height) / avg_height;
if (width_diff > 0.1 || height_diff > 0.1) {
return cv::Rect();
}
}
// 对图像边框每个方向扩大15个像素
bounding_box.x = std::max(0, bounding_box.x - 15);
bounding_box.y = std::max(0, bounding_box.y - 15);
bounding_box.width = std::min(image.cols - bounding_box.x, bounding_box.width + 30);
bounding_box.height = std::min(image.rows - bounding_box.y, bounding_box.height + 30);
// 返回最大轮廓的边界框
return bounding_box;
}
// 预处理数字区域
cv::Mat preprocess_digit_region(const cv::Mat ®ion)
{
// 将图像转换为灰度图像,然后调整大小为28x28,最后将像素值归一化为0到1之间的浮点数
cv::Mat gray, resized, bitwized, normalized;
cv::cvtColor(region, gray, cv::COLOR_BGR2GRAY);
// 扩大图像中的数字轮廓,使其更容易识别
cv::threshold(gray, gray, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
// 调整图像颜色,将图像颜色中低于127的像素值设置为0,高于200的像素值设置为255
cv::threshold(gray, gray, 127, 255, cv::THRESH_BINARY_INV);
// 对图像黑白进行反转,黑色变成白色,白色变成黑色
cv::bitwise_not(gray, bitwized);
// 手动实现黑白反转
for (int i = 0; i < bitwized.rows; i++)
{
for (int j = 0; j < bitwized.cols; j++)
{
bitwized.at<uchar>(i, j) = 255 - bitwized.at<uchar>(i, j);
}
}
// 将图片大小调整为28x28,图片形状不发生畸变,过短的部分使用黑色填充
cv::resize(bitwized, resized, cv::Size(28, 28), 0, 0, cv::INTER_AREA);
//定义一个局部静态变量,让其只初始化一次,让其在等于200时将图片保存到本目录下
static int count = 0;
if (count == 200)
{
cv::imwrite("pre.jpg", resized);
}
count++;
printf("count=%d\n", count);
return resized;
}
// 将量化的INT8数据转换为浮点数
// Parameters:
// qnt: 量化后的整数数据
// zp: 零点(zero point)值,用于零点偏移(zero-point offset)
// scale: 缩放因子,用于缩放量化后的整数数据到浮点数范围
// Returns:
// 浮点数,表示经过反量化(dequantization)后的数据
static float deqnt_affine_to_f32(int8_t qnt, int32_t zp, float scale) { return ((float)qnt - (float)zp) * scale; }
// 将模型输出进行归一化,并计算输出的概率分布
// Parameters:
// output_attrs: 输出张量属性,包含了零点(zero point)值和缩放因子等信息
// output: 模型输出的数据,以INT8格式存储
// out_fp32: 存储归一化后的浮点数输出数据
static void output_normalization(rknn_tensor_attr* output_attrs, uint8_t *output, float *out_fp32)
{
int32_t zp = output_attrs->zp;
float scale = output_attrs->scale;
// 将INT8格式的输出数据进行反量化为浮点数,并进行存储
for(int i = 0; i < 10; i ++)
out_fp32[i] = deqnt_affine_to_f32(output[i],zp,scale);
// 计算输出数据的L2范数
float sum = 0;
for(int i = 0; i < 10; i++)
sum += out_fp32[i] * out_fp32[i];
// 对归一化后的浮点数输出进行归一化处理,确保输出数据的范围在[0,1]之间
float norm = sqrt(sum);
for(int i = 0; i < 10; i++)
out_fp32[i] /= norm;
// 打印输出数据的值
printf("\n===================Output data values:===================\n");
for (int i = 0; i < 10; ++i)
{
printf("%f ", out_fp32[i]);
}
printf("\n");
// 找出最大概率对应的数字,并记录最大概率及其对应的数字
float max_prob = -1.0;
int predicted_digit = -1;
// 计算最大值的索引
for (int i = 0; i < 10; ++i)
{
if (out_fp32[i] > max_prob)
{
max_prob = out_fp32[i];
predicted_digit = i;
}
}
// 将预测的数字及其对应的概率记录到队列中
predictions_queue.push_back({predicted_digit, max_prob});
// 打印预测的数字与其对应的概率
printf("========Predicted digit: %d, Probability: %.2f========\n\n", predicted_digit, max_prob);
}
//定义init_mnist_model函数,用于初始化mnist模型
int init_mnist_model(const char *model_path,rknn_app_context_t *app_mnist_ctx)
{
int ret;
int model_len = 0;
rknn_context ctx_mnist = 0;
// char *model;
// ret = rknn_init(&ctx_mnist, (char *)model_path, 0, 0, NULL);
// if (ret < 0)
// {
// printf("rknn_init fail! ret=%d\n", ret);
// return -1;
// }
unsigned char * model = load_model(model_path, &model_len);
ret = rknn_init(&ctx_mnist, model, model_len, 0, NULL);
if (ret < 0)
{
printf("rknn_init failed! ret=%d", ret);
return -1;
}
// Get sdk and driver version
rknn_sdk_version sdk_ver;
ret = rknn_query(ctx_mnist, RKNN_QUERY_SDK_VERSION, &sdk_ver, sizeof(sdk_ver));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printf("rknn_api/rknnrt version: %s, driver version: %s\n", sdk_ver.api_version, sdk_ver.drv_version);
// Get Model Input Output Info
rknn_input_output_num io_num;
ret = rknn_query(ctx_mnist, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
// 打印输入张量
printf("\ninput tensors:\n");
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (uint32_t i = 0; i < io_num.n_input; i++)
{
input_attrs[i].index = i;
// query info
ret = rknn_query(ctx_mnist, RKNN_QUERY_NATIVE_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
if (ret < 0)
{
printf("rknn_init error! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&input_attrs[i]);
}
// 打印输出张量
printf("\noutput tensors:\n");
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (uint32_t i = 0; i < io_num.n_output; i++)
{
output_attrs[i].index = i;
// When using the zero-copy API interface, query the native output tensor attribute
ret = rknn_query(ctx_mnist, RKNN_QUERY_NATIVE_NHWC_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC)
{
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
dump_tensor_attr(&output_attrs[i]);
}
// default input type is int8 (normalize and quantize need compute in outside)
// if set uint8, will fuse normalize and quantize to npu
input_attrs[0].type = input_type;
// default fmt is NHWC, npu only support NHWC in zero copy mode
input_attrs[0].fmt = input_layout;
printf("input_attrs[0].size_with_stride=%d\n", input_attrs[0].size_with_stride);
// Create input tensor memory
app_mnist_ctx->input_mems[0] = rknn_create_mem(ctx_mnist, input_attrs[0].size_with_stride);
// 设置输入张量内存
ret = rknn_set_io_mem(ctx_mnist, app_mnist_ctx->input_mems[0], &input_attrs[0]);
if (ret < 0)
{
printf("rknn_set_io_mem fail! ret=%d\n", ret);
return -1;
}
// 设置输出张量内存
for (uint32_t i = 0; i < io_num.n_output; ++i)
{
app_mnist_ctx->output_mems[i] = rknn_create_mem(ctx_mnist, output_attrs[i].size_with_stride);
// printf("output_attrs[%d].size_with_stride=%d\n", i, output_attrs[i].size_with_stride);
// set output memory and attribute
ret = rknn_set_io_mem(ctx_mnist, app_mnist_ctx->output_mems[i], &output_attrs[i]);
if (ret < 0)
{
printf("rknn_set_io_mem fail! ret=%d\n", ret);
return -1;
}
}
// 将模型的上下文信息存储到app_mnist_ctx中
app_mnist_ctx->rknn_ctx = ctx_mnist;
// TODO
if (output_attrs[0].qnt_type == RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC)
{
app_mnist_ctx->is_quant = true;
}
else
{
app_mnist_ctx->is_quant = false;
}
app_mnist_ctx->io_num = io_num;
app_mnist_ctx->input_attrs = (rknn_tensor_attr *)malloc(io_num.n_input * sizeof(rknn_tensor_attr));
memcpy(app_mnist_ctx->input_attrs, input_attrs, io_num.n_input * sizeof(rknn_tensor_attr));
app_mnist_ctx->output_attrs = (rknn_tensor_attr *)malloc(io_num.n_output * sizeof(rknn_tensor_attr));
memcpy(app_mnist_ctx->output_attrs, output_attrs, io_num.n_output * sizeof(rknn_tensor_attr));
printf("model is NHWC input fmt\n");
app_mnist_ctx->model_height = input_attrs[0].dims[1];
app_mnist_ctx->model_width = input_attrs[0].dims[2];
app_mnist_ctx->model_channel = input_attrs[0].dims[3];
// 打印模型输出信息
printf("model input height=%d, width=%d, channel=%d\n",
app_mnist_ctx->model_height, app_mnist_ctx->model_width, app_mnist_ctx->model_channel);
printf("Init success \n");
return 0;
}
// 定义内存释放函数,用于释放模型的内存
int release_mnist_model(rknn_app_context_t *app_ctx)
{
if (app_ctx->rknn_ctx != 0)
{
rknn_destroy(app_ctx->rknn_ctx);
app_ctx->rknn_ctx = 0;
}
if (app_ctx->net_mem != NULL)
{
printf("destory mem\n");
rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->net_mem);
free(app_ctx->net_mem);
}
if (app_ctx->max_mem != NULL)
{
printf("destory mem\n");
rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->max_mem);
free(app_ctx->max_mem);
}
if (app_ctx->input_attrs != NULL)
{
free(app_ctx->input_attrs);
app_ctx->input_attrs = NULL;
}
if (app_ctx->output_attrs != NULL)
{
free(app_ctx->output_attrs);
app_ctx->output_attrs = NULL;
}
for (int i = 0; i < app_ctx->io_num.n_input; i++) {
if (app_ctx->input_mems[i] != NULL) {
rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->input_mems[i]);
free(app_ctx->input_mems[i]);
}
}
for (int i = 0; i < app_ctx->io_num.n_output; i++) {
if (app_ctx->output_mems[i] != NULL) {
rknn_destroy_mem(app_ctx->rknn_ctx, app_ctx->output_mems[i]);
free(app_ctx->output_mems[i]);
}
}
return 0;
}
int run_inference(rknn_app_context_t *app_ctx, cv::Mat &frame)
{
int ret;
//****************传入图像至推理内存区******************//
int width = app_ctx->input_attrs[0].dims[2];
int stride = app_ctx->input_attrs[0].w_stride;
if (width == stride)
{
memcpy(app_ctx->input_mems[0]->virt_addr, frame.data, width * app_ctx->input_attrs[0].dims[1] * app_ctx->input_attrs[0].dims[3]);
}
else
{
int height = app_ctx->input_attrs[0].dims[1];
int channel = app_ctx->input_attrs[0].dims[3];
// copy from src to dst with stride
uint8_t *src_ptr = frame.data;
uint8_t *dst_ptr = (uint8_t *)app_ctx->input_mems[0]->virt_addr;
// width-channel elements
int src_wc_elems = width * channel;
int dst_wc_elems = stride * channel;
for (int h = 0; h < height; ++h)
{
memcpy(dst_ptr, src_ptr, src_wc_elems);
src_ptr += src_wc_elems;
dst_ptr += dst_wc_elems;
}
}
// 运行推理
ret = rknn_run(app_ctx->rknn_ctx, nullptr);
if (ret < 0)
{
printf("rknn_run failed! %s\n", ret);
return -1;
}
uint8_t *output= (uint8_t*)malloc(sizeof(uint8_t) * 10);
float *out_fp32 = (float*)malloc(sizeof(float) * 10);
output = (uint8_t *)app_ctx->output_mems[0]->virt_addr;
output_normalization(&app_ctx->output_attrs[0], output, out_fp32);
return 0;
}
int main(int argc, char *argv[])
{
// rknn init
if (argc != 2)
{
printf("Usage: %s <model.rknn>", argv[0]);
return -1;
}
char *model_path = argv[1];
RK_S32 s32Ret = 0;
int sX, sY, eX, eY;
int width = 640;
int height = 480;
char fps_text[16];
float fps = 0;
memset(fps_text, 0, 16);
// h264_frame
VENC_STREAM_S stFrame;
stFrame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S));
VIDEO_FRAME_INFO_S h264_frame;
VIDEO_FRAME_INFO_S stVpssFrame;
// rkaiq init
RK_BOOL multi_sensor = RK_FALSE;
const char *iq_dir = "/etc/iqfiles";
rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL;
// hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2;
SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir);
SAMPLE_COMM_ISP_Run(0);
// rkmpi init
if (RK_MPI_SYS_Init() != RK_SUCCESS)
{
RK_LOGE("rk mpi sys init fail!");
return -1;
}
// rtsp init
rtsp_demo_handle g_rtsplive = NULL;
rtsp_session_handle g_rtsp_session;
g_rtsplive = create_rtsp_demo(554);
g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0");
rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime());
// vi init
vi_dev_init();
vi_chn_init(0, width, height);
// vpss init
vpss_init(0, width, height);
// bind vi to vpss
MPP_CHN_S stSrcChn, stvpssChn;
stSrcChn.enModId = RK_ID_VI;
stSrcChn.s32DevId = 0;
stSrcChn.s32ChnId = 0;
stvpssChn.enModId = RK_ID_VPSS;
stvpssChn.s32DevId = 0;
stvpssChn.s32ChnId = 0;
printf("====RK_MPI_SYS_Bind vi0 to vpss0====\n");
s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvpssChn);
if (s32Ret != RK_SUCCESS)
{
RK_LOGE("bind 0 ch venc failed");
return -1;
}
// venc init
RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC;
venc_init(0, width, height, enCodecType);
// rknn结构体变量
rknn_app_context_t app_mnist_ctx;
memset(&app_mnist_ctx, 0, sizeof(rknn_app_context_t));
init_mnist_model(model_path, &app_mnist_ctx);
while (1)
{
// get vpss frame
s32Ret = RK_MPI_VPSS_GetChnFrame(0, 0, &stVpssFrame, -1);
if (s32Ret == RK_SUCCESS)
{
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
// 复制一个帧,然后使用模型推理
cv::Mat frame(height, width, CV_8UC3, data);
// 在图像中找到数字的轮廓
cv::Rect digit_rect = find_digit_contour(frame);
if (digit_rect.area() > 0)
{
cv::Mat digit_region = frame(digit_rect);
cv::Mat preprocessed = preprocess_digit_region(digit_region);
// 运行推理
run_inference(&app_mnist_ctx, preprocessed);
// 从predictions_queue中获取预测到的数字和其对应的概率
if (!predictions_queue.empty())
{
Prediction prediction = predictions_queue.back();
cv::rectangle(frame, digit_rect, cv::Scalar(0, 255, 0), 2);
// 在图像上显示预测结果,显示字号为1,颜色为红色,粗细为2
cv::putText(frame, std::to_string(prediction.digit), cv::Point(digit_rect.x, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 0, 0), 2);
// 在图像上显示预测概率
cv::putText(frame, std::to_string(prediction.probability), cv::Point(digit_rect.x+ 30, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(230, 0, 0), 2);
// 打印预测到的数字和其对应的概率
// printf("****** Predicted digit: %d, Probability: %.2f ******\n", prediction.digit, prediction.probability);
// 从predictions_queue中删除最旧的元素
predictions_queue.pop_back();
}
}
sprintf(fps_text, "fps:%.2f", fps);
cv::putText(frame, fps_text,
cv::Point(40, 40),
cv::FONT_HERSHEY_SIMPLEX, 1,
cv::Scalar(0, 255, 0), 2);
memcpy(data, frame.data, width * height * 3);
}
// send stream
// encode H264
RK_MPI_VENC_SendFrame(0, &stVpssFrame, -1);
// rtsp
s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1);
if (s32Ret == RK_SUCCESS)
{
if (g_rtsplive && g_rtsp_session)
{
// printf("len = %d PTS = %d \n",stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
void *pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len,
stFrame.pstPack->u64PTS);
rtsp_do_event(g_rtsplive);
}
RK_U64 nowUs = TEST_COMM_GetNowUs();
fps = (float)1000000 / (float)(nowUs - stVpssFrame.stVFrame.u64PTS);
}
// release frame
s32Ret = RK_MPI_VPSS_ReleaseChnFrame(0, 0, &stVpssFrame);
if (s32Ret != RK_SUCCESS)
{
RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret);
}
s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame);
if (s32Ret != RK_SUCCESS)
{
RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
}
}
RK_MPI_SYS_UnBind(&stSrcChn, &stvpssChn);
RK_MPI_VI_DisableChn(0, 0);
RK_MPI_VI_DisableDev(0);
RK_MPI_VPSS_StopGrp(0);
RK_MPI_VPSS_DestroyGrp(0);
SAMPLE_COMM_ISP_Stop(0);
RK_MPI_VENC_StopRecvFrame(0);
RK_MPI_VENC_DestroyChn(0);
free(stFrame.pstPack);
if (g_rtsplive)
rtsp_del_demo(g_rtsplive);
RK_MPI_SYS_Exit();
// 释放模型内存
release_mnist_model(&app_mnist_ctx);
return 0;
}
可以看出当前的 main.cc 是在官方例子的基础上进行了扩展,增加了使用RKNN模型进行图像中数字识别的功能。最主要的区别是:
-
RKNN模型加载与推理:
-
加载RKNN模型到内存。
-
定义结构体存储预测的数字及其概率。
-
在图像中检测数字轮廓,并进行预处理。
-
执行模型推理,并将预处理后的图像数据传入模型。
-
将模型输出进行归一化,计算概率,并存储预测结果。
-
数字识别与显示:
-
在主循环中,检测图像中的数字,并使用RKNN模型进行识别。
-
在图像上绘制识别的数字及其概率。
-
维护一个预测结果队列,并在图像上显示最新预测结果。
下面我们详细分析下在main函数中都做了那些事情:
int main(int argc, char *argv[]) {
// ... 省略了共同的初始化代码 ...
// rknn init
if (argc != 2) {
printf("Usage: %s <model.rknn>", argv[0]);
return -1;
}
char *model_path = argv[1];
// ------- 初始化 RKNN 模型
rknn_app_context_t app_mnist_ctx;
memset(&app_mnist_ctx, 0, sizeof(rknn_app_context_t));
init_mnist_model(model_path, &app_mnist_ctx);
while (1) {
// get vpss frame
s32Ret = RK_MPI_VPSS_GetChnFrame(0, 0, &stVpssFrame, -1);
if (s32Ret == RK_SUCCESS) {
void *data = RK_MPI_MB_Handle2VirAddr(stVpssFrame.stVFrame.pMbBlk);
// 复制一个帧,然后使用模型推理
cv::Mat frame(height, width, CV_8UC3, data);
// ------- 在图像中找到数字的轮廓
cv::Rect digit_rect = find_digit_contour(frame);
if (digit_rect.area() > 0) {
cv::Mat digit_region = frame(digit_rect);
cv::Mat preprocessed = preprocess_digit_region(digit_region);
// ------- 运行推理
run_inference(&app_mnist_ctx, preprocessed);
// ------- 从predictions_queue中获取预测到的数字和其对应的概率
if (!predictions_queue.empty()) {
Prediction prediction = predictions_queue.back();
cv::rectangle(frame, digit_rect, cv::Scalar(0, 255, 0), 2);
cv::putText(frame, std::to_string(prediction.digit), cv::Point(digit_rect.x, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(255, 0, 0), 2);
cv::putText(frame, std::to_string(prediction.probability), cv::Point(digit_rect.x + 30, digit_rect.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(230, 0, 0), 2);
// ------- 从predictions_queue中删除最旧的元素
predictions_queue.pop_back();
}
}
sprintf(fps_text, "fps:%.2f", fps);
cv::putText(frame, fps_text,
cv::Point(40, 40),
cv::FONT_HERSHEY_SIMPLEX, 1,
cv::Scalar(0, 255, 0), 2);
memcpy(data, frame.data, width * height * 3);
}
// send stream
// ... 省略了共同的编码和发送代码 ...
// release frame
// ... 省略了共同的释放帧代码 ...
}
// ... 省略了共同的清理代码 ...
// ------- 释放 RKNN 模型内存
release_mnist_model(&app_mnist_ctx);
return 0;
}
可以看出,最主要是使用了零拷贝的API,想了解的可以看下大佬原帖:https://bbs.eeworld.com.cn/thread-1282745-1-1.html
大致框架也就是下面这个样子:
六、总结
最后总结下:
模型识别率在电脑中测试可以高达98%,但实时用摄像头采集并用模型识别准确率只能达到60%左右。
可能的原因:
-
光照和图像质量:实时摄像头采集的图像可能受到光照条件的影响,导致图像质量不佳。
-
图像分辨率和缩放:实时采集的图像分辨率与模型训练时使用的图像分辨率不同,图像缩放导致信息丢失。。
-
模型输入不一致:实时图像可能与训练模型时使用的图像在尺寸、颜色空间或数据归一化方面不一致。
-
摄像头硬件限制:摄像头本身的硬件限制,比如分辨率、对焦、镜头质量等,可能影响图像的清晰度和准确率。
改进措施:
-
改善摄像头硬件限制条件:确保摄像头在充足的光照下工作,或使用更好的分辨率、对焦、镜头的质量。
-
调整图像预处理:对实时采集的图像进行更细致的预处理,如直方图均衡化、对比度增强等。
-
优化模型输入:确保实时图像的分辨率、颜色空间和归一化方法与模型训练时保持一致。
-
模型重训练:使用实时采集的图像对模型进行进一步的训练或微调,以适应实际应用场景。
-
增加模型复杂度:可以尝试使用更复杂的模型结构来提高识别准确率。
-
性能优化:更换芯片提高计算能力,优化代码和算法,减少实时处理的延迟,确保有足够的时间进行图像处理。
-
数据增强:在模型训练阶段应用数据增强技术,使模型更适应各种不同的图像条件。
-
后处理改进:应用后处理技术,如非极大值抑制(NMS)或阈值处理,以提高识别准确率。