
YOLOv5 Lite在YOLOv5的基礎上進行一系列消融實驗,使其更輕(Flops更小,內存占用更低,參數更少),更快(加入shuffle channel,yolov5 head進行通道裁剪,在320的input_size至少能在樹莓派4B上的推理速度可以達到10+FPS),更易部署(摘除Focus層和4次slice操作,讓模型量化精度下降在可接受範圍內)。
1輸入端方法1.1、Mosaic數據增強YOLOv5 Lite的輸入端採用了和YOLOv5、YOLOv4一樣的Mosaic數據增強的方式。其實Mosaic數據增強的作者也是來自YOLOv5團隊的成員,不過,隨機縮放、隨機裁剪、隨機排布的方式進行拼接,對於小目標的檢測效果還是很不錯的。

為什麼要進行Mosaic數據增強呢?
在平時訓練模型時,一般來說小目標的AP比中目標和大目標低很多。而Coco數據集中也包含大量的小目標,但比較麻煩的是小目標的分布並不均勻。
首先看下小、中、大目標的定義:

可以看到小目標的定義是目標框的長寬0×0~32×32之間的物體。

但在整體的數據集中,小、中、大目標的占比並不均衡。
如上表所示,Coco數據集中小目標占比達到41.4%,數量比中目標和大目標都要多。但在所有的訓練集圖片中,只有52.3%的圖片有小目標,而中目標和大目標的分布相對來說更加均勻一些。
針對上述狀況採用了Mosaic數據增強的方式,主要有幾個優點:
隨機使用4張圖片,隨機縮放,再隨機分布進行拼接,大大豐富了檢測數據集,特別是隨機縮放增加了很多小目標,讓網絡的魯棒性更好。
可能會有人說,隨機縮放和普通的數據增強也可以做到類似的效果,在同等size的輸入下,普通的數據增強只能看到一張圖像,而Mosaic增強訓練時可以直接計算4張圖片的數據,這樣即使一個GPU也可以達到比較好的效果。
1.2、自適應Anchor計算YOLOv5 Lite依舊沿用YOLOv5的Anchor計算方式,我們知道,在YOLO算法之中,針對不同的數據集,都會設置固定的Anchor。
在網絡訓練中,網絡在初始錨框的基礎上輸出預測框,進而和Ground Truth進行比對,計算兩者差距,再反向更新,迭代網絡參數。
可以看出Anchor也是比較重要的一部分,比如Yolov5在Coco數據集上初始設定的錨框:

第1行是在最大的特徵圖上的錨框;第2行是在中間的特徵圖上的錨框;第3行是在最小的特徵圖上的錨框;
自適應計算Anchor的流程如下:將每張圖片中wh的最大值等比例縮放到指定大小img_size,較小邊也相應縮放;將bboxes從相對坐標改成絕對坐標(乘以縮放後的wh);篩選bboxes,保留wh都大於等於兩個像素的bboxes;使用k-means聚類得到n個anchors(掉k-means包 涉及一個白化操作);使用遺傳算法隨機對anchors的wh進行變異,如果變異後效果變得更好(使用anchor_fitness方法計算得到的fitness(適應度)進行評估)就將變異後的結果賦值給anchors,如果變異後效果變差就跳過,默認變異1000次;1.3 自適應縮放圖片在常用的目標檢測算法中,不同的圖片長寬都不相同,因此常用的方式是將原始圖片統一縮放到一個標準尺寸,再送入檢測網絡中。比如Yolo算法中常用416×416,608×608等尺寸。
但Yolov5代碼中對此進行了改進,也是Yolov5推理速度能夠很快的一個不錯的trick。作者認為,在項目實際使用時,很多圖片的長寬比不同。因此縮放填充後,兩端的黑邊大小都不同,而如果填充的比較多,則存在信息冗餘,影響推理速度。
圖像高度上兩端的黑邊變少了,在推理時,計算量也會減少,即目標檢測速度會得到提升。

下面根據上圖進行計算一下,主要是展示推理時的計算:
原始圖像的尺寸為640×427,與640的輸入尺寸計算得到2個縮放係數分別為1.0和1.499,這裡選擇較小的1.0參與縮放計算;
這裡將原始尺寸乘以縮放係數1.0,可以分別得到長寬為640,427
640-427=213,得到原本需要填充的高度。再採用numpy中np.mod取餘數的方式,得到21個像素,再除以2,即得到圖片高度兩端需要填充的數值(為10【向上取整】和11【向下取整】),於是得到推理結果的尺寸為640×448。
注意只是在測試,使用模型推理時,才採用縮減灰邊的方式,提高目標檢測,推理的速度。為什麼np.mod函數的後面用32?因為Yolov5的網絡經過5次下採樣,而2的5次方,等於32。所以至少要去掉32的倍數,以免產生尺度太小走不完stride的問題,再進行取余。
2 模型架構
2.1 去除Focus層為了充分理解,先來回顧一下Focus這個OP吧:
class Focus(nn.Module): # Focus wh information into c-space def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) 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))從直觀理解,其實就是將圖像進行切片,類似於下採樣取值,這樣得到4個圖像:

