點擊下方卡片,關注「新機器視覺」公眾號
重磅乾貨,第一時間送達
以下文章來源於:略略略@知乎
作者:略略略
編輯:極市平台
原文鏈接:https://zhuanlan.zhihu.com/p/358441134
本文僅用於學術分享,如有侵權,請聯繫後台作刪文處理

現成的YOLOv5代碼真的很香,不管口碑怎麼樣,我用着反正是挺爽的,畢竟一個開源項目學術價值和工程應用價值只要占其一就值得稱讚,而且v5確實在項目上手這一塊非常友好,建議大家自己上手體會一下。
本文默認讀者對YOLOv5的原理和代碼結構已經有了基礎了解,如果從未接觸過,可以參考這篇文章:
深度眸:進擊的後浪yolov5深度可視化解析:https://zhuanlan.zhihu.com/p/183838757
目標檢測方法所採取的邊框標註方式要按照被檢測物體本身的形狀特徵進行改變。原始YOLOv5項目的應用場景為自然場景下的目標,目標檢測邊框為水平矩形框(Horizontal Bounding Box,HBB),畢竟我們的視角就是水平視角。
而當視角發生改變時,物體呈現在二維圖像中的形狀特徵就會發生改變,為了更好的匹配圖像特徵,人們想出了多種邊框的標記方法,比如交通監控(鳥瞰)視角下的物體可以採取橢圓邊框進行標註:

視角繼續上升來到無人機/衛星的高度,俯視視角下的物體形狀特徵繼續發生改變,此時邊框標記方式就有了更多的選擇:

至於你問選擇適當的邊框標註方式有什麼作用,我個人的理解有以下兩點:

以本圖為例,精準的標註方式可以確保緊密的物體之間的IOU為0;如果標註方式改為水平目標邊框檢測效果將慘不忍睹。
那麼純俯視角度(無人機/遙感視角)下的物體有哪些常見的標註方式呢?可以參考下面這篇文章,且yangxue作者提出的Circular Smooth Label也是YOLOv5改建的關鍵之處:
旋轉目標檢測方法解讀(DCL, CVPR2021)
上面那篇文章的主要思想就是緩解旋轉目標標註方式在網絡訓練時產生的邊界問題,這種邊界問題其實可以一句話概括:由於學習的目標參數具有周期性,在周期變化的邊界處會導致損失值突增,因此增大網絡的學習難度。這句話可以參考下圖進行理解:

以180度回歸的長邊定義法中的θ為例,θ ∈[-90,90);正常訓練情況下,網絡預測的θ值為88,目標真實θ值為89,網絡學習到的角度距離為1,真實情況下的兩者差值為1;邊界情況下,網絡預測的θ值為89,目標真實θ值為-90,網絡學習到的角度距離為179,真實情況下的兩者差值為1.
那麼如何處理邊界問題呢:(以θ的邊界問題為例)
其中2,3yangxue大佬都有過相應的解決方案,大家可以去他的主頁參考。CSL就是3的思想體現,只不過CSL考慮的更多,因為當θ變為分類問題後,網絡就無法學習到角度距離信息了,比如真實角度為-90,網絡預測成89和-89產生的損失值我們期望是一樣的,因為角度距離實際上都是1。
所以CSL實際上是一個用分類實現回歸思想的解決方案,具體細節大家移步去上面的文章。我們直接用成果,基於180度回歸的長邊定義法中的參數只有θ存在邊界問題,而CSL剛好又能處理θ的邊界問題,那麼我們」暫且認為「CSL+長邊定義法的組合是比較優的。之所以說是」暫且「是因為yangxue大佬又在最新的文章裡面又提出了這種方式的缺點:

當時我的心情如下,那還是方法1的anchor free方案比較好,一勞永逸;

但是這篇文章有部分我還沒有理解透徹,我們還是只用CSL+長邊定義法就行了,後期的升級工作交給各位了。
標註方案確定之後,就可以開始一系列的改建工作了。
正文:基本所有基於深度學習的目標檢測器項目的結構都分為:
數據加載器(圖像預處理)--> BackBone(提取目標特徵) --> Neck(收集組合目標特徵) --> Head(預測部分) --> 損失函數部分

首先我們必須熟知自己的數據在進入網絡之前的數據形式是什麼樣的,因為我們採用的是長邊定義法,所以我們的注釋文件格式為:
[ classid x_c y_c longside shortside Θ ] Θ∈[0, 180)* longside: 旋轉矩形框的最長邊* shortside: 與最長邊對應的另一邊* Θ: x軸順時針旋轉遇到最長邊所經過的角度至於數據形式如何轉換,利用好cv2.minAreaRect()函數+總結規律就可以,我的另一篇文章里講的比較清楚,大家可以移步:
略略略:DOTA數據格式轉YOLO數據格式工具(cv2.minAreaRect踩坑記錄):https://zhuanlan.zhihu.com/p/356416158)
注意opencv4.1.2版本cv2.minAreaRect()函數生成的最小外接矩形框(x,y,w,h,θ)的幾個大坑:
(1) 在絕大數情況下 Θ∈[-90, 0);
(2) 部分水平或垂直的目標邊框,其θ值為0;
(3) width或height有時輸出0, 與此同時Θ = 90;
(4) 輸出的width或height有時會超過圖片本身的寬高,即歸一化時數據>1。
接下來圖像數據與label數據進入到程序中,我們必須熟知在進入backbone之前,數據加載器流程中labels數據的維度變化。

