高通QCS8550部署Yolov11系列模型

伊利丹~怒风
企业官方账号
2025-06-12 18:18:10

 前言

在边缘计算与 AI 深度融合的时代,硬件平台的算力释放与模型部署效率正成为技术落地的关键支点。高通 QCS8550 作为集成 48TOPS AI 算力的旗舰级芯片,其 4nm 制程工艺与 Hexagon 神经网络处理单元,为轻量化 AI 模型推理提供了强大的硬件基底。当 YOLO 系列迭代至 V11 版本,其在目标检测精度与实时性上的突破,与 QCS8550 的异构计算能力形成了天然适配。本文将聚焦于这一黄金组合,从模型转换优化到 NPU 加速推理,完整拆解如何在 QCS8550 平台上实现 YOLOV11 的高效部署,揭示边缘侧 AI 应用从理论到实践的关键路径,为开发者提供兼具技术深度与实操价值的落地指南。

Yolo11系列性能指标
模型尺寸(像素)QCS8550推理速度 NPU(ms)
YOLO11n6401.99
YOLO11s6402.90
YOLO11m6405.14
YOLO11l6406.72
YOLO11x64013.39

点击链接可以下载YOLO11系列模型的pt格式

(一)将pt模型转换为onnx格式

Step1:升级pip版本为25.1.1

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)

Step2:安装ultralytics和onnx

pip install ultralytics onnx

Step3:设置yolo命令的环境变量

方法 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.

Step4:将Yolov8系列模型的pt格式转换为onnx格式

新建一个python文件,命名自定义即可,用于模型转换以及导出:

from ultralytics import YOLO

# 加载同级目录下的.pt模型文件
model = YOLO('yolo11n.pt')  # 替换为实际模型文件名

# 导出ONNX配置参数
export_params = {
    'format': 'onnx',
    'opset': 12,          # 推荐算子集版本
    'simplify': True,     # 启用模型简化
    'dynamic': False,     # 固定输入尺寸
    'imgsz': 640,         # 标准输入尺寸
    'half': False         # 保持FP32精度
}

# 执行转换并保存到同级目录
model.export(**export_params)

执行该程序完成将pt模型导出为onnx模型。

提示:Yolo11s,Yolo11m,Yolo11l替换代码中Yolo11n即可;

(二)使用AIMO将onnx模型转换高通NPU可以运行的模型格式

Step1:选择模型优化,模型格式选择onnx格式上传模型

Step2:选择芯片型号以及目标框架,这里我们选择QCS8550+Qnn2.31

 Step3:点击查看模型,使用Netron查看模型结构,进行输入输出的填写

如上图output节点由Mul和Sigmod两个节点Concat而成,分别点击两个节点复制OUTPUTS的name名称到下图中,并且开启量化选择数据精度int8。

参考上图中红色框部分填写,其他不变,注意开启自动量化功能,AIMO更多操作查看使用说明或开发指南中的AIMO介绍

Step4:接下来进行提交即可,转换完成后将目标模型文件下载,解压缩后其中的.bin.aidem文件即为模型文件

(三)在QCS8550的NPU中推理Yolov11n_int8模型

检查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

 模型进行AI推理:

import numpy as np
import cv2
import argparse
import aidlite
import time

# 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 eqprocess(image, size1, size2):
    h, w, _ = image.shape
    # 创建全零掩码用于图像填充
    mask = np.zeros((size1, size2, 3), dtype=np.float32)
    # 计算宽高缩放比例
    scale1 = h / size1
    scale2 = w / size2
    # 取较大比例作为统一缩放因子,避免图像变形
    scale = max(scale1, scale2)
    # 按比例缩放图像
    img = cv2.resize(image, (int(w / scale), int(h / scale)))
    # 将缩放后的图像放置在掩码中央
    mask[:int(h / scale), :int(w / scale), :] = img
    return mask, scale