相當於是將空間信息繞到了通道信息中,cat後輸入通道變成4倍,再通過conv得到新的featuremap,這樣做的好處就是保持了下採樣的信息沒有丟失,圖像的信息都保留了下來,但是在淺層中的應用,作者也表示了單純是從計算量和參數量的角度上去設計,因為如上的所述的信息保存在淺層的意義並不大。所以後來查了下,作者的設計原因就是:為了減少浮點數和提高速度,而不是增加featuremap。
故,1個Focus可以替代更多的卷積層,而將空間信息聚焦到通道空間,這也會減少1像素的回歸精度,而大部分的檢測回歸精度都不會接近1,這也是為什麼Focus要放在輸入的第1層上的原因。
一句話解釋:Focus為了壓縮網絡層去提速。但是在YOLOv5 V6.0中,作者經過試驗後,結論就是使用卷積替代Focus獲得了更好的性能,且沒有之前的一些局限性和副作用,因此此後的迭代中YOLO v5均去除了Focus操作。
YOLOv5 Lite也在此之前也不約而同的選擇了摘除Focus層,避免多次採用slice操作,對於的芯片,特別是不含GPU、NPU加速的芯片,頻繁的slice操作只會讓緩存占用嚴重,加重計算處理的負擔。同時,在芯片部署的時候,Focus層的轉化對新手極度不友好。
2.2 ShuffleNet BackboneYOLOv5 Lite的Backbone選擇的是ShuffleNet,為什麼是ShuffleNet呢?
這裡給出輕量化設計的4個準則:
不能忽略元素級操作(比如shortcut和Add)。同時,YOLOv5 Lite避免多次使用C3 Leyer以及高通道的C3 Layer
C3 Leyer是YOLOv5作者提出的CSPBottleneck改進版本,它更簡單、更快、更輕,在近乎相似的損耗上能取得更好的結果。但C3 Layer採用多路分離卷積,測試證明,頻繁使用C3 Layer以及通道數較高的C3 Layer,占用較多的緩存空間,減低運行速度。
為什麼通道數越高的C3 Layer會對cpu不太友好?,主要還是因為Shufflenetv2的G1準則,通道數越高,hidden channels與c1、c2的階躍差距更大,來個不是很恰當的比喻,想象下跳一個台階和十個台階,雖然跳十個台階可以一次到達,但是你需要助跑,調整,蓄力才能跳上,可能花費的時間更久

針對ShuffleNet v2,作者首先復盤了ShuffleNetV1的問題,認為目前比較關鍵的問題是如何在全卷積或者分組卷積中維護大多數的卷積是平衡的。針對這個目標,作者提出了Channel Split的操作,同時構建了ShuffleNetV2。

上圖中(a)(b)是ShuffleNetV1的結構,而後面的(c)(d)是ShuffleNetV2的層結構,也是YOLOv5 Lite中的主要結構,分別對應的是結構圖中的SFB1_X和SFB2_X


下面稍微講一下筆者結合論文的理解:
Channel Split操作將整個特徵圖分為c』組(假設為A組)和c-c』(假設為B組)兩個部分,主要有3個好處:
整個特徵圖分為2個組了,但是這樣的分組又不像分組卷積一樣,增加了卷積時的組數,符合準則2;這樣分開之後,將A組認為是通過short-cut通道的,而B組經過的bottleneck層的輸入輸出的通道數就可以保持一致,符合準則1;同時由於最後使用的concat操作,沒有用TensorAdd操作,符合準則4;可以看到,這樣一個簡單的通道分離的操作帶來了諸多好處;但是從理論上來說,這樣的結構是否還符合short-cut的初衷(即bottleneck學到的是殘差Residual部分)?這裡筆者也不好妄加揣測,但是可以想到的是經過後面的Channel Shuffle的亂序之後,每個通道應該都會經過一次bottleneck結構。上述的結構是不改變輸入輸出通道數和特徵圖大小的情況,而池化操作使用圖(d)代替了,跟ShuffleNetV1類似,經過這樣的結構之後,圖像通道數擴張為原先的2倍。
YOLO v5 Lite在Backbone中還摘除shufflenetv2 backbone的1024 conv 和 5×5 pooling。
2.3 Neck在目標檢測領域,為了更好的提取融合特徵,通常在Backbone和輸出層,會插入一些層,這個部分稱為Neck。相當於目標檢測網絡的頸部,也是非常關鍵的。
而YOLO v5 Lite也不例外的使用了FPN+PAN的結構,但是Lite對yolov5 head進行通道剪枝,剪枝細則參考了ShuffleNet v2的設計準則,同時改進了YOLOv4中的FPN+PAN的結構,具體就是:
為了最優化內存的訪問和使用,選擇了使用相同的通道數量(e模型Neck通道為96);為了進一步優化內存的使用,選擇了使用原始的PANet結構,還原YOLOv4的cat操作為add操作;


