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