VPI - 视觉编程接口

3.2 版本

架构

概述

VPI 是一个软件库,提供了一系列计算机视觉和图像处理算法,这些算法可以在各种硬件加速器中无缝执行。这些加速器被称为 后端

VPI 的目标是为计算后端提供统一的接口,同时保持高性能。它通过对底层硬件及其操作的数据进行精简但有效的软件抽象来实现这一点。

下图说明了 VPI 的架构

API 遵循一种范式,其中对象分配和设置发生在初始化阶段。接下来是应用程序循环,主要处理发生在这里,使用初始化期间创建的对象。当主要处理完成时,创建的对象将被销毁,环境将被清理。在资源受限的嵌入式环境中,内存分配在时间和空间上都受到限制,VPI 提供的对内存分配和生命周期的控制是有益的。

VPI 的核心组件包括

  • 算法:表示不可分割的计算操作。
  • 后端:表示负责实际计算的硬件引擎。
  • :充当异步队列,算法被提交到该队列,最终在给定的后端上按顺序执行。流和事件是计算管道的构建块。
  • 缓冲区:存储输入和输出数据。
  • 事件:提供在流和/或应用程序线程上运行的同步原语。
  • 上下文:保存 VPI 的状态和创建的对象。

支持的平台

VPI 可以在以下平台/设备中使用

  • AGX Orin DevKit
  • AGX Orin 32/64GB/JAOi
  • Orin NX 16/8GB
  • Orin Nano 8/4GB
  • Orin Nano DevKit
  • Linux x86_64,配备 NVIDIA dGPU,起始于 Maxwell (sm_50 或更新版本)。
    • 已在 Ubuntu 20.04 和 Ubuntu 22.04 上测试

算法

算法表示实际的计算操作。它们作用于一个或多个输入缓冲区,并将结果写入用户提供的输出缓冲区。它们相对于应用程序线程异步运行。有关支持的算法列表,请参阅算法部分。

算法分为两类

  • 需要有效载荷的算法。
  • 无有效载荷的算法。

算法有效载荷

一些算法实现,例如 FFTKLT 特征跟踪器,需要临时资源才能正常运行。这些资源封装在与算法关联的 VPIPayload 对象中。

在执行算法之前,您必须在初始化时创建相应的有效载荷,传递用于分配临时资源并指定可能执行该算法的后端的参数。在执行计算的主循环中,您将算法实例提交到流以执行,同时提供相应的有效载荷以及输入和输出参数。您可以在多个算法实例中重用有效载荷,但必须确保有效载荷一次仅被一个实例使用。

当不再需要有效载荷时,您必须通过调用 vpiPayloadDestroy 来销毁它。此函数会释放有效载荷中封装的任何资源。

示例

无有效载荷的算法

一些算法不需要临时资源。此类算法包括 方框滤波器重缩放 等。对于这些算法,无需有效载荷处理,并且操作顺序简化。所有必需的数据都在算法提交期间发送。

示例

后端

VPI 支持的每个算法都在一个或多个后端中实现。相同算法的不同实现,在给定相同输入时会返回相似的结果,但它们的结果之间可能存在细微差异。这主要是由于针对特定后端量身定制的优化,例如使用定点算术而不是浮点算术。

CPU

此后端表示设备的 CPU。它可能会创建一组后台工作线程和数据结构,以支持跨多个内核的高效并行执行。这些工作线程可能在不同的流和/或上下文实例之间共享。

VPI 提供了允许您定义自己的 CPU 任务调度方案的机制,方法是调用 vpiContextSetParallelFor,并使用 VPIParallelForCallback 函数,当需要执行 CPU 任务时,VPI 将调用该函数。

CUDA

CUDA 后端与特定的启用 CUDA 的 GPU 具有显式关联,这在流的构造期间定义。这意味着提交到此流上执行的算法将由此 GPU 处理。

CUDA 后端管理一个 cudaStream_t 句柄和其他 CUDA 设备信息,这些信息允许它启动底层的 CUDA 内核。

VPI 利用 CUDA 内核启动的异步特性来优化它们的启动。在某些情况下,特别是当没有用户定义函数提交到流时,后端直接从调用者线程启动 CUDA 任务,完全绕过工作线程。

当仅涉及 CUDA 算法时,VPI 通常充当 CUDA SDK 之上的高效精简层。

在构造 API 上下文之前,您必须为调用线程正确设置 CUDA 上下文。生成的上下文对象将相应的 CUDA 上下文用于内部内核调用。

注意
目前不支持使用多个 GPU,并且可能导致未定义的行为。使用多个 GPU 获得的结果不可靠。

PVA

可编程视觉加速器 (PVA) 是 NVIDIA® Jetson AGX Orin™ 和 NVIDIA® Jetson Orin™ NX 设备中的一个处理器,专门用于图像处理和计算机视觉算法。

当您需要释放 GPU 以运行其他只有 GPU 才能执行的任务时,例如深度学习推理阶段和仅在 CUDA 后端上实现的算法,请使用 PVA 后端。

PVA 硬件比 CPU 和 CUDA 硬件更节能。因此,如果功耗是首要考虑因素,请尽可能使用 PVA 后端。

每个 Jetson AGX Orin 或 Jetson Orin NX 设备都包含一个 PVA 处理器,每个处理器包含两个向量处理器。因此,设备最多可以同时执行两个独立的 PVA 任务。

当多个 VPI 流启用了 PVA 后端时,它们每个都以轮询方式选择一个可用的 PVA 向量处理器。

注意
对于任何特定算法,PVA 后端不一定比 CUDA 或 CPU 后端更快。

VIC

视频图像合成器 (VIC) 是 Jetson 设备中的一个固定功能处理器,专门用于低级图像处理任务,例如重缩放、色彩空间转换、降噪和合成。

与 PVA 后端一样,如果性能不是首要考虑因素,VIC 后端允许您从 GPU 卸载任务,使其空闲以进行其他处理。

OFA

