YOLO V2學習總結
寫在前面
YOLO的升級版有兩種:YOLOv2和YOLO9000。作者採用了一系列的方法優化了YOLO的模型結構,產生了YOLOv2,在快速的同時準確率達到目前最好的結果(state of the art)。然後,作者提出了一種目標分類與檢測的聯合訓練方法,通過WordTree來混合檢測資料集與識別資料集之中的資料,同時在COCO和ImageNet資料集中進行訓練得到YOLO9000,實現9000多種物體的實時檢測。
YOLO V2是原作者在V1基礎上做出改進後提出的,論文的名稱就已經表達了作者的工作內容:
- Better 指的是和YOLO相比,YOLO V2有更好的精度
- Faster 指的是修改了網路結構,其檢測更快
- Stronger 指的就是YOLO 9000,使用聯合訓練的方法,同時使用目標檢測和影象分類的資料集,訓練YOLO V2,訓練出來的模型能夠實時的識別多達9000種目標,所以也稱為YOLO9000。
遵循原論文的結構,本文將從Better,Faster和Stronger三個方面對YOLO V2進行解讀。
一. Better
在YOLO V1的基礎上,作者提出了不少的改進來進一步提升演算法的效能(mAP),主要改進措施包括網路結構的改進(第1,3,5,6條)和Anchor Box的引進(第3,4,5條)以及訓練方法(第2,7條)。
1.1 引入BN層(Batch Normalization)
Batch Normalization能夠加快模型收斂,並提供一定的正則化。作者在每個conv層都加上了了BN層,同時去掉了原來模型中的drop out部分,實驗證明可以提高2%的mAP。
BN層進行如下變換:①對該批樣本的各特徵量(對於中間層來說,就是每一個神經元)分別進行歸一化處理,分別使每個特徵的資料分佈變換為均值0,方差1。從而使得每一批訓練樣本在每一層都有類似的分佈。這一變換不需要引入額外的引數。②對上一步的輸出再做一次線性變換,假設上一步的輸出為Z,則Z1=γZ + β。這裡γ、β是可以訓練的引數。增加這一變換是因為上一步驟中強制改變了特徵資料的分佈,可能影響了原有資料的資訊表達能力。增加的線性變換使其有機會恢復其原本的資訊。
關於批規一化的更多資訊可以參考 Batch Normalization原理與實戰。
1.2 高解析度分類器(High Resolution Classifier)
這裡要先清楚相比影象的分類任務,目標檢測需要更高的影象解析度。另外,訓練網路時一般都不會從隨機初始化所有的引數來開始的,一般都是用預訓練好的網路來fine-tuning自己的網路,預訓練的網路一般是在ImageNet上訓練好的分類網路。
- YOLOV1預訓練的時候使用224x224的輸入,檢測的時候採用的是448x448的輸入,這會導致分類切換到檢測的時候,模型需要適應影象解析度的改變。
- YOLOV2中將預訓練分成兩步:①:先用224x224的輸入來訓練大概160個epoch,然後再把輸入調整到448x448再訓練10個epoch,然後再與訓練好的模型上進行fine-tuning,檢測的時候用448x448就可以順利過渡了。
這個方法提高了3.7%的mAP.
1.3 引入先驗框(Anchor Box)
在YOLO中在最後網路的全連線層直接預測目標邊框的座標,在YOLO V2中借鑑 Fast R-CNN中的Anchor的思想。
- 去掉了YOLO網路的全連線層和最後的池化層,使提取特徵的網路能夠得到更高解析度的特徵。
- 使用416×416代替448×448作為網路的輸入。這是因為希望得到的特徵圖的尺寸為奇數。奇數大小的寬和高會使得每個特徵圖在劃分cell的時候就只有一個center cell(比如可以劃分成7x7或9x9個cell,center cell只有一個,如果劃分成8x8或10x10的,center cell就有4個)。為什麼希望只有一個center cell呢?因為大的object一般會佔據影象的中心,所以希望用一個center cell去預測,而不是4個center cell去預測。網路最終將416x416的輸入變成13x13大小的feature map輸出,也就是縮小比例為32。(5個池化層,每個池化層將輸入的尺寸縮小1/2)。
- Anchor Boxes 在YOLO中,每個grid cell只預測兩個bbox,最終只能預測98個bbox(7×7×2=98),而在Faster RCNN在輸入大小為1000×600時的boxes數量大概是6000,在SSD300中boxes數量是8732。顯然增加box數量是為了提高object的定位準確率。 過少的bbox顯然影響了YOLO的定位的精度,在YOLO V2中引入了Anchor Boxes的思想,其預測的bbox則會超過千個(以輸出的feature map為13×13為例,每個grid cell有9個anchor box的話,其預測的bbox數量為13×13×9=1521個)。
引入anchor box之後,相對YOLO1的81%的召回率,YOLO2的召回率大幅提升到88%。同時mAP有0.2%的輕微下降。
1.4 引入聚類提取先驗框尺度(Dimension Cluster)
在引入anchor box後,一個問題就是如何確定anchor的位置和大小?Faster RCNN中是手工選定的,每隔stride設定一個anchor,並根據不同的面積比例和長寬比例產生9個(3種大小,3種形狀共9種)anchor box。設想能否一開始就選擇了更好的、更有代表性的先驗Boxes維度,那麼網路就應該更容易學到準確的預測位置。作者的解決辦法就是統計學習中的K-means聚類方法,通過對資料集中的Ground True Box做聚類,找到Ground True Box的統計規律。以聚類個數k為Anchor Boxs個數,以k個聚類中心Box的寬高維度為Anchor Box的維度。
如果按照標準K-means使用歐式距離函式,大Boxes比小Boxes產生更多Error。但是,我們真正想要的是產生好的IOU得分的Boxes(與Box的大小無關)。因此採用瞭如下距離度量:
\[d(box,centroid)=1−IoU(box,centroid) \]
圖1是在VOC和COCO上的聚類結果:
實驗結論:
- 採用聚類分析得到的先驗框比手動設定的平均的IOU值更高,模型更容易訓練和學習。
- 隨著K的增加,平均的IOU是增加的。但是為了綜合考慮模型的複雜度和召回率。最終選擇K=5。使用5個聚類框就已經達到61 Avg IOU,相當於9個手工設定的先驗框60.9 Avg IOU。
作者還發現:The cluster centroids are significantly different than hand-picked anchor boxes. There are fewer short, wide boxes and more tall, thin boxes.這個是個無關緊要的結論了。
1.5 直接位置預測(Direct Location Prediction)
在引入anchor box後,另一個問題就是模型不穩定,特別是在訓練前期,作者認為這種不穩定是因為邊界框(bounding box)中心位置的預測不夠成功。
基於候選框的網路一般是通過預測相對於anchor box中心的偏移值來預測邊界框的的中心座標。公式如下:
\[x=(t_x*w_a)+x_a\\y=(t_y*h_a)+y_a \]
其中 \((x_a,y_a)\) 是anchor box的中心座標,\(w_a、h_a\) 是anchor box的寬和高, \((t_x,t_y)\) 表示預測的偏移值, \((x,y)\) 表示預測的邊界框的中心座標,這個公式對於 \((t_x,t_y)\) 沒有限制,這就表示預測的邊界框容易向任何一個方向偏移,比如當 \(t_x=1\) 時,邊界框就會向右偏移一個anchor box的寬度。所以,每一個預測的邊界框可能處於圖片中的任意位置,這就導致了模型的不穩定。
YOLI V2沿用了V1中的做法,預測邊界框的中心點相對於對應網格左上角的偏移值,每個網格有5個anchor box來預測5個邊界框,每個邊界框預測得到5個值:\(t_x,t_y,t_w,t_h,t_o\) ,前四個是邊界框的座標和邊長資訊,\(t_o\) 則類似於YOLO V1中的置信度,\((c_x,c_y)\) 是當前網格相對於影象左上角的座標,anchor box的先驗寬度和高度為 \(p_w,p_h\) ,那麼參照圖10,預測的公式為:
\[b_x=\delta(t_x)+c_x\\ b_y=\delta(t_y)+c_y\\ b_w=p_we^{(t_w)}\\ b_h=p_he^{(t_h)}\\ Pr(object)*IOU(b,object)=\sigma(t_o) \]
為了將邊界框的中心約束到當前網格中,利用sigmoid函式將 \(t_x,t_y\) 進行歸一化處理,使得模型更加穩定。
通過對比實驗發現,採用維度聚類與直接位置預測比單純使用anchor box的方法在精度能多出5%。
1.6 細粒度特徵(Fine-Gained Features)
YOLO V2最後一層卷積層輸出的是13x13的特徵圖,檢測時也是遵循的這個解析度。這個解析度對於大尺寸目標的檢測是足夠了,但是對於小目標則需要更細粒度的特徵,因為越小的物體在經過層層池化後,體現在最終特徵圖中的可能性越小。
Faser R-CNN和SSD都在不同層次的特徵圖上產生區域建議以獲得多尺度的適應性,YOLO V2則開創性地引入了直通層(passthrough layer),這個直通層有點類似ResNet的dentity mappings結構,將淺層和深層兩種不同尺寸的特徵連線起來。在這裡是將前一層高解析度的特徵圖連線到低解析度的特徵圖上:前一層的特徵圖的維度為26x26x512,在最後一個pooling之前將其1拆4形成4個13x13x512大小的特徵圖,然後將其與最後一層特徵圖(13x13x1024)連線成13x13x(1024+2048)的特徵圖,最後在此特徵圖上進行卷積預測(詳細過程見下圖3)。相當於做了一次特徵融合,有利於檢測小目標。
1.7 多尺度訓練(Multi-Scale Training)
在實際應用時,輸入的影象大小有可能是變化的。我們也將這一點考慮進來。因為我們的網路是全卷積神經網路,只有conv和pooling層,沒有全連線層,這樣就可以處理任意尺寸的影象。為了應對不同尺寸的影象,YOLO V2中在訓練的時候使用不同的尺寸影象。
具體來說,在訓練的時候,每隔一定的epoch(例如10)後就會微調網路,隨機改變網路的輸入影象大小。YOLO V2共進行5次最大池化,即最終的降取樣引數為32,所以隨機生成的影象大小為32的倍數,即{320,352,…,608},最終最小的尺寸為320×320,最大的尺寸為608×608。
該訓練規則強迫模型取適應不同的輸入解析度。模型對於小尺寸的輸入處理速度更快,因此YOLOv2可以按照需求調節速度和準確率。在低解析度情況下(288×288),YOLOv2可以在保持和Fast R-CNN持平的準確率的情況下,處理速度可以達到90FPS。在高解析度情況下,YOLOv2在VOC2007資料集上準確率可以達到state of the art(78.6mAP)
對於目前流行的檢測方法(Faster RCNN,SSD,YOLO)的精度和幀率之間的關係,見下圖4。可以看到,作者在30fps處畫了一條豎線,這是演算法能否達到實時處理的分水嶺。Faster RCNN敗下陣來,而YOLO V2的不同點代表了不同輸入影象解析度下演算法的表現。對於詳細資料,見表格1對比(VOC 2007上進行測試)。
小結
YOLO V2針對YOLO定位不準確以及召回率低的問題,進行一些改變。 主要是借鑑Faster R-CNN的思想,引入了Anchor box。並且使用k-means的方法,通過聚類得到每個Anchor應該生成的Anchor box的的大小和形狀。為了是提取到的特徵有更細的粒度,其網路中借鑑ResNet的思想,將淺層的高解析度特徵和深層的特徵進行了融合,這樣能夠更好的檢測小的目標。 最後,由於YOLO V2的網路是全卷積網路,能夠處理任意尺寸的影象,在訓練的時候使用不同尺度的影象,以應對影象尺寸的變換。
在Better這部分的末尾,作者給出了一個表格,指出了主要提升效能的措施。
二. Faster
為了精度與速度並重,作者在速度上也作了一些改進措施。大多數檢測網路依賴於VGG-16作為特徵提取網路,VGG-16是一個強大而準確的分類網路,但是確過於複雜。224*224的圖片進行一次前向傳播,其卷積層就需要多達306.9億次浮點數運算。
YOLO使用的是基於Googlenet的自定製網路,比VGG-16更快,一次前向傳播僅需85.2億次運算,不過它的精度要略低於VGG-16。224*224圖片取Single-Crop, Top-5 Accuracy,YOLO的定製網路得到88%(VGG-16得到90%)。
2.1 Darknet-19
YOLOv2使用了一個新的分類網路作為特徵提取部分,參考了前人的工作經驗。類似於VGG,網路使用了較多的33卷積核,在每一次池化操作後把通道數翻倍。借鑑了Network In Network的思想,網路使用了全域性平均池化(Global Average Pooling)做預測,把11的卷積核置於3*3的卷積核之間,用來壓縮特徵。使用Batch Normalization穩定模型訓練,加速收斂,正則化模型。
最終得出的基礎模型就是Darknet-19,包含19個卷積層、5個最大值池化層(Max Pooling Layers )。Darknet-19處理一張照片需要55.8億次運算,Imagenet的Top-1準確率為72.9%,Top-5準確率為91.2%。具體的網路結構見表3。
2.2 分類任務訓練(Training For Classification)
作者採用ImageNet1000類資料集來訓練分類模型。訓練過程中,採用了 random crops, rotations, and hue, saturation, and exposure shifts等data augmentation方法。預訓練後,作者採用高解析度影象(448×448)對模型進行finetune。高解析度下訓練的分類網路Top-1準確率76.5%,Top-5準確率93.3%。
2.3 檢測任務訓練(Training For Detection)
為了把分類網路改成檢測網路,作者將分類模型的最後一層卷積層去除,替換為三層卷積層(3×3,1024 filters),最後一層為1×1卷積層,輸出維度filters為需要檢測的數目。對於VOC資料集,預測5種Boxes,每個Box包含5個座標值和20個類別,所以總共是5 * (5+20)= 125個輸出維度。因此,輸出為125(5x20+5x5) filters。最後還加入了passthough 層,從最後3 x 3 x 512的卷積層連到倒數第二層,使模型有了細粒度特徵。
三. Stronger
如之前所說,物體分類,是對整張圖片打標籤,比如這張圖片中含有人,另一張圖片中的物體為狗;而物體檢測不僅對物體的類別進行預測,同時需要框出物體在圖片中的位置。物體分類的資料集,最著名的ImageNet,物體類別有上萬個,而物體檢測資料集,例如coco,只有80個類別,因為物體檢測、分割的打標籤成本比物體分類打標籤成本要高很多。所以在這裡,作者提出了分類、檢測訓練集聯合訓練的方案。
3.1 Joint Classification And Detection(聯合分類和檢測)
使用檢測資料集的圖片去學習檢測相關的資訊,例如Bounding Box 座標預測,是否包含物體以及屬於各個物體的概率。使用僅有類別標籤的分類資料集圖片去擴充套件可以檢測的種類。訓練過程中把監測資料和分類資料混合在一起。基本的思路是,如果是檢測樣本,訓練時其Loss包括分類誤差和定位誤差,如果是分類樣本,則Loss只包括分類誤差。當然,一般的訓練策略為,先在檢測資料集上訓練一定的epoch,待預測框的loss基本穩定後,再聯合分類資料集、檢測資料集進行交替訓練,同時為了分類、檢測資料量平衡,作者對coco資料集進行了上取樣,使得coco資料總數和ImageNet大致相同。
聯合分類與檢測資料集,這裡不同於將網路的backbone在ImageNet上進行預訓練,預訓練只能提高卷積核的魯棒性,而分類檢測資料集聯合,可以擴充識別物體種類。比如狗,ImageNet上就包含超過100多類品種的狗。如果要聯合訓練,需要將這些標籤進行合併。
大部分分類方法採用softmax輸出所有類別的概率。採用softmax的前提假設是類別之間不相互包含(比如,犬和牧羊犬就是相互包含)。因此,我們需要一個多標籤的模型來綜合資料集,使類別之間不相互包含。
作者最後採用WordTree來整合資料集,解決了ImageNet與coco之間的類別問題。
3.2 Dataset combination with WordTree
可以使用WordTree把多個數據集整合在一起。只需要把資料集中的類別對映到樹結構中的同義詞集合(Synsets)。使用WordTree整合ImageNet和COCO的標籤如圖5所示:
樹結構表示物體之間的從屬關係非常合適,第一個大類,物體,物體之下有動物、人工製品、自然物體等,動物中又有更具體的分類。此時,在類別中,不對所有的類別進行softmax操作,而對同一層級的類別進行softmax:
如圖6中所示,同一顏色的位置,進行softmax操作,使得同一顏色中只有一個類別預測分值最大。在預測時,從樹的根節點開始向下檢索,每次選取預測分值最高的子節點,直到所有選擇的節點預測分值連乘後小於某一閾值時停止。在訓練時,如果標籤為人,那麼只對人這個節點以及其所有的父節點進行loss計算,而其子節點,男人、女人、小孩等,不進行loss計算。
最後的結果是,Yolo v2可以識別超過9000個物體,作者美其名曰Yolo9000。當然原文中也提到,只有當父節點在檢測集中出現過,子節點的預測才會有效。如果子節點是褲子、T恤、裙子等,而父節點衣服在檢測集中沒有出現過,那麼整條預測類別支路幾乎都是檢測失效的狀態。這也合理,給神經網路看的都是狗,讓它去預測貓,目前神經網路還沒有這麼智慧。
四. 源程式
tensorflow版本為1.14 。程式碼結構如圖7所示。
訓練圖集為COOC資料集,為了方便,我直接使用的yolo2_coco_checkpoint權重檔案。
4.1 基於圖片的目標檢測
yolo_pic
import tensorflow as tf
import numpy as np
from cv2 import cv2 as cv2
from keras import backend as K
def leaky_relu(x): #leaky relu啟用函式,leaky_relu啟用函式一般用在比較深層次神經網路中
return tf.maximum(0.1*x,x)
class yolov2(object):
def __init__(self,cls_name):
self.anchor_size = [[0.57273, 0.677385], #coco
[1.87446, 2.06253],
[3.33843, 5.47434],
[7.88282, 3.52778],
[9.77052, 9.16828]]
self.num_anchors = len(self.anchor_size)
if cls_name == 'coco':
self.CLASS = ['person', 'bicycle', 'car', 'motorbike', 'aeroplane', 'bus', 'train',
'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep',
'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella',
'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon',
'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot',
'hot dog', 'pizza', 'donut', 'cake', 'chair', 'sofa', 'pottedplant',
'bed', 'diningtable', 'toilet', 'tvmonitor', 'laptop', 'mouse',
'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
'hair drier', 'toothbrush'] #coco
self.f_num = 425
else:
self.CLASS = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
self.f_num = 125
self.num_class = len(self.CLASS)
self.feature_map_size = (13,13)
self.object_scale = 5. #'物體位於gird cell時計算置信度的修正係數'
self.no_object_scale = 1. #'物體位於gird cell時計算置信度的修正係數'
self.class_scale = 1. #'計算分類損失的修正係數'
self.coordinates_scale = 1. #'計算座標損失的修正係數'
#################################NewWork
def conv2d(self,x,filters_num,filters_size,pad_size=0,stride=1,batch_normalize=True,activation=leaky_relu,use_bias=False,name='conv2d'):
if pad_size > 0:
x = tf.pad(x,[[0,0],[pad_size,pad_size],[pad_size,pad_size],[0,0]])
out = tf.layers.conv2d(x,filters=filters_num,kernel_size=filters_size,strides=stride,padding='VALID',activation=None,use_bias=use_bias,name=name)
# BN應該在卷積層conv和啟用函式activation之間,
# (後面有BN層的conv就不用偏置bias,並激活函式activation在後)
if batch_normalize:
out = tf.layers.batch_normalization(out,axis=-1,momentum=0.9,training=False,name=name+'_bn')
if activation:
out = activation(out)
return out
def maxpool(self,x, size=2, stride=2, name='maxpool'):
return tf.layers.max_pooling2d(x, pool_size=size, strides=stride,name=name)
# passthrough
def passthrough(self,x, stride):
return tf.space_to_depth(x, block_size=stride)
#或者tf.extract_image_patches
def darknet(self):
x = tf.placeholder(dtype=tf.float32,shape=[None,416,416,3])
net = self.conv2d(x, filters_num=32, filters_size=3, pad_size=1,
name='conv1')
net = self.maxpool(net, size=2, stride=2, name='pool1')
net = self.conv2d(net, 64, 3, 1, name='conv2')
net = self.maxpool(net, 2, 2, name='pool2')
net = self.conv2d(net, 128, 3, 1, name='conv3_1')
net = self.conv2d(net, 64, 1, 0, name='conv3_2')
net = self.conv2d(net, 128, 3, 1, name='conv3_3')
net = self.maxpool(net, 2, 2, name='pool3')
net = self.conv2d(net, 256, 3, 1, name='conv4_1')
net = self.conv2d(net, 128, 1, 0, name='conv4_2')
net = self.conv2d(net, 256, 3, 1, name='conv4_3')
net = self.maxpool(net, 2, 2, name='pool4')
net = self.conv2d(net, 512, 3, 1, name='conv5_1')
net = self.conv2d(net, 256, 1, 0, name='conv5_2')
net = self.conv2d(net, 512, 3, 1, name='conv5_3')
net = self.conv2d(net, 256, 1, 0, name='conv5_4')
net = self.conv2d(net, 512, 3, 1, name='conv5_5') #
# 這一層特徵圖,要進行後面passthrough
shortcut = net
net = self.maxpool(net, 2, 2, name='pool5') #
net = self.conv2d(net, 1024, 3, 1, name='conv6_1')
net = self.conv2d(net, 512, 1, 0, name='conv6_2')
net = self.conv2d(net, 1024, 3, 1, name='conv6_3')
net = self.conv2d(net, 512, 1, 0, name='conv6_4')
net = self.conv2d(net, 1024, 3, 1, name='conv6_5')
# 訓練檢測網路時去掉了分類網路的網路最後一個卷積層,
# 在後面增加了三個卷積核尺寸為3 * 3,卷積核數量為1024的卷積層,並在這三個卷積層的最後一層後面跟一個卷積核尺寸為1 * 1
# 的卷積層,卷積核數量是(B * (5 + C))。
# 對於VOC資料集,卷積層輸入影象尺寸為416 * 416
# 時最終輸出是13 * 13
# 個柵格,每個柵格預測5種boxes大小,每個box包含5個座標值和20個條件類別概率,所以輸出維度是13 * 13 * 5 * (5 + 20)= 13 * 13 * 125。
#
# 檢測網路加入了passthrough layer,從最後一個輸出為26 * 26 * 512
# 的卷積層連線到新加入的三個卷積核尺寸為3 * 3
# 的卷積層的第二層,使模型有了細粒度特徵。
# 下面這部分主要是training for detection
net = self.conv2d(net, 1024, 3, 1, name='conv7_1')
net = self.conv2d(net, 1024, 3, 1, name='conv7_2')
# shortcut增加了一箇中間卷積層,先採用64個1*1卷積核進行卷積,然後再進行passthrough處理
# 這樣26*26*512 -> 26*26*64 -> 13*13*256的特徵圖
shortcut = self.conv2d(shortcut, 64, 1, 0, name='conv_shortcut')
shortcut = self.passthrough(shortcut, 2)
# 連線之後,變成13*13*(1024+256)
net = tf.concat([shortcut, net],-1) # channel整合到一起,concatenated with the original features,passthrough層與ResNet網路的shortcut類似,以前面更高解析度的特徵圖為輸入,然後將其連線到後面的低解析度特徵圖上,
net = self.conv2d(net, 1024, 3, 1, name='conv8')
# detection layer: 最後用一個1*1卷積去調整channel,該層沒有BN層和啟用函式,變成: S*S*(B*(5+C)),在這裡為:13*13*425
output = self.conv2d(net, filters_num=self.f_num, filters_size=1, batch_normalize=False, activation=None,
use_bias=True, name='conv_dec')
return output,x
#生成anchor ---> decode
def decode(self,net):
self.anchor_size = tf.constant(self.anchor_size , dtype=tf.float32)
net = tf.reshape(net, [-1, 13 * 13, self.num_anchors, self.num_class + 5]) #[batch,169,5,85]
# 偏移量、置信度、類別
#中心座標相對於該cell坐上角的偏移量,sigmoid函式歸一化到(0,1)
xy_offset = tf.nn.sigmoid(net[:, :, :, 0:2])
wh_offset = tf.exp(net[:, :, :, 2:4])
obj_probs = tf.nn.sigmoid(net[:, :, :, 4]) # 置信度,這個東西就是相當於v1中的confidence
class_probs = tf.nn.softmax(net[:, :, :, 5:]) #
# 在feature map對應座標生成anchors,每個座標五個
height_index = tf.range(self.feature_map_size[0], dtype=tf.float32)
width_index = tf.range(self.feature_map_size[1], dtype=tf.float32)
x_cell, y_cell = tf.meshgrid(height_index, width_index)
x_cell = tf.reshape(x_cell, [1, -1, 1]) # 和上面[H*W,num_anchors,num_class+5]對應
y_cell = tf.reshape(y_cell, [1, -1, 1])
# decode
bbox_x = (x_cell + xy_offset[:, :, :, 0]) / 13
bbox_y = (y_cell + xy_offset[:, :, :, 1]) / 13
bbox_w = (self.anchor_size[:, 0] * wh_offset[:, :, :, 0]) / 13
bbox_h = (self.anchor_size[:, 1] * wh_offset[:, :, :, 1]) / 13
bboxes = tf.stack([bbox_x - bbox_w / 2, bbox_y - bbox_h / 2, bbox_x + bbox_w / 2, bbox_y + bbox_h / 2],
axis=3)
return bboxes, obj_probs, class_probs
#將邊界框超出整張圖片(0,0)—(415,415)的部分cut掉
def bboxes_cut(self,bbox_min_max, bboxes):
bboxes = np.copy(bboxes)
bboxes = np.transpose(bboxes)
bbox_min_max = np.transpose(bbox_min_max)
# cut the box
bboxes[0] = np.maximum(bboxes[0], bbox_min_max[0]) # xmin
bboxes[1] = np.maximum(bboxes[1], bbox_min_max[1]) # ymin
bboxes[2] = np.minimum(bboxes[2], bbox_min_max[2]) # xmax
bboxes[3] = np.minimum(bboxes[3], bbox_min_max[3]) # ymax
bboxes = np.transpose(bboxes)
return bboxes
def bboxes_sort(self,classes, scores, bboxes, top_k=400):
index = np.argsort(-scores)
classes = classes[index][:top_k]
scores = scores[index][:top_k]
bboxes = bboxes[index][:top_k]
return classes, scores, bboxes
def bboxes_iou(self,bboxes1, bboxes2):
bboxes1 = np.transpose(bboxes1)
bboxes2 = np.transpose(bboxes2)
int_ymin = np.maximum(bboxes1[0], bboxes2[0])
int_xmin = np.maximum(bboxes1[1], bboxes2[1])
int_ymax = np.minimum(bboxes1[2], bboxes2[2])
int_xmax = np.minimum(bboxes1[3], bboxes2[3])
int_h = np.maximum(int_ymax - int_ymin, 0.)
int_w = np.maximum(int_xmax - int_xmin, 0.)
# 計算IOU
int_vol = int_h * int_w # 交集面積
vol1 = (bboxes1[2] - bboxes1[0]) * (bboxes1[3] - bboxes1[1]) # bboxes1面積
vol2 = (bboxes2[2] - bboxes2[0]) * (bboxes2[3] - bboxes2[1]) # bboxes2面積
IOU = int_vol / (vol1 + vol2 - int_vol) # IOU=交集/並集
return IOU
# NMS,或者用tf.image.non_max_suppression
def bboxes_nms(self,classes, scores, bboxes, nms_threshold=0.2):
keep_bboxes = np.ones(scores.shape, dtype=np.bool)
for i in range(scores.size - 1):
if keep_bboxes[i]:
overlap = self.bboxes_iou(bboxes[i], bboxes[(i + 1):])
keep_overlap = np.logical_or(overlap < nms_threshold,
classes[(i + 1):] != classes[i]) # IOU沒有超過0.5或者是不同的類則儲存下來
keep_bboxes[(i + 1):] = np.logical_and(keep_bboxes[(i + 1):], keep_overlap)
idxes = np.where(keep_bboxes)
return classes[idxes], scores[idxes], bboxes[idxes]
def postprocess(self,bboxes, obj_probs, class_probs, image_shape=(416, 416), threshold=0.5):
bboxes = np.reshape(bboxes, [-1, 4])
# 將所有box還原成圖片中真實的位置
bboxes[:, 0:1] *= float(image_shape[1])
bboxes[:, 1:2] *= float(image_shape[0])
bboxes[:, 2:3] *= float(image_shape[1])
bboxes[:, 3:4] *= float(image_shape[0])
bboxes = bboxes.astype(np.int32) # 轉int
bbox_min_max = [0, 0, image_shape[1] - 1, image_shape[0] - 1]
bboxes = self.bboxes_cut(bbox_min_max, bboxes)
obj_probs = np.reshape(obj_probs, [-1]) # 13*13*5
class_probs = np.reshape(class_probs, [len(obj_probs), -1]) # (13*13*5,80)
class_max_index = np.argmax(class_probs, axis=1) # max類別概率對應的index
class_probs = class_probs[np.arange(len(obj_probs)), class_max_index]
scores = obj_probs * class_probs # 置信度*max類別概率=類別置信度scores
# 類別置信度scores>threshold的邊界框bboxes留下
keep_index = scores > threshold
class_max_index = class_max_index[keep_index]
scores = scores[keep_index]
bboxes = bboxes[keep_index]
# (2)排序top_k(預設為400)
class_max_index, scores, bboxes = self.bboxes_sort(class_max_index, scores, bboxes)
# (3)NMS
class_max_index, scores, bboxes = self.bboxes_nms(class_max_index, scores, bboxes)
return bboxes, scores, class_max_index
def preprocess_image(self,image, image_size=(416, 416)):
image_cp = np.copy(image).astype(np.float32)
image_rgb = cv2.cvtColor(image_cp, cv2.COLOR_BGR2RGB)
image_resized = cv2.resize(image_rgb, image_size)
image_normalized = image_resized.astype(np.float32) / 225.0
image_expanded = np.expand_dims(image_normalized, axis=0)
return image_expanded
'''
train part
'''
def preprocess_true_boxes(self,true_box,anchors,img_size = (416,416)):
'''
:param true_box:實際框的位置和類別,2D TENSOR:(batch,5)
:param anchors:anchors : 實際anchor boxes 的值,論文中使用了五個。[w,h],都是相對於gird cell 的比值。
2d
第二個維度:[w,h],w,h,都是相對於gird cell長寬的比值。
[1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52]
:param img_size:
:return:
-detectors_mask: 取值是0或者1,這裡的shape是[13,13,5,1]
第四個維度:0/1。1的就是用於預測改true boxes 的 anchor boxes
-matching_true_boxes:這裡的shape是[13,13,5,5]。
'''
w,h = img_size
feature_w = w // 32
feature_h = h // 32
num_box_params = true_box.shape[1]
detectors_mask = np.zeros((feature_h,feature_w,self.num_anchors,1),dtype=np.float32)
matching_true_boxes = np.zeros((feature_h,feature_w,self.num_anchors,num_box_params),dtype=np.float32)
for i in true_box:
#提取類別資訊,屬於哪類
box_class = i[4:5]
#換算成相對於gird cell的值
box = i[0:4] * np.array([feature_w, feature_h, feature_w, feature_h])
k = np.floor(box[1]).astype('int') #y方向上屬於第幾個gird cell
j = np.floor(box[0]).astype('int') #x方向上屬於第幾個gird cell
best_iou = 0
best_anchor = 0
#計算anchor boxes 和 true boxes的iou ,一個true box一個best anchor
for m,anchor in enumerate(anchors):
box_maxes = box[2:4] / 2.
box_mins = -box_maxes
anchor_maxes = (anchor / 2.)
anchor_mins = -anchor_maxes
intersect_mins = np.maximum(box_mins, anchor_mins)
intersect_maxes = np.minimum(box_maxes, anchor_maxes)
intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[0] * intersect_wh[1]
box_area = box[2] * box[3]
anchor_area = anchor[0] * anchor[1]
iou = intersect_area / (box_area + anchor_area - intersect_area)
if iou > best_iou:
best_iou = iou
best_anchor = m
if best_iou > 0:
detectors_mask[k, j, best_anchor] = 1
adjusted_box = np.array( #找到最佳預測anchor boxes
[
box[0] - j, box[1] - k, #'x,y都是相對於gird cell的位置,左上角[0,0],右下角[1,1]'
np.log(box[2] / anchors[best_anchor][0]), #'對應實際框w,h和anchor boxes w,h的比值取log函式'
np.log(box[3] / anchors[best_anchor][1]), box_class #'class實際框的物體是屬於第幾類'
],
dtype=np.float32)
matching_true_boxes[k, j, best_anchor] = adjusted_box
return detectors_mask, matching_true_boxes
def yolo_head(self,feature_map, anchors, num_classes):
'''
這個函式是輸入yolo的輸出層的特徵,轉化成相對於gird cell座標的x,y,相對於gird cell長寬的w,h,
pred_confidence是判斷否存在物體的概率,pred_class_prob是sofrmax後各個類別分別的概率
:param feats: 網路最後一層輸出 [none,13,13,125]/[none,13,13,425]
:param anchors:[5,n]
:param num_classes:類別數
:return:x,y,w,h在loss function中計算iou,然後計算iou損失。
然後和pred_confidence計算confidence_loss,pred_class_prob用於計算classification_loss。
box_xy : 每張圖片的每個gird cell中的每個pred_boxes中心點x,y相對於其所在gird cell的座標值,左上頂點為[0,0],右下頂點為[1,1]。
shape:[-1,13,13,5,2].
box_wh : 每張圖片的每個gird cell中的每個pred_boxes的w,h都是相對於gird cell的比值
shape:[-1,13,13,5,2].
box_confidence : 每張圖片的每個gird cell中的每個pred_boxes的,判斷是否存在可檢測物體的概率。
shape:[-1,13,13,5,1]。各維度資訊同上。
box_class_pred : 每張圖片的每個gird cell中的每個pred_boxes所框起來的各個類別分別的概率(經過了softmax)。
shape:[-1,13,13,5,20/80]
'''
anchors = tf.reshape(tf.constant(anchors,dtype=tf.float32),[1,1,1,self.num_anchors,2])
num_gird_cell = tf.shape(feature_map)[1:3] #[13,13]
conv_height_index = K.arange(0,stop=num_gird_cell[0])
conv_width_index = K.arange(0,stop=num_gird_cell[1])
conv_height_index = tf.tile(conv_height_index, [num_gird_cell[1]])
conv_width_index = tf.tile(
tf.expand_dims(conv_width_index, 0), [num_gird_cell[0], 1])
conv_width_index = K.flatten(K.transpose(conv_width_index))
conv_index = K.transpose(K.stack([conv_height_index,conv_width_index]))
conv_index = K.reshape(conv_index,[1,num_gird_cell[0],num_gird_cell[1],1,2])#[1,13,13,1,2]
conv_index = K.cast(conv_index,K.dtype(feature_map))
#[[0,0][0,1]....[0,12],[1,0]...]
feature_map = K.reshape(feature_map,[-1,num_gird_cell[0],num_gird_cell[1],self.num_anchors,self.num_class + 5])
num_gird_cell = K.cast(K.reshape(num_gird_cell,[1,1,1,1,2]),K.dtype(feature_map))
box_xy = K.sigmoid(feature_map[...,:2])
box_wh = K.exp(feature_map[...,2:4])
confidence = K.sigmoid(feature_map[...,4:5])
cls_prob = K.softmax(feature_map[...,5:])
xy = (box_xy + conv_index) / num_gird_cell
wh = box_wh * anchors / num_gird_cell
return xy,wh,confidence,cls_prob
def loss(self,
net,
true_boxes,
detectors_mask,
matching_true_boxes,
anchors,
num_classes):
'''
IOU損失,分類損失,座標損失
confidence_loss:
共有845個anchor_boxes,與true_boxes匹配的用於預測pred_boxes,
未與true_boxes匹配的anchor_boxes用於預測background。在未與true_boxes匹配的anchor_boxes中,
與true_boxes的IOU小於0.6的被標記為background,這部分預測正確,未造成損失。
但未與true_boxes匹配的anchor_boxes中,若與true_boxes的IOU大於0.6的我們需要計算其損失,
因為它未能準確預測background,與true_boxes重合度過高,就是no_objects_loss。
而objects_loss則是與true_boxes匹配的anchor_boxes的預測誤差。與YOLOv1不同的是修正係數的改變,
YOLOv1中no_objects_loss和objects_loss分別是0.5和1,而YOLOv2中則是1和5。
classification_loss:
經過softmax()後,20維向量(資料集中分類種類為20種)的均方誤差。
coordinates_loss:
計算x,y的誤差由相對於整個影象(416x416)的offset座標誤差的均方改變為相對於gird cell的offset(這個offset是取sigmoid函式得到的處於(0,1)的值)座標誤差的均方。
也將修正係數由5改為了1 。計算w,h的誤差由w,h平方根的差的均方誤差變為了,
w,h與對true_boxes匹配的anchor_boxes的長寬的比值取log函式,
和YOLOv1的想法一樣,對於相等的誤差值,降低對大物體誤差的懲罰,加大對小物體誤差的懲罰。同時也將修正係數由5改為了1。
:param net:[batch_size,13,13,125],網路最後一層輸出
:param true_boxes:實際框的位置和類別 [batch,5]
:param detectors_mask:取值是0或者1,[ batch_size,13,13,5,1]
1的就是用於預測改true boxes 的 anchor boxes
:param matching_true_boxes:[-1,13,13,5,5]
:param anchors:
:param num_classes:20
:return:
'''
xy, wh, confidence, cls_prob = self.yolo_head(net,anchors,num_classes)
shape = tf.shape(net)
feature_map = tf.reshape(net,[-1,shape[1],shape[2],self.num_anchors,num_classes + 5])
#用於和matching_true_boxes計算座標損失
pred_box = tf.concat([K.sigmoid(feature_map[...,0:2]),feature_map[...,2:4]],-1)
pred_xy = tf.to_float(tf.expand_dims(xy,4))#[-1,13,13,5,2]-->[-1,13,13,5,1,2]
pred_wh = tf.to_float(tf.expand_dims(wh,4))
pred_min = tf.to_float(pred_xy - pred_wh / 2.0)
pred_max = tf.to_float(pred_xy + pred_wh / 2.0)
true_box_shape = K.shape(true_boxes)
print(true_box_shape)
true_boxes = K.reshape(true_boxes,[-1,1,1,1,true_box_shape[1], 5])
#[-1,1,1,1,-1,5],batch, conv_height, conv_width, num_anchors, num_true_boxes, box_params'
true_xy = tf.to_float(true_boxes[...,0:2])
true_wh = tf.to_float(true_boxes[...,2:4])
true_min = tf.to_float(true_xy - true_wh / 2.0)
true_max = tf.to_float(true_xy + true_wh / 2.0)
#計算所以abox和tbox的iou
intersect_mins = tf.maximum(pred_min, true_min)
intersect_maxes = tf.minimum(pred_max, true_max)
intersect_wh = tf.maximum(intersect_maxes - intersect_mins, 0.)
intersect_areas = tf.to_float(intersect_wh[..., 0] * intersect_wh[..., 1])
pred_areas = pred_wh[..., 0] * pred_wh[..., 1]
true_areas = true_wh[..., 0] * true_wh[..., 1]
union_areas = pred_areas + true_areas - intersect_areas
iou_scores = intersect_areas / union_areas
#可能會有多個tbox落在同一個cell ,只去iou最大的
# tf.argmax(iou_scores,4)
best_ious = K.max(iou_scores, axis=4)
best_ious = tf.expand_dims(best_ious,axis=-1)
#選出IOU大於0.6的,若IOU小於0.6的被標記為background,
obj_dec = tf.cast(best_ious > 0.6,dtype=K.dtype(best_ious))
#IOU loss
no_obj_w = (self.no_object_scale * obj_dec * detectors_mask) #
no_obj_loss = no_obj_w * tf.square(-confidence)
obj_loss = self.object_scale * detectors_mask * tf.square(1 - confidence)
confidence_loss = no_obj_loss + obj_loss
#class loss
match_cls = tf.cast(matching_true_boxes[...,4],dtype=tf.int32)
match_cls = tf.one_hot(match_cls,num_classes)
class_loss = (self.class_scale * detectors_mask * tf.square(match_cls - cls_prob))
#座標loss
match_box = matching_true_boxes[...,0:4]
coord_loss = self.coordinates_scale * detectors_mask * tf.square(match_box - pred_box)
confidence_loss_sum = K.sum(confidence_loss)
class_loss_sum = K.sum(class_loss)
coord_loss_sum = K.sum(coord_loss)
all_loss = 0.5 * (confidence_loss_sum + class_loss_sum + coord_loss_sum)
return all_loss
def draw_detection(self,im, bboxes, scores, cls_inds, labels):
imgcv = np.copy(im)
h, w, _ = imgcv.shape
for i, box in enumerate(bboxes):
cls_indx = cls_inds[i]
thick = int((h + w) / 1000)
cv2.rectangle(imgcv, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), thick)
print("[x, y, w, h]=[%d, %d, %d, %d]" % (box[0], box[1], box[2], box[3]))
mess = '%s: %.3f' % (labels[cls_indx], scores[i])
text_loc = (box[0], box[1] - 10)
cv2.putText(imgcv, mess, text_loc, cv2.FONT_HERSHEY_SIMPLEX, 1e-3 * h, (0, 0, 255), thick)
# return imgcv
cv2.imshow("detection_results", imgcv) # 顯示圖片
cv2.waitKey(0)
#v1 - v2 , v2 - v3
# 1、加入BN層 批次歸一化 input --> 均值為0方差為1正太分佈
# ---》白化 --> 對‘input 變換到 均值0單位方差內的分佈
# #使用:input * w -->bn
if __name__ == '__main__':
network = yolov2('coco')
net,x = network.darknet()
saver = tf.train.Saver()
ckpt_path = './model/v2/yolo2_coco.ckpt'
sess = tf.Session()
sess.run(tf.global_variables_initializer())
saver.restore(sess,ckpt_path)
img = cv2.imread('./test/3.jpg')
#shape = img.shape[:2]
img_r = network.preprocess_image(img)
bboxes, obj_probs, class_probs = network.decode(net)
bboxes, obj_probs, class_probs = sess.run([bboxes, obj_probs, class_probs],feed_dict={x:img_r})
bboxes, scores, class_max_index = network.postprocess(bboxes, obj_probs, class_probs)
print('置信度:',end="")
print(scores)
print('類別資訊:',end="")
print(class_max_index)
img_detection = network.draw_detection(cv2.resize(img,(416,416)), bboxes, scores, class_max_index, network.CLASS)
'''
yi、
第一大層 :conv maxpoiling
第2大層:3個卷積,maxpool
3:3個卷積,maxpool
4:3卷積,maxpool
5:5卷積,maxpool -----------
6:5卷積 | + add
7三個卷積---------------------
conv
er:
ahchors生成和decode
san:
裁剪、選出前TOP_K,NMS
'''
執行結果:
測試1:
對同一張測試圖片分別做V1和V2版本的目標檢測,對比圖如下。
從對比圖中可以看出:在YOLO V1中,對於本張測試圖片,程式只檢測出了人和貓兩個物體,並且它們的置信度只有0.249和0.504;而在V2版本中,不僅檢測到了更多的物體,人和貓的檢測置信度也高達0.778和0.797,說明準確率也在提高。此外,程式在顯示多個boungding box的同時也輸出了他們的座標以及大小資訊。
測試2:
當然,在V1中有一個失敗的測試,即那個行人、車輛都很密集且都尺寸比較小的圖片,很遺憾在V2的版本中也沒有檢測到任何物體。
測試3:
最後,以我的女神tsy與她劇組的合照作為測試的結尾,效果還是很好的。
4.2 基於視訊的目標檢測
yolo_video
import tensorflow as tf
import numpy as np
from cv2 import cv2 as cv2
from keras import backend as K
def leaky_relu(x): #leaky relu啟用函式,leaky_relu啟用函式一般用在比較深層次神經網路中
return tf.maximum(0.1*x,x)
class yolov2(object):
def __init__(self,cls_name):
self.anchor_size = [[0.57273, 0.677385], #coco
[1.87446, 2.06253],
[3.33843, 5.47434],
[7.88282, 3.52778],
[9.77052, 9.16828]]
self.num_anchors = len(self.anchor_size)
if cls_name == 'coco':
self.CLASS = ['person', 'bicycle', 'car', 'motorbike', 'aeroplane', 'bus', 'train',
'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep',
'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella',
'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon',
'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot',
'hot dog', 'pizza', 'donut', 'cake', 'chair', 'sofa', 'pottedplant',
'bed', 'diningtable', 'toilet', 'tvmonitor', 'laptop', 'mouse',
'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
'hair drier', 'toothbrush'] #coco
self.f_num = 425
else:
self.CLASS = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
self.f_num = 125
self.num_class = len(self.CLASS)
self.feature_map_size = (13,13)
self.object_scale = 5. #'物體位於gird cell時計算置信度的修正係數'
self.no_object_scale = 1. #'物體位於gird cell時計算置信度的修正係數'
self.class_scale = 1. #'計算分類損失的修正係數'
self.coordinates_scale = 1. #'計算座標損失的修正係數'
# NewWork
def conv2d(self,x,filters_num,filters_size,pad_size=0,stride=1,batch_normalize=True,activation=leaky_relu,use_bias=False,name='conv2d'):
if pad_size > 0:
x = tf.pad(x,[[0,0],[pad_size,pad_size],[pad_size,pad_size],[0,0]])
out = tf.layers.conv2d(x,filters=filters_num,kernel_size=filters_size,strides=stride,padding='VALID',activation=None,use_bias=use_bias,name=name)
# BN應該在卷積層conv和啟用函式activation之間,
# (後面有BN層的conv就不用偏置bias,並激活函式activation在後)
if batch_normalize:
out = tf.layers.batch_normalization(out,axis=-1,momentum=0.9,training=False,name=name+'_bn')
if activation:
out = activation(out)
return out
def maxpool(self,x, size=2, stride=2, name='maxpool'):
return tf.layers.max_pooling2d(x, pool_size=size, strides=stride,name=name)
# passthrough
def passthrough(self,x, stride):
return tf.space_to_depth(x, block_size=stride)
#或者tf.extract_image_patches
def darknet(self):
x = tf.placeholder(dtype=tf.float32,shape=[None,416,416,3])
net = self.conv2d(x, filters_num=32, filters_size=3, pad_size=1,
name='conv1')
net = self.maxpool(net, size=2, stride=2, name='pool1')
net = self.conv2d(net, 64, 3, 1, name='conv2')
net = self.maxpool(net, 2, 2, name='pool2')
net = self.conv2d(net, 128, 3, 1, name='conv3_1')
net = self.conv2d(net, 64, 1, 0, name='conv3_2')
net = self.conv2d(net, 128, 3, 1, name='conv3_3')
net = self.maxpool(net, 2, 2, name='pool3')
net = self.conv2d(net, 256, 3, 1, name='conv4_1')
net = self.conv2d(net, 128, 1, 0, name='conv4_2')
net = self.conv2d(net, 256, 3, 1, name='conv4_3')
net = self.maxpool(net, 2, 2, name='pool4')
net = self.conv2d(net, 512, 3, 1, name='conv5_1')
net = self.conv2d(net, 256, 1, 0, name='conv5_2')
net = self.conv2d(net, 512, 3, 1, name='conv5_3')
net = self.conv2d(net, 256, 1, 0, name='conv5_4')
net = self.conv2d(net, 512, 3, 1, name='conv5_5') #
# 這一層特徵圖,要進行後面passthrough
shortcut = net
net = self.maxpool(net, 2, 2, name='pool5') #
net = self.conv2d(net, 1024, 3, 1, name='conv6_1')
net = self.conv2d(net, 512, 1, 0, name='conv6_2')
net = self.conv2d(net, 1024, 3, 1, name='conv6_3')
net = self.conv2d(net, 512, 1, 0, name='conv6_4')
net = self.conv2d(net, 1024, 3, 1, name='conv6_5')
# 訓練檢測網路時去掉了分類網路的網路最後一個卷積層,
# 在後面增加了三個卷積核尺寸為3 * 3,卷積核數量為1024的卷積層,並在這三個卷積層的最後一層後面跟一個卷積核尺寸為1 * 1
# 的卷積層,卷積核數量是(B * (5 + C))。
# 對於VOC資料集,卷積層輸入影象尺寸為416 * 416
# 時最終輸出是13 * 13
# 個柵格,每個柵格預測5種boxes大小,每個box包含5個座標值和20個條件類別概率,所以輸出維度是13 * 13 * 5 * (5 + 20)= 13 * 13 * 125。
#
# 檢測網路加入了passthrough layer,從最後一個輸出為26 * 26 * 512
# 的卷積層連線到新加入的三個卷積核尺寸為3 * 3
# 的卷積層的第二層,使模型有了細粒度特徵。
# 下面這部分主要是training for detection
net = self.conv2d(net, 1024, 3, 1, name='conv7_1')
net = self.conv2d(net, 1024, 3, 1, name='conv7_2')
# shortcut增加了一箇中間卷積層,先採用64個1*1卷積核進行卷積,然後再進行passthrough處理
# 這樣26*26*512 -> 26*26*64 -> 13*13*256的特徵圖
shortcut = self.conv2d(shortcut, 64, 1, 0, name='conv_shortcut')
shortcut = self.passthrough(shortcut, 2)
# 連線之後,變成13*13*(1024+256)
net = tf.concat([shortcut, net],axis=-1) # channel整合到一起,concatenated with the original features,passthrough層與ResNet網路的shortcut類似,以前面更高解析度的特徵圖為輸入,然後將其連線到後面的低解析度特徵圖上,
net = self.conv2d(net, 1024, 3, 1, name='conv8')
# detection layer: 最後用一個1*1卷積去調整channel,該層沒有BN層和啟用函式,變成: S*S*(B*(5+C)),在這裡為:13*13*425
output = self.conv2d(net, filters_num=self.f_num, filters_size=1, batch_normalize=False, activation=None,
use_bias=True, name='conv_dec')
return output,x
#生成anchor ---> decode
def decode(self,net):
self.anchor_size = tf.constant(self.anchor_size , dtype=tf.float32)
net = tf.reshape(net, [-1, 13 * 13, self.num_anchors, self.num_class + 5]) #[batch,169,5,85]
# 偏移量、置信度、類別
#中心座標相對於該cell坐上角的偏移量,sigmoid函式歸一化到(0,1)
xy_offset = tf.nn.sigmoid(net[:, :, :, 0:2])
wh_offset = tf.exp(net[:, :, :, 2:4])
obj_probs = tf.nn.sigmoid(net[:, :, :, 4]) # 置信度,這個東西就是相當於v1中的confidence
class_probs = tf.nn.softmax(net[:, :, :, 5:]) #
# 在feature map對應座標生成anchors,每個座標五個
height_index = tf.range(self.feature_map_size[0], dtype=tf.float32)
width_index = tf.range(self.feature_map_size[1], dtype=tf.float32)
x_cell, y_cell = tf.meshgrid(height_index, width_index)
x_cell = tf.reshape(x_cell, [1, -1, 1]) # 和上面[H*W,num_anchors,num_class+5]對應
y_cell = tf.reshape(y_cell, [1, -1, 1])
# decode
bbox_x = (x_cell + xy_offset[:, :, :, 0]) / 13
bbox_y = (y_cell + xy_offset[:, :, :, 1]) / 13
bbox_w = (self.anchor_size[:, 0] * wh_offset[:, :, :, 0]) / 13
bbox_h = (self.anchor_size[:, 1] * wh_offset[:, :, :, 1]) / 13
bboxes = tf.stack([bbox_x - bbox_w / 2, bbox_y - bbox_h / 2, bbox_x + bbox_w / 2, bbox_y + bbox_h / 2],
axis=3)
return bboxes, obj_probs, class_probs
#將邊界框超出整張圖片(0,0)—(415,415)的部分cut掉
def bboxes_cut(self,bbox_min_max, bboxes):
bboxes = np.copy(bboxes)
bboxes = np.transpose(bboxes)
bbox_min_max = np.transpose(bbox_min_max)
# cut the box
bboxes[0] = np.maximum(bboxes[0], bbox_min_max[0]) # xmin
bboxes[1] = np.maximum(bboxes[1], bbox_min_max[1]) # ymin
bboxes[2] = np.minimum(bboxes[2], bbox_min_max[2]) # xmax
bboxes[3] = np.minimum(bboxes[3], bbox_min_max[3]) # ymax
bboxes = np.transpose(bboxes)
return bboxes
def bboxes_sort(self,classes, scores, bboxes, top_k=400):
index = np.argsort(-scores)
classes = classes[index][:top_k]
scores = scores[index][:top_k]
bboxes = bboxes[index][:top_k]
return classes, scores, bboxes
def bboxes_iou(self,bboxes1, bboxes2):
bboxes1 = np.transpose(bboxes1)
bboxes2 = np.transpose(bboxes2)
int_ymin = np.maximum(bboxes1[0], bboxes2[0])
int_xmin = np.maximum(bboxes1[1], bboxes2[1])
int_ymax = np.minimum(bboxes1[2], bboxes2[2])
int_xmax = np.minimum(bboxes1[3], bboxes2[3])
int_h = np.maximum(int_ymax - int_ymin, 0.)
int_w = np.maximum(int_xmax - int_xmin, 0.)
# 計算IOU
int_vol = int_h * int_w # 交集面積
vol1 = (bboxes1[2] - bboxes1[0]) * (bboxes1[3] - bboxes1[1]) # bboxes1面積
vol2 = (bboxes2[2] - bboxes2[0]) * (bboxes2[3] - bboxes2[1]) # bboxes2面積
IOU = int_vol / (vol1 + vol2 - int_vol) # IOU=交集/並集
return IOU
# NMS,或者用tf.image.non_max_suppression
def bboxes_nms(self,classes, scores, bboxes, nms_threshold=0.2):
keep_bboxes = np.ones(scores.shape, dtype=np.bool)
for i in range(scores.size - 1):
if keep_bboxes[i]:
overlap = self.bboxes_iou(bboxes[i], bboxes[(i + 1):])
keep_overlap = np.logical_or(overlap < nms_threshold,
classes[(i + 1):] != classes[i]) # IOU沒有超過0.5或者是不同的類則儲存下來
keep_bboxes[(i + 1):] = np.logical_and(keep_bboxes[(i + 1):], keep_overlap)
idxes = np.where(keep_bboxes)
return classes[idxes], scores[idxes], bboxes[idxes]
def postprocess(self,bboxes, obj_probs, class_probs, image_shape=(416, 416), threshold=0.5):
bboxes = np.reshape(bboxes, [-1, 4])
# 將所有box還原成圖片中真實的位置
bboxes[:, 0:1] *= float(image_shape[1])
bboxes[:, 1:2] *= float(image_shape[0])
bboxes[:, 2:3] *= float(image_shape[1])
bboxes[:, 3:4] *= float(image_shape[0])
bboxes = bboxes.astype(np.int32) # 轉int
bbox_min_max = [0, 0, image_shape[1] - 1, image_shape[0] - 1]
bboxes = self.bboxes_cut(bbox_min_max, bboxes)
obj_probs = np.reshape(obj_probs, [-1]) # 13*13*5
class_probs = np.reshape(class_probs, [len(obj_probs), -1]) # (13*13*5,80)
class_max_index = np.argmax(class_probs, axis=1) # max類別概率對應的index
class_probs = class_probs[np.arange(len(obj_probs)), class_max_index]
scores = obj_probs * class_probs # 置信度*max類別概率=類別置信度scores
# 類別置信度scores>threshold的邊界框bboxes留下
keep_index = scores > threshold
class_max_index = class_max_index[keep_index]
scores = scores[keep_index]
bboxes = bboxes[keep_index]
# (2)排序top_k(預設為400)
class_max_index, scores, bboxes = self.bboxes_sort(class_max_index, scores, bboxes)
# (3)NMS
class_max_index, scores, bboxes = self.bboxes_nms(class_max_index, scores, bboxes)
return bboxes, scores, class_max_index
def preprocess_image(self,image, image_size=(416, 416)):
image_cp = np.copy(image).astype(np.float32)
image_rgb = cv2.cvtColor(image_cp, cv2.COLOR_BGR2RGB)
image_resized = cv2.resize(image_rgb, image_size)
image_normalized = image_resized.astype(np.float32) / 225.0
image_expanded = np.expand_dims(image_normalized, axis=0)
return image_expanded
'''
train part
'''
def preprocess_true_boxes(self,true_box,anchors,img_size = (416,416)):
'''
:param true_box:實際框的位置和類別,2D TENSOR:(batch,5)
:param anchors:anchors : 實際anchor boxes 的值,論文中使用了五個。[w,h],都是相對於gird cell 的比值。
2d
第二個維度:[w,h],w,h,都是相對於gird cell長寬的比值。
[1.08, 1.19], [3.42, 4.41], [6.63, 11.38], [9.42, 5.11], [16.62, 10.52]
:param img_size:
:return:
-detectors_mask: 取值是0或者1,這裡的shape是[13,13,5,1]
第四個維度:0/1。1的就是用於預測改true boxes 的 anchor boxes
-matching_true_boxes:這裡的shape是[13,13,5,5]。
'''
w,h = img_size
feature_w = w // 32
feature_h = h // 32
num_box_params = true_box.shape[1]
detectors_mask = np.zeros((feature_h,feature_w,self.num_anchors,1),dtype=np.float32)
matching_true_boxes = np.zeros((feature_h,feature_w,self.num_anchors,num_box_params),dtype=np.float32)
for i in true_box:
#提取類別資訊,屬於哪類
box_class = i[4:5]
#換算成相對於gird cell的值
box = i[0:4] * np.array([feature_w, feature_h, feature_w, feature_h])
k = np.floor(box[1]).astype('int') #y方向上屬於第幾個gird cell
j = np.floor(box[0]).astype('int') #x方向上屬於第幾個gird cell
best_iou = 0
best_anchor = 0
#計算anchor boxes 和 true boxes的iou ,一個true box一個best anchor
for m,anchor in enumerate(anchors):
box_maxes = box[2:4] / 2.
box_mins = -box_maxes
anchor_maxes = (anchor / 2.)
anchor_mins = -anchor_maxes
intersect_mins = np.maximum(box_mins, anchor_mins)
intersect_maxes = np.minimum(box_maxes, anchor_maxes)
intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[0] * intersect_wh[1]
box_area = box[2] * box[3]
anchor_area = anchor[0] * anchor[1]
iou = intersect_area / (box_area + anchor_area - intersect_area)
if iou > best_iou:
best_iou = iou
best_anchor = m
if best_iou > 0:
detectors_mask[k, j, best_anchor] = 1
adjusted_box = np.array( #找到最佳預測anchor boxes
[
box[0] - j, box[1] - k, #'x,y都是相對於gird cell的位置,左上角[0,0],右下角[1,1]'
np.log(box[2] / anchors[best_anchor][0]), #'對應實際框w,h和anchor boxes w,h的比值取log函式'
np.log(box[3] / anchors[best_anchor][1]), box_class #'class實際框的物體是屬於第幾類'
],
dtype=np.float32)
matching_true_boxes[k, j, best_anchor] = adjusted_box
return detectors_mask, matching_true_boxes
def yolo_head(self,feature_map, anchors, num_classes):
'''
這個函式是輸入yolo的輸出層的特徵,轉化成相對於gird cell座標的x,y,相對於gird cell長寬的w,h,
pred_confidence是判斷否存在物體的概率,pred_class_prob是sofrmax後各個類別分別的概率
:param feats: 網路最後一層輸出 [none,13,13,125]/[none,13,13,425]
:param anchors:[5,n]
:param num_classes:類別數
:return:x,y,w,h在loss function中計算iou,然後計算iou損失。
然後和pred_confidence計算confidence_loss,pred_class_prob用於計算classification_loss。
box_xy : 每張圖片的每個gird cell中的每個pred_boxes中心點x,y相對於其所在gird cell的座標值,左上頂點為[0,0],右下頂點為[1,1]。
shape:[-1,13,13,5,2].
box_wh : 每張圖片的每個gird cell中的每個pred_boxes的w,h都是相對於gird cell的比值
shape:[-1,13,13,5,2].
box_confidence : 每張圖片的每個gird cell中的每個pred_boxes的,判斷是否存在可檢測物體的概率。
shape:[-1,13,13,5,1]。各維度資訊同上。
box_class_pred : 每張圖片的每個gird cell中的每個pred_boxes所框起來的各個類別分別的概率(經過了softmax)。
shape:[-1,13,13,5,20/80]
'''
anchors = tf.reshape(tf.constant(anchors,dtype=tf.float32),[1,1,1,self.num_anchors,2])
num_gird_cell = tf.shape(feature_map)[1:3] #[13,13]
conv_height_index = K.arange(0,stop=num_gird_cell[0])
conv_width_index = K.arange(0,stop=num_gird_cell[1])
conv_height_index = tf.tile(conv_height_index, [num_gird_cell[1]])
conv_width_index = tf.tile(
tf.expand_dims(conv_width_index, 0), [num_gird_cell[0], 1])
conv_width_index = K.flatten(K.transpose(conv_width_index))
conv_index = K.transpose(K.stack([conv_height_index,conv_width_index]))
conv_index = K.reshape(conv_index,[1,num_gird_cell[0],num_gird_cell[1],1,2])#[1,13,13,1,2]
conv_index = K.cast(conv_index,K.dtype(feature_map))
#[[0,0][0,1]....[0,12],[1,0]...]
feature_map = K.reshape(feature_map,[-1,num_gird_cell[0],num_gird_cell[1],self.num_anchors,self.num_class + 5])
num_gird_cell = K.cast(K.reshape(num_gird_cell,[1,1,1,1,2]),K.dtype(feature_map))
box_xy = K.sigmoid(feature_map[...,:2])
box_wh = K.exp(feature_map[...,2:4])
confidence = K.sigmoid(feature_map[...,4:5])
cls_prob = K.softmax(feature_map[...,5:])
xy = (box_xy + conv_index) / num_gird_cell
wh = box_wh * anchors / num_gird_cell
return xy,wh,confidence,cls_prob
def loss(self,
net,
true_boxes,
detectors_mask,
matching_true_boxes,
anchors,
num_classes):
'''
IOU損失,分類損失,座標損失
confidence_loss:
共有845個anchor_boxes,與true_boxes匹配的用於預測pred_boxes,
未與true_boxes匹配的anchor_boxes用於預測background。在未與true_boxes匹配的anchor_boxes中,
與true_boxes的IOU小於0.6的被標記為background,這部分預測正確,未造成損失。
但未與true_boxes匹配的anchor_boxes中,若與true_boxes的IOU大於0.6的我們需要計算其損失,
因為它未能準確預測background,與true_boxes重合度過高,就是no_objects_loss。
而objects_loss則是與true_boxes匹配的anchor_boxes的預測誤差。與YOLOv1不同的是修正係數的改變,
YOLOv1中no_objects_loss和objects_loss分別是0.5和1,而YOLOv2中則是1和5。
classification_loss:
經過softmax()後,20維向量(資料集中分類種類為20種)的均方誤差。
coordinates_loss:
計算x,y的誤差由相對於整個影象(416x416)的offset座標誤差的均方改變為相對於gird cell的offset(這個offset是取sigmoid函式得到的處於(0,1)的值)座標誤差的均方。
也將修正係數由5改為了1 。計算w,h的誤差由w,h平方根的差的均方誤差變為了,
w,h與對true_boxes匹配的anchor_boxes的長寬的比值取log函式,
和YOLOv1的想法一樣,對於相等的誤差值,降低對大物體誤差的懲罰,加大對小物體誤差的懲罰。同時也將修正係數由5改為了1。
:param net:[batch_size,13,13,125],網路最後一層輸出
:param true_boxes:實際框的位置和類別 [batch,5]
:param detectors_mask:取值是0或者1,[ batch_size,13,13,5,1]
1的就是用於預測改true boxes 的 anchor boxes
:param matching_true_boxes:[-1,13,13,5,5]
:param anchors:
:param num_classes:20
:return:
'''
xy, wh, confidence, cls_prob = self.yolo_head(net,anchors,num_classes)
shape = tf.shape(net)
feature_map = tf.reshape(net,[-1,shape[1],shape[2],self.num_anchors,num_classes + 5])
#用於和matching_true_boxes計算座標損失
pred_box = tf.concat([K.sigmoid(feature_map[...,0:2]),feature_map[...,2:4]],axis=-1)
pred_xy = tf.to_float(tf.expand_dims(xy,4))#[-1,13,13,5,2]-->[-1,13,13,5,1,2]
pred_wh = tf.to_float(tf.expand_dims(wh,4))
pred_min = tf.to_float(pred_xy - pred_wh / 2.0)
pred_max = tf.to_float(pred_xy + pred_wh / 2.0)
true_box_shape = K.shape(true_boxes)
print(true_box_shape)
true_boxes = K.reshape(true_boxes,[-1,1,1,1,true_box_shape[1], 5])
#[-1,1,1,1,-1,5],batch, conv_height, conv_width, num_anchors, num_true_boxes, box_params'
true_xy = tf.to_float(true_boxes[...,0:2])
true_wh = tf.to_float(true_boxes[...,2:4])
true_min = tf.to_float(true_xy - true_wh / 2.0)
true_max = tf.to_float(true_xy + true_wh / 2.0)
#計算所以abox和tbox的iou
intersect_mins = tf.maximum(pred_min, true_min)
intersect_maxes = tf.minimum(pred_max, true_max)
intersect_wh = tf.maximum(intersect_maxes - intersect_mins, 0.)
intersect_areas = tf.to_float(intersect_wh[..., 0] * intersect_wh[..., 1])
pred_areas = pred_wh[..., 0] * pred_wh[..., 1]
true_areas = true_wh[..., 0] * true_wh[..., 1]
union_areas = pred_areas + true_areas - intersect_areas
iou_scores = intersect_areas / union_areas
#可能會有多個tbox落在同一個cell ,只去iou最大的
# tf.argmax(iou_scores,4)
best_ious = K.max(iou_scores, axis=4)
best_ious = tf.expand_dims(best_ious,axis=-1)
#選出IOU大於0.6的,若IOU小於0.6的被標記為background,
obj_dec = tf.cast(best_ious > 0.6,dtype=K.dtype(best_ious))
#IOU loss
no_obj_w = (self.no_object_scale * obj_dec * detectors_mask) #
no_obj_loss = no_obj_w * tf.square(-confidence)
obj_loss = self.object_scale * detectors_mask * tf.square(1 - confidence)
confidence_loss = no_obj_loss + obj_loss
#class loss
match_cls = tf.cast(matching_true_boxes[...,4],dtype=tf.int32)
match_cls = tf.one_hot(match_cls,num_classes)
class_loss = (self.class_scale * detectors_mask * tf.square(match_cls - cls_prob))
#座標loss
match_box = matching_true_boxes[...,0:4]
coord_loss = self.coordinates_scale * detectors_mask * tf.square(match_box - pred_box)
confidence_loss_sum = K.sum(confidence_loss)
class_loss_sum = K.sum(class_loss)
coord_loss_sum = K.sum(coord_loss)
all_loss = 0.5 * (confidence_loss_sum + class_loss_sum + coord_loss_sum)
return all_loss
def draw_detection(self,j,im, bboxes, scores, cls_inds, labels):
f = open('./output/final.txt', "a")
imgcv = np.copy(im)
h, w, _ = imgcv.shape
for i, box in enumerate(bboxes):
cls_indx = cls_inds[i]
thick = int((h + w) / 1000)
cv2.rectangle(imgcv, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), thick)
f.write('[x, y, w, h]=['+str(box[0])+','+str(box[1])+','+str(box[2])+','+str(box[3])+']\n')
#print("[x, y, w, h]=[%d, %d, %d, %d]" % (box[0], box[1], box[2], box[3]))
mess = '%s: %.3f' % (labels[cls_indx], scores[i])
text_loc = (box[0], box[1] - 10)
cv2.putText(imgcv, mess, text_loc, cv2.FONT_HERSHEY_SIMPLEX, 1e-3 * h, (0, 0, 255), thick)
# return imgcv
#將處理後的每幀圖片存到本地
address = './output/' + str(j)+ '.jpg'
cv2.imwrite(address,imgcv)
#將位置資訊寫入檔案
f.write('\n')
#v1 - v2 , v2 - v3
# 1、加入BN層 批次歸一化 input --> 均值為0方差為1正太分佈
# ---》白化 --> 對‘input 變換到 均值0單位方差內的分佈
# #使用:input * w -->bn
if __name__ == '__main__':
network = yolov2('coco')
net,x = network.darknet()
_bboxes, _obj_probs, _class_probs = network.decode(net)
saver = tf.train.Saver()
ckpt_path = './model/v2/yolo2_coco.ckpt'
sess = tf.Session()
sess.run(tf.global_variables_initializer())
saver.restore(sess,ckpt_path)
# 讀取視訊檔案
cap = cv2.VideoCapture("./test/3.mp4")
# 通過攝像頭的方式
# videoCapture=cv2.VideoCapture(1)
#讀幀
j=0
while cap.isOpened():
ret, frame = cap.read()
img_r = network.preprocess_image(frame)
bboxes, obj_probs, class_probs = sess.run([_bboxes, _obj_probs, _class_probs],feed_dict={x:img_r})
bboxes, scores, class_max_index = network.postprocess(bboxes, obj_probs, class_probs)
#print(scores, box_classes)
img_detection = network.draw_detection(j, cv2.resize(frame,(416,416)), bboxes, scores, class_max_index, network.CLASS)
j=j+1
'''
yi、
第一大層 :conv maxpoiling
第2大層:3個卷積,maxpool
3:3個卷積,maxpool
4:3卷積,maxpool
5:5卷積,maxpool -----------
6:5卷積 | + add
7三個卷積---------------------
conv
er:
ahchors生成和decode
san:
裁剪、選出前TOP_K,NMS
'''
執行結果(第30幀):
視訊還是上一篇文章中的測試視訊。限於上傳困難,在這裡依然只展示單幀的測試。對比圖如下(上面的是v1,下面的是v2)
從對比圖可以看出,與V1版本第30幀的檢測結果相比,V2可以檢測到更多的物體,並且準確率更高。
原視訊 。見:傳送門
處理後的視訊。見:傳送門
另外,檢測到的bbox位置也特別多,無法截圖展示,我就把資訊全部寫入到了txt文字中。見:傳送門
參考:
https://pjreddie.com/darknet/yolo/
https://xmfbit.github.io/2017/02/04/yolo-paper/
https://www.cnblogs.com/AntonioSu/p/12164255.html
https://zhuanlan.zhihu.com/p/25052190
http://lanbing510.info/2017/09/04/YOLOV2.html
https://segmentfault.com/a/1190000016842636#comment-area
https://www.youtube.com/watch?v=VOC3huqHrss
https://www.cnblogs.com/wangguchangqing/p/10480995.html
https://zhuanlan.zhihu.com/p/74540100