原始yolov5中,labels數據維度一直以(X_LT, Y_LT, X_RB, Y_RB)左上角右下角兩點坐標表示水平矩形框的形式存在,並一直在做歸一化和反歸一化操作
由於我們採用的邊框定義法是[x_c y_c longside shortside Θ],邊框的角度信息只存在於θ中,我們完全可以將 [x_c y_c longside shortside] 視為水平目標邊框,因此在數據加載部分我們只需要在labels原始數據的基礎上添加一個θ維度,只要不是涉及到會引起labels角度變化的代碼都不需要更改其處理邏輯。
注意:數據加載器中存在大量的歸一化和反歸一化的操作,以及大量涉及到圖像寬高度的數據變化,因此網絡輸入的圖像size:HEIGHT 必須= WIDTH,因為長邊定義法中的longside和shorside與圖像的寬高沒有嚴格的對應關係。
數據加載器中涉及三類數據增強方式:Mosaic,random_perspective(仿射矩陣增強),普通數據增強方式。
其中Mosaic,仿射矩陣增強都是針對(X_LT, Y_LT, X_RB, Y_RB)數據格式進行增強,修改時添加θ維度就可以,不過仿射矩陣增強函數內共有 Translation、Shear、Rotation、Scale、Perspective、Center 6種數據增強方式,其中旋轉與形變仿射的變換會引起目標角度上的改變。
所以只要超參數中的 ['perspective']=0,['degrees']=0 ,這塊函數代碼就不需要修改邏輯部分,為了方便我們直接把涉及到角度的增強放在最後的普通數據增強方式中。


注意:Mosaic操作中會同時觸發MixUp數據增強操作,但是在遙感/無人機應用場景中我個人認為並不適用,首先背景複雜就是該場景中的普遍難題,MixUp會融合兩張圖像,圖像中的小目標會摻雜另一張圖的背景信息(包含形似物或噪聲),從而影響小目標的特徵提取。(不過一切以實驗結果為準)

提取圖像特徵層的結構都不需要改動。
三、Head部分head部分也就是yolo.py文件中的Detect類,由於我們將θ轉為分類問題,因此每個anchor負責預測的參數數量為 (x_c y_c longside shortside score)+num_classes+angle_classes。修改Detect類的構造函數即可。

損失函數共有四個部分:置信度損失、class分類損失、θ角度分類損失、bbox邊框回歸損失。
(1)計算損失前的準備工作損失的計算需要 targets 與 predicts,每個數據的維度都要有所對應,因此需要general.py文件中的build_targets函數生成目標真實GT的類別信息列表、邊框參數信息列表、Anchor索引列表、Anchor尺寸信息列表、角度類別信息列表。

其中Anchor索引列表用於檢索網絡預測結果中對應的anchor,從而將其標記為正樣本。yolov5為了保證正樣本的數量,在正樣本標記策略中採用了比較暴力的策略:原本yolov3僅僅採用當前GT中心所在的網格中的anchor進行正樣本標記,而yolov5不僅採用當前網格中的anchor標記為正樣本,同時還會標記相鄰兩個網格的anchor為正樣本。
這種處理邏輯個人暫不評價好壞,但是yolov5源碼在代碼實現上顯然考慮不夠周全,目標中心所屬網格如果剛好在圖像的邊界位置,yolov5的源碼有時會輸出超過featuremap尺寸的索引。這種bug表現在訓練中就是某個時刻yolov5的訓練就會中斷:
Traceback (most recent call last):File "train.py", line 457, in <module>train(hyp, opt, device, tb_writer)File "train.py", line 270, in trainloss, loss_items = compute_loss(pred, targets.to(device), model) # loss scaled by batch_sizeFile "/mnt/G/1125/rotation-yolov5-master/utils/general.py", line 530, in compute_losstobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratioRuntimeError: CUDA error: device-side assert triggered上述報錯顯然是索引時超出數組取值範圍的問題,解決方法也很簡單,先查詢是哪些參數超出了索引範圍,當運行出錯時,進入pdb調試,打印當前所有索引參數:

然後就發現網格索引gj,gi偶爾會超出當前featuremap的索引範圍。(舉例:featuremap大小為32×32,網格索引範圍為0-31,但是build_targets函數偶爾會輸出索引值32,此時出現bug,訓練中斷)
然而我當時在yolov5項目源碼的Issues中卻發現沒人提交這種問題,原因也很簡單,自然場景的下的目標很難標註在圖片的邊界位置,但是遙感/無人機圖像顯然相反,由於會經過裁剪,極其容易出現目標標註在邊界位置的情況,如下圖所示:

