【Milk-V Duo 开发板1积分体验】分析源码,学习如何在Milk-V Duo 上实现TPU推理应用。

之前在Milk-V Duo上通过官方提供的分类程序运行了基于resnet18的模型推理,但对于C++具体程序怎么跑的还没有深入了解。这里找到源码,简单分析一下TPU程序的运行过程,资料主要参考官方《CVITECK TPU SDK使用指南》。

1. Runtime的基本开发流程

根据使用指南介绍,主要流程简要概括如下:

  • 加载模型。这里一般是CV180x对应的cvimodel模型文件。

  • 前处理。对输入数据进行处理,满足模型输入要求。

  • 获取输入、输出Tensor。这里使用的API函数:CVI_NN_GetInputOutputTensors,获取输入、输出张量数组和个数。

  • 执行推理。使用CVI_NN_Forward函数。

  • 后处理。进一步处理模型推理的结果,得到所需信息。

2. 源码分析Runtime库的使用细节

这里使用官方提供的分类源代码来主要分析如何利用TPU SDK使用模型实现推理功能,具体代码可以通过算能的ftp服务器下载。

2.1 首先是头文件,包括了程序中使用的库,重点是opencv和runtime库,其中图片的处理依靠OpenCV,而推理相关API依赖runtime库。

#include <stdio.h>
#include <fstream> //文件流
#include <string> 
#include <numeric>
#include <cviruntime.h>  // CV18系列TPU的运行时
#include <opencv2/opencv.hpp> //OpenCV库

2.2 归一化参数定义

对于输入图像数据归一化处理,提前定义了尺寸、mean,scale等参数,具体值与采用模型相关。

#define IMG_RESIZE_DIMS 256,256
#define BGR_MEAN        103.94,116.78,123.68
#define INPUT_SCALE     0.017

2.3 主程序关键代码分析

接下来是main函数的内容,这里主要截取关键代码,来分析模型文件的使用流程。

2.3.1 创建模型句柄,并注册加载模型。

CVI_MODEL_HANDLE model = nullptr; //定义了模型句柄model
int ret = CVI_NN_RegisterModel(model_file, &model);
//这里是注册(加载)模型文件到模型句柄
if (CVI_RC_SUCCESS != ret) {
printf("CVI_NN_RegisterModel failed, err %d\n", ret);
exit(1);
 }
 //判断加载是否正常,加载失败则退出程序。

2.3.2 获取模型输入、输出Tensor(张量)

在讲解代码前,我们看看CVI_TENSOR和CVI_SHAPE的定义。

CVI_TENSOR包含了张量的名称、形状(维度)、格式、数量、内存大小、内存指针、物理地址、输入内存类型、量化转换比例系数。

//关于张量的定义如下:
typedef struct {
char *name;  //张量的name
CVI_SHAPE shape; //维度信息
CVI_FMT fmt; //格式
size_t count;
size_t mem_size;
uint8_t *sys_mem;
uint64_t paddr;
CVI_MEM_TYPE_E mem_type;
float qscale;
...
} CVI_TENSOR;

而CVI_SHAPE的定义如下:

//关于张量维度的定义如下:
#define CVI_DIM_MAX (6)
typedef struct {
int32_t dim[CVI_DIM_MAX];
size_t dim_size;
} CVI_SHAPE;

其中dim[ ]按照n/channel/h/w顺序排序

然后看具体的代码处理过程:

// 定义输入、输出张量指针
CVI_TENSOR *input_tensors;
CVI_TENSOR *output_tensors;
  
  // 定义输入、输出张量数量
  int32_t input_num;
int32_t output_num;
  
  //获得张量输入输出,分别将模型的输入张量、输出张量、以及数量大小映射到前面定义的变量中。
CVI_NN_GetInputOutputTensors(model, &input_tensors, &input_num, &output_tensors,
&output_num);
  
  //利用张量默认名获得具体的输入、输出张量
  CVI_TENSOR *input = CVI_NN_GetTensorByName(CVI_NN_DEFAULT_TENSOR, input_tensors, input_num);
  CVI_TENSOR *output = CVI_NN_GetTensorByName(CVI_NN_DEFAULT_TENSOR, output_tensors, output_num);
  
  //获得输入张量的量化转换系数
float qscale = CVI_NN_TensorQuantScale(input);
printf("qscale:%f\n", qscale);

//获得输入张量的维度
CVI_SHAPE shape = CVI_NN_TensorShape(input);

  // nchw,请参考上面关于CVI_SHAPE的定义
int32_t height = shape.dim[2]; //这里是h
int32_t width = shape.dim[3]; //这里是w

以上代码获得了算法模型的输入和输出,主要包括输入张量的维度以及量化系数等,同时按照模型要求定义了height和width,用于后续OpenCV的处理。

2.3.3 利用OpenCV 处理输入图片数据
Resnet网络输入的图片是3通道244*244大小的图片。因此需要通过OpenCV对输入图片进行处理。

读取图像数据:

  // 读取图像,这里是命令行输入的第3个参数,即要推理的图片文件。
  cv::Mat image;
  image = cv::imread(argv[2]);
if (!image.data) {
printf("Could not open or find the image\n");
return -1;
  }

图像resize及crop处理:

// 对图像进行resize,这里默认采用线性模式。
  cv::resize(image, image, cv::Size(IMG_RESIZE_DIMS));
// 根据模型对输入大小的定义,取局部图片。
  cv::Size size = cv::Size(height, width);
