NVIDIA JetPack VPI笔记1:简介、安装与初步使用

Mar 14,2023   9057 words   33 min

Tags: GPU

因为一些契机,需要在实验室之前买的Jetson AGX Xavier嵌入式平台上实现一些算法,并且对于效率还有一定的要求。之前基于通用的OpenCV库实现了基本功能,但效率还有优化空间。所以这篇笔记主要介绍NVIDIA推出的一个跨平台、高校视觉编程接口VPI,它是作为JetPack SDK的一部分发布的。

1.下载SDK Manager

首先,打开Jetpack SDK的官网,然后找到SDK Manager的下载链接,如下所示。 等待下载完成即可。需要注意的是,下载Jetpack需要NVIDIA开发者账户,如果没有可以注册一个。

2.安装SDK Manager

下载完成以后,它就是一个常规的.deb软件包,打开终端,然后通过dpkg -i命令安装即可,如下。

3.启动SDK Manager

安装完成以后,可以在Ubuntu的所有程序(左上角或者左下角的9个点点的图标)里搜索”sdkmanager”找到程序,或者在终端中输入sdkmanager启动程序,启动后首先需要登录,登录完成以后,默认界面如下所示。 当然,一个细节是,SDK Manager不能以root账户运行,如果是root账户的话,建议换成普通的账户打开。

4.在Host上安装JetPack

因为是在Host设备上安装JetPack,所以我们需要把“Host Machine”勾选上,把”Target Hardware”取消勾选,如下所示。 完成以后点击Continue,进入第二步,如下所示。 这里列出了我们需要安装的组件,我们在下面指定安装路径,并勾选同意协议以后就可以进入下一步了。这里可以看到,在Computer Vision下面就有我们想要的VPI相关库。点击下一步之后,程序会检查当前网络状况,如下所示。 根据你的网络状态,这一步可能会很耗时,也可能很快。完成网络检查后,程序就会开始自动下载并安装组件了,如下所示。 因为我是校园网,所以下载速度还算比较快。最终安装完成后,如下所示。

5.JetPack VPI简介

根据官网描述,VPI全称为Visual Programming Interface,中文为视觉编程接口。是一个支持跨软硬件平台的软件库,可以实现常见的视觉编程功能。相比于常规的OpenCV,它的优势在于高效,针对GPU以及NVIDIA平台的硬件设备做了进一步优化,效率更高,如下图所示。 根据官网描述,相比于常规的OpenCV,在CPU下快7倍,在GPU下快11倍。另外一个优势就是多硬件平台支持,如下所示。 可以看到,相比于OpenCV它支持更多的硬件平台。我们只要开发好代码,就可以几乎不用修改的放到不同平台运行。比如在Host电脑上开发算法,测试完成以后可以直接放到有JetPack VPI的嵌入式平台上(如Jetson TX2)运行,无需修改代码。当然了,尽管基于OpenCV的程序也可以做到这点,但还是如第一点所说,效率会更高一些。

那么VPI支持哪些基本的功能呢?在官网中同样给出了一些说明,如下。 可以看到,一些常见的像素级操作、直方图处理等都是支持的。

6.VPI的安装

其实在前面第四步的时候,我们就已经随着JetPack SDK安装好了VPI。安装好以后,会在你电脑上创建/opt/nvidia/vpi2文件夹,里面包含了一些必要的头文件、示例程序等,如下所示。 当然,除了通过JetPack SDK Manager的方式安装,还可以通过apt的方式安装,需要的话可以查看这个官方文档。

根据官方文档,VPI的Python接口只支持Python3.8(Ubuntu 18)或Python3.9(Ubuntu 20)。所以如果你的电脑里没有对应环境就不太能跑VPI的Python接口。不过好在安装完VPI以后,会自动安装一个Python3.8,我们可以直接在终端中输入python3.8就能进入环境,如下。 一般情况下,系统里可能会有多个Python环境并存,所以我们可以进入Python环境,然后输入如下代码,就会打印出当前运行的Python环境的路径。

import sys
print(sys.path)

或者,我们可以使用Ubuntu的which命令直接查看地址,比如在终端输入which python3.8,输出如下。如果找不到路径,就什么都不输出。 那么如何查看系统中安装的全部Python位置呢,可以利用whereis命令,在终端中输入whereis python即会输出所有的Python环境位置,如下。 可以看到,主要安装了Python2.7, 3.6, 3.8。针对多Python版本并存的情况,我们可以使用update-alternatives进行管理。比如当前,python3默认指的是3.6,我们可以将其修改为3.8。步骤如下。首先分别利用which查找Python3.6, 3.8的路径,如下。 获得了它们的路径以后,就可以使用update-alternatives切换版本了,如下。

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 2

