pytorch转为onnx并调用时遇到的问题汇总
目前用yolov5已经生成了可用的pytorch的模型文件,后缀名为pt,加载和预测用的代码较多,打算试一下转换为onnx的类型用opencv加载,减少代码量也减少第三方包的安装。
发现没有想象中简便,有时间的时候逐步尝试并记录一下遇到的问题。
首先在用yolov5本身的export.py进行转换,然后用opencv加载onnx
import cv2
net = cv2.dnn.readNetFromONNX("./yolov5.onnx")
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
加载时遇到类似getLayerInstance或findCommonShape的错误
# [ERROR:0@26.902] global /Users/xperience/actions-runner/_work/opencv-python/opencv-python/opencv/modules/dnn/src/onnx/onnx_importer.cpp (1021) handleNode DNN/ONNX: ERROR during processing node with 3 inputs and 1 outputs: [Range]:(onnx_node!Range_296) from domain='ai.onnx'
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# cv2.error: OpenCV(4.6.0) /Users/xperience/actions-runner/_work/opencv-python/opencv-python/opencv/modules/dnn/src/onnx/onnx_importer.cpp:1040: error: (-2:Unspecified error) in function 'handleNode'
# > Node [Range@ai.onnx]:(onnx_node!Range_296) parse error: OpenCV(4.6.0) /Users/xperience/actions-runner/_work/opencv-python/opencv-python/opencv/modules/dnn/src/layer_internals.hpp:110: error: (-2:Unspecified error) Can't create layer "onnx_node!Range_296" of type "Range" in function 'getLayerInstance'
# [ERROR:0@0.089] global onnx_importer.cpp:1034 handleNode DNN/ONNX: ERROR during processing node with 2 inputs and 1 outputs: [Mul]:(onnx_node!Mul_401) from domain='ai.onnx'
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# cv2.error: OpenCV(4.9.0) /Users/xperience/GHA-OpenCV-Python2/_work/opencv-python/opencv-python/opencv/modules/dnn/src/onnx/onnx_importer.cpp:1053: error: (-2:Unspecified error) in function 'handleNode'
# > Node [Mul@ai.onnx]:(onnx_node!Mul_401) parse error: OpenCV(4.9.0) /Users/xperience/GHA-OpenCV-Python2/_work/opencv-python/opencv-python/opencv/modules/dnn/src/layers/nary_eltwise_layers.cpp:144: error: (-215:Assertion failed) shape[i] == 1 || outShape[i] == 1 in function 'findCommonShape'
此时重新指定export的参数,比如:
python export.py --weights ./yolov5.pt --img 640 --simplify --optimize --opset 12 --include onnx
然后确认一下opencv-python的版本,我试过4.6.0.66无法加载,但换做4.9.0.80则可以。
不过加载时成功了,在推理时却报了其他错误
[ERROR:0@0.168] global net_impl.cpp:1169 getLayerShapesRecursively OPENCV/DNN: [Slice]:(onnx_node_output_0!1276): getMemoryShapes() throws exception. inputs=1 outputs=1/1 blobs=0
[ERROR:0@0.168] global net_impl.cpp:1172 getLayerShapesRecursively input[0] = [ 1834 3456 ]
[ERROR:0@0.168] global net_impl.cpp:1176 getLayerShapesRecursively output[0] = [ 1834 3456 ]
[ERROR:0@0.168] global net_impl.cpp:1182 getLayerShapesRecursively Exception message: OpenCV(4.9.0) /Users/xperience/GHA-OpenCV-Python2/_work/opencv-python/opencv-python/opencv/modules/dnn/src/layers/slice_layer.cpp:259: error: (-215:Assertion failed) sliceRanges_rw[i].size() <= inpShape.size() in function 'getMemoryShapes'
Traceback (most recent call last):
File "ultralytics_yolov5/load_onnx.py", line 24, in <module>
preds = net.forward()
cv2.error: OpenCV(4.9.0) /Users/xperience/GHA-OpenCV-Python2/_work/opencv-python/opencv-python/opencv/modules/dnn/src/layers/slice_layer.cpp:259: error: (-215:Assertion failed) sliceRanges_rw[i].size() <= inpShape.size() in function 'getMemoryShapes'
未明确原因,这部分之后来更新
updated 2024.3.1, 目前能用export.py和detect.py来导出onnx和用onnx来识别图片,没报错,但是没有预测的结果。。。虽然用同样的pt文件,也是使用detect.py则有预测结果。。
python export.py --weights ./yolov5.pt --img 640 --simplify --optimize --include onnx # export
python detect.py --weights ./yolov5.onnx --source ./screenshot.png --dnn # detect
updated 2024.7.16, 用各种关键词搜索啊,比如'yolov5 onnx 摆脱深度学习框架', 还真找到了可用的。这里记录一下,发现虽然转换了onnx后能正常加载,也能推理给出检测结果,但是实际结果和区域与之前直接用pt模型有些许差异,考虑是不是因为图片预处理这里导致的影响,之后有时间看了后再更新。
首先切换到yolov5官方,拉取最新代码,执行export (实际这里的500会转换到512,但因为训练时指定的500,这里也用了500,实际是512,而且在检测时给与图片预处理时往往默认是640,会产生错误,还需要将640的地方改成512)
python export.py --weights ./models/elements.pt --img 500 --simplify --optimize --opset 12 --include onnx
然后改一改别人的脚本,发现如下的代码能用,不需要用到pytorch了,但代码有点乱,后续要整理一下,并看下预测结果差异的问题,是不是跟图片预处理有关。
先是onnx_detect.py:
import cv2
import onnxruntime
import argparse
import numpy as np
from utils import letterbox, scale_coords
class Detector:
"""
检测类
"""
def __init__(self, opt):
super(Detector, self).__init__()
self.img_size = opt.img_size
self.threshold = opt.conf_thres
self.iou_thres = opt.iou_thres
self.stride = 1
self.weights = opt.weights
self.init_model()
def init_model(self):
"""
模型初始化这一步比较固定写法
:return:
"""
sess = onnxruntime.InferenceSession(self.weights) # 加载模型权重
self.input_name = sess.get_inputs()[0].name # 获得输入节点
output_names = []
for i in range(len(sess.get_outputs())):
print("output node:", sess.get_outputs()[i].name)
output_names.append(sess.get_outputs()[i].name) # 所有的输出节点
print(output_names)
self.output_name = sess.get_outputs()[0].name # 获得输出节点的名称
print(f"input name {self.input_name}-----output_name{self.output_name}")
input_shape = sess.get_inputs()[0].shape # 输入节点形状
print("input_shape:", input_shape)
self.m = sess
def preprocess(self, img):
"""
图片预处理过程
:param img:
:return:
"""
img0 = img.copy()
img = letterbox(img, new_shape=self.img_size)[0] # 图片预处理
img = img[:, :, ::-1].transpose(2, 0, 1)
img = np.ascontiguousarray(img).astype(np.float32)
img /= 255.0
img = np.expand_dims(img, axis=0)
assert len(img.shape) == 4
return img0, img
def detect(self, im):
"""
:param img:
:return:
"""
img0, img = self.preprocess(im)
pred = self.m.run(None, {self.input_name: img})[0] # 执行推理
pred = pred.astype(np.float32)
pred = np.squeeze(pred, axis=0)
boxes = []
classIds = []
confidences = []
for detection in pred:
scores = detection[5:]
classID = np.argmax(scores)
confidence = scores[classID] * detection[4] # 置信度为类别的概率和目标框概率值得乘积
if confidence > self.threshold:
box = detection[0:4]
(centerX, centerY, width, height) = box.astype("int")
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))
boxes.append([x, y, int(width), int(height)])
classIds.append(classID)
confidences.append(float(confidence))
idxs = cv2.dnn.NMSBoxes(boxes, confidences, self.threshold, self.iou_thres) # 执行nms算法
pred_boxes = []
pred_confes = []
pred_classes = []
if len(idxs) > 0:
for i in idxs.flatten():
confidence = confidences[i]
if confidence >= self.threshold:
pred_boxes.append(boxes[i])
pred_confes.append(confidence)
pred_classes.append(classIds[i])
return im, pred_boxes, pred_confes, pred_classes
def main(opt):
det = Detector(opt)
image = cv2.imread(opt.img)
shape = (det.img_size, det.img_size)
names = ['text_field', 'button', 'checkbox', 'dropdown', 'link', 'radio_button', 'switch']
img, pred_boxes, pred_confes, pred_classes = det.detect(image)
if len(pred_boxes) > 0:
for i, _ in enumerate(pred_boxes):
box = pred_boxes[i]
left, top, width, height = box[0], box[1], box[2], box[3]
box = (left, top, left + width, top + height)
box = np.squeeze(
scale_coords(shape, np.expand_dims(box, axis=0).astype("float"), img.shape[:2]).round(), axis=0).astype(
"int") # 进行坐标还原
x0, y0, x1, y1 = box[0], box[1], box[2], box[3]
# 执行画图函数
cv2.rectangle(image, (x0, y0), (x1, y1), (0, 0, 255), thickness=2)
cv2.putText(image, '{0}--{1:.2f}'.format(names[pred_classes[i]], pred_confes[i]), (x0, y0 - 2),
cv2.FONT_HERSHEY_SIMPLEX, 1, (235, 123, 76), thickness=2)
cv2.imshow("detector", image)
while True:
user_input = cv2.waitKey(3000)
if user_input == ord('q'):
break
cv2.destroyAllWindows()
#
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default='./models/elements.onnx', help='onnx path(s)') # 留意这里,改下默认模型地址
parser.add_argument('--img', type=str, default='./img/screenshot.png', help='source') # file/folder, 0 for webcam # 留意这里,改下默认图片地址
parser.add_argument('--img-size', type=int, default=512, help='inference size (pixels)') # 留意这里,将默认的图片大小改为512以作对应
parser.add_argument('--conf-thres', type=float, default=0.33, help='object confidence threshold') # 置信度和下面的iou都可以按需要更改
parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS')
parser.add_argument('--line-thickness', default=1, type=int, help='bounding box thickness (pixels)')
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
opt = parser.parse_args()
main(opt)
然后是utils.py:
import numpy as np
import cv2
def letterbox(img, new_shape=(640, 640), auto=False, scaleFill=False, scaleUp=True):
"""
python的信封图片缩放
:param img: 原图
:param new_shape: 缩放后的图片
:param color: 填充的颜色
:param auto: 是否为自动
:param scaleFill: 填充
:param scaleUp: 向上填充
:return:
"""
shape = img.shape[:2] # current shape[height,width]
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
if not scaleUp:
r = min(r, 1.0) # 确保不超过1
ration = r, r # width,height 缩放比例
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
if auto:
dw, dh = np.mod(dw, 64), np.mod(dh, 64)
elif scaleFill:
dw, dh = 0.0, 0.0
new_unpad = (new_shape[1], new_shape[0])
ration = new_shape[1] / shape[1], new_shape[0] / shape[0]
# 均分处理
dw /= 2
dh /= 2
if shape[::-1] != new_unpad:
img = cv2.resize(img, 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))
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) # 添加边界
return img, ration, (dw, dh)
def clip_coords(boxes, img_shape):
"""
图片的边界处理
:param boxes: 检测框
:param img_shape: 图片的尺寸
:return:
"""
boxes[:, 0].clip(0, img_shape[1]) # x1
boxes[:, 1].clip(0, img_shape[0]) # y1
boxes[:, 2].clip(0, img_shape[1]) # x2
boxes[:, 3].clip(0, img_shape[0]) # x2
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
"""
坐标还原
:param img1_shape: 旧图像的尺寸
:param coords: 坐标
:param img0_shape:新图像的尺寸
:param ratio_pad: 填充率
:return:
"""
if ratio_pad is None: # 从img0_shape中计算
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain=old/new
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2
else:
gain = ratio_pad[0][0]
pad = ratio_pad[1]
coords[:, [0, 2]] -= pad[0] # x padding
coords[:, [1, 3]] -= pad[1] # y padding
coords[:, :4] /= gain
clip_coords(coords, img0_shape)
return coords
用于加载从pt转到onnx的模型也仅需要如下的包,少多了,requirements.txt如下:
coloredlogs==15.0.1
flatbuffers==24.3.25
humanfriendly==10.0
mpmath==1.3.0
numpy==1.24.4
onnxruntime==1.18.1
opencv-python==4.10.0.84
packaging==24.1
protobuf==5.27.2
sympy==1.13.0
对于paddle ocr的模型转换为onnx,可以先安装包
pip install paddle2onnx
pip install onnxruntime
然后开始转
paddle2onnx --model_dir ./paddle/det/en/en_PP-OCRv3_det_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./paddle/det/det_model.onnx --opset_version 11 --enable_onnx_checker True
paddle2onnx --model_dir ./paddle/rec/en/en_PP-OCRv3_rec_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./paddle/rec/rec_model.onnx --opset_version 11 --enable_onnx_checker True
paddle2onnx --model_dir ./paddle/cls/ch_ppocr_mobile_v2.0_cls_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./paddle/cls/cls_model.onnx --opset_version 11 --enable_onnx_checker True