教程:VENTUNO Q/实时语义分割

来自WindwizPi wiki

概述

本教程将在 Arduino® VENTUNO™ Q 上运行实时语义分割演示,使用来自 Qualcomm® AI Hub 的 segformer_base 模型。SegFormer 是基于 transformer 的分割模型(nvidia/segformer-b0-finetuned-ade-512-512),将每个像素分类为 150 个 ADE20K 场景类别之一(墙、地板、人、椅子、天空等)。该模型完全在开发板 Qualcomm® Dragonwing™ QCS8275 处理器的 Qualcomm® Hexagon™ NPU 上运行。

该演示是一个独立的 Python 脚本(segformer_camera.py),从 USB 相机读取画面,实时在实时画面上叠加彩色语义分割掩码,并在角落显示当前画面中最常见类别的小图例。

本指南将涵盖:

  1. 为 VENTUNO Q 供电并访问开发板。
  2. 设置 Python 虚拟环境。
  3. 直接在开发板上下载模型文件。
  4. 在 CPU 与 NPU 上运行分割演示。

硬件与软件要求

硬件

说明:

将 VENTUNO Q 用作 SBC 时,鼠标并非必需(但会更方便)。

软件

  • 主机上可用的 adb(Android® Platform Tools)或 ssh
  • Python 3.12(已预装在 VENTUNO Q 上)

警告:

必须先用电源为 VENTUNO Q 供电,然后再用 USB-C® 线缆连接主机,否则开发板可能崩溃。建议使用功率至少 65 W、电压范围 7–24 V 的电源。

访问开发板 Shell

在开发板通过电源圆口供电后,可使用 adb 或 ssh 访问 VENTUNO Q 上的 shell(终端)。

要通过 adb 连接,用 USB-C® 线缆连接 VENTUNO Q 与电脑,然后运行:

# Using adb (Android Debug Bridge)
adb shell

要通过 ssh 连接,请确保 VENTUNO Q 与电脑连接到同一网络,然后运行:

# Using ssh (Secure Shell)
ssh arduino@<ip-address>

若不知道开发板的 IP 地址,可连接键盘和显示器并在开发板上运行 hostname -I,或先用 sudo nmtui 配置 Wi-Fi®。

说明:

有关远程访问开发板的更多方式,请参阅 Remote Access 教程。

警告:

由于此演示会在屏幕上打开实时相机窗口,请在开发板的实际桌面会话中运行(物理显示器与键盘,或进入开发板的 VNC/X11 会话)。adb 与 ssh 仍是安装依赖并确认脚本可运行的最简便方式,然后再切换到桌面会话观看相机画面。

设置 Python 环境

本节中的所有命令都在 VENTUNO Q 上运行。

1. 创建虚拟环境

创建并激活 Python 虚拟环境以隔离依赖:

python3 -m venv ~/.venv
source ~/.venv/bin/activate

提示符中的 (.venv) 前缀表示环境已激活。每个新会话开始时请再次运行 source 命令。

2. 安装依赖

此演示需要 opencv-python、numpy 和 ai-edge-litert(后者捆绑了 TFLite 解释器以及用于 NPU 执行的 QNN HTP 委托加载器):

pip install ai-edge-litert==1.3.0 opencv-python numpy

NPU 执行还需要 QNN HTP 委托(libQnnTFLiteDelegate.so),由 Qualcomm® AI Runtime (QAIRT) 提供。它默认未安装,也不会被任何 pip 包拉取,因此请从开发板的 apt 仓库安装:

sudo apt update
sudo apt install qairt-libs qairt-dsp-binaries

继续之前请确认委托已存在:

ls /usr/lib/libQnnTFLiteDelegate.so

下载模型文件

除 Python 脚本外,演示还需要一个模型文件。你可以直接在开发板上下载并准备——无需从主机传输:

文件 说明 来源
segformer_base-w8a8.tflite 量化分割模型,在 NPU 上运行 Qualcomm AI Hub

首先,在开发板上创建并进入工作目录:

mkdir -p /home/arduino/segformer-seg
cd /home/arduino/segformer-seg

