3,303
社区成员




在人工智能与边缘计算深度融合的当下,强大的硬件平台与先进的算法模型成为推动各领域智能化变革的核心要素。高通骁龙 QCS8550 作为一款面向物联网(IoT)领域的旗舰级处理器,采用先进的 4 纳米工艺制程,集强大计算能力、高达 48TOPS 的极致边缘 AI 处理性能、前沿的 Wi-Fi 7 连接技术以及卓越的视频和图形处理能力于一身 。其搭载的 Qualcomm Kryo 中央处理器,计算能力相较于前代提升达 1.5 倍;Qualcomm Adreno GPU 性能更是前代的三倍之多,支持硬件加速光线追踪技术,能同时处理多种格式与分辨率的视频会话,并通过多显示接口以超高清分辨率在不同面板上同步渲染内容。此外,AidLux为 AI 应用赋能。这些特性使其成为众多对性能要求严苛的 IoT 应用,如自主移动机器人、工业无人机、智能安防监控、视频协作系统等的理想硬件基石。
YOLO11-seg 作为 YOLO11 系列中专注于实例分割任务的模型,在计算机视觉领域有着极为重要的作用。实例分割需要精准识别图像或视频中的每个目标实例,并为其生成像素级别的分割掩码,这在众多实际场景中都不可或缺。例如在智能安防中,它能够精确区分不同的人物、物体,不仅识别出目标,还能清晰勾勒出其轮廓,为安防监控提供更细致准确的信息,助力快速精准地判断异常情况;在工业检测中,可对生产线上的产品进行细致的缺陷检测与分割,明确缺陷所在位置及范围,极大提高产品质量检测的精度和效率;在自动驾驶领域,能精准分割出道路上的行人、车辆、交通标识等各类目标,为车辆的决策与行驶提供关键的视觉基础。
本次对高通 QCS8550 部署 YOLO11-seg 模型的研究与性能测试,旨在深入探究如何充分发挥 QCS8550 强大的硬件性能,高效运行 YOLO11-seg 这一先进的实例分割模型,为相关领域的应用落地提供有力的技术支撑和实践参考 。通过对模型部署过程的详细分析以及全面的性能测试,期望为后续基于该硬件平台和模型的应用开发、优化等工作提供极具价值的经验与数据,进一步推动人工智能在边缘计算场景下的广泛应用与深度发展。
Qualcomm Dragonwing™ QCM8550 | Qualcomm
模型优化平台 (AIMO) 用户指南 | APLUX Doc Center
模型 尺寸640*640 |
CPU |
NPU QNN2.31 | NPU QNN2.31 | |||
FP32 | FP16 | INT8 | ||||
YOLO11n-seg | 119.67 ms | 8.36 FPS | 16.71 ms | 59.84 FPS | 5.54 ms | 180.51 FPS |
YOLO11s-seg | 510.43 ms | 1.96 FPS | 9.62 ms | 103.95 FPS | 3.94 ms | 253.81 FPS |
YOLO11m-seg | 1568.51 ms | 0.64 FPS | 24.97 ms | 40.05 FPS | 7.33 ms | 136.43 FPS |
YOLO11l-seg | 1921.28 ms | 0.52 FPS | 26.97 ms | 37.08 FPS | 8.52 ms | 117.37 FPS |
YOLO11x-seg | 3981.91 ms | 0.25 FPS | 67.32 ms | 14.85 FPS | 20.64 ms | 48.45 FPS |
点击链接可以下载YOLO11-seg系列模型的pt格式,其他模型尺寸可以通过AIMO转换模型,并修改下面参考代码中的model_size测试即可。
python3.10 -m pip install --upgrade pip
pip -V
aidlux@aidlux:~/aidcode$ pip -V
pip 25.1.1 from /home/aidlux/.local/lib/python3.10/site-packages/pip (python 3.10)
pip install ultralytics onnx
方法 1:临时添加环境变量(立即生效)
在终端中执行以下命令,将 ~/.local/bin
添加到当前会话的环境变量中
export PATH="$PATH:$HOME/.local/bin"
yolo --version
,若输出版本号(如 0.0.2
),则说明命令已生效。方法 2:永久添加环境变量(长期有效)
echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.bashrc
source ~/.bashrc # 使修改立即生效
验证:执行 yolo --version
,若输出版本号(如 0.0.2
),则说明命令已生效。
测试环境中安装yolo版本为8.3.152
提示:如果遇到用户组权限问题,可以忽悠,因为yolo命令会另外构建临时文件,也可以执行下面命令更改用户组,执行后下面的警告会消失:
sudo chown -R aidlux:aidlux ~/.config/
sudo chown -R aidlux:aidlux ~/.config/Ultralytics
可能遇见的报错如下:
WARNING ⚠️ user config directory '/home/aidlux/.config/Ultralytics' is not writeable, defaulting to '/tmp' or CWD.Alternatively you can define a YOLO_CONFIG_DIR environment variable for this path.
新建一个python文件,命名自定义即可,用于模型转换以及导出:
from ultralytics import YOLO
# 加载同级目录下的.pt模型文件
model = YOLO('yolo11n-seg.pt') # 替换为实际模型文件名
# 导出ONNX配置参数
export_params = {
'format': 'onnx',
'opset': 12, # 推荐算子集版本
'simplify': True, # 启用模型简化
'dynamic': False, # 固定输入尺寸
'imgsz': 640, # 标准输入尺寸
'half': False # 保持FP32精度
}
# 执行转换并保存到同级目录
model.export(**export_params)
执行该程序完成将pt模型导出为onnx模型。
提示:Yolo11s-seg,Yolo11m-seg,Yolo11l-seg,Yolo11x-seg替换代码中Yolo11n即可;
使用Netron工具查看onnx模型结构,选择剪枝位置,由于yolov11-seg的输出有两个,所以剪枝参数有四个输入。
参考上图中红色框部分填写,其他不变,注意开启自动量化功能,AIMO更多操作查看使用说明或参考AIMO平台
output1
/model.23/Concat_output_0
/model.23/Mul_2_output_0
/model.23/Sigmoid_output_0
检查aidlux环境中的aidlite版本是否与我们转换模型时选择的Qnn版本一致,终端执行:
sudo aid-pkg installed
如果没有aidlite-qnn231,需要安装:
sudo aid-pkg update
sudo aid-pkg install aidlite-sdk
# Install the latest version of AidLite (latest QNN version)
sudo aid-pkg install aidlite
💡注意
Linux环境下,安装指定QNN版本的AidLite SDK:sudo aid-pkg install aidlite-{QNN Version}
例如:安装QNN2.31版本的AidLite SDK —— sudo aid-pkg install aidlite-qnn231
import numpy as np
import cv2
import aidlite # Qualcomm硬件加速库
import os
import time
import argparse
import onnxruntime
# 定义COCO数据集的类别名称
CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "bus ", "train", "truck ", "boat", "traffic light",
"fire hydrant", "stop sign ", "parking meter", "bench", "bird", "cat", "dog ", "horse ", "sheep", "cow", "elephant",
"bear", "zebra ", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
"baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife ",
"spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza ", "donut", "cake", "chair", "sofa",
"pottedplant", "bed", "diningtable", "toilet ", "tvmonitor", "laptop\t", "mouse\t", "remote ", "keyboard ", "cell phone", "microwave ",
"oven ", "toaster", "sink", "refrigerator ", "book", "clock", "vase", "scissors ", "teddy bear ", "hair drier", "toothbrush ")
# 定义相似度计算函数(用于比较不同框架输出结果)
def get_acc(onnx_out, other_out):
"""
计算两个输出向量的余弦相似度
:param onnx_out: 第一个输出向量
:param other_out: 第二个输出向量
:return: 余弦相似度
"""
cosine_similarity = np.dot(np.array(onnx_out), np.array(other_out)) / \
(np.linalg.norm(np.array(onnx_out)) * np.linalg.norm(np.array(other_out)))
return cosine_similarity
def eqprocess(image, size1, size2):
"""
对图像进行等比例缩放并填充,保持长宽比
:param image: 输入图像
:param size1: 目标高度
:param size2: 目标宽度
:return: 处理后的图像和缩放比例
"""
h, w, _ = image.shape
mask = np.zeros((size1, size2, 3), dtype=np.float32)
scale1 = h / size1
scale2 = w / size2
if scale1 > scale2:
scale = scale1
else:
scale = scale2
img = cv2.resize(image, (int(w / scale), int(h / scale)))
mask[:int(h / scale), :int(w / scale), :] = img
return mask, scale
def xywh2xyxy(x):
"""
将边界框的格式从 (center x, center y, width, height) 转换为 (x1, y1, x2, y2)
:param x: 输入的边界框数组
:return: 转换后的边界框数组
"""
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x
y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y
y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x
y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y
return y
def xyxy2xywh(box):
"""
将边界框的格式从 (left_top x, left_top y, right_bottom x, right_bottom y) 转换为 (left_top x, left_top y, width, height)
:param box: 输入的边界框数组
:return: 转换后的边界框数组
"""
box[:, 2:] = box[:, 2:] - box[:, :2]
return box
def NMS(dets, scores, thresh):
"""
单类NMS算法
:param dets: 边界框数组,形状为 (N, 5),格式为 (left_top x, left_top y, right_bottom x, right_bottom y, Scores)
:param scores: 置信度分数数组
:param thresh: IOU阈值
:return: 保留的边界框索引列表
"""
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
areas = (y2 - y1 + 1) * (x2 - x1 + 1)
keep = []
index = scores.argsort()[::-1]
while index.size > 0:
i = index[0] # every time the first is the biggest, and add it directly
keep.append(i)
x11 = np.maximum(x1[i], x1[index[1:]]) # calculate the points of overlap
y11 = np.maximum(y1[i], y1[index[1:]])
x22 = np.minimum(x2[i], x2[index[1:]])
y22 = np.minimum(y2[i], y2[index[1:]])
w = np.maximum(0, x22 - x11 + 1) # the weights of overlap
h = np.maximum(0, y22 - y11 + 1) # the height of overlap
overlaps = w * h
ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)
idx = np.where(ious <= thresh)[0]
index = index[idx + 1] # because index start from 1
return keep
def draw_detect_res(img, det_pred, segments):
"""
绘制检测结果(边界框、类别标签和分割掩码)
修改说明:为不同类别生成不同颜色,使分割实例按类别区分颜色
:param img: 原始图像
:param det_pred: 检测结果数组 [N, 6],每行格式为 [x1, y1, x2, y2, conf, class_id]
:param segments: 分割轮廓列表,每个元素是一个点集
:return: 绘制后的图像
"""
if det_pred is None:
return img
img = img.astype(np.uint8)
im_canvas = img.copy() # 用于透明效果的底图
# 预定义20种鲜艳的BGR颜色(可以根据需要扩展)
color_palette = [
(255, 0, 0), # 红色
(0, 255, 0), # 绿色
(0, 0, 255), # 蓝色
(255, 255, 0), # 青色
(255, 0, 255), # 品红
(0, 255, 255), # 黄色
(128, 0, 0), # 深红
(0, 128, 0), # 深绿
(0, 0, 128), # 深蓝
(128, 128, 0), # 橄榄绿
(128, 0, 128), # 紫色
(0, 128, 128), # 蓝绿色
(255, 165, 0), # 橙色
(255, 192, 203), # 粉红
(75, 0, 130), # 靛蓝
(0, 255, 127), # 春绿色
(220, 20, 60), # 猩红色
(0, 191, 255), # 天蓝色
(147, 112, 219), # 中紫色
(255, 215, 0) # 金色
]
for i in range(len(det_pred)):
x1, y1, x2, y2 = [int(t) for t in det_pred[i][:4]] # 边界框坐标
cls_id = int(det_pred[i][5]) # 类别ID
# 为不同类别选择不同颜色(使用取模运算循环使用调色板)
color = color_palette[cls_id % len(color_palette)]
# 绘制类别标签(带背景框提高可读性)
(text_w, text_h), _ = cv2.getTextSize(f'{CLASSES[cls_id]}',
cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
cv2.rectangle(img, (x1, y1 - 20), (x1 + text_w, y1), color, -1) # 标签背景
cv2.putText(img, f'{CLASSES[cls_id]}', (x1, y1 - 6),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
# 绘制边界框(使用类别对应颜色)
cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness=2)
# 绘制分割结果(使用类别对应颜色)
if len(segments[i]) > 0:
# 创建透明层用于填充
overlay = img.copy()
# 填充分割区域(半透明效果)
cv2.fillPoly(overlay, np.int32([segments[i]]), color)
# 绘制轮廓线(实线)
cv2.polylines(overlay, np.int32([segments[i]]), True, color, 2)
# 合并透明层
img = cv2.addWeighted(overlay, 0.3, img, 0.7, 0)
return img
def scale_mask(masks, im0_shape):
"""
对掩码进行缩放,使其尺寸与原始图像一致
:param masks: 输入的掩码数组
:param im0_shape: 原始图像的形状
:return: 缩放后的掩码数组
"""
masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]),
interpolation=cv2.INTER_LINEAR)
if len(masks.shape) == 2:
masks = masks[:, :, None]
return masks
def crop_mask(masks, boxes):
"""
根据边界框裁剪掩码
:param masks: 输入的掩码数组
:param boxes: 边界框数组
:return: 裁剪后的掩码数组
"""
n, h, w = masks.shape
x1, y1, x2, y2 = np.split(boxes[:, :, None], 4, 1)
r = np.arange(w, dtype=x1.dtype)[None, None, :]
c = np.arange(h, dtype=x1.dtype)[None, :, None]
return masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2))
def process_mask(protos, masks_in, bboxes, im0_shape):
"""
处理掩码输出,包括矩阵乘法、缩放和裁剪
:param protos: 原型掩码数组
:param masks_in: 输入的掩码特征数组
:param bboxes: 边界框数组
:param im0_shape: 原始图像的形状
:return: 处理后的掩码数组
"""
c, mh, mw = protos.shape
masks = np.matmul(masks_in, protos.reshape((c, -1))).reshape((-1, mh, mw)).transpose(1, 2, 0) # HWN
masks = np.ascontiguousarray(masks)
masks = scale_mask(masks, im0_shape) # re-scale mask from P3 shape to original input image shape
masks = np.einsum('HWN -> NHW', masks) # HWN -> NHW
masks = crop_mask(masks, bboxes)
return np.greater(masks, 0.5)
def masks2segments(masks):
"""
将掩码转换为轮廓线段
:param masks: 输入的掩码数组
:return: 轮廓线段列表
"""
segments = []
for x in masks.astype('uint8'):
c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0] # CHAIN_APPROX_SIMPLE
if c:
c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2)
else:
c = np.zeros((0, 2)) # no segments found
segments.append(c.astype('float32'))
return segments
class qnn_predict(object):
"""
QNN加速的YOLOv8-seg模型推理类
"""
def __init__(self, args) -> None:
# 初始化QNN推理环境 aidlite.Config.create_instance()
config = aidlite.Config.create_instance()
if config is None:
print("Create model failed !")
# 配置QNN运行参数
config.implement_type = aidlite.ImplementType.TYPE_LOCAL # 本地执行
config.framework_type = aidlite.FrameworkType.TYPE_QNN # 使用QNN框架
config.accelerate_type = aidlite.AccelerateType.TYPE_DSP # 使用DSP加速
config.is_quantify_model = 1 # 使用量化模型
# 加载模型文件
model = aidlite.Model.create_instance(args.target_model)
if model is None:
print("Create model failed !")
# 初始化模型参数
self.conf = args.conf_thres # 置信度阈值
self.iou = args.iou_thres # IOU阈值(NMS用)
self.width = args.width # 模型输入宽度
self.height = args.height # 模型输入高度
self.class_num = args.class_num # 类别数量(COCO为80)
# 输入输出张量形状配置
self.input_shape = [[1, self.height, self.width, 3]] # NHWC格式
# 输出特征图计算参数
self.blocks = int(self.height * self.width * (1 / 64 + 1 / 256 + 1 / 1024)) # 多尺度特征图总锚点数
self.maskw = int(self.width / 4) # 掩码宽度
self.maskh = int(self.height / 4) # 掩码高度
# 输出层形状配置(4个输出层)
self.output_shape = [
[1, 32, self.blocks], # 原型掩码(protos)
[1, 4, self.blocks], # 边界框(xywh)
[1, self.class_num, self.blocks], # 类别置信度
[1, self.maskh, self.maskw, 32] # 分割特征
]
# 设置模型属性
model.set_model_properties(self.input_shape, aidlite.DataType.TYPE_FLOAT32, self.output_shape, aidlite.DataType.TYPE_FLOAT32)
self.interpreter = aidlite.InterpreterBuilder.build_interpretper_from_model_and_config(model, config)
if self.interpreter is None:
print("build_interpretper_from_model_and_config failed !")
# 初始化模型
result = self.interpreter.init()
if result != 0:
print(f"interpreter init failed !")
# 加载模型
result = self.interpreter.load_model()
if result != 0:
print("interpreter load model failed !")
print("detect model load success!")
def pretreat_img(self, frame):
"""
图像预处理函数
:param frame: 输入的图像帧
:return: 处理后的图像和缩放比例
"""
# 1. 等比例缩放并填充(保持长宽比)
img, scale = eqprocess(frame, self.height, self.width)
# 2. 归一化到0-1范围
img = img / 255
# 3. 转换为float32类型
img = img.astype(np.float32)
return img, scale
def qnn_run(self, orig_imgs, args):
"""
执行推理的主函数
:param orig_imgs: 原始图像
:param args: 命令行参数
:return: 检测框和分割轮廓
"""
# 1. 图像预处理
input_img_f, scale = self.pretreat_img(orig_imgs) # 获取处理后的图像和缩放比例
input_img = np.expand_dims(input_img_f, 0) # 增加batch维度(NHWC)
invoke_time = [] # 记录每次推理耗时
for i in range(args.invoke_nums):
# 2. 设置输入张量
self.interpreter.set_input_tensor(0, input_img.data)
# 3. 执行推理并计时
t0 = time.time()
self.interpreter.invoke()
t1 = time.time()
cost_time = (t1 - t0) * 1000
invoke_time.append(cost_time)
# 4. 获取输出结果(4个输出层)
# output[2]: 32维特征向量(用于掩码计算)
input0_data = self.interpreter.get_output_tensor(3).reshape(1, 4, self.blocks)
input1_data = self.interpreter.get_output_tensor(2).reshape(1, self.class_num, self.blocks)
input2_data = self.interpreter.get_output_tensor(1).reshape(1, 32, self.blocks)
protos = self.interpreter.get_output_tensor(0).reshape(1, self.maskh, self.maskw, 32).transpose(0, 3, 1, 2)
# 5. 合并输出结果
boxes = np.concatenate([input0_data, input1_data, input2_data], axis=1)
x = boxes.transpose(0, 2, 1) # 转置为[blocks, features]
# 6. 根据置信度阈值过滤低质量检测
x = x[np.amax(x[..., 4:-32], axis=-1) > self.conf]
if len(x) < 1:
return None, None # 未检测到目标
# 7. 重组输出格式: [x1,y1,x2,y2,conf,class_id,mask_features]
x = np.c_[x[..., :4], np.amax(x[..., 4:-32], axis=-1), np.argmax(x[..., 4:-32], axis=-1), x[..., -32:]]
# 8. 转换框格式(xywh -> xyxy)
x[:, :4] = xywh2xyxy(x[:, :4])
# 9. 执行NMS非极大值抑制
index = NMS(x[:, :4], x[:, 4], self.iou)
out_boxes = x[index]
# 10. 将框坐标还原到原图尺寸
out_boxes[..., :4] = out_boxes[..., :4] * scale
# 11. 处理掩码输出
masks = process_mask(protos[0], out_boxes[:, -32:], out_boxes[:, :4], orig_imgs.shape)
# 12. 将掩码转换为轮廓线段
segments = masks2segments(masks)
# 13. 统计推理时间性能
max_invoke_time = max(invoke_time)
min_invoke_time = min(invoke_time)
mean_invoke_time = sum(invoke_time) / args.invoke_nums
var_invoketime = np.var(invoke_time)
print("========================================")
print(f"QNN inference {args.invoke_nums} times :\n --mean_invoke_time is {mean_invoke_time} \n --max_invoke_time is {max_invoke_time} \n --min_invoke_time is {min_invoke_time} \n --var_invoketime is {var_invoketime}")
print("========================================")
return out_boxes, segments # 返回检测框和分割轮廓
def parser_args():
"""
命令行参数解析
:return: 解析后的命令行参数
"""
parser = argparse.ArgumentParser(description="Run model benchmarks")
parser.add_argument('--target_model', type=str, default='yolov11n_seg/cutoff_yolo11n-seg_qcs8550_fp16.qnn231.ctx.bin',
help="inference model path")
parser.add_argument('--imgs', type=str, default='./python/bus.jpg', help="Predict images path")
parser.add_argument('--invoke_nums', type=int, default=10, help="Inference nums")
parser.add_argument('--model_type', type=str, default='QNN', help="run backend")
parser.add_argument('--width', type=int, default=640, help="Model input size")
parser.add_argument('--height', type=int, default=640, help="Model input size")
parser.add_argument('--conf_thres', type=float, default=0.45, help="confidence threshold for filtering the annotations")
parser.add_argument('--iou_thres', type=float, default=0.45, help="Iou threshold for filtering the annotations")
parser.add_argument('--class_num', type=int, default=80, help="Iou threshold for filtering the annotations")
args = parser.parse_args()
return args
def main(args):
"""
主执行函数
:param args: 命令行参数
"""
# 1. 初始化模型
model = qnn_predict(args)
# 2. 读取图像
frame = cv2.imread(args.imgs)
# 3. 执行推理
qnn_out_boxes, qnn_segments = model.qnn_run(frame, args)
# 4. 绘制结果
result = draw_detect_res(frame, qnn_out_boxes, qnn_segments)
# 5. 保存结果图像
cv2.imwrite("./bus_result.jpg", result)
if __name__ == "__main__":
args = parser_args()
main(args)