高通QCS8550部署Yolov10模型与性能测试
在人工智能飞速发展的当下,边缘计算设备凭借其低延迟、高隐私性等优势,成为众多实时性应用场景的关键支撑。高通骁龙 QCS8550 作为一款面向物联网(IoT)领域的高端处理器,集强大计算能力、极致边缘 AI 处理性能、Wi-Fi 7 连接技术以及出色的视频和图形处理能力于一身,为各类对性能要求严苛的 IoT 应用提供了坚实基础。
与此同时,目标检测作为计算机视觉领域的核心任务之一,在安防监控、自动驾驶、工业质检等诸多场景中发挥着不可或缺的作用。YOLO(You Only Look Once)系列算法以其高效、快速的检测性能,长期处于实时目标检测技术的前沿。而 YOLOv10 作为该系列的成果,更是通过创新性的架构设计与训练策略,在检测精度、推理速度以及部署便利性等方面实现了质的飞跃。其开创性地摒弃了传统非极大值抑制(NMS)后处理步骤,采用双标签分配策略和一致性匹配度量,大幅提升了困难样本的检测精度;在技术架构上,轻量化骨干网络结合多尺度特征协同融合模块以及空间 - 通道解耦检测头设计,使得模型在不同算力平台上均能展现出卓越性能。
将 YOLOv10 这一先进的目标检测模型部署于高通 QCS8550 处理器上,有望为边缘设备的实时视觉分析应用带来全新的突破。本研究聚焦于高通 QCS8550 部署 Yolov10 模型的全过程,并对其性能展开深入测试分析,旨在探索二者结合在实际应用场景中的潜力与优势,为推动边缘 AI 技术在各行业的广泛应用贡献有价值的参考。
Qualcomm Dragonwing™ QCM8550 | Qualcomm
Yolov10介绍
模型优化平台 (AIMO) 用户指南 | APLUX Doc Center
本次测试采用的硬件设备
模型 尺寸640*640 | CPU | NPU QNN2.31 | NPU QNN2.31 | |||
FP32 | FP16 | INT8 | ||||
YOLOv10n | 156.34 ms | 6.40 FPS | 4.72 ms | 211.86 FPS | 2.89 ms | 346.02 FPS |
YOLOv10s | 403.69 ms | 2.48 FPS | 6.77 ms | 147.71 FPS | 3.06 ms | 326.80 FPS |
YOLOv10m | 966.71 ms | 1.03 FPS | 14.2 ms | 70.42 FPS | 6.11 ms | 163.67 FPS |
YOLOv10b | 1394.27 ms | 0.72 FPS | 19.79 ms | 50.53 FPS | 7.14 ms | 140.06 FPS |
YOLOv10L | 1745.86 ms | 0.57 FPS | 25.7 ms | 38.91 FPS | 8.48 ms | 117.92 FPS |
YOLOv10x | 2326.03 ms | 0.43 FPS | 36.61 ms | 27.31 FPS | 11.82 ms | 84.60 FPS |
点击链接可以下载YOLOv10系列模型的pt格式,其他模型尺寸可以通过AIMO转换模型,并修改下面参考代码中的model_size测试即可
(一)将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:将Yolov10系列模型的pt格式转换为onnx格式
新建一个python文件,命名自定义即可,用于模型转换以及导出:
from ultralytics import YOLO# 加载同级目录下的.pt模型文件
model = YOLO('yolo10n.pt') # 替换为实际模型文件名# 导出ONNX配置参数
export_params = {'format': 'onnx','opset': 12, # 推荐算子集版本'simplify': True, # 启用模型简化'dynamic': False, # 固定输入尺寸'imgsz': 640, # 标准输入尺寸'half': False # 保持FP32精度
}# 执行转换并保存到同级目录
model.export(**export_params)
执行该程序完成将pt模型导出为onnx模型。
python convert_yolov10.py #这个python文件为上面所命名的py文件
提示:Yolov10s,Yolov10m,Yolov10b,Yolov10l,Yolov10x替换代码中Yolo10n即可;
(二)使用AIMO将onnx模型转换高通NPU可以运行的模型格式
Step1:选择模型优化,模型格式选择onnx格式上传模型
Step2:选择芯片型号以及目标框架,这里我们选择QCS8550+Qnn2.31
Step3:点击查看模型,使用Netron查看模型结构,进行输入输出的填写
如上图点击Transpose,复制OUTPUTS输出的/model.23/Transpose_output_0
参考上图中红色框部分填写,其他不变,注意开启自动量化功能,AIMO更多操作查看使用说明或开发指南中的AIMO介绍。
Step4:接下来进行提交即可,转换完成后将目标模型文件下载,解压缩后其中的.bin.aidem文件即为模型文件
(三)在QCS8550的NPU中推理Yolov10n_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 time # 用于计时
import numpy as np # 用于数值计算和数组操作
import cv2 # 用于图像处理
import os # 用于文件路径操作(本代码未直接使用,保留作为潜在扩展)
import aidlite # 用于QNN模型推理(底层加速框架)
import argparse # 用于解析命令行参数
import onnxruntime # 用于ONNX模型推理(后处理部分) """返回 COCO 数据集的类别名称(80 类),与模型输出的类别索引对应。"""
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", "mouse","remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator","book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"
]def letterbox(im,new_shape,color=(114, 114, 114),auto=False,scaleFill=False,scaleup=True,stride=32,
):"""对图像进行缩放和填充,使其满足模型输入的步长约束(常用于目标检测预处理)。避免直接拉伸图像导致的畸变,通过等比例缩放+边缘填充实现。参数:im: 输入图像,shape为[height, width, 3]new_shape: 目标尺寸,可为int(正方形)或tuple(h, w)color: 填充边缘的颜色,默认灰色(114,114,114)auto: 是否自动计算填充(确保填充后尺寸为stride的倍数)scaleFill: 是否直接拉伸图像至目标尺寸(可能导致畸变)scaleup: 是否允许放大图像(关闭时仅缩小,避免低清图放大模糊)stride: 模型要求的步长(如32,确保输出尺寸为其倍数)返回:im: 处理后的图像,shape为[new_height, new_width, 3]ratio: 缩放比例,[宽缩放比, 高缩放比](dw, dh): 填充的宽度和高度(双边填充,各占一半)"""shape = im.shape[:2] # 获取输入图像的原始尺寸 [height, width]if isinstance(new_shape, int): # 若目标尺寸为int,转为正方形 (h, w)new_shape = (new_shape, new_shape)# 计算缩放比例(新尺寸/原始尺寸,取宽高比例的最小值,避免图像超出目标尺寸)r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])if not scaleup: # 若不允许放大,则缩放比例不超过1.0(只缩小)r = min(r, 1.0)# 计算缩放后的尺寸(未填充)new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) # [new_width, new_height]# 计算需要填充的宽度和高度(目标尺寸 - 缩放后尺寸)dw, dh = (new_shape[1] - new_unpad[0],new_shape[0] - new_unpad[1],)if auto: # 自动填充:确保填充后尺寸为stride的倍数(便于模型下采样)dw, dh = np.mod(dw, stride), np.mod(dh, stride)elif scaleFill: # 拉伸填充:直接将图像拉伸至目标尺寸(可能失真)dw, dh = 0.0, 0.0new_unpad = (new_shape[1], new_shape[0])ratio = (new_shape[1] / shape[1], new_shape[0] / shape[0]) # 重新计算拉伸比例else:ratio = (r, r) # 等比例缩放# 填充分为上下/左右各一半(避免填充集中在一侧)dw /= 2dh /= 2# 对图像进行缩放if shape[::-1] != new_unpad: # 若缩放后的尺寸与原始不同,则执行缩放im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) # 线性插值缩放# 计算上下左右的填充量(四舍五入为整数)top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) # 上填充和下填充left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) # 左填充和右填充# 对图像进行边缘填充(BORDER_CONSTANT表示用固定颜色填充)im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)return im, ratio, (dw, dh)class Colors:"""定义目标检测框的颜色 palette,用于可视化不同类别的目标。支持通过类别索引获取对应颜色,支持BGR格式(OpenCV默认格式)。"""def __init__(self):# 预定义20种十六进制颜色(明亮且区分度高)hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB','2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7')# 转换为RGB格式的颜色列表self.palette = [self.hex2rgb(f'#{c}') for c in hexs]self.n = len(self.palette) # 颜色数量def __call__(self, i, bgr=False):"""根据类别索引i获取颜色,支持BGR格式(OpenCV显示需要)"""c = self.palette[int(i) % self.n] # 索引取模,避免超出颜色数量return (c[2], c[1], c[0]) if bgr else c # BGR格式交换R和B通道@staticmethoddef hex2rgb(h):"""将十六进制颜色(如#FF3838)转换为RGB元组"""return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))def rescale_coords(boxes, image_shape, input_shape):"""将模型输出的检测框坐标(基于预处理后的图像)映射回原始图像尺寸。抵消letterbox函数的缩放和填充影响。参数:boxes: 模型输出的检测框,shape为[N, 4],格式[x1, y1, x2, y2]image_shape: 原始图像尺寸 [height, width]input_shape: 预处理后的图像尺寸(模型输入尺寸)[height, width]返回:boxes: 映射后的检测框,基于原始图像尺寸,格式[x1, y1, x2, y2](整数)"""image_height, image_width = image_shapeinput_height, input_width = input_shape# 计算缩放比例(原始图像 -> 预处理图像的缩放比例的倒数)scale = min(input_width / image_width, input_height / image_height)# 计算letterbox填充的宽度和高度(单边填充量)pad_w = (input_width - image_width * scale) / 2pad_h = (input_height - image_height * scale) / 2# 抵消填充和缩放:先减去填充,再除以缩放比例boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad_w) / scale # 宽度方向(x1, x2)boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad_h) / scale # 高度方向(y1, y2)# 裁剪检测框,避免超出原始图像边界boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, image_width)boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, image_height)return boxes.astype(int) # 转换为整数坐标def preprocess(image, input_shape):"""对输入图像进行预处理,使其符合模型输入要求。包括:缩放填充(letterbox)、通道转换(BGR->RGB)、归一化等。参数:image: 原始图像(BGR格式,OpenCV读取默认)input_shape: 模型输入尺寸 [height, width]返回:blob: 预处理后的图像张量,shape为[1, height, width, 3],值范围[0,1]"""# 1. 缩放填充至模型输入尺寸input_img = letterbox(image, input_shape)[0]# 2. 转换通道:OpenCV默认BGR,模型通常需要RGB,因此翻转通道input_img = input_img[..., ::-1] # BGR -> RGB# 3. 增加批次维度(模型输入通常为[batch, h, w, 3])input_img = input_img[np.newaxis, :, :, :].astype(np.float32)# 4. 确保数组内存连续(优化计算效率)input_img = np.ascontiguousarray(input_img)# 5. 归一化:将像素值从[0,255]转换为[0,1]blob = input_img / 255.0return blobdef postprocess(output_data, conf_thres, image_shape, input_shape):"""对模型输出进行后处理,筛选有效检测框并映射回原始图像。参数:output_data: 模型输出的原始数据,shape为[8400, 84](8400个候选框,84=4坐标+1置信度+80类别)conf_thres: 置信度阈值(过滤低置信度框)image_shape: 原始图像尺寸 [height, width]input_shape: 模型输入尺寸 [height, width]返回:boxes: 筛选并映射后的检测框,[N,4],格式[x1,y1,x2,y2]scores: 检测框的置信度,[N,]labels: 检测框的类别索引,[N,]"""outs = output_data # 模型输出的候选框(8400个)# 1. 过滤低置信度框(置信度 >= conf_thres)outs = outs[outs[:, 4] >= conf_thres]if len(outs) == 0: # 无有效检测框return np.array([]), np.array([]), np.array([])# 2. 提取坐标、置信度和类别boxes = outs[:, :4] # 前4列为坐标 [x1, y1, x2, y2]# 计算每个框的最终得分(置信度 * 类别概率,此处简化为取最大类别概率)scores = np.max(outs[:, 5:], axis=1) * outs[:, 4]# 获取类别索引(概率最大的类别)labels = np.argmax(outs[:, 5:], axis=1).astype(int)# 3. 将检测框映射回原始图像尺寸boxes = rescale_coords(boxes, image_shape, input_shape)return boxes, scores, labelsclass qnn_yolov10:"""基于QNN框架的YOLOv10模型推理类,用于加载量化模型并执行推理。支持NPU/GPU/CPU加速,可多次推理并统计时间性能。"""def __init__(self, model_path, sdk="qnn", backend="npu"):"""初始化QNN模型,配置推理参数并加载模型。参数:model_path: QNN模型文件路径(如*.ctx.bin)sdk: 使用的SDK("qnn"或"snpe2")backend: 推理后端("npu"/"gpu"/"cpu")"""# 创建配置实例self.config = aidlite.Config.create_instance()if self.config is None:print("创建配置实例失败!")return False# 配置推理类型(本地推理)self.config.implement_type = aidlite.ImplementType.TYPE_LOCAL# 配置框架类型(QNN或SNPE2)if sdk.lower() == "qnn":self.config.framework_type = aidlite.FrameworkType.TYPE_QNNelse:self.config.framework_type = aidlite.FrameworkType.TYPE_SNPE2# 配置加速后端(NPU/GPU/CPU)if backend.lower() == "npu":self.config.accelerate_type = aidlite.AccelerateType.TYPE_DSP # NPU通常映射为DSPelif backend.lower() == "gpu":self.config.accelerate_type = aidlite.AccelerateType.TYPE_GPUelse:self.config.accelerate_type = aidlite.AccelerateType.TYPE_CPUself.config.is_quantify_model = 1 # 标记为量化模型(QNN模型通常已量化)# 加载模型文件self.model = aidlite.Model.create_instance(model_path)if self.model is None:print("创建模型实例失败!")return False# 构建推理器(结合模型和配置)self.interpreter = aidlite.InterpreterBuilder.build_interpretper_from_model_and_config(self.model, self.config)if self.interpreter is None:print("构建推理器失败!")return None# 初始化推理器result = self.interpreter.init()if result != 0:print("推理器初始化失败!")return False# 加载模型到推理器result = self.interpreter.load_model()if result != 0:print("模型加载到推理器失败!")return Falseprint("检测模型加载成功!")def __del__(self):"""析构函数:释放推理器资源"""self.interpreter.destory()def __call__(self, img_input, invoke_nums):"""执行模型推理,多次运行并统计时间性能。参数:img_input: 预处理后的输入图像张量invoke_nums: 推理次数(用于统计平均时间)返回:output1: 模型推理的原始输出数据"""# 设置输入张量(绑定输入数据到推理器)result = self.interpreter.set_input_tensor(0, img_input.data)if result != 0:print("设置输入张量失败!")# 多次推理并记录时间invoke_time = []for i in range(invoke_nums):t1 = time.time() # 记录开始时间result = self.interpreter.invoke() # 执行推理if result != 0:print("推理执行失败!")# 计算耗时(毫秒)cost_time = (time.time() - t1) * 1000invoke_time.append(cost_time)# 统计时间性能max_invoke_time = max(invoke_time)min_invoke_time = min(invoke_time)mean_invoke_time = sum(invoke_time) / invoke_numsvar_invoketime = np.var(invoke_time) # 方差(衡量时间稳定性)print("====================================")print(f"QNN推理{invoke_nums}次:")print(f" --平均耗时:{mean_invoke_time:.2f} ms")print(f" --最大耗时:{max_invoke_time:.2f} ms")print(f" --最小耗时:{min_invoke_time:.2f} ms")print(f" --耗时方差:{var_invoketime:.2f} (ms^2)") # 方差越小,性能越稳定print("====================================")# 获取推理输出output1 = self.interpreter.get_output_tensor(0)return output1class onnx_yolov10:"""基于ONNX Runtime的YOLOv10后处理模型推理类(用于处理QNN输出的原始数据)。"""def __init__(self, model_path):"""初始化ONNX模型,加载后处理模型(如NMS等操作)。参数:model_path: ONNX后处理模型路径"""# 配置ONNX Runtime会话选项(单线程,优化推理速度)self.sess_options = onnxruntime.SessionOptions()self.sess_options.intra_op_num_threads = 1 # intra-op并行线程数# 创建推理会话self.sess = onnxruntime.InferenceSession(model_path, sess_options=self.sess_options)# 获取输入输出节点名称self.outname = [i.name for i in self.sess.get_outputs()]self.inname = [i.name for i in self.sess.get_inputs()]def __call__(self, img_input):"""执行ONNX模型推理(后处理)。参数:img_input: QNN模型输出的原始数据返回:out_put: 后处理后的结果(筛选和优化后的检测框)"""# 构建输入字典(键为输入节点名称,值为输入数据)inp = {self.inname[0]: img_input}t1 = time.time() # 记录后处理时间(可选)# 执行推理out_put = self.sess.run(self.outname, inp)# 可选:打印后处理耗时# cost_time = (time.time() - t1) * 1000# print(f"ONNX后处理耗时:{cost_time:.2f} ms")return out_putdef main(args):"""主函数:加载模型、处理图像、执行推理并可视化结果"""input_shape = (640, 640) # 模型输入尺寸(YOLOv10常用640x640)conf_thres = 0.25 # 置信度阈值(过滤低置信度检测框)img_path = args.imgs # 输入图像路径invoke_nums = args.invoke_nums # 推理次数(用于统计性能)qnn_path = args.target_model # QNN模型路径# 1. 初始化QNN模型(主检测网络)和ONNX模型(后处理)qnn_model1 = qnn_yolov10(qnn_path) # QNN模型加载onnx_model_path = 'post_pro_1.onnx' # 后处理ONNX模型路径onnx_model = onnx_yolov10(onnx_model_path) # ONNX模型加载print("开始执行QNN推理...")# 2. 读取并预处理图像im0 = cv2.imread(img_path) # 读取原始图像(BGR格式)if im0 is None:print(f"无法读取图像:{img_path}")returnimage_shape = im0.shape[:2] # 原始图像尺寸 [height, width]img_qnn = preprocess(im0, input_shape) # 预处理(缩放、归一化等)# 3. 执行QNN推理(多次运行并统计时间)qnn_out_shape = (1, 8400, 84) # QNN输出的目标形状(1批,8400框,84维度)out1 = qnn_model1(img_qnn, invoke_nums) # 执行QNN推理out1 = out1.reshape(*qnn_out_shape) # 调整输出形状以匹配后处理要求# 4. 执行ONNX后处理(筛选和优化检测框)out2 = onnx_model(out1)[0] # 后处理输出# 5. 解析后处理结果(映射到原始图像并筛选)boxes, scores, labels = postprocess(out2, conf_thres, image_shape, input_shape)print(f"检测到{len(boxes)}个目标")# 6. 可视化检测结果(绘制边界框和标签)colors = Colors() # 初始化颜色生成器for label, score, box in zip(labels, scores, boxes):# 生成类别标签文本(类别名 + 置信度)label_text = f'{classes[label]}: {score:.2f}'# 获取该类别的颜色(BGR格式,用于OpenCV绘制)color = colors(label, True)# 绘制边界框(矩形)cv2.rectangle(im0, (box[0], box[1]), (box[2], box[3]), color, 2, lineType=cv2.LINE_AA)# 绘制标签文本(左上角,稍偏移避免覆盖框)cv2.putText(im0, label_text, (box[0], box[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)# 7. 保存并显示结果output_image_path = "detected_results.jpg"cv2.imwrite(output_image_path, im0) # 保存结果图像print(f"检测结果已保存至:{output_image_path}")# 可选:显示图像(需要GUI支持)# cv2.imshow("Detection Result", im0)# cv2.waitKey(0)# cv2.destroyAllWindows()def parser_args():"""解析命令行参数"""parser = argparse.ArgumentParser(description="YOLOv10模型推理脚本(基于QNN和ONNX)")parser.add_argument('--target_model', type=str, default='yolov10n/cutoff_yolov10n_qcs8550_w8a8.qnn231.ctx.bin',help="QNN目标模型路径")parser.add_argument('--imgs', type=str, default='bus.jpg',help="输入图像路径")parser.add_argument('--invoke_nums', type=int, default=100,help="推理次数(用于统计平均耗时)")args = parser.parse_args()return argsif __name__ == "__main__":args = parser_args() # 解析参数main(args) # 执行主函数