NVIDIA 光流加速器 (OFA) 是新型 Jetson AGX Orin 设备中的专用处理器,用于计算图像之间的光流。它目前被用作 立体视差估计器 中的后端。

VPIStream 对象是 API 的主要入口点。它大致基于 CUDA 的 cudaStream_t。此对象表示 FIFO 命令队列,其中存储要由某个后端执行的命令列表。命令可以运行特定的计算机视觉算法,执行主机函数(使用 vpiSubmitHostFunction),或发出事件信号。

在初始化时,流被配置为使用将执行提交给它的任务的后端。默认情况下,它使用当前上下文中启用的后端。创建流时,您可以设置标志以进一步限制可用后端的数量并减少资源使用。

每个流都会启动一个内部工作线程来分派任务,从而允许相对于调用(用户)线程的异步任务执行。这意味着当在特定后端上将算法提交调用调用到 VPI 流时,该函数会将相应的命令推送到 VPIStream 工作线程,并立即将执行返回给调用线程。

推送到工作线程的任务不会立即处理。它们最初聚集在暂存队列中。这些任务仅在流被刷新时才会被处理。此操作将所有任务从暂存队列移动到处理队列中,处理队列最终将任务提交到与其关联的后端。

以下事件会触发流刷新

暂存队列允许处理管道优化机会,例如最小化内存映射操作等。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的

缓冲区

缓冲区表示 VPI 算法处理的数据。VPI 支持以下三种数据类型的抽象

  • 图像:保存二维数据。
  • 数组:保存一维数据。
  • 金字塔:保存一系列具有不同细节程度的图像,从精细到粗略。

VPI 可以分配所有三种类型的缓冲区。对于图像和数组,它可以将数据包装到 VPI 缓冲区中并将其存储在预分配的内存中。当应用程序需要与 VPI 以外的库互操作时,这非常有用,例如当它使用 OpenCV cv::Mat 缓冲区作为 VPI 算法的输入时。

所有缓冲区类型都共享大小和元素类型属性。

图像

VPI 图像表示任何类型的 2D 数据,例如实际图像、嵌入在 2D 空间中的矢量场和 2D 热图。

VPI 图像的特征在于它们的大小(宽度和高度)和格式。

当应用程序创建 VPIImage 对象时,它会传递标志,指定图像可以与哪个后端一起工作。您可以使用 VPIBackend 枚举之一,或者使用两个或多个枚举进行“或”运算来设置标志。当没有传递后端标志时,VPI 会启用当前上下文中允许的所有后端,默认情况下是所有可用的后端。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 图像

图像视图

VPI 图像视图表示 图像 的现有 2D 数据中的矩形区域,请参阅上面 图像 的描述。

VPI 图像视图是从现有 VPI 图像创建的,其特征在于它们的剪辑区域,即由起始位置 (x, y) 和大小(宽度、高度)定义的矩形。它们共享原始源图像的相同上下文和格式,但它们的大小(宽度和高度)是矩形区域的大小。

当应用程序创建 VPIImage 对象作为图像视图时,它会传递标志,以与常规图像相同的方式指定后端。

有关更多信息,请参阅 vpiImageCreateViewvpiImageSetView 函数的参考文档。

锁定

为了使图像内容可用于 VPI 外部的访问,图像必须通过调用 vpiImageLockData 函数来锁定图像缓冲区。这确保对内存所做的所有更改都已提交,并且可用于 VPI 外部的访问。

根据要提供的缓冲区类型,图像必须启用某些后端,请参阅 vpiImageLockData 文档以获取更多详细信息。vpiImageLockData 使用图像信息填充 VPIImageData 对象,使您可以正确寻址和解释所有图像像素。当您完成在主机上处理图像数据时,调用 vpiImageUnlock

当图像包装在 VPI 外部分配的缓冲区中,并且这些缓冲区正在 VPI 外部访问时,也必须锁定图像。在这种情况下,您对通过 VPI 调用检索图像内容不感兴趣。相反,调用 vpiImageLock 以锁定图像内容。只有这样,才能通过包装的缓冲区直接访问它们。调用 vpiImageUnlock 以允许 VPI 在完成此操作后访问缓冲区。同样,当缓冲区被锁定时,尝试访问其内容的流将失败,并显示 VPI_ERROR_BUFFER_LOCKED

当图像被锁定时,异步运行的算法无法访问它。但是,它可以由最初锁定它的同一线程递归锁定。请记住,每个 vpiImageLockData 调用都应与相应的 vpiImageUnlock 配对。

图像格式

VPI 支持各种图像格式,表示不同的像素类型,例如单通道 8 位、16 位或 32 位,无符号和有符号,多通道 RGB 和 RGBA,半平面 NV12 等。

图像格式由 VPIImageFormat 枚举表示。每种格式都由多个属性定义,例如色彩空间、平面数和数据布局。有一些函数可以从图像格式中提取每个组件,以及修改现有组件。

并非所有算法都支持所有识别的图像格式。但是,大多数算法都提供了多种格式选择。支持的格式列在每个算法的 API 参考文档中。

2D 图像最常见的内存布局是 pitch-linear 格式,即逐行排列,一个接一个。每行可以大于容纳图像数据所需的尺寸,以符合行地址对齐限制。

您还可以使用专有的块线性布局创建或包装内存。对于某些算法和后端,使用此格式创建 2D 内存可能更有效。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 图像格式

包装外部内存

您可以使用函数 vpiImageCreateWrapper 创建包装外部分配内存的图像。在每种情况下,您都必须使用所需的信息填充 VPIImageData 结构,并将其传递给该函数。请参阅其 API 参考文档,以获取有关可以包装的内存类型的信息。

在所有这些情况下,VPIImage 对象不拥有内存缓冲区。当 VPIImage 被销毁时,缓冲区不会被释放。

与创建由 VPI 管理的图像缓冲区的函数一样,这些包装函数接受标志,指定它们可以与哪些后端一起使用。

数组

VPI 数组表示一维数据,例如关键点列表、边界框和变换。

