入门昇腾AI媒体预处理技术

马城林 发表于 2022/04/07 22:58:28 2022/04/07
【摘要】 记录训练营的学习记录

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.在专门的芯片上进行媒体数据处理这么厉害,我要怎么学习呢?

别急一步一来00BDE2F6.png

第一式,摸清敌情

媒体数据处理是什么
image.png
媒体数据都可以应用在哪些方面
image.png
媒体数据处理在接口调用流程中的位置
image.png

第二式,逐个击破

在计算机视觉领域处理最多的应该也就是图片和视频了,
所以,我们先学习JPEG图片的解码和编码

a.江湖规矩先看视频,

温馨提示视频中讲述的是各种数据处理的集合哦,可以按需跳跃观看
媒体数据处理
image.png

b.光说不练假把式,来个实验开开眼

实验体验
image.png

c.没记错的话,我们貌似需要写代码吧,那代码何在??

诶嘿嘿,代码来喽!!00CF7281.png
但是,写代码之前我们最好要知道我们整个程序的流程,那么它来喽
图片解码流程
首先就是创建数据处理的通道,你可以把它理解为一个专为处理媒体数据的生产线,在这个生产线上会对媒体数据(图片,视频)进行加工

//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_);

搞定了完成了
解码完成了,我们想对图片进行一个缩放,该咋办呢??011A3CAF.jpg

同样先来一个流程图
缩放
其实还是那个套路
先准备生产线

// 创建图片数据处理通道时的通道描述信息,dvppChannelDesc_是acldvppChannelDesc类型
dvppChannelDesc_ = acldvppCreateChannelDesc();

011FBEA8.jpg
创建图片缩放配置数据、指定缩放算法,图片缩放后会成什么鸟样子和采用什么算法有很大差异,中国厨子和外国厨子做出的菜能一个味吗??

//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);
}

这里有个点要注意,图片缩放以后占用的空间必然会发生变化,因此我们需要一个新的容器才能存下,正所谓鲲之大,一锅炖不下0128FA6D.jpg

//申请缩放输出内存resizeOutBufferDev_,内存大小resizeOutBufferSize_根据计算公式得出
//outputPicWidth、outputPicHeight分别表示图片的对齐后宽、对齐后高,此处以YUV420SP格式的图片为例
uint32_t resizeOutBufferSize_ = outputPicWidth * outputPicHeight * 3 / 2;
ret = acldvppMalloc(&resizeOutBufferDev_, resizeOutBufferSize_)

然后要告诉生产线,做之前材料是啥样子,完成后要是什么样子01282386.jpg

//创建缩放输入图片的描述信息,并设置各属性值
//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_);
}

嘿嘿,又完成了!0131D546.jpg

最后一个,解码视频流013399D9.jpg
流程图给爷上 01354C3C.jpg

视频解码

这个视频解码与图片有点不一样,我们都了解视频就是一堆图像连续播放.一秒钟可能有几十帧的图像,所以我们就不可以像,一条流水线一样从前到后完全做完在做下一个,要把解码完成的后处理动作转移到另一条生产线上01398441.jpg
创建回调函数,处理解码视频以外的所有事情013CF4DE.gif

//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_);

至此,图片解码,缩放,视频解码已经全部完成0144DE71.gif

本文的介绍,还有好多函数的细节未曾解释清楚,欢迎各位查看官方的文档,进行系统化的学习,让我们一起建设国产软件的生态
昇腾文档
0147996A.jpg

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。