一.前言python
本次分析的源碼爲大佬復現的keras版本,上一波地址:https://github.com/qqwweee/keras-yolo3 git
初步打算重點分析兩部分,第一部分爲數據,即分析圖像如何作等比變化,如何將標註框(groud truth boxs) 的信息轉換爲計算損失時使用的label。另外一部分爲損失函數計算的源碼。我的認爲這兩部分比較難理解,因此想把本身的理解寫出來,以便你們一塊兒交流。做爲菜鳥中的菜菜鳥可能理解有不到位的地方,但願你們批評指正。github
二.數據處理關鍵代碼位置及功能簡介app
在keras-yolo3工程下,主程序爲train.py。數據讀取的代碼位於train.py的59行,名稱爲data_generator_wrapper的函數。
data_generator_wrapper函數 調用位置:train.py的59行 函數定義位置:train.py的184行 功能:獲取讀取數據的長度並判斷長度是否爲0,調用data_generator函數。
data_generator函數 調用位置:train.py的187行 函數定義位置:model.py的165行 功能:按照batchsize大小讀取數據,並打亂順序送入到get_random_data函數,將獲得的圖像和標註信息轉換爲numpy格式,將獲得的標註信息送入到preprocess_true_boxes行處理。
get_random_data函數 調用位置:train.py的175行 函數定義位置:yolo3文件夾下util.py 的36行 功能:處理標註數據,限制最大框爲20(同時也方便了拼接操做,詳細緣由後邊解釋)。less
preprocess_true_boxes函數 調用位置: train.py的181行 函數定義位置:yolo3文件夾下model.py 的232行 功能:將boxs信息及與他們匹配的anchors,置信度信息,類別信息保存到y_true中,即label標籤的製做。
dom
三.關鍵代碼詳解函數
經過代碼邏輯能夠看出最主要的操做在 get_random_data,preprocess_true_boxes兩個函數中,爲了方便分析本次選取了6張圖片及其標註信息,模擬一個batchsize的數據。數據具體信息以下:
spa
------------------------------------------------------------------------------------------------------------------------------------code
./datas/000005.jpg 263,211,324,339,8 165,264,253,372,8 241,194,295,299,8
./datas/000007.jpg 141,50,500,330,6
./datas/000009.jpg 69,172,270,330,12 150,141,229,284,14 285,201,327,331,14 258,198,297,329,14
./datas/000016.jpg 92,72,305,473,1
./datas/000019.jpg 231,88,483,256,7 11,113,266,259,7
./datas/000020.jpg 33,148,371,416,6blog
------------------------------------------------------------------------------------------------------------------------------------
1. get_random_data關鍵部分分析
主要分析圖片等比變化的方式,等比變化的方式因爲不會改變物體的原有比例,這樣檢測精度會高一些(我的理解)。下面以一張圖片爲栗子,而且設置random=False,來看下具體操做吧!關鍵代碼及部分註釋以下:
1 #!/usr/bin/env python2 2 # -*- coding: utf-8 -*- 3 """ 4 Created on Wed Mar 27 22:02:48 2019 5 6 @author: wxz 7 """ 8 import numpy as np 9 from PIL import Image 10 def get_random_data(annotation_line, input_shape, random=False, max_boxes=20, jitter=.3, hue=.1, sat=1.5, val=1.5, proc_img=True): 11 line = annotation_line.split() 12 image = Image.open(line[0]) #獲取圖像位置並讀取 13 iw, ih = image.size #(500,375 ) 14 h, w = input_shape 15 box = np.array([np.array(list(map(int,box.split(',')))) for box in line[1:]]) 16 ''' 17 獲得物體座標信息及類別信息 [xmin, ymin, xmax, ymax, class] 18 [[263 211 324 339 8] 19 [165 264 253 372 8] 20 [241 194 295 299 8]] 21 ''' 22 if not random: 23 # resize image 24 scale = min(np.float(w)/iw, np.float(h)/ih) #python 3 scale = min(w/iw, np.h/ih) 25 nw = int(iw*scale) 26 nh = int(ih*scale) 27 dx = (w-nw)//2 28 dy = (h-nh)//2 29 image_data=0 30 if proc_img: 31 image = image.resize((nw,nh), Image.BICUBIC) 32 new_image = Image.new('RGB', (w,h), (128,128,128))#生成一個(416,416)灰色圖像 33 new_image.paste(image, (dx, dy)) #將image 放在灰色圖中央。 圖像開始的位置座標會增大哦! 34 image_data = np.array(new_image)/255. 35 # correct boxes 36 box_data = np.zeros((max_boxes,5)) 37 if len(box)>0: 38 np.random.shuffle(box) 39 if len(box)>max_boxes: box = box[:max_boxes] 40 box[:, [0,2]] = box[:, [0,2]]*scale + dx #dx 爲xmin,xmax增量 圖像位置變化了 因此 標註信息也要隨之變化 這樣標註框纔不會偏移。 41 box[:, [1,3]] = box[:, [1,3]]*scale + dy #dy 爲ymin,ymax的增量 42 box_data[:len(box)] = box 43 44 return image_data, box_data 45 if __name__=='__main__': 46 datas = './datas/data.txt' 47 input_shape = (416,416) 48 with open(datas,'r') as f: 49 annotation_lines = f.readlines() 50 image ,boxes=get_random_data(annotation_lines[0], input_shape) 51 ''' 52 boxes: 53 [[137. 271. 210. 361. 8.] 54 [218. 227. 269. 334. 8.] 55 [200. 213. 245. 300. 8.] 56 [ 0. 0. 0. 0. 0.] 57 [ 0. 0. 0. 0. 0.] 58 [ 0. 0. 0. 0. 0.] 59 [ 0. 0. 0. 0. 0.] 60 [ 0. 0. 0. 0. 0.] 61 [ 0. 0. 0. 0. 0.] 62 [ 0. 0. 0. 0. 0.] 63 [ 0. 0. 0. 0. 0.] 64 [ 0. 0. 0. 0. 0.] 65 [ 0. 0. 0. 0. 0.] 66 [ 0. 0. 0. 0. 0.] 67 [ 0. 0. 0. 0. 0.] 68 [ 0. 0. 0. 0. 0.] 69 [ 0. 0. 0. 0. 0.] 70 [ 0. 0. 0. 0. 0.] 71 [ 0. 0. 0. 0. 0.] 72 [ 0. 0. 0. 0. 0.]] 73 '''
圖像進行等比變化後的效果。灰色部分爲dy(dx=0)。是否是圖像開始的位置增大了呢嘿嘿,這就是等比變化的過程!有不清楚地方在交流吧。因爲時間有限,先寫這些,之後慢慢更新。想要一個batchsize操做代碼de可附郵箱。
2.preprocess_true_boxes 關鍵部分分析
輸入true_boxes類型爲np.array,形狀爲(6,20,5)的矩陣,6是本次選取了6張圖片做爲一個btachsize, 20由於每一個圖片定義包含的標註框最大爲20不夠的補0,超出的捨棄。5表明物體座標信息及類別信息 [xmin, ymin, xmax, ymax, class] 。
imput_shape=(416,416)。
anchors = [[10,13], [16,30], [33,23], [30,61], [62,45], [59,119], [116,90], [156,198], [373,326]]
num_classes = 20 (voc 2007包含20類檢測物體)
1 #!/usr/bin/env python2 2 # -*- coding: utf-8 -*- 3 """ 4 Created on Thu Mar 28 22:03:10 2019 5 6 @author: wxz 7 """ 8 def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes): 9 assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes' #判斷類別是否超出了20 10 '''true_boxes[...,4] 表示取出array的所的第四個數值 11 array([[ 8., 8., 8., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 12 0., 0., 0., 0., 0., 0., 0.], 第一張圖片20個標註框的類別信息 13 [ 6., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 14 0., 0., 0., 0., 0., 0., 0.], 第一張圖片20個標註框的類別信息 15 [12., 14., 14., 14., 0., 0., 0., 0., 0., 0., 0., 0., 0., 16 0., 0., 0., 0., 0., 0., 0.], 17 [ 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 18 0., 0., 0., 0., 0., 0., 0.], 19 [ 7., 7., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 20 0., 0., 0., 0., 0., 0., 0.], 21 [ 6., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 22 0., 0., 0., 0., 0., 0., 0.]]) 23 ''' 24 num_layers = len(anchors)//3 # default setting num_layers=3 25 anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]] 26 true_boxes = np.array(true_boxes, dtype='float32') 27 input_shape = np.array(input_shape, dtype='int32') 28 boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2 # 計算中心點座標 (xmin+xmax)/2 (ymin+ymax)/2 29 boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]#計算圖片寬高 xmax-xmin ymax-ymin 30 true_boxes[..., 0:2] = boxes_xy/input_shape[::-1] #將中心點及寬高 對輸入圖片416 作歸一化 31 true_boxes[..., 2:4] = boxes_wh/input_shape[::-1] 32 m = true_boxes.shape[0] #獲取barchsize大小 此處m=6 33 grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)] #獲取特徵圖的尺寸 13, 26,52 34 ''' 35 grid_shapes是一個長度爲3的列表具體數值以下 36 [array([13, 13], dtype=int32), array([26, 26], dtype=int32), array([52, 52], dtype=int32)] 37 38 ''' 39 y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes), 40 dtype='float32') for l in range(num_layers)] 41 ''' 42 y_true是一個長度爲3的列表,列表包含三個numpyarray float32類型的全零矩陣,具體形狀以下 43 (6, 13, 13, 3, 25) (6, 26, 26, 3, 25) (6, 52, 52, 3, 25) 即三個尺度特徵圖大小 44 45 ''' 46 # Expand dim to apply broadcasting. 47 anchors = np.expand_dims(anchors, 0)#擴展第一個維度原來爲(9,2) --->(1,9,2)這樣操做能夠充分利用numpy的廣播機制 48 anchor_maxes = anchors / 2. #將anchors 中心點放(0,0) 由於anchors沒有中心點只有寬高,計算與boxs計算iou時二者中心點均爲(0.0) 49 anchor_mins = -anchor_maxes # anchor_mins 記錄了xim ymin 兩個座標點 50 valid_mask = boxes_wh[..., 0]>0 #判斷是否有異常標註boxes 51 52 for b in range(m): 53 # Discard zero rows. 54 wh = boxes_wh[b, valid_mask[b]] #第一個圖片爲例 wh=[[ 51. 107.] shape=(3,2) 55 if len(wh)==0: continue # [ 45. 87.] 56 # Expand dim to apply broadcasting. [ 73. 90.]] 57 wh = np.expand_dims(wh, -2)#在第二個維度擴展 [[[ 51. 107.]] shape=(3,1,2) 58 box_maxes = wh / 2. # [[ 45. 87.]] 59 box_mins = -box_maxes # [[ 73. 90.]]] 60 intersect_mins = np.maximum(box_mins, anchor_mins) 61 intersect_maxes = np.minimum(box_maxes, anchor_maxes) 62 intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.) 63 intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] 64 box_area = wh[..., 0] * wh[..., 1] 65 anchor_area = anchors[..., 0] * anchors[..., 1] 66 iou = intersect_area / (box_area + anchor_area - intersect_area) 67 print iou 68 # Find best anchor for each true box 69 best_anchor = np.argmax(iou, axis=-1) 70 print best_anchor 71 for t, n in enumerate(best_anchor): 72 print t 73 print n 74 for l in range(num_layers): 75 if n in anchor_mask[l]: 76 i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32') 77 j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32') 78 k = anchor_mask[l].index(n) 79 c = true_boxes[b,t, 4].astype('int32') 80 y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4] 81 y_true[l][b, j, i, k, 4] = 1 82 y_true[l][b, j, i, k, 5+c] = 1 83 84 return y_true
如下計算過程均以第一張圖片爲例:
2.1 boxes_wh 的值爲
[[[ 51. 107.]
[ 45. 87.]
[ 73. 90.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]
[ 0. 0.]]
boxes_wh[..., 0]--->array([[ 51., 45., 73., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]
valid_mask=boxes_wh[..., 0]>0--->[[ True True True False False False False False False False False False False False False False False False False False]
能夠看到大於0的位置對應的爲true。
wh=boxes_wh[b, valid_mask[b]] 保留對應位置爲true的行去掉位置爲false的行,結果以下所示:
[[ 51. 107.]
[ 45. 87.]
[ 73. 90.]]
2.2 boxes與anchors計算iou
anchors在第一維度進行了擴展維度(9,2)--> (1,9,2) wh在第二個維度進行了擴展(3,2)--->(3,1,2)
anchor_maxes: [[[ 5. 6.5] box_maxes: [[[25.5 53.5]]
[ 8. 15. ] [[22.5 43.5]]
[ 16.5 11.5] [[36.5 45. ]]]
[ 15. 30.5]
[ 31. 22.5]
[ 29.5 59.5]
[ 58. 45. ]
[ 78. 99. ]
[ 186.5 163. ]]]
計算時廣播後的形式:[[[ 5. 6.5] shape=(3,9,2) box_maxes: [[[25.5 53.5] shape=(3,9,2) 大佬的操做很666,充分利用了廣播的特性。
[ 8. 15. ] [25.5 53.5]
[ 16.5 11.5] [25.5 53.5]
[ 15. 30.5] [25.5 53.5]
[ 31. 22.5] [25.5 53.5]
[ 29.5 59.5] [25.5 53.5]
[ 58. 45. ] [25.5 53.5]
[ 78. 99. ] [25.5 53.5]
[186.5 163. ]] [25.5 53.5]]
[[ 5. 6.5] [[22.5 43.5]
[ 8. 15. ] [22.5 43.5]
[ 16.5 11.5] [22.5 43.5]
[ 15. 30.5] [22.5 43.5]
[ 31. 22.5] [22.5 43.5]
[ 29.5 59.5] [22.5 43.5]
[ 58. 45. ] [22.5 43.5]
[ 78. 99. ] [22.5 43.5]
[186.5 163. ]] [22.5 43.5]]
[[ 5. 6.5] [[36.5 45.]
[ 8. 15. ] [36.5 45.]
[ 16.5 11.5] [36.5 45.]
[ 15. 30.5] [36.5 45.]
[ 31. 22.5] [36.5 45.]
[ 29.5 59.5] [36.5 45.]
[ 58. 45. ] [36.5 45.]
[ 78. 99. ] [36.5 45.]
[186.5 163. ]]] [36.5 45.]]]
計算時至關於每一個box與每一個anchor都計算了iou。
2.3.label存儲形式及具體含義
依舊以第一張圖片爲例,第一張圖片有三個標註框,經過計算獲得第一張圖片的iou矩陣,shape=(3,9)
[[[0.02382261 0.08796042 0.13908741 0.33534909 0.38558468 0.77723971 0.40594322 0.17667055 0.04487738]
[0.03320562 0.12260536 0.19386973 0.46743295 0.43269231 0.55761288 0.375 0.12674825 0.03219625]
[0.01978691 0.07305936 0.11552511 0.27853881 0.42465753 0.6412269 0.62931034 0.21270396 0.05403049]]]
第一行爲標註的boxes中第一個box與9個anchors的iou,第二行爲標註boxes的中第二個box與9個anchors的iou,第三行爲標註boxes的中第三個box與9個anchors的iou。
best_anchor = np.argmax(iou, axis=-1) 取最後一個維度最大值的索引。
best_anchor = [5 5 5]可見三個boxes都與第五個anchor的iou最大。
enumerate(best_anchor)---> [(1,5),(2,5),(3,5)]
代碼中t表示圖片中的第幾個box,n爲第幾個anchor使得此box取得最大值。
前邊提到過,y_true是一個長度爲3的列表,列表包含三個numpyarray float32類型的全零矩陣,numpyarray具體形狀以下
( 6, 13, 13, 3, 25) (6, 26, 26, 3, 25) (6, 52, 52, 3, 25) 即三個尺度特徵圖大小。
將座標位置映射到對應特徵圖上。
i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
true_boxes爲歸一化的後的座標。歸一化的好處是不管對應特徵圖大小爲多少,原圖上框的座標位置信息總能映射到特徵圖的對應位置。
摘取關鍵代碼,l表示第幾個尺度特徵圖,一共三個尺度,l=(0,1,2)
y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4] --->b表示這一個batchsize的第幾個圖片,k記錄是三套anchor中的第幾個anchor。最後一維度0,1,2,3索引保存box映射到不一樣尺度特徵圖時的座標值。j,i爲四個座標的位置信息。
y_true[l][b, j, i, k, 4] = 1 ---> 第4個索引記錄的爲box包含物體的置信度信息,即這個box有沒有包含物體,包含爲1,不包含爲0。
y_true[l][b, j, i, k, 5+c] = 1 ---> 第5+c索引記錄box包含的具體物體的種類。
數據處理部分就結束了!歡迎各位大佬一塊兒討論!