借助 NVIDIA 视觉编程接口库(VPI),您可以更有效地利用 Jetson Thor 的计算性能。
构建自主机器人需要具备可靠且低延迟的视觉感知能力,以实现在动态环境中的深度估计、障碍物识别、定位与导航。这些功能对计算性能有较高要求。NVIDIA Jetson平台虽为深度学习提供了强大的GPU支持,但随着AI模型复杂性的提升以及对实时性能的更高需求,GPU可能面临过载风险。若将所有感知任务完全依赖GPU执行,不仅容易造成性能瓶颈,还可能导致功耗上升和散热压力加剧,这在功耗受限且散热条件有限的移动机器人应用中尤为突出。
为解决上述挑战,NVIDIA Jetson平台将高性能GPU与专用硬件加速器相结合。Jetson AGX Orin和Jetson Thor等平台均配备专用硬件加速器,专为高效执行图像处理和计算机视觉任务而设计,从而释放GPU资源,使其能够专注于处理更复杂的深度学习工作负载。NVIDIA视觉编程接口(VPI)进一步充分激活了不同类型硬件加速器的性能潜力。
在本博客中,我们将探讨使用这些加速器的优势,并详细介绍开发者如何通过VPI充分发挥Jetson平台的性能潜力。作为示例,我们将展示如何运用这些加速器开发一个用于立体视差的低延迟、低功耗的感知应用。首先,我们将构建单路立体摄像头的工作流,随后扩展至多流工作流,在Thor T5000上支持8路立体摄像头同时以30 FPS运行,其性能相较Orin AGX 64 GB提升至10倍。
在开始开发之前,让我们快速了解Jetson平台提供的各类加速器,它们的优势所在,能够支持哪些应用场景,以及VPI如何为开发提供助力。
除了GPU之外,Jetson还配备了哪些其他加速器?
Jetson设备配备了强大的GPU,适用于深度学习任务,但随着AI复杂性的提升,对GPU资源的高效管理变得愈发重要。Jetson为计算机视觉(CV)工作负载提供了专用的硬件加速引擎。这些引擎与GPU协同工作,在保持灵活性的同时,显著提升了计算效率。通过VPI,开发者可以更便捷地访问这些硬件资源,简化实验流程并实现高效的负载分配。

图1:面向Jetson开发者的视觉编程接口(VPI)
下面我们逐一深入了解每个加速器,以及其用途与优势。
可编程视觉加速器(PVA):
PVA是一款可编程的数字信号处理(DSP)引擎,配备超过1024位的单指令多数据(SIMD)单元,以及支持灵活直接内存访问(DMA)的本地内存,专为视觉和图像处理任务优化,具备出色的每瓦性能。它能够与CPU、GPU及其他加速器异步运行,除NVIDIA Jetson Nano外,其他所有Jetson平台均配备该加速器。
通过VPI,开发者可以调用现成的算法,如AprilTag检测、物体追踪,和立体视差估计。对于需要自定义算法的场景,Jetson开发者现在还可使用PVA SDK,该SDK提供了C/C API及相关工具,支持直接在PVA上开发视觉算法。
光流加速器(OFA):
OFA是一种固定功能的硬件加速器,用于基于立体摄像头对的数据,计算光流和立体视差。OFA支持两种工作模式:在视差模式下,通过处理立体摄像头的左右校正图像来生成视差图;在光流模式下,则用于估算连续两帧之间的二维运动矢量。
视频和图像合成器(VIC):
VIC是Jetson设备中的一种专用硬件加速器,具备固定功能,能够高效节能地处理图像缩放、重映射、扭曲、色彩空间转换和降噪等基础图像处理任务。
哪些用例可以从这些加速器中获益?
在某些场景下,开发者可能会考虑采用GPU以外的解决方案,以更好地满足特定应用的需求。
GPU资源过载应用:为实现高效运行,开发者应优先将深度学习(DL)工作负载分配给GPU,同时利用VPI将计算机视觉任务卸载至PVA、OFA或VIC等专用加速器。例如,DeepStream的Multi Object Tracker在Orin AGX平台上若仅依赖GPU,可处理12路视频流;而通过引入PVA实现负载均衡后,支持的视频流数量可提升至16路。
功耗敏感型应用:在哨兵模式(sentry mode)或持续监控等场景中,将主要计算任务转移至低功耗加速器(如PVA、OFA、VIC),有助于显著提升效率。
存在热限制的工业应用:在高温运行环境下,合理分配任务至各类加速器可有效降低GPU负载,减少因过热导致的性能节流,从而在限定的热预算内维持稳定的延迟与吞吐表现。
如何使用VPI解锁所有加速器
VPI提供了一个统一且灵活的框架,使开发者能够在Jetson模组、工作站或配备独立GPU的PC等不同平台上无缝访问加速器。
现在,我们来看一个综合运用上述内容的示例。
示例:立体视觉工作流
现代机器人系统通常采用被动立体视觉技术实现对周围环境的三维感知。因此,计算立体视差图成为构建复杂感知系统的关键环节。本文将介绍一个示例流程,帮助开发者生成立体视差图及其对应的置信度图。同时,我们将展示如何利用VPI提供的各类加速器,构建低延迟、高能效的处理工作流。