這樣結合操作,FPN層自頂向下傳達強語義特徵(High-Level特徵),而特徵金字塔則自底向上傳達強定位特徵(Low-Level特徵),兩兩聯手,從不同的主幹層對不同的檢測層進行特徵聚合。
FPN+PAN借鑑的是18年CVPR的PANet,當時主要應用於圖像分割領域,但Alexey將其拆分應用到Yolov4中,進一步提高特徵提取的能力。
2.4 Head對於Head部分,YOLO V5 Lite並沒有對YOLOv5進行改進,所以可以看到三個紫色箭頭處的特徵圖是40×40、20×20、10×10。以及最後Prediction中用於預測的3個特徵圖:
①==>40×40×255②==>20×20×255③==>10×10×255
2.5 Anchor機制及坐標變換1、Anchor機制對於YOLOv5,Anchor對應與Yolov3則恰恰相反,對於所設置的Anchor:
第一個Yolo層是最大的特徵圖40×40,對應最小的anchor box。第二個Yolo層是中等的特徵圖20×20,對應中等的anchor box。第三個Yolo層是最小的特徵圖10×10,對應最大的anchor box。# anchors:# - [10,13, 16,30, 33,23] # P3/8# - [30,61, 62,45, 59,119] # P4/16# - [116,90, 156,198, 373,326] # P5/322、樣本匹配策略在yolo v3&v4中,Anchor匹配策略和SSD、Faster RCNN類似:保證每個gt bbox有一個唯一的Anchor進行對應,匹配規則就是IOU最大,並且某個gt不能在三個預測層的某幾層上同時進行匹配。不考慮一個gt bbox對應多個Anchor的場合,也不考慮Anchor是否設置合理。
這裡先說一下YOLOv3的匹配策略:
假設一個圖中有一個目標,這個被分割成三種格子的形式,分割成13×13 、26 × 26、52 × 52 。
這個目標中心坐標下採樣8倍,(416/8=52),會落在 52 × 52 這個分支的所有格子中的某一個格子,落在的格子會產生3個anchor,3個anchor和目標(已經下採樣8倍的目標框)分別計算iou,得到3個iou,凡是iou大於閾值0.3的,就記為正樣本,就會將label[0]中這個iou大於0.3的anchor的相應位置 賦上真實框的值。這個目標中心坐標下採樣16倍,(416/16=26),會落在 26 × 26 這個分支的所有格子中的某一個格子,落在的格子會產生3個anchor,3個anchor和目標(已經下採樣16倍的目標框)分別計算iou,得到三個iou,凡是iou大於閾值0.3的,就記為正樣本,就會將label[1]中這個iou大於0.3的anchor的相應位置 賦上真實框的值。這個目標中心坐標下採樣32倍,(416/32=13),會落在 13 × 13 這個分支的所有格子中的某一個格子,落在的格子會產生3個anchor,3個anchor和目標(已經下採樣32倍的目標框)分別計算iou,得到三個iou,凡是iou大於閾值0.3的,就記為正樣本,就會將label[2]中這個iou大於0.3的anchor的相應位置 賦上真實框的值。如果目標所有的anchor,9個anchor,iou全部小於閾值0.3,那麼選擇9個anchor中和下採樣後的目標框iou最大的,作為正樣本,將目標真實值賦值給相應的anchor的位置。總的來說,就是將目標先進行3種下採樣,分別和目標落在的網格產生的 9個anchor分別計算iou,大於閾值0.3的記為正樣本。如果9個iou全部小於0.3,那麼和目標iou最大的記為正樣本。對於正樣本,我們在label上 相對應的anchor位置上,賦上真實目標的值。
而yolov5採用了跨網格匹配規則,增加正樣本Anchor數目的做法:
對於任何一個輸出層,yolov5拋棄了Max-IOU匹配規則而採用shape匹配規則,計算標籤box和當前層的anchors的寬高比,即:wb/wa,hb/ha。如果寬高比大於設定的閾值說明該box沒有合適的anchor,在該預測層之間將這些box當背景過濾掉。
# r為目標wh和錨框wh的比值,比值在0.25到4之間的則採用該種錨框預測目標r = t[:, :, 4:6] / anchors[:, None] # wh ratio:計算標籤box和當前層的anchors的寬高比,即:wb/wa,hb/ha# 將比值和預先設置的比例anchor_t對比,符合條件為True,反之Falsej = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare對於剩下的bbox,計算其落在哪個網格內,同時利用四捨五入規則,找出最近的2個網格,將這3個網格都認為是負責預測該bbox的,可以發現粗略估計正樣本數相比前yolo系列,增加了3倍。code如下:
# Offsets# 得到相對於以左上角為坐標原點的坐標gxy = t[:, 2:4] # grid xy# 得到相對於右下角為坐標原點的坐標gxi = gain[[2, 3]] - gxy # inverse# 這兩個條件可以用來選擇靠近的兩個鄰居網格# jk和lm是判斷gxy的中心點更偏向哪裡j, k = ((gxy % 1 < g) & (gxy > 1)).Tl, m = ((gxi % 1 < g) & (gxi > 1)).Tj = torch.stack((torch.ones_like(j), j, k, l, m))# yolov5不僅用目標中心點所在的網格預測該目標,還採用了距目標中心點的最近兩個網格# 所以有五種情況,網格本身,上下左右,這就是repeat函數第一個參數為5的原因t = t.repeat((5, 1, 1))[j]# 這裡將t複製5個,然後使用j來過濾# 第一個t是保留所有的gtbox,因為上一步裡面增加了一個全為true的維度,# 第二個t保留了靠近方格左邊的gtbox,# 第三個t保留了靠近方格上方的gtbox,# 第四個t保留了靠近方格右邊的gtbox,# 第五個t保留了靠近方格下邊的gtbox,offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]對於YOLOv5,不同於yolov3,yolov4的是:其gt box可以跨層預測,即有些gt box在多個預測層都算正樣本;同時其gt box可匹配的anchor數可為3~9個,顯著增加了正樣本的數量。不再是gt box落在那個網格就只由該網格內的anchor來預測,而是根據中心點的位置增加2個鄰近的網格的anchor來共同預測。
如下圖所示,綠點表示該gt bbox中心,現在需要額外考慮其2個最近的鄰域網格的anchor也作為該gt bbox的正樣本,明顯增加了正樣本的數量。
# 輸入參數pred為網絡的預測輸出,它是一個list包含三個檢測頭的輸出tensor。def build_targets(self, p, targets): ''' build_targets()函數的作用:找出與該gtbox最匹配的先驗框(anchor) ''' # 這裡na為Anchor框種類數 nt為目標數 這裡的na為3,nt為2 na, nt = self.na, targets.shape[0] # number of anchors, targets # 類別 邊界框 索引 錨框 tcls, tbox, indices, anch = [], [], [], [] # 利用gain來計算目標在某一個特徵圖上的位置信息,初始化為1 gain = torch.ones(7, device=targets.device) # normalized to gridspace gain # 第2個維度複製nt遍 ai = torch.arange(na, device=targets.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt) # targets.shape = (na, nt, 7)(3,2,7)給每個目標加上Anchor框索引 targets = torch.cat((targets.repeat(na, 1, 1), ai[:, :, None]), 2) # append anchor indices g = 0.5 # bias # 上下左右4個網格 off = torch.tensor([[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m ], device=targets.device).float() * g # offsets # 處理每個檢測層(3個) for i in range(self.nl): ''' tensor([[[ 1.25000, 1.62500], #10,13, 16,30, 33,23 每一個數除以8 [ 2.00000, 3.75000], [ 4.12500, 2.87500]], [[ 1.87500, 3.81250], #30,61, 62,45, 59,119 每一個數除以16 [ 3.87500, 2.81250], [ 3.68750, 7.43750]], [[ 3.62500, 2.81250], #116,90, 156,198, 373,326 每一個數除以32 [ 4.87500, 6.18750], [11.65625, 10.18750]]]) ''' # 3個anchors,已經除以當前特徵圖對應的stride anchors = self.anchors[i] # 將target中歸一化後的xywh映射到3個尺度(80,80, 40,40, 20,20)的輸出需要的放大係數 gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain # 將xywh映射到當前特徵圖,即乘以對應的特徵圖尺寸,targets*gain將歸一化的box乘以特徵圖尺度,將box坐標分別投影到對應的特徵圖上 t = targets * gain if nt: # r為目標wh和錨框wh的比值,比值在0.25到4之間的則採用該種錨框預測目標 # 計算當前tartget的wh和anchor的wh比值 # 如果最大比值大於預設值model.hyp['anchor_t']=4,則當前target和anchor匹配度不高,不強制回歸,而把target丟棄 r = t[:, :, 4:6] / anchors[:, None] # wh ratio:計算標籤box和當前層的anchors的寬高比,即:wb/wa,hb/ha # 篩選滿足條件1/hyp['anchor_t] < target_wh / anchor_wh < hyp['anchor_t]的框 #.max(2)對第3維度的值進行max,將比值和預先設置的比例anchor_t對比,符合條件為True,反之False j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare # 根據j篩選符合條件的坐標 t = t[j] # filter # Offsets # 得到相對於以左上角為坐標原點的坐標 gxy = t[:, 2:4] # grid xy # 得到相對於右下角為坐標原點的坐標 gxi = gain[[2, 3]] - gxy # inverse # 這2個條件可以用來選擇靠近的2個臨近網格 # jk和lm是判斷gxy的中心點更偏向哪裡(如果中心點的相對左上角的距離大於1,小於1.5,則滿足臨近選擇的條件) j, k = ((gxy % 1 < g) & (gxy > 1)).T # jk和lm是判斷gxi的中心點更偏向哪裡(如果中心點的相對右下角的距離大於1,小於1.5,則滿足臨近選擇的條件) l, m = ((gxi % 1 < g) & (gxi > 1)).T j = torch.stack((torch.ones_like(j), j, k, l, m)) # yolov5不僅用目標中心點所在的網格預測該目標,還採用了距目標中心點的最近兩個網格 # 所以有五種情況,網格本身,上下左右,這就是repeat函數第一個參數為5的原因 t = t.repeat((5, 1, 1))[j] # 這裡將t複製5個,然後使用j來過濾 # 第1個t是保留所有的gtbox,因為上一步裡面增加了一個全為true的維度, # 第2個t保留了靠近方格左邊的gtbox, # 第3個t保留了靠近方格上方的gtbox, # 第4個t保留了靠近方格右邊的gtbox, # 第5個t保留了靠近方格下邊的gtbox, offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j] else: t = targets[0] offsets = 0 """ 對每個bbox找出對應的正樣本anchor。 a 表示當前bbox和當前層的第幾個anchor匹配 b 表示當前bbox屬於batch內部的第幾張圖片, c 是該bbox的類別 gi,gj 是對應的負責預測該bbox的網格坐標 gxy 負責預測網格中心點坐標xy gwh 是對應的bbox的wh """ b, c = t[:, :2].long().T # image, class b表示當前bbox屬於該batch內第幾張圖片 gxy = t[:, 2:4] # grid xy真實目標框的xy坐標 gwh = t[:, 4:6] # grid wh真實目標框的寬高 gij = (gxy - offsets).long() #取整 gi, gj = gij.T # grid xy indices (gi,gj)是計算出來的負責預測該gt box的網格的坐標 # Append a = t[:, 6].long() # anchor indices a表示當前gt box和當前層的第幾個anchor匹配上了 indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices tbox.append(torch.cat((gxy - gij, gwh), 1)) # gtbox與3個負責預測的網格的坐標偏移量 anch.append(anchors[a]) # anchors tcls.append(c) # class return tcls, tbox, indices, anch最後返回四個列表:
tbox:gtbox與3個負責預測的網格的xy坐標偏移量,gtbox的寬高indices:b表示當前gtbox屬於該batch內第幾張圖片,a表示gtbox與anchors的對應關係,負責預測的網格縱坐標,負責預測的網格橫坐標yolov5增加正樣本的方法,最多可增大到原來的3倍,大大增加了正樣本的數量,加速了模型的收斂。目標檢測重中之重可以理解為Anchor的匹配策略,當下流行的Anchor-Free不過換了一種匹配策略罷了。當下真正可創新之處在於更優的匹配策略。
3、正樣本個數的增加策略yolov5共有3個預測分支(FPN、PAN結構),共有9種不同大小的anchor,每個預測分支上有3種不同大小的anchor。Yolov5算法通過以下3種方法大幅增加正樣本個數:
跨預測分支預測:假設一個ground truth框可以和2個甚至3個預測分支上的anchor匹配,則這2個或3個預測分支都可以預測該ground truth框,即一個ground truth框可以由多個預測分支來預測。跨網格預測:假設一個ground truth框落在了某個預測分支的某個網格內,則該網格有左、上、右、下4個鄰域網格,根據ground truth框的中心位置,將最近的2個鄰域網格也作為預測網格,也即一個ground truth框可以由3個網格來預測;跨anchor預測:假設一個ground truth框落在了某個預測分支的某個網格內,該網格具有3種不同大小anchor,若ground truth可以和這3種anchor中的多種anchor匹配,則這些匹配的anchor都可以來預測該ground truth框,即一個ground truth框可以使用多種anchor來預測。4、坐標變換對於之前的YOLOv3和YOLOv4,使用的是如下圖所示的坐標表示形式:


