2,861
社区成员




在当今科技飞速发展的时代,边缘计算和人工智能的融合正推动着众多领域的创新变革。而在这一进程中,硬件平台的性能起着至关重要的作用。今天,我们将聚焦于一款强大的硬件 —— 高通 QCS8550,探索如何在其上部署热门的 Yolov5 模型。
高通 QCS8550 采用先进的 4 纳米制程工艺,在提升性能的同时有效降低了功耗。其核心由八核 Kryo CPU 构成,具体包括一个主频高达 3.2GHz 的超大核,能轻松应对高强度任务;四个主频为 2.8GHz 的性能内核,兼顾性能与能效;还有三个主频 2.0GHz 的效率内核,负责日常轻负载操作,这种合理的内核配置实现了任务处理的高效与节能。在图形处理方面,集成的 Adreno 740 GPU 表现卓越,支持 4K@60Hz 的显示输出,为高清多媒体应用提供有力支撑。不仅如此,该平台在视频处理能力上也十分出色,支持 8K@30fps 的视频编码以及 8K@60fps 的视频解码,并且兼容 H.264 和 H.265 编码标准,能满足各类高品质视频应用的需求。
然而,QCS8550 最为引人注目的,当属其强大的 AI 处理能力。整体 AI 算力高达 48TOPS,内部集成的高通 Hexagon 神经网络处理单元支持先进的 INT4 AI 精度格式,相比上一代产品,在持续 AI 推理方面实现了 60% 的能效提升。这意味着设备在拥有强大 AI 性能的同时,能够极大地降低能耗,尤其适用于智能监控、机器人、自动驾驶辅助等对 AI 性能和能耗有严苛要求的长时间运行智能场景。
模型 尺寸640*640 |
CPU |
NPU QNN2.31 | NPU QNN2.31 | |||
FP32 | FP16 | INT8 | ||||
YOLOv5n | 137.26ms | 7.3FPS | 2.89ms | 346 FPS | 1.43ms | 699 FPS |
YOLOv5s | 511.52ms | 2FPS | 5.12ms | 195 FPS | 1.93ms | 518 FPS |
YOLOv5m | 1312.95ms | 0.8FPS | 4.96ms | 201 FPS | 3.43ms | 291FPS |
YOLOv5l | 3474.34ms | 0.3FPS | 26.39ms | 37 FPS | 6.91ms | 144FPS |
YOLOv5x | 6122.27ms | 0.2FPS | 55.08ms | 18 FPS | 13.91ms | 72 FPS |
点击链接可以下载YOLOv5系列模型的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('yolo5n.pt') # 替换为实际模型文件名
# 导出ONNX配置参数
export_params = {
'format': 'onnx',
'opset': 12, # 推荐算子集版本
'simplify': True, # 启用模型简化
'dynamic': False, # 固定输入尺寸
'imgsz': 640, # 标准输入尺寸
'half': False # 保持FP32精度
}
# 执行转换并保存到同级目录
model.export(**export_params)
执行该程序完成将pt模型导出为onnx模型。
提示:Yolov5s,Yolov5m,Yolov5l,Yolov5x替换代码中Yolo5n即可;
如上图output节点由Mul和Sigmod两个节点Concat而成,分别点击两个节点复制OUTPUTS的name名称到下图中,并且开启量化选择数据精度int8,注意yolov5有3个output,都要填上。
参考上图中红色框部分填写,其他不变,注意开启自动量化功能,AIMO更多操作查看使用说明或开发指南中的AIMO介绍。
检查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 aidlite
import argparse
# --------------------- 模型配置参数 ---------------------
OBJ_CLASS_NUM = 80 # COCO数据集类别数量
NMS_THRESH = 0.45 # 非极大值抑制阈值
BOX_THRESH = 0.5 # 边界框置信度阈值
MODEL_SIZE = 640 # 模型输入尺寸
OBJ_NUMB_MAX_SIZE = 64 # 最大检测目标数量
PROP_BOX_SIZE = (5 + OBJ_CLASS_NUM) # 每个边界框属性大小(4坐标+1置信度+类别数)
STRIDE8_SIZE = (MODEL_SIZE / 8) # 8倍下采样特征图尺寸
STRIDE16_SIZE = (MODEL_SIZE / 16) # 16倍下采样特征图尺寸
STRIDE32_SIZE = (MODEL_SIZE / 32) # 32倍下采样特征图尺寸
# --------------------- 锚框定义 ---------------------
anchors = [[10, 13, 16, 30, 33, 23], # 适用于小目标的锚框
[30, 61, 62, 45, 59, 119], # 适用于中目标的锚框
[116, 90, 156, 198, 373, 326]] # 适用于大目标的锚框
# --------------------- COCO类别列表 ---------------------
coco_class = [
'person', 'bicycle', 'car', 'motorcycle', 'airplane', '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', 'couch', 'potted plant', 'bed', 'dining table', 'toilet',
'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator',
'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
# --------------------- 图像处理函数 ---------------------
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):
'''
将边界框从(中心x, 中心y, 宽, 高)转换为(左上角x, 左上角y, 右下角x, 右下角y)
:param x: 输入边界框数组
:return: 转换后的边界框数组
'''
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
def xyxy2xywh(box):
'''
将边界框从(左上角x, 左上角y, 右下角x, 右下角y)转换为(左上角x, 左上角y, 宽, 高)
:param box: 输入边界框数组
:return: 转换后的边界框数组
'''
box[:, 2:] = box[:, 2:] - box[:, :2] # 计算宽高
return box
# --------------------- 非极大值抑制函数 ---------------------
def NMS(dets, scores, thresh):
'''
单类非极大值抑制算法
:param dets: 边界框数组 (N, 5),格式为(左上角x, 左上角y, 右下角x, 右下角y, 分数)
: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] # 每次取分数最高的边界框
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)
# 计算IOU
overlaps = w * h
ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)
# 保留IOU小于阈值的边界框索引
idx = np.where(ious <= thresh)[0]
index = index[idx + 1] # 调整索引以匹配原数组
return keep
# --------------------- 边界框裁剪函数 ---------------------
def clip_coords(boxes, img_shape):
# 将边界框坐标裁剪到图像尺寸范围内
boxes[:, 0].clip(0, img_shape[1], out=boxes[:, 0]) # x1
boxes[:, 1].clip(0, img_shape[0], out=boxes[:, 1]) # y1
boxes[:, 2].clip(0, img_shape[1], out=boxes[:, 2]) # x2
boxes[:, 3].clip(0, img_shape[0], out=boxes[:, 3]) # y2
# --------------------- 检测结果后处理函数 ---------------------
def detect_postprocess(prediction, img0shape, img1shape, conf_thres=0.25, iou_thres=0.45):
'''
检测输出后处理函数
:param prediction: aidlite模型预测输出
:param img0shape: 原始图片尺寸
:param img1shape: 输入图片尺寸
:param conf_thres: 置信度阈值
:param iou_thres: IOU阈值
:return: 处理后的边界框信息,格式为(xywh, 置信度, 类别)
'''
h, w, _ = img1shape
# 筛选出置信度高于阈值的候选框
valid_condidates = prediction[prediction[..., 4] > conf_thres]
# 将类别置信度乘以目标置信度
valid_condidates[:, 5:] *= valid_condidates[:, 4:5]
# 将边界框从xywh转换为xyxy
valid_condidates[:, :4] = xywh2xyxy(valid_condidates[:, :4])
max_det = 300 # 最大检测目标数
max_wh = 7680 # 最大宽高
max_nms = 30000 # NMS处理的最大边界框数
# 提取最高类别置信度和对应的类别索引
valid_condidates[:, 4] = valid_condidates[:, 5:].max(1)
valid_condidates[:, 5] = valid_condidates[:, 5:].argmax(1)
# 按置信度降序排列
sort_id = np.argsort(valid_condidates[:, 4])[::-1]
valid_condidates = valid_condidates[sort_id[:max_nms]]
# 执行NMS
boxes, scores = valid_condidates[:, :4] + valid_condidates[:, 5:6] * max_wh, valid_condidates[:, 4]
index = NMS(boxes, scores, iou_thres)[:max_det]
out_boxes = valid_condidates[index]
# 将边界框坐标裁剪到原始图像尺寸
clip_coords(out_boxes[:, :4], img0shape)
# 将边界框从xyxy转换为xywh
out_boxes[:, :4] = xyxy2xywh(out_boxes[:, :4])
print(f"检测到{len(out_boxes)}个目标区域")
return out_boxes
# --------------------- 检测结果绘制函数 ---------------------
def draw_detect_res(img, det_pred):
'''
在图像上绘制检测结果
:param img: 输入图像
:param det_pred: 检测结果
:return: 绘制结果后的图像
'''
img = img.astype(np.uint8)
# 为不同类别生成不同颜色
color_step = int(255 / len(coco_class))
for i in range(len(det_pred)):
x1, y1, x2, y2 = [int(t) for t in det_pred[i][:4]]
score = det_pred[i][4]
cls_id = int(det_pred[i][5])
print(f"目标 {i + 1}: 坐标[{x1}, {y1}, {x2}, {y2}], 置信度{score}, 类别{coco_class[cls_id]}")
# 在边界框上方绘制类别标签和置信度
cv2.putText(img, f'{coco_class[cls_id]}:{score:.2f}', (x1, y1 - 6),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
# 绘制边界框
cv2.rectangle(img, (x1, y1), (x1 + x2, y1 + y2),
(0, int(cls_id * color_step), int(255 - cls_id * color_step)),
thickness=2)
return img
# --------------------- YOLOv5检测头实现 ---------------------
class Detect():
# YOLOv5检测头,用于检测模型
def __init__(self, nc=80, anchors=(), stride=[], image_size=640): # 检测层
super().__init__()
self.nc = nc # 类别数量
self.no = nc + 5 # 每个锚框的输出数量(4坐标+1置信度+类别数)
self.stride = stride # 特征图下采样步长
self.nl = len(anchors) # 检测层数量
self.na = len(anchors[0]) // 2 # 每个检测层的锚框数量
self.grid, self.anchor_grid = [0] * self.nl, [0] * self.nl # 网格和锚框网格
# 初始化锚框
self.anchors = np.array(anchors, dtype=np.float32).reshape(self.nl, -1, 2)
base_scale = image_size // 8
# 为每个检测层生成网格和锚框网格
for i in range(self.nl):
self.grid[i], self.anchor_grid[i] = self._make_grid(base_scale // (2 ** i), base_scale // (2 ** i), i)
def _make_grid(self, nx=20, ny=20, i=0):
# 生成网格和锚框网格
y, x = np.arange(ny, dtype=np.float32), np.arange(nx, dtype=np.float32)
yv, xv = np.meshgrid(y, x)
yv, xv = yv.T, xv.T
# 添加网格偏移,即y = 2.0 * x - 0.5
grid = np.stack((xv, yv), 2)
grid = grid[np.newaxis, np.newaxis, ...]
grid = np.repeat(grid, self.na, axis=1) - 0.5
# 生成锚框网格
anchor_grid = self.anchors[i].reshape((1, self.na, 1, 1, 2))
anchor_grid = np.repeat(anchor_grid, repeats=ny, axis=2)
anchor_grid = np.repeat(anchor_grid, repeats=nx, axis=3)
return grid, anchor_grid
def sigmoid(self, arr):
# Sigmoid激活函数
return 1 / (1 + np.exp(-arr))
def __call__(self, x):
z = [] # 推理输出
for i in range(self.nl):
bs, _, ny, nx = x[i].shape
# 调整输出形状
x[i] = x[i].reshape(bs, self.na, self.no, ny, nx).transpose(0, 1, 3, 4, 2)
y = self.sigmoid(x[i])
# 解码边界框坐标
y[..., 0:2] = (y[..., 0:2] * 2. + self.grid[i]) * self.stride[i] # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
z.append(y.reshape(bs, self.na * nx * ny, self.no))
return np.concatenate(z, 1)
# --------------------- 主函数 ---------------------
def main():
args = parser_args() # 解析命令行参数
target_model = args.target_model # 模型路径
model_type = args.model_type # 模型类型
size = int(args.size) # 模型输入尺寸
imgs = args.imgs # 输入图像路径
invoke_nums = int(args.invoke_nums) # 推理次数
print("Start main ... ...")
# 初始化aidlite配置
config = aidlite.Config.create_instance()
if config is None:
print("创建配置失败 !")
return False
# 设置推理配置
config.implement_type = aidlite.ImplementType.TYPE_LOCAL # 本地实现
if model_type.lower() == "qnn":
config.framework_type = aidlite.FrameworkType.TYPE_QNN # QNN框架
elif model_type.lower() == "snpe2" or model_type.lower() == "snpe":
config.framework_type = aidlite.FrameworkType.TYPE_SNPE2 # SNPE2框架
config.accelerate_type = aidlite.AccelerateType.TYPE_DSP # 使用DSP加速
config.is_quantify_model = 1 # 量化模型
# 加载模型
model = aidlite.Model.create_instance(target_model)
if model is None:
print("创建模型失败 !")
return False
# 设置输入输出形状
input_shapes = [[1, size, size, 3]]
output_shapes = [[1, 20, 20, 255], [1, 40, 40, 255], [1, 80, 80, 255]]
model.set_model_properties(input_shapes, aidlite.DataType.TYPE_FLOAT32,
output_shapes, aidlite.DataType.TYPE_FLOAT32)
# 构建解释器
interpreter = aidlite.InterpreterBuilder.build_interpretper_from_model_and_config(model, config)
if interpreter is None:
print("构建解释器失败 !")
return None
# 初始化和加载模型
result = interpreter.init()
if result != 0:
print(f"解释器初始化失败 !")
return False
result = interpreter.load_model()
if result != 0:
print("解释器加载模型失败 !")
return False
print("检测模型加载成功!")
# 图像处理
frame = cv2.imread(imgs) # 读取图像
if frame is None:
print(f"无法读取图像: {imgs}")
return False
# 图片等比缩放处理
img_processed = np.copy(frame)
[height, width, _] = img_processed.shape
length = max((height, width))
scale = length / size
ratio = [scale, scale]
image = np.zeros((length, length, 3), np.uint8)
image[0:height, 0:width] = img_processed
# 图像预处理: BGR转RGB并调整尺寸
img_input = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
img_input = cv2.resize(img_input, (size, size))
# 归一化处理
mean_data = [0, 0, 0]
std_data = [255, 255, 255]
img_input = (img_input - mean_data) / std_data # HWC
img_input = img_input.astype(np.float32)
# 性能测试
invoke_time = []
for i in range(invoke_nums):
# 设置输入张量
result = interpreter.set_input_tensor(0, img_input.data)
if result != 0:
print("设置输入张量失败")
# 记录推理时间
t1 = time.time()
result = interpreter.invoke()
cost_time = (time.time() - t1) * 1000
invoke_time.append(cost_time)
if result != 0:
print("推理调用失败")
# 获取输出张量
out1 = interpreter.get_output_tensor(0)
out2 = interpreter.get_output_tensor(1)
out3 = interpreter.get_output_tensor(2)
output = [out1, out2, out3]
output = sorted(output, key=len) # 按长度排序输出
# 释放资源
result = interpreter.destory()
# 统计推理时间
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_nums}次统计:\n --平均推理时间: {mean_invoke_time:.2f}ms\n --最大推理时间: {max_invoke_time:.2f}ms\n --最小推理时间: {min_invoke_time:.2f}ms\n --时间方差: {var_invoketime:.2f}")
print("=======================================")
## 后处理
stride = [8, 16, 32] # 特征图步长
# 初始化YOLO检测头
yolo_head = Detect(OBJ_CLASS_NUM, anchors, stride, MODEL_SIZE)
# 调整输出形状
validCount0 = output[2].reshape(*output_shapes[2]).transpose(0, 3, 1, 2)
validCount1 = output[1].reshape(*output_shapes[1]).transpose(0, 3, 1, 2)
validCount2 = output[0].reshape(*output_shapes[0]).transpose(0, 3, 1, 2)
# 执行检测头处理
pred = yolo_head([validCount0, validCount1, validCount2])
# 执行后处理
det_pred = detect_postprocess(pred, frame.shape, [MODEL_SIZE, MODEL_SIZE, 3], conf_thres=0.5, iou_thres=0.45)
# 处理可能的NaN值
det_pred[np.isnan(det_pred)] = 0.0
# 还原边界框到原始图像尺寸
det_pred[:, :4] = det_pred[:, :4] * scale
# 绘制检测结果
res_img = draw_detect_res(frame, det_pred)
# 保存结果图像
save_path = "./python/yolov5s_result.jpg"
cv2.imwrite(save_path, res_img)
print(f"检测结果已保存至: {save_path}")
print("=======================================")
return True
# --------------------- 命令行参数解析函数 ---------------------
def parser_args():
parser = argparse.ArgumentParser(description="运行模型基准测试")
parser.add_argument('--target_model', type=str, default='yolov5x/cutoff_yolov5x_qcs8550_w8a8.qnn231.ctx.bin',
help="推理模型路径")
parser.add_argument('--imgs', type=str, default='bus.jpg', help="预测图像路径")
parser.add_argument('--invoke_nums', type=str, default=10, help="推理次数")
parser.add_argument('--model_type', type=str, default='QNN', help="运行后端")
parser.add_argument('--size', type=str, default=640, help="模型输入尺寸")
args = parser.parse_args()
return args
if __name__ == "__main__":
main()
这段代码实现了一个完整的 YOLOv5 目标检测流程,主要包含以下几个部分:
预处理模块:对输入图像进行等比例缩放和填充,确保图像尺寸符合模型输入要求,并进行归一化处理。
推理模块:使用 aidlite 库加载量化后的 YOLOv5 模型,在嵌入式设备上执行高效推理,并记录推理时间用于性能分析。
后处理模块:
结果可视化模块:在原始图像上绘制检测到的目标边界框,并标注类别和置信度,最后保存结果图像。
该代码适合在嵌入式设备 (如手机、边缘计算设备) 上运行,通过 aidlite 库利用高通设备的 DSP 等硬件加速单元,实现实时目标检测功能。
1.bin文件是什么?
二进制文件
.bin文件是二进制文件的缩写,泛指所有的二进制内容文件格式。它可以包含任何类型的二进制数据,通常用于存储软件、固件、操作系统映像等内容。在嵌入式系统中….bin文件常用于将代码和数据加载到非易失性存储器中。
2.aidlite修改代码位置