计算机视觉/视频分析

借助 NVIDIA Jetson Thor 提升机器人感知效能

借助 NVIDIA 视觉编程接口库,您可以更有效地利用所有 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,开发者可以更便捷地访问这些硬件资源,简化实验流程并实现高效的负载分配。

Diagram showing the relationship between Jetson hardware, VPI operators, and developer applications. Diagram showing the relationship between Jetson hardware, VPI operators, and developer applications.
图 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 路。
  • 功耗敏感型应用: 在前哨模式或持续监控等场景中,将主要计算任务转移至低功耗加速器(如 PVA、OFA、VIC),有助于显著提升能效。
  • 工业级热约束应用: 在高温运行环境下,合理分配任务至各类加速器可有效降低 GPU 负载,减少因过热导致的性能节流,从而在限定的热预算内维持稳定的延迟与吞吐表现。

如何使用 VPI 解锁所有加速器

VPI 提供了一个统一且灵活的框架,使开发者能够在 Jetson 模组、工作站或配备独立 GPU 的 PC 等不同平台上无缝访问加速器。

现在,我们来看一个综合运用上述内容的示例。

示例:立体视觉工作流

现代机器人系统通常采用被动立体视觉技术实现对周围环境的三维感知。因此,计算立体视差图成为构建复杂感知系统的关键环节。本文将介绍一个示例流程,帮助开发者生成立体视差图及其对应的置信度图。同时,我们将展示如何利用 VPI 提供的各类加速器,构建低延迟、高能效的处理工作流。

A sample stereo vision pipeline using multiple Jetson accelerators A sample stereo vision pipeline using multiple Jetson accelerators
图2。立体视觉工作流在Jetson多个加速器上的部署示意图。PVA+:可编程视觉阵列;VIC:视频与图像合成器;OFA:光流加速器。
  • 在 CPU 上进行预处理:由于预处理仅需执行一次,因此适合在 CPU 上完成。该步骤用于计算校正图,以校正立体相机图像中的镜头失真。
  • 在 VIC 上进行重映射:此阶段利用预先计算的校正图消除图像失真,并对齐双摄像头的帧,确保光轴保持水平且平行。VPI 支持多种失真模型,包括多项式和鱼眼模型,同时允许开发者自定义映射方式。更多细节可参考 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++ 基准测试,用于演示多流立体差异工作流的构建与执行过程。该示例通过 SimpleMultiStreamBenchmark C++ 应用实现:首先预生成合成的 NV12_BL 格式图像,以消除运行时生成数据带来的开销;随后并行处理多个数据流,并测量每秒帧数(FPS)以评估吞吐性能。此外,该工具支持保存输入图像以及差异图和置信度图,便于调试分析。通过预生成数据的方式,本示例可有效模拟高速实时工作负载场景。

资源配置、对象声明与初始化

我们首先声明并初始化 VPI 中执行该流水线所需的全部对象,包括创建流、输入/输出图像以及立体视觉处理所需的有效载荷。由于立体算法的输入图像格式为 NV12_BL,因此我们将其与 Y8_Er 图像类型一同设置为中间格式转换的格式。

int totalIterations = itersPerStream * numStreams;
std::vector<VPIImage> leftInputs(numStreams), rightInputs(numStreams), confidences(numStreams), leftTmps(numStreams), rightTmps(numStreams);
std::vector<VPIImage> leftOuts(numStreams), rightOuts(numStreams), disparities(numStreams);
std::vector<VPIPayload> stereoPayloads(numStreams);
std::vector<VPIStream> streamsLeft(numStreams), streamsRight(numStreams);
std::vector<VPIEvent> events(numStreams);
int width   = cvImageLeft.cols;
int height  = cvImageLeft.rows;
int vic_pva_ofa = VPI_BACKEND_VIC | VPI_BACKEND_OFA | VPI_BACKEND_PVA;
VPIStereoDisparityEstimatorCreationParams 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 在 RELATIVE 模式下立体差异处理流程与 Thor T5000 的对比

Boston Dynamics 如何使用 VPI 

作为 Jetson 平台的深度用户,波士顿动力公司借助视觉编程接口(VPI)来加速其感知系统的处理流程。

VPI 支持无缝访问 Jetson 的专用硬件加速器,提供一系列优化的视觉算法(如 AprilTags 和 SGM 立体匹配)以及特征检测器(如 ORB、Harris 角点、金字塔光流法和基于 OFA 的光流算法)。这些功能构成了 Boston Dynamics 感知系统的核心,支持负载均衡,便于进行原型验证与系统优化。通过采用 VPI,工程师能够快速适应硬件更新,显著缩短从开发到实现价值的周期。

要点

Jetson Thor 平台以及 VPI 等库在硬件功能上的进步,使开发者能够为边缘端机器人设计出高效且低延迟的解决方案。

通过充分发挥 Jetson 平台上各款可用加速器的独特优势,机器人公司如 Boston Dynamics 能够实现高效且可扩展的复杂视觉处理,从而推动智能自主机器人在多种现实应用场景中的落地与发展。

要开始在 Jetson 上开发自己的计算机视觉应用,建议参考以下内容:

 

标签