然后,我们可以在终端中输入sudo update-alternatives --config python3来配置刚刚新建的python3变量,如下。 我们可以根据提示,选择想要的Python版本。比如这里,我们选择了3.8版本。完成以后,我们再在终端中输入python3,进入的就是3.8环境了。

到这里,还有最后一个问题,就是pip的配置。VPI自带的这个Python3.8环境没有pip,也就意味着不能方便地通过pip安装各种包,我们需要手动安装一下。在终端中输入python3 -m pip install --upgrade pip即可完成安装。当然,如果你只是想测试一下,也可以不用这么麻烦地配Python环境。

7.JetPack VPI初体验

本部分主要参考这个官方文档。官方给了两个带界面的例子,分别是Stereo Disparity EstimatorRemap。根据名字就知道,一个是双目视差估计,一个是重映射。运行这些例子之前,先确保系统(Ubuntu 18)中安装了如下的库。

sudo apt-get install -y libopencv-core3.2 libopencv-imgcodecs3.2 libopencv-videoio3.2 libopencv-calib3d3.2
sudo apt-get install -y libfltk1.3 libfltk-gl1.3 libfltk-images1.3
sudo apt-get install -y libgl1

然后,我们在所有程序里搜索VPI,如下就能看到这两个例子的图标了。

运行双目例子效果如下。 可以看到,可以以非常快的速度进行稠密双目视差估计,而通过监控GPU使用情况也可以看到确实是在用CUDA进行加速。类似的,我们可以运行重映射示例。 这个例子展示了像素级的变换,可以做到非常高效。

8.VPI中的基本概念

本部分主要参考这个官方文档。总体而言,VPI适合以异步(asynchronous)的方式实现一些实时的影像处理需求。这主要通过计算流(stream)来实现,不同流之间的同步通过事件(event)来完成。在进一步介绍之前,需要先明确一些概念。

8.1 流(Streams)

简单来说,VPIStream是一个基于后端设备支撑、顺序执行算法的异步队列(A VPIStream is an asynchronous queue that executes algorithms in sequence on a given backend device)。不同流之间可以通过同步机制的帮助来交换数据。

8.2 后端(Backends)

后端主要是指运行我们编写算法的硬件平台。目前VPI支持的后端包括:CPU、使用CUDA的GPU、PVA(Programmable Vision Accrlerator)、VIC(Video and Image Compositor)、NVENC(Video encoder engine)、OFA(Optical Flow Accerlerator),详细说明见下表。 简单翻译如下:

  • CPU: 所有x86(Linux)和Jetson aarch64平台
  • GPU: 所有带有Maxwell架构及以上GPU的x86(Linux)和Jetson aarch64平台
  • PVA: 所有Jetson AGX Xavier系列和Jetson Xavier NX设备
  • VIC: 所有Jetson设备
  • NVENC: 所有Jetson设备(注:只有在Jetson AGX Xavier系列上的NVENC支持稠密光流)
  • OFA: Jetson AGX Orin设备
8.3 算法(Algorithms)

VPI支持多种计算机视觉算法。有些算法会使用临时缓冲区(temporary buffers),又被成为VPIPayload。这种Payload可以在一开始创建一次,后面重复使用。有时,payload也会基于给定影像的大小来创建,这种情况下,如果影像大小发生变化,那么payload就需要重新创建。

8.4 数据缓冲区(Data Buffers)

VPI会为算法将需要处理的数据自动封装到数据缓冲区中,目前支持2D影像、1D数组和2D影像金字塔。在VPI中,为了加速处理,会默认尝试使用浅拷贝来处理数据,如果目标平台不支持这种操作,那么就会无缝切换为深拷贝。

8.5 2D影像(2D Images)

在VPI中,2D影像其实就是内存中指定宽、高和影像格式的区域。一旦影像的尺寸和格式定义好了就不能再修改。

8.6 1D数组(1D Arrays)

1D数组其实和2D影像类似的,本质上都是一块内存空间。所不同的是,数组的大小可以随时修改,只要不超过设备所能承受的上限即可。

