目录
一、VScode配置
1.1 下载和安装
1.2 安装和配置需要的插件
二、构建项目
2.1 项目架构
2.2 解决代码高亮显示
2.3 测试编译
2.4 总结出最简单的代码
2.5 vscode报错找不到头文件解决方法
三、代码简单讲解
3.1 初始化部分
3.2 拷贝数据到NPU显存中
3.3 准备裁剪区域
3.4 准备输入输出描述符
3.5 收尾阶段
3.6 其他历程如何学习
一、VScode配置
1.1 下载和安装
1. 下载地址
Download Visual Studio Code - Mac, Linux, Windows
2. 安装中要注意的内容
1.2 安装和配置需要的插件
1. chinese中文
如果在这里安装不了的话,可以使用离线安装的方式,首先进入官网下载离线插件包:Extensions for Visual Studio family of products | Visual Studio Marketplace
然后在vscode中这么操作,选择刚刚下载的VSIX文件即可完成安装
2. 安装远程工具Remote
设置要连接的主机,然后两次回车
重启vscode
选择linux然后再输入密码
进入文件夹
二、构建项目
2.1 项目架构
data数据是从上面的crop中拿过来的
cmake_minimum_required(VERSION 3.22)
# 设置变量PROJECT_NAME的值为crop
set(PROJECT_NAME "crop")
# 设置项目名称为crop
project(${PROJECT_NAME})
# 添加dvpp需要的宏定义,相当于在代码中: #define ENABLE_DVPP_INTERFACE
add_definitions(-DENABLE_DVPP_INTERFACE)
# 设置头文件目录
include_directories(
$ENV{INSTALL_DIR}/runtime/include/
./include
)
# 设置动态库目录
link_directories(
$ENV{INSTALL_DIR}/runtime/lib64/stub
)
# 查找所有CMakeLists.txt所在目录下,src目录中的.c和.cpp文件
file(GLOB_RECURSE CPP_FILES
${PROJECT_SOURCE_DIR}/src/*.c
${PROJECT_SOURCE_DIR}/src/*.cpp
)
# 编译输出的文件名为: crop 依赖src目录下的所有.c和.cpp结尾的文件
add_executable(${PROJECT_NAME}
${CPP_FILES}
)
target_link_libraries(${PROJECT_NAME}
ascendcl
acl_dvpp
stdc++
)
2.2 解决代码高亮显示
CMake或者cpp没有高亮显示就上传vsix文件到香橙派
然后在vscode中安装
2.3 测试编译
提示要增加一行内容,这行内容指定了CMake需要的最小版本
然后./crop就能运行程程序了
2.4 总结出最简单的代码
#include <iostream>
#include <memory>
#include <dirent.h>
#include <fstream>
#include "acl/acl.h"
#include "acl/ops/acl_dvpp.h"
using namespace std;
typedef struct PicDesc {
string picName;
int left;
int top;
int width;
int height;
} PicDesc;
aclrtContext g_context;
aclrtStream g_stream;
aclrtRunMode g_runMode;
acldvppChannelDesc * g_dvppChannelDesc;
char * ReadBinFile(string fileName, uint32_t &fileSize)
{
ifstream binFile(fileName, ifstream::binary);
if (binFile.is_open() == false)
{
printf("Open file %s failed.\n", fileName.c_str());
return nullptr;
}
binFile.seekg(0, binFile.end);
uint32_t binFileBufferLen = binFile.tellg();
if (binFileBufferLen == 0)
{
printf("Binfile is empty, filename is %s.\n", fileName.c_str());
binFile.close();
return nullptr;
}
binFile.seekg(0, binFile.beg);
char * binFileBufferData = new(nothrow) char[binFileBufferLen];
if (binFileBufferData == nullptr) {
printf("Malloc binFileBufferData failed.\n");
binFile.close();
return nullptr;
}
binFile.read(binFileBufferData, binFileBufferLen);
binFile.close();
fileSize = binFileBufferLen;
return binFileBufferData;
}
uint32_t AlignmentHelper(uint32_t origSize, uint32_t alignment)
{
if (alignment == 0) {
return 0;
}
uint32_t alignmentH = alignment - 1;
return (origSize + alignmentH) / alignment * alignment;
}
uint32_t SaveDvppOutputData(const char *fileName, const void *devPtr, uint32_t dataSize)
{
FILE * outFileFp = fopen(fileName, "wb+");
if (g_runMode == ACL_HOST) {
void * hostPtr = nullptr;
aclrtMallocHost(&hostPtr, dataSize);
aclrtMemcpy(hostPtr, dataSize, devPtr, dataSize, ACL_MEMCPY_DEVICE_TO_HOST);
fwrite(hostPtr, sizeof(char), dataSize, outFileFp);
(void)aclrtFreeHost(hostPtr);
} else {
fwrite(devPtr, sizeof(char), dataSize, outFileFp);
}
fflush(outFileFp);
fclose(outFileFp);
return 0;
}
int main()
{
/*
* 初始化device、context、stream和dvppChannelDesc,并获取运行模式
*/
aclInit("./src/acl.json");
aclrtSetDevice(0);
aclrtCreateContext(&g_context, 0);
aclrtCreateStream(&g_stream);
g_dvppChannelDesc = acldvppCreateChannelDesc(); // 创建图像数据处理通道时的通道描述信息 g_dvppChannelDesc is acldvppChannelDesc type
acldvppCreateChannel(g_dvppChannelDesc); // 创建图像数据处理通道
aclrtGetRunMode(&g_runMode);
/*
* 读取输入和输出
*/
// 下面设置输入图片的信息,图片的left和top参数用不到
PicDesc inPicDesc = { "./data/wood_rabbit_1024_1068_nv12.yuv", 0, 0, 1024, 1068 };
// 下面设置输出图片的信息
PicDesc outPicDesc = { "./output/dvpp_rabbit_224_224_nv12.yuv", 350, 280, 224, 224 };
/*
* 根据输入读取文件到NPU显存中
*/
uint32_t buffSize = 0;
// inPicDesc.picName:要读取的文件名 buffSize:得到的文件大小 inputBuff:得到的文件的内容
char * inputBuff = ReadBinFile(inPicDesc.picName, buffSize);
void * inputBufferDev = nullptr;
acldvppMalloc(&inputBufferDev, buffSize); // 在NPU上分配显存
if (g_runMode == ACL_HOST) // 如果本段C++代码是运行在CPU上
{ // 把数据从内存条拷贝到NPU的显存中
aclrtMemcpy(inputBufferDev, buffSize, inputBuff, buffSize, ACL_MEMCPY_HOST_TO_DEVICE);
}
else // 如果本段C++代码是运行在NPU中的CPU上
{ // 把数据从NPU显存拷贝到NPU显存中
aclrtMemcpy(inputBufferDev, buffSize, inputBuff, buffSize, ACL_MEMCPY_DEVICE_TO_DEVICE);
}
delete[] inputBuff;
/*
* 设置要裁剪输入图片的区域
*/
uint32_t outputWidth = AlignmentHelper(outPicDesc.width, 16); // 输入图片宽度按照16字节对其,例如输入17,得到的outputWidth就是32
uint32_t outputHeight = AlignmentHelper(outPicDesc.height, 2); // 输入图片高度按照2字节对其,例如输入13,得到的outputHeight就是2
uint32_t cropLeftOffset = outPicDesc.left; // must 偶数
uint32_t cropRightOffset = cropLeftOffset + outputWidth - 1; // must 奇数
uint32_t cropTopOffset = outPicDesc.top; // must 偶数
uint32_t cropBottomOffset = cropTopOffset + outputHeight - 1; // must 奇数
acldvppRoiConfig * cropArea_ = acldvppCreateRoiConfig(cropLeftOffset, cropRightOffset, cropTopOffset, cropBottomOffset);
/*
* 创建输入图片描述符,并填写输入图片信息
*/
uint32_t inputWidthStride = AlignmentHelper(inPicDesc.width, 16);
uint32_t inputHeightStride = AlignmentHelper(inPicDesc.height, 2);
uint32_t inputBufferSize = inputWidthStride * inputHeightStride * 3 / 2;
acldvppPicDesc * vpcInputDesc_ = acldvppCreatePicDesc(); // 创建入参数描述符
acldvppSetPicDescData(vpcInputDesc_, inputBufferDev); // 原图片存放位置的指针
acldvppSetPicDescFormat(vpcInputDesc_, PIXEL_FORMAT_YUV_SEMIPLANAR_420); // 原图片的格式
acldvppSetPicDescWidth(vpcInputDesc_, inPicDesc.width); // 原图片宽度
acldvppSetPicDescHeight(vpcInputDesc_, inPicDesc.height); // 原图片高度
acldvppSetPicDescWidthStride(vpcInputDesc_, inputWidthStride); // 原图片对齐后的宽度
acldvppSetPicDescHeightStride(vpcInputDesc_, inputHeightStride); // 原图片对齐后的高度
acldvppSetPicDescSize(vpcInputDesc_, inputBufferSize); // 输入图片的图片大小
/*
* 创建输出图片描述符,并填写输出图片信息
*/
void * outputBufferDev = nullptr;
uint32_t outputBufferSize = outputWidth * outputHeight * 3 / 2;
acldvppMalloc(&outputBufferDev, outputBufferSize); // 在NPU上给输出图片分配内存
acldvppPicDesc * vpcOutputDesc_ = acldvppCreatePicDesc();
acldvppSetPicDescData(vpcOutputDesc_, outputBufferDev); // 输出图片存放位置指针
acldvppSetPicDescFormat(vpcOutputDesc_, PIXEL_FORMAT_YUV_SEMIPLANAR_420); // 输出图片的格式
cout << outPicDesc.width << " " << outPicDesc.height << " " << outputWidth << " " << outputHeight << endl;
acldvppSetPicDescWidth(vpcOutputDesc_, outPicDesc.width); // 输出图片的宽
acldvppSetPicDescHeight(vpcOutputDesc_, outPicDesc.height); // 输出图片的高
acldvppSetPicDescWidthStride(vpcOutputDesc_, outputWidth); // 输出图片对齐后的宽
acldvppSetPicDescHeightStride(vpcOutputDesc_, outputHeight); // 输出图片对齐后的高
acldvppSetPicDescSize(vpcOutputDesc_, outputBufferSize);
/*
* 图片的处理和保存
*/
// 由g_stream这个流水线来运行处理程序
acldvppVpcCropAsync(g_dvppChannelDesc, vpcInputDesc_, vpcOutputDesc_, cropArea_, g_stream);
// 等待g_stream流水线完成
aclrtSynchronizeStream(g_stream);
// 保存图片
SaveDvppOutputData(outPicDesc.picName.c_str(), outputBufferDev, outputBufferSize);
/*
* 释放占用的资源
*/
acldvppFree(outputBufferDev);
acldvppDestroyRoiConfig(cropArea_);
acldvppDestroyPicDesc(vpcInputDesc_);
acldvppDestroyPicDesc(vpcOutputDesc_);
aclrtDestroyStream(g_stream);
aclrtDestroyContext(g_context);
aclrtResetDevice(0);
aclFinalize();
return 0;
}
下图代表裁剪成功了
注意:在使用上面代码的时候,我修改了其他的尺寸,输出的图片就是不正确的,所以我现在也没完全搞懂库函数要求传入的尺寸。但是后续我会持续更新,分享我的学习心得
2.5 vscode报错找不到头文件解决方法
按 F1 或 Ctrl+Shift+p 在弹出的备选选项中选择 C/C++:Edit Configurations (JSON),自动打开c_cpp_properties.json配置文件
在.vscode中添加头文件搜索路径的json,然后配置路径
三、代码简单讲解
下面的内容全是我个人的理解,如果有佬,感谢在评论区指正,我会及时修改文章
3.1 初始化部分
88行:aclInit("./src/acl.json");
● 一个进程内只能调用一次aclInit接口。使用AscendCL接口开发应用时,必须先调用aclInit接口,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
● 输入参数是一个json格式的文件。json里面可以写啥,这个可以看官方文档:
aclInit-系统配置-AscendCL API(C&C++)-应用开发接口-CANN社区版8.0.RC2.alpha001开发文档-昇腾社区 (hiascend.com)
目前我是在json里面就写了一对花括号:{}
● 在进程的最后,要成对的使用 aclFinalize(); 函数,做收尾工作
89行:aclrtSetDevice(0);
● 我的理解就是类似于下图(下图是我的猜想,并不来自于官方),一个板子上面有3块昇腾310B的芯片。上面的代码就是设置本线程(一个进程有一个主线程)使用索引为0的昇腾芯片
● 查看官方的device操作发现,aclrtGetDeviceCount函数可以获取Device的数量,我觉得如果有3个昇腾芯片,可能返回的是3。目前香橙派上面只有一个昇腾310B芯片,返回的就是1
hiascend.com/doc_center/source/zh/canncommercial/601/inferapplicationdev/aclcppdevg/aclcppdevg_03_0019.html
● 在线程的最后,要成对的使用aclrtResetDevice(0);函数,做收尾工作
90行:aclrtCreateContext(&g_context, 0);
● 香橙派上是昇腾310B芯片,昇腾310B芯片device中默认存在1个context,本程序中可创建也可不创建context
● 猜测context可能不是一个实体的内容,而是stream集合的一个概念,几个stream组成一个context
● 本程序就是在昇腾设备0上创建了一个context。创建好了以后,本线程就默认使用新创建的context。当前线程在同一时刻内只能使用其中一个Context
● 在线程的最后,要成对的使用aclrtDestroyContext(g_context);函数,做收尾工作
91行:aclrtCreateStream(&g_stream);
● 用于给当前context创建一个stream。上图昇腾310B芯片硬件资源最多支持1024个stream
● 一个stream可以用于执行一个任务,本案例中使用这个stream执行了图片裁剪指定区域的任务。多个stream可以用于同时执行多个任务
● 在线程的最后,要成对的使用aclrtDestroyStream(g_stream);函数,做收尾工作
92、93行:g_dvppChannelDesc = acldvppCreateChannelDesc();
acldvppCreateChannel(g_dvppChannelDesc);
● 这个是图片处理函数必须要的一个参数,在这里提前准备一下
acldvppVpcCropAsync(g_dvppChannelDesc, vpcInputDesc_, vpcOutputDesc_, cropArea_, g_stream);
94行:aclrtGetRunMode(&g_runMode);
● 获取当前程序的运行模式
ACL_DEVICE:昇腾AI软件栈运行在Device的Control CPU或板端环境上
ACL_HOST:昇腾AI软件栈运行在Host CPU上
如果是ACL_DEVICE,就代表当前程序是在AI CPU上运行的;如果是ACL_HOST,就代表当前程序是在CPU上运行的,如下图红色圆圈中写的那样
3.2 拷贝数据到NPU显存中
● 111行,通过ReadBinFile函数读取数据到内存中。如果程序是运行在CPU上,那么读取到的数据就放在运行内存上;如果程序是运行在昇腾芯片的AI CPU上,那么读取到的数据就放在昇腾芯片的内存,也就是显存上
● 114行,在显存上分配一段文件大小的内存。
● 115行,做判断。如果程序是运行在CPU上,就要把数据从运行内存上搬运到显存上;如果程序是运行在昇腾芯片的AI CPU上,那么就把数据从显存搬运到显存上(这里也可以不搬运,主要是为了后面写库的时候使用)
在运行内存上对应的就是HOST,到显存上对应的就是TO_DEVICE;在显存上对应的就是DEVICE,到显存上对应的就是TO_DEVICE。同理也可以HOST_TO_HOST,DEVICE_TO_HOST。
● 补充一下:如果程序运行在昇腾AI CPU上,想要在运行内存中分配内存,就需要使用aclrtMallocHost来申请内存
3.3 准备裁剪区域
上面分别生成了cropArea_,用于告诉下面的函数,输入图片要裁剪的范围
acldvppVpcCropAsync(g_dvppChannelDesc, vpcInputDesc_, vpcOutputDesc_, cropArea_, g_stream);
因为我对芯片对齐等不是很了解,也不清楚这里到底怎么实现随便裁剪,这里我就不分析了,等后面学习完了再来补充,有知道怎么使用的佬也可以在评论区帮助我一下
3.4 准备输入输出描述符
● 143行,为什么输入的图片大小是这么计算的呢
首先是对齐后图片的宽高如下图,前面这部分很好理解:
● 后面的*3,是因为存储图片的是YUV 3个通道,这样就需要 对齐后的宽高*3得到需要的字节数,
● 那么为什么要除2呢?这是因为YUV本来YUV三个通道都应该有4个信息,也就是8bit来表示Y,8bit来表示U,8bit来表示V。YUV420图片,有8bit来表示Y,只有4bit来表述U,0bit来表示V,字节数自然就少了一半,所以要/2
3.5 收尾阶段
在图片处理的过程中,程序可以异步的去执行了,但是176行,我们任然阻塞的去等待图片处理结果。等到图片处理结束,就将输出的图片指针和大小传入181行的函数中,函数会将图片保存到picName指定的位置去
做完上面这些任务,就释放掉占用的资源
3.6 其他历程如何学习
把其他历程中的关键内容抽取出来,写成像上面那种形式的程序,多动手练习。自己依次分析函数的功能等。
上面代码过程分为这几步:初始化->读取要处理的图片->创建输入图片描述符->创建输出图片描述符->acl处理图片->把输出的图片保存->释放占用的资源