犀牛派 X1 实战:端侧部署 Qwen2.5-VL-3B,打造一个能“看懂”摄像头画面的视觉助手

asda53asd 2026-09-20 15:09:21

最近 VLM,也就是 Vision Language Model,越来越多地被用到机器人上,传统视觉算法如Yolo此类,它们通常是:摄像头>目标检测>  person / cup / chair  > 固定业务逻辑 > 回复结果,它很擅长回答:

“画面里面有什么,在画面的哪个位置”

但是如果我们把问题变成更为复杂带有主观判断的问题,如:

“这个人在做什么?他离我有多远”
“桌面现在是什么状态?我收拾起来需要多久”
“机器人前面有什么?能够被机器人拿起来吗”
“这张图片里有哪些值得注意的内容?”

传统 YOLO 就没有那么好用了,我们继续用用更为智能的AI模型——VLM(Vision-Language Models),核心思想:将图片使用Visual encoder(一般都是Vision Transformer)进行编码,然后与文本的编码特征进行拼接,再送到大模型里面进行预测后面的内容。

这次我就在 犀牛派 X1 上部署一个真正运行在端侧的 Qwen2.5-VL-3B,并接入 USB 摄像头,实现一个简单的:

端侧视觉场景理解助手。

最终的数据链路是:

USB Camera
    ↓
OpenCV
    ↓
抓取当前画面
    ↓
图片 Base64
    ↓
AidGenSE
    ↓
Qwen2.5-VL-3B
    ↓
QCS8550
    ↓
自然语言描述

整个 VLM 推理运行在 X1 本地,不需要把图片发送到云端,纯端侧重复保障了数据不泄露的隐私安全问题


1. 为什么选择犀牛派 X1 跑 VLM?

犀牛派 X1 基于 Qualcomm QCS8550,官方给出的 NPU INT8 稠密算力为约 48 TOPS,同时提供 CPU、NPU、GPU 等异构计算能力,并支持 Ubuntu 22.04 与 AidLux 环境。相比传统的目标检测模型,VLM 对设备要求明显更高。

因为整个过程实际上不仅仅是:

Image
 ↓
CNN
 ↓
Classification

而更接近:

             Image
               ↓
        Vision Encoder
               ↓
       Visual Embedding
               ↓
            Prompt
               ↓
        Language Model
               ↓
       Natural Language

也就是说,一次推理同时包含视觉编码和语言模型生成,这也是为什么 VLM 特别适合机器人,以前机器人看到杯子,只能知道:

cup

现在它可以进一步理解:

桌面右侧有一个白色杯子,
杯子旁边放着一台笔记本电脑,
当前桌面整体比较整洁。

这种能力对于服务机器人、巡检机器人和具身智能明显更加有价值。


2. 本次使用的模型

这里没有直接去 Hugging Face 下载一个原始 Qwen2.5-VL 然后自己折腾模型转换。

我使用的是目前 APLUX 官方已经针对 X1 提供部署路径的:

Qwen2.5-VL-3B-Instruct

Resolution:
392 × 392

Precision:
W4A16

Target:
QCS8550

Backend:
QNN 2.36

官方当前的 VLM 部署文档就是基于 Rhino Pi-X1 + Ubuntu 22.04 + Qwen2.5-VL-3B 进行演示,同时也提供 AidLux 运行方式。

在 AidGenSE 当前模型列表中,还能看到:

qwen2.5-vl-3b-instruct-392x392-qnn2.36-w4a16-qcs8550

qwen2.5-vl-3b-instruct-672x672-qnn2.36-w4a16-qcs8550

也就是目前至少提供 392×392 和 672×672 两种视觉输入版本。

这次 Demo 优先选择:

392 × 392

因为我们的目标不是做 OCR Benchmark,而是先把:

摄像头 → VLM → 场景理解

这一整条链路跑通。


3. 为什么这次选择 AidGenSE?

官方其实提供两种比较典型的玩法。

第一种是直接调用 AidGen C++ SDK:

Application
    ↓
AidGen SDK
    ↓
Qwen2.5-VL

