树莓派4B上ONNX Runtime加速YOLOv8-Pose:从模型转换到性能调优实战
1. 为什么要在树莓派上折腾ONNX Runtime和YOLOv8-Pose?
如果你手头有一块树莓派4B,又对计算机视觉感兴趣,那你很可能想过在上面跑点“酷炫”的AI应用,比如实时的人体姿态估计。但现实往往是骨感的:原生的PyTorch模型在树莓派上跑起来慢得像幻灯片,CPU占用率直接拉满,风扇呼呼转,体验极差。我之前就踩过这个坑,用PyTorch直接加载YOLOv8-Pose的.pt模型文件,处理一张640x640的图片平均要花1秒多,这还谈什么实时性?
后来我把模型转换成了ONNX格式,再用ONNX Runtime来推理,速度直接提升了一倍多,平均耗时降到了500毫秒左右。这个提升非常直观,而且最关键的是,ONNX Runtime的环境依赖非常轻量,安装包只有几十MB,相比动辄几个GB的PyTorch完整环境,对树莓派这种资源紧张的设备简直是福音。
那么,ONNX到底是什么?你可以把它理解为一个AI模型的“通用翻译官”。不同的深度学习框架(PyTorch, TensorFlow, MXNet等)训练出来的模型,就像说着不同方言的人。ONNX定义了一套标准的“普通话”(中间表示格式),让这些模型都能被统一理解。ONNX Runtime就是这个“翻译官”的高效运行时,它负责在各种硬件平台(x86, ARM, GPU等)上执行这个标准化后的模型。这样一来,你就不需要把庞大的PyTorch或TensorFlow框架搬到资源有限的边缘设备上了,大大简化了部署流程。
对于树莓派4B这样的ARM架构设备来说,使用ONNX Runtime有三大核心优势:
- 部署轻量化:无需安装完整的训练框架,节省大量存储空间和内存。
- 性能优化:ONNX Runtime针对不同硬件(包括CPU)有专门的优化,执行效率更高。
- 跨平台通用:一次转换,到处运行。无论是在Windows服务器、树莓派还是手机上,只要支持ONNX Runtime,你的模型就能跑起来。
所以,如果你也想在树莓派上实现流畅的人体姿态估计,跟着我走完从模型转换到性能调优的完整流程,你就能获得一个即插即用、速度翻倍的解决方案。整个过程不需要深厚的嵌入式开发经验,只要会点Python,就能搞定。
2. 环境准备与模型转换:第一步就走对
万事开头难,但准备工作做得好,后面就能少踩很多坑。在树莓派上玩转ONNX,第一步不是急着写代码,而是把环境和模型都准备好。
2.1 树莓派系统与基础环境搭建
我强烈建议使用64位的树莓派操作系统(Raspberry Pi OS 64-bit)。虽然32位系统也能用,但64位系统能更好地利用ARMv8架构的特性,并且很多最新的优化库对64位支持更好。你可以从树莓派官网下载镜像,用Raspberry Pi Imager工具烧录到SD卡。
系统装好后,第一件事是更新软件源并安装必要的编译工具和Python环境:
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv build-essential cmake
接下来,为这个项目创建一个独立的Python虚拟环境,避免污染系统环境:
python3 -m venv ~/onnx_env
source ~/onnx_env/bin/activate
激活虚拟环境后,你会看到命令行提示符前面多了(onnx_env),这表示你已经在这个独立的环境里了。
2.2 安装核心依赖:Ultralytics YOLO与ONNX Runtime
模型转换和推理需要两个核心库:ultralytics用于导出YOLOv8模型,onnxruntime用于在树莓派上运行模型。
# 安装YOLOv8的Python包,它包含了模型导出功能
pip install ultralytics
# 安装ONNX Runtime的ARM64版本
# 注意:一定要安装`onnxruntime`而不是`onnxruntime-gpu`,树莓派4B没有NVIDIA GPU。
pip install onnxruntime
这里有个小细节:ultralytics包可能会自动安装一些像torch(PyTorch)这样的依赖。在树莓派上直接装完整的PyTorch非常耗时且占用空间,但好在我们只是用它来导出模型,并不在树莓派上用它做训练或重型推理,所以可以接受。安装完成后,你可以用pip list看看,onnxruntime的包大小通常只有几十MB,而torch可能超过1GB,这直观地体现了ONNX Runtime的轻量优势。
2.3 从PyTorch到ONNX:关键参数详解
现在,我们进入核心环节——模型转换。假设你已经从Ultralytics官网或通过代码下载了YOLOv8-Pose的预训练模型(例如yolov8n-pose.pt,这是最小的纳米模型,最适合树莓派)。转换代码其实非常简单:
from ultralytics import YOLO
# 加载预训练的PyTorch模型
model = YOLO('yolov8n-pose.pt')
# 导出为ONNX格式
model.export(format='onnx', imgsz=(640, 640), opset=12)
运行这段代码,你会在当前目录得到一个yolov8n-pose.onnx文件。别看代码简单,这里的几个参数至关重要,直接影响了后续推理的速度和精度:
format='onnx': 指定导出格式,这不用说。imgsz=(640, 640): 定义模型的固定输入尺寸。YOLOv8支持动态尺寸,但固定尺寸能让ONNX Runtime进行更激进的内核优化,推理速度更快。我实测下来,在树莓派上,固定尺寸比动态尺寸能快10%-20%。你就记住,在边缘设备上,能用固定尺寸就用固定尺寸。opset=12: 这是ONNX的算子集版本。算子(Operator)是构成模型的基本计算单元,比如卷积(Conv)、激活(Relu)。ONNX标准在不断演进,新版本会支持更多、更高效的算子。opset=12是一个广泛兼容且稳定的版本,对YOLOv8的支持很好。不建议盲目追求最新版本,可能导致某些算子不被老版本的ONNX Runtime支持。
一个你可能遇到的“坑”:直接导出的ONNX模型,在树莓派上推理时输出可能全是0或者乱码。这很可能是因为模型里包含了某些只在特定框架下支持的复杂操作(比如一些后处理逻辑)。Ultralytics的export方法默认会尝试优化并导出包含后处理(如非极大值抑制NMS)的模型,但这有时会出问题。
更稳妥的做法是导出只包含主干网络(不含后处理)的ONNX模型:
model.export(format='onnx', imgsz=(640, 640), opset=12, simplify=True)
添加simplify=True参数会调用ONNX Simplifier工具对模型进行简化,移除冗余操作,有时能解决兼容性问题。如果还有问题,可以尝试在导出时指定dynamic=False来禁用动态轴,强制使用静态图,兼容性更好。
导出成功后,我强烈推荐你用Netron这个工具打开生成的.onnx文件看一眼。Netron是一个神经网络模型可视化工具,有网页版和桌面版。它能让你直观地看到模型的整个计算图,输入输出节点叫什么名字,中间经过了哪些层。这对于后续写推理代码时理解模型的输入输出格式非常有帮助。
3. 在树莓派上编写ONNX推理代码
模型转换好了,接下来就是重头戏:在树莓派上写代码加载ONNX模型并进行推理。我会把完整的代码拆解开,一步步讲清楚每个部分的作用。
3.1 核心推理流程与代码结构
我们先来看一个最精简的推理脚本框架,了解整个流程:
import cv2
import numpy as np
import onnxruntime as ort
# 1. 加载ONNX模型,创建推理会话
session = ort.InferenceSession('yolov8n-pose.onnx')
input_name = session.get_inputs()[0].name # 获取输入节点名,通常是'images'
# 2. 读取并预处理图像
image = cv2.imread('test.jpg')
# 将图像缩放、填充到模型需要的640x640,并归一化到[0,1]
input_tensor = preprocess(image)
# 3. 运行模型推理
outputs = session.run(None, {input_name: input_tensor})
# 4. 解析模型的原始输出
# YOLOv8-Pose的输出是一个形状为(1, 56, 8400)的张量,需要解析成边界框和关键点
boxes, keypoints = parse_yolo_output(outputs[0], original_image_shape)
# 5. 将结果绘制到图像上并显示
result_image = draw_results(image, boxes, keypoints)
cv2.imshow('Result', result_image)
cv2.waitKey(0)
这个框架包含了五个核心步骤。其中,预处理(第2步)和输出解析(第4步) 是最容易出错也最影响性能的两个地方,我们重点讲。
3.2 图像预处理:速度与精度的平衡
预处理的目标是把任意尺寸的图片,变成模型需要的(1, 3, 640, 640)形状的、归一化的float32张量。这里不仅要做得对,还要做得快。下面是我优化过的预处理函数:
def preprocess_image(img, target_size=(640, 640)):
"""
将OpenCV读取的BGR图像预处理为ONNX模型输入。
采用“保持长宽比的缩放+灰边填充”策略,避免图像变形。
"""
h, w = img.shape[:2]
target_w, target_h = target_size
# 计算缩放比例,保持原图长宽比
scale = min(target_w / w, target_h / h)
new_w = int(w * scale)
new_h = int(h * scale)
# 使用OpenCV的INTER_LINEAR插值进行缩放,质量和速度平衡
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
# 创建目标尺寸的灰色画布(RGB值[114, 114, 114]是YOLO训练时常用的填充色)
padded = np.full((target_h, target_w, 3), 114, dtype=np.uint8)
# 将缩放后的图像居中粘贴到画布上
pad_x = (target_w - new_w) // 2
pad_y = (target_h - new_h) // 2
padded[pad_y:pad_y+new_h, pad_x:pad_x+new_w] = resized
# 图像归一化:像素值从0-255缩放到0.0-1.0
# 注意:OpenCV是BGR顺序,但YOLOv8训练时用的是RGB。是否需要转换取决于你的模型。
# 实测发现,使用Ultralytics官方代码导出的ONNX模型,输入应为RGB。
# 所以这里进行BGR到RGB的转换
padded = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
input_img = padded.astype(np.float32) / 255.0
# 调整维度顺序:HWC -> CHW -> NCHW
# OpenCV图像格式是(高度,宽度,通道),即HWC
# PyTorch/ONNX模型通常期望(批次,通道,高度,宽度),即NCHW
input_img = input_img.transpose(2, 0, 1) # HWC to CHW
input_img = np.expand_dims(input_img, axis=0) # CHW to NCHW (批次为1)
# 返回处理后的张量,以及用于后续还原坐标的变换参数
return input_img, (h, w, scale, pad_x, pad_y)
关键点解析:
- 保持长宽比:直接拉伸图像会导致人体变形,影响关键点检测精度。先按比例缩放,再用灰边填充,是目标检测中的标准做法。
- 颜色空间转换:
cv2.imread读进来的是BGR格式,而YOLOv8训练时通常使用RGB。这个细节如果搞错,模型性能会严重下降。务必确认你的模型期望的通道顺序。 - 维度变换:从OpenCV的
(H, W, C)到ONNX模型期待的(N, C, H, W),transpose和expand_dims这两个操作必不可少。 - 返回变换参数:
scale, pad_x, pad_y这三个值必须返回。因为模型预测的坐标是在填充后的640x640画布上的,我们需要用这些参数把坐标映射回原始图像尺寸。
3.3 解析模型输出:从8400个预测中找出目标
YOLOv8-Pose的ONNX模型输出是一个形状为(1, 56, 8400)的张量。这个8400是怎么来的?它是模型在640x640输入上,在不同尺度的特征图上生成的锚框(anchor)总数。56是每个预测框的属性数量:4个边界框坐标(中心x, y, 宽, 高)+ 1个物体置信度 + 17个关键点 * 3个值(x, y, 置信度)= 56。
我们的任务就是从这8400个嘈杂的预测中,筛选出真正的人体,并提取出正确的边界框和17个关键点。这个过程包括置信度过滤、坐标还原和非极大值抑制(NMS)。
def parse_yolo_output(output, orig_shape, scale, pad_x, pad_y):
"""
解析YOLOv8-Pose模型的输出。
output: ONNX模型输出的原始张量,形状(1, 56, 8400)
orig_shape: 原始图像的(高度, 宽度)
scale, pad_x, pad_y: 预处理时记录的参数
"""
# 1. 调整输出维度: (1, 56, 8400) -> (8400, 56)
output = output[0].transpose(1, 0)
orig_h, orig_w = orig_shape
# 2. 拆分数据
boxes_xywh = output[:, :4] # 前4列:边界框 (中心x, 中心y, 宽, 高)
confidences = output[:, 4] # 第5列:物体置信度
keypoints_data = output[:, 5:] # 后51列:关键点 (17*3)
# 3. 初步过滤:只保留置信度高于阈值的预测
conf_threshold = 0.5
mask = confidences > conf_threshold
boxes_xywh = boxes_xywh[mask]
confidences = confidences[mask]
keypoints_data = keypoints_data[mask]
if len(boxes_xywh) == 0:
return [], [] # 没有检测到任何目标
# 4. 将边界框格式从 (中心x, 中心y, 宽, 高) 转换为 (左上x, 左上y, 右下x, 右下y)
boxes_x1y1x2y2 = np.zeros_like(boxes_xywh)
boxes_x1y1x2y2[:, 0] = boxes_xywh[:, 0] - boxes_xywh[:, 2] / 2 # x1
boxes_x1y1x2y2[:, 1] = boxes_xywh[:, 1] - boxes_xywh[:, 3] / 2 # y1
boxes_x1y1x2y2[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2] / 2 # x2
boxes_x1y1x2y2[:, 3] = boxes_xywh[:, 1] + boxes_xywh[:, 3] / 2 # y2
# 5. 关键点数据重塑: (N, 51) -> (N, 17, 3)
keypoints = keypoints_data.reshape(-1, 17, 3)
# 6. 坐标映射:将坐标从预处理后的画布空间,还原到原始图像空间
# 减去填充的偏移量
boxes_x1y1x2y2 -= [pad_x, pad_y, pad_x, pad_y]
keypoints[:, :, :2] -= [pad_x, pad_y] # 只处理x, y坐标
# 除以缩放比例
boxes_x1y1x2y2 /= scale
keypoints[:, :, :2] /= scale
# 确保坐标不超出原始图像边界
boxes_x1y1x2y2 = np.clip(boxes_x1y1x2y2, 0, [orig_w, orig_h, orig_w, orig_h])
keypoints[:, :, :2] = np.clip(keypoints[:, :, :2], 0, [orig_w, orig_h])
# 7. 非极大值抑制 (NMS):去除重叠的冗余框
iou_threshold = 0.45
keep_indices = non_max_suppression(boxes_x1y1x2y2, confidences, iou_threshold)
final_boxes = boxes_x1y1x2y2[keep_indices].astype(int)
final_keypoints = keypoints[keep_indices]
return final_boxes, final_keypoints
非极大值抑制(NMS) 是一个经典算法,目的是在多个重叠的检测框中选出最好的一个。这里给出一个简单的实现:
def non_max_suppression(boxes, scores, iou_threshold):
"""
简单的非极大值抑制实现。
boxes: [N, 4],格式为 (x1, y1, x2, y2)
scores: [N]
iou_threshold: 交并比阈值,高于此值的框将被抑制
"""
if len(boxes) == 0:
return []
# 按置信度从高到低排序
order = scores.argsort()[::-1]
keep = []
while len(order) > 0:
i = order[0] # 当前置信度最高的框
keep.append(i)
if len(order) == 1:
break
# 计算当前框与剩余所有框的IoU
ious = compute_iou(boxes[i], boxes[order[1:]])
# 保留IoU低于阈值的框(即不重叠或重叠度低的框)
order = order[1:][ious < iou_threshold]
return keep
def compute_iou(box, boxes):
"""计算一个框与一组框的交并比(IoU)"""
# box: [x1, y1, x2, y2]
# boxes: [M, 4]
x1 = np.maximum(box[0], boxes[:, 0])
y1 = np.maximum(box[1], boxes[:, 1])
x2 = np.minimum(box[2], boxes[:, 2])
y2 = np.minimum(box[3], boxes[:, 3])
inter_area = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
box_area = (box[2] - box[0]) * (box[3] - box[1])
boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
union_area = box_area + boxes_area - inter_area
return inter_area / (union_area + 1e-6) # 加一个小数避免除零
3.4 结果可视化:画出骨骼关键点
解析出边界框和关键点后,最后一步就是把它们漂亮地画在图上。这不仅能验证结果,也更有成就感。
def draw_keypoints(image, boxes, keypoints):
"""
在图像上绘制检测到的人体边界框和姿态关键点骨架。
"""
# COCO关键点连接关系(17个关键点)
skeleton = [
(0, 1), (0, 2), (1, 3), (2, 4), # 头部:鼻子-左眼,鼻子-右眼,左眼-左耳,右眼-右耳
(5, 6), # 肩膀:左肩-右肩
(5, 7), (7, 9), # 左臂:左肩-左肘,左肘-左手腕
(6, 8), (8, 10), # 右臂:右肩-右肘,右肘-右手腕
(11, 12), # 臀部:左髋-右髋
(11, 13), (13, 15), # 左腿:左髋-左膝,左膝-左脚踝
(12, 14), (14, 16) # 右腿:右髋-右膝,右膝-右脚踝
]
for i, (box, kpt) in enumerate(zip(boxes, keypoints)):
x1, y1, x2, y2 = box
# 画边界框
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 画关键点(圆圈)
for j in range(17):
x, y, conf = kpt[j]
if conf > 0.3: # 关键点置信度阈值
cv2.circle(image, (int(x), int(y)), 4, (0, 0, 255), -1) # 红色实心圆
# 画骨架连线
for (start_idx, end_idx) in skeleton:
x1_kpt, y1_kpt, conf1 = kpt[start_idx]
x2_kpt, y2_kpt, conf2 = kpt[end_idx]
if conf1 > 0.3 and conf2 > 0.3: # 两端关键点都可信才画线
cv2.line(image, (int(x1_kpt), int(y1_kpt)), (int(x2_kpt), int(y2_kpt)), (255, 0, 0), 2)
return image
把上面所有的函数组合起来,加上图像读取和显示的逻辑,你就得到了一个完整的、可以在树莓派上运行的YOLOv8-Pose ONNX推理脚本。运行它,你应该能看到一个带有人体骨骼框的实时检测窗口。
4. 性能调优实战:让树莓派跑得更快
代码能跑通只是第一步,让它在树莓派上跑得“流畅”才是我们的终极目标。树莓派4B的CPU是四核Cortex-A72,内存通常1GB到8GB不等,资源非常有限。下面是我实测有效的几种性能优化方法,有些能让速度提升超过50%。
4.1 ONNX Runtime会话配置优化
创建InferenceSession时,通过SessionOptions可以传入很多优化参数。这是提升性能最直接有效的方法之一。
import onnxruntime as ort
# 创建会话选项
options = ort.SessionOptions()
# 1. 启用线程池并设置线程数
# 树莓派是四核,设置4个线程可以充分利用CPU。
# 注意:并不是线程越多越好,太多线程会导致上下文切换开销。
options.intra_op_num_threads = 4
options.inter_op_num_threads = 2 # 如果模型有并行分支可以设置,否则保持1
# 2. 启用CPU性能模式(如果ONNX Runtime版本支持)
# 这会让CPU运行在更高频率,但可能增加功耗和发热。
try:
options.add_session_config_entry('session.intra_op.allow_spinning', '1')
except:
pass # 旧版本可能不支持此配置
# 3. 设置执行模式为“顺序执行”
# 对于YOLO这种主要是顺序结构的模型,顺序执行效率更高。
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
# 4. 启用图优化
# ONNX Runtime会在加载模型时进行一系列图结构优化,如算子融合、常量折叠等。
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
# 使用优化后的选项创建会话
session = ort.InferenceSession('yolov8n-pose.onnx', sess_options=options)
重点解释一下图优化:ONNX模型本质上是一个计算图。ORT_ENABLE_ALL会让ONNX Runtime在模型加载时,自动对计算图进行优化。比如,把连续的Conv(卷积)和BatchNorm(批归一化)层合并成一个操作,或者把某些激活函数融合到前一层中。这些优化能减少计算量和内存访问次数,对速度提升非常明显,而且完全免费,不需要你修改任何模型结构。
4.2 模型量化:用精度换速度的大杀器
如果你的应用对精度要求不是极端苛刻(比如,关键点坐标差几个像素可以接受),那么模型量化是提升边缘设备推理速度最有效的手段,没有之一。量化的核心思想是将模型权重和激活值从32位浮点数(FP32)转换为低精度格式,如16位浮点数(FP16)甚至8位整数(INT8)。
FP16量化:这是最简单安全的量化方式,速度提升明显(通常30%-50%),精度损失很小。你甚至可以在模型导出时就完成FP16转换:
from ultralytics import YOLO
model = YOLO('yolov8n-pose.pt')
# 导出时指定 half=True,模型内部权重就会转换为FP16
model.export(format='onnx', imgsz=(640, 640), half=True)
在树莓派上加载这个FP16模型,ONNX Runtime会自动利用ARMv8架构的半精度计算指令,速度更快。但要注意,树莓派的CPU不一定对所有FP16操作有硬件加速,部分计算可能仍需回退到FP32,但整体仍有收益。
INT8量化:这是更激进的量化,能将模型大小减少至原来的1/4,推理速度进一步提升。但INT8量化需要一个校准数据集来统计激活值的动态范围,过程更复杂,且精度损失风险更大。对于YOLOv8-Pose这种密集预测任务,我实测在树莓派上直接使用动态量化(QuantType.QInt8)有时会导致关键点坐标偏差较大。如果你是新手,建议先从FP16开始尝试。
4.3 输入输出与内存优化
使用静态输入尺寸:我们在导出模型时已经指定了imgsz=(640, 640),这就是静态尺寸。确保你的推理代码也始终喂给模型这个尺寸的输入。动态尺寸会阻止ONNX Runtime进行许多编译时优化。
避免不必要的内存拷贝:在预处理函数中,我们使用了np.full创建画布,然后进行数组切片赋值。这会产生中间内存分配。对于追求极致的场景,可以预分配一个全局的numpy数组作为画布缓冲区,每次复用,减少内存分配开销。
关闭调试信息:在最终部署时,确保关闭所有不必要的print语句和OpenCV的等待窗口(cv2.waitKey)。I/O操作在循环中会成为性能瓶颈。
4.4 多线程与流水线处理
如果你的应用是处理视频流,可以考虑使用生产者-消费者模型。用一个线程专门负责从摄像头抓取帧(生产者),另一个线程专门负责模型推理(消费者),中间用一个固定大小的队列连接。这样,I/O等待和计算等待可以重叠,提高整体吞吐量。
from queue import Queue
import threading
frame_queue = Queue(maxsize=2) # 小队列避免积压
result_queue = Queue(maxsize=2)
def capture_thread(cap):
while True:
ret, frame = cap.read()
if not ret:
break
if not frame_queue.full():
# 简单的预处理(如缩放)也可以放在这个线程
frame_queue.put(frame)
def inference_thread(session):
while True:
frame = frame_queue.get()
# 进行完整的预处理和推理
results = run_inference(session, frame)
result_queue.put(results)
# 启动线程
cap = cv2.VideoCapture(0)
threading.Thread(target=capture_thread, args=(cap,), daemon=True).start()
threading.Thread(target=inference_thread, args=(session,), daemon=True).start()
# 主线程负责从result_queue取结果并显示
while True:
if not result_queue.empty():
annotated_frame = result_queue.get()
cv2.imshow('Pose', annotated_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
这种架构在树莓派上能有效缓解因单线程顺序处理导致的卡顿,让视频看起来更流畅。
5. 常见问题排查与进阶技巧
即使按照上面的步骤操作,你可能还是会遇到一些奇怪的问题。这里我总结几个最常见的坑和解决办法。
5.1 模型输出全是0或NaN
这是最让人头疼的问题之一。现象是推理能跑通,但输出的边界框坐标和关键点全是0或者NaN(非数字)。
- 可能原因1:预处理错误。这是最大的嫌疑。请逐行检查预处理代码:
- 确认图像归一化是否正确(除以255.0)。
- 确认颜色通道顺序(BGR vs RGB)是否与模型训练时一致。一个快速的验证方法是:用PyTorch原模型和ONNX模型对同一张预处理后的
numpy数组进行推理,对比输出是否接近。如果PyTorch输出正常,ONNX输出异常,那问题很可能出在导出环节。
- 可能原因2:ONNX导出问题。尝试在导出时添加
simplify=True和dynamic=False参数。也可以尝试不同的opset版本(如11, 12, 13)。 - 可能原因3:ONNX Runtime版本不兼容。确保你安装的ONNX Runtime版本与导出的
opset兼容。可以尝试升级或降级onnxruntime包。
5.2 推理速度不达标
你按照教程做了,但速度还是不如预期。
- 检查CPU频率:树莓派默认可能运行在节能模式。你可以通过命令
vcgencmd measure_clock arm查看当前ARM核心频率,或者安装cpufrequtils来设置性能模式:sudo cpufreq-set -g performance。这会显著提升性能,但会增加功耗和发热,记得做好散热。 - 使用性能分析工具:用Python的
cProfile模块分析代码,找到耗时最长的函数。
你会发现,大部分时间可能花在图像预处理(python -m cProfile -s time your_inference_script.pycv2.resize,transpose)和后处理(NMS循环)上,而不是模型推理本身。针对这些部分进行优化(如使用更快的插值算法cv2.INTER_NEAREST,或用numpy向量化操作替代循环)。 - 尝试更小的模型:
yolov8n-pose已经是最小的了,但如果还嫌慢,可以考虑使用剪枝或知识蒸馏得到的更小模型,或者尝试专门为边缘设备设计的姿态估计模型,如MoveNet(有TensorFlow Lite版本)。
5.3 内存占用过高导致崩溃
树莓派内存小,跑大模型容易崩溃。
- 监控内存:使用
free -h命令实时查看内存使用情况。 - 减少批次大小:确保你的推理输入批次(batch size)是1。在边缘设备上跑批量推理不现实。
- 及时释放内存:在循环中,对于大的中间变量(如原始高分辨率图像),用完及时
del掉,或者重用变量。 - 使用
'cpu'执行提供器:在创建InferenceSession时,可以显式指定:
这能确保ONNX Runtime不会尝试加载任何可能占用额外内存的GPU相关库。providers = ['CPUExecutionProvider'] session = ort.InferenceSession('model.onnx', providers=providers)
5.4 进阶:尝试ONNX Runtime的ARMNN后端
ONNX Runtime支持不同的执行提供器。默认的CPU提供器已经不错,但对于ARM架构,还有一个专门的ARMNN提供器(ARM Compute Library),它针对ARM CPU和GPU做了深度优化。启用它可能会获得额外的性能提升。
安装ARMNN支持可能需要从源码编译ONNX Runtime,过程比较复杂。但对于追求极致性能的开发者来说,这是一条值得探索的路。你可以查阅ONNX Runtime官方GitHub仓库中关于ARMNN的编译指南。
从我的经验来看,在树莓派4B上,经过上述优化后,使用YOLOv8n-pose模型处理640x640的图像,推理时间稳定在400-600毫秒是完全可行的。这意味着每秒可以处理1-2帧,对于很多非严格实时的监控、分析应用已经足够。如果你能接受更低的分辨率(如320x320),速度还可以更快。整个过程中,模型转换和ONNX Runtime的轻量化部署优势体现得淋漓尽致,它让在资源受限的设备上运行先进的AI模型从“不可能”变成了“触手可及”。
更多推荐
所有评论(0)