概述
在视频处理流程中,视频的解码通常在 CPU 中进行,若用户需要使用集成显卡进行深度学习推理,解码数据需要从 CPU 的缓存中拷贝至集成显卡中进行推理。本文旨在通过集成显卡进行硬件解码,使用FFmpeg 集成 VAAPI 进行硬解码并使用滤镜进行图像缩放以及使用OpenVINO™ 的 Remote Blob 来避免解码后数据在集成显卡与 CPU之间的拷贝,最终将视频处理全流程部署在集成显卡中,实现图像数据传输零拷贝的方案。

1. 背景介绍

在日新月异的市场环境下,AI 技术不论是在工业界还是在学术界都获得了前所未有的革新与发展。训练和推理是使用深度学习模型的两个主要的部分。在推理阶段中,更看重推理延时和实时帧率在实际应用中的数值,使用 Intel® CPU 或 CPU 中的集成显卡(iGPU)能够进行低延迟模型推理,并且由 Intel®推出的 OpenVINO™ 工具套件还可以帮助开发者加速模型的在CPU 或 iGPU 上的推理。

在使用深度学习模型的计算机视觉工作流程中,从视频文件或者视频流的输入开始,需要经过视频解包,解码器解码,裸流前处理,模型推理,后处理,编码,封装视频文件之后我们才能从播放器中观看到显示推理结果的视频。iGPU 同样拥有解码能力,并且 OpenVINO™ 支持 iGPU 进行推理,若使用iGPU 推理可以降低 CPU 的运算压力,提高整体的设备利用率。在第 11 代酷睿™ 处理器中,新架构的加成下 CPU 性能提升 20%,AI 算力提升 5 倍[1],CPU 中内置 的 Xe iGPU 所包含的 Execution Unit (EU) 数量最高达到了上一代的两倍,一共 96 个,相当于在深度学习计算方面能够提供相较于上一代两倍的性能。

2. 提出问题

实际开发过程中, OpenVINO™ 通过调用 iGPU Plugin,便可以轻松完成 iGPU 推理的部署。若直接使用 OpenCV* 的 Video Capture 函数进行编解码,Libscale 库进行图像大小缩放,视频的解码与缩放通常由 CPU 完成,上述的视频处理流程如下图所示:

ffmpeg python解码速度 ffmpeg vaapi解码_缩放


上述工作流程中解码与缩放在 CPU 中完成,而我们需要利用iGPU 中编解码单元进行硬件编解码加速从而获得更好的解码性能,同时降低 CPU 的工作负载,解码工作迁移至 iGPU 后的流程如下图所示:

ffmpeg python解码速度 ffmpeg vaapi解码_opencv_02


若只有解码在 iGPU 中进行,图像缩放工作运行在 CPU 中,那么解码完成的数据需要拷贝回 CPU 进行缩放,缩放完成后再拷贝至 iGPU 中进行推理。可以看到,这个流程中有很明显的内存拷贝,数据在 iGPU 与 CPU 中来回拷贝传输,频繁的拷贝势必会造成 CPU 带宽资源的浪费,所以我们需要有一个更优的解决方案实现更少的数据拷贝从而提高性能。优化方案在 iGPU 解码的基础上,可以将图像缩放也通过 iGPU 完成,并且缩放完成后的数据可以直接传输给 OpenVINO™ 的 iGPU Plugin 进行推理。

3. 方案说明

通过利用开源视频处理软件 FFmpeg 可以很容易地进行硬件解码工作。FFmpeg作为市面上比较流行的开源视频处理工具,其后端可以支持 LibVA 或者 MediaSDK 进行硬件解码。

在本文的方案中,选择使用 LibVA 作为处理后端,同时 LibVA也在 iGPU 中提供了名为 scale_vaapi 的滤镜可以用来进行图片的缩放。这样保证了解码和图像缩放的流程是在 iGPU 中进行,同时,我们必须要通过设置好滤镜的相关参数来保证从解码到缩放直接数据的连通性,而不进行任何多余的数据拷贝。第一步优化后的流程如下图所示:

ffmpeg python解码速度 ffmpeg vaapi解码_计算机视觉_03