数组的特征在于它们的容量、大小和元素类型。与图像一样,标志用于指定它们可以与哪些后端一起工作。

数组类型从枚举 VPIArrayType 中提取。需要数组作为输入或输出的算法(例如 KLT 模板跟踪器)通常接受一种特定的数组类型。

VPIArray 的行为与其他内存缓冲区略有不同:虽然数组的容量在其对象的生命周期内是固定的,但其大小可以更改。任何写入数组的 API 都必须将大小参数设置为数组中有效元素的数量。您可以使用 vpiArrayGetSizevpiArraySetSize 来查询和修改数组的大小。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 数组

锁定

可以使用 vpiArrayLockData 函数在 VPI 外部访问数组数据。此函数的工作方式类似于其图像对应函数。它也支持同一线程的递归锁定。

包装外部内存

您还可以使用函数 vpiArrayCreateWrapper 创建包装外部分配的 CUDA 和主机内存的数组。在这两种情况下,您都必须使用所需的信息填充 VPIArrayData 结构,并将其传递给该函数。

金字塔

VPI 金字塔表示堆叠在一起的 VPI 图像的集合,所有图像都具有相同的格式,但尺寸可能不同。

金字塔的特征在于其级别数、基本级别尺寸、缩放因子和图像格式。缩放因子表示一个级别的维度与前一个级别的维度之比。例如,当 scale=0.5 时,金字塔是二元的,即尺寸是 2 的幂。

通常需要将一个金字塔级别作为 VPI 算法的输入或输出进行处理。然后,您必须使用 vpiImageCreateWrapperPyramidLevel 来标识要包装的金字塔及其级别。生成的图像继承金字塔的已启用后端。您可以像使用任何其他图像一样使用返回的 VPIImage 句柄。当您完成使用图像后,必须使用 vpiImageDestroy 销毁它。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 金字塔

锁定

与图像和数组一样,您可以使用函数 vpiPyramidLockData 在 VPI 外部访问整个金字塔的内容,前提是金字塔已启用与返回的缓冲区类型对应的后端。有关更多信息,请参阅 vpiPyramidLockData。此函数填充一个 VPIPyramidData 结构,其中包含 VPIImageData 数组。当您完成使用 VPIPyramidData 时,调用 vpiPyramidUnlock 以从主机取消映射金字塔并释放其资源。

递归锁定对金字塔的工作方式与对图像和数组相同。

事件

API 中的每个计算函数都相对于调用线程异步执行;也就是说,它会立即返回,而不是等待操作完成。有两种方法可以将操作与后端同步。

一种方法是等待 VPIStream 队列中的所有命令都完成后,通过调用 vpiStreamSync。此方法很简单,但它无法提供细粒度的同步(例如,“等待函数 X 完成”)或跨流同步(例如,“等待流 D 中的函数 C 完成后再运行流 B 中的函数 A”)。

另一种方法通过使用 VPIEvent 对象提供更灵活的同步。这些对象在概念上类似于二进制信号量,旨在密切模仿 CUDA API 中的事件

  • 您可以在事件实例中捕获提交到 VPIStream 实例的所有命令(请参阅 vpiEventRecord)。当所有捕获的命令都已处理并从 VPIStream 命令队列中删除后,事件会发出信号。
  • 您可以使用 vpiStreamWaitEvent 调用执行跨流同步,该调用将命令推送到 VPIStream 队列,该队列会阻止处理未来排队的命令,直到给定事件发出信号。
  • 应用程序可以使用 vpiEventQuery 查询事件的状态。
  • 应用程序线程可以阻塞,直到事件完成,使用 vpiEventSync
  • 事件可以在完成时添加时间戳。
  • 您可以计算同一流以及不同流中已完成事件的时间戳之间的差异。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 事件

上下文

上下文封装了 VPI 用于执行操作的所有资源。它会在上下文销毁时自动清理这些资源。

每个应用程序 CPU 线程都有一个活动上下文。每个上下文都拥有在其活动时创建的 VPI 对象。

默认情况下,所有应用程序线程都与同一个全局上下文关联,该全局上下文在创建第一个 VPI 资源时由 VPI 自动创建。在这种情况下,您无需执行任何显式上下文管理,一切都由 VPI 在幕后处理。

当需要对上下文进行更精细的控制时,用户创建的上下文是一种选择。创建上下文后,可以将其推送到当前应用程序线程的上下文堆栈,或者可以替换当前上下文。这两种操作都会使创建的上下文变为活动状态。有关如何操作上下文的更多信息,请参阅 上下文堆栈

您可以在创建上下文时指定与上下文关联的多个属性,例如当上下文处于活动状态时,创建的对象支持哪些后端。这有效地允许您屏蔽对特定后端的支持。例如,如果当前上下文未设置 VPI_BACKEND_CUDA 标志,则 CUDA 后端的流创建将失败。如果您不传递后端标志,则上下文会检查运行平台并启用与所有可用硬件引擎关联的后端。

注意
CPU 后端无法屏蔽,并且必须始终作为后备实现来支持。

对象(缓冲区、有效载荷、事件等)不能在不同的上下文之间共享。

创建的上下文数量没有限制,除非可用内存不足。

有关更多信息,请参阅 VPI - 视觉编程接口 的 “C API 参考” 部分中的 上下文

全局上下文

默认情况下,VPI 在创建任何 VPI 对象之前创建一个全局上下文。此全局上下文最初在所有应用程序线程之间共享,并且用户无法销毁它。

对于大多数用例,应用程序可以使用全局上下文。当应用程序需要对对象的分组方式进行更精细的控制,或者需要在管道之间具有一定程度的独立性时,您可能需要显式创建和操作上下文。

上下文堆栈

每个应用程序线程都有一个上下文堆栈,该堆栈不与其他线程共享。

堆栈顶部的上下文是该线程的当前上下文。

默认情况下,上下文堆栈中有一个上下文,即全局上下文。因此,所有新线程都将相同的全局上下文设置为当前线程。

