日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

详细介绍 Yolov5 转 ONNX模型 + 使用ONNX Runtime 的 Python 部署(包含官方文档的介绍)

發布時間:2023/12/18 python 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 详细介绍 Yolov5 转 ONNX模型 + 使用ONNX Runtime 的 Python 部署(包含官方文档的介绍) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1 Pytorch模型轉Onnx

對ONNX的介紹強烈建議看,本文做了很多參考:模型部署入門教程(一):模型部署簡介
模型部署入門教程(三):PyTorch 轉 ONNX 詳解
以及Pytorch的官方介紹:(OPTIONAL) EXPORTING A MODEL FROM PYTORCH TO ONNX AND RUNNING IT USING ONNX RUNTIME
C++的部署:詳細介紹 Yolov5 轉 ONNX模型 + 使用 ONNX Runtime 的 C++ 部署(包含官方文檔的介紹)。

1.1 獲得自己的PyTorch模型

我用的是自己訓練好的一個yolov5-5.0模型。

1.2 Yolov5-5.0 的模型轉換成 ONNX 格式的模型

PyCharm環境如下:

yolov5 可以使用官方的 export.py 腳本進行轉換,這里不做詳細解析
可參考:yolov5轉onnx,c++調用完美復現
在網站 Netron (開源的模型可視化工具)來可視化 ONNX 模型。
想要理解 1.2 節的內容,請看對參數詳細介紹的第3章

1.2.1 查看導出的模型(多輸出版本)

  • 點擊input 或者 output,可以查看 ONNX 模型的基本信息,包括模型的版本信息,以及模型輸入、輸出的名稱和數據類型。參數具體代表什么看第3章。。
    此處三個輸出的 onnx 模型只是為了便于只管的看出參數的維度,實際上部署的話使用單輸出的onnx模型,單輸出的模型見下面的第二張圖的部分。
  • 如果導出為動態:python ./models/export.py --weights ./weights/best20221027.pt --img 640 --batch 1 --dynamic

1.2.2 查看導出的模型(單輸出版本)

  • 上面的三個輸出結構很清晰,但是這種多輸出的情況是一個問題,維度太多且參數還沒有進行處理,很很很不利于部署,不如在導出的時候就處理好參數為單輸出的情況,輸出轉成常用的 1 × Anchors數目 × 9(即紅圈中的結果轉換成),這樣的話每個Anchor的坐標信息就是映射到原圖中的,省去了很多處理數據的麻煩。步驟如下,參考 YOLOv5導出onnx、TrensorRT部署(LINUX):

    我采取的方案是,訓練好的模型用yolov5-master的export.py來導出即可解決:python ./export.py --weights ./best20221027.pt --img 640 --batch 1 --include=onnx
    得到的onnx文件如下! 25200 = 3 × ( 802 + 402 + 202 ),接下來就可以部署了

    onnx模型可視化,看出輸出部分進行的處理如下:三輸出模型的輸出結果是 tx ty tw th 和 t0,即下圖中sigmoid之前的參數,單輸出的模型直接輸出的是 bx by bw bh 和 score,即直接對應到原圖中的坐標參數。

    如果此時導出為動態模型python ./export.py --weights ./best20221027.pt --img 640 --batch 1 --dynamic ,則如下圖所示:

  • 點擊某一個算子節點,可以看到算子的具體信息。比如點擊第一個 Conv 可以看到每個算子記錄了算子屬性、圖結構、權重三類信息。

1.3 簡化 ONNX 模型

參考:【OpenVino CPU模型加速(二)】使用openvino加速推理
yolov5部署1——pytorch->onnx
簡化步驟:

pip install onnx-simplifier python -m onnxsim input_onnx_model output_onnx_model

結果如下:

python -m onnxsim ./best20221027.onnx ./sim_best20221027.onnxSimplifying... Finish! Here is the difference: ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ ┃ Original Model ┃ Simplified Model ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ Add │ 77 │ │ Concat │ 1414 │ │ Constant │ 340 │ │ Conv │ 6262 │ │ MaxPool │ 33 │ │ Mul │ 5959 │ │ Reshape │ 33 │ │ Resize │ 22 │ │ Sigmoid │ 5959 │ │ Slice │ 88 │ │ Transpose │ 33 │ │ Model Size │ 27.0MiB │ 27.0MiB │ └────────────┴────────────────┴──────────────────┘