模型来自 Qualcomm AI Hub 上的 segformer_base,通过 qai-hub-models CLI 获取。请先安装该工具(体积较大,可能需要一些时间),然后拉取量化精度版本:

pip install qai-hub-models

# Quantized precision -> extracts segformer_base-tflite-w8a8/
qai-hub-models fetch segformer_base -r tflite -p w8a8 -o .

这会解压出一个包含单个 segformer_base.tflite 的 segformer_base-tflite-w8a8/ 文件夹。与多阶段模型不同,这是单阶段模型,因此无需丢弃额外文件。将其重命名为脚本所期望的文件名:

cp segformer_base-tflite-w8a8/segformer_base.tflite ./segformer_base-w8a8.tflite

完成此步骤后,/home/arduino/segformer-seg 目录中应包含 segformer_base-w8a8.tflite。

警告:

qai-hub-models 依赖 ai-edge-litert>=2.0.2,因此安装它会升级你先前安装的 ai-edge-litert==1.3.0。2.x 版本无法与 QNN HTP 委托配合:委托会以 Failed to validate op ... Conv2d 拒绝每个卷积,并静默回退到 CPU,由于额外的委托开销,最终会比纯 CPU 执行更慢。拉取模型文件后,请将版本恢复为 1.3.0:

pip install ai-edge-litert==1.3.0

运行演示前,请确认处于可用版本:

pip show ai-edge-litert | grep Version   # must report 1.3.0

说明:

pip 会打印以 ERROR: 开头的一行,提示 qai-hub-models 需要更新的 ai-edge-litert。降级仍然会成功,之后 qai-hub-models fetch 仍可使用,因此可忽略该消息。

或者,将 qai-hub-models 放在仅用于下载模型的单独虚拟环境中,而将演示环境固定为 ai-edge-litert==1.3.0。

创建脚本

在 VENTUNO Q 上,进入工作目录并创建脚本文件:

cd /home/arduino/segformer-seg
nano segformer_camera.py

将下方 代码示例 一节中的完整脚本粘贴到编辑器中。在 nano 中,用 Ctrl+X,然后 Y,再按 Enter 保存并退出。

运行演示

在虚拟环境已激活且所有文件位于 /home/arduino/segformer-seg 的情况下,从开发板的桌面会话运行演示:

cd /home/arduino/segformer-seg
source ~/.venv/bin/activate            # if not already active

python3 segformer_camera.py            # model on the CPU (noticeably laggy)
python3 segformer_camera.py --use-npu  # model on the Hexagon NPU (real time)

会打开标题为 "Segformer Segmentation" 的窗口,显示带彩色叠加的实时相机画面:每个像素按其预测类别着色,角落还有小图例(按像素数排名的前 5 个类别及色块)。屏幕叠加层会报告模型 invoke 时间与每帧总延迟。在窗口中按 q 退出。

在 NPU 上运行比在 CPU 上快得多——差别相当于卡顿预览与流畅实时画面。在运行 Ubuntu 24.04 的 VENTUNO Q 上,对 45 帧取平均:

运行于 模型 invoke 每帧总计
CPU 271.7 ms 287.2 ms(约 3 fps)
NPU 20.9 ms 37.9 ms(约 26 fps)

模型本身大约有 13 倍加速。这是本系列中 NPU 收益最大的模型,因为 SegFormer 相对较大、计算密集——正是 Hexagon™ Tensor Processor 所擅长的工作负载。

若从远程主机(adb 或 ssh)启动脚本,需要先允许其在显示器上渲染。

在开发板本地(不使用 adb 或 ssh)打开终端并运行以下命令:

xhost +
export DISPLAY=:0   # replace 0 with your display number

这将允许远程主机在显示器上运行应用程序。若不执行此步骤,从远程主机运行脚本时会出现与 xcb 相关的错误。

代码示例

下方给出完整、自包含的演示。请将其保存为工作目录中与模型文件同级的 segformer_camera.py。

import colorsys
import sys
import time

import cv2
import numpy as np
from ai_edge_litert.interpreter import Interpreter, load_delegate

