ONNX Runtime Python端侧模型部署YOLOv5
- Ai
- 3天前
- 30热度
- 0评论
ONNX Runtime介绍
ONNX Runtime不依赖于Pytorch、tensorflow等机器学习训练模型框架。他提供了一种简单的方法,可以在CPU、GPU、NPU上运行模型。通常ONNX Runtime用于端侧设备模型的运行推理。要使用ONNX Runtime运行模型,一般的步骤如下:
- 用你最喜欢的框架(如pytorch、tensorflow、paddle等)训练一个模型。
- 将模型转换或导出为ONNX格式。
- 在端侧使用ONNX Runtime加载并运行模型。
模型的训练和导出为ONNX格式这里就不再阐述了。下面基于python在端侧运行模型的示例:
import numpy
# 导入numpy模块
import onnxruntime as rt
# 导入onnxruntime模块
sess = rt.InferenceSession(
"logreg_iris.onnx", providers=rt.get_available_providers())
# 加载模型logreg_iris.onnx
input_name = sess.get_inputs()[0].name
# 获取模型的输入名称,对应的是使用https://netron.app/中intput name。
pred_onx = sess.run(None, {input_name: X_test.astype(numpy.float32)})[0]
# 运行模型推理,返回结果到pred_onx中
print(pred_onx)
上面给出的python示例中,端侧运行模型可以总结为2个步骤。加载模型,模型推理。
加载模型
class onnxruntime.InferenceSession(
path_or_bytes: str | bytes | os.PathLike,
sess_options: onnxruntime.SessionOptions | None = None,
providers: Sequence[str | tuple[str, dict[Any, Any]]] | None = None,
provider_options: Sequence[dict[Any, Any]] | None = None, **kwargs)
- path_or_bytes: 模型文件名或者ONNX、ORT格式二进制。
- sess_options: 会话选项,比如配置线程数、优先级。
- providers: 指定执行提供者优先级(['CUDAExecutionProvider','CPUExecutionProvider'])
- provider_options: 字典序列,为每个提供者配置专属参数(如CUDA设备ID)
options = onnxruntime.SessionOptions()
options.SetIntraOpNumThreads(4)
# 多设备优先级配置
session = InferenceSession(
"model.onnx",
sess_options=options,
providers=[
('CUDAExecutionProvider', {'device_id': 0}),
'CPUExecutionProvider'
]
)
模型推理
outputs = senssion.run(output_names, input_feed, run_options=None)
- output_names:输出节点名称,字符串列表,指定需要获取的输出节点名称,若为None则返回所有输出
- input_feed:输入数据,字典类型,结构为{"输入节点名": numpy数组/ORTValue},建议使用ORTValue封装输入数据以减少CPU-GPU拷贝开销。
- run_options:运行参数,如日志级别。
import numpy as np
import onnxruntime as ort
# 创建示例数据
cpu_data = np.random.rand(1, 3, 224, 224).astype(np.float32)
# 转换为GPU上的ORTValue
gpu_ort_value = ort.OrtValue.ortvalue_from_numpy(
cpu_data,
device_type='cuda', # 关键参数:指定GPU设备
device_id=0 # GPU设备ID(多卡时指定)
)
print(gpu_ort_value.device_name()) # 输出: 'Cuda'
results = session.run(
["output_name"],
{"input_name": gpu_ort_value} # 避免CPU->GPU拷贝
)
在运行模型是,需要获取模型的输入和输出名称,可以通过调用对应的函数session.get_inputs(),session.get_outputs()来获取。inputs和outputs函数返回的是onnxruntime.NodeArg类,该类是ONNX Runtime中表示计算图节点输入/输出参数的核心类,该类有3个成员变量,如下:
- property name: 参数唯一标识符,对应计算图中的节点名称。
- property shape: 张量形状。
- property type:数据类型(如tensor(float32)/tensor(int64))
以下是获取输入名称和输出名称的示例。
input_name = session.get_inputs()[0].name
output_names = [output.name for output in session.get_outputs()]
详细请参考:https://onnxruntime.ai/docs/api/python/api_summary.html
YOLOv5运行示例
加载模型
session_options = ort.SessionOptions()
session_options.intra_op_num_threads = 1
# 加载 ONNX 模型
session = ort.InferenceSession(
"yolov5_n.q.onnx",
sess_options=session_options,
providers=["XXXExecutionProvider"])
创建SessionOptions对象用于定制化会话行为,限制算子内部并行线程数为1,加载名为yolov5_n.q.onnx的量化版YOLOv5模型,指定自定义执行提供者XXXExecutionProvider。
图像预处理
image = cv2.imread(args.image)
#image shape (375, 500, 3)
image_shape = image.shape[:2]
#image_shape的值(375, 500),取前面2个值为图像的宽高
# 获取图像的尺寸大小高和宽。
input_tensor = preprocess(image)
# 图像预处理函数
def preprocess(image, input_size=(640, 640)):
# 调整图像大小为640*640,
image = cv2.resize(image, input_size)
# 转换颜色空间RGB
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 归一化处理
#astype(np.float32)将图像像素值从整数类型(如uint8)转换为32位浮点数,
#避免后续除法运算的精度损失,/ 255.0将像素值从[0,255]的原始范围线性映射到[0,1]区间,
#符合神经网络输入的典型数值范围要求
image = image.astype(np.float32) / 255.0
# 转置模型为CHW格式,原输入是HWC格式。
# 输入的数据是(640,640,3),需要调整为NCHW模型格式 [batch, channel, height, width]
# 使用np.transpose进行转置,变换成(3,640,640)
image = np.transpose(image, (2, 0, 1))
image = np.expand_dims(image, axis=0)
# 接着再加上一个轴变化成(1,3,640,640)tensor。
return image
如何理解深度学习中的轴了? 在深度学习中,轴可以理解为维度。如上图是一个NCHW排布的格式,把N当成第一个维度即位轴0,C第二维度即为轴1,H第三维度即为轴2,W为第四维度即为轴3。np.expand_dims(image, axis=0)即拓展了轴0,原来只有3个维度现在变成4个维度了,N为1。还可以按照指定的轴进行求和,即做压缩。执行np.sum(data, axis=0)时,也就是沿着N的维度就行压缩求和,就变成如上图。由原来的(N,C,W,H)变成了(C',W',H'),即N个CWH中的各自相加。如果是np.sum(data,axis=1),那就是按照C维度方向进行相加,结果就是(N,W,H),即如RGB格式就是每个图像RGB 3通道的像素相加,如下图所示。
模型推理
模型推理前,需要获取计算图输入和输入的名称
input_name = session.get_inputs()[0].name
output_names = [output.name for output in session.get_outputs()]
print('input name', input_name)
print('output name', output_names)
输出结果与下图对应。
input name input
output name ['dets', 'labels']
获取到intput_name和ouput_names后,即可调用运行推理。
outputs = session.run(output_names, {input_name: input_tensor})
模型后处理
# 把batch这个维度去掉
dets = outputs[0].squeeze()
labels_pred = outputs[1].squeeze()
#将坐标进行缩放以适应实际图片的大小。
input_size = (640, 640)
scale_x = image_shape[1] / input_size[0]
scale_y = image_shape[0] / input_size[1]
dets[:, 0] *= scale_x
dets[:, 1] *= scale_y
dets[:, 2] *= scale_x
dets[:, 3] *= scale_y
模型outputs有两个输出,一个是dets,这是一个二位数组dets[n][5],其中det[5]包含了坐标x1, y1, x2, y2,score,前面4个预选框的坐标,后面一个为预选框的分数。
def visualize_results(image, dets, labels_pred, labels, conf_threshold):
for i in range(len(dets)):
det = dets[i]
score = det[4] #每个框的分数
if score > conf_threshold: #小于分数的剔除
class_id = int(labels_pred[i])
x1, y1, x2, y2 = map(int, det[:4])
label = labels[class_id]
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(image, f'{label}: {score:.2f}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
return image
根据阈值分数进行画框,最终完成结果的后处理,注意上面并没有进行极大值抑制。