目標檢測和目標分類
圖像識別演算法是計算機視覺的基礎演算法,例如VGG,GoogLeNet,ResNet等,這類演算法主要是判斷圖片中目標的種類。
目標檢測演算法和圖像識別演算法類似,但是目標檢測演算法不僅要識別出圖像中的物體,還需要獲得圖像中物體的大小和位置,使用坐標的形式表示出來。如下圖:
下面我們舉個例子來說明 對於三種物體 人、車、摩托車,對於圖像識別,輸出列表為三個數字,分別代表圖像中物體是人、車、摩托車的概率,例如對於上圖的輸出值或許是[0.001, 0.998, 0.001]。被預測為車的概率為最高。
而對於目標檢測演算法來說,它的輸出值更像是這樣:
其中:
c1、c2、c3是目標屬於人、車、摩托車的概率,基於此,我們可以使用滑動窗口對整張圖片進行處理來定位目標。就像是用一個放大鏡去圖像的每個區域去查找是否包含目標。這樣的方法簡單粗暴有效,但是效率極低,計算複雜度太高,所以不會這麼去做。
然而,使用滑動窗口時,許多計算都是重複性計算,我們可以使用卷積神經網路的思想。
如上圖所示,如果我們用14*14的窗口進行計算,需要4次 5*5的卷積、2*2的max pool、55的卷積、1*1的卷積,而使用16*16的窗口1次計算就可以得到結果,效率更高,這個在Andrew Wu的deeplearning.ai的課程中有詳述,至於節省了多少計算量,這裡就偷懶不算了,直觀印象節省了很大很大的計算量。這裡的窗口是固定的,如果需要檢測不同大小的物體,需要不同大小的很多的窗口,這也是YOLO演算法需要解決的重要問題。
至於目標檢測的用處,現在最大的場景就是無人駕駛,在無人駕駛中,需要實時檢測出途中的人、車、物體、信號燈、交通標線等,再通過融合技術將各類感測器獲得的數據提供給控制中心進行決策。而目標檢測相當於無人駕駛系統的眼睛。
在目標檢測技術領域,有包含region proposals提取階段的兩階段(two-stage)檢測框架如R-CNN/Fast-RCNN/R-FCN等,再就是端到端的但階段目標檢測框架如YOLO系列和SSD等。
下面我們詳述YOLO的思想。
YOLO是You Only Look Once的縮寫。這也是為了特別突出YOLO區別於兩階段演算法的特點,從名字就可以感受到,YOLO演算法速度很快,事實上也是如此。可以看出在同樣的設備上,YOLO可以達到45幀每秒的速度。
Grid Cell
在YOLO中,目標圖片被劃分為Grid Cell,實際應用中,會使用19*19的grid cell, 為了容易理解,這裡暫時使用3*3的grid cell。
圖片中的汽車使用紅色線框標識出來。
下一步,對於圖中的每一個grid cell,我們都會用如下的標籤(前文已經解釋過具體含義)來標識。
然而,如何將grid cell和物體聯繫起來呢?
對於右邊的汽車,相對簡單,由於汽車完全處於右方的grid cell,於是它屬於右邊的grid cell。
而對於中間的卡車來說,它的bounding box跨越了好幾個grid cell。YOLO的做法是,物體的中心處於哪個grid cell,那麼物體就屬於哪個grid cell, 因此卡車屬於最中間的grid cell。
每個grid cell的標籤如上圖所示,沒有物體時,pc為0,其他的值也就沒必要關注了,當pc為1時,bx/by為物體中心處於grid cell的相對位置,這時,grid cell的高和寬為1,因此bx/by小於1,bh/bw為物體相對於grid cell的高和寬,其值可以大於1,之後的c1/c2/c3為預測的目標的概率。
這裡為了方便起見,只有3類物體,實際應用,會使用80種物體,於是會有c1/c2/c3.../c80。
考慮特殊情況下,如果兩個物體的中心處於同一個grid cell的情況。需要使用Anchor Box。
Anchor Box
Anchor Box使得YOLO可以檢測出同一個grid cell中包含多個物體的情況。
上圖中,人和車的中心都處於中間的grid cell中。Anchor box為標籤增加了更多的緯度。如果我們可以對每個物體對應一個anchor box來標識。為了解釋方便,這裡我們只使用兩個anchor box。
每一個grid cell包含兩個anchor box,意味著每一個grid cell可以預測兩個物體。至於為什麼要選擇不同形狀的anchor box呢?直觀印象是這樣,我們將物體與anchor box進行比較,看看更像哪個anchor box的形狀,和anchor box更像的物體傾向於被識別為anchor box代表的物體形狀。例如anchor box1 更像行人的形狀,而anchor box2 更像汽車的形狀。
如圖所示,圖像中間位置的grid cell可以用此來標識。這麼做的另一個原因是使得模型更專業化。某些輸出被用來訓練檢測像車一樣寬形的物體,而另外一個則被用來檢測行人一樣的高瘦的物體。
那麼如何定義、如何判斷物體具體相似的形狀呢?
Intersection over Union
Intersection over Union IOU 的定義是兩個box的交集面積和並集面積的比值。(x1,y1,x2,y2) 分別代表box 左上角和右下角的坐標。
計算IOU的代碼如下所示:
def iou(box1, box2): """Implement the intersection over union (IoU) between box1 and box2 Arguments: box1 -- first box, list object with coordinates (x1, y1, x2, y2) box2 -- second box, list object with coordinates (x1, y1, x2, y2) """
# Calculate the (y1, x1, y2, x2) coordinates of the intersection of box1 and box2. Calculate its Area. ### START CODE HERE ### (≈ 5 lines) xi1 = max(box1[0], box2[0]) yi1 = max(box1[1], box2[1]) xi2 = min(box1[2], box2[2]) yi2 = min(box1[3], box2[3]) inter_area = (xi2 - xi1) * (yi2 - yi1) ### END CODE HERE ###
# Calculate the Union area by using Formula: Union(A,B) = A + B - Inter(A,B) ### START CODE HERE ### (≈ 3 lines) box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1]) box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1]) union_area = box1_area + box2_area - inter_area ### END CODE HERE ###
# compute the IoU ### START CODE HERE ### (≈ 1 line) iou = inter_area / union_area ### END CODE HERE ###
return iou
介紹了YOLO中的一些基本概念後,我們先看看YOLO是如何進行目標檢測的
假設我們已經訓練出了YOLO的模型
首先輸入待檢測的圖片,對圖片進行一系列的處理,使得圖片的規格符合數據集的要求。
第二,通過模型計算獲得預測輸出,假如使用的是19*19的grid cell數目,5個anchor box, 80個分類,於是輸出的緯度為(1,19,19,5,80+5)
第三,對於輸出值進行處理,過濾掉得分低的值,輸出值中的Pc 在原論文中被稱為confidence 而C被稱為 probs,得分為confidence * probs,可以看出,所謂的得分就是含有目標的概率值。
代碼實現如下:
def yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = .6): """Filters YOLO boxes by thresholding on object and class confidence. Arguments: box_confidence -- tensor of shape (19, 19, 5, 1) boxes -- tensor of shape (19, 19, 5, 4) box_class_probs -- tensor of shape (19, 19, 5, 80) threshold -- real value, if [ highest class probability score < threshold], then get rid of the corresponding box Returns: scores -- tensor of shape (None,), containing the class probability score for selected boxes boxes -- tensor of shape (None, 4), containing (b_x, b_y, b_h, b_w) coordinates of selected boxes classes -- tensor of shape (None,), containing the index of the class detected by the selected boxes Note: "None" is here because you dont know the exact number of selected boxes, as it depends on the threshold. For example, the actual output size of scores would be (10,) if there are 10 boxes. """
# Step 1: Compute box scores ### START CODE HERE ### (≈ 1 line) box_scores = box_confidence * box_class_probs ### END CODE HERE ###
# Step 2: Find the box_classes thanks to the max box_scores, keep track of the corresponding score ### START CODE HERE ### (≈ 2 lines) box_classes = K.argmax(box_scores, axis=-1) # 五個boxes裡面分數最高的 box_class_scores = K.max(box_scores, axis=-1) ### END CODE HERE ###
# Step 3: Create a filtering mask based on "box_class_scores" by using "threshold". The mask should have the # same dimension as box_class_scores, and be True for the boxes you want to keep (with probability >= threshold) ### START CODE HERE ### (≈ 1 line) filtering_mask = box_class_scores >= threshold ### END CODE HERE ###
# Step 4: Apply the mask to scores, boxes and classes ### START CODE HERE ### (≈ 3 lines) ### filtering_mask 是 False True Flase True組成的列表 通過tf.boolean_mask 過濾掉值為False ### 的值 於是 scores boxes classes 都是至為True的index對應的列表 scores = tf.boolean_mask(box_class_scores, filtering_mask) boxes = tf.boolean_mask(boxes, filtering_mask) classes = tf.boolean_mask(box_classes, filtering_mask) ### END CODE HERE ###
return scores, boxes, classes
第四,同一個物體可能會有多個grid cell預測到,那麼同一個物體就會有多個bouding box,我們需要留下具有最高pc值的預測值,將其他的預測值過濾掉。如何判斷多個bounding box預測的是同一個物體呢,這裡就需要使用IOU演算法。最後得到的值就是圖片中被預測的目標的類型和位置值,再經過一系列計算和轉換變成圖片上的真實坐標值,使用工具畫到原圖上。
第四步中的篩選過程被稱為Non-max suppression演算法
代碼如下:
def yolo_non_max_suppression(scores, boxes, classes, max_boxes = 10, iou_threshold = 0.5): """ Applies Non-max suppression (NMS) to set of boxes Arguments: scores -- tensor of shape (None,), output of yolo_filter_boxes() boxes -- tensor of shape (None, 4), output of yolo_filter_boxes() that have been scaled to the image size (see later) classes -- tensor of shape (None,), output of yolo_filter_boxes() max_boxes -- integer, maximum number of predicted boxes youd like iou_threshold -- real value, "intersection over union" threshold used for NMS filtering Returns: scores -- tensor of shape (, None), predicted score for each box boxes -- tensor of shape (4, None), predicted box coordinates classes -- tensor of shape (, None), predicted class for each box Note: The "None" dimension of the output tensors has obviously to be less than max_boxes. Note also that this function will transpose the shapes of scores, boxes, classes. This is made for convenience. """
max_boxes_tensor = K.variable(max_boxes, dtype=int32) # tensor to be used in tf.image.non_max_suppression() K.get_session().run(tf.variables_initializer([max_boxes_tensor])) # initialize variable max_boxes_tensor
# Use tf.image.non_max_suppression() to get the list of indices corresponding to boxes you keep ### START CODE HERE ### (≈ 1 line) 這是TensorFlow的一個庫函數 在很多演算法中都會用到 nms_indices = tf.image.non_max_suppression( boxes, scores, max_boxes_tensor, iou_threshold) ### END CODE HERE ###
# Use K.gather() to select only nms_indices from scores, boxes and classes ### START CODE HERE ### (≈ 3 lines) scores = tf.gather(scores, nms_indices) boxes = tf.gather(boxes, nms_indices) classes = tf.gather(classes, nms_indices) ### END CODE HERE ###
具體處理的情況如上圖,有三個紅色的bounding box預測到皮卡,兩個黃色的bounding box預測到汽車,我們需要留下pc最高的一個,但是怎麼判斷哪些bounding box是預測的同一個物體呢,就需要使用IOU方法。
tf.image.non_maxsuppression函數的具體做法是對於所有的boxes先選取具有分數最高pc的box,然後用剩餘所有的box和選出的box進行計算IOU的值,當IOU大於iouthreshold時,box被刪除掉,然後再在剩餘的boxes里取最大值,再做同樣的操作,直到boxes的數目為max_boexes_tensor的數目。
處理的結果如下圖。
總結一下整個預測過程如下圖:
YOLO的檢測過程在deepsystems.io中圖示的很清晰,可以作為參考。
對於檢測的過程下文將對代碼部分進行詳細分析。
檢測的核心代碼如下:
def predict(sess, image_file): """ Runs the graph stored in "sess" to predict boxes for "image_file". Prints and plots the preditions. Arguments: sess -- your tensorflow/Keras session containing the YOLO graph image_file -- name of an image stored in the "images" folder. Returns: out_scores -- tensor of shape (None, ), scores of the predicted boxes out_boxes -- tensor of shape (None, 4), coordinates of the predicted boxes out_classes -- tensor of shape (None, ), class index of the predicted boxes Note: "None" actually represents the number of predicted boxes, it varies between 0 and max_boxes. """
# Preprocess your image image, image_data = preprocess_image("images/" + image_file, model_image_size = (608, 608))
# Run the session with the correct tensors and choose the correct placeholders in the feed_dict. # Youll need to use feed_dict={yolo_model.input: ... , K.learning_phase(): 0}) ### START CODE HERE ### (≈ 1 line) out_scores, out_boxes, out_classes = sess.run([scores, boxes, classes], feed_dict={yolo_model.input: image_data, input_image_shape: [image.size[1], image.size[0]], K.learning_phase(): 0}) ### END CODE HERE ###
# Print predictions info print(Found {} boxes for {}.format(len(out_boxes), image_file)) # Generate colors for drawing bounding boxes. colors = generate_colors(class_names) # Draw bounding boxes on the image file draw_boxes(image, out_scores, out_boxes, out_classes, class_names, colors) # Save the predicted bounding box on the image image.save(os.path.join("out", image_file), quality=90) # Display the results in the notebook output_image = scipy.misc.imread(os.path.join("out", image_file)) imshow(output_image)
return out_scores, out_boxes, out_classes
代碼塊中有英文注釋,簡單來說分為幾步:
首先先看下preprocess_image函數做了什麼事情,函數中每一行都使用中文做了注釋
def preprocess_image(img_path, model_image_size): image_type = imghdr.what(img_path) ## 獲得圖片類型 image = Image.open(img_path) ## 將圖片處理為608*608的固定大小的圖片 resized_image = image.resize(tuple(reversed(model_image_size)), Image.BICUBIC) #讀取圖片數據存到數組中 image_data = np.array(resized_image, dtype=float32) # 除去rgb最大值 image_data /= 255. # 在最前面加一維批處理緯 image_data = np.expand_dims(image_data, 0) # Add batch dimension. return image, image_data
之後關注yolo_eval函數
def yolo_eval(yolo_outputs, image_shape, max_boxes=10, score_threshold=.6, iou_threshold=.5): """ Evaluate YOLO model on given input batch and return filtered boxes. Arguments: yolo_outputs -- 經過模型計算後的值再經過yolo_head函數計算得到的 box_confidence, box_xy, box_wh, box_class_probs image_shape -- 輸入圖片的維度 max_boxes -- 每一張圖片中預測出的boxes的最大值 score_threshold -- 最小得分值的閾值 iou_threshold -- IOU的閾值 """ ## 提取box_confidence, box_xy, box_wh, box_class_probs box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs ## 將boxesd 坐標值轉換為四個角的坐標值 boxes = yolo_boxes_to_corners(box_xy, box_wh) ## 過濾掉得分小於score_threshold的boxes 前文之中已有代碼實現 boxes, scores, classes = yolo_filter_boxes( box_confidence, boxes, box_class_probs, threshold=score_threshold)
# Scale boxes back to original image shape. ## 將boxes的坐標值按照image_shape的變換比例還原 height = image_shape[0] width = image_shape[1] ## 講image_dims變成(4,)的張量 image_dims = K.stack([height, width, height, width]) image_dims = K.reshape(image_dims, [1, 4])
boxes = boxes * image_dims
# Use one of the functions youve implemented to perform Non-max suppression with a threshold of iou_threshold (≈1 line) ## yolo_non_max_suppression 函數上文中已有解析 scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes = max_boxes, iou_threshold = iou_threshold)
return boxes, scores, classes
於是我們需要關注yolo_outputs是如何獲得,即yolo_head函數,至於yolo_boxes_to_corners實現方法比較簡,但是需要注意坐標軸的方向
def yolo_boxes_to_corners(box_xy, box_wh): """Convert YOLO box predictions to bounding box corners.""" box_mins = box_xy - (box_wh / 2.) box_maxes = box_xy + (box_wh / 2.)
## 這裡返回的值是[y,x,y,x]的組合 return K.concatenate([ box_mins[..., 1:2], # y_min box_mins[..., 0:1], # x_min box_maxes[..., 1:2], # y_max box_maxes[..., 0:1] # x_max ])
yolo_head函數中涉及到的計算較多,數值和維度的變化也比較多,需要深入理解其細節
def yolo_head(feats, anchors, num_classes): """Convert final layer features to bounding box parameters. Parameters ---------- feats : tensor shape為(?,19,19,425) Final convolutional layer features. yolo_model的最後一層輸出是一個(m,19,19,5,85)的tensor anchors : array-like Anchor box widths and heights. 5個anchor的寬和高 num_classes : int Number of target classes. 80個分類 Returns ------- box_confidence : tensor grid cell內是否含有物體 shape為(?,?,?,5,80) Probability estimate for whether each box contains any object. 通過模型最後一層的計算得到的所有的bounding boxes的坐標值(中心坐標和寬高值) box_xy : tensor shape為(?,?,?,5,2) x, y box predictions adjusted by spatial location in conv layer. box_wh : tensor shape為(?,?,?,5,2) w, h box predictions adjusted by anchors and conv spatial resolution. box_class_pred : tensor 物體是80個分類的概率值 shape為(?,?,?,5,1) Probability distribution estimate for each box over class labels. """ num_anchors = len(anchors) ## num_anchors 為5
# Reshape to batch, height, width, num_anchors, box_params. anchors_tensor = K.reshape(K.variable(anchors), [1, 1, 1, num_anchors, 2])
# 取第2、3維 即19*19 conv_dims = K.shape(feats)[1:3] #conv_dims變成2維 每個維度19個值
# In YOLO the height index is the inner most iteration. [0,1,2,3,4,5,...,18] conv_height_index = K.arange(0, stop=conv_dims[0]) conv_width_index = K.arange(0, stop=conv_dims[1]) #tile(x, n)函數 將x在各個維度上重複n次,x為張量,n為與x維度數目相同的列表 這裡conv_heifh_index只有一維 # 下面這部分看著很繞 但實際上是獲得(1,19,19,1,2)維度的位移表 conv_height_index = K.tile(conv_height_index, [conv_dims[1]]) conv_width_index = K.tile(K.expand_dims(conv_width_index, 0), [conv_dims[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, conv_dims[0], conv_dims[1], 1, 2]) conv_index = K.cast(conv_index, K.dtype(feats)
feats = K.reshape(feats, [-1, conv_dims[0], conv_dims[1], num_anchors, num_classes + 5]) conv_dims = K.cast(K.reshape(conv_dims, [1, 1, 1, 1, 2]), K.dtype(feats))
# Static generation of conv_index: 這種靜態方法更容易理解一些 # conv_index = np.array([_ for _ in np.ndindex(conv_width, conv_height)]) # conv_index = conv_index[:, [1, 0]] # swap columns for YOLO ordering. # conv_index = K.variable( # conv_index.reshape(1, conv_height, conv_width, 1, 2)) # feats = Reshape( # (conv_dims[0], conv_dims[1], num_anchors, num_classes + 5))(feats)
box_confidence = K.sigmoid(feats[..., 4:5]) box_xy = K.sigmoid(feats[..., :2])
box_wh = K.exp(feats[..., 2:4]) box_class_probs = K.softmax(feats[..., 5:])
#box_xy 是圖片通過model的預測得出的可能是object的85位值中的第1-2位值 代表的是物體被預測的中心位置 #box_wh 是圖片通過model的預測得出的object的寬度和高度 再乘以對應的anchors_tensor之後 按照anchors的形狀等比例放大
# conv_index 對應的grid cell格子數(從左和從上數的位移) box_xy = (box_xy + conv_index) / conv_dims
#在這裡+conv_index是為了找到對應的cell 將圖片的中心落於預測的cell中 得到相對於整張圖片的box_xy
box_wh = box_wh * anchors_tensor / conv_dims
return box_confidence, box_xy, box_wh, box_class_probs
特徵轉換為坐標值的公式如下所示:
至此,對於YOLO的監測過程已經解析完畢
參考資料
YOLO官網
燎原火:Yolo-v3
deepsystems.io
YAD2K github
keras-yolo3
推薦閱讀:
※商湯及聯合實驗室37篇論文入選ECCV 2018※砸了140億的計算機視覺,未來到底如何?※稀疏編碼用於產品表面異常檢測※到底怎麼選擇※OPPO和IFAA共同定義安全人臉,引領3D視覺技術發展
TAG:深度學習(DeepLearning) | 計算機視覺 |