# --- SETUP ---
MODEL_PATH = 'segformer_base-w8a8.tflite'
TOP_K = 5  # how many classes to list in the on-screen legend, ranked by pixel count

# The 150 ADE20K scene-parsing classes this model was finetuned on (from
# nvidia/segformer-b0-finetuned-ade-512-512's config.json id2label, index == class id).
CLASS_NAMES = [
    "wall", "building", "sky", "floor", "tree", "ceiling", "road", "bed", "windowpane",
    "grass", "cabinet", "sidewalk", "person", "earth", "door", "table", "mountain",
    "plant", "curtain", "chair", "car", "water", "painting", "sofa", "shelf", "house",
    "sea", "mirror", "rug", "field", "armchair", "seat", "fence", "desk", "rock",
    "wardrobe", "lamp", "bathtub", "railing", "cushion", "base", "box", "column",
    "signboard", "chest of drawers", "counter", "sand", "sink", "skyscraper",
    "fireplace", "refrigerator", "grandstand", "path", "stairs", "runway", "case",
    "pool table", "pillow", "screen door", "stairway", "river", "bridge", "bookcase",
    "blind", "coffee table", "toilet", "flower", "book", "hill", "bench", "countertop",
    "stove", "palm", "kitchen island", "computer", "swivel chair", "boat", "bar",
    "arcade machine", "hovel", "bus", "towel", "light", "truck", "tower", "chandelier",
    "awning", "streetlight", "booth", "television receiver", "airplane", "dirt track",
    "apparel", "pole", "land", "bannister", "escalator", "ottoman", "bottle", "buffet",
    "poster", "stage", "van", "ship", "fountain", "conveyer belt", "canopy", "washer",
    "plaything", "swimming pool", "stool", "barrel", "basket", "waterfall", "tent",
    "bag", "minibike", "cradle", "oven", "ball", "food", "step", "tank", "trade name",
    "microwave", "pot", "animal", "bicycle", "lake", "dishwasher", "screen", "blanket",
    "sculpture", "hood", "sconce", "vase", "traffic light", "tray", "ashcan", "fan",
    "pier", "crt screen", "plate", "monitor", "bulletin board", "shower", "radiator",
    "glass", "clock", "flag",
]
NUM_CLASSES = len(CLASS_NAMES)

use_npu = True if len(sys.argv) >= 2 and sys.argv[1] == '--use-npu' else False

experimental_delegates = []
if use_npu:
    experimental_delegates = [load_delegate("libQnnTFLiteDelegate.so", options={"backend_type": "htp"})]

interpreter = Interpreter(model_path=MODEL_PATH, experimental_delegates=experimental_delegates)
interpreter.allocate_tensors()

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

_, M_H, M_W, _ = input_details[0]['shape']
in_scale, in_zp = input_details[0]['quantization']
out_scale, out_zp = output_details[0]['quantization']

# Deterministic BGR color per class, spread evenly around the hue wheel.
PALETTE = np.array(
    [tuple(int(c * 255) for c in reversed(colorsys.hsv_to_rgb(i / NUM_CLASSES, 0.85, 1.0)))
     for i in range(NUM_CLASSES)],
    dtype=np.uint8,
)

# --- NPU WARMUP ---
if use_npu:
    print("Warming up NPU...")
    dummy = np.zeros(input_details[0]['shape'], dtype=input_details[0]['dtype'])
    for _ in range(3):
        interpreter.set_tensor(input_details[0]['index'], dummy)
        interpreter.invoke()
    print("Warmup done.")

def letterbox(frame_rgb, dst_h, dst_w):
    h, w = frame_rgb.shape[:2]
    scale = min(dst_h / h, dst_w / w)
    new_h, new_w = int(h * scale), int(w * scale)

    resized = cv2.resize(frame_rgb, (new_w, new_h))
    canvas = np.zeros((dst_h, dst_w, 3), dtype=np.uint8)

    pad_top = (dst_h - new_h) // 2
    pad_left = (dst_w - new_w) // 2
    canvas[pad_top:pad_top + new_h, pad_left:pad_left + new_w] = resized

    return canvas, (pad_left, pad_top, new_w, new_h)