Constant 變成了 0 ,得到了簡化。

2 ONNX Runtime(Python)讀取并運行 ONNX 格式的模型

onnxruntime python 推理模型,主要是為了測試模型的準確,模型部署的最終目的的用 C++ 部署,從而部署在嵌入式設備等。
ONNX Runtime Docs(官方文檔)
推理總流程示例如下:

# 檢驗模型是否正確 import onnx onnx_model = onnx.load("fashion_mnist_model.onnx") onnx.checker.check_model(onnx_model)# 加載和運行 ONNX 模型,以及指定環境和應用程序配置 import onnxruntime as ort import numpy as np x, y = test_data[0][0], test_data[0][1] ort_sess = ort.InferenceSession('fashion_mnist_model.onnx') outputs = ort_sess.run(None, {'input': x.numpy()})# Print Result predicted, actual = classes[outputs[0][0].argmax(0)], classes[y] print(f'Predicted: "{predicted}", Actual: "{actual}"')

推理的全部代碼如下:其中 輸入和輸出的數據 需要根據ONNX模型的輸入輸出格式進行處理
代碼參考的文章是(基本是復制過來進行微小修改和添加注釋,建議收藏原文):YOLOV5模型轉onnx并推理,后面的章節均是對代碼的介紹。

import onnx import onnxruntime as ort import numpy as npimport sysimport onnx import onnxruntime as ort import cv2 import numpy as npCLASSES = ['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'] # coco80類別# CLASSES = ['electrode', 'breathers', 'ventilate', 'press']class Yolov5ONNX(object):def __init__(self, onnx_path):"""檢查onnx模型并初始化onnx"""onnx_model = onnx.load(onnx_path)try:onnx.checker.check_model(onnx_model)except Exception:print("Model incorrect")else:print("Model correct")options = ort.SessionOptions()options.enable_profiling = True# self.onnx_session = ort.InferenceSession(onnx_path, sess_options=options,# providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])self.onnx_session = ort.InferenceSession(onnx_path)self.input_name = self.get_input_name() # ['images']self.output_name = self.get_output_name() # ['output0']def get_input_name(self):"""獲取輸入節點名稱"""input_name = []for node in self.onnx_session.get_inputs():input_name.append(node.name)return input_namedef get_output_name(self):"""獲取輸出節點名稱"""output_name = []for node in self.onnx_session.get_outputs():output_name.append(node.name)return output_namedef get_input_feed(self, image_numpy):"""獲取輸入numpy"""input_feed = {}for name in self.input_name:input_feed[name] = image_numpyreturn input_feeddef inference(self, img_path):""" 1.cv2讀取圖像并resize2.圖像轉BGR2RGB和HWC2CHW(因為yolov5的onnx模型輸入為 RGB:1 × 3 × 640 × 640)3.圖像歸一化4.圖像增加維度5.onnx_session 推理 """img = cv2.imread(img_path)or_img = cv2.resize(img, (640, 640)) # resize后的原圖 (640, 640, 3)img = or_img[:, :, ::-1].transpose(2, 0, 1) # BGR2RGB和HWC2CHWimg = img.astype(dtype=np.float32) # onnx模型的類型是type: float32[ , , , ]img /= 255.0img = np.expand_dims(img, axis=0) # [3, 640, 640]擴展為[1, 3, 640, 640]# img尺寸(1, 3, 640, 640)input_feed = self.get_input_feed(img) # dict:{ input_name: input_value }pred = self.onnx_session.run(None, input_feed)[0] # <class 'numpy.ndarray'>(1, 25200, 9)return pred, or_img# dets: array [x,6] 6個值分別為x1,y1,x2,y2,score,class # thresh: 閾值 def nms(dets, thresh):# dets:x1 y1 x2 y2 score class# x[:,n]就是取所有集合的第n個數據x1 = dets[:, 0]y1 = dets[:, 1]x2 = dets[:, 2]y2 = dets[:, 3]# -------------------------------------------------------# 計算框的面積# 置信度從大到小排序# -------------------------------------------------------areas = (y2 - y1 + 1) * (x2 - x1 + 1)scores = dets[:, 4]# print(scores)keep = []index = scores.argsort()[::-1] # np.argsort()對某維度從小到大排序# [::-1] 從最后一個元素到第一個元素復制一遍。倒序從而從大到小排序while index.size > 0:i = index[0]keep.append(i)# -------------------------------------------------------# 計算相交面積# 1.相交# 2.不相交# -------------------------------------------------------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)overlaps = w * h# -------------------------------------------------------# 計算該框與其它框的IOU,去除掉重復的框,即IOU值大的框# IOU小于thresh的框保留下來# -------------------------------------------------------ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)idx = np.where(ious <= thresh)[0]index = index[idx + 1]return keepdef xywh2xyxy(x):# [x, y, w, h] to [x1, y1, x2, y2]y = np.copy(x)y[:, 0] = x[:, 0] - x[:, 2] / 2y[:, 1] = x[:, 1] - x[:, 3] / 2y[:, 2] = x[:, 0] + x[:, 2] / 2y[:, 3] = x[:, 1] + x[:, 3] / 2return ydef filter_box(org_box, conf_thres, iou_thres): # 過濾掉無用的框# -------------------------------------------------------# 刪除為1的維度# 刪除置信度小于conf_thres的BOX# -------------------------------------------------------org_box = np.squeeze(org_box) # 刪除數組形狀中單維度條目(shape中為1的維度)# (25200, 9)# […,4]:代表了取最里邊一層的所有第4號元素,…代表了對:,:,:,等所有的的省略。此處生成:25200個第四號元素組成的數組conf = org_box[..., 4] > conf_thres # 0 1 2 3 4 4是置信度,只要置信度 > conf_thres 的box = org_box[conf == True] # 根據objectness score生成(n, 9),只留下符合要求的框print('box:符合要求的框')print(box.shape)# -------------------------------------------------------# 通過argmax獲取置信度最大的類別# -------------------------------------------------------cls_cinf = box[..., 5:] # 左閉右開(5 6 7 8),就只剩下了每個grid cell中各類別的概率cls = []for i in range(len(cls_cinf)):cls.append(int(np.argmax(cls_cinf[i]))) # 剩下的objecctness score比較大的grid cell,分別對應的預測類別列表all_cls = list(set(cls)) # 去重,找出圖中都有哪些類別# set() 函數創建一個無序不重復元素集,可進行關系測試,刪除重復數據,還可以計算交集、差集、并集等。# -------------------------------------------------------# 分別對每個類別進行過濾# 1.將第6列元素替換為類別下標# 2.xywh2xyxy 坐標轉換# 3.經過非極大抑制后輸出的BOX下標# 4.利用下標取出非極大抑制后的BOX# -------------------------------------------------------output = []for i in range(len(all_cls)):curr_cls = all_cls[i]curr_cls_box = []curr_out_box = []for j in range(len(cls)):if cls[j] == curr_cls:box[j][5] = curr_clscurr_cls_box.append(box[j][:6]) # 左閉右開,0 1 2 3 4 5curr_cls_box = np.array(curr_cls_box) # 0 1 2 3 4 5 分別是 x y w h score class# curr_cls_box_old = np.copy(curr_cls_box)curr_cls_box = xywh2xyxy(curr_cls_box) # 0 1 2 3 4 5 分別是 x1 y1 x2 y2 score classcurr_out_box = nms(curr_cls_box, iou_thres) # 獲得nms后,剩下的類別在curr_cls_box中的下標for k in curr_out_box:output.append(curr_cls_box[k])output = np.array(output)return outputdef draw(image, box_data):# -------------------------------------------------------# 取整,方便畫框# -------------------------------------------------------boxes = box_data[..., :4].astype(np.int32) # x1 x2 y1 y2scores = box_data[..., 4]classes = box_data[..., 5].astype(np.int32)for box, score, cl in zip(boxes, scores, classes):top, left, right, bottom = boxprint('class: {}, score: {}'.format(CLASSES[cl], score))print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom))cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),(top, left),cv2.FONT_HERSHEY_SIMPLEX,0.6, (0, 0, 255), 2)return imageif __name__ == "__main__":# onnx_path = 'weights/sim_best20221027.onnx'onnx_path = 'weights/yolov5s.onnx'model = Yolov5ONNX(onnx_path)# output, or_img = model.inference('data/images/img.png')output, or_img = model.inference('data/images/street.jpg')print('pred: 位置[0, 10000, :]的數組')print(output.shape)print(output[0, 10000, :])outbox = filter_box(output, 0.5, 0.5) # 最終剩下的Anchors:0 1 2 3 4 5 分別是 x1 y1 x2 y2 score classprint('outbox( x1 y1 x2 y2 score class):')print(outbox)if len(outbox) == 0:print('沒有發現物體')sys.exit(0)or_img = draw(or_img, outbox)cv2.imwrite('./run/images/res.jpg', or_img)