在给定堆栈中设置当前上下文相当于用给定上下文替换顶部上下文,即全局上下文或最近推送的上下文。被替换的上下文不再属于堆栈。

但是,将上下文推送到堆栈不会替换任何内容。顶部上下文保留在堆栈中,新的推送上下文放在顶部,从而成为新的当前上下文。

用户可以随意从堆栈中推送和弹出上下文。这允许在新的上下文中临时创建管道,而不会干扰现有上下文。

为避免泄漏,务必匹配给定上下文堆栈上的推送和弹出次数。请注意,上下文堆栈最多可以包含八个上下文。

线程安全

所有 API 函数都是线程安全的。对 API 对象的并发主机访问将被序列化并以未指定的顺序执行。所有 API 调用都使用 VPIContext 实例,该实例是线程特定的,并存储在线程本地存储 (TLS) 中。如果当前线程的上下文指针为 NULL(未设置上下文),则所有 API 调用都使用在库初始化期间创建的默认全局上下文。

API 对象没有线程亲和性的概念;也就是说,如果多个线程使用相同的上下文实例,则在一个线程中创建的对象可以安全地由另一个线程销毁。

大多数 API 函数都是非阻塞的。调用时可能阻塞的函数是 vpiStreamSyncvpiStreamDestroyvpiContextDestroyvpiEventSync 和几个 vpiSubmit* 函数(当流命令队列已满时会阻塞)。由于 API 实现中的隐式同步是最小的,因此您必须确保依赖函数调用的最终顺序是合法的。

管道示例以及如何使用 VPI 实现它们将在以下部分中进行说明。

简单管道

在此示例中,实现了一个具有简单方框滤波器操作的管道来处理输入图像。这与 图像模糊教程 非常相似。

实现管道的代码如下

语言
  1. 导入 vpi 模块。

    import vpi
  2. 创建要使用的输入图像缓冲区

    该示例创建了一个 640x480 1 通道(灰度)输入图像,具有无符号 8 位像素元素,由 vpi.Format.U8 表示。VPI 在创建时使用零初始化图像。

    注意
    此示例创建了一个空的输入图像缓冲区,但在实际用例中,可以将现有内存缓冲区包装到 VPI 图像缓冲区中,或者可以使用来自早期管道阶段的图像。有关更完整的示例,请参阅 图像模糊教程
    input = vpi.Image((640,480), vpi.Format.U8)
  3. 在将 vpi.Backend.CUDA 定义为默认后端的 Python 上下文中,在输入图像上调用 box_filter 方法。3x3 方框滤波器算法将由默认流上的 CUDA 后端执行。结果将返回到新图像 output 中。

    with vpi.Backend.CUDA
    output = input.box_filter(3)
注意
为简单起见,此示例不检查函数返回值中的错误。有关简单但完整的应用程序示例,请参阅捆绑的 示例
  1. 包含必要的头文件。此示例需要图像缓冲区、流和 方框滤波器 算法的头文件。

    #include <vpi/Image.h>
    #include <vpi/Stream.h>
    声明实现方框滤波器算法的函数。
    用于处理 VPI 图像的函数和结构。
    声明处理 VPI 流的函数。
  2. 创建要使用的图像缓冲区。

    int main()
    {
    VPIImage input, output;
    vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U8, 0, &input);
    vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U8, 0, &output);
    #define VPI_IMAGE_FORMAT_U8
    具有一个 8 位无符号整数通道的单平面。
    struct VPIImageImpl * VPIImage
    图像的句柄。
    定义: Types.h:256
    VPIStatus vpiImageCreate(int32_t width, int32_t height, VPIImageFormat fmt, uint64_t flags, VPIImage *img)
    使用指定的标志创建空的图像实例。

    此示例创建了一个 640x480 单通道(灰度)输入图像,其像素元素为 8 位无符号整数,由 VPI_IMAGE_FORMAT_U8 枚举表示。VPI 在创建时将图像初始化为零。传递全零图像标志表示此图像可用于所有可用的硬件后端。这使得以后更容易将算法提交到不同的后端,但代价是使用更多资源。输出图像以相同的方式创建。

    注意
    此示例创建了一个空的输入图像缓冲区,但在实际用例中,可以将现有内存缓冲区包装到 VPI 图像缓冲区中,或者可以使用来自早期管道阶段的图像。有关更完整的示例,请参阅 图像模糊教程
  3. 创建一个流来执行算法。传递全零流标志表示该算法可以在任何可用的硬件后端中执行,稍后指定。

    VPIStream stream;
    vpiStreamCreate(0, &stream);
    struct VPIStreamImpl * VPIStream
    流的句柄。
    定义: Types.h:250
    VPIStatus vpiStreamCreate(uint64_t flags, VPIStream *stream)
    创建流实例。
  4. 将盒式滤波器算法连同输入和输出图像以及其他参数一起提交到流。在本例中,滤波器算法是具有 Clamp 边界条件的 3x3 盒式滤波器。它将由 CUDA 后端执行。

    vpiSubmitBoxFilter(stream, VPI_BACKEND_CUDA, input, output, 3, 3, VPI_BORDER_CLAMP);

    通常,由于流的异步特性,算法会在流的工作线程上排队,并且函数立即返回。稍后,它会提交到后端执行。使用工作线程允许程序继续组装处理流水线,或执行其他任务,同时算法并行执行。

  5. 等待直到流完成处理。

    vpiStreamSync(stream);
    VPIStatus vpiStreamSync(VPIStream stream)
    阻塞调用线程,直到此流队列中的所有已提交命令都完成(队列为空)...

    此函数会阻塞,直到提交到流的所有算法都执行完成。流水线必须执行此操作,然后才能显示输出或将其保存到磁盘等。

  6. 销毁创建的对象。

    vpiImageDestroy(output);
    return 0;
    }
    void vpiImageDestroy(VPIImage img)
    销毁图像实例。
    void vpiStreamDestroy(VPIStream stream)
    销毁流实例并释放所有硬件资源。

    当流水线完成使用创建的对象后,它会销毁它们以防止内存泄漏。销毁流会强制其同步,但是销毁仍在被算法使用的图像会导致未定义的行为,最有可能导致程序崩溃。