官方示例中可以直接执行:

./test_multimodal \
config3b_392.json \
test-1.jpg \
"请描述这张图片"

就可以完成图片问答。

但如果后面要做机器人应用,我个人更喜欢第二种:

Application
    ↓
HTTP API
    ↓
AidGenSE
    ↓
AidGen
    ↓
QNN
    ↓
QCS8550

AidGenSE 会直接在 X1 上启动一个兼容 OpenAI 风格的 HTTP API。

这样无论是:

Python
C++
Web
ROS2
Android
机器人 Agent

都可以通过统一接口调用 VLM。

所以这次选择 AidGenSE。


4. 安装 VLM 运行环境

进入 X1 的 Ubuntu Terminal。

首先更新软件源:

sudo aid-pkg update

安装 AidGenSE:

sudo aid-pkg -i aidgense

然后安装 AidGen SDK:

sudo aid-pkg -i aidgen-sdk

接着安装 QNN 后端:

sudo aid-pkg -i aidgen-qnn236

sudo aid-pkg -i aidgen-qnn240

这里我们实际使用的 Qwen2.5-VL 模型对应:

QNN 2.36

这些正是官方当前给 X1 VLM 案例提供的安装步骤。


5. 检查当前支持哪些 VLM

AidGenSE 安装以后,可以先执行:

aidllm remote-list api

这里有一个地方我很喜欢。

它不是让开发者自己猜:

这个模型到底能不能在8550跑?

而是直接给出当前 SoC 对应的模型列表。

X1 上应该可以看到类似:

Current Soc : 8550

以及 VLM:

qwen2.5-vl-3b-instruct-392x392-qnn2.36-w4a16-qcs8550

qwen2.5-vl-3b-instruct-672x672-qnn2.36-w4a16-qcs8550

官方当前文档给出的模型名称也是这两个版本。

这样做的好处就是:

模型和芯片是一一对应的。

避免下载一个模型以后才发现:

平台不对
QNN版本不对
量化格式不对
Context不对

6. 下载 Qwen2.5-VL-3B

执行:

aidllm pull api \
aplux/qwen2.5-vl-3b-instruct-392x392-qnn2.36-w4a16-qcs8550

下载完成以后检查:

aidllm list api

如果能够看到:

qwen2.5-vl-3b-instruct-392x392-qnn2.36-w4a16-qcs8550

说明模型已经准备完成。

官方 AidGenSE 文档当前同样采用 aidllm pull api 获取针对 QCS8550 优化好的 VLM 模型。


7. 启动端侧 VLM 服务

接下来启动模型:

aidllm start api \
-m qwen2.5-vl-3b-instruct-392x392-qnn2.36-w4a16-qcs8550

查看服务状态:

aidllm status api

如果启动正常,AidGenSE 默认提供:

http://127.0.0.1:8888

官方当前给出的默认 API 端口就是:

8888

并提供:

/v1/chat/completions

这一类 OpenAI 风格调用接口。

于是现在 X1 上已经存在一个:

Local VLM Server

架构变成:

                     犀牛派 X1
┌────────────────────────────────────┐
│                                    │
│ Python / ROS2 / Web                │
│          │                         │
│          ▼                         │
│ http://127.0.0.1:8888              │
│          │                         │
│          ▼                         │
│       AidGenSE                     │
│          │                         │
│          ▼                         │
│ Qwen2.5-VL-3B W4A16               │
│          │                         │
│          ▼                         │
│ QNN 2.36 / QCS8550                │
│                                    │
└────────────────────────────────────┘

这一步完成以后,其实模型部署部分已经结束了。

下面开始真正做应用。


8. 先不用摄像头,测试一张图片

准备一张图片:

test.jpg

放到:

/home/aidlux/vlm_demo/

创建:

mkdir -p /home/aidlux/vlm_demo

cd /home/aidlux/vlm_demo

接下来写一个:

test_vlm.py

代码如下:

import base64
import json
import requests


MODEL = (
    "qwen2.5-vl-3b-instruct-392x392-"
    "qnn2.36-w4a16-qcs8550"
)

