YOLOv5在無人機/遙感場景下做旋轉(zhuǎn)目標(biāo)檢測時進(jìn)行的適應(yīng)性改建詳解(踩坑記錄)
來源丨h(huán)ttps://zhuanlan.zhihu.com/p/358441134編輯丨極市平臺
文章開頭直接放上我自己的項目代碼:
https://github.com/hukaixuan19970627/YOLOv5_DOTA_OBBgithub.com/hukaixuan19970627/YOLOv5_DOTA_OBB
(以下為最初版本代碼,最新代碼以GitHub為準(zhǔn))
star?還請多多益善。
前言:以下改建基于2020.10.11日上傳的YOLOv5項目
現(xiàn)成的YOLOv5代碼真的很香,不管口碑怎么樣,我用著反正是挺爽的,畢竟一個開源項目學(xué)術(shù)價值和工程應(yīng)用價值只要占其一就值得稱贊,而且v5確實在項目上手這一塊非常友好,建議大家自己上手體會一下。
本文默認(rèn)讀者對YOLOv5的原理和代碼結(jié)構(gòu)已經(jīng)有了基礎(chǔ)了解,如果從未接觸過,可以參考這篇文章:
深度眸:進(jìn)擊的后浪yolov5深度可視化解析:https://zhuanlan.zhihu.com/p/183838757
目標(biāo)檢測方法所采取的邊框標(biāo)注方式要按照被檢測物體本身的形狀特征進(jìn)行改變。原始YOLOv5項目的應(yīng)用場景為自然場景下的目標(biāo),目標(biāo)檢測邊框為水平矩形框(Horizontal Bounding Box,HBB),畢竟我們的視角就是水平視角。
而當(dāng)視角發(fā)生改變時,物體呈現(xiàn)在二維圖像中的形狀特征就會發(fā)生改變,為了更好的匹配圖像特征,人們想出了多種邊框的標(biāo)記方法,比如交通監(jiān)控(鳥瞰)視角下的物體可以采取橢圓邊框進(jìn)行標(biāo)注:
視角繼續(xù)上升來到無人機/衛(wèi)星的高度,俯視視角下的物體形狀特征繼續(xù)發(fā)生改變,此時邊框標(biāo)記方式就有了更多的選擇:
至于你問選擇適當(dāng)?shù)倪吙驑?biāo)注方式有什么作用,我個人的理解有以下兩點:
- 標(biāo)注方式越精準(zhǔn),提供給網(wǎng)絡(luò)訓(xùn)練時的冗余信息就越少;先驗越充分,網(wǎng)絡(luò)的可學(xué)習(xí)方案就越少,有利于約束網(wǎng)絡(luò)的訓(xùn)練方向和減少網(wǎng)絡(luò)的收斂時間;
- 當(dāng)目標(biāo)物體過于緊密時,精準(zhǔn)的標(biāo)注方式可以避免被NMS”錯殺“已經(jīng)檢出的目標(biāo)。
以本圖為例,精準(zhǔn)的標(biāo)注方式可以確保緊密的物體之間的IOU為0;如果標(biāo)注方式改為水平目標(biāo)邊框檢測效果將慘不忍睹。
那么純俯視角度(無人機/遙感視角)下的物體有哪些常見的標(biāo)注方式呢?可以參考下面這篇文章,且yangxue作者提出的Circular Smooth Label也是YOLOv5改建的關(guān)鍵之處:
旋轉(zhuǎn)目標(biāo)檢測方法解讀(DCL, CVPR2021)
上面那篇文章的主要思想就是緩解旋轉(zhuǎn)目標(biāo)標(biāo)注方式在網(wǎng)絡(luò)訓(xùn)練時產(chǎn)生的邊界問題, 這種邊界問題其實可以一句話概括:由于學(xué)習(xí)的目標(biāo)參數(shù)具有周期性,在周期變化的邊界處會導(dǎo)致?lián)p失值突增,因此增大網(wǎng)絡(luò)的學(xué)習(xí)難度。 這句話可以參考下圖進(jìn)行理解:
以180度回歸的長邊定義法中的θ為例,θ ∈[-90,90);正常訓(xùn)練情況下,網(wǎng)絡(luò)預(yù)測的θ值為88,目標(biāo)真實θ值為89,網(wǎng)絡(luò)學(xué)習(xí)到的角度距離為1,真實情況下的兩者差值為1;邊界情況下,網(wǎng)絡(luò)預(yù)測的θ值為89,目標(biāo)真實θ值為-90,網(wǎng)絡(luò)學(xué)習(xí)到的角度距離為179,真實情況下的兩者差值為1.
那么如何處理邊界問題呢:(以θ的邊界問題為例)
- 尋找一種新的旋轉(zhuǎn)目標(biāo)定義方式,定義方式中不含具有周期變化性的參數(shù),卻又能表示周期旋轉(zhuǎn)的目標(biāo)物體,根本上杜絕邊界問題的產(chǎn)生;(Anchor free/mask的思路,PolarDet、P-RSDet基于極坐標(biāo)系表示一個任意四邊形物體,BBA-Vectors、O^2-DNet基于向量來表示一個有向矩形,ROPDet、Beyond Bounding Box、Oriented Reppoints基于點集來表示一個任意形狀的物體,)
- 從損失函數(shù)上入手,使用Smooth L1單獨考慮每個參數(shù)時,賦予損失函數(shù)和角度同樣的周期性,使得邊界處θ之間差值可以很大,但loss變化實際很小;或者綜合考慮所有回歸參數(shù)的影響,使用旋轉(zhuǎn)IoU損失函數(shù)也可以規(guī)避邊界問題,不過RIoU不可導(dǎo),近似可導(dǎo)的相關(guān)工作可以參考KLD、GWD,工程上實現(xiàn)RIoU可導(dǎo)的工作可以參考:https://github.com/csuhan/s2anet/blob/master/configs/rotated_iou/README.mdθ由回歸問題轉(zhuǎn)為分類問題。(把連續(xù)問題直接離散化,避開邊界情況)
其中2,3yangxue大佬都有過相應(yīng)的解決方案,大家可以去他的主頁參考。CSL就是3的思想體現(xiàn),只不過CSL考慮的更多,因為當(dāng)θ變?yōu)榉诸悊栴}后,網(wǎng)絡(luò)就無法學(xué)習(xí)到角度距離信息了,比如真實角度為-90,網(wǎng)絡(luò)預(yù)測成89和-89產(chǎn)生的損失值我們期望是一樣的,因為角度距離實際上都是1。
所以CSL實際上是一個用分類實現(xiàn)回歸思想的解決方案, 具體細(xì)節(jié)大家移步去上面的文章。我們直接用成果,基于180度回歸的長邊定義法中的參數(shù)只有θ存在邊界問題,而CSL剛好又能處理θ的邊界問題,那么我們”暫且認(rèn)為“CSL+長邊定義法的組合是比較優(yōu)的。之所以說是”暫且“是因為yangxue大佬又在最新的文章里面又提出了這種方式的缺點:
當(dāng)時我的心情如下,那還是方法1的anchor free方案比較好,一勞永逸;
但是這篇文章有部分我還沒有理解透徹,我們還是只用CSL+長邊定義法就行了,后期的升級工作交給各位了。
標(biāo)注方案確定之后,就可以開始一系列的改建工作了。
正文:基本所有基于深度學(xué)習(xí)的目標(biāo)檢測器項目的結(jié)構(gòu)都分為:
數(shù)據(jù)加載器(圖像預(yù)處理)--> BackBone(提取目標(biāo)特征) --> Neck(收集組合目標(biāo)特征) --> Head(預(yù)測部分) --> 損失函數(shù)部分
首先我們必須熟知自己的數(shù)據(jù)在進(jìn)入網(wǎng)絡(luò)之前的數(shù)據(jù)形式是什么樣的,因為我們采用的是長邊定義法,所以我們的注釋文件格式為:
[ classid x_c y_c longside shortside Θ ] Θ∈[0, 180)
* longside: 旋轉(zhuǎn)矩形框的最長邊
* shortside: 與最長邊對應(yīng)的另一邊
* Θ: x軸順時針旋轉(zhuǎn)遇到最長邊所經(jīng)過的角度
至于數(shù)據(jù)形式如何轉(zhuǎn)換,利用好cv2.minAreaRect()函數(shù)+總結(jié)規(guī)律就可以,我的另一篇文章里講的比較清楚,大家可以移步:
略略略:DOTA數(shù)據(jù)格式轉(zhuǎn)YOLO數(shù)據(jù)格式工具(cv2.minAreaRect踩坑記錄):https://zhuanlan.zhihu.com/p/356416158)
注意opencv4.1.2版本cv2.minAreaRect()函數(shù)生成的最小外接矩形框(x,y,w,h,θ)的幾個大坑:
(1) 在絕大數(shù)情況下 Θ∈[-90, 0);
(2) 部分水平或垂直的目標(biāo)邊框,其θ值為0;
(3) width或height有時輸出0, 與此同時Θ = 90;
(4) 輸出的width或height有時會超過圖片本身的寬高,即歸一化時數(shù)據(jù)>1。
接下來圖像數(shù)據(jù)與label數(shù)據(jù)進(jìn)入到程序中,我們必須熟知在進(jìn)入backbone之前,數(shù)據(jù)加載器流程中l(wèi)abels數(shù)據(jù)的維度變化。
原始yolov5中,labels數(shù)據(jù)維度一直以(X_LT, Y_LT, X_RB, Y_RB)左上角右下角兩點坐標(biāo)表示水平矩形框的形式存在,并一直在做歸一化和反歸一化操作
由于我們采用的邊框定義法是[x_c y_c longside shortside Θ],邊框的角度信息只存在于θ中,我們完全可以將 [x_c y_c longside shortside] 視為水平目標(biāo)邊框,因此在數(shù)據(jù)加載部分我們只需要在labels原始數(shù)據(jù)的基礎(chǔ)上添加一個θ維度,只要不是涉及到會引起labels角度變化的代碼都不需要更改其處理邏輯。
注意: 數(shù)據(jù)加載器中存在大量的歸一化和反歸一化的操作,以及大量涉及到圖像寬高度的數(shù)據(jù)變化,因此網(wǎng)絡(luò)輸入的圖像size:HEIGHT 必須= WIDTH,因為長邊定義法中的longside和shorside與圖像的寬高沒有嚴(yán)格的對應(yīng)關(guān)系。
數(shù)據(jù)加載器中涉及三類數(shù)據(jù)增強方式:Mosaic,random_perspective(仿射矩陣增強),普通數(shù)據(jù)增強方式。
其中Mosaic,仿射矩陣增強都是針對(X_LT, Y_LT, X_RB, Y_RB)數(shù)據(jù)格式進(jìn)行增強,修改時添加θ維度就可以,不過仿射矩陣增強函數(shù)內(nèi)共有 Translation、Shear、Rotation、Scale、Perspective、Center 6種數(shù)據(jù)增強方式,其中旋轉(zhuǎn)與形變仿射的變換會引起目標(biāo)角度上的改變。
所以只要超參數(shù)中的 ['perspective']=0,['degrees']=0 ,這塊函數(shù)代碼就不需要修改邏輯部分,為了方便我們直接把涉及到角度的增強放在最后的普通數(shù)據(jù)增強方式中。
注意:Mosaic操作中會同時觸發(fā)MixUp數(shù)據(jù)增強操作,但是在遙感/無人機應(yīng)用場景中我個人認(rèn)為并不適用,首先背景復(fù)雜就是該場景中的普遍難題,MixUp會融合兩張圖像,圖像中的小目標(biāo)會摻雜另一張圖的背景信息(包含形似物或噪聲),從而影響小目標(biāo)的特征提取。(不過一切以實驗結(jié)果為準(zhǔn))
提取圖像特征層的結(jié)構(gòu)都不需要改動。
三、Head部分head部分也就是yolo.py文件中的Detect類,由于我們將θ轉(zhuǎn)為分類問題,因此每個anchor負(fù)責(zé)預(yù)測的參數(shù)數(shù)量為 (x_c y_c longside shortside score)+num_classes+angle_classes。修改Detect類的構(gòu)造函數(shù)即可。
損失函數(shù)共有四個部分:置信度損失、class分類損失、θ角度分類損失、bbox邊框回歸損失。
(1)計算損失前的準(zhǔn)備工作損失的計算需要 targets 與 predicts,每個數(shù)據(jù)的維度都要有所對應(yīng),因此需要general.py文件中的build_targets函數(shù)生成目標(biāo)真實GT的類別信息列表、邊框參數(shù)信息列表、Anchor索引列表、Anchor尺寸信息列表、角度類別信息列表。
其中Anchor索引列表用于檢索網(wǎng)絡(luò)預(yù)測結(jié)果中對應(yīng)的anchor,從而將其標(biāo)記為正樣本。yolov5為了保證正樣本的數(shù)量,在正樣本標(biāo)記策略中采用了比較暴力的策略:原本yolov3僅僅采用當(dāng)前GT中心所在的網(wǎng)格中的anchor進(jìn)行正樣本標(biāo)記,而yolov5不僅采用當(dāng)前網(wǎng)格中的anchor標(biāo)記為正樣本,同時還會標(biāo)記相鄰兩個網(wǎng)格的anchor為正樣本。
這種處理邏輯個人暫不評價好壞,但是yolov5源碼在代碼實現(xiàn)上顯然考慮不夠周全,目標(biāo)中心所屬網(wǎng)格如果剛好在圖像的邊界位置,yolov5的源碼有時會輸出超過featuremap尺寸的索引。這種bug表現(xiàn)在訓(xùn)練中就是某個時刻yolov5的訓(xùn)練就會中斷:
Traceback (most recent call last):
File "train.py", line 457, in <module>
train(hyp, opt, device, tb_writer)
File "train.py", line 270, in train
loss, loss_items = compute_loss(pred, targets.to(device), model) # loss scaled by batch_size
File "/mnt/G/1125/rotation-yolov5-master/utils/general.py", line 530, in compute_loss
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio
RuntimeError: CUDA error: device-side assert triggered
上述報錯顯然是索引時超出數(shù)組取值范圍的問題,解決方法也很簡單,先查詢是哪些參數(shù)超出了索引范圍,當(dāng)運行出錯時,進(jìn)入pdb調(diào)試,打印當(dāng)前所有索引參數(shù):
然后就發(fā)現(xiàn)網(wǎng)格索引gj,gi偶爾會超出當(dāng)前featuremap的索引范圍。(舉例:featuremap大小為32×32,網(wǎng)格索引范圍為0-31,但是build_targets函數(shù)偶爾會輸出索引值32,此時出現(xiàn)bug,訓(xùn)練中斷)
然而我當(dāng)時在yolov5項目源碼的Issues中卻發(fā)現(xiàn)沒人提交這種問題,原因也很簡單,自然場景的下的目標(biāo)很難標(biāo)注在圖片的邊界位置,但是遙感/無人機圖像顯然相反,由于會經(jīng)過裁剪,極其容易出現(xiàn)目標(biāo)標(biāo)注在邊界位置的情況, 如下圖所示:
這個BUG屬于yolov5源碼build_targets函數(shù)生成anchor索引時考慮不周全導(dǎo)致的,解決辦法也很簡單,在生成的索引處加上數(shù)值范圍限制(壞處就是可能出現(xiàn)網(wǎng)格重復(fù)利用的情況,比較浪費):
2021.04.25更新:
重復(fù)利用就重復(fù)利用唄(~破罐破摔!~),本來yolov5的跨網(wǎng)格正負(fù)樣本標(biāo)記方式就會產(chǎn)生同一個anchor與不同gt進(jìn)行l(wèi)oss計算的問題,這個地方感覺還有很多地方可以優(yōu)化,但就是想不明白這樣子回歸明明會產(chǎn)生二義性問題為什么效果還是很好?
之后的改建部分也比較機械,在compute_loss函數(shù)和build_targets函數(shù)中添加θ角度信息的處理即可,主要注意數(shù)據(jù)索引的代碼塊就可以,由于添加了‘θ’ 180個通道,所以函數(shù)中所有的索引部分都要更改。
今天(2021年3月21日) 我又去yolov5的issues上看了看,似乎20年11月份修復(fù)了這個問題。我這個是基于20年10月11日的代碼改建的,要是晚下載幾天就好了,興許能避開這個坑。
又看了下新的yolov5源碼,很多地方大換血...... 改建的速度還沒人家更新的速度快。
(2)計算損失- class分類損失:
無需更改,注意數(shù)據(jù)索引部分即可。
- θ角度分類損失:
由于我們添加的θ是分類任務(wù),照葫蘆畫瓢,添加分類損失就可以了,值得注意的是θ部分的損失我們有兩種方案:
- 一種就是正常的分類損失,同類別損失一樣:BCEWithLogitsLoss;
- 先將GT的θ label經(jīng)CSL處理后,再計算類別損失:BCEWithLogitsLoss。
項目代碼中同時實現(xiàn)了兩種方案,由csl_label_flag進(jìn)行控制,csl_label_flag為True則進(jìn)行CSL處理,否則計算正常分類損失,方便大家查看CSL在自己數(shù)據(jù)集上的提升效果:
- bbox邊框回歸損失:
yolov5源碼中邊框損失函數(shù)采用的是IOU/GIOU/CIOU/DIOU,適用于水平矩形邊框之間計算IOU,原本是不適用于旋轉(zhuǎn)框之間計算IOU的。由于框會旋轉(zhuǎn)等原因,計算兩個旋轉(zhuǎn)框之間的IOU公式通常都不可導(dǎo),如果θ為回歸任務(wù),勢必要通過旋轉(zhuǎn)IOU損失函數(shù)進(jìn)行反向傳播從而調(diào)整自身參數(shù),大多數(shù)旋轉(zhuǎn)檢測器的處理辦法都是將不可導(dǎo)的旋轉(zhuǎn)IOU損失函數(shù)進(jìn)行近似,使得網(wǎng)絡(luò)可以正常進(jìn)行訓(xùn)練。
不過因為我們將θ視為分類任務(wù)來處理,相當(dāng)于將角度信息與邊框參數(shù)信息解耦,所以旋轉(zhuǎn)框的損失計算部分也分為角度損失和水平邊框損失兩個部分,因此源碼部分可以不進(jìn)行改動,邊框回歸損失部分依舊采用IOU/GIOU/CIOU/DIOU損失函數(shù)。
- 置信度損失:
這一部分我們需要考慮清楚,yolov5源碼是將GT水平邊框與預(yù)測水平邊框的IOU/GIOU/CIOU/DIOU值作為該預(yù)測框的置信度分支的權(quán)重系數(shù),由于改建的情況特殊(水平邊框+角度),我們有兩種選擇:
- 置信度分支的權(quán)重系數(shù)依然選擇水平邊框的之間的IOU/GIOU/CIOU/DIOU;
- 置信度分支的權(quán)重系數(shù)為旋轉(zhuǎn)框IOU。
方案1相當(dāng)于完全解耦預(yù)測角度與預(yù)測置信度之間的關(guān)聯(lián),置信度只與邊框參數(shù)有關(guān)聯(lián),但事實上角度的一點偏差對旋轉(zhuǎn)框IOU的影響是很大的,這種做法可能會影響網(wǎng)絡(luò)最后對目標(biāo)的score預(yù)測,導(dǎo)致部分明明角度預(yù)測錯誤但是邊框參數(shù)預(yù)測正確的冗余框有過大的score,從而NMS無法濾除,最終影響檢測精度。
2021.04.22更新:方案1速度比方案2訓(xùn)練快很多,gpu利用率也更穩(wěn)定,而且預(yù)測出來的框的置信度相比來說會更高,就是可能錯檢的情況會多一點(θloss收斂正常,置信度loss收斂正常的話該情況會得到明顯緩解)
方案2除了錯檢情況少一點以外,其余都是缺點,大家可以自行對比嘗試。不過缺點后期可以通過cuda加速來改善,畢竟DOTA_devkit提供的C++庫計算效率確實不高。再加上代碼不是自己寫的,想直接套用別的旋轉(zhuǎn)IoU代碼就只能用時間效率賊低的for循環(huán)來做。
方案2自然是為了避免上述情況的產(chǎn)生,此外也是對將角度解耦出去的一種”補償“。(至于網(wǎng)絡(luò)能否學(xué)到這一層補償那就不得而知,畢竟conf分支的權(quán)重系數(shù)不會通過反向傳播的方式進(jìn)行更新——detach的參數(shù)不會參與網(wǎng)絡(luò)訓(xùn)練)
不會計算旋轉(zhuǎn)IOU也沒關(guān)系,DOTA數(shù)據(jù)集的作者額外提供了一個DOTA_devkit工具,里面有現(xiàn)成的C++庫,我們直接調(diào)用即可。
數(shù)據(jù)加載器(圖像預(yù)處理)--> BackBone(提取目標(biāo)特征) --> Neck(收集組合目標(biāo)特征) --> Head(預(yù)測部分) --> 損失函數(shù)部分
以上部分基本修改完畢,接下來就是可視化的部分,利用好Opencv的三個函數(shù)即可:
# rect = cv2.minAreaRect(poly) # 得到poly最小外接矩形的(中心(x,y), (寬,高), 旋轉(zhuǎn)角度)
# box = np.float32(cv2.boxPoints(rect)) # 返回最小外接矩形rect的四個點的坐標(biāo)
# cv2.drawContours(image=img, contours=[poly], contourIdx=-1, color=color, thickness=2*tl)
大家可以參考我上傳的項目代碼,里面基本每段代碼都會有我的注解(主要是當(dāng)時自己剛開始看yolov5源碼,每句話都有注釋)。
改建部分完結(jié)撒花,歡迎討論!
本文僅做學(xué)術(shù)分享,如有侵權(quán),請聯(lián)系刪文。
*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。