4251|0

64

帖子

0

TA的资源

一粒金砂(中级)

楼主
 

#AI挑战营终点站#基于RV1106通过摄像头采集实现手写数字识别部署 [复制链接]

本帖最后由 硬核王同学 于 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)环境准备

 

  1. 安装驱动,下载RK驱动助手 DriverAssitant(去官网下载)。打开RK驱动助手 DriverAssitant 安装 USB 驱动程序,此过程无需连接,安装完成后重启电脑。

  2. 镜像下载(网盘下载),太大了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 固件。

  3. 镜像烧录,下载和解压烧录工具(去官网下载)。

 

2)烧录详细步骤

 

  1. 按住 BOOT 键后连接电脑后,松开 BOOT 键,瑞芯微刷机工具就会显示 MaskRom 设备。

  2. 加载固件的存放目录,重载 env 文件,勾选所有项。

  3. 点击下载。

 

 

这样就成功了。

 

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)搭建编译环境

  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

 

  1. 获取最新的 SDK :

git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git

 

git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git

2)SDK 目录说明

  1. SDK目录结构

├── build.sh - project/build.sh ---- SDK编译脚本
├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
├── project ------------------------- 参考应用、编译配置以及脚本目录
├── output -------------------------- SDK编译后镜像文件存放目录
└── tools --------------------------- 烧录镜像打包工具以及烧录工具
  1. 镜像存放目录

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这个源码环境包是必须的,后边很多都依赖这里边的工具。

 

  1. 安装交叉编译工具链

cd tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/ source env_install_toolchain.sh
  1. 编译

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.代码框架

 

  1. 3rdparty: 这个目录通常用于存放第三方库或依赖。在这里,它包含 allocator、librga 和 rknpu2 三个子目录。

  2. build: 这个目录用于存放构建(编译)过程中生成的文件。

  3. common: 这个目录包含项目中多个地方会用到的通用代码、库或资源。

  4. include: 这个目录通常用于存放项目中用到的头文件。

  5. lib: 这个目录通常用于存放库文件(如 .a 或 .so 文件)。

  6. luckfox_rtsp_opencv_demo: 这个目录包含与项目演示或示例相关的代码或资源。

  7. 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;
}

 

最后我们总结下整体的代码逻辑:

  1. 初始化阶段:

    1. 设置视频帧的宽度和高度。

    2. 初始化帧率显示文本和计算变量。

    3. 初始化编码后视频流和视频帧信息的结构体。

    4. 初始化ISP模块。

    5. 初始化Rockchip MPI系统。

    6. 初始化RTSP会话和设置视频流参数。

    7. 初始化视频输入设备和VPSS。

    8. 将视频输入(VI)绑定到视频处理子系统(VPSS)。

    9. 初始化视频编码器,设置编码参数。

  2. 主循环阶段:

    1. 获取VPSS帧: 从VPSS获取视频帧。

    2. 处理和显示帧:

      • 将VPSS帧的内存块地址转换为虚拟地址。

      • 使用OpenCV创建Mat对象,处理和显示视频帧。

      • 更新帧的内容,如将处理后的图像数据复制回帧的内存空间。

    3. 编码H264视频帧: 发送处理后的帧到视频编码器进行编码。

    4. 获取编码后的视频流: 从视频编码器获取编码后的视频流。

    5. RTSP传输:

      • 如果存在有效的RTSP会话,将编码后的视频流通过RTSP传输。

    6. 计算帧率: 根据当前时间和上一帧的时间戳计算帧率。

    7. 释放资源: 释放VPSS帧和编码流占用的资源。

  3. 清理阶段:

    1. 解除VI和VPSS的绑定。

    2. 停止并销毁VPSS组。

    3. 停止ISP模块。

    4. 停止视频编码器接收帧,销毁编码器通道。

    5. 释放分配的内存。

    6. 退出MPI系统。

基本上可以看成这张图:

 

 

2)大佬例子

