入门昇腾AI媒体预处理技术
CANN媒体数据处理
1.为什么我们要学习CANN的媒体数据处理
当然是速度快呀
媒体数据处理在不同的应用里面有不同的含义,今天我们说的媒体数据处理主要是指开发AI应用,为模型制造合适的数据,和模型推理完后后处理的一系列操作,一般包括图片的解码,编码,缩放,裁剪,视频的解码编码等等操作。
但是对深度学习了解一点的朋友可能都知道我们一般会用opencv等加速数据的处理,既然有解决办法了,再学习一种新的,有点多余吧?
NO!NO!NO!
我们都了解GPU可以加速神经网络模型的训练和推理,但是CPU也行啊,但是你了解他们之间的速度差异吗?二者效率相差可能达到几十倍甚至是上百倍,并不是CPU太逊了,也不是GPU太强了,这两种都是计算机的硬件都有着不同的特点。
CPU的可以形象的理解为有25%的ALU(运算单元)、有25%的Control(控制单元)、50%的Cache(缓存单元)
GPU可以形象的理解为90%的ALU(运算单元),5%的Control(控制单元)、5%的Cache(缓存单元)
二者天然就有非常大的差异,然而各种数据处理运算都要有ALU完成,但是CPU为了完成一系列复杂的控制功能,运算单元的数量相比于GPU其实很少,所以面对高密度的计算任务时GPU就有天然的优势。这就是硬件优势。
然而我们的主角昇腾AI处理器是NPU,是一种相比于GPU更加适合AI运算的硬件,通过NPU的加速又可以实现数量级的效率提升。因此数据处理的操作在NPU上进行专门的加速,就可以达到数倍提升效率的成果。当需要处理的数据比较多的时候,优势就会进一步体现。
2.在专门的芯片上进行媒体数据处理这么厉害,我要怎么学习呢?
别急一步一来
第一式,摸清敌情
媒体数据处理是什么
媒体数据都可以应用在哪些方面
媒体数据处理在接口调用流程中的位置
第二式,逐个击破
在计算机视觉领域处理最多的应该也就是图片和视频了,
所以,我们先学习JPEG图片的解码和编码,
a.江湖规矩先看视频,
温馨提示视频中讲述的是各种数据处理的集合哦,可以按需跳跃观看
媒体数据处理
b.光说不练假把式,来个实验开开眼
c.没记错的话,我们貌似需要写代码吧,那代码何在??
诶嘿嘿,代码来喽!!
但是,写代码之前我们最好要知道我们整个程序的流程,那么它来喽
首先就是创建数据处理的通道,你可以把它理解为一个专为处理媒体数据的生产线,在这个生产线上会对媒体数据(图片,视频)进行加工
//1.创建和销毁图片数据处理的通道。
dvppChannelDesc_ = acldvppCreateChannelDesc();
aclError ret = acldvppCreateChannel(dvppChannelDesc_);
acldvppDestroyChannel(dvppChannelDesc_);
然后要为生产线准备原材料,也就是图片或者视频的源数据(把数据加载到显存里面)
//调用aclrtGetRunMode接口获取软件栈的运行模式,如果调用aclrtGetRunMode接口获取软件栈的运行模式为ACL_HOST,则需要通过aclrtMemcpy接口将输入图片数据传输到Device,数据传输完成后,需及时释放内存;否则直接申请并使用Device的内存
aclrtRunMode runMode;
ret = aclrtGetRunMode(&runMode);
if(runMode == ACL_HOST){
//申请Host内存inputHostBuff,并将输入图片读入该地址,inDevBufferSize为读入图片大小
void* inputHostBuff = nullptr;
inputHostBuff = malloc(inDevBufferSize);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inputHostBuff, inDevBufferSize);
//申请Device内存inDevBuffer_
aclRet = acldvppMalloc(&inDevBuffer_, inDevBufferSize);
//通过aclrtMemcpy接口将输入图片数据传输到Device
aclRet = aclrtMemcpy(inDevBuffer_, inDevBufferSize, inputHostBuff, inDevBufferSize, ACL_MEMCPY_HOST_TO_DEVICE);
} else {
//申请Device输入内存inDevBuffer_
ret = acldvppMalloc(&inDevBuffer_, inBufferSize);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inDevBuffer_, inBufferSize);
}
然后要为生产过程设置参数,设置图片的描述信息,这个你可以类比于我们需要告诉生产线,这批原材料的相关信息,家庭住址,高矮胖瘦,什么成分
//创建解码输出图片的描述信息,设置各属性值
//decodeOutputDesc是acldvppPicDesc类型
decodeOutputDesc_ = acldvppCreatePicDesc();
//数据描述,这个为了告诉生产线要去哪里找数据,其实就是类似于数据的首地址
acldvppSetPicDescData(decodeOutputDesc_, decodeOutDevBuffer_);
//告诉生产线这张图片占用多少内存,以防一会取数据取多了
acldvppSetPicDescSize(decodeOutputDesc_, decodeOutBufferSize);
//告诉生产线图片是什么格式的
acldvppSetPicDescFormat(decodeOutputDesc_, PIXEL_FORMAT_YUV_SEMIPLANAR_420);
//告诉生产线图片宽多少
acldvppSetPicDescWidth(decodeOutputDesc_, inputWidth_);
//告诉生产线图片高多少
acldvppSetPicDescHeight(decodeOutputDesc_, inputHeight_);
//告诉生产线数据对齐后的宽是多少
acldvppSetPicDescWidthStride(decodeOutputDesc_, decodeOutWidthStride);
告诉生产线对齐后的高是多少
acldvppSetPicDescHeightStride(decodeOutputDesc_, decodeOutHeightStride);
然后准备工作完成,开始加工了
//执行异步解码,再调用aclrtSynchronizeStream接口阻塞程序运行,直到指定Stream中的所有任务都完成
ret = acldvppJpegDecodeAsync(dvppChannelDesc_, inDevBuffer_, inDevBufferSize, decodeOutputDesc_, stream_);
ret = aclrtSynchronizeStream(stream_);
加工完了,该打包装箱了,贴封条了(其实就是把数据从显存拷贝回内存里面,为后面保存做准备)
if(runMode == ACL_HOST) {
//该模式下,由于处理结果在Device侧,因此需要调用内存复制接口传输结果数据后,再释放Device侧内存
//申请Host内存vpcOutHostBuffer
void* vpcOutHostBuffer = nullptr;
vpcOutHostBuffer = malloc(decodeOutBufferSize);
//通过aclrtMemcpy接口将Device的处理结果数据传输到Host
aclRet = aclrtMemcpy(vpcOutHostBuffer, decodeOutBufferSize, decodeOutDevBuffer_, decodeOutBufferSize, ACL_MEMCPY_DEVICE_TO_HOST);
//释放掉输入输出的device内存
(void)acldvppFree(inDevBuffer_);
(void)acldvppFree(decodeOutDevBuffer_);
//数据使用完成后,释放内存
free(vpcOutHostBuffer);
} else {
//此时运行在device侧,处理结果也在Device侧,可以根据需要操作处理结果后,释放Device侧内存
(void)acldvppFree(inDevBuffer_);
(void)acldvppFree(decodeOutDevBuffer_);
}
解码结束后,释放资源,包括解码输出图片的描述信息、解码输出内存、通道描述信息、通道等(干完活你得把场地收拾好)
acldvppDestroyPicDesc(decodeOutputDesc_);
acldvppDestroyChannel(dvppChannelDesc_);
(void)acldvppDestroyChannelDesc(dvppChannelDesc_);
搞定了
解码完成了,我们想对图片进行一个缩放,该咋办呢??
同样先来一个流程图
其实还是那个套路
先准备生产线
// 创建图片数据处理通道时的通道描述信息,dvppChannelDesc_是acldvppChannelDesc类型
dvppChannelDesc_ = acldvppCreateChannelDesc();
创建图片缩放配置数据、指定缩放算法,图片缩放后会成什么鸟样子和采用什么算法有很大差异,中国厨子和外国厨子做出的菜能一个味吗??
//resizeConfig_是acldvppResizeConfig类型
acldvppResizeConfig *resizeConfig_ = acldvppCreateResizeConfig();
aclError ret = acldvppSetResizeConfigInterpolation(resizeConfig_, 0);
准备原材料(将数据读到内存,再拷贝到显存)
//申请输入内存(区分运行状态)
//调用aclrtGetRunMode接口获取软件栈的运行模式,如果调用aclrtGetRunMode接口获取软件栈的运行模式为ACL_HOST,则需要通过aclrtMemcpy接口将输入图片数据传输到Device,数据传输完成后,需及时释放内存;否则直接申请并使用Device的内存
aclrtRunMode runMode;
ret = aclrtGetRunMode(&runMode);
//inputPicWidth、inputPicHeight分别表示图片的对齐后宽、对齐后高,此处以YUV420SP格式的图片为例
uint32_t resizeInBufferSize = inputPicWidth * inputPicHeight * 3 / 2;
if(runMode == ACL_HOST) {
//申请Host内存vpcInHostBuffer
void* vpcInHostBuffer = nullptr;
vpcInHostBuffer = malloc(resizeInBufferSize);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, vpcInHostBuffer, resizeInBufferSize);
//申请Device内存resizeInDevBuffer_
aclRet = acldvppMalloc(&resizeInDevBuffer_, resizeInBufferSize);
//通过aclrtMemcpy接口将输入图片数据传输到Device
aclRet = aclrtMemcpy(resizeInDevBuffer_, resizeInBufferSize, vpcInHostBuffer, resizeInBufferSize, ACL_MEMCPY_HOST_TO_DEVICE);
//数据传输完成后,及时释放内存
free(vpcInHostBuffer);
} else {
//申请Device输入内存resizeInDevBuffer_
ret = acldvppMalloc(&resizeInDevBuffer_, resizeInBufferSize);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, resizeInDevBuffer_, resizeInBufferSize);
}
这里有个点要注意,图片缩放以后占用的空间必然会发生变化,因此我们需要一个新的容器才能存下,正所谓鲲之大,一锅炖不下
//申请缩放输出内存resizeOutBufferDev_,内存大小resizeOutBufferSize_根据计算公式得出
//outputPicWidth、outputPicHeight分别表示图片的对齐后宽、对齐后高,此处以YUV420SP格式的图片为例
uint32_t resizeOutBufferSize_ = outputPicWidth * outputPicHeight * 3 / 2;
ret = acldvppMalloc(&resizeOutBufferDev_, resizeOutBufferSize_)
然后要告诉生产线,做之前材料是啥样子,完成后要是什么样子
//创建缩放输入图片的描述信息,并设置各属性值
//resizeInputDesc_是acldvppPicDesc类型
resizeInputDesc_ = acldvppCreatePicDesc();
//告诉昇腾处理器去哪个货架找数据
acldvppSetPicDescData(resizeInputDesc_, resizeInDevBuffer_);
//拿多少数据
acldvppSetPicDescSize(resizeInputDesc_, resizeInBufferSize);
//输入的图片是哪片山头的
acldvppSetPicDescFormat(resizeInputDesc_, PIXEL_FORMAT_YUV_SEMIPLANAR_420);
//输入图片宽度
acldvppSetPicDescWidth(resizeInputDesc_, inputWidth_);
//输入图片的高度
acldvppSetPicDescHeight(resizeInputDesc_, inputHeight_);
//对齐后的宽度
acldvppSetPicDescWidthStride(resizeInputDesc_, inputWidthStride);
//对齐后的高度
acldvppSetPicDescHeightStride(resizeInputDesc_, inputHeightStride);
//创建缩放输出图片的描述信息,并设置各属性值
//如果缩放的输出图片作为模型推理的输入,则输出图片的宽高要与模型要求的宽高保持一致
//resizeOutputDesc_是acldvppPicDesc类型
resizeOutputDesc_ = acldvppCreatePicDesc();
//处理完的数据放哪里,包装箱放哪里
acldvppSetPicDescData(resizeOutputDesc_, resizeOutBufferDev_);
//这个产品多大,占多大地方
acldvppSetPicDescSize(resizeOutputDesc_, resizeOutBufferSize_);
//哪个山头的
acldvppSetPicDescFormat(resizeOutputDesc_, PIXEL_FORMAT_YUV_SEMIPLANAR_420);
//想要缩放到的宽度
acldvppSetPicDescWidth(resizeOutputDesc_, resizeOutputWidth_);
//想要缩放的高度
acldvppSetPicDescHeight(resizeOutputDesc_, resizeOutputHeight_);
//对齐后的宽度
acldvppSetPicDescWidthStride(resizeOutputDesc_, resizeOutputWidthStride);
//对齐后的高度
acldvppSetPicDescHeightStride(resizeOutputDesc_, resizeOutputHeightStride);
开始缩放吧
//执行异步缩放,再调用aclrtSynchronizeStream接口阻塞程序运行,直到指定Stream中的所有任务都完成
ret = acldvppVpcResizeAsync(dvppChannelDesc_, resizeInputDesc_,
resizeOutputDesc_, resizeConfig_, stream_);
ret = aclrtSynchronizeStream(stream_);
缩放结束后,释放资源,包括缩放输入/输出图片的描述信息、缩放输入/输出内存
acldvppDestroyPicDesc(resizeInputDesc_);
acldvppDestroyPicDesc(resizeOutputDesc_);
if(runMode == ACL_HOST) {
//该模式下,由于处理结果在Device侧,因此需要调用内存复制接口传输结果数据后,再释放Device侧内存
//申请Host内存vpcOutHostBuffer
void* vpcOutHostBuffer = nullptr;
vpcOutHostBuffer = malloc(resizeOutBufferSize_);
//通过aclrtMemcpy接口将Device的处理结果数据传输到Host
aclRet = aclrtMemcpy(vpcOutHostBuffer, resizeOutBufferSize_, resizeOutBufferDev_, resizeOutBufferSize_, ACL_MEMCPY_DEVICE_TO_HOST);
//释放掉输入输出的device内存
(void)acldvppFree(resizeInDevBuffer_);
(void)acldvppFree(resizeOutBufferDev_);
//数据使用完成后,释放内存
free(vpcOutHostBuffer);
} else {
//此时运行在device侧,处理结果也在Device侧,可以根据需要操作处理结果后,释放Device侧内存
(void)acldvppFree(resizeInDevBuffer_);
(void)acldvppFree(resizeOutBufferDev_);
}
嘿嘿,又完成了!
最后一个,解码视频流
流程图给爷上
这个视频解码与图片有点不一样,我们都了解视频就是一堆图像连续播放.一秒钟可能有几十帧的图像,所以我们就不可以像,一条流水线一样从前到后完全做完在做下一个,要把解码完成的后处理动作转移到另一条生产线上
创建回调函数,处理解码视频以外的所有事情
//3.创建回调函数,这个函数就是另一条生产线上做的事情,除了不负责解码,啥都做
void callback(acldvppStreamDesc *input, acldvppPicDesc *output, void *userdata)
{
static int count = 1;
if (output != nullptr) {
//获取VDEC解码的输出内存,调用自定义函数WriteToFile将输出内存中的数据写入文件后,再调用acldvppFree接口释放输出内存
void *vdecOutBufferDev = acldvppGetPicDescData(output);
if (vdecOutBufferDev != nullptr) {
// 0: vdec success; others, vdec failed
//retCode为0表示解码成功,为1表示解码失败。如果解码失败,需要根据日志中的返回码判断具体的问题,返回码请参见返回码说明。
int retCode = acldvppGetPicDescRetCode(output);
if (retCode == 0) {
// process task: write file
uint32_t size = acldvppGetPicDescSize(output);
std::string fileNameSave = "outdir/image" + std::to_string(count);
// vdec输出结果在device侧,在WriteToFile方法中进行下述处理
if (!Utils::WriteToFile(fileNameSave.c_str(), vdecOutBufferDev, size)) {
ERROR_LOG("write file failed.");
}
} else {
ERROR_LOG("vdec decode frame failed.");
}
// free output vdecOutBufferDev
aclError ret = acldvppFree(vdecOutBufferDev);
}
// 释放acldvppPicDesc类型的数据,表示解码后输出图片描述数据
aclError ret = acldvppDestroyPicDesc(output);
}
// free input vdecInBufferDev and destroy stream desc
if (input != nullptr) {
void *vdecInBufferDev = acldvppGetStreamDescData(input);
if (vdecInBufferDev != nullptr) {
aclError ret = acldvppFree(vdecInBufferDev);
}
// 释放acldvppStreamDesc类型的数据,表示解码的输入码流描述数据
aclError ret = acldvppDestroyStreamDesc(input);
}
INFO_LOG("success to callback %d.", count);
count++;
}
准备数据,把数据的准备好,各项信息都标好
//4.创建视频码流处理通道时的通道描述信息,设置视频处理通道描述信息的属性,其中线程、callback回调函数需要用户提前创建。
//vdecChannelDesc_是aclvdecChannelDesc类型
vdecChannelDesc_ = aclvdecCreateChannelDesc();
//创建一个标致这条生产线的标志
ret = aclvdecSetChannelDescChannelId(vdecChannelDesc_, 10);
//另一条生产线的位置
ret = aclvdecSetChannelDescThreadId(vdecChannelDesc_, threadId_);
//另一条生产线要干的工作
ret = aclvdecSetChannelDescCallback(vdecChannelDesc_, callback);
//示例中使用的是H265_MAIN_LEVEL视频编码协议,视频的编码
ret = aclvdecSetChannelDescEnType(vdecChannelDesc_, static_cast<acldvppStreamFormat>(enType_));
//示例中使用的是PIXEL_FORMAT_YVU_SEMIPLANAR_420,解码后的图片的编码
ret = aclvdecSetChannelDescOutPicFormat(vdecChannelDesc_, static_cast<acldvppPixelFormat>(format_));
//5.创建视频码流处理的通道
ret = aclvdecCreateChannel(vdecChannelDesc_);
//调用aclrtGetRunMode接口获取软件栈的运行模式,如果调用aclrtGetRunMode接口获取软件栈的运行模式为ACL_HOST,则需要通过aclrtMemcpy接口将输入图片数据传输到Device,数据传输完成后,需及时释放内存;否则直接申请并使用Device的内存
aclrtRunMode runMode;
ret = aclrtGetRunMode(&runMode);
if(runMode == ACL_HOST){
//申请Host内存inputHostBuff
void* inputHostBuff= nullptr;
//inBufferSize_为输入码流大小
inputHostBuff= malloc(inBufferSize_);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inputHostBuff, inBufferSize_);
//申请Device内存inBufferDev_
aclRet = acldvppMalloc(&inBufferDev_, inBufferSize_);
//通过aclrtMemcpy接口将输入图片数据传输到Device
aclRet = aclrtMemcpy(inBufferDev_, inBufferSize_, inputHostBuff, inBufferSize_, ACL_MEMCPY_HOST_TO_DEVICE);
//数据传输完成后,及时释放内存
free(inputHostBuff);
} else {
//申请Device输入内存dataDev, StreamBufferSize为输入码流大小
ret = acldvppMalloc(&inBufferDev_, inBufferSize_);
//将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inBufferDev_, inBufferSize_);
}
标志出来输入的视频的个人信息,以及输出的图片的个人信息
//7.1 创建输入视频码流描述信息,设置码流信息的属性
streamInputDesc_ = acldvppCreateStreamDesc();
//inBufferDev_表示Device存放输入视频数据的内存,inBufferSize_表示内存大小
ret = acldvppSetStreamDescData(streamInputDesc_, inBufferDev_);
ret = acldvppSetStreamDescSize(streamInputDesc_, inBufferSize_);
//7.2 申请Device内存picOutBufferDev_,用于存放VDEC解码后的输出数据
ret = acldvppMalloc(&picOutBufferDev_, size);
//7.3 创建输出图片描述信息,设置图片描述信息的属性
//picOutputDesc_是acldvppPicDesc类型
picOutputDesc_ = acldvppCreatePicDesc();
ret = acldvppSetPicDescData(picOutputDesc_, picOutBufferDev_);
ret = acldvppSetPicDescSize(picOutputDesc_, size);
ret = acldvppSetPicDescFormat(picOutputDesc_, static_cast<acldvppPixelFormat>(format_));
开始解码
//7.4 执行视频码流解码,解码每帧数据后,系统自动调用callback回调函数将解码后的数据写入文件,再及时释放相关资源
ret = aclvdecSendFrame(vdecChannelDesc_, streamInputDesc_, picOutputDesc_, nullptr, nullptr);
清理一下场地
//8.释放图片处理通道、图片描述信息
ret = aclvdecDestroyChannel(vdecChannelDesc_);
aclvdecDestroyChannelDesc(vdecChannelDesc_);
至此,图片解码,缩放,视频解码已经全部完成
本文的介绍,还有好多函数的细节未曾解释清楚,欢迎各位查看官方的文档,进行系统化的学习,让我们一起建设国产软件的生态
昇腾文档
- 点赞
- 收藏
- 关注作者
评论(0)