API_URL = (
    "http://127.0.0.1:8888/"
    "v1/chat/completions"
)


def image_to_data_url(image_path):

    with open(image_path, "rb") as f:
        image_bytes = f.read()

    image_base64 = base64.b64encode(
        image_bytes
    ).decode("utf-8")

    return (
        "data:image/jpeg;base64,"
        + image_base64
    )


def ask_vlm(
    image_path,
    question
):

    image_url = image_to_data_url(
        image_path
    )

    payload = {

        "model": MODEL,

        "stream": True,

        "messages": [

            {
                "role": "system",
                "content":
                    "你是运行在机器人上的"
                    "视觉场景理解助手。"
            },

            {
                "role": "user",

                "content": [

                    {
                        "type": "text",
                        "text": question
                    },

                    {
                        "type": "image_url",

                        "image_url": {
                            "url": image_url
                        }
                    }

                ]
            }

        ]
    }


    response = requests.post(

        API_URL,

        headers={
            "Content-Type":
                "application/json"
        },

        json=payload,

        stream=True,

        timeout=180
    )


    response.raise_for_status()


    print("VLM: ", end="", flush=True)


    for line in response.iter_lines():

        if not line:
            continue

        line = line.decode("utf-8")


        if not line.startswith("data: "):
            continue


        data = line[6:]


        if data.strip() == "[DONE]":
            break


        try:

            chunk = json.loads(data)

            content = (
                chunk["choices"][0]
                ["delta"]
                .get("content")
            )

            if content:

                print(
                    content,
                    end="",
                    flush=True
                )

        except Exception:
            pass


    print()


ask_vlm(

    "test.jpg",

    "请描述这张图片中的主要内容,"
    "并告诉我有哪些值得关注的物体。"
)

这里的核心不是 requests

而是这段:

{
    "type": "image_url",

    "image_url": {
        "url": image_url
    }
}

图片被编码成:

data:image/jpeg;base64,...

然后与文本 Prompt 一起发送给本地 VLM。

这也是官方当前 AidGenSE Python 示例采用的调用方式。

运行:

python3 test_vlm.py

此时整个推理链路是:

test.jpg
   ↓
Base64
   ↓
localhost:8888
   ↓
Qwen2.5-VL
   ↓
视觉理解
   ↓
Token Streaming
   ↓
Terminal

到这里,我们已经完成了最基础的:

X1 端侧看图问答。


9. 接下来才是这个 Demo 真正有意思的地方:接摄像头

如果只是上传一张 JPG,其实还没有真正发挥 X1 作为机器人开发板的价值。

所以接下来插入一个普通 USB 摄像头。

先确认设备:

ls /dev/video*

如果识别正常,一般能够看到:

/dev/video0

可以继续查看设备:

v4l2-ctl --list-devices

如果没有 v4l2-ctl

sudo apt install v4l-utils

Python 侧安装 OpenCV:

sudo apt install python3-opencv

检查:

python3 -c "import cv2; print(cv2.__version__)"

10. 编写摄像头 + VLM 完整程序

这次我们做一个很简单但比较实用的功能:

摄像头看到当前环境以后,让 VLM 告诉机器人“眼前是什么”。

创建:

camera_vlm.py

完整代码如下:

import cv2
import json
import base64
import requests
import time


MODEL = (
    "qwen2.5-vl-3b-instruct-392x392-"
    "qnn2.36-w4a16-qcs8550"
)


API_URL = (
    "http://127.0.0.1:8888/"
    "v1/chat/completions"
)


# ----------------------------------------------------
# OpenCV Frame -> Base64 Data URL
# ----------------------------------------------------

def frame_to_data_url(frame):

    success, buffer = cv2.imencode(
        ".jpg",
        frame,
        [
            cv2.IMWRITE_JPEG_QUALITY,
            85
        ]
    )

    if not success:
        raise RuntimeError(
            "JPEG encode failed"
        )


    image_base64 = base64.b64encode(
        buffer
    ).decode("utf-8")


    return (
        "data:image/jpeg;base64,"
        + image_base64
    )