# 边界框坐标转换:从(中心x,中心y,宽,高)转换为(左上x,左上y,右下x,右下y)
def xywh2xyxy(x):
    y = np.copy(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # 计算左上角x坐标
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # 计算左上角y坐标
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # 计算右下角x坐标
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # 计算右下角y坐标
    return y

# 边界框坐标转换:从(左上x,左上y,右下x,右下y)转换为(左上x,左上y,宽,高)
def xyxy2xywh(box):
    box[:, 2:] = box[:, 2:] - box[:, :2]  # 宽高 = 右下坐标 - 左上坐标
    return box

# 单类非极大值抑制(NMS)算法:过滤重叠的边界框
def NMS(dets, scores, thresh):
    x1 = dets[:, 0]  # 左上角x坐标
    y1 = dets[:, 1]  # 左上角y坐标
    x2 = dets[:, 2]  # 右下角x坐标
    y2 = dets[:, 3]  # 右下角y坐标
    # 计算边界框面积
    areas = (y2 - y1 + 1) * (x2 - x1 + 1)
    keep = []  # 保留的边界框索引
    # 按得分降序排列索引
    index = scores.argsort()[::-1]
    
    while index.size > 0:
        i = index[0]  # 取出当前得分最高的边界框索引
        keep.append(i)  # 保留该边界框
        
        # 计算当前边界框与剩余边界框的重叠区域
        x11 = np.maximum(x1[i], x1[index[1:]])
        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)
        h = np.maximum(0, y22 - y11 + 1)
        overlaps = w * h  # 重叠区域面积
        
        # 计算交并比(IoU)
        ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)
        # 保留IoU小于阈值的边界框索引
        idx = np.where(ious <= thresh)[0]
        # 更新剩余边界框索引(跳过已处理的索引)
        index = index[idx + 1]
    return keep

# 绘制检测结果:在原图上绘制边界框和类别标签
def draw_detect_res(img, det_pred):
    if det_pred is None:
        return img  # 无检测结果时直接返回原图
    
    img = img.astype(np.uint8)
    im_canvas = img.copy()  # 复制原图用于混合绘制
    # 按类别数生成颜色梯度(不同类别使用不同颜色)
    color_step = int(255 / len(CLASSES))
    
    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
        # 打印检测结果(序号、坐标、得分、类别)
        print(i + 1, [x1, y1, x2, y2], det_pred[i][4], f'{CLASSES[cls_id]}')
        
        # 绘制类别标签文本
        cv2.putText(img, f'{CLASSES[cls_id]}', (x1, y1 - 6), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        # 绘制边界框(颜色根据类别ID渐变)
        cv2.rectangle(img, (x1, y1), (x2, y2), 
                     (0, int(cls_id * color_step), int(255 - cls_id * color_step)), 
                     thickness=2)
    
    # 图像混合(原图30% + 绘制结果70%),使边界框更清晰
    img = cv2.addWeighted(im_canvas, 0.3, img, 0.7, 0)
    return img

# 掩码缩放函数:将掩码调整为原始图像尺寸
def scale_mask(masks, im0_shape):
    # 使用双线性插值调整掩码尺寸
    masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]), interpolation=cv2.INTER_LINEAR)
    # 确保掩码为三维张量(H,W,C)
    if len(masks.shape) == 2:
        masks = masks[:, :, None]
    return masks

# 掩码裁剪函数:根据边界框裁剪对应区域的掩码
def crop_mask(masks, boxes):
    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):
    c, mh, mw = protos.shape  # 原型张量尺寸(通道数、高、宽)
    # 矩阵乘法生成掩码(HWN格式)
    masks = np.matmul(masks_in, protos.reshape((c, -1))).reshape((-1, mh, mw)).transpose(1, 2, 0)
    masks = np.ascontiguousarray(masks)  # 确保内存连续
    masks = scale_mask(masks, im0_shape)  # 缩放掩码至原始图像尺寸
    masks = np.einsum('HWN -> NHW', masks)  # 转换维度顺序(HWN->NHW)
    masks = crop_mask(masks, bboxes)  # 按边界框裁剪掩码
    return np.greater(masks, 0.5)  # 二值化掩码(阈值0.5)

