用opencv的dnn模塊做yolov5目標檢測

最近在微信公衆號裏看到多篇講解yolov5在openvino部署做目標檢測文章,但是沒看到過用opencv的dnn模塊做yolov5目標檢測的。於是,我就想着編寫一套用opencv的dnn模塊做yolov5目標檢測的程序。在編寫這套程序時,遇到的bug和解決辦法,在這篇文章裏講述一下。

在yolov5之前的yolov3和yolov4的官方代碼都是基於darknet框架的實現的,因此opencv的dnn模塊做目標檢測時,讀取的是.cfg和.weight文件,那時候編寫程序很順暢,沒有遇到bug。但是yolov5的官方代碼(https://github.com/ultralytics/yolov5)是基於pytorch框架實現的,但是opencv的dnn模塊不支持讀取pytorch的訓練模型文件的。如果想要把pytorch的訓練模型.pth文件加載到opencv的dnn模塊裏,需要先把pytorch的訓練模型.pth文件轉換到.onnx文件,然後才能載入到opencv的dnn模塊裏。

因此,用opencv的dnn模塊做yolov5目標檢測的程序,包含兩個步驟:(1).把pytorch的訓練模型.pth文件轉換到.onnx文件。(2).opencv的dnn模塊讀取.onnx文件做前向計算。

(1).把pytorch的訓練模型.pth文件轉換到.onnx文件

在做這一步時,我得吐槽一下官方代碼:https://github.com/ultralytics/yolov5,這套程序裏的代碼混亂,在pytorch裏,通常是在.py文件裏定義網絡結構的,但是官方代碼是在.yaml文件定義網絡結構,利用pytorch動態圖特性,解析.yaml文件自動生成網絡結構。在.yaml文件裏有depth_multiple和width_multiple,它是控制網絡的深度和寬度的參數。這麼做的好處是能夠靈活的配置網絡結構,但是不利於理解網絡結構,假如你想設斷點查看某一層的參數和輸出數值,那就沒辦法了。因此,在我編寫的轉換到.onnx文件的程序裏,網絡結構是在.py文件裏定義的。其次,在官方代碼裏,還有一個奇葩的地方,那就是.pth文件。起初,我下載官方代碼到本地運行時,torch.load讀取.pth文件總是出錯,後來把pytorch升級到1.7,就讀取成功了。可以看到版本兼容性不好,這是它的一個不足之處。設斷點查看讀取的.pth文件裏的內容,可以看到.pth裏既存儲有模型參數,也存儲有網絡結構,還儲存了一些超參數,包括anchors,stride等等的。第一次見到有這種操作的,通常情況下,.pth文件裏只存儲了訓練模型參數的。

查看models\yolo.py裏的Detect類,在構造函數裏,有這麼兩行代碼:

我嘗試過把這兩行代碼改成self.anchors = a 和 self.anchor_grid = a.clone().view(self.nl, 1, -1, 1, 1, 2),程序依然能正常運行,但是torch.save保存模型文件後,可以看到.pth文件裏沒有存儲anchors和anchor_grid了,在百度搜索register_buffer,解釋是:pytorch中register_buffer模型保存和加載的時候可以寫入和讀出。

在這兩行代碼的下一行:

它的作用是做特徵圖的輸出通道對齊,通過1x1卷積把三種尺度特徵圖的輸出通道都調整到 num_anchors*(num_classes+5)。

閱讀Detect類的forward函數代碼,可以看出它的作用是根據偏移公式計算出預測框的中心座標和高寬,這裏需要注意的是,計算高和寬的代碼:

                                                                 pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]

沒有采用exp操作,而是直接乘上anchors[i],這是yolov5與yolov3v4的一個最大區別(還有一個區別就是在訓練階段的loss函數裏,yolov5採用鄰域的正樣本anchor匹配策略,增加了正樣本。其它的是一些小區別,比如yolov5的第一個模塊採用FOCUS把輸入數據2倍下采樣切分成4份,在channel維度進行拼接,然後進行卷積操作,yolov5的激活函數沒有使用Mish)。

現在可以明白Detect類的作用是計算預測框的中心座標和高寬,簡單來說就是生成proposal,作爲後續NMS的輸入,進而輸出最終的檢測框。我覺得在Detect類裏定義的1x1卷積是不恰當的,應該把它定義在Detect類的外面,緊鄰着Detect類之前定義1x1卷積。

在官方代碼裏,有轉換到onnx文件的程序: python models/export.py --weights yolov5s.pt --img 640 --batch 1

在pytorch1.7版本里,程序是能正常運行生成onnx文件的。觀察export.py裏的代碼,在執行torch.onnx.export之前,有這麼一段代碼:

注意其中的for循環,我試驗過註釋掉它,重新運行就會出錯,打印出的錯誤如下:

由此可見,這段for循環代碼是必需的。

(2).opencv的dnn模塊讀取.onnx文件做前向計算

在生成.onnx文件後,就可以用opencv的dnn模塊裏的cv2.dnn.readNet讀取它。然而,在讀取時,出現瞭如下錯誤:

我在百度搜索這個問題的解決辦法,看到一篇知乎文章(https://zhuanlan.zhihu.com/p/286298001?utm_source=wechat_timeline),文章裏講述的第一條:

於是查看yolov5的代碼,在common.py文件的Focus類,torch.cat的輸入裏有4次切片操作,代碼如下:

那麼現在需要更換索引式的切片操作,觀察到註釋的Contract類,它就是用view和permute函數完成切片操作的,於是修改代碼如下:

其次,在models\yolo.py裏的Detect類裏,也有切片操作,代碼如下:

前面說過,Detect類的作用是計算預測框的中心座標和高寬,生成proposal,這個是屬於後處理的,因此不需要把它寫入到onnx文件裏。

總結一下,按照上面的截圖代碼,修改Focus類,把Detect類裏面的1x1卷積定義在緊鄰着Detect類之前的外面,然後去掉Detect類,組成新的model,作爲torch.onnx.export的輸入,

torch.onnx.export(model, inputs, output_onnx, verbose=False, opset_version=12, input_names=['images'], output_names=['out0', 'out1', 'out2'])

最後生成的onnx文件,opencv的dnn模塊就能成功讀取了,接下來對照Detect類裏的forward函數,用python或者C++編寫計算預測框的中心座標和高寬的功能。

週末這兩天,我在win10+cpu機器裏編寫了用opencv的dnn模塊做yolov5目標檢測的程序,包含Python和C++兩個版本的。程序都調試通過了,運行結果也是正確的。我把這套代碼發佈在github上,地址是  https://github.com/hpc203/yolov5-dnn-cpp-python

後處理模塊,python版本用numpy array實現的,C++版本的用vector和數組實現的,整套程序只依賴opencv庫(opencv4版本以上的)就能正常運行,徹底擺脫對深度學習框架pytorch,tensorflow,caffe,mxnet等等的依賴。用openvino作目標檢測,需要把onnx文件轉換到.bin和.xml文件,相比於用dnn模塊加載onnx文件做目標檢測是多了一個步驟的。因此,我就想編寫一套用opencv的dnn模塊做yolov5目標檢測的程序,用opencv的dnn模塊做深度學習目標檢測,在win10和ubuntu,在cpu和gpu上都能運行,可見dnn模塊的通用性更好,很接地氣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章