下面對代碼進行展開介紹:

2.1 onnxruntime python推理模型

對于 PyTorch - ONNX - ONNX Runtime 這條部署流水線,只要在目標設備中得到 .onnx 文件,并在 ONNX Runtime 上運行模型,模型部署就算大功告成了。
這里進行 Python ONNX Runtime 的推理嘗試,如果不需要的直接看下一章節的 TensorRT 部署。
參考官網:ONNX Runtime | Home 的CV部分

對函數有疑問參考官方API :Python API Reference Docs
代碼的解釋和 ONNX Runtime 的學習如下:

2.2 Load the onnx model with onnx.load,并檢查模型:

import onnx onnx_model = onnx.load("sim_best20221027.onnx") try: onnx.checker.check_model(onnx_model) except Exception: print("Model incorrect") else: print("Model correct")

檢測異常:try except (異常捕獲),沒有問題,可以開始下一步,Load and run a model。

2.3 Create inference session

using ort.InferenceSession
流程如下:

import onnxruntime as ort options = ort.SessionOptions() options.enable_profiling=True ort_sess = ort.InferenceSession('sim_best20221027.onnx', sess_options=options, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) outputs = ort_sess.run([output names], inputs)# Print Result predicted, actual = classes[outputs[0][0].argmax(0)], classes[y] print(f'Predicted: "{predicted}", Actual: "{actual}"')