def process_frame(frame_bgr):
    frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
    h, w = frame_rgb.shape[:2]
    canvas, (pad_left, pad_top, new_w, new_h) = letterbox(frame_rgb, M_H, M_W)

    if in_scale:
        q = np.rint((canvas.astype(np.float32) / 255.0) / in_scale) + in_zp
        model_input = np.clip(q, 0, 255).astype(input_details[0]['dtype'])
    else:
        model_input = (canvas.astype(np.float32) / 255.0)

    interpreter.set_tensor(input_details[0]['index'], model_input[np.newaxis, ...])
    t0 = time.perf_counter()
    interpreter.invoke()
    invoke_ms = (time.perf_counter() - t0) * 1000

    logits = interpreter.get_tensor(output_details[0]['index'])[0]  # (128, 128, 150)
    if out_scale:
        logits = (logits.astype(np.float32) - out_zp) * out_scale
    class_map = logits.argmax(axis=-1).astype(np.uint8)  # (128, 128)

    # Upsample to the letterboxed canvas size, then crop out the real (non-pad) area
    # and resize that back to the original frame size -- nearest-neighbor throughout
    # so we never invent a class at a boundary between two real ones.
    class_map = cv2.resize(class_map, (M_W, M_H), interpolation=cv2.INTER_NEAREST)
    class_map = class_map[pad_top:pad_top + new_h, pad_left:pad_left + new_w]
    class_map = cv2.resize(class_map, (w, h), interpolation=cv2.INTER_NEAREST)

    return class_map, invoke_ms

# --- MAIN LOOP ---
mode = "NPU" if use_npu else "CPU"
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    t_start = time.perf_counter()
    class_map, invoke_ms = process_frame(frame)
    total_ms = (time.perf_counter() - t_start) * 1000

    mask_color = PALETTE[class_map]
    overlay = cv2.addWeighted(frame, 0.55, mask_color, 0.45, 0)

    ids, counts = np.unique(class_map, return_counts=True)
    top = ids[np.argsort(-counts)][:TOP_K]
    for i, class_id in enumerate(top):
        swatch = tuple(int(c) for c in PALETTE[class_id])
        y = 70 + i * 26
        cv2.rectangle(overlay, (20, y - 14), (40, y + 4), swatch, -1)
        cv2.putText(overlay, CLASS_NAMES[class_id], (48, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1)

    label = f"{mode}  invoke={invoke_ms:.1f}ms  total={total_ms:.1f}ms"
    cv2.putText(overlay, label, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

    cv2.imshow('Segformer Segmentation', overlay)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

工作原理

  • 预处理。 帧经 letterbox(保持宽高比缩放并填充)到模型的 512×512 输入。
  • 推理。 模型输出 128×128×150 的类别 logit 图(内部下采样 4×)。脚本在每个位置对 150 个类别做 argmax 得到类别图,再用最近邻插值将该 128×128 图上采样回 512×512(避免在两类真实边界之间凭空产生新类别),裁掉 letterbox 填充,再缩放回原始帧尺寸。
  • 类别名称。 这 150 个类别是标准 ADE20K 标签集,取自该 checkpoint(nvidia/segformer-b0-finetuned-ade-512-512)的 id2label 映射。
  • 颜色。 通过在 HSV 色环上均匀分配 150 个色相,为每个类别指定确定性颜色,因此跨次运行同一类别颜色始终一致。

总结

在本教程中,你配置了 VENTUNO Q,下载了 segformer_base 模型文件,并运行了在 Qualcomm® Hexagon™ NPU 上加速的实时语义分割独立 Python 脚本。在此基础上,你可以直接在 Dragonwing™ QCS8275 处理器上利用逐像素类别图——例如把人从背景中分离、掩蔽特定物体类别,或驱动场景感知交互。


原文:https://docs.arduino.cc/tutorials/ventuno-q/segformer-seg/ 本页为 Arduino 官方教程的中文翻译,供内部查阅。命令与代码保持原文。