這個BUG屬於yolov5源碼build_targets函數生成anchor索引時考慮不周全導致的,解決辦法也很簡單,在生成的索引處加上數值範圍限制(壞處就是可能出現網格重複利用的情況,比較浪費):

2021.04.25更新:
重複利用就重複利用唄(~破罐破摔!~),本來yolov5的跨網格正負樣本標記方式就會產生同一個anchor與不同gt進行loss計算的問題,這個地方感覺還有很多地方可以優化,但就是想不明白這樣子回歸明明會產生二義性問題為什麼效果還是很好?
之後的改建部分也比較機械,在compute_loss函數和build_targets函數中添加θ角度信息的處理即可,主要注意數據索引的代碼塊就可以,由於添加了『θ』 180個通道,所以函數中所有的索引部分都要更改。

今天(2021年3月21日)我又去yolov5的issues上看了看,似乎20年11月份修復了這個問題。我這個是基於20年10月11日的代碼改建的,要是晚下載幾天就好了,興許能避開這個坑。

又看了下新的yolov5源碼,很多地方大換血...... 改建的速度還沒人家更新的速度快。
(2)計算損失無需更改,注意數據索引部分即可。
由於我們添加的θ是分類任務,照葫蘆畫瓢,添加分類損失就可以了,值得注意的是θ部分的損失我們有兩種方案:
項目代碼中同時實現了兩種方案,由csl_label_flag進行控制,csl_label_flag為True則進行CSL處理,否則計算正常分類損失,方便大家查看CSL在自己數據集上的提升效果:

yolov5源碼中邊框損失函數採用的是IOU/GIOU/CIOU/DIOU,適用於水平矩形邊框之間計算IOU,原本是不適用於旋轉框之間計算IOU的。由於框會旋轉等原因,計算兩個旋轉框之間的IOU公式通常都不可導,如果θ為回歸任務,勢必要通過旋轉IOU損失函數進行反向傳播從而調整自身參數,大多數旋轉檢測器的處理辦法都是將不可導的旋轉IOU損失函數進行近似,使得網絡可以正常進行訓練。
不過因為我們將θ視為分類任務來處理,相當於將角度信息與邊框參數信息解耦,所以旋轉框的損失計算部分也分為角度損失和水平邊框損失兩個部分,因此源碼部分可以不進行改動,邊框回歸損失部分依舊採用IOU/GIOU/CIOU/DIOU損失函數。
這一部分我們需要考慮清楚,yolov5源碼是將GT水平邊框與預測水平邊框的IOU/GIOU/CIOU/DIOU值作為該預測框的置信度分支的權重係數,由於改建的情況特殊(水平邊框+角度),我們有兩種選擇:
方案1相當於完全解耦預測角度與預測置信度之間的關聯,置信度只與邊框參數有關聯,但事實上角度的一點偏差對旋轉框IOU的影響是很大的,這種做法可能會影響網絡最後對目標的score預測,導致部分明明角度預測錯誤但是邊框參數預測正確的冗餘框有過大的score,從而NMS無法濾除,最終影響檢測精度。
2021.04.22更新:方案1速度比方案2訓練快很多,gpu利用率也更穩定,而且預測出來的框的置信度相比來說會更高,就是可能錯檢的情況會多一點(θloss收斂正常,置信度loss收斂正常的話該情況會得到明顯緩解)
方案2除了錯檢情況少一點以外,其餘都是缺點,大家可以自行對比嘗試。不過缺點後期可以通過cuda加速來改善,畢竟DOTA_devkit提供的C++庫計算效率確實不高。再加上代碼不是自己寫的,想直接套用別的旋轉IoU代碼就只能用時間效率賊低的for循環來做。

方案2自然是為了避免上述情況的產生,此外也是對將角度解耦出去的一種」補償「。(至於網絡能否學到這一層補償那就不得而知,畢竟conf分支的權重係數不會通過反向傳播的方式進行更新——detach的參數不會參與網絡訓練)
不會計算旋轉IOU也沒關係,DOTA數據集的作者額外提供了一個DOTA_devkit工具,裡面有現成的C++庫,我們直接調用即可。

數據加載器(圖像預處理)--> BackBone(提取目標特徵) --> Neck(收集組合目標特徵) --> Head(預測部分) --> 損失函數部分
以上部分基本修改完畢,接下來就是可視化的部分,利用好Opencv的三個函數即可:
# rect = cv2.minAreaRect(poly) # 得到poly最小外接矩形的(中心(x,y), (寬,高), 旋轉角度)# box = np.float32(cv2.boxPoints(rect)) # 返回最小外接矩形rect的四個點的坐標# cv2.drawContours(image=img, contours=[poly], contourIdx=-1, color=color, thickness=2*tl)大家可以參考我上傳的項目代碼,裡面基本每段代碼都會有我的註解(主要是當時自己剛開始看yolov5源碼,每句話都有注釋)。


本文僅做學術分享,如有侵權,請聯繫刪文。