、、、分別是即邊界框bbox相對於feature map的位置和寬高;
和分別代表feature map中grid cell的左上角坐標,在yolo中每個grid cell在feature map中的寬和高均為1;
和分別代表Anchor映射到feature map中的的寬和高,anchor box原本設定是相對於坐標系下的坐標,需要除以stride如32映射到feature map坐標系中;
、、、這4個參數化坐標是網絡學習的目標,其中,是預測的坐標偏移值,和是尺度縮放,sigma代表sigmoid函數。
與faster rcnn和ssd中的參數化坐標不同的是x和y的回歸方式,YOLO v3&v4使用了sigmoid函數進行偏移量的規則化,而faster和ssd中對x,y除以anchor的寬和高進行規則化。
YOLOv5參數化坐標的方式和yolo v3&v4是不一樣的,如下:

用公式表示如下:
xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xywh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh這樣,pxy的取值範圍是[-0.5,1.5],pwh的取值範圍是(0,4×anchors[i]),這是因為採用了跨網格匹配規則,要跨網格預測了。
為什麼這麼改造呢?
可以看出,對於不同的和,當他們大於零比較多時,YOLOv5的反饋更加平滑,相對於v3、v4也就更容易收斂。
5、為什麼進行坐標參數化?為什麼要學習偏移而不是實際值?Anchor已經粗略地「框住了」輸入圖像中的目標,明顯的一個問題是,框的不夠準確。因為受限於Anchor的生成方式,Anchor的坐標永遠都是固定的那幾個。所以,如果我們預測相對於Anchor的offset,那麼,就可以通過預測的offset調整錨框位置,從而得到更精準的bounding box。
為什麼要學習偏移係數而不是偏移量?首先,對於預測的bounding box的w和h可以通過anchor進行縮放,但有一個基本的要求,就是h和w都必須為正值,而網絡最後一層的預測輸出是沒法保證正負的,所以最簡單的方法就是對預測輸出求exp,這樣就保證了預測值恆為正。那麼反過來,對預測目標就是求log。
其次,對cx和cy除以anchor的寬和高的處理是為了做尺度歸一化。例如,大的box的絕對偏移量一般較大,而小的box的絕對偏移量一般較小,除以寬和高消除這種影響。即兩個框大小不一,但相對值卻一致。
為什麼都要進行Sigmoid計算?yolov5需要的訓練數據的label是根據原圖尺寸歸一化了的,這樣做是因為怕大的邊框的影響比小的邊框影響大,因此做了歸一化的操作,這樣大的和小的邊框都會被同等看待了,而且訓練也容易收斂。所以在網絡輸出的部分也需要對輸出進行歸一化操作,因此選擇了Sigmoid計算。
3 輸出端3.1 優化方法YOLO V5的作者提供了2個優化函數Adam和SGD,並都預設了與之匹配的訓練超參數。默認為SGD。YOLO V4使用SGD。
YOLO V5的作者建議是,如果需要訓練較小的自定義數據集,Adam是更合適的選擇,儘管Adam的學習率通常比SGD低。但是如果訓練大型數據集,對於YOLOV5來說SGD效果比Adam好。
實際上學術界上對於SGD和Adam哪個更好,一直沒有統一的定論,取決於實際項目情況。
3.2 損失函數
通過上圖我們可以看到,對於圖中的目標,都會輸出class_num+4+1長度的向量,比如針對coco數據集有80個類別,就會輸出長度為85的特徵向量,其中所包含的內容如下圖所示:

圖中向量包含4個坐標信息,一個包含目標概率和80個類別得分,換句話解釋就是「這個圖像中是否有目標(物體出現的概率)?有的話是什麼(80類的類別得分)?然後就是這個目標物體在哪裡(box坐標位置)?」
其實面對上述的3個輸出,也對應YOLOv5的3個分支的,其分別是obj分支、cls分支和box分支。
1、obj分支obj分支輸出的是該anchor中是否含有物體的概率,默認使用BCEWithLogits Loss。
BCEWithLogitsLoss是將BCELoss(BCE:Binary cross entropy)和sigmoid融合了,也就是說省略了sigmoid這個步驟;BCELoss的數學公式如下:
class BCEBlurWithLogitsLoss(nn.Module): # BCEwithLogitLoss() with reduced missing label effects. def __init__(self, alpha=0.05): super(BCEBlurWithLogitsLoss, self).__init__() self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss() self.alpha = alpha def forward(self, pred, true): loss = self.loss_fcn(pred, true) pred = torch.sigmoid(pred) # prob from logits dx = pred - true # reduce only missing label effects # dx = (pred - true).abs() # reduce missing label and false label effects alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4)) loss *= alpha_factor return loss.mean()2、cls分支cls分支輸出的是該anchor屬於哪一類的概率,也默認使用BCEWithLogits Loss。
class BCEBlurWithLogitsLoss(nn.Module): # BCEwithLogitLoss() with reduced missing label effects. def __init__(self, alpha=0.05): super(BCEBlurWithLogitsLoss, self).__init__() self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss() self.alpha = alpha def forward(self, pred, true): loss = self.loss_fcn(pred, true) pred = torch.sigmoid(pred) # prob from logits dx = pred - true # reduce only missing label effects # dx = (pred - true).abs() # reduce missing label and false label effects alpha_factor = 1 - torch.exp((dx - 1) / (self.alpha + 1e-4)) loss *= alpha_factor return loss.mean()例如,對於coco數據集上訓練的YOLO的每個anchor的維度都是85,前5個屬性是(Cx,Cy,w,h,confidence),confidence對應obj,後80個維度對應cls。
3、box分支這裡的box分支輸出的便是物體的具體位置信息了,通過前面對於坐標參數化的分析可以知道,具體的輸出4個值為、、以及,然後通過前面的參數化反轉方式與GT進行計算loss,對於回歸損失,yolov3使用的loss是smooth l1損失。Yolov5的邊框(Bounding box)回歸的損失函數默認使用的是CIoU,不是GIoU,不是DIoU,是CIoU。

下面用一張圖粗略看一下IoU,GIoU,DIoU,CIoU:


式中,、,、分別代表候選框的中心點坐標。
下面大概說一下每個IOU損失的局限性:
IoU Loss 有2個缺點:
當預測框和目標框不相交時,IoU(A,B)=0時,不能反映A,B距離的遠近,此時損失函數不可導,IoU Loss 無法優化兩個框不相交的情況。假設預測框和目標框的大小都確定,只要兩個框的相交值是確定的,其IoU值是相同時,IoU值不能反映兩個框是如何相交的。GIoU Loss 有1個缺點:
當目標框完全包裹預測框的時候,IoU和GIoU的值都一樣,此時GIoU退化為IoU, 無法區分其相對位置關係;DIoU Loss 有1個缺點:
當預測框的中心點的位置都一樣時, DIoU無法區分候選框位置的質量;綜合IoU、GIoU、DIoU的種種局限性,總結一個好的bounding box regressor包含3個要素:
因此,YOLOv5使用的是CIoU Loss:
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target)lbox += (1.0 - iou).mean() # iou lossdef bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7): # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4 box2 = box2.T # Get the coordinates of bounding boxes if x1y1x2y2: # x1, y1, x2, y2 = box1 b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3] b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3] else: # transform from xywh to xyxy b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2 b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2 b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2 b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2 # Intersection area inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \ (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0) # Union Area w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps union = w1 * h1 + w2 * h2 - inter + eps iou = inter / union if CIoU or DIoU or GIoU: cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1) # convex (smallest enclosing box) width ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1) # convex height if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1 c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center distance squared if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47 v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2) with torch.no_grad(): alpha = v / (v - iou + (1 + eps)) return iou - (rho2 / c2 + v * alpha) # CIoU return iou - rho2 / c2 # DIoU c_area = cw * ch + eps # convex area return iou - (c_area - union) / c_area # GIoU https://arxiv.org/pdf/1902.09630.pdf return iou # IoU4、Loss計算def compute_loss(p, targets, model): # predictions, targets, model device = targets.device lcls, lbox, lobj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device) tcls, tbox, indices, anchors = build_targets(p, targets, model) # targets h = model.hyp # hyperparameters # Define criteria BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device) BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device) # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3 cp, cn = smooth_BCE(eps=0.0) # Focal loss g = h['fl_gamma'] # focal loss gamma if g > 0: BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g) 。。。。。。3.3、後處理之DIoU NMS