# 掩码转线段函数:将二值掩码转换为轮廓线段
def masks2segments(masks):
    segments = []
    for x in masks.astype('uint8'):
        # 查找轮廓(只保留外轮廓,不简化点)
        c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0]
        if c:
            # 取最长轮廓(假设为主物体轮廓)
            c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2)
        else:
            c = np.zeros((0, 2))  # 无轮廓时返回空数组
        segments.append(c.astype('float32'))
    return segments

# 检测阈值设置
OBJ_THRESH = 0.45  # 目标置信度阈值
NMS_THRESH = 0.45   # NMS重叠阈值

class Yolo11n(object):
    def __init__(self, model_path, width, height, class_num):
        """YOLOv11模型推理类初始化
        Args:
            model_path: 量化后模型路径(.qnn231.ctx.bin格式)
            width/height: 模型输入图像宽高
            class_num: 检测类别数(COCO数据集为80类)
        """
        self.class_num = class_num
        self.width = width
        self.height = height
        # 模型输入形状定义(批量大小1,高,宽,通道数3)
        input_shape = [[1, height, width, 3]]
        
        # 计算输出特征块数量(基于输入尺寸的1/64+1/256+1/1024)
        self.blocks = int(height * width * (1 / 64 + 1 / 256 + 1 / 1024))
        # 掩码特征图尺寸(输入尺寸的1/4)
        self.maskw = int(width / 4)
        self.maskh = int(height / 4)
        # 模型输出形状定义(边界框参数、类别置信度)
        self.output_shape = [[1, 4, self.blocks], [1, class_num, self.blocks]]
        
        # 加载模型实例
        self.model = aidlite.Model.create_instance(model_path)
        if self.model is None:
            print("Create model failed !")
            return
        # 设置模型输入输出属性(形状、数据类型)
        self.model.set_model_properties(input_shape, aidlite.DataType.TYPE_FLOAT32, 
                                        self.output_shape, aidlite.DataType.TYPE_FLOAT32)
        
        # 创建推理配置实例
        self.config = aidlite.Config.create_instance()
        if self.config is None:
            print("build_interpretper_from_model_and_config failed !")
            return
        
        # 设置推理框架为QNN2.31(适配QCS8550 NPU)
        self.config.framework_type = aidlite.FrameworkType.TYPE_QNN231
        # 设置加速类型为DSP(利用芯片数字信号处理器)
        self.config.accelerate_type = aidlite.AccelerateType.TYPE_DSP
        # 设置为量化模型(INT8精度)
        self.config.is_quantify_model = 1
        
        # 构建解释器(模型与配置绑定)
        self.interpreter = aidlite.InterpreterBuilder.build_interpretper_from_model_and_config(self.model, self.config)
        if self.interpreter is None:
            print("build_interpretper_from_model_and_config failed !")
            return
        
        # 初始化解释器并加载模型
        self.interpreter.init()
        self.interpreter.load_model()

    def __call__(self, frame, invoke_nums):
        """执行模型推理并返回检测结果
        Args:
            frame: 输入图像(BGR格式)
            invoke_nums: 推理调用次数(用于性能测试)
        Returns:
            检测结果数组(包含边界框、置信度、类别ID)
        """
        # 图像预处理(调整尺寸并填充)
        img, scale = eqprocess(frame, self.height, self.width)
        # 归一化处理(像素值范围0-1)
        img = img / 255
        img = img.astype(np.float32)
        # 设置模型输入张量
        self.interpreter.set_input_tensor(0, img.data)
        
        # 性能测试计时
        invoke_time = []
        for i in range(invoke_nums):
            t1 = time.time()
            self.interpreter.invoke()  # 执行模型推理
            cost_time = (time.time() - t1) * 1000  # 转换为毫秒
            invoke_time.append(cost_time)
        
        # 计算性能指标
        max_invoke_time = max(invoke_time)
        min_invoke_time = min(invoke_time)
        mean_invoke_time = sum(invoke_time) / invoke_nums
        var_invoketime = np.var(invoke_time)
        print("====================================")
        print(f"QNN invoke {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("====================================")
        
        # 获取模型输出张量
        qnn_1 = self.interpreter.get_output_tensor(0)
        qnn_2 = self.interpreter.get_output_tensor(1)
        # 按张量长度排序(确保边界框参数在前,类别置信度在后)
        qnn_out = sorted([qnn_1, qnn_2], key=len)
        
        # 重塑输出张量形状
        qnn_local = qnn_out[0].reshape(*self.output_shape[0])  # 边界框参数
        qnn_conf = qnn_out[1].reshape(*self.output_shape[1])    # 类别置信度
        
        # 合并边界框参数和类别置信度,转置维度
        x = np.concatenate([qnn_local, qnn_conf], axis=1).transpose(0, 2, 1)
        # 过滤掉置信度低于阈值的预测
        x = x[np.amax(x[..., 4:], axis=-1) > OBJ_THRESH]
        if len(x) < 1:
            return None  # 无有效检测结果
        
        # 提取最大置信度和对应类别ID
        x = np.c_[x[..., :4], np.amax(x[..., 4:], axis=-1), np.argmax(x[..., 4:], axis=-1)]
        
        # 边界框坐标转换(中心坐标转左上右下坐标)
        x[:, :4] = xywh2xyxy(x[:, :4])
        # 执行NMS过滤重叠边界框
        index = NMS(x[:, :4], x[:, 4], NMS_THRESH)
        out_boxes = x[index]
        # 按预处理比例还原边界框坐标到原始图像尺寸
        out_boxes[..., :4] = out_boxes[..., :4] * scale
        
        return out_boxes

def parser_args():
    """命令行参数解析函数"""
    parser = argparse.ArgumentParser(description="Run model benchmarks")
    # 模型路径参数(默认指向QCS8550量化模型)
    parser.add_argument('--target_model', type=str, default='/home/aidlux/yolo11/8550_models/cutoff_yolo11n_qcs8550_w8a8.qnn231.ctx.bin',
                        help="inference model path")
    # 输入图像路径参数
    parser.add_argument('--imgs', type=str, default='bus.jpg', help="Predict images path")
    # 模型输入高度参数
    parser.add_argument('--height', type=int, default=640, help="run backend")
    # 模型输入宽度参数
    parser.add_argument('--weight', type=int, default=640, help="run backend")
    # 检测类别数参数
    parser.add_argument('--cls_num', type=int, default=80, help="run backend")
    # 推理调用次数参数(用于性能测试)
    parser.add_argument('--invoke_nums', type=int, default=100, help="Inference nums")
    # 推理框架类型参数
    parser.add_argument('--model_type', type=str, default='QNN', help="run backend")
    args = parser.parse_args()
    return args

if __name__ == "__main__":
    """主函数:解析参数->创建模型->执行推理->保存结果"""
    args = parser_args()
    height = args.height
    weight = args.weight

    # 创建YOLOv11推理实例
    model = Yolo11n(args.target_model, args.weight, args.height, args.cls_num)
    # 读取输入图像
    frame = cv2.imread(args.imgs)

    # 执行推理并获取检测结果
    out_boxes = model(frame, args.invoke_nums)
    print(f"=================== \n Detect {len(out_boxes)} targets.")
    # 在原图上绘制检测结果
    result = draw_detect_res(frame, out_boxes)
    # 保存结果图像
    cv2.imwrite("result.jpg", result)

推理结果

...全文
58 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

2,852

社区成员

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

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