CANN媒体数据处理代码V2再学习
CANN媒体数据处理的示例代码(处理流程)还是比较简单明了的,这里再进一步学习一下。先看一下 JPEGD,对照看一下 VPC的resize,比较一下其中的异同。
初始化没有什么好说的,开始就要做。
// 1.AscendCL初始化
aclRet = aclInit(nullptr);
然后就要指定用于操作的设备编号 aclrtSetDevice
:Specify the device to use for the operation. implicitly create the default context and the default stream。这个设备的单位应该是指昇腾芯片,比如一块板卡上面可能有多个芯片,那么实际上是多个设备可以用的,就有多个设备编号,去指定用哪一个。
这个设备又是可以在多个进程或线程之间共享使用的,比如 Device is specified in a process, and multiple threads in the process can share this device to explicitly create a Context (aclrtCreateContext interface). 那么,如果在进程内的多个线程共用一个设备,那么线程之间的使用如何区分呢,就是用 Context
上下文来区分。
至于软件栈的运行模式,目前还只碰到 Host 模式。
// 2.运行管理资源申请(依次申请Device、Context)
aclrtContext g_context;
aclRet = aclrtSetDevice(0);
aclRet = aclrtCreateContext(&g_context, 0);
// 获取软件栈的运行模式: Host or Device
aclrtRunMode runMode;
aclError aclRet = aclrtGetRunMode(&runMode);
前面初始化了ACL,这里要初始化ACL的具体处理模块/子系统 MPP:init mpp system。VPC和编解码功能都属于这个MPP 媒体数据预处理
子系统里提供的功能。
// 3.初始化媒体数据处理系统
int32_t ret = hi_mpi_sys_init();
上面的部分,解码和缩放是一样一样的,但从下面开始,就有些小小的差异了。
解码
是创建视频解码通道 hi_mpi_vdec_create_chn
:create video decoder channel。为什么是视频解码通道,不是图片解码通道呢?我们要解码的是一张JPEG图片呀?原因可能是简化接口了,解码视频接口当然能处理图片,视频不就是一帧帧的图片组成了吗,图片也可以看做视频的特例。
然后是获取并设置解码通道参数:get/set video decoder channel parameter。
创建视频解码通道 使用到 hi_vdec_chn_attr 结构体
获取并设置解码通道参数 使用到 hi_vdec_chn_param 结构体
// 4.创建通道
hi_vdec_chn chnId;
hi_vdec_chn_attr chnAttr;
chnAttr.type = HI_PT_JPEG;
chnAttr.mode = HI_VDEC_SEND_MODE_FRAME;
chnAttr.pic_width = 1920;
chnAttr.pic_height = 1080;
chnAttr.stream_buf_size = 1920 * 1080;
ret = hi_mpi_vdec_create_chn(chnId, &chnAttr);
// 5.设置通道属性
hi_vdec_chn_param chnParam;
ret = hi_mpi_vdec_get_chn_param(chnId, &chnParam);
chnParam.pic_param.pixel_format = HI_PIXEL_FORMAT_YUV_SEMIPLANAR_420;
chnParam.pic_param.alpha = 255;
chnParam.display_frame_num = 0;
ret = hi_mpi_vdec_set_chn_param(chnId, &chnParam);
而缩放
是创建VPC通道 hi_mpi_vpc_sys_create_chn
:create system vpc channel for single channel multi-core acceleration。通道类型是不一样的, 并且这里要简单一些,不需设置创建属性,直接创建就完了,也没有获取和设置参数这些细节的操作:
// 4.创建通道
hi_vpc_chn chnId;
hi_vpc_chn_attr stChnAttr;
ret = hi_mpi_vpc_sys_create_chn(&chnId, &stChnAttr);
视频解码通道开始接受码流:video decoder channel start receive stream
解码的话,就是这个样子处理的:先把接受码流打开,然后发送码流,并且涉及到线程的使用。而缩放不存在这么复杂,就是直接调用缩放接口处理就行。
// 6.解码器启动接收码流
ret = hi_mpi_vdec_start_recv_stream(chnId);
好了,接下来就是要为输入输出图片分配内存了,这个是挺繁琐的事情。就好比看房子搬家,就是个头疼的事情,要看多大的房子,行李有多少,要打多少包,租多大的车子等等,烦死了。
hi_mpi_dvpp_malloc
分配设备上的内存:alloc device memory for dvpp。
aclrtMallocHost
分配主机上的内存,设备是不能访问到这块内存的,需要显式的拷贝到设备的内存里:alloc memory on host,The requested memory cannot be used in the Device and needs to be explicitly copied to the Device.
aclrtMemcpy
进行主机和设备间内存的拷贝:synchronous memory replication between host and device。
所以你可以看到,分配主机内存、分配设备内存,读取数据到主机内存,拷贝主机内存内容到设备内存,然后才能在设备上处理,这一套下来,还是比较繁琐的。
这里解码
为输入图片分配的内存大小比较好理解,文件有多大,就分配多大。
这个和缩放
就完全不一样了,缩放比较复杂,需要按stride计算。
// 7.发送码流
// 7.1 申请输入内存(在设备上)
uint8_t* inputAddr = nullptr;
// inputsize表示输入图片占用的内存大小,此处以1024 Byte为例,用户需根据实际情况计算内存大小
int32_t inputSize = 1024;
ret = hi_mpi_dvpp_malloc(0, &inputAddr, inputSize);
// 如果运行模式为ACL_HOST,则需要申请Host内存,将输入图片数据读入Host内存,再通过aclrtMemcpy接口将Host的图片数据传输到Device,数据传输完成后,需及时释放Host内存;否则直接将输入图片数据读入Device内存
if (runMode == ACL_HOST) {
void* hostInputAddr = nullptr;
// 申请Host内存
aclRet = aclrtMallocHost(&hostInputAddr, inputSize);
// 将输入图片读入内存中,该自定义函数ReadStreamFile由用户实现
ReadStreamFile(fileName, hostInputAddr, inputSize);
// 数据传输 从Host到Device
aclRet = aclrtMemcpy(inputAddr, inputSize, hostInputAddr, inputSize, ACL_MEMCPY_HOST_TO_DEVICE);
} else {
// 将输入图片读入内存中,该自定义函数ReadStreamFile由用户实现
ReadStreamFile(fileName, inputAddr, inputSize);
}
解码是在设备上操作的,那么输出图片的内存当然是分配在设备上,应该分配多大的内存呢?是通过 hi_mpi_dvpp_get_image_info
来获取到的:get input image’s information parsed by dvpp,
它有3个参数,内存大小就输出在第3个参数 hi_img_info
的成员里。
第1个参数 img_type: payload type of input image 给值是 HI_PT_JPEG
(value of payload type from RTP/RTSP definition),
第2个参数是 hi_vdec_stream
stream info pointer,这个结构体里带了输入图片的大小,并且有指向输入图片的主机内存的指针。在调用了这个函数后,主机内存地址就被释放了。
// 7.2 构造存放输入图片信息的结构体
hi_vdec_stream stStream{};
hi_img_info stImgInfo{};
stStream.pts = 0;
if (g_runMode == ACL_HOST) {
stStream.addr = (uint8_t *)hostInputAddr;
} else {
stStream.addr = (uint8_t *)inputAddr;
}
stStream.len = **inputSize**;
stStream.end_of_frame = HI_TRUE;
stStream.end_of_stream = HI_FALSE;
stStream.need_display = HI_TRUE;
ret = hi_mpi_dvpp_get_image_info(HI_PT_JPEG, &stStream, &stImgInfo);
if (g_runMode == ACL_HOST) {
// 如果不使用Host上的数据,需及时释放
aclrtFreeHost(hostInputAddr);
hostInputAddr = nullptr;
}
stStream.addr = (uint8_t *)inputAddr;
// 7.3 构造存放输出图片信息的结构体,并申请输出内存
hi_vdec_pic_info outPicInfo{};
void *outBuffer = nullptr;
outPicInfo.width = stImgInfo.width;
outPicInfo.height = stImgInfo.height;
outPicInfo.width_stride = stImgInfo.width_stride;
outPicInfo.height_stride = stImgInfo.height_stride;
outPicInfo.buffer_size = stImgInfo.img_buf_size;
outPicInfo.pixel_format = HI_PIXEL_FORMAT_UNKNOWN;
ret = hi_mpi_dvpp_malloc(0, &outBuffer, outPicInfo.buffer_size);
outPicInfo.vir_addr = (uint64_t)outBuffer;
然后就可以发送码流到解码通道 hi_mpi_vdec_send_stream
:send stream and outbuffer to video decoder channel。这个函数里新的参数是后2个,
一个是 hi_vdec_pic_info 存放输出图片信息的结构体,
一个是调用是否阻塞以及超时设置(-1 is block,0 is no block,other positive number is timeout)。
解码完成后,获取处理后的图像帧 hi_mpi_vdec_get_frame
:get frame from video decoder channel。最后一个参数同上。
结果图像地址在参数 hi_video_frame_info
的成员 hi_video_frame
的成员 virt_addr
里
// 7.4 发送需解码的输入图片
ret = hi_mpi_vdec_send_stream(chnId, &stStream, &outPicInfo, 0);
// 8.接收解码结果
// 8.1 获取解码结果
hi_video_frame_info frame;
hi_vdec_stream stream;
hi_vdec_supplement_info stSupplement;
ret = hi_mpi_vdec_get_frame(chnId, &frame, &stSupplement, &stream, 0);
if (ret == HI_SUCCESS) {
decResult = frame.v_frame.frame_flag;
if (decResult == 0) { // 0: Decode success
printf("[%s][%d] Chn %u GetFrame Success, Decode Success \n",__FUNCTION__, __LINE__, chnId);
} else { // Decode fail
printf("[%s][%d] Chn %u GetFrame Success, Decode Fail \n",__FUNCTION__, __LINE__, chnId);
}
}
输出图片从device拷贝到host
// 8.2 如果运行模式为ACL_HOST,且Host上需要展示JPEGD输出的图片数据,则需要申请Host内存,通过aclrtMemcpy接口将Device的输出图片数据传输到Host
if (g_runMode == ACL_HOST) {
void* hostOutputAddr = nullptr;
aclRet = aclrtMallocHost(&hostOutputAddr, outputSize);
aclRet = aclrtMemcpy(hostOutputAddr, outputSize, frame.v_frame.virt_addr[0], outputSize, ACL_MEMCPY_DEVICE_TO_HOST);
// ......
// 数据使用完成后,及时释放不使用的内存
aclrtFreeHost(hostOutputAddr);
hostOutputAddr = nullptr;
} else {
// 可以直接使用JPEGD的输出图片数据,在frame.v_frame.virt_addr[0]指向的内存中
// ......
}
释放资源、去初始化。
// 8.3 释放输入、输出内存
ret = hi_mpi_dvpp_free(frame.v_frame.virt_addr[0]);
ret = hi_mpi_dvpp_free(stream.addr);
// 8.4 释放资源
ret = hi_mpi_vdec_release_frame(chnId, &frame);
// 9.解码器停止接收码流
ret = hi_mpi_vdec_stop_recv_stream(chnId);
// 10.销毁通道
ret = hi_mpi_vdec_destroy_chn(chnId);
// 11. 媒体数据处理系统去初始化
ret = hi_mpi_sys_exit();
// 12. 释放运行管理资源(依次释放Context、Device)
aclRet = aclrtDestroyContext(g_context);
aclRet = aclrtResetDevice(0);
// 13.AscendCL去初始化
aclRet = aclFinalize();
花开二朵,各表一枝。接着前面建立通道之后,来说一下 VPC resize
的代码流程。
极简代码建立通道之后,计算分配的内存大小却是要比解码要复杂。
先有图片的宽高,然后算stride,然后是相乘再乘3除2,得到要分配内存的大小;我有点奇怪,文件在这里,操作系统层面不知道该文件大小吗?按这个大小分配不就完了吗。但是我又想到,查看一个YUV文件真的很费劲,要指定宽高,要指定采样模式,不然你就看不到图像的真面目。那么这样说来,也许就不奇怪了。
// 5.执行缩放
// 5.1 构造存放输入图片信息的结构体
hi_vpc_pic_info inputPic;
inputPic.picture_width = 1920;
inputPic.picture_height = 1080;
inputPic.picture_format = HI_PIXEL_FORMAT_YUV_SEMIPLANAR_420;
inputPic.picture_width_stride = 1920;
inputPic.picture_height_stride = 1080;
inputPic.picture_buffer_size = inputPic.picture_width_stride * inputPic.picture_height_stride * 3 / 2;
// 5.2 准备输入图片数据
// 申请Device内存,用于媒体数据处理
ret = hi_mpi_dvpp_malloc(0, &inputPic.picture_address, inputPic.picture_buffer_size);
// 如果运行模式为ACL_HOST,则需要申请Host内存,将输入图片数据读入Host内存,再通过aclrtMemcpy接口将Host的图片数据传输到Device,数据传输完成后,需及时释放Host内存;否则直接将输入图片数据读入Device内存
if (runMode == ACL_HOST) {
void* inputAddr = nullptr;
// 申请Host内存
aclRet = aclrtMallocHost(&inputAddr, inputPic.picture_buffer_size);
// 将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inputAddr, inputPic.picture_buffer_size);
// 数据传输
aclRet = aclrtMemcpy(inputPic.picture_address, inputPic.picture_buffer_size, inputAddr,
inputPic.picture_buffer_size, ACL_MEMCPY_HOST_TO_DEVICE);
// 完成数据传输后,需及时释放内存
aclrtFreeHost(inputAddr);
inputAddr = nullptr;
}
else {
// 将输入图片读入内存中,该自定义函数ReadPicFile由用户实现
ReadPicFile(picName, inputPic.picture_address, inputPic.picture_buffer_size);
}
输入输出图片的内存分配方式都类似。hi_mpi_vpc_resize
resize方法调用也简单,可以查询任务是否完成 hi_mpi_vpc_get_process_result
: query whether the task has been completed, base on task_id
// 5.3 构造存放输出图片信息的结构体
hi_vpc_pic_info outputPic;
outputPic.picture_width = 960;
outputPic.picture_height = 540;
outputPic.picture_format = HI_PIXEL_FORMAT_YUV_SEMIPLANAR_420;
outputPic.picture_width_stride = 960;
outputPic.picture_height_stride = 540;
outputPic.picture_buffer_size = outputPic.picture_width_stride * outputPic.picture_height_stride * 3 / 2;
ret = hi_mpi_dvpp_malloc(0, &outputPic.picture_address, outputPic.picture_buffer_size);
// 初始化内存
if (runMode == ACL_HOST) {
aclRet = aclrtMemset(outputPic.picture_address, outputPic.picture_buffer_size, 0, outputPic.picture_buffer_size);
} else {
memset(outputPic.picture_address, 0, outputPic.picture_buffer_size);
}
// 5.4 调用缩放接口
uint32_t taskID = 0;
ret = hi_mpi_vpc_resize(chnId, &inputPic, &outputPic, 0, 0, 0, &taskID, -1);
// 5.5 等待任务处理结束,任务处理结束后,输出图片数据在outputPic.picture_address指向的内存中
uint32_t taskIDResult = taskID;
ret = hi_mpi_vpc_get_process_result(chnId, taskIDResult, -1);
// 5.6 如果运行模式为ACL_HOST,且Host上需要展示VPC输出的图片数据,则需要申请Host内存,通过aclrtMemcpy接口将Device的输出图片数据传输到Host;如果Host上不需要展示VPC输出的图片数据,则VPC的输出图片数据可以直接作为模型推理的输入
if (g_runMode == ACL_HOST) {
hi_vpc_pic_info outputPicHost = outputPic;
aclRet = aclrtMallocHost(&outputPicHost.picture_address, outputPic.picture_buffer_size);
aclRet = aclrtMemcpy(outputPicHost.picture_address, outputPic.picture_buffer_size, outputPic.picture_address,outputPic.picture_buffer_size, ACL_MEMCPY_DEVICE_TO_HOST);
// ......
// Host侧的数据使用完成后,释放Host内存
aclrtFreeHost(outputPicHost.picture_address);
outputPicHost.picture_address = nullptr;
} else {
// 可以直接使用VPC的输出图片数据,在outputPic.picture_address指向的内存中
// ......
}
后面的资源释放、去初始化也是类似的,无需多说。
- 点赞
- 收藏
- 关注作者
评论(0)