之前在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
今天分享到此,如有错误请指正,感谢!