然后,引入 OpenVINO™ 工具套件提供的 Remote Blob 来作为缩放完成的数据与推理引擎之间的数据衔接桥梁。在图像在 iGPU 中完成缩放后,图像数据全部存储与 iGPU 的内存中,构建Remote Blob时可以很轻易地获取到VAAPI给它的输入参数,所以我们只需要使用 Remote Blob 便可以很方便地将数据从 iGPU 内存中输出给 iGPU 进行推理。由于使用硬件解码输出的视频格式是 NV12,图像缩放完成之后图像格式依旧为 NV12,Remote Blob 可以支持的数据输入格式也为 NV12,所以可以无缝衔接 iGPU 内存中的数据发送至Remote Blob。最终避免了数据从 CPU 到 iGPU 的内存拷贝,整体流程如下图所示:

ffmpeg python解码速度 ffmpeg vaapi解码_计算机视觉_04


项目整体结构以及软件库之间的所属关系如下图所示:

ffmpeg python解码速度 ffmpeg vaapi解码_opencv_05


我们的方案将会使用开源软件FFmpeg调用VAAPI,使用VAAPI 提供的硬件解码和硬件缩放,然后将 FFmpeg 的代码集成于 OpenVINO™ 的推理示例中,在使用 iGPU 推理前进行视频解码和缩放,然后利用缩放完成的数据构建 OpenVINO™ 的 Remote Blob。最终目的是将解码、缩放和推理的工作流程都在 iGPU 中完成,减少数据在 CPU 和 iGPU 间无用的拷贝,从而更好地利用 iGPU 的性能的同时减轻 CPU 的负载。

本方案实验环境:

硬件:Intel® Core™ i5-7200U 2.5GHz, HD Graphics 620 (KBL GT2), MEM: 8G DDR4软件版本:Linux 18.04.4 LTS, OpenVINO 2020.3,FFmpeg 4.3.1, LibVA: 2.1.0, VA-API version 1.6.0

3.1 iGPU 的硬件解码与硬件缩放

首先,确保环境中成功安装 FFmpeg 与 LibVA,并且完成将VAAPI 编译至 FFmpeg 的可用库中,具体安装方法可参考文末 git 中的安装手册。

由于代码比较长,这里节选一些比较关键的代码片段进行一个分析。需要注意,FFmpeg 使用 C 语言进行编程。

3.1.1 基于 VAAPI 的硬件解码

首先,检查硬件设备,保证测试系统中有 iGPU 的存在。初始化硬解设备,选择硬件解码方式 :“VAAPI”。

使用初始化完成的解码器,进行解码前的准备工作:

type = av_hwdevice_find_type_by_name("vaapi");   //确认使用 VAAPI 的解码 tpye...const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i); // 检查解码器设备是否支持 VAAPI 的解码 tpye...decoder_ctx = avcodec_alloc_context3(decoder))  //给解码器的 Ctx 分配内存...av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0)  // 输入硬件 ctx 和解码 tpye 初始化硬件,获取硬解码设备的 context...avcodec_parameters_to_context(decoder_ctx, video->codecpar)   // 把 video 的一些信息给到解码器的 ctx 中...ret = avcodec_open2(decoder_ctx, decoder, NULL)   // 打开解码器,准备进行解码

3.1.2 基于 VAAPI 的硬件缩放

ffmpeg python解码速度 ffmpeg vaapi解码_ffmpeg python解码速度_06


如上图所示,结构体 AVFilterGraph 是用于缩放的网络,可以看出这个网络图结构是由头尾固定的 buffersrc 和 buffersink加上自定义功能的滤镜所组成。Filter1 就是缩放滤镜所在的位置,scale_vaapi 的默认输入格式为 NV12,由于硬件解码出来的数据格式也是 NV12,所以可以直接传输数据至 VAAPI所提供的 scale_vaapi 滤镜,然后由 buffersink 输出。

硬件缩放代码展现如下,VAAPI 的硬件缩放要指定滤镜为 scale_vaapi,并输入目标缩放比例,其中 iw 代表缩放后的长度,ih代表缩放后的宽度:

const char *filter_descr = "scale_vaapi=iw:ih";  // 确定目标缩放数值...static int init_filters(const char *filters_descr,AVBufferRef  *hw_frames_ctx); //输入缩放参数 与 帧的 context 数据进行滤镜初始化

初始化 filter 的函数中,先要设置滤镜输入对应的数据格式。