其中對onnxruntime.InferenceSession.run()的解釋:API Detail | InferenceSession

  • InferenceSession 是 ONNX Runtime 的主要類。它用于加載和運行 ONNX 模型,以及指定環境和應用程序配置選項。
import onnxruntime as ort ort_sess = ort.InferenceSession('sim_best20221027.onnx') outputs = ort_sess.run([output names], inputs)
  • An execution provider contains the set of kernels for a specific execution target (CPU, GPU, IoT etc). The list of available execution providers:ONNX Runtime Execution Providers
    執行內核是使用 providers 參數配置,根據提供者列表中給出的優先順序選擇來自不同執行提供者的內核。在 CPU 上運行是唯一一次 API 允許不顯式設置提供程序參數,所以如果有CPU內核,且不設置執行內核的話,默認CPU。
    自己選擇內核的優先順序如下:
session = onnxruntime.InferenceSession(model,providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
  • 如果要改為特定于您的環境的執行提供程序,可以通過會話選項參數提供其他會話配置。例如,要在會話上啟用分析:onnxruntime.SessionOptions().enable_profiling=True
    C++API 中對于onnxruntime.SessionOptions() 的解釋 :Ort::SessionOptions Struct Reference
options = onnxruntime.SessionOptions() options.enable_profiling=True session = onnxruntime.InferenceSession('model.onnx', sess_options=options, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))

2.4 Data inputs and outputs

The ONNX Runtime Inference Session consumes and produces data using its OrtValue class.
數據的處理代碼如下:選擇的方案是

  • Data on CPU
    代碼如下,可以通過OrtValue的成員函數檢查輸入的數據。
# X is numpy array on cpu ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X) ortvalue.device_name() # 'cpu' ortvalue.shape() # shape of the numpy array X ortvalue.data_type() # 'tensor(float)' ortvalue.is_tensor() # 'True' np.array_equal(ortvalue.numpy(), X) # 'True'# ortvalue can be provided as part of the input feed to a model session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) results = session.run(["Y"], {"X": ortvalue})

默認情況下,ONNX 運行時始終將輸入和輸出放在 CPU 上。如果在 CPU 以外的設備上消耗和生成輸入或輸出,則將數據放在 CPU 上可能不是最佳選擇,因為它會在 CPU 和設備之間引入數據復制。

2.4.1 Data inputs and outputs Data on decice

ONNX 運行時支持自定義數據結構,該結構支持所有 ONNX 數據格式,允許用戶將支持這些格式的數據放置在設備上,例如,支持 CUDA 的設備上。在 ONNX Runtime 中,這稱為 IOBinding。
要使用 IOBinding 功能,需要將 InferenceSession.run() 替換為 InferenceSession.run_with_iobinding()。

2.4.1.1 A graph is executed on a device other than CPU

