目录
一、准备工作
1.1 代码裁剪
1.2 测试运行
二、程序讲解
2.1 初始化
2.2 处理模型图片输入
2.3 推理函数
2.4 对输出结果进行处理
前言
本节主要讲解昇腾芯片,例程中使用resnet50推理图像类别的程序。本节讲解的程序,它的环境搭建与使用方法在前面的教程有讲解,第一次看的宝子可以移步前面的教程先搭建一下开发环境
一、准备工作
1.1 代码裁剪
代码目录:cd ~/samples/inference/modelInference/sampleResnetQuickStart
下图,对原来的代码进行备份,然后压缩一下代码量
#include <opencv2/opencv.hpp>
#include <dirent.h>
#include "acl/acl.h"
#include "label.h"
using namespace cv;
using namespace std;
class SampleResnetQuickStart {
public:
SampleResnetQuickStart(const char* ModelPath, int32_t modelWidth, int32_t modelHeight);
void InitResource();
void ProcessInput(const string testImgPath);
void Inference();
void GetResult();
private:
aclrtContext context_;
aclrtStream stream_;
uint32_t modelId_;
const char * modelPath_;
int32_t modelWidth_;
int32_t modelHeight_;
aclmdlDesc * modelDesc_;
aclmdlDataset * inputDataset_;
aclmdlDataset * outputDataset_;
void * inputBuffer_;
void * outputBuffer_;
size_t inputBufferSize_;
float * imageBytes;
String imagePath;
Mat srcImage;
aclrtRunMode runMode_;
};
SampleResnetQuickStart::SampleResnetQuickStart(const char* modelPath, int32_t modelWidth, int32_t modelHeight)
{
modelPath_ = modelPath;
modelWidth_ = modelWidth;
modelHeight_ = modelHeight;
}
void SampleResnetQuickStart::InitResource()
{
aclInit(""); // AscendCL初始化函数
aclrtSetDevice(0); // 设置本进程使用哪一个昇腾芯片
aclrtCreateContext(&context_, 0); // 在昇腾芯片上面创建一个集合,指定当前线程使用这个集合,传出集合的context(句柄)
aclrtCreateStream(&stream_); // 在集合中创建一个用来处理任务的stream(一个昇腾芯片可以创建几百上千的stream)
aclrtGetRunMode(&runMode_);
aclmdlLoadFromFile(modelPath_, &modelId_); // 加载一个模型,传出这个模型的modelId(句柄)
modelDesc_ = aclmdlCreateDesc(); // 创建模型描述符
aclmdlGetDesc(modelDesc_, modelId_); // 从模型modelId(句柄)中获取模型信息到模型描述符中
// 分配模型输入数据空间
inputDataset_ = aclmdlCreateDataset(); // 获取模型推理时传入的结构体
inputBufferSize_ = aclmdlGetInputSizeByIndex(modelDesc_, 0);
// 通过模型描述符,获取模型第1个输入需要的空间(模型可能有多个输入)
aclrtMalloc(&inputBuffer_, inputBufferSize_, ACL_MEM_MALLOC_HUGE_FIRST);
aclDataBuffer * inputData = aclCreateDataBuffer(inputBuffer_, inputBufferSize_);
aclmdlAddDatasetBuffer(inputDataset_, inputData); // 把分配的空间大小信息和地址指针,给模型推理时传入的结构体
// 分配模型输出数据空间
outputDataset_ = aclmdlCreateDataset(); // 获取模型推理时传出的结构体
size_t modelOutputSize = aclmdlGetOutputSizeByIndex(modelDesc_, 0);
// 通过模型描述符,获取模型第1个输出需要的空间(模型可能有多个输入)
aclrtMalloc(&outputBuffer_, modelOutputSize, ACL_MEM_MALLOC_HUGE_FIRST);
cout << modelOutputSize << endl;
aclDataBuffer * outputData = aclCreateDataBuffer(outputBuffer_, modelOutputSize);
aclmdlAddDatasetBuffer(outputDataset_, outputData); // 把分配的空间大小信息和地址指针,给模型推理时传出的结构体
}
void SampleResnetQuickStart::ProcessInput(const string testImgPath)
{
// read image from file by cv
imagePath = testImgPath;
srcImage = imread(testImgPath);
// zoom image to modelWidth_ * modelHeight_
Mat resizedImage;
resize(srcImage, resizedImage, Size(modelWidth_, modelHeight_));
// get properties of image
int32_t channel = resizedImage.channels();
int32_t resizeHeight = resizedImage.rows;
int32_t resizeWeight = resizedImage.cols;
// data standardization
const float min_chn_0 = 123.675;
const float min_chn_1 = 116.28;
const float min_chn_2 = 103.53;
const float var_reci_chn_0 = 0.0171247538316637;
const float var_reci_chn_1 = 0.0175070028011204;
const float var_reci_chn_2 = 0.0174291938997821;
float meanRgb[3] = {min_chn_2, min_chn_1, min_chn_0};
float stdRgb[3] = {var_reci_chn_2, var_reci_chn_1, var_reci_chn_0};
// create malloc of image, which is shape with NCHW
imageBytes = (float*)malloc(channel * resizeHeight * resizeWeight * sizeof(float));
memset(imageBytes, 0, channel * resizeHeight * resizeWeight * sizeof(float));
uint8_t bgrToRgb=2;
// image to bytes with shape HWC to CHW, and switch channel BGR to RGB
for (int c = 0; c < channel; ++c)
{
for (int h = 0; h < resizeHeight; ++h)
{
for (int w = 0; w < resizeWeight; ++w)
{
int dstIdx = (bgrToRgb - c) * resizeHeight * resizeWeight + h * resizeWeight + w;
imageBytes[dstIdx] = static_cast<float>((resizedImage.at<cv::Vec3b>(h, w)[c] - 1.0f*meanRgb[c]) * 1.0f*stdRgb[c] );
}
}
}
}
void SampleResnetQuickStart::Inference()
{
// 拷贝host数据到device中
aclrtMemcpyKind kind;
if (runMode_ == ACL_DEVICE)
{
kind = ACL_MEMCPY_DEVICE_TO_HOST;
}
else{
kind = ACL_MEMCPY_HOST_TO_DEVICE;
}
// 把opencv处理好的数据放入输入缓冲区中
aclrtMemcpy(inputBuffer_, inputBufferSize_, imageBytes, inputBufferSize_, kind);
// 执行推理
aclmdlExecute(modelId_, inputDataset_, outputDataset_);
}
void SampleResnetQuickStart::GetResult()
{
// get result from output data set
void * outHostData = nullptr;
float * outData = nullptr;
size_t outputIndex = 0;
aclDataBuffer * dataBuffer = aclmdlGetDatasetBuffer(outputDataset_, outputIndex);
void * data = aclGetDataBufferAddr(dataBuffer);
uint32_t len = aclGetDataBufferSizeV2(dataBuffer);
// copy device output data to host
aclrtMemcpyKind kind;
if (runMode_ == ACL_DEVICE)
{
kind = ACL_MEMCPY_DEVICE_TO_HOST;
}
else
{
kind = ACL_MEMCPY_HOST_TO_DEVICE;
}
aclrtMallocHost(&outHostData, len);
aclrtMemcpy(outHostData, len, data, len, kind);
outData = reinterpret_cast<float*>(outHostData);
// create map<confidence, class> and sorted by maximum
map<float, unsigned int, greater<float>> resultMap;
for (unsigned int j = 0; j < len / sizeof(float); ++j)
{
resultMap[*outData] = j;
outData++;
}
// do data processing with softmax and print top 1 classes
double totalValue=0.0;
for (auto it = resultMap.begin(); it != resultMap.end(); ++it)
{
totalValue += exp(it->first);
}
// get max <confidence, class>
float confidence = resultMap.begin()->first;
unsigned int index = resultMap.begin()->second;
string line = format("label:%d conf:%lf class:%s", index, exp(confidence) / totalValue, label[index].c_str());
// write image to ../out/
cv::putText(srcImage, line, Point(0,35), cv::FONT_HERSHEY_TRIPLEX, 1.0, Scalar(255,0,0),2);
int sepIndex = imagePath.find_last_of("/");
string fileName = imagePath.substr(sepIndex + 1, -1);
string outputName = "out_" + fileName;
imwrite(outputName, srcImage);
cout << outputName << endl;
cout << line << endl;
aclrtFreeHost(outHostData);
outHostData = nullptr;
outData = nullptr;
}
int main()
{
const char * modelPath = "../model/resnet50.om";
int32_t modelWidth = 224;
int32_t modelHeight = 224;
vector<string> allPath;
allPath.push_back("../data/dog1_1024_683.jpg");
SampleResnetQuickStart sampleResnet(modelPath, modelWidth, modelHeight);
sampleResnet.InitResource();
for (size_t i = 0; i < allPath.size(); i++)
{
sampleResnet.ProcessInput(allPath.at(i));
sampleResnet.Inference();
sampleResnet.GetResult();
}
return 0;
}
1.2 测试运行
如果报错,解决方法可以看前面的教程。下图是推理成功了,类别是 label:162,名字是class:beagle,概率是conf:0.902209
数据集所有类别:
ImageNet图像库1000个类别名称(中文注释不断更新)_imagene162t类别-CSDN博客
二、程序讲解
2.1 初始化
204行,SampleResnetQuickStart sampleResnet(modelPath, modelWidth, modelHeight);
这是在程序中自定义的一个类,这个类是用来进行流程控制的。初始化类的时候,将模型和模型要求输入的图片宽高输入进去了
这个模型的宽高是根据模型来确定的,resnet50要求输入224*224*3的矩阵,代表图片宽是224,高是224,有RGB三个颜色通道
205行,sampleResnet.InitResource();
在模型参数传进去后,SampleResnetQuickStart类会根据传入的模型文件、模型宽高来对模型推理需要的参数进行初始化
44行,aclInit("");
● 只有执行了这一行,才能使用后续的acl库函数,一个进程只执行一次
● 在进程的最后,要成对的使用 aclFinalize(); 函数,做收尾工作
45行,clrtSetDevice(0);
● 一个主板上有多个昇腾芯片(主板上插了多块显卡),选择索引号为0的昇腾芯片(选择第一块显卡)
● 在线程的最后,要成对的使用aclrtResetDevice(0);函数,做收尾工作
46行,aclrtCreateContext(&context_, 0);
● 香橙派上是昇腾310B芯片,昇腾310B芯片device中默认存在1个context,本程序中可创建也可不创建context
● 猜测context可能不是一个实体的内容,而是stream集合的一个概念,几个stream组成一个context
● 本程序就是在昇腾设备0上创建了一个context。创建好了以后,本线程就默认使用新创建的context。当前线程在同一时刻内只能使用其中一个Context
● 在线程的最后,要成对的使用aclrtDestroyContext(g_context);函数,做收尾工作
47行:aclrtCreateStream(&stream_);
● 用于给当前context创建一个stream。上图昇腾310B芯片硬件资源最多支持1024个stream
● 一个stream可以用于执行一个任务,本案例中使用这个stream执行了图片裁剪指定区域的任务。多个stream可以用于同时执行多个任务
● 在线程的最后,要成对的使用aclrtDestroyStream(g_stream);函数,做收尾工作
48行:aclrtGetRunMode(&runMode_);
● 获取当前程序的运行模式
ACL_DEVICE:昇腾AI软件栈运行在Device的Control CPU或板端环境上
ACL_HOST:昇腾AI软件栈运行在Host CPU上
如果是ACL_DEVICE,就代表当前程序是在AI CPU上运行的;如果是ACL_HOST,就代表当前程序是在CPU上运行的,如下图红色圆圈中写的那样
50行,aclmdlLoadFromFile(modelPath_, &modelId_);
把硬盘里面的模型,加载到内存条上。因为可能加载了多个不同的模型到内存条上了,所以要通过modelId_(句柄)去找到modelPath("../model/resnet50.om")模型在内存条上加载的位置。
51-52行,modelDesc_ = aclmdlCreateDesc(); aclmdlGetDesc(modelDesc_, modelId_);
创建一个模型的描述符,类似于创建了一个结构体,如下图:
struct ModelDesc {
vector<int> allBufferSize;
...
};
strcut ModelDesc * modelDesc_ = new ModelDesc;
然后aclmdlGetDesc(modelDesc_, modelId_);就是根据模型的句柄,在内存上找到模型,将模型的各种信息赋值给上面的结构体
55行,inputDataset_ = aclmdlCreateDataset();
创建一个输入数据描述符,类似于创建一个结构体,如下图:
struct Dataset {
uint32_t bufLen;
char * bufData;
...
};
struct Dataset * inputDataset_ = new Dataset;
56行,inputBufferSize_ = aclmdlGetInputSizeByIndex(modelDesc_, 0);
这段代码相当于获取结构体ModelDesc的vector<int> allBufferSize;中第一个值。
allBufferSize中存储的是模型从第1个到第N个输入需要的空间大小(单位是字节),resnet50这个模型只有1个输入,所以现在只能取到索引号为0的输入
58行,aclrtMalloc(&inputBuffer_, inputBufferSize_, ACL_MEM_MALLOC_HUGE_FIRST);
根据模型输入需要的大小,在显存上分配空间,也就是下图昇腾310B芯片中的内存上分配空间
59-60行,aclDataBuffer * inputData = aclCreateDataBuffer(inputBuffer_, inputBufferSize_); aclmdlAddDatasetBuffer(inputDataset_, inputData);
相当于给结构体struct Dataset中的数据赋值。inputDataset_->bufLen=inputBufferSize_; inputDataset_->bufData=inputBuffer_;
输出结构体的代码逻辑和输入是一样的,这里不多赘述了,至本段代码结束,我们得到了下面三个关键的变量,其中inputDataset_->bufData和inputBuffer_同时指向大小为602112字节的内存空间
outputBuffer_->bufData指向大小为4000字节的内存空间
2.2 处理模型图片输入
下图209行,就是处理模型输入图片的,使用opencv方法将图片resize成224*224*3格式的矩阵,然后将图片的数据给到602112字节的输入数组中(在上图中)
下图210行,对图片进行推理,得到1000*4的输出矩阵,输出矩阵的数据存放到4000字节的输出数组中(在上图)
下图211行,对输出矩阵中的内容进行处理
下面详细的看一下ProcessInput函数
76行,srcImage = imread(testImgPath);
使用opencv,读取图片到内存中
80行,resize(srcImage, resizedImage, Size(modelWidth_, modelHeight_));
将图片大的宽高修改为224*224
83-85行,获取图片的通道数为3,RGB。图片的宽高为224*224
88-113行
● 对图片进行处理,将图片从HWC转成CHW。
HWC格式是 [224, 224, 3] 类型的矩阵,相当于有一个二维数组arr[224][224],数组的元素是那个位置的RGB,符合我们的直观感觉
CHW格式是 [3, 224, 224] 类型的矩阵,相当于有一个一维数组arr[3],这个数组的元素是一个二维数组,arr[0]、arr[1]、arr[2]中的存储的分别是RGB三个通道在224*224图片每个位置的颜色值
● 将图片从BGR转换成RGB。如果是HWC,则arr[3][2](随机的一个位置)的变化如下图
如果是CHW,则变化则如下,下图从上面的BGR变成RGB。下图中,每个方格里面存储的是1字节数据,区别于上图一个方格里面存储的是RGB 3个字节数据
● ProcessInput(const string testImgPath)这个函数会将得到的数据存储到char * imageBytes[602112]; 字节数组中
2.3 推理函数
129行,aclrtMemcpy(inputBuffer_, inputBufferSize_, imageBytes, inputBufferSize_, kind);
如下图,前面处理模型图片输入函数已经将读取到 并且处理好的图片放到imageBytes数组中了,129行代码的作用就是将imageBytes的数据拷贝到inputBuffer_数组中去
● 132行,aclmdlExecute(modelId_, inputDataset_, outputDataset_);
函数根据模型的id,获取到模型信息。根据输入图片获取到输入图片的数据,然后进行推理。将推理得到的数据存放到outputDataset_,等下就可以通过outputDataset_来获取输出的推理结果
2.4 对输出结果进行处理
141-143行,通过推理得到的outputDataset_获取其中的数据。得到的数据存储在void * data;数组中,将这个数组强转成float * outData;数组。得到的长度len应该是4000字节
161-166行,float的长度是4,所以最终float数组的长度是1000,利用map对float数组进行排序,得到float数组中最大的数字。
这个数字就是程序最终的预测概率,又通过这个最大的数字在数组中的位置,找到这个类别。例如本程序 outData[162] 是最大的,并且通过outData[162] 计算出预测准确率是90.2209%