//根据point位置和size大小,取局部图片信息
  cv::Rect crop(cv::Point(0.5 * (image.cols - size.width),
0.5 * (image.rows - size.height)), 
                          size);
  image = image(crop);

将图像分割成3个通道:

  // 按照模型尺寸要求,将图像分割为3个单通道
  cv::Mat channels[3]; //定义了3个矩阵通道
  for (int i = 0; i < 3; i++) {
    channels[i] = cv::Mat(height, width, CV_8SC1);//依次按照模型要求赋值
  }
// CV_8SC1  8位无符号单通道矩阵
//把图像分割对应到3个通道,对应B G R 三个顺序的通道。
  cv::split(image, channels);

// normalize 归一化
//定义了3个通道的mean参数

float mean[] = {BGR_MEAN};

//依次对3个通道进行归一化处理
for (int i = 0; i < 3; ++i) {
    channels[i].convertTo(channels[i], CV_8SC1, INPUT_SCALE * qscale,-1 * mean[i] * INPUT_SCALE * qscale); 
  }

2.3.4 把图像 数据加载到模型中,并开始推理运行。

// fill to input tensor 将图像数据填充到输入张量中
 //指针类型对应CV_8SC1 
int8_t *ptr = (int8_t *)CVI_NN_TensorPtr(input);
int channel_size = height * width;
for (int i = 0; i < 3; ++i) {
//这里采用的memcpy,内存拷贝的方式。按照B G R的顺序将数据拷贝进去输入张量中。
memcpy(ptr + i * channel_size, channels[i].data, channel_size);
  }

// run inference 执行推理过程
  CVI_NN_Forward(model, input_tensors, input_num, output_tensors, output_num);
printf("CVI_NN_Forward succeeded\n");

2.3.5 处理推理运行的结果

处理过程包括:

  • 定义labels向量,读取字典文件,将字典每行数据映射到labels中。

  • 获得输出张量的指针和数量。

  • 定义idx索引,利用模型输出的分类值大小排序。

  • 根据定义的top_num,来筛选出结果并打印出对应的参数、索引及内容。

具体代码注释如下:

// output result
std::vector<std::string> labels; //定义了一个标签向量
std::ifstream file(argv[3]); //定义了字典位置
if (!file) {
printf("Didn't find synset_words file\n");
exit(1);
  } else {
std::string line;
while (std::getline(file, line)) { //在文件中查找对应行,并以此填写进label向量中
      labels.push_back(std::string(line));
    }
  }

int32_t top_num = 5; 
//获取模型输出的指针
float *prob = (float *)CVI_NN_TensorPtr(output);
//获取模型输出的数量
int32_t count = CVI_NN_TensorCount(output);

// find top-k prob and cls
std::vector<size_t> idx(count); //定义了一个索引
std::iota(idx.begin(), idx.end(), 0); //填充索引都为0,?
//对prob指向的内容排序?存放在idx里?
std::sort(idx.begin(), idx.end(), [&prob](size_t idx_0, size_t idx_1) {return prob[idx_0] > prob[idx_1];});
// show results.
printf("------\n");
//依次获得最前5名的idx
for (size_t i = 0; i < top_num; i++) {
int top_k_idx = idx[i]; //读取idx中存的那个index索引
printf("  %f, idx %d", prob[top_k_idx], top_k_idx); //打印相关的信息和索引
if (!labels.empty())
printf(", %s", labels[top_k_idx].c_str()); //打印label中的信息,对应的txt中的内容
printf("\n");
  }
printf("------\n");
  CVI_NN_CleanupModel(model); //关掉模型,清空内存。。。
printf("CVI_NN_CleanupModel succeeded\n");
return 0;

上面代码使用了C++ vector向量,用于处理模型推理输出的矩阵数据。

至此代码分析完毕,这里是一个简单的分类模型代码分析,输出的结果仅是分类可能性最大的前5名。而yolo等识别算法除了分类以外,还涉及到bonding box的输出,output输出处理更为复杂,感兴趣的朋友可以耐心分析一下。

3. Runtime的使用及程序编译

3.1 经过上面分析,我们大体了解了算法模型在CV1800x上的使用流程,其实我们完全可以训练自己的模型,然后修改输入和输出相关的代码,通过对图形进行合理处理,然后将图像数据拷贝到模型中推理,从而完成个人模型的程序编写。

例如自己训练的垃圾分类模型,然后编写C++程序尝试运行。

3.2 TPU Runtime程序编译,可以参考官方的编译教程,需要注意TPU_SDK及交叉编译工具的环境变量设置,此外cmake的版本号也要关注修正,这样确保编译成功。
主要的编译指令参考如下:

cd cvitek_tpu_samples
mkdir build_soc
cd build_soc
cmake -G Ninja \
    -DCMAKE_BUILD_TYPE=RELEASE \
    -DCMAKE_C_FLAGS_RELEASE=-O3 \
    -DCMAKE_CXX_FLAGS_RELEASE=-O3 \
    -DCMAKE_TOOLCHAIN_FILE=$TPU_SDK_PATH/cmake/toolchain-riscv64-linux-musl-x86_64.cmake \
    -DTPU_SDK_PATH=$TPU_SDK_PATH \
    -DOPENCV_PATH=$TPU_SDK_PATH/opencv \
    -DCMAKE_INSTALL_PREFIX=../install_samples \
    ..
cmake --build . --target install

今天分享到此,如有错误请指正,感谢!

1 Like

非常好文章 :heart: 爱来自SOPHGO
@Lilp