最近在微信公眾號里看到多篇講解yolov5在openvino部署做目標(biāo)檢測文章,但是沒看到過用opencv的dnn模塊做yolov5目標(biāo)檢測的。于是,我就想著編寫一套用opencv的dnn模塊做yolov5目標(biāo)檢測的程序。在編寫這套程序時,遇到的bug和解決辦法,在這篇文章里講述一下。
在yolov5之前的yolov3和yolov4的官方代碼都是基于darknet框架的實現(xiàn)的,因此opencv的dnn模塊做目標(biāo)檢測時,讀取的是.cfg和.weight文件,那時候編寫程序很順暢,沒有遇到bug。但是yolov5的官方代碼(https://github.com/ultralytics/yolov5)是基于pytorch框架實現(xiàn)的,但是opencv的dnn模塊不支持讀取pytorch的訓(xùn)練模型文件的。如果想要把pytorch的訓(xùn)練模型.pth文件加載到opencv的dnn模塊里,需要先把pytorch的訓(xùn)練模型.pth文件轉(zhuǎn)換到.onnx文件,然后才能載入到opencv的dnn模塊里。
因此,用opencv的dnn模塊做yolov5目標(biāo)檢測的程序,包含兩個步驟:(1).把pytorch的訓(xùn)練模型.pth文件轉(zhuǎn)換到.onnx文件。(2).opencv的dnn模塊讀取.onnx文件做前向計算。
(1).把pytorch的訓(xùn)練模型.pth文件轉(zhuǎn)換到.onnx文件
在做這一步時,我得吐槽一下官方代碼:https://github.com/ultralytics/yolov5,這套程序里的代碼混亂,在pytorch里,通常是在.py文件里定義網(wǎng)絡(luò)結(jié)構(gòu)的,但是官方代碼是在.yaml文件定義網(wǎng)絡(luò)結(jié)構(gòu),利用pytorch動態(tài)圖特性,解析.yaml文件自動生成網(wǎng)絡(luò)結(jié)構(gòu)。在.yaml文件里有depth_multiple和width_multiple,它是控制網(wǎng)絡(luò)的深度和寬度的參數(shù)。這么做的好處是能夠靈活的配置網(wǎng)絡(luò)結(jié)構(gòu),但是不利于理解網(wǎng)絡(luò)結(jié)構(gòu),假如你想設(shè)斷點查看某一層的參數(shù)和輸出數(shù)值,那就沒辦法了。因此,在我編寫的轉(zhuǎn)換到.onnx文件的程序里,網(wǎng)絡(luò)結(jié)構(gòu)是在.py文件里定義的。其次,在官方代碼里,還有一個奇葩的地方,那就是.pth文件。起初,我下載官方代碼到本地運行時,torch.load讀取.pth文件總是出錯,后來把pytorch升級到1.7,就讀取成功了??梢钥吹桨姹炯嫒菪圆缓茫@是它的一個不足之處。設(shè)斷點查看讀取的.pth文件里的內(nèi)容,可以看到ultralytics的.pt文件里既存儲有模型參數(shù),也存儲有網(wǎng)絡(luò)結(jié)構(gòu),還儲存了一些超參數(shù),包括anchors,stride等等的。第一次見到有這種操作的,通常情況下,.pth文件里只存儲了訓(xùn)練模型參數(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了,在百度搜索register_buffer,解釋是:pytorch中register_buffer模型保存和加載的時候可以寫入和讀出。
在這兩行代碼的下一行:
它的作用是做特征圖的輸出通道對齊,通過1x1卷積把三種尺度特征圖的輸出通道都調(diào)整到 num_anchors*(num_classes+5)。
閱讀Detect類的forward函數(shù)代碼,可以看出它的作用是根據(jù)偏移公式計算出預(yù)測框的中心坐標(biāo)和高寬,這里需要注意的是,計算高和寬的代碼:
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
沒有采用exp操作,而是直接乘上anchors[i],這是yolov5與yolov3v4的一個最大區(qū)別(還有一個區(qū)別就是在訓(xùn)練階段的loss函數(shù)里,yolov5采用鄰域的正樣本anchor匹配策略,增加了正樣本。其它的是一些小區(qū)別,比如yolov5的第一個模塊采用FOCUS把輸入數(shù)據(jù)2倍下采樣切分成4份,在channel維度進(jìn)行拼接,然后進(jìn)行卷積操作,yolov5的激活函數(shù)沒有使用Mish)。
現(xiàn)在可以明白Detect類的作用是計算預(yù)測框的中心坐標(biāo)和高寬,簡單來說就是生成proposal,作為后續(xù)NMS的輸入,進(jìn)而輸出最終的檢測框。我覺得在Detect類里定義的1x1卷積是不恰當(dāng)?shù)模瑧?yīng)該把它定義在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)代碼是必需的。SiLU其實就是swish激活函數(shù),而在onnx模型里是不直接支持swish算子的,因此在轉(zhuǎn)換生成onnx文件時,SiLU激活函數(shù)不能直接使用nn.Module里提供的接口,而需要自定義實現(xiàn)它。
(2).opencv的dnn模塊讀取.onnx文件做前向計算
在生成.onnx文件后,就可以用opencv的dnn模塊里的cv2.dnn.readNet讀取它。然而,在讀取時,出現(xiàn)了如下錯誤:
我在百度搜索這個問題的解決辦法,看到一篇知乎文章(Pytorch轉(zhuǎn)ONNX-實戰(zhàn)篇2(實戰(zhàn)踩坑總結(jié)) - 知乎),文章里講述的第一條:
于是查看yolov5的代碼,在common.py文件的Focus類,torch.cat的輸入里有4次切片操作,代碼如下:
那么現(xiàn)在需要更換索引式的切片操作,觀察到注釋的Contract類,它就是用view和permute函數(shù)完成切片操作的,于是修改代碼如下:
其次,在models\yolo.py里的Detect類里,也有切片操作,代碼如下:
前面說過,Detect類的作用是計算預(yù)測框的中心坐標(biāo)和高寬,生成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++編寫計算預(yù)測框的中心坐標(biāo)和高寬的功能。
周末這兩天,我在win10+cpu機器里編寫了用opencv的dnn模塊做yolov5目標(biāo)檢測的程序,包含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版本以上的)就能正常運行,徹底擺脫對深度學(xué)習(xí)框架pytorch,tensorflow,caffe,mxnet等等的依賴。用openvino作目標(biāo)檢測,需要把onnx文件轉(zhuǎn)換到.bin和.xml文件,相比于用dnn模塊加載onnx文件做目標(biāo)檢測是多了一個步驟的。因此,我就想編寫一套用opencv的dnn模塊做yolov5目標(biāo)檢測的程序,用opencv的dnn模塊做深度學(xué)習(xí)目標(biāo)檢測,在win10和ubuntu,在cpu和gpu上都能運行,可見dnn模塊的通用性更好,很接地氣。
生成yolov5s_param.pth 的步驟,首先下載https://github.com/ultralytics/yolov5?的源碼到本地,在yolov5-master主目錄(注意不是我發(fā)布的github代碼目錄)里新建一個.py文件,把下面的代碼復(fù)制到.py文件里
import torch
from collections import OrderedDict
import pickle
import os
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if __name__=='__main__':
choices = ['yolov5s', 'yolov5l', 'yolov5m', 'yolov5x']
modelfile = choices[0]+'.pt'
utl_model = torch.load(modelfile, map_location=device)
utl_param = utl_model['model'].model
torch.save(utl_param.state_dict(), os.path.splitext(modelfile)[0]+'_param.pth')
own_state = utl_param.state_dict()
print(len(own_state))
numpy_param = OrderedDict()
for name in own_state:
numpy_param[name] = own_state[name].data.cpu().numpy()
print(len(numpy_param))
with open(os.path.splitext(modelfile)[0]+'_numpy_param.pkl', 'wb') as fw:
????????pickle.dump(numpy_param,?fw)
運行這個.py文件,這時候就可以生成yolov5s_param.pth文件。之所以要進(jìn)行這一步,我在上面講到過:ultralytics的.pt文件里既存儲有模型參數(shù),也存儲有網(wǎng)絡(luò)結(jié)構(gòu),還儲存了一些超參數(shù),包括anchors,stride等等的。torch.load加載ultralytics的官方.pt文件,也就是utl_model = torch.load(modelfile, map_location=device)這行代碼,在這行代碼后設(shè)斷點查看utl_model里的內(nèi)容,截圖如下
可以看到utl_model里含有既存儲有模型參數(shù),也存儲有網(wǎng)絡(luò)結(jié)構(gòu),還儲存了一些超參數(shù)等等的,這會嚴(yán)重影響轉(zhuǎn)onnx文件。此外,我還發(fā)現(xiàn),如果pytorch的版本低于1.7,那么在torch.load加載.pt文件時就會出錯的。
因此在程序里,我把模型參數(shù)轉(zhuǎn)換到cpu.numpy形式的,最后保存在.pkl文件里。這時候在win10系統(tǒng)cpu環(huán)境里,即使你的電腦沒有安裝pytorch,也能通過python程序訪問到模型參數(shù)。
pytorch轉(zhuǎn)onnx常見坑:
- onnx只能輸出靜態(tài)圖,因此不支持if-else分支。一次只能走一個分支。如果代碼中有if-else語句,需要改寫。
- onnx不支持步長為2的切片。例如a[::2,::2]
- onnx不支持對切片對象賦值。例如a[0,:,:,:]=b, 可以用torch.cat改寫
- onnx里面的resize要求output shape必須為常量??梢杂靡韵麓a解決:
if isinstance(size, torch.Size):
??? size = tuple(int(x) for x in size)
此外,在torch.onnx.export(model, inputs, output_onnx)的輸入?yún)?shù)model里,應(yīng)該只包含網(wǎng)絡(luò)結(jié)構(gòu),也就是說model里只含有nn.Conv2d, nn.MaxPool2d, nn.BatchNorm2d, F.relu等等的這些算子組件,而不應(yīng)該含有后處理模塊的。圖像預(yù)處理和后處理模塊需要自己使用C++或者Python編程實現(xiàn)。
在明白了這些之后,在轉(zhuǎn)換生成onnx文件,你需要執(zhí)行兩個步驟,第一步把原始訓(xùn)練模型.pt文件里的參數(shù)保存到新的.pth文件里,第二步編寫yolov5.py文件,把yolov5的往來結(jié)構(gòu)定義在.py文件里,此時需要注意網(wǎng)絡(luò)結(jié)構(gòu)里不能包含切片對象賦值操作,F(xiàn).interpolate里的size參數(shù)需要加int強制轉(zhuǎn)換。在執(zhí)行完這兩步之后才能生成一個opencv能成功讀取并且做前向推理的onnx文件。
不過,最近我發(fā)現(xiàn)在yolov5-pytorch程序里,其實可以直接把原始訓(xùn)練模型.pt文件轉(zhuǎn)換生成onnx文件的,而且我在一個yolov5檢測人臉+關(guān)鍵點的程序里實驗成功了。
這套程序發(fā)布在github上,地址是 :
https://github.com/hpc203/yolov5-face-landmarks-opencv
https://github.com/hpc203/yolov5-face-landmarks-opencv-v2
這套程序只依賴opencv庫就可以運行yolov5檢測人臉+關(guān)鍵點,程序依然是包含C++和Python兩個版本的,這套程序里還有一個轉(zhuǎn)換生成onnx文件的python程序文件。只需運行這一個.py文件就可以生成onnx文件,而不需要之前講的那樣執(zhí)行兩個步驟,這樣大大簡化了生成onnx文件的流程,使用方法可以閱讀程序里的README文檔。
在這個新的轉(zhuǎn)換生成onnx文件的程序里,需要重新定義yolov5網(wǎng)絡(luò)結(jié)構(gòu),主要是修改第一個模塊Focus,用Contract類替換索引式的切片操作,在最后一個模塊Detect類里,只保留三個1x1卷積,剩下的make_grid和decode屬于后處理,不能包含在網(wǎng)絡(luò)結(jié)構(gòu)里,代碼截圖如下
如果要轉(zhuǎn)換生成onnx文件,需要設(shè)置export = True,這時候Detect模塊的forward就只進(jìn)行1x1卷積,這時的網(wǎng)絡(luò)結(jié)構(gòu)就可以作為torch.onnx.export(model, inputs, output_onnx)的輸入?yún)?shù)model。不過由于ultralytics的yolov5代碼倉庫幾乎每天都在更新,因此你現(xiàn)在看到的ultralytics的yolov5里的Detect類很有可能不是這么寫的,那這是需要你手動修改程序,然后再運行。
看到最近曠視發(fā)布的anchor-free系列的YOLOX,而在github開源的代碼里,并沒有使用opencv部署的程序。因此,我就編寫了一套使用OpenCV部署YOLOX的程序,支持YOLOX-S、YOLOX-M、YOLOX-L、YOLOX-X、YOLOX-Darknet53五種結(jié)構(gòu),包含C++和Python兩種版本的程序?qū)崿F(xiàn)。在今天我在github發(fā)布了這套程序,地址是
https://github.com/hpc203/yolox-opencv-dnn
在曠視發(fā)布的YOLOX代碼里,提供了在COCO數(shù)據(jù)集上訓(xùn)練出來的.pth模型文件,并且也提供了導(dǎo)出onnx模型的export_onnx.py文件,起初我運行export_onnx.py生成onnx文件之后Opencv讀取onnx文件失敗了,報錯原因跟文章最開始的第(2)節(jié)里的一樣,這說明在YOLOX的網(wǎng)絡(luò)結(jié)構(gòu)里有切片操作,經(jīng)過搜索后,在 yolox\models\network_blocks.py 里有個Focus類,它跟YOLOv5里的Focus是一樣的,都是把輸入張量切分成4份,然后concat+conv。這時按照第(2)節(jié)里講述的解決辦法,修改Focus類,重新運行export_onnx.py生成onnx文件,Opencv讀取onnx文件就不會再出錯了。
在github發(fā)布了一套使用OpenCV部署Yolo-FastestV2的程序,依然是包含C++和Python兩種版本的程序?qū)崿F(xiàn)。地址是
https://github.com/hpc203/yolo-fastestv2-opencv
經(jīng)過運行,體驗到這個Yolo-FastestV2的速度確實很快,而且onnx文件只有957kb大小,不超過1M。在官方代碼https://github.com/dog-qiuqiu/Yolo-FastestV2里,學(xué)習(xí)它的網(wǎng)絡(luò)結(jié)構(gòu)。設(shè)斷點調(diào)試,查看中間變量可以看到,在model/detector.py,網(wǎng)絡(luò)輸出了6個張量
它們的形狀分別是
torch.Size([1, 12, 22, 22])
torch.Size([1, 3, 22, 22])
torch.Size([1, 80, 22, 22])
torch.Size([1, 12, 11, 11])
torch.Size([1, 3, 11, 11])
torch.Size([1, 80, 11, 11])
結(jié)合配置文件data/coco.data,可以看到模型輸入是352x352的圖片,而輸出有22x22和11x11這兩種尺度的特征圖,這說明Yolo-FastestV2的輸出只有縮放16倍和縮放32倍這兩種尺度的特征圖,比yolov3,v4,v5系列的都要少一個尺度特征圖。其次在配置文件data/coco.data還可以看到anchor一共有6個,分別給兩個尺度特征圖里的網(wǎng)格點分配3個。觀察輸出的6個張量的形狀信息,很明顯前3個張量是22x22尺度特征圖的檢測框坐標(biāo)回歸量bbox_reg,檢測框目標(biāo)置信度obj_conf,檢測框類別置信度cls_conf。由于給每個網(wǎng)格點分配3個anchor,檢測框坐標(biāo)包含(center_x, center_y, width, height),因此維數(shù)是43=12,這也就明白了bbox_reg的第1個維度是12,obj_conf的第1個維度是3,而COCO數(shù)據(jù)集有80類,那么cls_conf的第1個維度應(yīng)該是380=240,但是在上面調(diào)試信息里顯示的是80類。繼續(xù)設(shè)斷點調(diào)試代碼,在utils/utils.py里,第326行有這么一行代碼
類別置信度復(fù)制了3份,結(jié)合這個后處理代碼,可以看出類別置信度對3個anchor是共享的。
在觀察出Yolo-FastestV2的這些特性之后,可以理解為何它的速度快和模型文件小的原因了。主要是因為它的輸入圖片尺寸比傳統(tǒng)yolov3,v4,v5系列的要小,它的輸出特征圖尺寸個數(shù),也比傳統(tǒng)yolo的要少,最后對網(wǎng)格點上的3個anchor是共享類別置信度的,這也減少了特種通道數(shù)。
8月29日,我在github發(fā)布了一套使用OpenCV部署全景駕駛感知網(wǎng)絡(luò)YOLOP,可同時處理交通目標(biāo)檢測、可駕駛區(qū)域分割、車道線檢測,三項視覺感知任務(wù),依然是包含C++和Python兩種版本的程序?qū)崿F(xiàn)。地址是:
https://github.com/hpc203/YOLOP-opencv-dnn
在這里我講一下生成onnx文件需要注意的地方,YOLOP的官方代碼地址是 https://github.com/hustvl/YOLOP ?,它是華中科技大學(xué)視覺團(tuán)隊發(fā)布的,它的代碼是使用pytorch作為深度學(xué)習(xí)框架。仔細(xì)閱讀和運行調(diào)試他的代碼,可以看出,它的代碼是在ultralytics的yolov5里修改的,添加了可行駛區(qū)域分割和車道線分割這兩個分割頭,在bdd100k數(shù)據(jù)集上的訓(xùn)練的,不過YOLOP的檢測類別只保留了bdd100k數(shù)據(jù)集里的車輛這一個類別。生成onnx文件,第一步是把我發(fā)布的代碼里的export_onnx.py拷貝到https://github.com/hustvl/YOLOP的主目錄里。第二步,在https://github.com/hustvl/YOLOP的主目錄里,打開lib/models/common.py,首先修改Focus類,原始的Focus類的forward函數(shù)里是由切片操作的,那么這時按照第(2)節(jié)里講述的解決辦法,修改Focus類,示例代碼如下
class Contract(nn.Module):
# Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40)
def __init__(self, gain=2):
super().__init__()
self.gain = gain
def forward(self, x):
N, C, H, W = x.size() # assert (H / s == 0) and (W / s == 0), 'Indivisible gain'
s = self.gain
x = x.view(N, C, H // s, s, W // s, s) # x(1,64,40,2,40,2)
x = x.permute(0, 3, 5, 1, 2, 4).contiguous() # x(1,2,2,64,40,40)
return x.view(N, C * s * s, H // s, W // s) # x(1,256,40,40)
class Focus(nn.Module):
# Focus wh information into c-space
# slice concat conv
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Focus, self).__init__()
self.conv = Conv(c1 * 4, c2, k, s, p, g, act)
self.contract = Contract(gain=2)
def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2)
# return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
return self.conv(self.contract(x))
接下來修改Detect類里的forward函數(shù),示例代碼如下
def forward(self, x):
if not torch.onnx.is_in_onnx_export():
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
# print(str(i)+str(x[i].shape))
bs, _, ny, nx = x[i].shape # x(bs,255,w,w) to x(bs,3,w,w,85)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
# print(str(i)+str(x[i].shape))
if not self.training: # inference
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
y = x[i].sigmoid()
# print("**")
# print(y.shape) #[1, 3, w, h, 85]
# print(self.grid[i].shape) #[1, 3, w, h, 2]
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i].to(x[i].device)) * self.stride[i] # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
"""print("**")
print(y.shape) #[1, 3, w, h, 85]
print(y.view(bs, -1, self.no).shape) #[1, 3*w*h, 85]"""
z.append(y.view(bs, -1, self.no))
return x if self.training else (torch.cat(z, 1), x)
else:
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
# print(str(i)+str(x[i].shape))
bs, _, ny, nx = x[i].shape # x(bs,255,w,w) to x(bs,3,w,w,85)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
x[i] = torch.sigmoid(x[i])
x[i] = x[i].view(-1, self.no)
????????return?torch.cat(x,?dim=0)
RUBY 復(fù)制 全屏
修改完之后,運行export_onnx.py就能生成onnx文件,并且opencv讀取正常的。
9月18日,我在github上發(fā)布了一套使用ONNXRuntime部署anchor-free系列的YOLOR,依然是包含C++和Python兩種版本的程序。起初我是想使用OpenCV部署的,但是opencv讀取onnx文件總是出錯,于是我換用ONNXRuntime部署。地址是:文章來源:http://www.zghlxwxcb.cn/news/detail-787287.html
https://github.com/hpc203/yolor-onnxruntime文章來源地址http://www.zghlxwxcb.cn/news/detail-787287.html
到了這里,關(guān)于用opencv的DNN模塊做Yolov5目標(biāo)檢測(純干貨,源碼已上傳Github)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!