8.7 2D影像金字塔(2D Image Pyramids)

本质上来说就是具有相同格式的2D影像的集合。在VPI中是按如下定义的:

  • 金字塔的层数,从细到粗
  • 最精细的那一层的影像宽高
  • 金字塔层级之间的缩放比例
  • 影像的格式
8.8 同步机制(Synchronization Primitives)

前面说了,VPI通过异步的执行流来提升效率,因此VPI中提供了几种流之间的同步机制。 我们可以通过calling线程来同步等待所有正在执行的流,这可以用在比如可视化上。或者,我们可以通过VPIEvent进行更细粒度的同步。这一块目前简单了解即可,之后会有进一步介绍。

8.9 VPI应用(VPI Applications)

对于一个VPI应用而言,其主要包含三个阶段:

  • 初始化阶段(Initialization):在该阶段中会分配内存、创建如流、影像、数组等对象,执行一些比较耗时的、一次性的操作
  • 处理循环阶段(Processing Loop):随着外部数据的输入,程序开始循环执行编写的算法。在这个阶段中会提交在初始化阶段创建的payloads
  • 清理阶段(Cleanup):销毁初始化以及运行时用到的一些变量

9.简单示例——图像模糊

本部分主要参考这个官方文档,如下所示。这个例子实现了从硬盘读取一张影像、调用VPI基于CUDA进行模糊、最后输出保存到硬盘的这个流程。 VPI提供了C++和Python两个接口。和常识认知相同的,Python可以用于一些算法的原型开发与测试,C++则适合于对全流程的完全掌控与极致的性能追求。下面简单介绍。

9.1 图像模糊的Python接口示例

本部分主要参考这个文档。这个官方例子除了VPI,还用到了Numpy和Pillow包,所以可以通过pip3 install numpypip3 install pillow来安装,如下。 当然,除了这些库,还可以把OpenCV也装上,方便使用和对比。这样我们就把所有准备工作做完了,可以进入Python环境,然后import vpi,是没有报错的。

利用Python接口实现图像模糊的步骤也非常简单,代码如下。代码在官方原版基础上进一步精简,保留了核心步骤。

import vpi
import cv2

if __name__ == '__main__':
    img_path = "../imgs/genshin.jpg"
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

    # step1 将读取的Numpy Array对象转换成VPI的影像格式
    input = vpi.asimage(img)

    # step2 开始执行模糊卷积
    # 对于任何一个VPI函数都需要显式指定backend(以函数参数方式指定或者以with方式指定)
    # box_filter是VPI的函数
    with vpi.Backend.CUDA:
        output = input.box_filter(5, border=vpi.Border.ZERO)

    # step3 结果输出
    with output.rlock_cpu() as outData:
        cv2.imwrite("../imgs/blurred_with_python.jpg", outData)

执行完以后,就会输出一个blurred.jpg,原图与输出结果如下所示。

当然,我们可以看看耗时情况,并与OpenCV进行对比,代码如下。

import vpi
import cv2
import time

if __name__ == '__main__':
    img_path = "../imgs/genshin.jpg"
    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

    input = vpi.asimage(img)

    t1 = time.time()
    with vpi.Backend.CUDA:
        output = input.box_filter(5, border=vpi.Border.ZERO)
    t2 = time.time()
    
    dt_vpi_cuda = 1000 * (t2 - t1)
    print("dt_vpi_cuda:", dt_vpi_cuda, "ms")

    t3 = time.time()
    output_blur = cv2.blur(img, (5, 5))
    t4 = time.time()
    
    dt_opencv = 1000 * (t4 - t3)
    print("dt_opencv:", dt_opencv, "ms")

运行以后,输出的时间对比如下。 可以看到,基于CUDA的VPI模糊处理一张2048x1188的图片大约需要0.28ms,OpenCV需要1.19ms,大约节省了76%的时间,确实效果还是挺显著的。而且经过测试,在我的这台电脑上,选择CUDA作为后端和CPU作为后端,耗时基本是相同的。

至此,我们便完成了基于Python接口的图像模糊。

9.2 图像模糊的C++接口示例

本部分主要参考这个文档。对于C++接口,其实是类似的。同样VPI只支持Ubuntu18.04及以上的系统。可以在终端中输入如下内容安装一些必要的包: sudo apt-get install g++ cmake libopencv-dev。然后就可以写代码了。

首先是CMakeLists.txt文件,内容如下。