# ----------------------------------------------------
# 调用本地 VLM
# ----------------------------------------------------

def ask_vlm(
    frame,
    prompt
):

    image_url = frame_to_data_url(
        frame
    )


    payload = {

        "model": MODEL,

        "stream": True,

        "messages": [

            {
                "role": "system",

                "content":
                    "你是一台机器人上的视觉助手。"
                    "请根据摄像头图片准确回答问题,"
                    "不要描述图片中不存在的内容。"
            },

            {
                "role": "user",

                "content": [

                    {
                        "type": "text",
                        "text": prompt
                    },

                    {
                        "type": "image_url",

                        "image_url": {
                            "url": image_url
                        }
                    }

                ]
            }

        ]
    }


    response = requests.post(

        API_URL,

        headers={
            "Content-Type":
                "application/json"
        },

        json=payload,

        stream=True,

        timeout=180
    )


    response.raise_for_status()


    result = ""


    for line in response.iter_lines():

        if not line:
            continue


        line = line.decode("utf-8")


        if not line.startswith(
            "data: "
        ):
            continue


        data = line[6:]


        if data.strip() == "[DONE]":
            break


        try:

            chunk = json.loads(
                data
            )


            content = (

                chunk["choices"][0]
                ["delta"]
                .get(
                    "content",
                    ""
                )

            )


            if content:

                result += content

                print(
                    content,
                    end="",
                    flush=True
                )


        except Exception:
            continue


    print()

    return result


# ----------------------------------------------------
# Camera
# ----------------------------------------------------

camera = cv2.VideoCapture(0)


if not camera.isOpened():

    raise RuntimeError(
        "Cannot open camera"
    )


camera.set(
    cv2.CAP_PROP_FRAME_WIDTH,
    1280
)

camera.set(
    cv2.CAP_PROP_FRAME_HEIGHT,
    720
)


print(
    "Camera ready."
)

print(
    "Press ENTER to analyze scene."
)

print(
    "Input q to exit."
)


# ----------------------------------------------------
# Main Loop
# ----------------------------------------------------

while True:

    ret, frame = camera.read()


    if not ret:

        print(
            "Failed to capture frame"
        )

        time.sleep(1)

        continue


    command = input(
        "\nCommand > "
    ).strip()


    if command.lower() == "q":
        break


    if command == "":

        prompt = (
            "请观察当前摄像头画面。"
            "先用一句话描述当前场景,"
            "然后说明画面中主要有哪些物体,"
            "最后告诉机器人有什么值得关注的信息。"
        )

    else:

        prompt = command


    print("\nUser:", prompt)

    print("VLM: ", end="", flush=True)


    try:

        ask_vlm(
            frame,
            prompt
        )

    except Exception as e:

        print(
            "\nVLM request failed:",
            e
        )


camera.release()

11. 运行

确保 AidGenSE 已经启动:

aidllm status api

然后:

python3 camera_vlm.py

程序启动以后:

Camera ready.

Press ENTER to analyze scene.

Input q to exit.

直接回车:

Command >

就会抓取当前摄像头的一帧。

然后发送给本地 Qwen2.5-VL。

假设镜头前是:

一个人
+
一台笔记本电脑
+
一个水杯
+
桌子

模型的实际回复会取决于画面和 Prompt,可能形成类似这样的描述:

当前画面是一个室内办公场景。

画面中可以看到一名人员、一台打开的笔记本电脑、
桌面以及一个杯子。

值得关注的是人物正在电脑前活动,
机器人前方存在人员和桌面物体,
移动时应注意保持安全距离。

这里这段文字只是为了说明输出形式,并不是冒充本次真机实测结果。

真正发布文章的时候,我建议直接把你的 X1 实际执行日志截图放到这里。

这样文章可信度会高很多。


12. 不仅能“描述图片”,还可以直接问问题

VLM 和传统 CV 最大的区别,在这里马上就体现出来了。

程序运行以后,可以输入:

桌面上有什么?

或者:

画面里面有没有人?

甚至可以问:

如果你是一台服务机器人,
现在继续向前移动可能需要注意什么?

