1 简介
1.1 Openvx简介
OpenVX 是一个开放的计算机视觉应用程序跨平台加速标准。OpenVX 可以实现性能和功率优化的计算机视觉处理,特别是在嵌入式和实时的应用案例中,如人脸、身体和手势跟踪、智能视频监控、高级驾驶辅助系统(ADAS)、对象和场景重建、增强现实、视觉检测、机器人等。
OpenVX 可以认为是 OpenCV 的嵌入式版本,OpenVX 提供一些图像处理的标准 API,系统第三方 Vendor 可以用硬件来实现一些图像处理功能,效率更高;而 OpenCV 则偏向于用软件来实现一些定义的功能。
由非盈利开源组织 Khronos定义的一套 API 框架,包括:(1) 宏的定义与含义 (2) 结构体的定义与含义 (3) 函数的定义与行为。而框架的代码完全由各个公司自行实现 (实现的 API 的行为符合 Khronos 的定义即可),例如 Tiovx是 Ti 公司对 Openvx 的实现,Khronos 组织本身也提供了一个 Openvx 实现作为参考。
与平台无关。Openvx 提出的初衷之一是统一各个平台的图像处理接口,提高业务代码在不同平台下的移植性。明显,框架使用了面向对象的设计思路,即结构体 (对象) 中的数据对使用者隐藏,使用者只能调用相应的函数读取及修改结构体中的数据。故在学习、理解 Openvx 时需要保持面向对象的思维方式,即不需要揪着其底层代码如何实现,从抽象层面理解各个函数的作用即可。
使用一个有向图来描述一个图像处理任务。任务的每个子步骤都对应图中的一个节点。不考虑数据在图中的表示,如果步骤 1 (设对应图中的节点 1) 的输出为步骤 2 (设对应图中的节点 2) 的输入,则在图中会使用一个边连接节点 1 和节点 2,方向由 1 到 2 (即 ①→②)。Graph是 Openvx 的核心。
API 的扩展性强。Openvx 框架本身支持的功能不多,基本上只支持单通道图像处理、甚至没有专门用于图像读取或存储的 API (类似 Opencv 中的 imread 和 imwrite)。很多资料 (包括英文资料) 都不会刻意提及如何扩展 Openvx 的 API,但想要在具体业务中应用 Openvx,学习并掌握 Openvx API 的扩展方法必不可少。
1.2 Tiovx简介
- Ti 公司提供的 Openvx 实现。
- 它是 Ti RTOS SDK 的一个子模块,而 Ti RTOS SDK 是 Ti 公司针对其产品提供的开发工具链, 包含完整的操作系统。J721E 产品系列的 SDK 可以在这里下载。
- 为 Openvx 能在 Ti 的多核平台上运行提供了一套解决方案。
- 提供了适用于 Ti 产品的获取图象、输出图象的 Kernel。
- 支持部署、运行部分深度学习模型。
实际上 Tiovx 还依赖另一个模块 vxlib。vxlib 负责 Tiovx 中各 Kernel 的执行函数的底层实现,与硬件强相关 (包含非常多硬件指令的调用)。
Tiovx 还支持 PC 平台 (Ubuntu 或 Windows) 仿真。SDK 通常包含用于 PC 平台仿真的、已经编译好的 Tiovx 库及 vxlib 库,如果不包含可以通过编译整个 SDK 来获得。如果觉得编译 SDK 比较麻烦,也可以自行构建 makefile 工程或 cmake 工程单独编译这两个库。
注:根据官方说法,当前的 Tiovx 只支持 Openvx 1.1 版本,缺失很多与 Tensor 对象相关的 API。
2 基本概念
在 OpenVX 中,有一个关键词叫做 kernel,这里的 kernel 并不是指操作系统的 kernel,而是指 OpenVX 中的一种功能,比如对一个图片进行高通滤波的功能就是 OpenVX 的一个功能,这在 OpenVX 里面叫做一个 user kernel。
既然 kernel 是一种提供给用户使用的功能,那么这个功能函数一般支持传参,通过参数的不同来改变 kernel 的运行结果(如传入的图片就算一个参数)。
在 OpenVX 中,把参数定义初始化好后的 kernel 叫做 node,因此,node 就是 kernel 的一个实例化-----即拥有指定参数的 kernel。node 可以在用任何语言编码的硬件或处理器上。例如,在 GPU 上,node 可以在 OpenCL 中实现。
vx_context、vx_graph、vx_node,这三个概念是OpenVX的应用基础,每个进程内可以有多个context(上下文),每个context内可以有多个graph(图,或连接关系),每个graph内可以有多个node(节点)。
一个node就是一个最小的调度单元,可以是图像预处理算法,可以是边缘检测算法;一个graph就是一个功能,是由多个步骤连接在一起的完整功能;
一个context就是一个运行环境,包含多种不同的功能,在不同场景下被调度。一般很少有使用到多context的场景。
使用方法如下:
vx_context context = vxCreateContext();
vx_graph graph = vxCreateGraph(context);
//参数格式对于不同的node会不相同
vx_node node = createXXXXnode(graph, CONFIG, INPUT, OUTPUT);
//释放过程
releaseXXXXNode(node);
vxReleaseGraph(graph);
vxRealeaseContext(context);
图 (Graph 对象) 是 Openvx 框架的核心:
- 一个图代表一个图像处理流程。
- 图中的每个节点 (Node 对象) 对应图像处理流程中的一个步骤。在代码中通常会将每个步骤封装成一个个函数,于是图中的每个节点对应代码中的一个函数调用。
- 根据图的结构决定每个节点的运行顺序。
3 简单示例
An OpenVX “Hello, World !!!” Program
// init
vx_context context = vxCreateContext();//创建openvx上下文
vx_uint32 width = 1024, height = 760;
vx_graph graph = vxCreateGraph( context );//基于上下文创建Graph
vx_image input = vxCreateImage( context, width, height, VX_DF_IMAGE_RGB );//基于上下文创建image变量
vx_image output = vxCreateImage( context, width, height, VX_DF_IMAGE_NV12 );
vx_image intermediate = vxCreateVirtualImage( graph, width, height, VX_DF_IMAGE_U8 );//在图中创建虚拟image变量
vx_node n1 = vxColorConvertNode( graph, input, intermediate );
vx_node n2 = vxChannelExtractNode( graph, intermediate, 0, output );
vxVerifyGraph( graph );//校验graph中的node是否正常
vxProcessGraph( graph );//运行graph,这里会将刚才链接进来的所有Node的func执行一次
3.1 Graph的构建
用户需要 (通过相关的 API 调用) 描述每个节点 (节点执行时实际运行某个特定的函数) 依赖的输入数据对象 (该特定函数的输入参数) 及输出数据对象 (该特定函数的输出参数),Openvx 根据不同节点对数据对象的依赖关系自动完成Graph的构建。
在上面的简单例子中构建了一个包含两个节点的Graph,代码实际上只描述了节点的依赖关系:
1.节点 n1 依赖输入数据 input,并将输出数据写入 intermediate。
2.节点 n2 依赖输入数据 intermediate,并将输出数据写入 output。
3.调用 vxVerifyGraph() 函数时会构建整个Graph并进行验证。由于 n1 的输出为 n2 的输入,故在Graph中 n2 为 n1 的子节点,连接两个节点的桥梁为 intermediate。
大致如下 (为了方便描述关系,将 intermediate 等数据也表示在Graph中):
其中每一步骤的含义如下:
1.Context 是 Openvx 中的一种对象,负责“管理”各种资源。根据 Tiovx 源码: Context 对象会维护一个对象列表,通过该 Context 对象生成的其它对象会加入该列表中,当析构 Context 时,会递归该对象列表并析构列表中的对象;Context 同时也维护一个 Kernel 对象列表,每个 Kernel 对象代表一个该 Context 对象可支持的操作 (Kernel 对象是 Node 对象的“原型”)。
2.input: 分辨率为 1024* 760 的 RGB 图像。intermediate: 分辨率为 1024* 760 的 NV12 图像。output: 分辨率为 1024* 760 、每个像素数据为 8 位无符号数的图像。
3.节点 n1 表示图像格式转换操作,这里是将图像从 RGB 格式转换为 NV12 格式。n1 表示图像通道数据提取操作,这里是提取 intermediate 图像通道 0 的数据 (根据 Openvx 的定义,NV12 图像通道 0 存储的是亮度数据)。于是构建的 graph 代表的图像处理流程是 RGB 转灰度图。
3.2 Graph的验证
前面的过程全部都是在构造graph里面node的连接关系,当graph构造完成后,即可调用 vxVerifyGraph() 函数进行图的验证,交由OpenVX后端去检查参数是否合法,一个Graph运行前必须通过验证。如果没有验证直接调用 vxProcessGraph() 或 vxScheduleGraph(),则会自动调用 vxVerifyGraph() 进行验证。
除了上面提到的Graph的构建,验证过程进行以下操作 (包括但不限于):
- Graph的构建。确定Graph的拓扑结构。
- 判断Graph是否为有向无环图。
- 判断是否存在多个节点对同一个数据对象的输出。
- 对于多 CPU 平台,会根据设定将节点分配给各个 CPU。对于 Tiovx,则是通过 IPC 协议在目标 CPU 的进程或任务中建立相应的变量。
- 检验每个节点的输入输出是否满足要求。例如对 vxAndNode() 函数创建的节点 (两张图像的与操作),会检验输入输出的长、宽、图像类型是否一致。
- 确定各个虚拟数据对象的参数。 (虚拟数据对象是一类特殊的数据对象, 所有参数以及数据存储空间仅在图的运行过程中有意义,与Graph绑定,不能在Graph之间共享)
- 初始化各个节点。确定输出图像的 ROI、动态申请必要的内存。
- 为所有数据对象 (包括虚拟数据对象) 分配存储数据的空间。(在 Openvx 的设计中,所有数据对象创建时不会分配存储数据的空间。除了图验证过程,主动调用相应的访问 API 也会为数据对象分配存储数据的空间。)
Graph在验证后,除非更改了Graph的拓扑结构,否则不需要再次验证。
在节点执行的过程中,不需要再次判断输入输出参数是否满足条件。例如 vxAndNode() 函数创建的节点执行时,对应的函数内部可以直接默认输入图像和输出图像的长、宽、图像类型均一致。这样可以省去大量校验参数的时间开销。
3.3 Graph的运行
调用 vxVerifyGraph() 函数对Graph进行验证后,如果合法,即可调用vxProcessGraph函数,交由OpenVX后端将任务分发给特定的加速器和异构核心,等待全部计算完成后即函数返回。
如上图所示,系统在创建阶段处于IDLE状态,待所有的Graph和node创建完毕后,即可通过vxProcessGraph函数启动Graph的运行,进入运行状态,运行完成后即返回。也可以通过pipeline的API启动graph,此时Graph会一直保持运行状态,由输入数据驱动,直到用户通过API停止Graph。在停止Graph后,可通过API释放资源销毁Graph。
执行图的方式有两种:
/* 方式一 */
/* 会阻塞程序直至Graph执行结束 */
vxProcessGraph(graph);
/* 方式二 */
/* 函数会直接返回,不会阻塞程序 */
vxScheduleGraph(graph);
/* 阻塞程序直至Graph执行结束 */
vxWaitGraph(graph);
/* vxProcessGraph() 函数的调用,相当于依次调用 vxScheduleGraph() 和 vxWaitGraph() */
在上面的简单例子中是采用了方式一.
一般来说,Graph的执行顺序如下图所示:
运行流程:
1.找到所有Graph的所有头节点 (没有父节点的节点),这些节点满足执行条件 (因为节点对应的函数的输入参数已经确定)。运行这些头节点,当然这些节点执行的先后顺序是不定的。
2.当某个节点执行完毕后,会遍历该节点的所有子节点。如果某个节点的所有父节点 (一个节点可以同时是多个节点的子节点) 均执行完毕,则运行该子节点。一个节点的所有父节点执行结束,表示该节点对应函数的输入参数已经确定。为了便于确定某个节点的父节点是否执行完毕,Openvx 要求节点执行完毕时标记其所有输出,则如果某个节点的所有输入均被标记,即可认为该节点可执行。
3.重复步骤 2 直至所有节点运行完毕。为了保证步骤 2 能结束,Openvx 要求Graph必须是有向无环图,在这个要求下,每个节点均会执行且总共只执行一次。
4 Graph和Pipeline的区别和对比
1)Pipeline的本质:通过分时复用的方式,提高了硬件的整体利用率,但在本质上,运行单条流水线和运行一次graph的时间是相同的。
2)原有的Graph,如果执行vxProcessGraph,那么graph一旦运行起来,就不允许被打断;且同一个时刻,只有一个核参与整体的graph的处理过程。
3)但是Pipeline,可以实现在graph运行过程中,对关心的数据即通过(graph_parameters_queue_params_list)进行调整,比如图片替换、复制等等。
4)同时,Pipeline还允许不同的核在同一个时间,参与到不同流水线的处理过程中。整体上提升了硬件的利用效率。
Graph | Pipeline | |
---|---|---|
是否可打断 | 不可以 | 可以(通过graph_parameters_queue_params_list) |
硬件同时运行 | 不可以 | 可以(同一个时刻,允许多个核同时运行不同的pipeline的处理) |
4.1 Node 和 Graph 参数定义
1.Graph 参数:在 pipelining 模式下识别用户可排队在一个 graph 中的参数。通过创建一个Graph参数并显式地从 graph 中进入和离开该参数,应用程序就能够访问对象数据。否则,当使用流水线时,非 graph 参数在 graph 执行期间是不可访问的。
2.Node 参数:在 TIOVX 实现中,应用使用 node 参数来标识在流水线的情况下需要在何处创建多个缓冲区。
4.2 数据流调度框架 (流水线)
在异构多核处理器中,流水线调度是提高系统并行度非常重要的一点。传统架构下,ARM大核+小核的SMP(对称多处理器)架构中运行的是一个OS,在这个OS里可以通过多线程的方法,将多个核心充分利用起来。如下图的两种方式:
左图表示的是对于单一功能,多线程可以通过数据并行的方式加快计算速度,但是任务分发和同步会带来性能开销。右图表示的是对于无法数据并行的处理任务,可以使用多线程的方式多个核心流水线的方式提高系统的并行度。这样系统的运行帧率取决于流水线中最慢的一个环节,而不是所有环节运行时间之和。
同样,在异构多核的处理器中,也需要流水线方式的并行加速。OpenVX提供了流水线(pipeline)的配置API,可以开启自动化的调度。流水线调度可以将传统编程方法中,一个线程从头到尾依次执行的速度大幅提高,不会造成加速器的闲置。
当OpenVX打开流水线开关后,OpenVX会自动在多个硬件加速器和不同核心之间建立流水线调度机制,每两个node之间会创建一个队列,用于缓冲流水线之间的速度差异。
其中在流水线处理中涉及到几个比较重要的API如下:
1.tivxSetNodeParameterNumBufByIndex: 在开发流水线应用时,必须在流水线之间创建多个缓冲区,以避免流水线中的任何停顿。根据 graph 中节点的延迟情况,需要针对特定的应用调整缓冲区的数量。这个 API 允许 graph 开发人员指定 框架在给定节点参数处内部使用的缓冲区数量。提供给这个 API 的节点参数必须是输出类型。此外,提供给该 API 的节点参数必须不能作为 graph 参数创建。要创建如下图所示的场景,使用 tivxSetNodeParameterNumBufByIndex 将 Node1 输出的缓冲区数设置为N。
2.tivxSetGraphPipelineDepth: 这个 API 允许应用程序开发人员显式地设置给定 graph 的管道深度。默认情况下,该值设置为 1。然而,为了获得充分的硬件利用率,需要根据 graph 配置设置管道深度值。考虑下图中所示的基本图表。这张图在 SoC 上使用了 3 个核心:一个 ISP, DSP 和 CPU。
如果没有流水线,一个新的 graph 执行将不能开始,直到最初的 graph 执行完成。然而,因为这里面的每个内核都可以并发运行,这个 graph 的执行并没有考虑到最佳的硬件利用率。TIOVX 流水线实现考虑到基于管道深度在内部创建多个 graph 实例来充分利用硬件。因此,这些 graph 都将同时执行,这样每个核心都在积极地处理。tivxSetGraphPipelineDepth 允许应用开发人员告诉框架要创建多少个这个 graph 的内部 instance。(注意:即使一个节点实现可以在多个核上运行,graph 的内部 instance 也被限制为跨所有 instance 的同一个核。)此值的最佳值必须基于给定的 graph 配置进行调整。下图显示了这种情况的图解说明,如果上面显示的基本 graph 被设置为 3,框架将把图形当作有 3 个 instance 同时处理相同的 graph。下图显示了 T=2 时的图形处理,每个处理单元都是活动的。
除了设置流水线深度之外,上面所示的基本 graph 还可以使用 tivxSetNodeParameterNumBufByIndex API 在节点的输出处设置多个缓冲区。下图显示了当每个输出的 tivxSetNodeParameterNumBufByIndex 设置为 4 时,框架如何处理基本图形。如图所示,节点将写入缓冲区队列中的缓冲区,而下游节点正在处理缓冲区队列中的前一个缓冲区。
当使用 TIOVX 设计流水线 graph 时,在实现 tivxSetGraphPipelineDepth 和 tivxSetNodeParameterNumBufByIndex API 时需要进行权衡。虽然性能会随着这些 API 设置的值的增加而增加,但使用的内存也会增加。对于 tivxSetGraphPipelineDepth API,如果管道深度值设置得太高,可能会创建不必要的内部创建的图结构。类似地,如果 tivxSetNodeParameterNumBufByIndex API 创建的缓冲区多于所需的,那么框架内用于额外缓冲区的内存也将是不必要的。TI 对应用程序开发的建议是,首先在初始开发时使用较大的管道深度和缓冲区数量,然后减少这些值,直到观察到性能下降。
4.3 例子
为什么要引入缓冲区?
高速设备与低速设备的不匹配,势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区。
缓冲区的作用:
1.可以解除两者的制约关系:数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。(例如:我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情)
2.可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写需要花上较多时间,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。(例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉。)
简单来说,缓冲区就是一块内存区,它用在输入输出设备和 CPU 之间,用来存储数据。它使得低速的输入输出设备和高速的 CPU 能够协调工作,避免低速的输入输出设备占用 CPU,解放出 CPU,使其能够高效率工作。
5 自定义节点
可参考博文https://blog.csdn.net/j000007/article/details/132780122?spm=1001.2014.3001.5502
5.1 host端注册
1.在app_init函数中完成host端的kernel注册:tivxMytiovxLoadKernels( )
5.2 target端注册
Target端跑在MCU2_0上,所以要找到跑在MCU2_0的demo,在哪个核跑就找到对应的Demo。
在appRegisterOpenVXTargetKernels函数中添加tivxRegisterMytiovxTargetIpuKernels函数
5.3 创建Node并添加进Graph,然后运行起来
在app_create_graph函数中添加app_create_graph_mytiovx
再在app_create_graph_mytiovx中创建节点tivxmytiovxtestNode( )
执行以上三步,即可完成Host端/Target端的Kernel注册;完成Node的创建。
6 DMPAC光流模块Demo源码解析
6.1 总体流程
主要的流程有:读取参数配置文件并设置相关参数, 程序初始化操作, 构建Graph操作, 执行Graph操作以及删除Graph和释放资源操作.其中最重要的是程序初始化操作, 构建Graph操作以及执行Graph操作, 这里重点讲一下这三部分的源码.
6.2 程序初始化操作
首先是程序初始化操作部分,这部分主要是干三件事:
1.创建上下文context
2.host端的kernel注册
3.相应模块的初始化, 根据读取的参数创建相对应的输入,输出以及中间变量
static vx_status app_init(AppObj *obj)
{
app_grpx_init_prms_t grpx_prms;
vx_status status = VX_SUCCESS;
//1.创建上下文context
obj->context = vxCreateContext();
if(status == VX_SUCCESS)
{
status = vxGetStatus((vx_reference) obj->context);
}
//2.host端的kernel注册
tivxHwaLoadKernels(obj->context);
//3.相应模块的初始化, 根据读取的参数创建相对应的输入,输出以及中间变量
if(status == VX_SUCCESS)
{
//对图像进行金字塔处理的初始化操作
status = app_init_pyramid(obj->context, &obj->pyramidObj, "pyramidObj", MAX_NUM_BUF );
}
if(status == VX_SUCCESS)
{
//进行稠密光流处理的初始化操作
status = app_init_dofproc(obj->context, &obj->dofprocObj, "dofprocObj");
}
if(status == VX_SUCCESS)
{
//进行光流结果可视化的初始化操作
status = app_init_dofviz( obj->context, &obj->dofvizObj , "dofvizObj");
}
if(status == VX_SUCCESS)
{
status = app_init_display1( obj->context, &obj->displayObj , "display1Obj");
}
if(status == VX_SUCCESS)
{
status = app_init_display2( obj->context, &obj->displayObj , "display2Obj");
}
if (1 == obj->displayObj.display_option)
{
appGrpxInitParamsInit(&grpx_prms, obj->context);
grpx_prms.draw_callback = app_draw_graphics;
appGrpxInit(&grpx_prms);
}
return status;
}
//对图像进行金字塔处理的初始化操作
vx_status app_init_pyramid(vx_context context, PyramidObj *pyramidObj, char *objName,vx_int32 bufq_depth)
{
vx_status status = VX_SUCCESS;
vx_pyramid pyr_exemplar;
uint32_t buf_id;
//根据缓冲区数量创建对应数量的vximage作为输入图像
for(buf_id=0; buf_id<bufq_depth; buf_id++)
{
pyramidObj->input_img[buf_id] = vxCreateImage(context, pyramidObj->width, pyramidObj->height, pyramidObj->in_vx_df_image);
APP_ASSERT_VALID_REF(pyramidObj->input_img[buf_id]);
}
//根据设置参数创建图像金字塔
pyr_exemplar = vxCreatePyramid(context,
pyramidObj->dof_levels, VX_SCALE_PYRAMID_HALF,
pyramidObj->width, pyramidObj->height,
pyramidObj->in_vx_df_image);
APP_ASSERT_VALID_REF(pyr_exemplar);
//创建大小为2的延迟对象
pyramidObj->pyr_delay = vxCreateDelay(context, (vx_reference)pyr_exemplar, 2);
APP_ASSERT_VALID_REF(pyramidObj->pyr_delay);
//延迟对象在[-count+1,0]范围内。0始终是当前对象。
//在延迟对象中索引为0是当前对象,索引为-1为上一个对象
pyramidObj->pyr_ref = (vx_pyramid)vxGetReferenceFromDelay(pyramidObj->pyr_delay, -1);
APP_ASSERT_VALID_REF(pyramidObj->pyr_ref);
//此函数用于将名称与被引用的对象关联
vxSetReferenceName((vx_reference)pyramidObj->pyr_ref, "PyramidReference");
pyramidObj->pyr_cur = (vx_pyramid)vxGetReferenceFromDelay(pyramidObj->pyr_delay, 0);
APP_ASSERT_VALID_REF(pyramidObj->pyr_cur);
vxSetReferenceName((vx_reference)pyramidObj->pyr_cur, "PyramidCurrent");
/* exemplar not needed any more */
vxReleasePyramid(&pyr_exemplar);
return status;
}
//稠密光流计算的初始化操作
vx_status app_init_dofproc(vx_context context, DofProcObj *dofprocObj, char *objName)
{
vx_status status = VX_SUCCESS;
tivx_dmpac_dof_params_init(&dofprocObj->dof_params);
if(dofprocObj->enable_temporal_predicton_flow_vector == 0)
{
dofprocObj->dof_params.base_predictor[0] = TIVX_DMPAC_DOF_PREDICTOR_DELAY_LEFT;
dofprocObj->dof_params.base_predictor[1] = TIVX_DMPAC_DOF_PREDICTOR_PYR_COLOCATED;
dofprocObj->dof_params.inter_predictor[0] = TIVX_DMPAC_DOF_PREDICTOR_DELAY_LEFT;
dofprocObj->dof_params.inter_predictor[1] = TIVX_DMPAC_DOF_PREDICTOR_PYR_COLOCATED;
}
//创建用户数据对象,它可以用于将用户内核定义的数据结构或内存块作为参数传递给用户内核。
dofprocObj->dof_config = vxCreateUserDataObject(context, "tivx_dmpac_dof_params_t",
sizeof(tivx_dmpac_dof_params_t), NULL);
APP_ASSERT_VALID_REF(dofprocObj->dof_config);
vxSetReferenceName((vx_reference)dofprocObj->dof_config, "DofConfiguration");
//参数设置
dofprocObj->dof_params.vertical_search_range[0] = 48;
dofprocObj->dof_params.vertical_search_range[1] = 48;
dofprocObj->dof_params.horizontal_search_range = 191;
dofprocObj->dof_params.median_filter_enable = 1;
dofprocObj->dof_params.motion_smoothness_factor = 12;
dofprocObj->dof_params.motion_direction = 1; /* 0: for side camera */
status = vxCopyUserDataObject(dofprocObj->dof_config, 0, sizeof(tivx_dmpac_dof_params_t), &dofprocObj->dof_params, VX_WRITE_ONLY, VX_MEMORY_TYPE_HOST);
APP_ASSERT(status==VX_SUCCESS);
uint32_t buf_id, num_buf;
num_buf = MAX_NUM_BUF;
if(dofprocObj->enable_temporal_predicton_flow_vector)
{
vx_image flow_vector_field_exemplar;
flow_vector_field_exemplar = vxCreateImage(context, dofprocObj->width, dofprocObj->height, VX_DF_IMAGE_U32);
APP_ASSERT_VALID_REF(flow_vector_field_exemplar);
// 创建一个长度为2的delay对象,内部每个元素都会从exemplar中进行元拷贝(数据内容会被忽略)
dofprocObj->flow_vector_field_delay = vxCreateDelay(context, (vx_reference)flow_vector_field_exemplar, 2);
APP_ASSERT_VALID_REF(dofprocObj->flow_vector_field_delay);
// 返回delay中第index个reference
dofprocObj->flow_vector_field_in = (vx_image)vxGetReferenceFromDelay(dofprocObj->flow_vector_field_delay, -1);
APP_ASSERT_VALID_REF(dofprocObj->flow_vector_field_in);
vxSetReferenceName((vx_reference)dofprocObj->flow_vector_field_in, "FlowVectorIn");
dofprocObj->flow_vector_field_out = (vx_image)vxGetReferenceFromDelay(dofprocObj->flow_vector_field_delay, 0);
APP_ASSERT_VALID_REF(dofprocObj->flow_vector_field_out);
vxSetReferenceName((vx_reference)dofprocObj->flow_vector_field_out, "FlowVectorOut");
vxReleaseImage(&flow_vector_field_exemplar);
}
else
{
dofprocObj->flow_vector_field_delay = NULL;
dofprocObj->flow_vector_field_in = NULL;
dofprocObj->flow_vector_field_out = vxCreateImage(context, dofprocObj->width, dofprocObj->height, VX_DF_IMAGE_U32);
APP_ASSERT_VALID_REF(dofprocObj->flow_vector_field_out[buf_id]);
vxSetReferenceName((vx_reference)dofprocObj->flow_vector_field_out, "FlowVectorOut");
}
return status;
}
//光流结果可视化的初始化操作
vx_status app_init_dofviz(vx_context context, DofVizObj *dofvizObj, char *objName)
{
vx_status status = VX_SUCCESS;
uint32_t buf_id, num_buf;
vx_uint32 confidence_threshold_value = 9; /* valid range, 0 (low confidence) .. 15 (high confidence) */
num_buf = MAX_NUM_BUF;
//根据缓冲区数量创建对应数量的flow img和conf img
for(buf_id=0; buf_id<num_buf; buf_id++)
{
dofvizObj->flow_vector_field_img[buf_id] = vxCreateImage(context, dofvizObj->width, dofvizObj->height, VX_DF_IMAGE_RGB);
APP_ASSERT_VALID_REF(dofvizObj->flow_vector_field_img[buf_id]);
dofvizObj->confidence_img[buf_id] = vxCreateImage(context, dofvizObj->width, dofvizObj->height, VX_DF_IMAGE_U8);
APP_ASSERT_VALID_REF(dofvizObj->confidence_img[buf_id]);
}
dofvizObj->confidence_threshold = vxCreateScalar(context, VX_TYPE_UINT32, &confidence_threshold_value);
APP_ASSERT_VALID_REF(dofvizObj->confidence_threshold);
vxSetReferenceName((vx_reference)dofvizObj->confidence_threshold, "ConfidenceThreshold");
return status;
}
6.3 构建Graph
这部分是整个程序的核心, 主要定义了三个节点, 分别是高斯金字塔节点, Dmpac稠密光流节点, 稠密光流可视化节点, 并分别在VPAC, DMPAC, DSP硬件加速器上运行.输入为RGB图像, 输出为光流结果伪彩图以及光流结果置信度图, 这将构建成一张Graph, 构建完成等待执行命令启动,由于采用的是流水的处理方式, 当启动后Graph会一直保持运行状态,由输入数据驱动,直到用户发送停止命令停止Graph。在停止Graph后,释放资源销毁Graph。
//构建图操作
static vx_status app_create_graph(AppObj *obj)
{
uint32_t num_buf, pipeline_depth;
vx_graph_parameter_queue_params_t graph_parameters_queue_params_list[3];
vx_status status = VX_SUCCESS;
num_buf = MAX_NUM_BUF;
pipeline_depth = MAX_NUM_BUF;
//构建一个顶层的graph
obj->graph = vxCreateGraph(obj->context);
status = vxGetStatus((vx_reference) obj->graph);
if(status == VX_SUCCESS)
{
status = app_create_graph_display2(obj->graph,
&obj->displayObj,
obj->pyramidObj.input_img[0]);
}
if (status == VX_SUCCESS)
{
//创建高斯金字塔节点
status = app_create_graph_pyramid(obj->graph,
&obj->pyramidObj,
obj->pyramidObj.input_img[0],
obj->pyramidObj.pyr_ref );
}
if (status == VX_SUCCESS)
{
//创建稠密光流计算节点
status = app_create_graph_dofproc(obj->graph,
&obj->dofprocObj,
obj->dofprocObj.dof_config,
obj->pyramidObj.pyr_cur,
obj->pyramidObj.pyr_ref,
obj->dofprocObj.flow_vector_field_in,
obj->dofprocObj.flow_vector_field_out );
}
if (status == VX_SUCCESS)
{
//创建稠密光流结果可视化节点
status = app_create_graph_dofviz(obj->graph,
&obj->dofvizObj,
obj->dofprocObj.flow_vector_field_out,
obj->dofvizObj.confidence_threshold,
obj->dofvizObj.flow_vector_field_img[0],
obj->dofvizObj.confidence_img[0]);
}
if (status == VX_SUCCESS)
{
status = app_create_graph_display1(obj->graph, &obj->displayObj, obj->dofvizObj.flow_vector_field_img[0]);
}
if (status == VX_SUCCESS)
{
/*
此函数通过图表[REQ-0619]登记要自动老化的延迟对象。每次成功完成该图[REQ-0620]后,该延迟对象将自动老化。
在图形执行期间无法调用延迟对象的老化。由于节点回调而放弃的图将触发自动老化[REQ-0621]。
如果在同一图表中多次登记了用于自动老化的延迟,则在每次图表完成时延迟将仅老化一次[REQ-0622]。
如果在多个图形中登记了自动老化的延迟,则该延迟将在每次成功完成这些图形后自动老化[REQ-0623]。
*/
status = vxRegisterAutoAging(obj->graph, obj->pyramidObj.pyr_delay);
}
if(obj->dofprocObj.enable_temporal_predicton_flow_vector && (status == VX_SUCCESS))
{
status = vxRegisterAutoAging(obj->graph,obj->dofprocObj.flow_vector_field_delay);
}
int graph_parameter_num = 0;
// 设置Graph参数
if(status == VX_SUCCESS)
{
//将高斯金字塔节点中的输入图像参数添加到Graph参数中,后续应用程序才可以访问该数据
status = add_graph_parameter_by_node_index(obj->graph, obj->pyramidObj.node, 0);
obj->input_img_graph_parameter_index = graph_parameter_num;
//graph_parameters_queue_params_list 必须被设置,这个列表表示在graph运行过程中可以被访问对象的索引。
graph_parameters_queue_params_list[graph_parameter_num].graph_parameter_index = graph_parameter_num;
graph_parameters_queue_params_list[graph_parameter_num].refs_list_size = num_buf;
graph_parameters_queue_params_list[graph_parameter_num].refs_list = (vx_reference*)&obj->pyramidObj.input_img[0];
graph_parameter_num++;
}
if(status == VX_SUCCESS)
{
//将光流结果可视化节点中的伪彩色图像参数添加到Graph参数中
status = add_graph_parameter_by_node_index(obj->graph, obj->dofvizObj.node, 2);
obj->flow_vector_field_img_graph_parameter_index = graph_parameter_num;
graph_parameters_queue_params_list[graph_parameter_num].graph_parameter_index = graph_parameter_num;
graph_parameters_queue_params_list[graph_parameter_num].refs_list_size = num_buf;
graph_parameters_queue_params_list[graph_parameter_num].refs_list = (vx_reference*)&obj->dofvizObj.flow_vector_field_img[0];
graph_parameter_num++;
}
if(status == VX_SUCCESS)
{
//将光流结果可视化节点中的置信度图像参数添加到Graph参数中
status = add_graph_parameter_by_node_index(obj->graph, obj->dofvizObj.node, 3);
obj->confidence_img_graph_parameter_index = graph_parameter_num;
graph_parameters_queue_params_list[graph_parameter_num].graph_parameter_index = graph_parameter_num;
graph_parameters_queue_params_list[graph_parameter_num].refs_list_size = num_buf;
graph_parameters_queue_params_list[graph_parameter_num].refs_list = (vx_reference*)&obj->dofvizObj.confidence_img[0];
graph_parameter_num++;
}
if(status == VX_SUCCESS)
{
//用于设置Graph的调度程序配置,以允许用户调度一个Graph的多个实例以供执行。
//设置Graph运行模式,设置好相关的参数,采用自动调度的排队模式进行流水线操作
status = vxSetGraphScheduleConfig(obj->graph,
VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO,
graph_parameter_num,
graph_parameters_queue_params_list);
}
if(status == VX_SUCCESS)
{
//这个API允许应用程序开发人员显式地设置给定graph的pipeline深度
//允许应用开发人员告诉框架要创建多少个这个 graph 的内部 instance
//将流水深度设置为3,相当于创建了3个graph实例来充分利用硬件
status = tivxSetGraphPipelineDepth(obj->graph, pipeline_depth);
}
if(status == VX_SUCCESS)
{
//在开发流水线应用时,必须在流水线之间创建多个缓冲区,以避免流水线中的任何停顿。
//将金字塔节点的输出参数(金字塔图像)设置大小为3的缓冲区
status = tivxSetNodeParameterNumBufByIndex(obj->pyramidObj.node, 1, num_buf);
}
if(status == VX_SUCCESS)
{
//将光流计算节点的输出参数(光流结果)设置大小为3的缓冲区
status = tivxSetNodeParameterNumBufByIndex(obj->dofprocObj.node, 8, num_buf);
}
if(status == VX_SUCCESS)
{
//进行Graph的验证
status = vxVerifyGraph(obj->graph);
}
appPerfPointSetName(&obj->total_perf , "TOTAL");
appPerfPointSetName(&obj->fileio_perf, "FILEIO");
return status;
}
//创建高斯金字塔节点
vx_status app_create_graph_pyramid(vx_graph graph, PyramidObj *pyramidObj, vx_image input_img, vx_pyramid output_pyramid )
{
vx_status status = VX_SUCCESS;
//创建高斯金字塔节点
pyramidObj->node = vxGaussianPyramidNode( graph, input_img , output_pyramid);
//将节点目标设置为提供的值。成功将使节点所属的图形无效(在下次执行之前必须调用vxVerifyGraph)
APP_ASSERT_VALID_REF(pyramidObj->node);
//设定高斯金字塔节点在VPAC上运行
status = vxSetNodeTarget(pyramidObj->node, VX_TARGET_STRING, TIVX_TARGET_VPAC_MSC1);
APP_ASSERT(status==VX_SUCCESS);
vxSetReferenceName((vx_reference)pyramidObj->node, "GaussianPyramid");
return status;
}
//创建稠密光流计算节点
vx_status app_create_graph_dofproc(vx_graph graph, DofProcObj *dofprocObj,
vx_user_data_object configuration,
vx_pyramid input_current_base,
vx_pyramid input_reference_base,
vx_image flow_vector_in,
vx_image flow_vector_out )
{
vx_status status = VX_SUCCESS;
//创建稠密光流计算节点
dofprocObj->node = tivxDmpacDofNode(
graph,
configuration,
NULL,
NULL,
input_current_base,
input_reference_base,
flow_vector_in,
NULL,
NULL,
flow_vector_out,
NULL);
APP_ASSERT_VALID_REF(dofprocObj->node);
//设定稠密光流计算节点在DMPAC上运行
status = vxSetNodeTarget(dofprocObj->node, VX_TARGET_STRING, TIVX_TARGET_DMPAC_DOF);
APP_ASSERT(status==VX_SUCCESS);
vxSetReferenceName((vx_reference)dofprocObj->node, "DenseOpticalFlow");
return status;
}
//创建光流结果可视化节点
vx_status app_create_graph_dofviz(vx_graph graph, DofVizObj *dofvizObj,
vx_image flow_vector,
vx_scalar confidence_threshold,
vx_image flow_vector_rgb,
vx_image confidence_image)
{
vx_status status = VX_SUCCESS;
//创建光流结果可视化节点
dofvizObj->node = tivxDofVisualizeNode(
graph,
flow_vector,
confidence_threshold,
flow_vector_rgb,
confidence_image);
APP_ASSERT_VALID_REF(dofvizObj->node );
//设定光流结果可视化节点在DSP上运行
status = vxSetNodeTarget(dofvizObj->node , VX_TARGET_STRING, TIVX_TARGET_DSP1);
APP_ASSERT(status==VX_SUCCESS);
vxSetReferenceName((vx_reference)dofvizObj->node , "DofVisualize");
return status;
}
6.4 执行Graph(流水方式)
当上一步将整张Graph构建完毕后, 将按照流水的方式来执行Graph, 由输入数据驱动, 节点之间的缓冲区为3, 流水的深度也为3, 因此创建了3个graph实例, 这个3个graph都将同时执行, 通过分时复用的方式充分的利用了硬件资源,可以看到:
当t=1时, Graph Instance0中的vxGaussianPyramidNode的输入为input img Buf1, 并输出pyramid img Buf1;
当t=2时,Graph Instance0中的tivxDmpacDofNode的输入为pyramid img Buf1,并输出flow vector Buf1, 与此同时Graph Instance1中vxGaussian-PyramidNode同时在处理input img Buf2, 并输出pyramid img Buf2;
当t=3时,Graph Instance0中的tivxDofVisualizeNode的输入为flow vector Buf1, 并输出flow img Buf1和conf img Buf1, 与此同时Graph Instance1中tivxDmpacDofNode同时处理pyramid img Buf2, 并输出flow vector Buf2, Graph Instance2中vxGaussianPyramidNode也同时处理input img Buf3, 并输出 pyramid img Buf3.
其中当一个Buf被用完后, 就会重新加入一个新的Buf, 只要有输入数据, 就会如此循环往复, 每一个处理单元都是处于活动状态, 充分地利用硬件资源, 提高了运行效率.
static vx_status app_run_graph(AppObj *obj)
{
char input_file_name[APP_MAX_FILE_PATH];
char output_file_name_flow_img[APP_MAX_FILE_PATH];
char output_file_name_conf_img[APP_MAX_FILE_PATH];
uint32_t curFileNum;
int32_t outputFileNum;
uint32_t iterations = 1;
vx_status status = VX_SUCCESS;
uint32_t num_buf;
num_buf = MAX_NUM_BUF;
int32_t pipeline = -num_buf;
int32_t enqueueCnt = 0;
vx_int32 img_array_idx = -1;
/* create output directory is not already existing */
mkdir(obj->output_file_path, S_IRWXU | S_IRWXG | S_IRWXO);
/* run pyramid only for 1st frame */
//对第一帧图像先进行搞死金字塔图像处理
status = app_run_pyramid_for_first_frame(obj);
/* run DOF for 2nd frame onwards */
vx_uint32 test_counter = 0;
for(curFileNum = obj->start_fileno+1; curFileNum <= obj->end_fileno; curFileNum++)
{
if(obj->stop_task)
{
break;
}
appPerfPointBegin(&obj->total_perf);
snprintf(input_file_name, APP_MAX_FILE_PATH, "%s/%s%05d%s.%s",
obj->input_file_path,
obj->input_file_prefix,
curFileNum,
obj->input_file_postfix,
obj->in_file_ext
);
if(pipeline < 0)
{
/* Enqueue outpus */
if (status == VX_SUCCESS)
{
//将flow img逐个入队到Graph参数中
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->flow_vector_field_img_graph_parameter_index, (vx_reference*)&obj->dofvizObj.flow_vector_field_img[enqueueCnt], 1);
if (status == VX_SUCCESS)
{
//将conf img逐个入队到Graph参数中
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->confidence_img_graph_parameter_index, (vx_reference*)&obj->dofvizObj.confidence_img[enqueueCnt], 1);
}
}
if(0 == obj->is_interactive)
{
printf(" %d of %d: Loading [%s] ...\n", curFileNum, obj->end_fileno, input_file_name);
}
appPerfPointBegin(&obj->fileio_perf);
/* Read input */
if (status == VX_SUCCESS)
{
//读取图像
status = app_load_vximage_from_file(obj->pyramidObj.input_img[enqueueCnt], input_file_name, obj->in_file_format);
}
printf("app_load_vximage_from_file status: %d\n",status);
appPerfPointEnd(&obj->fileio_perf);
/* Enqueue input - start execution */
if (status == VX_SUCCESS)
{
//将图像逐个入队到Graph参数中
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->input_img_graph_parameter_index, (vx_reference*)&obj->pyramidObj.input_img[enqueueCnt], 1);
}
enqueueCnt++;
enqueueCnt = (enqueueCnt >= num_buf)? 0 : enqueueCnt;
pipeline++;
}
else if(pipeline >= 0)
{
vx_image input_image;
vx_image flow_vector_field_image;
vx_image confidence_image;
uint32_t num_refs;
vx_uint32 actual_checksum = 0;
/* Dequeue input */
if (status == VX_SUCCESS)
{
//将经过vxGaussianPyramidNode,得到的输出图像出队
status = vxGraphParameterDequeueDoneRef(obj->graph, obj->input_img_graph_parameter_index, (vx_reference*)&input_image, 1, &num_refs);
}
if ((vx_true_e == tivxIsTargetEnabled(TIVX_TARGET_DISPLAY1)) && (1 == obj->displayObj.display_option) && (!obj->test_mode))
{
if (status == VX_SUCCESS)
{
status = vxGraphParameterDequeueDoneRef(obj->graph, obj->flow_vector_field_img_graph_parameter_index, (vx_reference*)&flow_vector_field_image, 1, &num_refs);
}
if (status == VX_SUCCESS)
{
status = vxGraphParameterDequeueDoneRef(obj->graph, obj->confidence_img_graph_parameter_index, (vx_reference*)&confidence_image, 1, &num_refs);
}
/* Enqueue output */
if (status == VX_SUCCESS)
{
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->flow_vector_field_img_graph_parameter_index, (vx_reference*)&flow_vector_field_image, 1);
}
if (status == VX_SUCCESS)
{
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->confidence_img_graph_parameter_index, (vx_reference*)&confidence_image, 1);
}
}
else
{
/* Dequeue & Save output */
outputFileNum = curFileNum - num_buf;
outputFileNum = (outputFileNum <=obj->start_fileno) ? (obj->end_fileno-obj->start_fileno+outputFileNum+1) : outputFileNum;
if (status == VX_SUCCESS)
{
//将经过tivxDofVisualizeNode,得到的输出flow img图像出队,可供用户访问数据和操作
status = vxGraphParameterDequeueDoneRef(obj->graph, obj->flow_vector_field_img_graph_parameter_index, (vx_reference*)&flow_vector_field_image, 1, &num_refs);
snprintf(output_file_name_flow_img, APP_MAX_FILE_PATH, "%s/%sflov_%05d.%s",
obj->output_file_path,
obj->output_file_prefix,
outputFileNum,
obj->out_file_ext
);
}
if ((status == VX_SUCCESS) && (!obj->test_mode))
{
if(0 == obj->is_interactive)
{
printf(" %d of %d: Saving [%s] ...\n", outputFileNum, obj->end_fileno, output_file_name_flow_img);
}
//保存光流图像
status = app_save_vximage_to_file(output_file_name_flow_img, flow_vector_field_image, obj->out_file_format);
}
if (status == VX_SUCCESS)
{
//将经过tivxDofVisualizeNode,得到的输出conf img图像出队,可供用户访问数据和操作
status = vxGraphParameterDequeueDoneRef(obj->graph, obj->confidence_img_graph_parameter_index, (vx_reference*)&confidence_image, 1, &num_refs);
snprintf(output_file_name_conf_img, APP_MAX_FILE_PATH, "%s/%sconf_%05d.%s",
obj->output_file_path,
obj->output_file_prefix,
outputFileNum,
obj->out_file_ext
);
}
if ((status == VX_SUCCESS) && (!obj->test_mode))
{
if(0 == obj->is_interactive)
{
printf(" %d of %d: Saving [%s] ...\n", outputFileNum, obj->end_fileno, output_file_name_conf_img);
}
status = app_save_vximage_to_file(output_file_name_conf_img, confidence_image, obj->out_file_format);
}
if((status == VX_SUCCESS) && (obj->test_mode == 1))
{
if (app_test_check_image(confidence_image, checksums_expected[0][test_counter], &actual_checksum) == vx_false_e)
{
test_result = vx_false_e;
populate_gatherer(0, test_counter, actual_checksum);
}
if (app_test_check_image(flow_vector_field_image, checksums_expected[1][test_counter], &actual_checksum) == vx_false_e)
}
/* Enqueue output */
if (status == VX_SUCCESS)
{
//将映射到用户空间的flow img对象重新入队。原来的位置会被自动置为NULL,直到下次出队。
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->flow_vector_field_img_graph_parameter_index, (vx_reference*)&flow_vector_field_image, 1);
}
if (status == VX_SUCCESS)
{
//将映射到用户空间的conf img对象重新入队。原来的位置会被自动置为NULL,直到下次出队。
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->confidence_img_graph_parameter_index, (vx_reference*)&confidence_image, 1);
}
}
if (status == VX_SUCCESS)
{
//查找被dequeue的对象在array中的索引。
app_find_image_array_index(obj->pyramidObj.input_img,(vx_reference)input_image, num_buf, &img_array_idx);
}
if(img_array_idx != -1)
{
if(0 == obj->is_interactive)
{
printf(" %d of %d: Loading [%s] ...\n", curFileNum, obj->end_fileno, input_file_name);
}
appPerfPointBegin(&obj->fileio_perf);
if (status == VX_SUCCESS)
{
//读取图像
status = app_load_vximage_from_file(obj->pyramidObj.input_img[img_array_idx], input_file_name, obj->in_file_format);
}
appPerfPointEnd(&obj->fileio_perf);
}
/* Enqueue input - start execution */
if (status == VX_SUCCESS)
{
//将映射到用户空间的输入图像对象重新入队。原来的位置会被自动置为NULL,直到下次dequeue。
status = vxGraphParameterEnqueueReadyRef(obj->graph, obj->input_img_graph_parameter_index, (vx_reference*)&input_image, 1);
}
test_counter++;
}
appPerfPointEnd(&obj->total_perf);
/* Run for num_iterations - loop to first image if we're on the last image */
if ((curFileNum == obj->end_fileno) && (iterations < obj->num_iterations))
{
test_counter = 0;
if(0 == obj->is_interactive)
{
printf("Iteration %d of %d: Done !!!\n", iterations, obj->num_iterations);
}
#if 1 //def APP_DEBUG
appPerfPointPrintFPS(&obj->total_perf);
appPerfPointReset(&obj->total_perf);
if(iterations==1)
{
/* after first iteration reset performance stats */
appPerfStatsResetAll();
}
#endif
curFileNum = obj->start_fileno - 1u;
iterations++;
}
}
if(0 == obj->is_interactive)
{
printf("Ran %d times successfully!\n", obj->num_iterations * (obj->end_fileno-obj->start_fileno));
}
if (status == VX_SUCCESS)
{
status = vxWaitGraph(obj->graph);
}
obj->stop_task = 1;
return status;
}
参考资料:
https://zhuanlan.zhihu.com/p/469882197
https://registry.khronos.org/OpenVX/specs/1.3.1/html/OpenVX_Specification_1_3_1.html#sec_extending
https://registry.khronos.org/OpenVX/specs/1.0/html/index.html
https://registry.khronos.org/OpenVX/extensions/vx_khr_pipelining/1.0/html/index.html