在这里需要注意的是,buffersrc_ctx 作为 AVFilterContext类型的结构体,它在定义里虽然包含了 AVBufferRef 所对应的是 hw_device_ctx,但是实际对应的输入参数应该是 AVBuffer-SrcParameters 结构体下属的 AVBufferRef * hw_frames_ctx,这样才能正确将 hw_frames_ctx 作为 buffersrc_ctx 的输入参数,基于 scale_vaapi 的滤镜才能够正确地被初始化,具体方法如下所示:

// 这里必须要设置我们需要使用的数据格式,硬解对应的数据格式是 NV12enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_VAAPI_VLD, AV_PIX_FMT_NV12, AV_PIX_FMT_NONE };     AVBufferSrcParameters *par = av_buffersrc_parameters_alloc(); // 分配内存...par->hw_frames_ctx =  hw_frames_ctx; // 将 hw_frame_ctx 赋值给 par...ret = av_buffersrc_parameters_set(buffersrc_ctx,par); // 将 par 赋值到 buffersrc 中

构建好的初始化函数之后,初始化函数的运行位置需要在decode_write函数中以便于它能够读取到解码器所输出的hw_frames_ctx 参数,如下:

static int decode_write(AVCodecContext *avctx, AVPacket *packet,AVFormatContext *input_ctx,VADisplay va_display){ ...if ((ret = init_filters(filter_descr,avctx->hw_frames_ctx)) < 0) // 初始化 filter...}

3.1.3 硬件解码与硬件缩放之间的零拷贝链接

完成了硬件解码器的初始化和滤镜的初始化,程序需要将解码器输出的结果数据传输给滤镜。由于这两个操作都发生在iGPU 中,方案中解码与缩放之间不存在多余的数据拷贝。

进入 decode_write 函数,通过 avcodec_send_packet(avctx, packet) 和 avcodec_receive_frame(avctx, frame) 进行解码工作之后,得到解码好的帧数据:frame,然后我们利用函数av_buffersrc_add_frame_flags(),将解码数据输送到缩放滤镜中,然后这里需要选择“AV_BUFFERSRC_FLAG_PUSH”参数,这样可以避免数据在传入滤镜去前自我拷贝一次而是可以直接将解码数据传给缩放滤镜继续工作流程。最终通过 av_buffersink_get_frame(buffersink_ctx, filt_frame) 获得最终缩放完成的帧数据:filt_frame,流程如下所示:

ret = avcodec_send_packet(avctx, packet);while (1) {ret = avcodec_receive_frame(avctx, frame); // 在解码后将 frame 塞入 filter 进行缩放...if (av_buffersrc_add_frame_flags(buffersrc_ctx, frame, AV_BUFFERSRC_FLAG_PUSH) < 0) // 将 frame 塞入 filter中进行缩放...

3.2 FFmpeg 与 OpenVINO™ 集成

实验前,请确保 FFmpeg 与 OpenVINO™ 安装成功,由于 Remote Blob 只支持 C++ 接口,示例中,第一步是把刚才完成的 FFmpeg的 C 代码,使用 C++ 进行重写。由于 c 的库和 C++ 的库是通用的,只需要简单地引用 ( 使用 extern “C”) 即可使用。将之前完成的解码与缩放代码导入 OpenVINO™ 的 C++ 代码中实现硬件解码与缩放的功能。

extern "C" {...include "config.h"include "libavformat/avformat.h"... }

3.2.1 构建 Remote Blob

为了实现零拷贝方案,我们需要构建OpenVINO™ 提供的Remote Blob 为 iGPU 解码完成之后的数据和 iGPU Plugin之间数据传输的通道。Remote Blob 是 OpenVINO™ 提供给 iGPU 开发人员在使用例如 OpenCL *,Microsoft DirectX * 或 VAAPI * 等 API 的时候,OpenVINO™ 推理引擎能够从iGPU缓存中调用这些API产生的数据, 并且使用Remote Blob 可以避免 OpenVINO™ 与这些 API 数据共享传输时产生任何内存复制开销。

OpenVINO™ 官网上使用 VAAPI 构建 Remote Blob 的伪代码如下

VADisplay disp = get_VA_Device();auto shared_va_context = iGPU::make_shared_context(ie, "GPU", disp); // 创建 shared context object...VASurfaceID va_surface = decoder.get_VA_output_surface();// 将输出数据包入 Remote blobs 并且将其设为推理引擎的inputauto nv12_blob = iGPU::make_shared_blob_nv12 (ieInHeight,ieInWidth,shared_va_context,va_surface)

从代码中可知,若要构建 Remote Blob,必须获取到 VAAPI中 VADisplay 与 VASurfaceID 这两个参数。

3.2.1.1 VADisplay 的获取

在硬件解码的流程中,hw_device_ctx 由 av_hwdevice_ctx_create() 创建,用于储存硬件设备的上下文信息。hw_device_ctx 是 AVBufferRef 的数据结构,所以 hw_device_ctx 包含了名为 data 的 data buffer。 同时我们还需要关注两个关键的数据结构 AVHWDeviceContext 与 AVVAAPIDeviceContext。AVHWDeviceContext 所代表的是硬件设备的上下文信息,AVVAAPIDeviceContext 代表的是 VAAPI 设备的一些上下文信息。

hw_device_ctx->data 正好对应我们所需要的 AVHWDeviceContext 结构体,而 AVHWDeviceContext 中的 hwctx 参数,通过资料可知,不同的格式解码方式,对应就是不同解码后端所应用的解码格式,我们这里使用的是 VAAPI 的硬件解码,所以 (void*)hwctx 对应就是 AVVAAPIDeviceContext ,并且在此结构体中包含所需要的 VADisplay 参数,我们将获取该handle 作为构建 Remote Blob 的输入参数。代码在进行实际解码之前获取到 VADisplay,然后将 VADisplay 传入解码函数中进行使用。在程序中获取 VADisplay,并且传入解码函数中:

VADisplay display=NULL;hw_device_data = (AVHWDeviceContext *)hw_device_ctx ->data;va_device_data = (AVVAAPIDeviceContext *)hw_device_data ->hwctx;display= va_device_data -> display;...while (ret >= 0) // 解码循环{ ...             ret = decode_write(decoder_ctx, &packet,input_ctx,display);...}

3.2.1.2 VASurfaceID 的获取

根据前文得知构建 Remote Blob 除了 VADisplay 之外,还需要一个 VASurfaceID 参数,查看相关的 hwcontext_vaapi.c源代码,引用对 VASurfaceID 的定义:

static int vaapi_map_frame(AVHWFramesContext *hwfc, AVFrame *dst, const AVFrame *src, int flags){...VASurfaceID surface_id;surface_id = (VASurfaceID)(uintptr_t)src->data[3];  //定位 VASurfaceID...}

对照 pixfmt.h 中对硬件 pixfmt 的定义:

AV_PIX_FMT_VAAPI_VLD,   //< HW decoding through VA API, Picture.data[3] contains a VASurfaceID/* Hardware acceleration through VA-API, data[3] contains a VASurfaceID.*/

由此可以得知,VAAPI 所获得的 VASurfaceID 保存在 src->data[3] 中,而 src/picture 对应的数据结构是 AVFrame,则在我们解码完成之后的 AVFrame 中去获取 data[3],那么AVFrame->data[3] 就是我们所需要的 VASurfaceID,最后将其作为 Remote Blob 的输入参数进行调用。示例代码解码完成之后立即进行缩放操作,filt_frame 是缩放之后得到的AVFrame, 获取缩放完成帧的 VASurfaceID 代码如下

ret = avcodec_receive_frame(avctx, frame); // 获取解码完成帧 frame...ret = av_buffersink_get_frame(buffersink_ctx, filt_frame); // 获取缩放完成帧 filt_frame...surface_id=(VASurfaceID)(uintptr_t)filt_frame->data[3];  // 从 filter_frame->data[3] 获取到 VASurfaceID的数据

3.2.1.3 Remote Blob 的构建代码

获取 VASurfaceID 与 VADisplay 构建 Remote Blob 的代码如下 :

//  创 建 the shared context object,IGPU 和 va_display构建成 shared_va_contextauto shared_va_context = iGPU::make_shared_context(ie, "GPU", va_display);...ExecutableNetwork executable_network = ie.Load Network (network,                 shared_va_context,                 {{ CLDNNConfigParams::KEY_CLDNN_NV12_                  TWO_INPUTS,PluginConfigParams::YES }});...auto nv12_blob = iGPU::make_shared_blob_nv12(300,300,shared_va_context,surface_id); // 创建 remoteblob,它的格式为 NV12,输入模型input shape 的长宽, shared_va_context,,surface_id// --------------------------- 5. Create infer request ---------------------------InferRequest infer_request = executable_network.CreateInferRequest();// --------------------------- 6. Prepare input --------------------------
infer_request.SetBlob(input_name, nv12_blob);  //  设置推理 bolbif (FLAGS_auto_resize) {    getPreProcess().setResizeAlgorithm(ResizeAlgorithm::RESIZE_BILINEAR);     }// 请保证输入图像满足模型的 input shape// --------------------------- 7. Do inference ---------------------------infer_request.Infer();

全部代码片段解释已经全部结束,之后可以进行整体代码的编译工作。

本文只提供代码片段作为参考,完整代码以及编译手册将提供于文末 git 中,有需要的同学可以自行下载查看。

4. 性能对比测试:

测试设备:

硬件:Intel® Core™ i5-7200U 2.5GHzx4, HD Graphics 620 (KBL GT2), MEM: 8G DDR4软件版本:Linux 18.04.4 LTS, OpenVINO 2020.3,FFmpeg 4.3.1, LibVA: 2.1.0, VA-API version 1.6.0

测试日期:2021.7.30

Demo 1:OpenVINO Obeject_detection_async_ssd 官 方demo(CPU 解码 +iGPU 推理 ):

ffmpeg python解码速度 ffmpeg vaapi解码_人工智能_07


本文优化后的 Demo2:零拷贝方案 Obeject_detection sample(iGPU 解码 +iGPU 推理 ):

ffmpeg python解码速度 ffmpeg vaapi解码_opencv_08


ffmpeg python解码速度 ffmpeg vaapi解码_opencv_09

4.1 结果分析:

将推理的各项性能数据进行对比,由于两种方案都是采用iGPU 进行推理,所以推理时间大致相同,这里比较了推理前的前处理的用时,平均 CPU/iGPU 占用率以及平均 FPS,原始方案中平均花费 2.7485 毫秒进行前处理,使用本文所提到的硬解加零拷贝方案的示例只需要 0.3581 毫秒完成前处理,零拷贝方案可以减少 87% 的前处理时间。其次,在 CPU 占用率方面,原始方案的平均 CPU 占用率为 206%,零拷贝方案 CPU 的平均占用率为 100%,相较之前的方案,CPU 释放了 51% 的计算资源。比较平均 FPS,由于前处理时间的缩短带来的是 FPS 的升高 15%。对于输出的推理结果,两种方法的推理结果输出数据相差 0.1% 以内,表明两种方案最终得到的推理结果是一致的,Remote Blob 不会对精度造成损失。

硬件解码加零拷贝方案的优化解决方案与原生示例最主要的差别就是在解码和缩放的时候使用 iGPU 进行解码以及缩放,并且减少了工作流程中间存在的 CPU 至 iGPU 或者 iGPU 至CPU 的数据资料拷贝。本文优化方案使用 iGPU 进行图像的解码以及缩放,并且避免了原来从 iGPU 至 CPU 以及 CPU 至iGPU 拷贝图像数据的过程,最终达到降低流程延迟,避免内存拷贝,提升性能的同时可以释放 CPU 一部分计算资源和带宽资源。

5. 总结

在传统视频处理流程中,CPU 承担了视频解码以及视频缩放的任务,这使得 CPU 在工作中负载大,并且使与 CPU 通信的带宽比较拥挤。本文方案利用Intel® CPU所包含强大的iGPU 芯片,将解码与缩放的任务置于 iGPU 中完成,并且构建 Remote Blob 使硬件缩放的数据能够零消耗地读入 iGPU Plugin 进行推理。

本文通过使用 FFmpeg 调用 VAAPI 的 LibVA 作为解码后端和缩放后端,解码产生的数据存储在 iGPU 的缓存中,利用硬解参数 VASurefaceID 和 VADisplay 构建 Remote Blob,使得iGPU Plugin 在推理的时候可以直接获取 iGPU 缓存中这些格式为 NV12 的图像数据。通过结果的数据分析比对,经过优化的方案前处理所消耗的时间更少,解码与缩放操作从 CPU 转到 iGPU 中,通过 Remote Blob 使 iGPU Plugin 推理时能直接获取解码缩放数据,减少了 CPU 与 iGPU 间不必要的数据拷贝,使得 CPU 的负载明显降低了。整个视频处理流程运行在 iGPU 上,可以很好地降低带宽负载并且降低 CPU 负载,能够让整个应用程序更加轻盈,性能更好。