在本示例中,NVIDIA 建议检查多个 VPI 对象如何协同工作,并检查对象之间的所有权关系。

这是提供的 C/C++ 示例的概念结构

其中

  • 默认上下文是自动创建并变为活动的上下文。在本示例中,默认上下文是流和图像缓冲区。
  • 拥有一个工作线程,该线程将任务排队并分派到后端设备并处理同步。它还拥有代表硬件后端的对象,算法最终将在这些后端执行。
  • 盒式滤波器是提交到流的算法。在内部,作业 1 是使用算法内核及其所有参数创建的。然后将其排队到工作线程上,当提交给它的所有先前任务都完成时,工作线程将其提交到硬件。由于此算法没有有效负载(或状态),因此不必担心其生命周期。
  • 同步表示 vpiStreamSync 调用。它将作业 2 排队到工作线程上,并且作业在执行时发出内部事件信号。调用线程等待直到事件发出信号,从而保证到目前为止排队的所有任务都已完成。在 vpiStreamSync 返回之前,其他线程的提交将被阻止。
注意
在本示例中,由于在提交算法时工作线程为空,并且 CUDA 内核执行是异步的,因此 VPI 将算法直接提交到 CUDA 设备,从而完全绕过工作线程。
当仅使用 CUDA 后端进行算法提交和同步时,VPI 在底层 CUDA 执行之上的开销通常会最小化,并且通常可以忽略不计。对于仅使用其他后端之一的流也是如此。在同一流中将算法提交到不同的后端会产生较小的内部同步开销。

复杂流水线

更复杂的场景可能会利用设备上不同的加速处理器,并创建一个最能充分利用其全部计算能力的流水线。为此,流水线必须具有可并行化的阶段。

下一个示例实现了一个完整的立体视差估计和 Harris 角点提取流水线,该流水线提供了大量的并行化机会。

该图揭示了三个阶段的并行化机会:独立的左右图像预处理和 Harris 角点提取。流水线为每个处理阶段使用不同的后端,具体取决于每个后端的处理速度、功耗要求、输入和输出限制以及可用性。在本例中,处理在以下后端之间分配

  • VIC:执行立体像对校正和降采样。
  • CUDA:执行图像格式转换。
  • PVA:执行立体视差计算。
  • CPU:处理一些预处理和 Harris 角点的提取。

后端选择使 GPU 可以空闲出来处理其他任务,例如深度学习推理阶段。图像格式转换操作在 CUDA 上非常快,并且不会产生太多干扰。CPU 保持忙碌状态,不受干扰地提取 Harris 关键点。

下图显示了算法如何拆分为流以及如何同步流。

左流和右流都启动立体像对预处理,而关键点流则等待直到右侧灰度图像准备就绪。一旦准备就绪,Harris 角点检测开始,同时右流继续预处理。当左流上的预处理结束时,该流等待直到右侧降采样图像准备就绪。最后,立体视差估计开始,其具有两个立体输入。在任何时候,主机线程都可以在左流和关键点流中发出 vpiStreamSync 调用,以等待视差和关键点数据准备好进行进一步处理或显示。

