- 1.下载SDK Manager
- 2.安装SDK Manager
- 3.启动SDK Manager
- 4.在Host上安装JetPack
- 5.JetPack VPI简介
- 6.VPI的安装
- 7.JetPack VPI初体验
- 8.VPI中的基本概念
- 9.简单示例——图像模糊
- 10.VPI支持的函数与平台
- 11.参考资料
因为一些契机,需要在实验室之前买的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 Estimator和Remap。根据名字就知道,一个是双目视差估计,一个是重映射。运行这些例子之前,先确保系统(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 numpy
,pip3 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
本文作者原创,未经许可不得转载,谢谢配合