7,697
社区成员
发帖
与我相关
我的任务
分享最近 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 本地,不需要把图片发送到云端,纯端侧重复保障了数据不泄露的隐私安全问题。
犀牛派 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
现在它可以进一步理解:
桌面右侧有一个白色杯子,
杯子旁边放着一台笔记本电脑,
当前桌面整体比较整洁。
这种能力对于服务机器人、巡检机器人和具身智能明显更加有价值。
这里没有直接去 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 → 场景理解
这一整条链路跑通。
官方其实提供两种比较典型的玩法。
第一种是直接调用 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。
进入 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 案例提供的安装步骤。
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不对
执行:
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 模型。
接下来启动模型:
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 │
│ │
└────────────────────────────────────┘
这一步完成以后,其实模型部署部分已经结束了。
下面开始真正做应用。
准备一张图片:
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 端侧看图问答。
如果只是上传一张 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__)"
这次我们做一个很简单但比较实用的功能:
摄像头看到当前环境以后,让 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()
确保 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 实际执行日志截图放到这里。
这样文章可信度会高很多。
VLM 和传统 CV 最大的区别,在这里马上就体现出来了。
程序运行以后,可以输入:
桌面上有什么?
或者:
画面里面有没有人?
甚至可以问:
如果你是一台服务机器人,
现在继续向前移动可能需要注意什么?
代码根本不用改。
以前使用传统 CV:
有没有人
→ Person Detector
有没有杯子
→ Object Detector
桌面是否整洁
→ 再训练一个模型
用户在做什么
→ Action Recognition
现在则变成:
┌── “有人吗?”
│
├── “桌上有什么?”
Camera → VLM ──────┼── “人在做什么?”
│
├── “场景安全吗?”
│
└── “机器人应该注意什么?”
模型不变,只改变 Prompt。
这才是 VLM 对机器人开发比较大的意义。
如果我们真的准备把这个模型装进机器人,可以把 Prompt 改得更加结构化。
比如:
prompt = """
你是服务机器人的视觉感知模块。
请分析当前摄像头画面,并按照以下格式回答:
场景:
简要判断当前是什么环境。
人员:
说明是否有人以及人物的大致状态。
关键物体:
只列出与机器人行动有关的主要物体。
潜在障碍:
判断是否存在影响机器人移动的明显障碍物。
建议:
给机器人一句简短的下一步行动建议。
不要猜测图片中无法确认的信息。
"""
输出就不再是随意的一段话。
而更像:
场景:
室内办公区域。
人员:
前方存在一名人员,正在桌前工作。
关键物体:
桌子、椅子、笔记本电脑、水杯。
潜在障碍:
桌椅位于机器人移动方向附近。
建议:
保持与人员和桌椅的安全距离后再继续移动。
对于后续程序而言,这种输出明显更好处理。
如果只是给人看,自然语言就够了。
但是如果要给机器人程序使用,建议 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)
就可以继续做决策。
这里是做端侧 VLM 非常容易出现的一个误区。
很多人第一反应是:
Camera
↓
30 FPS
↓
每一帧 VLM
其实没有必要。
VLM 和 YOLO 的定位完全不同。
YOLO 更适合:
Camera
↓
30 FPS
↓
实时目标检测
VLM 更适合:
Camera
↓
关键帧
↓
复杂场景理解
一个更加合理的机器人视觉系统应该是:
Camera
│
├───────────────┐
│ │
▼ ▼
YOLO / SAM VLM
│ │
│ 场景理解
实时检测 │
│ │
└──────┬────────┘
▼
Agent
↓
Robot Action
也就是说:
小模型负责持续感知,大模型负责关键时刻理解。
例如 YOLO 一直运行:
person detected
当人物进入某个区域以后,再触发一次 VLM:
这个人在做什么?
这种方式才更适合真实机器人。
如果我们只是做一个智能摄像头,也可以每隔 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
就可以做机器人语义感知。
目前官方模型列表同时提供了:
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
业务容忍延迟
所以最终还是应该用自己的真实数据集测。
VLM 最大的问题之一就是:
它不仅会看,也会猜。
例如画面比较模糊的时候,模型有可能把某个物体理解错。
所以我在 System Prompt 中专门加入了:
不要描述图片中不存在的内容。
如果是在机器人系统中,还可以更加严格:
无法确认的信息必须回答“无法确认”。
不要因为常识推测图片中未明确出现的物体。
涉及安全判断时,不确定就返回 unknown。
甚至可以让返回结果包含:
{
"object": "cup",
"confidence": "uncertain"
}
虽然这个 confidence 并不是传统检测模型意义上的概率,但至少能在业务层区分:
明确观察
和:
模型推测
还有一点非常重要。
不建议这么做:
Camera
↓
VLM
↓
“向前走”
↓
Motor
更加合理的是:
Camera
│
├── YOLO
├── Depth Camera
├── LiDAR
└── VLM
↓
Semantic Context
↓
Agent
↓
Safety / Rule Check
↓
ROS2 Control
↓
Motor
VLM 可以负责:
理解环境
理解任务
理解物体关系
但运动控制仍然应该结合:
深度信息
定位
导航
碰撞检测
实时控制
尤其 VLM 本身存在生成式模型的不确定性,因此不应该单独承担安全闭环。
做到这一步,其实已经可以继续往 Agent 方向走了。
例如用户说:
帮我看看桌上有没有我的水杯。
系统可以这样工作:
用户语音
↓
ASR
↓
“帮我看看桌上有没有我的水杯”
↓
Camera
↓
Qwen2.5-VL
↓
“桌面右侧有一个白色水杯”
↓
Agent
↓
回答用户
进一步还能:
用户:
“帮我把桌上的杯子拿过来。”
↓
VLM
识别杯子在哪里
↓
Agent
理解需要抓取
↓
Object / Depth
精确定位杯子
↓
Motion Planner
生成运动轨迹
↓
Robot Arm
执行抓取
这时候:
VLM 就从一个“会看图聊天的模型”,逐渐变成了机器人的视觉语义入口。
跑完以后,我觉得真正值得注意的是整个开发方式发生了变化。
以前在高通端侧跑模型,我们关注的是:
ONNX
↓
QNN
↓
Tensor
↓
Output
现在则开始变成:
Camera
↓
Qwen2.5-VL
↓
Natural Language
↓
Agent
↓
Application
底层仍然是 QCS8550、QNN 和 NPU。
但是应用开发者看到的接口已经变成:
POST /v1/chat/completions
应用层只需要描述:
“你想让 AI 看什么、理解什么。”
模型转换、量化、QNN Backend 和芯片适配被进一步封装到了下面。