在上圖重疊的摩托車檢測中,中間的摩托車因為考慮邊界框中心點的位置信息,也可以回歸出來。因此在重疊目標的檢測中,DIOU_nms的效果優於傳統的nms。
為什麼不用CIoU NMS呢?因為前面講到的CIOU loss,是在DIOU loss的基礎上,添加的影響因子,包含ground truth標註框的信息,在訓練時用於回歸。但在測試過程中,並沒有ground truth的信息,不用考慮影響因子,因此直接用DIOU NMS即可。
4 YOLOv5 Lite訓練自己的數據集4.1 git clone倉庫代碼clone YOLOv5 Lite代碼並下載coco的預訓練權重。
$ git clone https://github.com/ppogg/YOLOv5-Lite$ cd YOLOv5-Lite$ pip install -r requirements.txt4.2 處理數據集格式這裡可以直接參考coco128的數據集形式進行整理:
文件夾目錄如下圖所示:

4.3 配置超參數主要是配置data文件夾下的coco128.yaml中的數據集位置和種類:

4.4 配置模型這裡主要是配置models目錄下的模型yaml文件,主要是進去後修改nc這個參數來進行類別的修改。

目前支持的模型種類如下所示:
4.5. 訓練$ python train.py --data coco.yaml --cfg v5lite-e.yaml --weights v5lite-e.pt --batch-size 128 v5lite-s.yaml --weights v5lite-s.pt --batch-size 128 v5lite-c.yaml v5lite-c.pt 96 v5lite-g.yaml v5lite-g.pt 64如果您是多卡進行訓練,則:
$ python -m torch.distributed.launch --nproc_per_node 2 train.py4.6 檢測結果$ python path/to/detect.py --weights v5lite-e.pt --source 0 img.jpg # image
5 TensorRT部署5.1 目標檢測常見的落地形式
1、TensorRT是什麼TensorRT是推理優化器,能對訓練好的模型進行優化。可以理解為只有前向傳播的深度學習框架,這個框架可以將Caffe,TensorFlow的網絡模型解析,然後與TensorRT中對應的層進行一一映射,把其他框架的模型統一全部轉換到TensorRT中,然後在TensorRT中可以針對NVIDIA自家GPU實施優化策略,並進行部署加速。當你的網絡訓練完之後,可以將訓練模型文件直接丟進TensorRT中,而不再需要依賴深度學習框架(Caffe,TensorFlow等)。
2、本文AI部署流程先把onnx轉化為TensorRT的Engine文件,然後讓c++環境下的TensorRT直接加載Engine文件,從而構建engine,本文主要講解onnx轉換至Engine,然後進行基於TensorRT的C++推理檢測。