代码根本不用改。

以前使用传统 CV:

有没有人
→ Person Detector

有没有杯子
→ Object Detector

桌面是否整洁
→ 再训练一个模型

用户在做什么
→ Action Recognition

现在则变成:

                   ┌── “有人吗?”
                   │
                   ├── “桌上有什么?”
Camera → VLM ──────┼── “人在做什么?”
                   │
                   ├── “场景安全吗?”
                   │
                   └── “机器人应该注意什么?”

模型不变,只改变 Prompt。

这才是 VLM 对机器人开发比较大的意义。


13. 再做一个“机器人视觉 Prompt”

如果我们真的准备把这个模型装进机器人,可以把 Prompt 改得更加结构化。

比如:

prompt = """
你是服务机器人的视觉感知模块。

请分析当前摄像头画面,并按照以下格式回答:

场景:
简要判断当前是什么环境。

人员:
说明是否有人以及人物的大致状态。

关键物体:
只列出与机器人行动有关的主要物体。

潜在障碍:
判断是否存在影响机器人移动的明显障碍物。

建议:
给机器人一句简短的下一步行动建议。

不要猜测图片中无法确认的信息。
"""

输出就不再是随意的一段话。

而更像:

场景:
室内办公区域。

人员:
前方存在一名人员,正在桌前工作。

关键物体:
桌子、椅子、笔记本电脑、水杯。

潜在障碍:
桌椅位于机器人移动方向附近。

建议:
保持与人员和桌椅的安全距离后再继续移动。

对于后续程序而言,这种输出明显更好处理。


14. 更进一步:让 VLM 输出 JSON

如果只是给人看,自然语言就够了。

但是如果要给机器人程序使用,建议 Prompt 直接要求输出 JSON:

prompt = """
分析当前机器人摄像头画面。

只输出合法 JSON,不要输出其他内容。

格式:

{
  "scene": "",
  "people": [],
  "objects": [],
  "obstacles": [],
  "risk_level": "low|medium|high",
  "suggestion": ""
}
"""

这样 VLM 就可以承担一部分:

视觉
 ↓
语义
 ↓
结构化环境理解

例如:

{
  "scene": "office",
  "people": [
    "one person sitting at desk"
  ],
  "objects": [
    "desk",
    "chair",
    "laptop",
    "cup"
  ],
  "obstacles": [
    "chair"
  ],
  "risk_level": "low",
  "suggestion": "slow down when approaching the desk"
}

然后机器人程序只需要:

result = json.loads(vlm_result)

就可以继续做决策。


15. 为什么我没有做“每一帧都跑 VLM”?

这里是做端侧 VLM 非常容易出现的一个误区。

很多人第一反应是:

Camera
 ↓
30 FPS
 ↓
每一帧 VLM

其实没有必要。

VLM 和 YOLO 的定位完全不同。

YOLO 更适合:

Camera
 ↓
30 FPS
 ↓
实时目标检测

VLM 更适合:

Camera
 ↓
关键帧
 ↓
复杂场景理解

一个更加合理的机器人视觉系统应该是:

Camera
   │
   ├───────────────┐
   │               │
   ▼               ▼
YOLO / SAM        VLM
   │               │
   │             场景理解
实时检测           │
   │               │
   └──────┬────────┘
          ▼
        Agent
          ↓
      Robot Action

也就是说:

小模型负责持续感知,大模型负责关键时刻理解。

例如 YOLO 一直运行:

person detected

当人物进入某个区域以后,再触发一次 VLM:

这个人在做什么?

这种方式才更适合真实机器人。


16. 可以加入定时触发

如果我们只是做一个智能摄像头,也可以每隔 10 秒分析一次。

例如:

while True:

    ret, frame = camera.read()

    if not ret:
        continue


    result = ask_vlm(

        frame,

        "请用一句话描述"
        "当前摄像头场景。"
    )


    print(result)


    time.sleep(10)

于是就变成:

Camera
 ↓
每10秒抓取关键帧
 ↓
Qwen2.5-VL
 ↓
场景描述
 ↓
