目录
一、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
● 在线程的最后,要成对的使用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);
这里为什么减1呢,我猜测可能是因为DVPP主要应用于人工智能,少一行信息对人工智能来说是没影响的
3.3和3.4为什么要使用AlignmentHelper对输入和输出数据进行16位和2位对齐呢,这要看第四节讲的内容
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处理图片->把输出的图片保存->释放占用的资源
四、DVPP输入输出图片限制
4.1 裁剪的输入图片要求
官方文档:
输入图片有4个信息,图片宽度按照2字节对齐,高度按照2字节对齐。还有两个信息是按照宽16字节对齐,高2字节对齐输入进去的
4.2 裁剪的输出图片要求
直观的看一下,如果我们指定输出的图片宽高不是按照2字节对齐的,输出出来的图片对应的就是绿色
输出描述符中的图片宽高要求2字节对齐:
输出图片另外两个信息分别是,宽16字节对齐,高2字节对齐
4.3 输出图片的格式和宽高
经过上面一通运算,输出出来的yuv图片宽高是按照原图片,宽=原图片按16字节对齐,高=原图片按2字节对齐
最后输出的图片右边有黑边
4.4 对输出的图片进行处理
我不太会使用AIPP的工具,所以使用python对输出的图片进行处理
读取yuv格式图片,对它进行裁剪,再输出jpg格式图片。
所以,我们现在学习这个DVPP的目的是干嘛呢?opencv不是一样可以处理,当然是有原因的,DVPP是使用硬件加速,大量的运算都交给昇腾完成,我们的CPU只需要执行好自己的复杂任务。
import numpy as np
import cv2
import sys
def AlignmentHelper(origSize, alignment):
alignmentH = alignment - 1
return (origSize + alignmentH) // alignment * alignment
width = int(sys.argv[1])
height = int(sys.argv[2])
yuv_file = "./output/dvpp_rabbit_" + sys.argv[1] + "_" + sys.argv[2] + "_nv12.yuv"
alignment_width = AlignmentHelper(width, 16)
alignment_height = AlignmentHelper(height, 2)
yuv_file = open(yuv_file, "rb")
frame_size = alignment_width * alignment_height * 3 // 2
data = yuv_file.read(frame_size)
yuv_image = np.frombuffer(data, dtype=np.uint8)
yuv_image = yuv_image.reshape((alignment_height*3//2, alignment_width))
bgr_image = cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR_I420)
patch_tree = bgr_image[0:width, 0:height]
cv2.imwrite("./output/dvpp_rabbit_" + sys.argv[1] + "_" + sys.argv[2] + "_nv12.jpg", patch_tree)