上面的概述解释了实现此流水线的代码

  1. 包含所有使用的对象以及所有必需算法的头文件。
    #include <string.h>
    #include <vpi/Array.h>
    #include <vpi/Context.h>
    #include <vpi/Event.h>
    #include <vpi/Image.h>
    #include <vpi/Stream.h>
    #include <vpi/WarpMap.h>
    #include <vpi/algo/Remap.h>
    用于处理 VPI 数组的函数和结构。
    声明实现双边滤波器算法的函数。
    用于处理 VPI 上下文的函数和结构。
    声明处理图像格式转换的函数。
    用于处理 VPI 事件的函数和结构。
    声明实现 Harris 角点检测器算法的函数。
    声明用于基于常用镜头畸变模型生成扭曲映射的函数。
    声明实现 Remap 算法的函数。
    声明实现 Rescale 算法的函数。
    声明实现立体视差估计算法的函数。
    声明实现 WarpMap 结构和相关函数的函数。
  2. 执行初始化阶段,其中创建所有必需的对象。
    1. 创建上下文并使其处于活动状态。

      尽管可以使用自动创建的默认上下文来管理 VPI 状态,但创建上下文并使用它来处理与其生命周期内特定流水线链接的所有对象可能更方便。最后,上下文销毁会触发在其下创建的对象的销毁。使用专用上下文还可以更好地隔离此流水线和应用程序可能使用的其他流水线。

      int main()
      {
      vpiContextCreate(0, &ctx);
      VPIStatus vpiContextCreate(uint64_t flags, VPIContext *ctx)
      创建上下文实例。
      VPIStatus vpiContextSetCurrent(VPIContext ctx)
      为调用线程设置上下文。
      struct VPIContextImpl * VPIContext
      上下文的句柄。
      定义: Types.h:238
    2. 创建流。

      使用全零标志创建流,这意味着它们可以处理所有后端的任务。

      有两个流来处理立体像对预处理,第三个流用于 Harris 角点检测。当预处理完成时,stream_left 将重用于立体视差估计。

      VPIStream stream_left, stream_right, stream_keypoints;
      vpiStreamCreate(0, &stream_left);
      vpiStreamCreate(0, &stream_right);
      vpiStreamCreate(0, &stream_keypoints);
    3. 创建输入图像缓冲区包装器。

      假设输入来自捕获流水线作为 EGLImage,您可以将缓冲区包装在 VPIImage 中,以便在 VPI 流水线中使用。整个流水线只需要来自每个立体输入的帧(通常是第一帧)。

      EGLImageKHR eglLeftFrame = /* 来自左侧摄像机的第一个帧 */;
      EGLImageKHR eglRightFrame = /* 来自右侧摄像机的第一个帧 */;
      VPIImage left, right;
      VPIImageData dataLeft;
      dataLeft.buffer.egl = eglLeftFrame;
      vpiImageCreateWrapper(&dataLeft, NULL, 0, &left);
      VPIImageData dataRight;
      dataRight.buffer.egl = eglRightFrame;
      vpiImageCreateWrapper(&dataRight, NULL, 0, &right);
      VPIImageBuffer buffer
      存储图像内容。
      定义: Image.h:241
      EGLImageKHR egl
      图像存储为 EGLImageKHR。
      定义: Image.h:222
      VPIImageBufferType bufferType
      图像缓冲区类型。
      定义: Image.h:238
      VPIStatus vpiImageCreateWrapper(const VPIImageData *data, const VPIImageWrapperParams *params, uint64_t flags, VPIImage *img)
      通过包装现有内存块来创建图像对象。
      @ VPI_IMAGE_BUFFER_EGLIMAGE
      EGLImage。
      定义: Image.h:185
      存储有关图像特征和内容的信息。
      定义: Image.h:234
    4. 创建要使用的图像缓冲区。

      与简单流水线一样,此流水线创建空的输入图像。这些输入图像必须通过包装内存中已有的图像或来自早期 VPI 流水线的输出来填充。

      输入是 640x480 NV12(彩色)立体像对,通常由摄像机捕获流水线输出。临时图像是存储中间结果所必需的。格式转换是必要的,因为立体视差估计器和 Harris 角点提取器需要灰度图像。此外,立体视差要求其输入正好为 480x270。这是通过上图中的缩放阶段完成的。

      VPIImage left_rectified, right_rectified;
      vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_NV12_ER, 0, &left_rectified);
      vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_NV12_ER, 0, &right_rectified);
      VPIImage left_grayscale, right_grayscale;
      vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U16, 0, &left_grayscale);
      vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U16, 0, &right_grayscale);
      VPIImage left_reduced, right_reduced;
      vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &left_reduced);
      vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &right_reduced);
      VPIImage disparity;
      vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &disparity);
      #define VPI_IMAGE_FORMAT_U16
      具有一个 16 位无符号整数通道的单平面。
      #define VPI_IMAGE_FORMAT_NV12_ER
      具有全范围的 YUV420sp 8 位 pitch-linear 格式。
    5. 定义立体视差算法参数并创建有效负载。

      立体视差处理需要一些临时数据。VPI 将此数据称为有效负载。在本示例中,调用了 vpiCreateStereoDisparityEstimator 并传递了内部分配器所需的所有参数,以指定临时数据的大小。

      由于临时数据是在后端设备上分配的,因此有效负载与后端紧密耦合。如果相同的算法要在不同的后端中执行,或者使用同一后端在不同的流中并发执行,则每个后端或流都需要一个有效负载。在本例中,有效负载是为 PVA 后端执行而创建的。

      对于算法参数,VPI 立体视差估计器由半全局立体匹配算法实现。估计器需要人口普查变换窗口大小(指定为 5)和最大视差级别(指定为 64)。有关更多信息,请参见 立体视差估计器

      stereo_params.windowSize = 5;
      stereo_params.maxDisparity = 64;
      stereo_params.maxDisparity = stereo_params.maxDisparity;
      VPIPayload stereo;
      VPI_IMAGE_FORMAT_U16, &stereo_creation_params, &stereo);
      int32_t windowSize
      表示 OFA+PVA+VIC 后端上的中值滤波器大小或人口普查变换窗口大小(其他后端...)。
      int32_t maxDisparity
      匹配搜索的最大视差。
      VPIStatus vpiInitStereoDisparityEstimatorCreationParams(VPIStereoDisparityEstimatorCreationParams *params)
      使用默认值初始化 VPIStereoDisparityEstimatorCreationParams。
      VPIStatus vpiCreateStereoDisparityEstimator(uint64_t backends, int32_t imageWidth, int32_t imageHeight, VPIImageFormat inputFormat, const VPIStereoDisparityEstimatorCreationParams *params, VPIPayload *payload)
      为 vpiSubmitStereoDisparityEstimator 创建有效负载。
      定义 vpiCreateStereoDisparityEstimator 参数的结构。
      定义 vpiSubmitStereoDisparityEstimator 参数的结构。
      @ VPI_BACKEND_PVA
      PVA 后端。
      定义: Types.h:94
      @ VPI_BACKEND_OFA
      OFA 后端。
      定义: Types.h:97
      @ VPI_BACKEND_VIC
      VIC 后端。
      定义: Types.h:95
    6. 创建图像校正有效负载和相应的参数。它使用 Remap 算法进行镜头畸变校正。此处指定了立体镜头参数。由于左右镜头的参数不同,因此创建了两个重映射有效负载。有关更多详细信息,请参见 镜头畸变校正

      memset(&dist, 0, sizeof(dist));
      dist.k1 = -0.126;
      dist.k2 = 0.004;
      const VPICameraIntrinsic Kleft =
      {
      {466.5, 0, 321.2},
      {0, 466.5, 239.5}
      };
      const VPICameraIntrinsic Kright =
      {
      {466.2, 0, 320.3},
      {0, 466.2, 239.9}
      };
      {
      {1, 0.0008, -0.0095, 0},
      {-0.0007, 1, 0.0038, 0},
      {0.0095, -0.0038, 0.9999, 0}
      };
      memset(&map, 0, sizeof(map));
      map.grid.regionWidth[0] = 640;
      map.grid.regionHeight[0] = 480;
      map.grid.horizInterval[0] = 4;
      map.grid.vertInterval[0] = 4;
      VPIPayload ldc_left;
      vpiCreateRemap(VPI_BACKEND_VIC, &map, &ldc_left);
      VPIPayload ldc_right;
      vpiCreateRemap(VPI_BACKEND_VIC, &map, &ldc_right);
      VPIStatus vpiWarpMapGenerateFromPolynomialLensDistortionModel(const VPICameraIntrinsic Kin, const VPICameraExtrinsic X, const VPICameraIntrinsic Kout, const VPIPolynomialLensDistortionModel *distModel, VPIWarpMap *warpMap)
      生成使用多项式镜头畸变模型校正图像的映射。
      float VPICameraExtrinsic[3][4]
      相机外参矩阵。
      定义: Types.h:668
      float VPICameraIntrinsic[2][3]
      相机内参矩阵。
      定义: Types.h:655
      保存多项式镜头畸变模型的系数。
      VPIStatus vpiCreateRemap(uint64_t backends, const VPIWarpMap *warpMap, VPIPayload *payload)
      为 Remap 算法创建有效负载。
      int8_t numHorizRegions
      水平方向的区域数。
      VPIWarpGrid grid
      扭曲网格控制点结构定义。
      定义: WarpMap.h:91
      int16_t horizInterval[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
      给定区域内控制点之间的水平间距。
      int8_t numVertRegions
      垂直方向的区域数。
      int16_t vertInterval[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
      给定区域内控制点之间的垂直间距。
      int16_t regionWidth[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
      每个区域的宽度。
      int16_t regionHeight[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
      每个区域的高度。
      VPIStatus vpiWarpMapAllocData(VPIWarpMap *warpMap)
      为给定的扭曲网格分配扭曲映射的控制点数组。
      定义输入和输出图像像素之间的映射。
      定义: WarpMap.h:88
    7. Harris 角点检测器创建输出缓冲区。

      此算法接收图像并输出两个数组,一个包含关键点本身,另一个包含每个关键点的分数。最多返回 8192 个关键点,这必须是数组容量。关键点由 VPIKeypointF32 结构表示,分数由 32 位无符号值表示。有关更多信息,请参见 Harris 角点检测器

      VPIArray keypoints, scores;
      vpiArrayCreate(8192, VPI_ARRAY_TYPE_U32, 0, &scores);
      VPIStatus vpiArrayCreate(int32_t capacity, VPIArrayType type, uint64_t flags, VPIArray *array)
      创建空数组实例。
      struct VPIArrayImpl * VPIArray
      数组的句柄。
      定义: Types.h:232
      @ VPI_ARRAY_TYPE_U32
      无符号 32 位。
      @ VPI_ARRAY_TYPE_KEYPOINT_F32
      VPIKeypointF32 元素。
    8. 定义 Harris 检测器参数并创建检测器的有效负载。

      使用所需参数填充 VPIHarrisCornerDetectorParams 结构。有关每个参数的更多信息,请参见结构文档。

      与立体视差一样,Harris 检测器需要有效负载。这次只需要输入大小 (640x480)。流水线仅接受此大小的输入有效负载。

      harris_params.gradientSize = 5;
      harris_params.blockSize = 5;
      harris_params.strengthThresh = 10;
      harris_params.sensitivity = 0.4f;
      VPIPayload harris;
      int32_t gradientSize
      梯度窗口大小。
      int32_t blockSize
      用于计算 Harris 角点分数的块窗口大小。
      float strengthThresh
      指定消除 Harris 角点分数的最小阈值。
      float sensitivity
      指定 Harris-Stephens 方程的灵敏度阈值。
      VPIStatus vpiInitHarrisCornerDetectorParams(VPIHarrisCornerDetectorParams *params)
      使用默认值初始化 VPIHarrisCornerDetectorParams。
      VPIStatus vpiCreateHarrisCornerDetector(uint64_t backends, int32_t inputWidth, int32_t inputHeight, VPIPayload *payload)
      创建 Harris 角点检测器有效负载。
      定义 vpiSubmitHarrisCornerDetector 参数的结构。
      @ VPI_BACKEND_CPU
      CPU 后端。
      定义: Types.h:92
    9. 创建事件以实现屏障同步。

      事件用于流间同步。它们使用 VPIEvent 实现。流水线需要两个屏障:一个等待 Harris 角点提取的输入准备就绪,另一个等待预处理的右侧图像。

      VPIEvent barrier_right_grayscale, barrier_right_reduced;
      vpiEventCreate(0, &barrier_right_grayscale);
      vpiEventCreate(0, &barrier_right_reduced);
      struct VPIEventImpl * VPIEvent
      事件的句柄。
      定义: Types.h:244
      VPIStatus vpiEventCreate(uint64_t flags, VPIEvent *event)
      创建一个事件实例。
  3. 初始化之后是主要处理阶段,该阶段通过以正确的顺序向流提交算法和事件来实现管线。管线的主循环可以使用相同的事件、负载、临时缓冲区和输出缓冲区多次执行此操作。输入通常在每次迭代时重新定义,如下所示。
    1. 提交左帧处理阶段。

      镜头畸变校正、图像格式转换和降尺度被提交到左流。再次注意,提交操作是非阻塞的,并立即返回。

      vpiSubmitRemap(stream_left, VPI_BACKEND_VIC, ldc_left, left, left_rectified, VPI_INTERP_CATMULL_ROM,
      vpiSubmitConvertImageFormat(stream_left, VPI_BACKEND_CUDA, left_rectified, left_grayscale, NULL);
      vpiSubmitRescale(stream_left, VPI_BACKEND_VIC, left_grayscale, left_reduced, VPI_INTERP_LINEAR, VPI_BORDER_CLAMP,
      0);
      VPIStatus vpiSubmitConvertImageFormat(VPIStream stream, uint64_t backend, VPIImage input, VPIImage output, const VPIConvertImageFormatParams *params)
      将图像内容转换为所需的格式,可选择缩放和偏移。
      VPIStatus vpiSubmitRemap(VPIStream stream, uint64_t backend, VPIPayload payload, VPIImage input, VPIImage output, VPIInterpolationType interp, VPIBorderExtension border, uint64_t flags)
      向流提交一个 Remap 操作。
      VPIStatus vpiSubmitRescale(VPIStream stream, uint64_t backend, VPIImage input, VPIImage output, VPIInterpolationType interpolationType, VPIBorderExtension border, uint64_t flags)
      更改 2D 图像的尺寸和比例。
      @ VPI_BORDER_ZERO
      图像外部的所有像素都被视为零。
      定义: Types.h:278
      @ VPI_INTERP_LINEAR
      线性插值。
      @ VPI_INTERP_CATMULL_ROM
      Catmull-Rom 三次插值。
    2. 提交右帧预处理的最初几个阶段。

      镜头畸变校正和图像格式转换阶段生成灰度图像,作为 Harris 角点提取的输入。

      vpiSubmitRemap(stream_right, VPI_BACKEND_VIC, ldc_right, right, right_rectified, VPI_INTERP_CATMULL_ROM,
      vpiSubmitConvertImageFormat(stream_right, VPI_BACKEND_CUDA, right_rectified, right_grayscale, NULL);
    3. 记录右流状态,以便关键点流可以与之同步。

      只有当关键点流的输入就绪时才能启动。首先,barrier_right_grayscale 事件必须记录右流状态,方法是向其提交一个任务,该任务将在格式转换完成时发出事件信号。

      vpiEventRecord(barrier_right_grayscale, stream_right);
      VPIStatus vpiEventRecord(VPIEvent event, VPIStream stream)
      在此调用时,捕获事件中流命令队列的内容。
    4. 通过降尺度操作完成右帧预处理。

      vpiSubmitRescale(stream_right, VPI_BACKEND_VIC, right_grayscale, right_reduced, VPI_INTERP_LINEAR, VPI_BORDER_CLAMP,
      0);
    5. 记录右流状态,以便左流可以与之同步。

      在提交了整个右帧预处理之后,必须再次记录流状态,以便左流可以等待直到右帧就绪。

      vpiEventRecord(barrier_right_reduced, stream_right);
    6. 使左流等待直到右帧就绪。

      立体视差需要左右帧都准备就绪。管线使用 vpiStreamWaitEvent 向左流提交一个任务,该任务将等待直到右流上的 barrier_right_reduced 事件发出信号,这意味着右帧预处理已完成。

      vpiStreamWaitEvent(stream_left, barrier_right_reduced);
      VPIStatus vpiStreamWaitEvent(VPIStream stream, VPIEvent event)
      推送一个命令,该命令会阻止处理所有未来提交到流的命令,直到...
    7. 提交立体视差算法。

      输入图像现在已准备就绪。调用 vpiSubmitStereoDisparityEstimator 以提交视差估计器。

      left_reduced, right_reduced, disparity, NULL, &stereo_params);
      VPIStatus vpiSubmitStereoDisparityEstimator(VPIStream stream, uint64_t backend, VPIPayload payload, VPIImage left, VPIImage right, VPIImage disparity, VPIImage confidenceMap, const VPIStereoDisparityEstimatorParams *params)
      在一对图像上运行立体处理,并输出视差图。
    8. 提交关键点检测器管线。

      对于关键点检测,首先在 barrier_right_grayscale 事件上提交一个等待操作,使管线等待直到输入就绪。然后在其上提交 Harris 角点检测器。

      vpiStreamWaitEvent(stream_keypoints, barrier_right_grayscale);
      vpiSubmitHarrisCornerDetector(stream_keypoints, VPI_BACKEND_CPU, harris, right_grayscale, keypoints, scores,
      &harris_params);
      VPIStatus vpiSubmitHarrisCornerDetector(VPIStream stream, uint64_t backend, VPIPayload payload, VPIImage input, VPIArray outFeatures, VPIArray outScores, const VPIHarrisCornerDetectorParams *params)
      向流提交一个 Harris 角点检测器操作。
    9. 同步流以使用视差图和检测到的关键点。

      请记住,到目前为止在处理阶段调用的函数都是异步的;一旦作业在流中排队等待稍后执行,它们就会立即返回。

      现在可以在主线程上执行更多处理,例如更新 GUI 状态信息或显示上一帧。这发生在 VPI 执行管线时。一旦执行了这些额外的处理,处理当前帧最终结果的流必须使用 vpiStreamSync 进行同步。然后可以访问结果缓冲区。

      vpiStreamSync(stream_left);
      vpiStreamSync(stream_keypoints);
    10. 获取下一帧并更新输入封装器。

      可以重新定义现有的输入 VPI 图像封装器,以封装接下来的两个立体对帧,前提是它们的尺寸和格式相同。此操作非常高效,因为它是在没有堆内存分配的情况下完成的。

      eglLeftFrame = /* 从左侧相机获取下一帧 */;
      eglRightFrame = /* 从右侧相机获取下一帧 */;
      dataLeft.buffer.egl = eglLeftFrame;
      vpiImageSetWrapper(left, &dataLeft);
      dataRight.buffer.egl = eglRightFrame;
      vpiImageSetWrapper(right, &dataRight);
      VPIStatus vpiImageSetWrapper(VPIImage img, const VPIImageData *data)
      重新定义现有 VPIImage 封装器中封装的内存。
  4. 销毁上下文。

    此示例在当前上下文中创建了许多对象。一旦所有处理完成且不再需要管线,请销毁上下文。然后,所有流以及所有其他使用的对象都将被同步并销毁。不可能发生内存泄漏。

    销毁当前上下文会重新激活在当前上下文激活之前处于活动状态的上下文。

    return 0;
    }
    void vpiContextDestroy(VPIContext ctx)
    销毁上下文实例以及它拥有的所有资源。

从这些示例中要了解的重要要点

  • 算法提交立即返回。
  • 算法执行相对于主机线程是异步发生的。
  • 不同的流可以使用相同的缓冲区,但您必须通过使用事件来避免竞争条件。
  • 当上下文处于激活状态时,它拥有一个用户线程创建的所有对象。这允许一些有趣的场景,其中一个线程设置上下文并触发所有处理管线,然后将整个上下文移动到另一个线程,该线程等待管线结束,然后触发进一步的数据处理。