日志

如果再接 MQTT:

VLM
 ↓
MQTT
 ↓
Home Assistant

就可以做智能家居。

如果接 ROS2:

VLM
 ↓
ROS2 Topic
 ↓
Robot Agent

就可以做机器人语义感知。


17. 392×392 和 672×672 怎么选?

目前官方模型列表同时提供了:

392 × 392

672 × 672

两个 Qwen2.5-VL-3B W4A16 / QNN 2.36 / QCS8550 版本。

我的理解是应用时可以这么考虑:

场景建议
普通场景理解392×392
判断人物/物体关系392×392 先测试
图片细节更多可测试 672×672
OCR / 小字内容更值得尝试 672×672
对响应速度更敏感优先 392×392

但这里不要简单理解成:

672一定比392好

真实部署还需要综合考虑:

视觉细节
模型处理时间
内存
Prompt复杂度
输出Token
业务容忍延迟

所以最终还是应该用自己的真实数据集测。


18. VLM“胡说”怎么办?

VLM 最大的问题之一就是:

它不仅会看,也会猜。

例如画面比较模糊的时候,模型有可能把某个物体理解错。

所以我在 System Prompt 中专门加入了:

不要描述图片中不存在的内容。

如果是在机器人系统中,还可以更加严格:

无法确认的信息必须回答“无法确认”。

不要因为常识推测图片中未明确出现的物体。

涉及安全判断时,不确定就返回 unknown。

甚至可以让返回结果包含:

{
  "object": "cup",
  "confidence": "uncertain"
}

虽然这个 confidence 并不是传统检测模型意义上的概率,但至少能在业务层区分:

明确观察

和:

模型推测

19. 真正做机器人时,别让 VLM 直接控制电机

还有一点非常重要。

不建议这么做:

Camera
 ↓
VLM
 ↓
“向前走”
 ↓
Motor

更加合理的是:

Camera
     │
     ├── YOLO
     ├── Depth Camera
     ├── LiDAR
     └── VLM
          ↓
       Semantic Context
          ↓
         Agent
          ↓
   Safety / Rule Check
          ↓
       ROS2 Control
          ↓
        Motor

VLM 可以负责:

理解环境
理解任务
理解物体关系

但运动控制仍然应该结合:

深度信息
定位
导航
碰撞检测
实时控制

尤其 VLM 本身存在生成式模型的不确定性,因此不应该单独承担安全闭环。


20. 下一步:从 VLM 升级成机器人 Agent

做到这一步,其实已经可以继续往 Agent 方向走了。

例如用户说:

帮我看看桌上有没有我的水杯。

系统可以这样工作:

用户语音
   ↓
ASR
   ↓
“帮我看看桌上有没有我的水杯”
   ↓
Camera
   ↓
Qwen2.5-VL
   ↓
“桌面右侧有一个白色水杯”
   ↓
Agent
   ↓
回答用户

进一步还能:

用户:
“帮我把桌上的杯子拿过来。”

        ↓

      VLM
识别杯子在哪里

        ↓

      Agent
理解需要抓取

        ↓

   Object / Depth
精确定位杯子

        ↓

    Motion Planner
生成运动轨迹

        ↓

   Robot Arm
执行抓取

这时候:

VLM 就从一个“会看图聊天的模型”,逐渐变成了机器人的视觉语义入口。


21. 这个案例最值得关注的,其实不是模型本身

跑完以后,我觉得真正值得注意的是整个开发方式发生了变化。

以前在高通端侧跑模型,我们关注的是:

ONNX
 ↓
QNN
 ↓
Tensor
 ↓
Output

现在则开始变成:

Camera
   ↓
Qwen2.5-VL
   ↓
Natural Language
   ↓
Agent
   ↓
Application

底层仍然是 QCS8550、QNN 和 NPU。

但是应用开发者看到的接口已经变成:

POST /v1/chat/completions

应用层只需要描述:

“你想让 AI 看什么、理解什么。”

模型转换、量化、QNN Backend 和芯片适配被进一步封装到了下面。

...全文
47 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

7,697

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