cmake_minimum_required(VERSION 3.10)
project(vpi_demo)

set(CMAKE_CXX_STANDARD 11)

# 寻找VPI和OpenCV
find_package(vpi REQUIRED)
find_package(OpenCV REQUIRED)

add_executable(vpi_demo main.cpp)
# 链接库文件
target_link_libraries(vpi_demo vpi ${OpenCV_LIBS})

然后,主体代码文件main.cpp如下。

#include <iostream>

#include <vpi/OpenCVInterop.hpp>
#include <vpi/Image.h>
#include <vpi/Stream.h>
#include <vpi/algo/BoxFilter.h>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main() {
    // 第一阶段:初始化
    // step1 利用OpenCV读取影像
    string img_path = "../../imgs/genshin.jpg";
    Mat img = imread(img_path, IMREAD_GRAYSCALE);

    // step2 创建stream,第一个参数是backend,如果指定为0则表示可以在任何backend执行
    VPIStream stream;
    vpiStreamCreate(0, &stream);

    // step3 基于读取的影像构造VPIImage对象
    VPIImage image;
    vpiImageCreateWrapperOpenCVMat(img, 0, &image);

    // step4 新建VPIImage变量用于储存模糊结果
    VPIImage blurred;
    vpiImageCreate(img.cols, img.rows, VPI_IMAGE_FORMAT_U8, 0, &blurred);

    // 第二阶段:执行
    time_t t1 = clock();

    // step1 开始滤波
    vpiSubmitBoxFilter(stream, VPI_BACKEND_CUDA, image, blurred, 5, 5, VPI_BORDER_ZERO);

    // 等待所有操作执行完成
    vpiStreamSync(stream);
    time_t t2 = clock();
    double dt = 1000 * (double) (t2 - t1) / CLOCKS_PER_SEC;
    cout << "vpi cost time: " << dt << " ms" << endl;

    // step2 锁定blurred对象,取出数据
    VPIImageData outData;
    vpiImageLockData(blurred, VPI_LOCK_READ, VPI_IMAGE_BUFFER_HOST_PITCH_LINEAR, &outData);

    // step3 将取出的数据转换为OpenCV格式并保存
    Mat out_mat;
    vpiImageDataExportOpenCVMat(outData, &out_mat);
    imwrite("../../imgs/blurred_with_cpp.jpg", out_mat);

    // step4 解除对于blurred对象的锁定
    vpiImageUnlock(blurred);

    // 第三阶段:清理
    vpiStreamDestroy(stream);
    vpiImageDestroy(image);
    vpiImageDestroy(blurred);

    // 额外步骤,对比OpenCV的速度
    Mat out_img_opencv;
    time_t t3 = clock();
    blur(img, out_img_opencv, Size(5, 5));
    time_t t4 = clock();
    double dt2 = 1000 * (double) (t4 - t3) / CLOCKS_PER_SEC;
    cout << "opencv cost time: " << dt2 << " ms" << endl;

    return 0;
}

类似的,运行结束以后,会输出模糊影像。控制台输出时间。 可以看到,和Python接口类似的,相比于OpenCV有比较显著的提升,提升了一半以上。上面提到的Python和C++的例子,完整项目都放到了Github上,感兴趣可以点击查看,欢迎Fork或Star。

10.VPI支持的函数与平台

目前,VPI 2.2版本支持的函数和平台关系如下,内容摘录自这个官方文档 总体而言,CUDA后端是支持最全的。至此,本篇笔记的主要内容就结束了。之后我们会稍微详细地讨论一下VPI的架构以及一些示例程序,并尝试写一个实际的例子。

11.参考资料

  • [1] https://developer.nvidia.cn/embedded/vpi
  • [2] https://docs.nvidia.com/vpi/installation.html
  • [3] https://docs.nvidia.com/vpi/demo_apps.html
  • [4] https://docs.nvidia.com/vpi/algo_stereo_disparity.html
  • [5] https://docs.nvidia.com/vpi/algo_remap.html
  • [6] https://docs.nvidia.com/vpi/basic_concepts.html
  • [7] https://docs.nvidia.com/vpi/tutorial.html
  • [8] https://docs.nvidia.com/vpi/tutorial_python.html
  • [9] https://docs.nvidia.com/vpi/tutorial_c.html
  • [10] https://docs.nvidia.com/vpi/algorithms.html

本文作者原创,未经许可不得转载,谢谢配合

返回顶部