實用教程詳解:用OpenCV的DNN模塊部署YOLOv5目標檢測

極市導讀
?本文中介紹的整套程序只依賴OpenCV庫就能正常運行,徹底擺脫了對深度學習框架的依賴。文章講述了作者在自己編寫用OpenCV的dnn模塊做YOLOv5目標檢測的程序的過程中遇到的bug以及解決的辦法。?>>加入極市CV技術(shù)交流群,走在計算機視覺的最前沿
最近看到多篇講解YOLOv5在OpenVINO部署做目標檢測文章,但是沒看到過用OpenCV的DNN模塊做YOLOv5目標檢測的。于是,我就想編寫一套用OpenCV的DNN模塊做YOLOv5目標檢測的程序。
在編寫這套程序時,遇到的bug和解決辦法,在這篇文章里講述一下。
在YOLOv5之前的YOLOv3和YOLOv4的官方代碼都是基于darknet框架的實現(xiàn)的,因此OpenCV的DNN模塊做目標檢測時,讀取的是.cfg和.weight文件,那時候編寫程序很順暢,沒有遇到bug。
但是YOLOv5的官方代碼(https://github.com/ultralytics/yolov5)是基于Pytorch框架實現(xiàn)的,而OpenCV的DNN模塊不支持讀取Pytorch的訓練模型文件。如果想要把Pytorch的訓練模型.pth文件加載到OpenCV的DNN模塊里,需要先把Pytorch的訓練模型.pth文件轉(zhuǎn)換到.onnx文件,然后才能載入到Opencv的DNN模塊里。
因此,用OpenCV的DNN模塊做YOLOv5目標檢測的程序,包含兩個步驟:
1. 把Pytorch的訓練模型.pth文件轉(zhuǎn)換到.onnx文件。
2. OpenCV的DNN模塊讀取.onnx文件做前向計算。
1. 把Pytorch的訓練模型.pth文件轉(zhuǎn)換到.onnx文件
在做這一步時,我得吐槽一下官方代碼:
https://github.com/ultralytics/yolov5
這套程序里的代碼混亂,在Pytorch里,通常是在.py文件里定義網(wǎng)絡結(jié)構(gòu)的,但是官方代碼是在.yaml文件定義網(wǎng)絡結(jié)構(gòu),利用Pytorch動態(tài)圖特性,解析.yaml文件自動生成網(wǎng)絡結(jié)構(gòu)。在.yaml文件里有depth_multiple和width_multiple,它是控制網(wǎng)絡的深度和寬度的參數(shù)。
這么做的好處是能夠靈活的配置網(wǎng)絡結(jié)構(gòu),但是不利于理解網(wǎng)絡結(jié)構(gòu)。假如你想設斷點查看某一層的參數(shù)和輸出數(shù)值,那就沒辦法了。因此,在我編寫的轉(zhuǎn)換到.onnx文件的程序里,網(wǎng)絡結(jié)構(gòu)是在.py文件里定義的。
其次,在官方代碼里,還有一個奇葩的地方,那就是.pth文件。
起初,我下載官方代碼到本地運行時,torch.load讀取.pth文件總是出錯,后來把pytorch升級到1.7,就讀取成功了。可以看到版本兼容性不好,這是它的一個不足之處。
設斷點查看讀取的.pth文件里的內(nèi)容,可以看到.pth里既存儲有模型參數(shù),也存儲有網(wǎng)絡結(jié)構(gòu),還儲存了一些超參數(shù),包括anchors,stride等等的。第一次見到有這種操作的,通常情況下,.pth文件里只存儲了訓練模型參數(shù)的。
查看models\yolo.py里的Detect類,在構(gòu)造函數(shù)里,有這么兩行代碼:

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

它的作用是做特征圖的輸出通道對齊,通過1x1卷積把三種尺度特征圖的輸出通道都調(diào)整到 num_anchors*(num_classes+5)。
閱讀Detect類的forward函數(shù)代碼,可以看出它的作用是根據(jù)偏移公式計算出預測框的中心坐標和高寬,這里需要注意的是,計算高和寬的代碼:
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
沒有采用exp操作,而是直接乘上anchors[i],這是YOLOv5與YOLOv3v4的一個最大區(qū)別(還有一個區(qū)別就是在訓練階段的loss函數(shù)里,YOLOv5采用鄰域的正樣本anchor匹配策略,增加了正樣本。其它的是一些小區(qū)別,比如YOLOv5的第一個模塊采用FOCUS把輸入數(shù)據(jù)2倍下采樣切分成4份,在channel維度進行拼接,然后進行卷積操作,YOLOv5的激活函數(shù)沒有使用Mish)。
現(xiàn)在可以明白Detect類的作用是計算預測框的中心坐標和高寬,簡單來說就是生成proposal,作為后續(xù)NMS的輸入,進而輸出最終的檢測框。我覺得在Detect類里定義的1x1卷積是不恰當?shù)?,應該把它定義在Detect類的外面,緊鄰著Detect類之前定義1x1卷積。
在官方代碼里,有轉(zhuǎn)換到onnx文件的程序:python models/export.py --weights yolov5s.pt --img 640 --batch 1
在pytorch1.7版本里,程序是能正常運行生成onnx文件的。觀察export.py里的代碼,在執(zhí)行torch.onnx.export之前,有這么一段代碼:

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

由此可見,這段for循環(huán)代碼是必需的。
2. OpenCV的DNN模塊讀取.onnx文件做前向計算
在生成.onnx文件后,就可以用OpenCV的DNN模塊里的cv2.dnn.readNet讀取它。然而,在讀取時,出現(xiàn)了如下錯誤:

我在網(wǎng)頁搜索這個問題的解決辦法,看到一篇技術(shù)文章(https://zhuanlan.zhihu.com/p/286298001),文章里講述的第一條:

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

那么現(xiàn)在需要更換索引式的切片操作,觀察到注釋的Contract類,它就是用view和permute函數(shù)完成切片操作的,于是修改代碼如下:

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

前面說過,Detect類的作用是計算預測框的中心坐標和高寬,生成proposal,這個是屬于后處理的,因此不需要把它寫入到onnx文件里。
總結(jié)一下,按照上面的截圖代碼,修改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函數(shù),用python或者C++編寫計算預測框的中心坐標和高寬的功能。
周末這兩天,我在win10+cpu機器里編寫了用OpenCV的DNN模塊做Yolov5目標檢測的程序,包含Python和C++兩個版本的。程序都調(diào)試通過了,運行結(jié)果也是正確的。
我把這套代碼發(fā)布在了Github上,地址是:
https://github.com/hpc203/yolov5-dnn-cpp-python
后處理模塊,python版本用numpy array實現(xiàn)的,C++版本的用vector和數(shù)組實現(xiàn)的,整套程序只依賴OpenCV庫(opencv4版本以上的)就能正常運行,徹底擺脫對深度學習框架pytorch,tensorflow,caffe,mxnet等等的依賴。
用OpenVINO作目標檢測,需要把onnx文件轉(zhuǎn)換到.bin和.xml文件,相比于用DNN模塊加載onnx文件做目標檢測是多了一個步驟的。因此,我就想編寫一套用OpenCV的DNN模塊做YOLOv5目標檢測的程序,用Opencv的DNN模塊做深度學習目標檢測,在win10和ubuntu,在cpu和gpu上都能運行,可見DNN模塊的通用性更好,很接地氣。
推薦閱讀