图2:在Jetson多加速器上部署的立体视觉流程示意图。PVA :可编程视觉阵列;VIC:视频与图像合成器;OFA:光流加速器。
在CPU上进行预处理:预处理步骤可以在CPU上运行,因为它只发生一次。该步骤计算一个校正映射(rectification map),用于纠正立体相机帧中的镜头畸变。
在VIC上进行重映射:这一步骤使用预计算的校正映射对相机帧去畸变并对齐,确保两条光轴水平且平行。VPI支持多项式与鱼眼畸变模型,并允许开发者定义自定义warp映射。更多细节可参考Remap文档。
在OFA上计算立体视差:校正后的图像对作为半全局匹配(SGM)算法的输入。在实际应用中,SGM可能会产生噪声或错误的视差值。通过生成置信度图,可以剔除低置信度的视差估计,从而提升结果质量。有关SGM算法及其支持参数的更多信息,请参阅立体视差文档。
在PVA上生成置信度图:VPI提供三种置信度图模式:绝对值(Absolute)、相对值(Relative)和推理(Inference)。绝对值和相对值模式需要两个OFA通道(左/右视差)并结合PVA的交叉检查机制;而推理模式仅需一个OFA通道,并在PVA上运行一个轻量级CNN(包含两个卷积层和两个非线性激活层)。跳过置信度计算虽然速度较快,但会产生噪声视差图;相比之下,采用相对值或推理模式可显著提升视差结果的精度与可靠性。
VPI的统一内存架构避免了跨引擎的不必要数据复制,其异步流与事件机制使开发者能够提前规划任务负载和同步点。由硬件管理的调度支持跨引擎并行执行,既释放了CPU资源,又通过高效的流式流程设计避免了延迟。
使用VPI构建高性能立体视差工作流
开始使用Python API
本教程介绍如何使用VPI Python API实现基础的立体视差工作流,且无需进行图像重映射。
需要提前准备:
NVIDIA Jetson设备(例如Jetson AGX Thor)
通过NVIDIA SDK Manager或apt安装VPI
Python库:vpi、numpy、Pillow、opencv-python
在本教程中,我们将:
加载左右立体图像
转换图像格式以适配处理需求
同步数据流,确保信息准备就绪
执行立体匹配算法以计算视差
对输出结果进行后处理并保存
设置和初始化
第一步是导入所需的库并创建VPIStream对象。VPIStream充当命令队列,可用于提交任务以实现异步执行。为了演示并行处理,我们将使用两个流。
import vpi import numpy as np from PIL import Image from argparse import ArgumentParser # Create two streams for parallel processing streamLeft = vpi.Stream() streamRight = vpi.Stream()
streamLeft用于处理左侧图像,streamRight用于处理右侧图像。
加载和转换图像
VPI的Python API可直接使用NumPy数组。我们首先通过Pillow加载图像,然后利用VPI的asimage函数将其封装为VPI图像对象。接着,将图像转换为适用于立体匹配算法的格式。在本例中,图像将从RGBA8格式转换为Y8_ER_BL格式(即8位灰度、块线性布局)。
# Load images and wrap them in VPI images left_img = np.asarray(Image.open(args.left)) right_img = np.asarray(Image.open(args.right)) left = vpi.asimage(left_img) right = vpi.asimage(right_img) # Convert images to Y8_ER_BL format in parallel on different backends left = left.convert(vpi.Format.Y8_ER_BL, scale=1, stream=streamLeft, backend=vpi.Backend.VIC) right = right.convert(vpi.Format.Y8_ER_BL, scale=1, stream=streamRight, backend=vpi.Backend.CUDA)
左侧图像通过streamLeft提交至VIC后端进行处理,右侧图像则通过streamRight提交给NVIDIA CUDA后端。这种设计使得两项操作能够在不同的硬件单元上并行执行,充分体现了VPI的核心优势。
同步并执行立体差异
在执行立体差异计算之前,必须确保两张图像均已准备就绪。我们调用streamLeft.sync()来阻塞主线程,直至左侧图像的转换完成。随后,便可向streamRight提交vpi.stereodisp操作。
# Synchronize streamLeft to ensure the left image is ready streamLeft.sync() # Submit the stereo disparity operation on streamRight disparityS16 = vpi.stereodisp(left, right, backend=vpi.Backend.OFA|vpi.Backend.PVA|vpi.Backend.VIC, stream=streamRight)
立体差异算法在VPI后端(OFA、PVA、VIC)的组合上运行,以充分利用专用硬件,最终生成一张S16格式的差异图,用于表示两幅图像中对应像素之间的水平偏移。
后处理和可视化
对原始差异图进行后处理以实现可视化时,将Q10.5定点格式表示的差异值缩放到0-255范围内并保存。
# Post-process the disparity map
# Convert Q10.5 to U8 and scale for visualization
disparityU8 = disparityS16.convert(vpi.Format.U8, scale=255.0/(32*128), stream=streamRight, backend=vpi.Backend.CUDA)
# make accessible in cpu
disparityU8 = disparityU8.cpu()
#save with pillow
d_pil = Image.fromarray(disparityU8)
d_pil.save('./disparity.png')
最后一步是将原始数据转换为人类可读的图像,其中灰度值代表深度信息。
使用C API的多流差异工作流
先进的机器人技术依赖于高吞吐量,而VPI通过并行多流传输实现了这一需求。凭借简洁的API与硬件加速器的高效结合,VPI使开发者能够构建快速且可靠的视觉处理流程——与波士顿动力(Boston Dynamics)新一代机器人系统的处理流程相似。
VPI采用VPIStream对象,这些对象作为先进先出(FIFO)的命令队列,可异步地向后端提交任务,从而实现不同硬件单元上的并行运算执行(异步流)。
对于任务关键、追求极致性能的应用,VPI的C API是理想之选。
以下代码片段源自C 基准测试,用于演示多流立体视差工作流的构建与执行过程。该示例通过SimpleMultiStreamBenchmarkC 应用实现:首先预生成合成的NV12_BL格式图像,以消除运行时生成数据带来的开销;随后并行处理多个数据流,并测量每秒帧数(FPS)以评估吞吐性能。此外,该工具支持保存输入图像以及差异图和置信度图,便于调试分析。通过预生成数据的方式,本示例可有效模拟高速实时工作负载场景。
资源配置、对象声明与初始化
我们首先声明并初始化VPI中执行该流水线所需的全部对象,包括创建流、输入/输出图像以及立体视觉处理所需的有效载荷。由于立体算法的输入图像格式为NV12_BL,因此我们将其与Y8_Er图像类型一同设置为中间格式转换的格式。
int totalIterations = itersPerStream * numStreams;
std::vector
leftInputs(numStreams), rightInputs(numStreams), confidences(numStreams), leftT
mps(numStreams), rightTmps(numStreams);
std::vector
leftOuts(numStreams), rightOuts(numStreams), disparities(numStreams); std::vector
stereoPayloads(numStreams); std::vector
streamsLeft(numStreams), streamsRight(numStreams); std::vector
events(numStreams); int width = cvImageLeft.cols; int height = cvImageLeft.rows; int vic_pva_ofa = VPI_BACKEND_VIC | VPI_BACKEND_OFA | VPI_BACKEND_PVA; VPIStereoDisparityEstimatorCreationPa
rams stereoPayloadParams; VPIStereoDisparityEstimatorParams stereoParams; CHECK_STATUS(vpiInitStereoDisparityEstimatorCreationParams(&stereoPayloadParams)); CHECK_STATUS(vpiInitStereoDisparityEstimatorParams(&stereoParams)); stereoPayloadParams.maxDisparity = 128; stereoParams.maxDisparity= 128; stereoParams.confidenceType = VPI_STEREO_CONFIDENCE_RELATIVE; for (int i = 0; i < numStreams; i ) { CHECK_STATUS(vpiImageCreateWrapperOpenCVMat(cvImageLeft, 0, &leftInputs[i])); CHECK_STATUS(vpiImageCreateWrapperOpenCVMat(cvImageRight, 0, &rightInputs[i])); CHECK_STATUS(vpiStreamCreate(0, &streamsLeft[i])); CHECK_STATUS(vpiStreamCreate(0, &streamsRight[i])); CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_Y8_ER, 0, &leftTmps[i])); CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_NV12_BL, 0, &leftOuts[i])); CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_Y8_ER, 0, &rightTmps[i])); CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_NV12_BL, 0, &rightOuts[i])); CHECK_STATUS(vpiCreateStereoDisparityEstimator(vic_pva_ofa, width, height, VPI_IMAGE_FORMAT_NV12_BL, &stereoPayloadParams, &stereoPayloads[i])); CHECK_STATUS(vpiEventCreate(0, &events[i])); } int outCount = saveOutput ? (numStreams * itersPerStream) : numStreams; disparities.resize(outCount); confidences.resize(outCount); for (int i = 0; i < outCount; i ) { CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_S16, 0, &disparities[i])); CHECK_STATUS(vpiImageCreate(width, height, VPI_IMAGE_FORMAT_U16, 0, &confidences[i])); }
转换图像格式
我们使用VPI的C API为每个流提交图像转换操作,将来自摄像头的NV12_BL输入模拟帧进行格式转换。
for (int i = 0; i < numStreams; i )
{
CHECK_STATUS(vpiSubmitConvertImageFormat(streamsLeft[i], VPI_BACKEND_CPU, leftInputs[i], leftTmps[i], NULL));
CHECK_STATUS(vpiSubmitConvertImageFormat(streamsLeft[i], VPI_BACKEND_VIC, leftTmps[i], leftOuts[i], NULL));
CHECK_STATUS(vpiEventRecord(events[i], streamsLeft[i]));
CHECK_STATUS(vpiSubmitConvertImageFormat(streamsRight[i], VPI_BACKEND_CPU, rightInputs[i], rightTmps[i], NULL));
CHECK_STATUS(vpiSubmitConvertImageFormat(streamsRight[i], VPI_BACKEND_VIC, rightTmps[i], rightOuts[i], NULL));
CHECK_STATUS(vpiStreamWaitEvent(streamsRight[i], events[i]));
}
for (int i = 0; i < numStreams; i )
{
CHECK_STATUS(vpiStreamSync(streamsLeft[i]));
CHECK_STATUS(vpiStreamSync(streamsRight[i]));
}
我们将操作分别提交到两个独立流的不同硬件上,具体类型由输入/输出图像的类型推断得出。此次,我们还将在左侧流完成转换操作后记录一个VPIEvent。VPIEvent是一种VPI对象,能够在流录制过程中等待另一个流完成所有操作。通过这种方式,我们可以让右侧流等待左侧流的转换操作完成,而无需阻塞调用线程(即主线程),从而实现多个左侧流与右侧流的并行执行。
同步并执行立体差异
我们通过VPI的C API提交立体匹配计算任务,并使用std::chrono对其性能进行基准测试。
auto benchmarkStart = std::chrono::high_resolution_clock::now();
for (int iter = 0; iter < itersPerStream; iter )
{
for (int i = 0; i < numStreams; i )
{
int dispIdx = saveOutput ? (i * itersPerStream iter) : i;
CHECK_STATUS(vpiSubmitStereoDisparityEstimator(streamsRight[i], vic_pva_ofa, stereoPayloads[i], leftOuts[i],
rightOuts[i], disparities[dispIdx], confidences[dispIdx],
&stereoParams));
}
}
// ====================
// End Benchmarking
for (int i = 0; i < numStreams; i )
{
CHECK_STATUS(vpiStreamSync(streamsRight[i]));
}
auto benchmarkEnd = std::chrono::high_resolution_clock::now();
我们继续使用confidenceMap提交计算任务,并生成结果差异图。同时,停止基准测试计时器,记录转换和生成差异所耗的时间。在向所有流提交任务后,显式同步各个流,以确保调用线程在提交过程中不会被阻塞。
后处理和清理
我们利用VPI的C API与OpenCV的互操作性对差异图进行后处理,并在每次迭代循环中将其保存。可根据需要选择保留输出数据以供检查,循环结束后再清理相关对象。
// ====================
// Save Outputs
if (saveOutput)
{
for (int i = 0; i < numStreams * itersPerStream; i )
{
VPIImageData dispData, confData;
cv::Mat cvDisparity, cvDisparityColor, cvConfidence, cvMask;
CHECK_STATUS(
vpiImageLockData(disparities[i], VPI_LOCK_READ, VPI_IMAGE_BUFFER_HOST_PITCH_LINEAR, &dispData));
vpiImageDataExportOpenCVMat(dispData, &cvDisparity);
cvDisparity.convertTo(cvDisparity, CV_8UC1, 255.0 / (32 * stereoParams.maxDisparity), 0);
applyColorMap(cvDisparity, cvDisparityColor, cv::COLORMAP_JET);
CHECK_STATUS(vpiImageUnlock(disparities[i]));
std::ostringstream fpStream;
fpStream << "stream_" << i / itersPerStream << "_iter_" << i % itersPerStream << "_disparity.png";
imwrite(fpStream.str(), cvDisparityColor);
// Confidence output (U16 -> scale to 8-bit and save)
CHECK_STATUS(
vpiImageLockData(confidences[i], VPI_LOCK_READ, VPI_IMAGE_BUFFER_HOST_PITCH_LINEAR, &confData));
vpiImageDataExportOpenCVMat(confData, &cvConfidence);
cvConfidence.convertTo(cvConfidence, CV_8UC1, 255.0 / 65535.0, 0);
CHECK_STATUS(vpiImageUnlock(confidences[i]));
std::ostringstream fpStreamConf;
fpStreamConf << "stream_" << i / itersPerStream << "_iter_" << i % itersPerStream << "_confidence.png";
imwrite(fpStreamConf.str(), cvConfidence);
}
}
// ====================
// Clean Up VPI Objects
for (int i = 0; i < numStreams; i )
{
CHECK_STATUS(vpiStreamSync(streamsLeft[i]));
CHECK_STATUS(vpiStreamSync(streamsRight[i]));
vpiStreamDestroy(streamsLeft[i]);
vpiStreamDestroy(streamsRight[i]);
vpiImageDestroy(rightInputs[i]);
vpiImageDestroy(leftInputs[i]);
vpiImageDestroy(leftTmps[i]);
vpiImageDestroy(leftOuts[i]);
vpiImageDestroy(rightTmps[i]);
vpiImageDestroy(rightOuts[i]);
vpiPayloadDestroy(stereoPayloads[i]);
vpiEventDestroy(events[i]);
}
// Destroy all disparity and confidence images
for (int i = 0; i < (int)disparities.size(); i )
{
vpiImageDestroy(disparities[i]);
}
for (int i = 0; i < (int)confidences.size(); i )
{
vpiImageDestroy(confidences[i]);
}
收集基准测试结果
我们现在能够收集并展示基准测试的结果。
double totalTimeSeconds = totalTime / 1000000.0; double avgTimePerFrame = totalTimeSeconds / totalIterations; double throughputFPS= totalIterations / totalTimeSeconds; std::cout << "\n" << std::string(70, '=') << std::endl; std::cout << "SIMPLE MULTI-STREAM RESULTS" << std::endl; std::cout << std::string(70, '=') << std::endl; std::cout << "Input: RGB8 -> Y8_BL_ER" << std::endl; std::cout << "Total time: " << totalTimeSeconds << " seconds" << std::endl; std::cout << "Avg time per frame: " << (avgTimePerFrame * 1000) << " ms" << std::endl; std::cout << "THROUGHPUT: " << throughputFPS << " FPS" << std::endl; std::cout << std::string(70, '=') << std::endl; std::cout << "THROUGHPUT: " << throughputFPS << " FPS" << std::endl; std::cout << std::string(70, '=') << std::endl;
查看结果
在图像分辨率为960 × 600、最大视差为128的条件下,该方案在Thor T5000上以30 FPS的帧率运行立体视差估计,同时支持8个并行数据流(包括置信度图),且无需占用GPU资源。在MAX_N功耗模式下,其性能相比Orin AGX 64 GB提升至10倍。具体性能数据如表1所示。
| 立体视差全工作流(相对模式,分辨率:960 × 600,最大差异:128)的帧率(FPS)加速比随流数量变化情况:Orin AGX(64 GB)、Jetson Thor、T5000分别达到122、122.5、212、111.9、54.6、58.9、78.3、299.7。 | |||
| 帧率(FPS) | 加速比 | ||
| 流数量 | Orin AGX(64 GB) | Jetson Thor T5000 | |
| 1 | 22 | 122 | 5.5 |
| 2 | 12 | 111 | 9.5 |
| 4 | 6 | 58 | 9.7 |
| 8 | 3 | 29 | 9.7 |
表1:Orin AGX与Thor T5000在RELATIVE模式下,立体视差处理流程的对比
波士顿动力如何使用VPI
作为Jetson平台的深度用户,波士顿动力借助视觉编程接口(VPI)来加速其感知系统的处理流程。
VPI支持无缝访问Jetson的专用硬件加速器,提供一系列优化的视觉算法(如AprilTags和SGM立体匹配),以及ORB、Harris Corner、Pyramidal LK等特征检测器,和由OFA加速的光流计算。这些技术构成了波士顿动力感知系统的核心,可通过负载均衡同时支撑原型验证与系统优化。通过采用VPI,工程师能够快速适配硬件更新,显著缩短从开发到实现价值的周期。
要点总结
Jetson Thor平台以及VPI等库在硬件功能上的进步,使开发者能够为边缘端机器人设计出高效且低延迟的解决方案。
通过充分发挥Jetson平台上各款可用加速器的独特优势,像波士顿动力这样的机器人公司能够实现高效且可扩展的复杂视觉处理,从而推动智能自主机器人在多种现实应用场景中的落地与发展。
关于作者
Chintan Intwala 是 NVIDIA 核心计算机视觉产品管理团队的成员,专注于构建 AI 赋能的云技术,为各行各业的大型计算机视觉开发者提供支持。在加入 NVIDIA 之前,Chintan 曾在 Adobe 工作,专注于构建 AI/ML 和 AR/Camera 产品及功能。他拥有麻省理工学院斯隆分校 (MIT Sloan) 的 MBA 学位,并已获得超过 25 项美国专利。
Jonas Toelke 是 NVIDIA 核心计算机视觉团队的一员,领导的团队专注于构建经过优化的计算机视觉产品,为各行各业的 TegraSoC 提供支持。加入 NVIDIA 之前,Jonas 曾就职于 Halliburton,专注于构建 AI/ ML 应用以解决石油物理问题。他拥有慕尼黑理工大学工程学博士学位,并已获得 20 项专利。
Colin Tracey 是 NVIDIA 的高级系统软件工程师。他在 Jetson 和 DRIVE 平台的嵌入式计算机视觉库和 SDK 上工作。
Arjun Verma 是 NVIDIA 的系统软件工程师,也是 NVIDIA 嵌入式设备计算机视觉产品的核心开发者。Arjun 最近刚从佐治亚理工学院获得机器学习硕士学位。
关注
213文章
30699浏览量
220119关注
33文章
9456浏览量
156341免责声明:本文为转载,非本网原创内容,不代表本网观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如有疑问请发送邮件至:bangqikeconnect@gmail.com