1.代码框架

 

  1. 3rdparty: 此目录通常用于存放项目中使用的第三方库或依赖。

  2. build: 此目录用于存放构建(编译)过程中生成的文件。

  3. common: 此目录包含项目中多个地方会用到的通用代码、库或资源。

  4. include: 此目录通常用于存放项目中用到的头文件。

  5. lib: 此目录通常用于存放库文件(如 .a 或 .so 文件)。

  6. luckfox_rtsp_mnist_dir: 这个目录与MNIST数据集的处理或识别有关,包含模型文件和库文件。

  7. model: 此目录用于存放项目中使用的模型文件,这些文件可以是配置文件或数据模型。

  8. 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)

 

这部分是大佬修改的,重新定义了项目的安装规则。

  1. 设置安装前缀:

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 的子目录。

  1. 收集文件:

file(GLOB RKNN_FILES "${CMAKE_CURRENT_SOURCE_DIR}/model/model.rknn")

这条命令使用 file(GLOB ...) 来收集与模式匹配的文件列表。GLOB 是 "global" 的缩写,用于查找所有与给定路径模式匹配的文件。这里,它查找位于 ${CMAKE_CURRENT_SOURCE_DIR}/model/ 目录下所有名为 model.rknn 的文件,并将这些文件的路径存储在变量 RKNN_FILES 中。

  1. 安装目标:

install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_PREFIX})

这条命令指定了项目构建生成的目标(可能是可执行文件或库文件)的安装规则。install() 函数定义了如何将构建目标安装到系统中。${PROJECT_NAME} 是在 project() 命令中定义的项目名称,这里假设它是一个构建目标。DESTINATION 指定了这些目标文件的安装目录,即上面设置的 CMAKE_INSTALL_PREFIX。

  1. 安装文件:

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模型进行图像中数字识别的功能。最主要的区别是:

  1. RKNN模型加载与推理:

    1. 加载RKNN模型到内存。

    2. 定义结构体存储预测的数字及其概率。

    3. 在图像中检测数字轮廓,并进行预处理。

    4. 执行模型推理,并将预处理后的图像数据传入模型。

    5. 将模型输出进行归一化,计算概率,并存储预测结果。

  2. 数字识别与显示:

    1. 在主循环中,检测图像中的数字,并使用RKNN模型进行识别。

    2. 在图像上绘制识别的数字及其概率。

    3. 维护一个预测结果队列,并在图像上显示最新预测结果。

 

下面我们详细分析下在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%左右。

 

可能的原因:

  1. 光照和图像质量:实时摄像头采集的图像可能受到光照条件的影响,导致图像质量不佳。

  2. 图像分辨率和缩放:实时采集的图像分辨率与模型训练时使用的图像分辨率不同,图像缩放导致信息丢失。。

  3. 模型输入不一致:实时图像可能与训练模型时使用的图像在尺寸、颜色空间或数据归一化方面不一致。

  4. 摄像头硬件限制:摄像头本身的硬件限制,比如分辨率、对焦、镜头质量等,可能影响图像的清晰度和准确率。

改进措施:

  1. 改善摄像头硬件限制条件:确保摄像头在充足的光照下工作,或使用更好的分辨率、对焦、镜头的质量。

  2. 调整图像预处理:对实时采集的图像进行更细致的预处理,如直方图均衡化、对比度增强等。

  3. 优化模型输入:确保实时图像的分辨率、颜色空间和归一化方法与模型训练时保持一致。

  4. 模型重训练:使用实时采集的图像对模型进行进一步的训练或微调,以适应实际应用场景。

  5. 增加模型复杂度:可以尝试使用更复杂的模型结构来提高识别准确率。

  6. 性能优化:更换芯片提高计算能力,优化代码和算法,减少实时处理的延迟,确保有足够的时间进行图像处理。

  7. 数据增强:在模型训练阶段应用数据增强技术,使模型更适应各种不同的图像条件。

  8. 后处理改进:应用后处理技术,如非极大值抑制(NMS)或阈值处理,以提高识别准确率。

点赞 关注

回复
举报
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/10 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表