轉換和部署模型5個基本步驟:
5.2 ONNX-TensorRT的部署流程1、ONNX轉化為TRT Engine# 導出onnx文件python export.py ---weights weights/v5lite-g.pt --batch-size 1 --imgsz 640 --include onnx --simplify# 使用TensorRT官方的trtexec工具將onnx文件轉換為enginetrtexec --explicitBatch --onnx=./v5lite-g.onnx --saveEngine=v5lite-g.trt --fp16閒話不多說,這裡已經拿到了trt的engine,那麼如何進行推理呢?總的來說,分為3步:
首先load你的engine,拿到一個ICudaEngine, 這個是TensorRT推理的核心;forward模型,然後拿到輸出,對輸出進行後處理。當然這裡最核心的東西其實就兩個,一個是如何導入拿到CudaEngine,第二個是比較麻煩的後處理。
2、加載TRT EngineboolModel::readTrtFile(){std::stringcached_engine;std::fstreamfile;std::cout<<"loadingfilenamefrom:"<<engine_file<<std::endl;nvinfer1::IRuntime*trtRuntime;file.open(engine_file,std::ios::binary|std::ios::in);if(!file.is_open()){std::cout<<"readfileerror:"<<engine_file<<std::endl;cached_engine="";}while(file.peek()!=EOF){std::stringstreambuffer;buffer<<file.rdbuf();cached_engine.append(buffer.str());}file.close();trtRuntime=nvinfer1::createInferRuntime(gLogger.getTRTLogger());engine=trtRuntime->deserializeCudaEngine(cached_engine.data(),cached_engine.size(),nullptr);std::cout<<"deserializedone"<<std::endl;}//加載TensorRTEnginevoidv5Lite::LoadEngine(){//createandloadenginestd::fstreamexistEngine;existEngine.open(engine_file,std::ios::in);//如果存在已經轉換完成的TensorRTEngine文件,則直接加載if(existEngine){readTrtFile(engine_file,engine);assert(engine!=nullptr);}//如果不存在已經轉換完成的TensorRTEngine文件,則直接加載ONNX權重進行在線生成else{onnxToTRTModel(onnx_file,engine_file,engine,BATCH_SIZE);assert(engine!=nullptr);}}3、後處理之坐標轉換
std::vector<std::vector<V5lite::DetectRes>>V5lite::postProcess(conststd::vector<cv::Mat>&vec_Mat,float*output,constint&outSize){std::vector<std::vector<DetectRes>>vec_result;intindex=0;for(constcv::Mat&src_img:vec_Mat){std::vector<DetectRes>result;floatratio=float(src_img.cols)/float(IMAGE_WIDTH)>float(src_img.rows)/float(IMAGE_HEIGHT)?float(src_img.cols)/float(IMAGE_WIDTH):float(src_img.rows)/float(IMAGE_HEIGHT);float*out=output+index*outSize;intposition=0;for(intn=0;n<(int)grids.size();n++){for(intc=0;c<grids[n][0];c++){std::vector<int>anchor=anchors[n*grids[n][0]+c];for(inth=0;h<grids[n][1];h++)for(intw=0;w<grids[n][2];w++){float*row=out+position*(CATEGORY+5);position++;DetectResbox;automax_pos=std::max_element(row+5,row+CATEGORY+5);box.prob=row[4]*row[max_pos-row];if(box.prob<obj_threshold)continue;box.classes=max_pos-row-5;//坐標的反參數化,和前文的坐標轉換對接box.x=(row[0]*2-0.5+w)/grids[n][2]*IMAGE_WIDTH*ratio;box.y=(row[1]*2-0.5+h)/grids[n][1]*IMAGE_HEIGHT*ratio;box.w=pow(row[2]*2,2)*anchor[0]*ratio;box.h=pow(row[3]*2,2)*anchor[1]*ratio;result.push_back(box);}}}NmsDetect(result);vec_result.push_back(result);index++;}returnvec_result;}4、進行模型推理//推理整個文件夾的文件boolYOLOv5::InferenceFolder(conststd::string&folder_name){//讀取文件夾下面的文件,並返回為一個stringvector迭代器std::vector<std::string>sample_images=readFolder(folder_name);//getcontextassert(engine!=nullptr);//創建上下文,創建一些空間來存儲中間值。一個engine可以創建多個context,分別執行多個推理任務。context=engine->createExecutionContext();assert(context!=nullptr);//傳遞給Engine的輸入輸出buffers指針,這裡對應一個輸入和一個輸出assert(engine->getNbBindings()==2);void*buffers[2];std::vector<int64_t>bufferSize;intnbBindings=engine->getNbBindings();bufferSize.resize(nbBindings);for(inti=0;i<nbBindings;++i){//獲取輸入或輸出的維度信息nvinfer1::Dimsdims=engine->getBindingDimensions(i);//獲取輸入或輸出的數據類型信息nvinfer1::DataTypedtype=engine->getBindingDataType(i);int64_ttotalSize=volume(dims)*1*getElementSize(dtype);bufferSize[i]=totalSize;std::cout<<"binding"<<i<<":"<<totalSize<<std::endl;//&buffers是雙重指針相當於改變指針本身,這裡就是把輸入或輸出進行向量化操作cudaMalloc(&buffers[i],totalSize);}//getstreamcudaStream_tstream;//創建StreamcudaStreamCreate(&stream);intoutSize=bufferSize[1]/sizeof(float)/BATCH_SIZE;//執行推理EngineInference(sample_images,outSize,buffers,bufferSize,stream);//釋放stream和bufferscudaStreamDestroy(stream);cudaFree(buffers[0]);cudaFree(buffers[1]);//destroytheenginecontext->destroy();engine->destroy();}voidYOLOv5::EngineInference(conststd::vector<std::string>&image_list,constint&outSize,void**buffers,conststd::vector<int64_t>&bufferSize,cudaStream_tstream){intindex=0;intbatch_id=0;std::vector<cv::Mat>vec_Mat(BATCH_SIZE);std::vector<std::string>vec_name(BATCH_SIZE);floattotal_time=0;//遍歷圖像路徑listfor(conststd::string&image_name:image_list){index++;std::cout<<"Processing:"<<image_name<<std::endl;//讀取圖像內容到cv_matcv::Matsrc_img=cv::imread(image_name);//把圖像和圖像名分別保存在vec_Mat和vec_name之中if(src_img.data){vec_Mat[batch_id]=src_img.clone();vec_name[batch_id]=image_name;batch_id++;}if(batch_id==BATCH_SIZEorindex==image_list.size()){//聲明時間戳t_start_preautot_start_pre=std::chrono::high_resolution_clock::now();std::cout<<"prepareImage"<<std::endl;std::vector<float>curInput=prepareImage(vec_Mat);autot_end_pre=std::chrono::high_resolution_clock::now();//至此,prepareImage的時間已經計算完成floattotal_pre=std::chrono::duration<float,std::milli>(t_end_pre-t_start_pre).count();std::cout<<"prepareimagetake:"<<total_pre<<"ms."<<std::endl;total_time+=total_pre;batch_id=0;if(!curInput.data()){std::cout<<"prepareimagesERROR!"<<std::endl;continue;}//將數據從CPU端傳送到GPU端std::cout<<"host2device"<<std::endl;cudaMemcpyAsync(buffers[0],curInput.data(),bufferSize[0],cudaMemcpyHostToDevice,stream);//執行推理std::cout<<"execute"<<std::endl;autot_start=std::chrono::high_resolution_clock::now();context->execute(BATCH_SIZE,buffers);autot_end=std::chrono::high_resolution_clock::now();floattotal_inf=std::chrono::duration<float,std::milli>(t_end-t_start).count();std::cout<<"Inferencetake:"<<total_inf<<"ms."<<std::endl;total_time+=total_inf;std::cout<<"executesuccess"<<std::endl;std::cout<<"device2host"<<std::endl;std::cout<<"postprocess"<<std::endl;autor_start=std::chrono::high_resolution_clock::now();auto*out=newfloat[outSize*BATCH_SIZE];//CopyGPU端的推理結果到CPU端cudaMemcpyAsync(out,buffers[1],bufferSize[1],cudaMemcpyDeviceToHost,stream);//阻塞當前程序的執行,直到所有任務都處理完畢,這樣可以將計算和主機與設備之前的傳輸並行化,提高效率。cudaStreamSynchronize(stream);//進行後處理操作autoboxes=postProcess(vec_Mat,out,outSize);autor_end=std::chrono::high_resolution_clock::now();floattotal_res=std::chrono::duration<float,std::milli>(r_end-r_start).count();std::cout<<"Postprocesstake:"<<total_res<<"ms."<<std::endl;total_time+=total_res;for(inti=0;i<(int)vec_Mat.size();i++){autoorg_img=vec_Mat[i];if(!org_img.data)continue;autorects=boxes[i];for(constauto&rect:rects){chart[256];sprintf(t,"%.2f",rect.prob);std::stringname=coco_labels[rect.classes]+"-"+t;//圖書添加文字cv::putText(org_img,name,cv::Point(rect.x-rect.w/2,rect.y-rect.h/2-5),cv::FONT_HERSHEY_COMPLEX,0.7,class_colors[rect.classes],2);//繪製矩形框cv::Rectrst(rect.x-rect.w/2,rect.y-rect.h/2,rect.w,rect.h);cv::rectangle(org_img,rst,class_colors[rect.classes],2,cv::LINE_8,0);}intpos=vec_name[i].find_last_of(".");std::stringrst_name=vec_name[i].insert(pos,"_");std::cout<<rst_name<<std::endl;//保存檢測結果cv::imwrite(rst_name,org_img);}vec_Mat=std::vector<cv::Mat>(BATCH_SIZE);delete[]out;}}std::cout<<"Averageprocessingtimeis"<<total_time/image_list.size()<<"ms"<<std::endl;}5、後處理之NMS C++實現voidV5lite::NmsDetect(std::vector<DetectRes>&detections){sort(detections.begin(),detections.end(),[=](constDetectRes&left,constDetectRes&right){returnleft.prob>right.prob;});for(inti=0;i<(int)detections.size();i++)for(intj=i+1;j<(int)detections.size();j++){if(detections[i].classes==detections[j].classes){//計算DIoU的值floatiou=IOUCalculate(detections[i],detections[j]);if(iou>nms_threshold)detections[j].prob=0;}}detections.erase(std::remove_if(detections.begin(),detections.end(),[](constDetectRes&det){returndet.prob==0;}),detections.end());}//計算DIOUfloatv5Lite::IOUCalculate(constYOLOv5::DetectRes&det_a,constYOLOv5::DetectRes&det_b){cv::Point2fcenter_a(det_a.x,det_a.y);cv::Point2fcenter_b(det_b.x,det_b.y);//計算左上角角點坐標cv::Point2fleft_up(std::min(det_a.x-det_a.w/2,det_b.x-det_b.w/2),std::min(det_a.y-det_a.h/2,det_b.y-det_b.h/2));//計算右下角角點坐標cv::Point2fright_down(std::max(det_a.x+det_a.w/2,det_b.x+det_b.w/2),std::max(det_a.y+det_a.h/2,det_b.y+det_b.h/2));//計算框的中心點距離floatdistance_d=(center_a-center_b).x*(center_a-center_b).x+(center_a-center_b).y*(center_a-center_b).y;//計算框的角點距離floatdistance_c=(left_up-right_down).x*(left_up-right_down).x+(left_up-right_down).y*(left_up-right_down).y;floatinter_l=det_a.x-det_a.w/2>det_b.x-det_b.w/2?det_a.x-det_a.w/2:det_b.x-det_b.w/2;floatinter_t=det_a.y-det_a.h/2>det_b.y-det_b.h/2?det_a.y-det_a.h/2:det_b.y-det_b.h/2;floatinter_r=det_a.x+det_a.w/2<det_b.x+det_b.w/2?det_a.x+det_a.w/2:det_b.x+det_b.w/2;floatinter_b=det_a.y+det_a.h/2<det_b.y+det_b.h/2?det_a.y+det_a.h/2:det_b.y+det_b.h/2;if(inter_b<inter_t||inter_r<inter_l)return0;//計算交集floatinter_area=(inter_b-inter_t)*(inter_r-inter_l);//計算並集floatunion_area=det_a.w*det_a.h+det_b.w*det_b.h-inter_area;if(union_area==0)return0;elsereturninter_area/union_area-distance_d/distance_c;}CMakeLists.txt如下:
cmake_minimum_required(VERSION3.5)project(v5lite_trt)set(CMAKE_CXX_STANDARD14)#CUDAfind_package(CUDAREQUIRED)message(STATUS"FindCUDAincludeat${CUDA_INCLUDE_DIRS}")message(STATUS"FindCUDAlibraries:${CUDA_LIBRARIES}")#TensorRTset(TENSORRT_ROOT"/home/chaucer/TensorRT-8.0.1.6")find_path(TENSORRT_INCLUDE_DIRNvInfer.hHINTS${TENSORRT_ROOT}PATH_SUFFIXESinclude/)message(STATUS"FoundTensorRTheadersat${TENSORRT_INCLUDE_DIR}")find_library(TENSORRT_LIBRARY_INFERnvinferHINTS${TENSORRT_ROOT}${TENSORRT_BUILD}${CUDA_TOOLKIT_ROOT_DIR}PATH_SUFFIXESliblib64lib/x64)find_library(TENSORRT_LIBRARY_ONNXPARSERnvonnxparserHINTS${TENSORRT_ROOT}${TENSORRT_BUILD}${CUDA_TOOLKIT_ROOT_DIR}PATH_SUFFIXESliblib64lib/x64)set(TENSORRT_LIBRARY${TENSORRT_LIBRARY_INFER}${TENSORRT_LIBRARY_ONNXPARSER})message(STATUS"FindTensorRTlibs:${TENSORRT_LIBRARY}")#OpenCVfind_package(OpenCVREQUIRED)message(STATUS"FindOpenCVincludeat${OpenCV_INCLUDE_DIRS}")message(STATUS"FindOpenCVlibraries:${OpenCV_LIBRARIES}")set(COMMON_INCLUDE./includes/common)set(YAML_INCLUDE./includes/yaml-cpp/include)set(YAML_LIB_DIR./includes/yaml-cpp/libs)include_directories(${CUDA_INCLUDE_DIRS}${TENSORRT_INCLUDE_DIR}${OpenCV_INCLUDE_DIRS}${COMMON_INCLUDE}${YAML_INCLUDE})link_directories(${YAML_LIB_DIR})add_executable(v5lite_trtmain.cppv5lite.cpp)target_link_libraries(v5lite_trt${OpenCV_LIBRARIES}${CUDA_LIBRARIES}${TENSORRT_LIBRARY}yaml-cpp)mkdirbuildcdbuildcmake..make-j8v5lite_trt../config.yaml../samples/5、檢測結果和時間

參考
https://link.zhihu.com/?target=https%3A//github.com/ppogg/YOLOv5-Lite
https://zhuanlan.zhihu.com/p/400545131
https://link.zhihu.com/?target=https%3A//github.com/ultralytics/yolov5
https://zhuanlan.zhihu.com/p/172121380
https://zhuanlan.zhihu.com/p/143747206