例如 CUDA。用戶可以使用 IOBinding 將數據復制到 GPU 上:

# X is numpy array on cpu session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) io_binding = session.io_binding() # OnnxRuntime will copy the data over to the CUDA device if 'input' is consumed by nodes on the CUDA device io_binding.bind_cpu_input('input', X) io_binding.bind_output('output') session.run_with_iobinding(io_binding) Y = io_binding.copy_outputs_to_cpu()[0]

2.4.1.2 輸入數據在設備上

用戶直接使用輸入。輸出數據在 CPU 上:

# X is numpy array on cpu X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0) session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) io_binding = session.io_binding() io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr()) io_binding.bind_output('output') session.run_with_iobinding(io_binding) Y = io_binding.copy_outputs_to_cpu()[0]

2.4.1.3 輸入數據和輸出數據都在設備上

用戶直接使用輸入,也可以將輸出放在設備上:

#X is numpy array on cpu X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0) Y_ortvalue = onnxruntime.OrtValue.ortvalue_from_shape_and_type([3, 2], np.float32, 'cuda', 0) # Change the shape to the actual shape of the output being bound session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) io_binding = session.io_binding() io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr()) io_binding.bind_output(name='output', device_type=Y_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=Y_ortvalue.shape(), buffer_ptr=Y_ortvalue.data_ptr()) session.run_with_iobinding(io_binding)

2.4.1.4 用戶可以請求 ONNX 運行時在設備上分配輸出。

這對于動態整形輸出特別有用。用戶可以使用 get_outputs() API 來訪問與分配的輸出對應的 OrtValue。因此,用戶可以將 ONNX 運行時分配的內存作為 OrtValue 用于輸出:

#X is numpy array on cpu X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0) session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) io_binding = session.io_binding() io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr()) #Request ONNX Runtime to bind and allocate memory on CUDA for 'output' io_binding.bind_output('output', 'cuda') session.run_with_iobinding(io_binding) # The following call returns an OrtValue which has data allocated by ONNX Runtime on CUDA ort_output = io_binding.get_outputs()[0]

