详细介绍 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
簡化步驟:
結果如下:
python -m onnxsim ./best20221027.onnx ./sim_best20221027.onnxSimplifying... Finish! Here is the difference: ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ ┃ Original Model ┃ Simplified Model ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ Add │ 7 │ 7 │ │ Concat │ 14 │ 14 │ │ Constant │ 34 │ 0 │ │ Conv │ 62 │ 62 │ │ MaxPool │ 3 │ 3 │ │ Mul │ 59 │ 59 │ │ Reshape │ 3 │ 3 │ │ Resize │ 2 │ 2 │ │ Sigmoid │ 59 │ 59 │ │ Slice │ 8 │ 8 │ │ Transpose │ 3 │ 3 │ │ Model Size │ 27.0MiB │ 27.0MiB │ └────────────┴────────────────┴──────────────────┘Constant 變成了 0 ,得到了簡化。
2 ONNX Runtime(Python)讀取并運行 ONNX 格式的模型
onnxruntime python 推理模型,主要是為了測試模型的準確,模型部署的最終目的的用 C++ 部署,從而部署在嵌入式設備等。
ONNX Runtime Docs(官方文檔)
推理總流程示例如下:
推理的全部代碼如下:其中 輸入和輸出的數據 需要根據ONNX模型的輸入輸出格式進行處理
代碼參考的文章是(基本是復制過來進行微小修改和添加注釋,建議收藏原文):YOLOV5模型轉onnx并推理,后面的章節均是對代碼的介紹。
下面對代碼進行展開介紹:
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
流程如下:
其中對onnxruntime.InferenceSession.run()的解釋:API Detail | InferenceSession
- InferenceSession 是 ONNX Runtime 的主要類。它用于加載和運行 ONNX 模型,以及指定環境和應用程序配置選項。
- 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。
自己選擇內核的優先順序如下:
- 如果要改為特定于您的環境的執行提供程序,可以通過會話選項參數提供其他會話配置。例如,要在會話上啟用分析:onnxruntime.SessionOptions().enable_profiling=True
C++API 中對于onnxruntime.SessionOptions() 的解釋 :Ort::SessionOptions Struct Reference
2.4 Data inputs and outputs
The ONNX Runtime Inference Session consumes and produces data using its OrtValue class.
數據的處理代碼如下:選擇的方案是
- Data on CPU
代碼如下,可以通過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),同時推斷模型(如果作為輸入提要的一部分提供):
3 Yolov5 ONNX 模型的輸入輸出數據處理
- 其實輸入輸出數據的處理代碼,寫得最好的代碼還是 YOLOV5 github中的代碼,這里的代碼主要是了解一下理念,如果要想優化,就參考:https://github.com/ultralytics/yolov5
如果這里的解析看不懂也很正常,至少要看過YOLOV2的論文才能看懂這里的解析
3.1 對 YOLOV5 輸入輸出數據的解釋
可以從ONNX 格式的模型看到數據的輸入輸出格式為:展示的是整個模型的所有輸入輸出節點,可以看到有一個輸入(名稱為images)和三個輸出。實際部署的時候是導出的單輸出模型,三個輸出只是便于介紹。
輸入格式:1x3x640x640,3是RGB三通道,總體是 Batch Channel H W。 輸出有三層,分別在三個不同的位置,有不同的格式。下面對其進行簡單的解釋。
- 上圖的介紹:三輸出模型的輸出結果是 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 + 每種類別的條件概率
- 進行置信度過濾、極大值抑制和坐標轉換,即可得到結果了。
代碼:
其中非極大值抑制 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 部署(包含官方文档的介绍)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Matlab学习——求解微分方程(组)
- 下一篇: 自学Python 57 多线程开发(七)