NVIDIA 视觉编程接口 (VPI) 是一个软件库,可提供一组计算机视觉和图像处理算法。这些算法的实现在 NVIDIA Jetson 嵌入式计算机或独立 GPU 上可用的不同硬件引擎上得到加速。
在这篇博文中,我们将向您展示如何在 Jetson 产品系列上运行时间降噪 (TNR) 示例应用程序。有关更多信息,请参阅:
https://docs.nvidia.com/vpi/index.html
在 Jetson 设备上设置 VPI
通过 SDK Manger设置 Jetson 设备时,请确保选中 Jetson SDK 组件框。然后在设备刷机时安装 VPI。有关安装的更多信息,请参阅: https://developer.nvidia.com/nvidia-sdk-manager
安装完成后,可以在如下路径下找到VPI:
代码语言:javascript复制/opt/nvidia/vpi1/
要验证环境设置是否正确,请将 VPI 示例应用程序复制到您的主目录中,然后构建 TNR 示例。
代码语言:javascript复制$ vpi1_install_samples.sh $HOME
$ cd $HOME/NVIDIA_VPI–samples/09-tnr
$ cmake 。
$ make
TNR 示例应用
VPI 提供了一组 CV 算法,这些算法利用多个后端来有效地使用设备的可用计算资源。TNR 是一种降噪方法,常用于在 Jetson 设备上运行的计算机视觉应用程序。这篇博文使用 TNR 示例应用程序来演示如何使用 VPI 中的一些关键概念和组件来实现自己的应用程序。
我们在这篇文章中涵盖了以下主题:
- 创建构建 VPI 管道所需的元素
- 了解与 OpenCV 的互操作性是如何发生的
- 将处理任务提交到流
- 同步流中的任务
- 锁定图像缓冲区,以便 CPU 可以访问它
TNR 示例可以在以下路径中找到:
代码语言:javascript复制$HOME/NVIDIA_VPI–samples/09-tnr/main.cpp
有关示例应用程序和算法的更多信息,请参阅以下资源: 1.应用程序:https://docs.nvidia.com/vpi/sample_tnr.html
2.算法:https://docs.nvidia.com/vpi/algo_tnr.html
去噪前:
去噪后:
硬件引擎在 VPI 中被命名为后端。这些后端使您能够卸载可并行处理阶段并通过使用 Jetson 设备固有的可用系统级并行性来加速应用程序。后端是 CPU、CUDA (GPU)、PVA 和 VIC。特定后端引擎的确切可用性取决于部署应用程序的 Jetson 平台。
VPI 目前为 TNR 提供了两种不同的实现方式,分别适合不同的场景和需求。这些版本采用双边滤波的组合来平滑平坦区域,同时保留边缘,并结合使用运动检测器的时间无限脉冲响应 (IIR) 滤波来处理跨帧的时间噪声。
- VPI_TNR_V2 —与 VPI_TNR_V3 相比,此版本提供了更轻的降噪和一定程度的可配置性,即可以调整光照条件以更好地适应给定场景。这个版本减少了计算需求,这转化为速度。它适用于执行时间比降噪质量更重要的用例。
- VPI_TNR_V3 —适用于需要更好质量的降噪的用例。使用此变体,与 VPI_TNR_V2 相比,您应该预计计算需求会增加。在此之上,可配置性得到进一步扩展。推荐用于具有挑战性的低光场景。
- VPI_TNR_DEFAULT —您可以使用默认值,而不是指定确切的版本,该值会选择给定后端支持的降噪最强的版本。
在决定哪种算法版本适合您的用例时要考虑的另一个标准是它对不同后端和设备的支持。下表总结了 TNR 支持。
VPI_TNR_V2 和 VPI_TNR_V3 都允许您明确设置您正在捕捉的场景的照明条件,从而启用调整。这在低光场景或以高增益捕获的流的背景下很重要,这些流可能包含更高的噪声级别,因此需要更高级别的降噪。
更高的强度级别可能会影响帧纹理区域中的细节数量,从而使它们平滑。另一个副作用是在有快速移动物体的场景中出现重影。支持的场景照明条件在类型(室内、室外)和强度(低、中和高)方面有所不同,如下表所示。
通过不同的版本和相关的照明条件预设,您可以根据用例的具体情况调整 TNR 算法。这可以通过所谓的强度系数进一步定制。它是一个范围从 0 到 1 的浮点参数,其中较大的值对应于增加的降噪强度。
通过不同的版本和相关的照明条件预设,您可以根据用例的具体情况调整 TNR 算法。这可以通过所谓的强度系数进一步定制。它是一个范围从 0 到 1 的浮点参数,其中较大的值对应于增加的降噪强度。
VPI应用
VPI 的关键方面之一是它如何管理和协调在不同后端之间运行应用程序所需的资源。使用 VPI,可以避免处理阶段之间浪费的内存副本。VPI 为高效内存管理而强制执行的另一种机制是其接口处的内存包装。
利用 VPI 的所有内存管理功能取决于您的代码的结构。最佳实践是将您的代码视为一个三阶段工作流:
- 初始化
- 处理循环
- 清理
大多数内存分配应该发生在初始化阶段。这在嵌入式应用程序的上下文中尤为重要,这些应用程序在可用资源方面有限制的设备上运行。最重要的是,可以更有效、更谨慎地进行内存管理,以避免可能的内存泄漏。
VPI 中的一个好做法是指定使用一块内存的后端。在这一点上,当管道在这些后端之间流动时,仅将 VPI 对象订阅到您需要的一组后端可确保您获得最有效的内存路径。
处理循环是执行处理管道的地方。想象一个应用程序迭代具有数百个单独帧的视频文件。主循环将主要负责对像素信息执行所需的转换,以实现给定计算机视觉任务的预期结果。
最后,清理阶段处理在任务执行期间使用的资源的所有必要释放和重新分配。坚持这种范式使 VPI 能够使用最高效的处理管道,并帮助您坚持良好的编码实践。
与 OpenCV 接口
VPI 与 OpenCV 的互操作性是该库的一个显着特征。如果您熟悉 OpenCV,您可以轻松地将 VPI 与您的工作流集成或扩展现有数据管道,以更好地使用 VPI 提供的硬件加速。
这在 TNR 示例中通过以下实用函数进行了演示,该函数将使用 OpenCV 捕获的输入视频帧包装到 VPI 图像对象中。
代码语言:javascript复制69 // Utility function to wrap a cv::Mat into a VPIImage
70 static VPIImage ToVPIImage(VPIImage image, const cv::Mat &frame)
71 {
72 if (image == nullptr)
73 {
74 // Create a VPIImage that wraps the frame
75 CHECK_STATUS(vpiImageCreateOpenCVMatWrapper(frame, 0, &image));
76 }
77 else
78 {
79 // reuse existing VPIImage wrapper to wrap the new frame.
80 CHECK_STATUS(vpiImageSetWrappedOpenCVMat(image, frame));
81 }
82 return image;
83 }
首先深入研究前面描述的函数。它旨在将 OpenCV 矩阵 ( cv::Mat) 对象包装到 VPI 图像对象 ( VPIImage) 中。就上下文而言,VPI 图像本质上是任何可以根据宽度、高度和格式进行描述的 2D 数据结构。尽管将图像数据视为VPIImage对象很直观,但其用途也可以扩展到其他类型的数据,例如 2D 矢量场和热图。
实用程序包装函数调用与 VPIOpenCVInterop.hpp模块相关的另外两个函数,旨在提供有用的基础设施来将基于 OpenCV 的代码与 VPI 集成。
vpiImageCreateOpenCVMatWrapper — 一个重载函数,将cv:Mat对象包装成VPIImage两种不同的风格。第一个尝试直接从输入类型(遵循特定规则)推断格式,而第二个将显式格式作为其参数之一。
vpiImageSetWrappedOpenCVMat —重用为特定cv::Mat对象定义的包装器来包装新的传入cv::Mat对象。这里的重点是避免首先创建包装器引起的内存分配,因此更有效。传入的cv::Mat对象必须具有与创建时使用的原始对象相同的特征(格式和尺寸)。
流创建
main 函数捕获设置 VPI 管道以完成工作的相关步骤。管道的定义很简单,也很直观。在 VPI 中,管道是流经不同处理阶段的一个或多个数据流的组合。
图 1 以通用方式显示了管道及其构建块(流、缓冲区、算法等)。为简单起见,省略了一些组件。
流的目的是强制执行数据需要通过的排队步骤序列来完成特定的计算机视觉任务。这些步骤可能包括数据的预处理或后处理,甚至包括 TNR 等成熟的算法。图 2 显示了 VPIStream 对象的示例。
VPI 适应不同范围的管道复杂性。您可以使用单个流实现一个简单的管道,或者使用多个并行流实现更复杂的实现,这些并行流将不同阶段卸载到不同的计算后端。这是 API 的一项强大功能,因为它使您能够更好地控制 Jetson 设备提供的系统级并行性。
以下代码示例演示了如何在 TNR 示例中创建流。
代码语言:javascript复制143 VPIStream stream;
144 // PVA backend doesn't have currently Convert Image Format algorithm.
145 // Use the CUDA backend to do that.
146 CHECK_STATUS(vpiStreamCreate(VPI_BACKEND_CUDA | backend, &stream));
正在将选择的后端传递到流中。这是一个可选步骤。使用零值将启用所有可用的后端。但是,推荐的做法是分配一组特定的后端,因为它有助于优化内存分配。
TNR 有效载荷
有效负载本质上是管道执行期间所需的临时资源。例如,有效载荷可以是一个中间内存缓冲区,用于存储在流的后续阶段之间交易的数据。许多算法,包括 TNR,都需要显式创建有效载荷,这可以通过以下方式实现。
代码语言:javascript复制172 // Create a TNR payload configured to process NV12
173 // frames under outdoor low-light scenarios.
174 VPIPayload tnr;
175 CHECK_STATUS(vpiCreateTemporalNoiseReduction(backend, w, h, VPI_IMAGE_FORMAT_NV12_ER, VPI_TNR_DEFAULT,
176 VPI_TNR_PRESET_INDOOR_LOW_LIGHT, 1, &tnr));
对于 TNR 负载,提供以下参数:
- 图片尺寸(宽高)
- 后端
- 图片数据的格式(目前只支持NV12)
- TNR算法版本
- 光照条件
- 降噪强度
- 参考算法有效载荷
最终,该函数创建一个有效负载并将其绑定到指定的后端。
图像缓冲区
除了流和负载创建之外,还必须创建 VPI 算法所需的图像缓冲区。在 TNR 中,使用双边和 IIR 滤波器的组合,因此需要三种不同的缓冲器;即当前和上一个图像输入和图像输出。
可以按如下方式创建图像缓冲区:
代码语言:javascript复制167 VPIImage imgPrevious, imgCurrent, imgOutput;
168 CHECK_STATUS(vpiImageCreate(w, h,VPI_IMAGE_FORMAT_NV12_ER, 0, &imgPrevious));
169 CHECK_STATUS(vpiImageCreate(w, h,VPI_IMAGE_FORMAT_NV12_ER, 0, &imgCurrent));
170 CHECK_STATUS(vpiImageCreate(w, h,VPI_IMAGE_FORMAT_NV12_ER, 0, &imgOutput));
这将创建具有以下指定特征的空缓冲区:
- 图片尺寸(宽高)
- 格式(根据算法要求)
- 图像标志(当前用于分配后端)
- 指向
VPIImage
返回创建图像句柄的变量的指针
流处理
构建块已经就位后,您可以进入主处理循环,在那里执行降噪算法。在 TNR 样本上,循环迭代视频文件中的每个单独帧,并执行必要的顺序步骤以实现所需的结果。
当从视频中收集帧时,第一步是VPIImage
使用前面描述的效用函数将其包装成一个对象。
186 frameBGR = ToVPIImage(frameBGR, cvFrame);
包装完成后,VPI 现在可以对 VPIImage 对象中的像素数据进行操作。由于 TNR 要求帧为 NV12 格式,因此需要一个转换步骤。
代码语言:javascript复制188 // First convert it to NV12
189 CHECK_STATUS(vpiSubmitConvertImageFormat(stream,VPI_BACKEND_CUDA, frameBGR, imgCurrent, NULL));
在这个阶段,转换图像的特定任务与之前实例化的流相关联。最重要的是,任务被设置为在 GPU 上执行。输入帧的图像缓冲区以及刚刚从cv::Mat
对象中包装的数据用于此目的。
当格式转换完成后,可以将输入缓冲区传递给 TNR 算法进行处理。
代码语言:javascript复制191 // Apply TNR
192 // For first frame, you must pass nullptr as the previous frame, this resets the internal
193 // state.
194 CHECK_STATUS(vpiSubmitTemporalNoiseReduction(stream, 0, tnr, curFrame == 1 ? nullptr : imgPrevious,
195 imgCurrent, imgOutput));
196
要调用 TNR 算法,请设置以下参数:
- 算法关联的流
- 后端
- 算法负载,如之前实例化的
- 图像缓冲区:以前和当前的输入和输出
在第一次迭代 ( curFrame == 1
) 时,缓冲区中没有有效的先前图像,而是传递了一个空指针。对于以下迭代,缓冲区会相应地填充。在执行 TNR 算法后,输出缓冲区可以从 NV12 转换回其先前的 BGR 格式。
197 // Convert output back to BGR
198 CHECK_STATUS(vpiSubmitConvertImageFormat(stream,VPI_BACKEND_CUDA, imgOutput, frameBGR, NULL));
在这一点上,重要的是要提到 VPI 对流阶段强制执行非阻塞异步范式。这对于作为后端的不同协处理器之间分布的工作负载的平稳和高效编排至关重要。对于进一步的步骤,请确保在继续之前已完成向流发出的所有活动。这时候同步功能就派上用场了。
代码语言:javascript复制199 CHECK_STATUS(vpiStreamSync(stream));
VPI 现在确保与流相关的每个正在进行的活动都已完成,然后再进入管道的下一个阶段。同步完成后,该帧已准备就绪并可在连接到指定后端的输出缓冲区中使用。为了能够将其写入输出视频流(在本例中为文件),必须锁定图像,以便 CPU 可以使用缓冲区。
这解释了为什么在锁定帧之前同步是避免处理问题的关键步骤。因为 VPI 是异步操作的,所以可能会发生在没有同步的情况下,缓冲区在前一阶段完成之前被锁定。这里的结果将是不可预测的。
代码语言:javascript复制201 // Now add it to the output video stream
202 VPIImageData imgdata;
203 CHECK_STATUS(vpiImageLock(frameBGR,VPI_LOCK_READ, &imgdata));
204
205 cv::Mat outFrame;
206 CHECK_STATUS(vpiImageDataExportOpenCVMat(imgdata, &outFrame));
207 outVideo << outFrame;
208
209 CHECK_STATUS(vpiImageUnlock(frameBGR));
如您所见,锁定的缓冲区由 CPU 处理以供进一步使用。锁被设置为只读,然后图像缓冲区被映射到 CPU。锁定时,VPI 无法在缓冲区上工作。CPU 将输出帧提供给视频编码器后,缓冲区可以解锁并进一步供 VPI 使用。
VPI数据流
TNR 示例应用程序可以总结为以下数据流。其他小步骤也是应用程序的一个组成部分,但为了简单起见,图 3 中只包含了宏步骤。
- 输入帧是从视频流或文件中收集的。OpenCV 已用于此目的。
- 必要的 VPI 元素被实例化:单个流、TNR 算法有效负载以及用于先前和当前输入和输出图像的图像缓冲区。
- 输入帧被包装到一个VPIImage缓冲区中。
- 缓冲区上的像素数据被转换为 NV12,以便 TNR 算法可以处理它。当算法完成执行时,它会恢复到其原始格式。
- 图像缓冲区被锁定,以便 CPU 可以访问数据。将图像提供给视频输出后,可以解锁缓冲区,VPI 可以进一步处理它。
翻译转载自:
https://developer.nvidia.com/blog/reducing-temporal-noise-on-images-with-vpi-on-jetson-embedded-computers/?ncid=so-twit-772627#cid=dl13_so-twit_en-sg