此外,ONNX 運行時支持直接使用 OrtValue (s),同時推斷模型(如果作為輸入提要的一部分提供):

  • Users can bind OrtValue (s) directly.
  • #X is numpy array on cpu #X is numpy array on cpu X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0) Y_ortvalue = onnxruntime.OrtValue.ortvalue_from_shape_and_type([3, 2], np.float32, 'cuda', 0) # Change the shape to the actual shape of the output being bound session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) io_binding = session.io_binding() io_binding.bind_ortvalue_input('input', X_ortvalue) io_binding.bind_ortvalue_output('output', Y_ortvalue) session.run_with_iobinding(io_binding)
  • You can also bind inputs and outputs directly to a PyTorch tensor.
  • # X is a PyTorch tensor on device session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])) binding = session.io_binding()X_tensor = X.contiguous()binding.bind_input(name='X',device_type='cuda',device_id=0,element_type=np.float32,shape=tuple(x_tensor.shape),buffer_ptr=x_tensor.data_ptr(),)## Allocate the PyTorch tensor for the model output Y_shape = ... # You need to specify the output PyTorch tensor shape Y_tensor = torch.empty(Y_shape, dtype=torch.float32, device='cuda:0').contiguous() binding.bind_output(name='Y',device_type='cuda',device_id=0,element_type=np.float32,shape=tuple(Y_tensor.shape),buffer_ptr=Y_tensor.data_ptr(), )session.run_with_iobinding(binding)

    3 Yolov5 ONNX 模型的輸入輸出數據處理

    • 其實輸入輸出數據的處理代碼,寫得最好的代碼還是 YOLOV5 github中的代碼,這里的代碼主要是了解一下理念,如果要想優化,就參考:https://github.com/ultralytics/yolov5
      如果這里的解析看不懂也很正常,至少要看過YOLOV2的論文才能看懂這里的解析

    3.1 對 YOLOV5 輸入輸出數據的解釋

    可以從ONNX 格式的模型看到數據的輸入輸出格式為:展示的是整個模型的所有輸入輸出節點,可以看到有一個輸入(名稱為images)和三個輸出。實際部署的時候是導出的單輸出模型,三個輸出只是便于介紹。

    輸入格式:1x3x640x640,3是RGB三通道,總體是 Batch Channel H W。 輸出有三層,分別在三個不同的位置,有不同的格式。

    下面對其進行簡單的解釋。

  • 標準的 yolov5 的 輸出 有 三個 ,分別是 1x255x80x80 1x255x40x40 1x255x20x20 其中這里的255是85*3,80×80、40×40 和 20×20 是特征圖分辨率
  • 這里的3是指3個錨框,而這里的85是指5+80=85,其中80是類別數量,每個類別數量對應一個label score,一共80個label score,而5是指box的四個坐標加一個box score.
  • 三個輸出層中淺層特征圖分辨率是80乘80,中層是40乘40,深層是20乘20
  • 一般來說淺層用于預測小物體,深層用于預測大物體。可以根據感受野的概念來理解。
    • 上圖的介紹:三輸出模型的輸出結果是 tx ty tw th 和 t0,即sigmoid之前的參數,單輸出的模型直接輸出的是 bx by bw bh 和 score,即直接對應到原圖中的坐標參數。
    • 實際上單輸出其實就是在導出模型的時候多做了 tx ty tw th t0 -----> bx by bw bh score 的步驟,直接獲取每個Anchor對于原圖的信息,而不用自己進行復雜的處理,會非常有利于部署。
    • 上圖是YOLOV2 和 YOLOV3 的參數,YOLOV3 相對于YOLOV2的改進就是 objectness score 非0即1

    3.2 YOLOV4 和 V5 相對于 V2 V3 的改進

    • 實際上在 YOLOV4 和 YOLOV5 中為了消除 Grid 敏感度,參數關系略有不同,如下圖所示:這樣可以取到 0 和 1。
      YOLOV4:對 bx by 的改進如下

      YOLOV5:在YOLOV4對對 bx by 的改進基礎上,對 bw bh進行了改進

    • 根據論文和代碼,上面的虛線部分,在80×80;40×40 和 20×20 大小的輸出中,錨框大小(pw ph)為:
      [(10,13), (16,30), (33,23)] # 80×80的三個錨框
      [(30,61), (62,45), (59,119)] # 40×40的三個錨框
      [(116,90), (156,198), (373,326)] #20×20 的三個錨框

    • 其損失函數是結合三層輸出的損失值:

    3.3 輸入數據的處理

    代碼:

    def inference(self, img_path):""" 1.cv2讀取圖像并resize2.圖像轉BGR2RGB和HWC2CHW(因為yolov5的onnx模型輸入為 RGB:1 × 3 × 640 × 640)3.圖像歸一化4.圖像增加維度5.onnx_session 推理 """img = cv2.imread(img_path)or_img = cv2.resize(img, (640, 640)) # resize后的原圖 (640, 640, 3)img = or_img[:, :, ::-1].transpose(2, 0, 1) # BGR2RGB和HWC2CHWimg = img.astype(dtype=np.float32) # onnx模型的類型是type: float32[ , , , ]img /= 255.0img = np.expand_dims(img, axis=0) # [3, 640, 640]擴展為[1, 3, 640, 640]# img尺寸(1, 3, 640, 640)input_feed = self.get_input_feed(img) # dict:{ input_name: input_value }pred = self.onnx_session.run(None, input_feed)[0] # <class 'numpy.ndarray'>(1, 25200, 9)return pred, or_img

    把輸入的圖片轉換成 1x3x640x640,再作為模型的輸入:
    opencv python 把圖(cv2下)BGR轉RGB,且HWC轉CHW
    如果想要使用可變的輸入尺寸,參考下面yolov5的源碼中的 padded resize 方法,檢測效果其實更好:

    • detect.py:
    • dataset.py: class LoadImages:的函數

    3.4 輸出數據的處理

    當輸入圖像是 640×640 時,輸出數據是 (1, 25200, 4+1+class):4+1+class 是檢測框的坐標、大小 和 分數。導出為這種單輸出,直接獲得的就是 每個預測框 的 bx by bw bh,而不是 Anchor 的 tx ty tw th

    • 輸出數據對應的位置是:0 - 8 對應的是 bx by bw bh score + 每種類別的條件概率
    • 進行置信度過濾、極大值抑制和坐標轉換,即可得到結果了。
      代碼:
    def filter_box(org_box, conf_thres, iou_thres): # 過濾掉無用的框# -------------------------------------------------------# 刪除為1的維度# 刪除置信度小于conf_thres的BOX# -------------------------------------------------------org_box = np.squeeze(org_box) # 刪除數組形狀中單維度條目(shape中為1的維度)# (25200, 9)# […,4]:代表了取最里邊一層的所有第4號元素,…代表了對:,:,:,等所有的的省略。此處生成:25200個第四號元素組成的數組conf = org_box[..., 4] > conf_thres # 0 1 2 3 4 4是置信度,只要置信度 > conf_thres 的box = org_box[conf == True] # 根據objectness score生成(n, 9),只留下符合要求的框print('box:符合要求的框')print(box.shape)# -------------------------------------------------------# 通過argmax獲取置信度最大的類別# -------------------------------------------------------cls_cinf = box[..., 5:] # 左閉右開(5 6 7 8),就只剩下了每個grid cell中各類別的概率cls = []for i in range(len(cls_cinf)):cls.append(int(np.argmax(cls_cinf[i]))) # 剩下的objecctness score比較大的grid cell,分別對應的預測類別列表all_cls = list(set(cls)) # 去重,找出圖中都有哪些類別# set() 函數創建一個無序不重復元素集,可進行關系測試,刪除重復數據,還可以計算交集、差集、并集等。# -------------------------------------------------------# 分別對每個類別進行過濾# 1.將第6列元素替換為類別下標# 2.xywh2xyxy 坐標轉換# 3.經過非極大抑制后輸出的BOX下標# 4.利用下標取出非極大抑制后的BOX# -------------------------------------------------------output = []for i in range(len(all_cls)):curr_cls = all_cls[i]curr_cls_box = []curr_out_box = []for j in range(len(cls)):if cls[j] == curr_cls:box[j][5] = curr_clscurr_cls_box.append(box[j][:6]) # 左閉右開,0 1 2 3 4 5curr_cls_box = np.array(curr_cls_box) # 0 1 2 3 4 5 分別是 x y w h score class# curr_cls_box_old = np.copy(curr_cls_box)curr_cls_box = xywh2xyxy(curr_cls_box) # 0 1 2 3 4 5 分別是 x1 y1 x2 y2 score classcurr_out_box = nms(curr_cls_box, iou_thres) # 獲得nms后,剩下的類別在curr_cls_box中的下標for k in curr_out_box:output.append(curr_cls_box[k])output = np.array(output)return output

    其中非極大值抑制 curr_out_box = nms(curr_cls_box, iou_thres) 和 坐標轉換 curr_cls_box = xywh2xyxy(curr_cls_box):

    # dets: array [x,6] 6個值分別為x1,y1,x2,y2,score,class # thresh: 閾值 def nms(dets, thresh):# dets:x1 y1 x2 y2 score class# x[:,n]就是取所有集合的第n個數據x1 = dets[:, 0]y1 = dets[:, 1]x2 = dets[:, 2]y2 = dets[:, 3]# -------------------------------------------------------# 計算框的面積# 置信度從大到小排序# -------------------------------------------------------areas = (y2 - y1 + 1) * (x2 - x1 + 1)scores = dets[:, 4]# print(scores)keep = []index = scores.argsort()[::-1] # np.argsort()對某維度從小到大排序# [::-1] 從最后一個元素到第一個元素復制一遍。倒序從而從大到小排序while index.size > 0:i = index[0]keep.append(i)# -------------------------------------------------------# 計算相交面積# 1.相交# 2.不相交# -------------------------------------------------------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)overlaps = w * h# -------------------------------------------------------# 計算該框與其它框的IOU,去除掉重復的框,即IOU值大的框# IOU小于thresh的框保留下來# -------------------------------------------------------ious = overlaps / (areas[i] + areas[index[1:]] - overlaps)idx = np.where(ious <= thresh)[0]index = index[idx + 1]return keepdef xywh2xyxy(x):# [x, y, w, h] to [x1, y1, x2, y2]y = np.copy(x)y[:, 0] = x[:, 0] - x[:, 2] / 2y[:, 1] = x[:, 1] - x[:, 3] / 2y[:, 2] = x[:, 0] + x[:, 2] / 2y[:, 3] = x[:, 1] + x[:, 3] / 2return y

    識別結果如下:脫離Pytorch環境部署成功!如果對輸入數據處理時,長寬比不變,效果會更好,如何處理參考 YOLOV5源碼。

    總結

    以上是生活随笔為你收集整理的详细介绍 Yolov5 转 ONNX模型 + 使用ONNX Runtime 的 Python 部署(包含官方文档的介绍)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。