coursera 吴恩达深度学习 Specialization 编程作业(course 4 week 3)

这次编程作业介绍了一个极其牛逼的算法——yolo 算法,用来检测马路上的车辆和路标等,这次作业主要介绍了 yolo 算法的数据后处理部分,即从输出挑选出正确预测方框的过程。

先把需要的包引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import argparse
import os
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
import scipy.io
import scipy.misc
import numpy as np
import pandas as pd
import PIL
import tensorflow as tf
from keras import backend as K # K 相当于 keras.backend.K
from keras.layers import Input, Lambda, Conv2D
from keras.models import load_model, Model
from yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxes
from yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body

%matplotlib inline

问题描述

我们在车顶放置摄像头拍摄图片以实现自动驾驶系统,搜集所有的图片进文件夹然后通过在车的周围画框获得标签值。

由于 YOLO 算法需要非常大的算力去计算,所以采用预训练好的模型来使用。

这个模型是用微软的 coco 图像数据集进行预训练的,该数据集一共有八十种物体类别,所以类别向量有 80 个数。

YOLO 算法

详细的算法介绍见上一篇博客。

模型细节

  • 输入:(m, 608, 608, 3)的图像
  • 输出:(m, 19, 19, 5, 85)的标签
    • 19×19 的网格
    • 5 个锚框
    • 一个锚框包含:1 个 Pc 值 + 4 个坐标 + 80 个类别

对于每个格子的每个锚框,我们计算这个锚框最可能包含某一类物体的“分数”,分数越高,则这个锚框包含某一类物体的概率也最大。“分数” 的计算方法如下:

每个网格有五个锚框,一个锚框可以预测出一个方框,总共模型可以预测出 19×19×5 = 1805 个方框出来,从里面挑一些具有高概率的方框如下图所示,可是方框还是很多,需要进一步地筛选。

用“分数”门槛进行筛选

首先将“分数”不满足某个阈值的方框都去掉,也就是说这些方框预测出里面有某一类物体的概率太小了。

下面实现筛选函数。

最后的标签向量是一个形状为(19, 19, 5, 85)的向量,将其分为三个向量:

  • box_confidence:包含每个网格的每个锚框的 $p_c$ 值的向量,形状为(19, 19, 5, 1)
  • boxes: 包含每个网格每个锚框的四个坐标的向量,形状为(19, 19, 5, 4)
  • box_class_probs: 包含每个网格每个锚框的 80 个类的概率值,形状为(19, 19, 5, 80)

用到的函数介绍:

  • keras.backend.argmax(x, axis=-1):得到 x 的最大值的索引值,axis = -1 表示找最小的维度里的最大值,-1 是反方向第一个的意思
  • keras.backend.max(x, axis=None, keepdims=False): 返回 x 某个维度的最大值
  • tf.boolean_mask(tensor, mask, name='boolean_mask', axis=None): 用只有 ture 和 false 两种值的向量 mask 筛选 tensor。mask 的维度 K 必须小于 tensor 的维度 N,最后的返回值的维度是 N-K+1。 假如 tensor 形状为(a, b, c, d),那么 mask 的形状可以是 (a, b, c, d),此时返回的是 4-4+1=1 维向量,也可以是(a, b, c),此时返回的是后面 2 维向量,依次类推。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 筛选掉分数较低的方框
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 don't 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
box_scores = box_confidence * box_class_probs

# Step 2: Find the box_classes thanks to the max box_scores, keep track of the corresponding score
box_classes = K.argmax(box_scores, axis = -1) # axis = -1 指的是从反向取值,在这里相当于 axis = 3
box_class_scores = K.max(box_scores, axis = -1)

# 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)
filtering_mask = box_class_scores >= threshold

# Step 4: Apply the mask to scores, boxes and classes
scores = tf.boolean_mask(box_class_scores, filtering_mask) # tf.boolean_mask:如果第一个参数维度为 N,那么第二个mask 参数维度K必须小于等于N,返回的值维度为 N-K+1
boxes = tf.boolean_mask(boxes, filtering_mask) # 所以这里用 19*19*5 的 mask 筛选 19*19*5 的向量,最后返回的维度是 1,即(?,),?是因为不知道多少box被选出来
classes = tf.boolean_mask(box_classes, filtering_mask) # 所有选出来的 box 被展开在一个一维向量里


return scores, boxes, classes

Keras 是基于 tensorflow 的,所以我们用如下方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
with tf.Session() as test_a:
box_confidence = tf.random_normal([19, 19, 5, 1], mean=1, stddev=4, seed = 1)
boxes = tf.random_normal([19, 19, 5, 4], mean=1, stddev=4, seed = 1)
box_class_probs = tf.random_normal([19, 19, 5, 80], mean=1, stddev=4, seed = 1)
scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, threshold = 0.5)
print("scores[2] = " + str(scores[2].eval()))
print("boxes[2] = " + str(boxes[2].eval()))
print("classes[2] = " + str(classes[2].eval()))
print("scores.shape = " + str(scores.shape))
print("boxes.shape = " + str(boxes.shape))
print("classes.shape = " + str(classes.shape))

可以看到,去掉分数较低的方框后,剩余的方框都被统一展开成一个 1 维向量。其中 ? 表示,无法确定多少个方框会被筛选掉,所以用 ? 代替。

非最大值抑制

就算筛掉很多低概率方框,最后还是会剩下很多重复检测的方框,即一个车被好几个不同的框认出来,但是其中只有一个是最符合的,使用非最大值抑制将这些多余的去掉,如下图所示。

交并比 IoU 如何实现

非最大值抑制中要用到一个很重要的函数,交并比,确定两个方框的重合程度,重合程度越高即交并比越高,两个方框越可能是在检测同一个物体。

提示:

  • 假设输入的是两方框两个角的坐标 (x1, y1, x2, y2),而不是中心点和宽高
  • 矩形的面积这样计算: (y2 - y1) × (x2 - x1)
  • 为了计算交集的面积,必须获得交集的坐标 (xi1, yi1, xi2, yi2),如何确定:
    • xi1 = maximum of the x1 coordinates of the two boxes
    • yi1 = maximum of the y1 coordinates of the two boxes
    • xi2 = minimum of the x2 coordinates of the two boxes
    • yi2 = minimum of the y2 coordinates of the two boxes
    • 要确保交集区域是正的,否则它就是 0,使用 max(height, 0) and max(width, 0)
  • 并集面积如何确定:两个方框面积相加再减去交集部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
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)

# Calculate the Union area by using Formula: Union(A,B) = A + B - Inter(A,B)
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

# compute the IoU
iou = inter_area / union_area

return iou

非最大值抑制如何实现

我们在第一步筛选出了一系列的方框,我们在这些方框中:

  • 选择出分数最高的
  • 计算它和其他所有的方框的交并比,去掉其中和它交并比大于某个阈值 iou_threshold 的,因为这种方框我们认为重合率太高,是在检测同一个物体
  • 将刚刚选出的分数最高的排出在外,在剩下的方框中重复上述操作,直到最后没有分数比现在选择的方框更低的了。

需要用到的函数:

  • tf.image.non_max_suppression(boxes, scores, max_output_size, iou_threshold=0.5, score_threshold=float('-inf'), name=None): boxes 是形状为 (方框总数量, 4) 的二维数组,scores 是对应的方框的“分数”的一维向量,用它们进行非最大值抑制,输出最后得到的方框的索引值,是一个一维向量。
  • tf.keras.backend.gather(reference, indices): 用索引值 indices 把向量 reference 中对应索引值的向量取出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 非最大值抑制
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 you'd 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
nms_indices = tf.image.non_max_suppression(boxes, scores, max_boxes, iou_threshold) # 返回一个一维的索引值向量,形状为 (?,) ,表示那些最后被留下来的方框的索引值

# Use K.gather() to select only nms_indices from scores, boxes and classes
scores = K.gather(scores, nms_indices) # 根据索引值取出对应的方框的值
boxes = K.gather(boxes, nms_indices)
classes = K.gather(classes, nms_indices)

return scores, boxes, classes

yolo 算法过滤函数

下面我们将所有的函数合并起来,获得最终的需要的结果。

两个实现的细节:

  • 由于 tf.image.non_max_suppression 中的参数 boxes 的坐标是 (x1, y1, x2, y2),而我们输出的坐标是 (x, y, w, h),所以需要对坐标进行转换,使用 yolo_boxes_to_corners(box_xy, box_wh)函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.)

    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 算法是在 608×608 的图像上训练的,如果我们在 720×1280 的图片上进行测试,那么方框就不匹配这个图片,我们需要一个 scale_boxes(boxes, image_shape)来让方框可以适配新的图片形状

    1
    2
    3
    4
    5
    6
    7
    8
    def scale_boxes(boxes, image_shape):
    """ Scales the predicted boxes in order to be drawable on the image"""
    height = image_shape[0]
    width = image_shape[1]
    image_dims = K.stack([height, width, height, width])
    image_dims = K.reshape(image_dims, [1, 4])
    boxes = boxes * image_dims
    return boxes

过滤函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def yolo_eval(yolo_outputs, image_shape = (720., 1280.), max_boxes=10, score_threshold=.6, iou_threshold=.5):
"""
Converts the output of YOLO encoding (a lot of boxes) to your predicted boxes along with their scores, box coordinates and classes.

Arguments:
yolo_outputs -- output of the encoding model (for image_shape of (608, 608, 3)), contains 4 tensors:
box_confidence: tensor of shape (None, 19, 19, 5, 1)
box_xy: tensor of shape (None, 19, 19, 5, 2)
box_wh: tensor of shape (None, 19, 19, 5, 2)
box_class_probs: tensor of shape (None, 19, 19, 5, 80)
image_shape -- tensor of shape (2,) containing the input shape, in this notebook we use (608., 608.) (has to be float32 dtype)
max_boxes -- integer, maximum number of predicted boxes you'd like
score_threshold -- real value, if [ highest class probability score < threshold], then get rid of the corresponding box
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 (None, 4), predicted box coordinates
classes -- tensor of shape (None,), predicted class for each box
"""

# Retrieve outputs of the YOLO model
box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs

# Convert boxes to be ready for filtering functions
boxes = yolo_boxes_to_corners(box_xy, box_wh)

# Use one of the functions you've implemented to perform Score-filtering with a threshold of score_threshold (≈1 line)
scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, score_threshold)

# Scale boxes back to original image shape.
boxes = scale_boxes(boxes, image_shape)

# Use one of the functions you've implemented to perform Non-max suppression with a threshold of iou_threshold (≈1 line)
scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes, iou_threshold)

return scores, boxes, classes

总结

  • Input image (608, 608, 3)
  • The input image goes through a CNN, resulting in a (19,19,5,85) dimensional output.
  • After flattening the last two dimensions, the output is a volume of shape (19, 19, 425):
    • Each cell in a 19x19 grid over the input image gives 425 numbers.
    • 425 = 5 x 85 because each cell contains predictions for 5 boxes, corresponding to 5 anchor boxes, as seen in lecture.
    • 85 = 5 + 80 where 5 is because $(p_c, b_x, b_y, b_h, b_w)$ has 5 numbers, and and 80 is the number of classes we’d like to detect
  • You then select only few boxes based on:
    • Score-thresholding: throw away boxes that have detected a class with a score less than the threshold
    • Non-max suppression: Compute the Intersection over Union and avoid selecting overlapping boxes
  • This gives you YOLO’s final output.

测试 yolo 算法

首先开始一个会话:

1
sess = K.get_session()

定义类别,锚框和图片尺寸:

1
2
3
class_names = read_classes("model_data/coco_classes.txt")
anchors = read_anchors("model_data/yolo_anchors.txt")
image_shape = (720., 1280.)

加载预训练模型:

由于训练 yolo 算法要花费很多时间,我们加载一个训练好权重的预训练模型,被放在 “yolo.h5” 当中。

1
yolo_model = load_model("model_data/yolo.h5")

查看模型概况:

1
yolo_model.summary()

将输出变成我们能用的形式:

1
yolo_outputs = yolo_head(yolo_model.output, anchors, len(class_names))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def yolo_head(feats, anchors, num_classes):
"""Convert final layer features to bounding box parameters.

Parameters
----------
feats : tensor
Final convolutional layer features.
anchors : array-like
Anchor box widths and heights.
num_classes : int
Number of target classes.

Returns
-------
box_xy : tensor
x, y box predictions adjusted by spatial location in conv layer.
box_wh : tensor
w, h box predictions adjusted by anchors and conv spatial resolution.
box_conf : tensor
Probability estimate for whether each box contains any object.
box_class_pred : tensor
Probability distribution estimate for each box over class labels.
"""
num_anchors = len(anchors)
# Reshape to batch, height, width, num_anchors, box_params.
anchors_tensor = K.reshape(K.variable(anchors), [1, 1, 1, num_anchors, 2])
# Static implementation for fixed models.
# TODO: Remove or add option for static implementation.
# _, conv_height, conv_width, _ = K.int_shape(feats)
# conv_dims = K.variable([conv_width, conv_height])

# Dynamic implementation of conv dims for fully convolutional model.
conv_dims = K.shape(feats)[1:3] # assuming channels last
# In YOLO the height index is the inner most iteration.
conv_height_index = K.arange(0, stop=conv_dims[0])
conv_width_index = K.arange(0, stop=conv_dims[1])
conv_height_index = K.tile(conv_height_index, [conv_dims[1]])

# TODO: Repeat_elements and tf.split doesn't support dynamic splits.
# conv_width_index = K.repeat_elements(conv_width_index, conv_dims[1], axis=0)
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:])

# Adjust preditions to each spatial grid point and anchor size.
# Note: YOLO iterates over height index before width index.
box_xy = (box_xy + conv_index) / conv_dims
box_wh = box_wh * anchors_tensor / conv_dims

return box_confidence, box_xy, box_wh, box_class_probs

过滤方框:

1
scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)

在一张图片上运行计算图

计算图为:

yolo_model.input —> yolo_model —> yolo_modeloutput —> yolo_head —> yolo_outputs —> yolo_eval —> scores, boxes, classes

输入图片需要进行预处理以符合输入格式(608, 608):

1
image, image_data = preprocess_image("images/" + image_file, model_image_size = (608, 608))

image 是用来画框的图片,image_data 是转化为数组的图片。

我们将上述计算图放入 predicet():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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.
# You'll need to use feed_dict={yolo_model.input: ... , K.learning_phase(): 0})
out_scores, out_boxes, out_classes = sess.run([scores, boxes, classes], feed_dict={yolo_model.input:image_data, K.learning_phase(): 0})

# 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

用自己的图片进行预测:

1
out_scores, out_boxes, out_classes = predict(sess, "1.jpg")

结论:

  • YOLO is a state-of-the-art object detection model that is fast and accurate
  • It runs an input image through a CNN which outputs a 19x19x5x85 dimensional volume.
  • The encoding can be seen as a grid where each of the 19x19 cells contains information about 5 boxes.
  • You filter through all the boxes using non-max suppression. Specifically:
    • Score thresholding on the probability of detecting a class to keep only accurate (high probability) boxes
    • Intersection over Union (IoU) thresholding to eliminate overlapping boxes
  • Because training a YOLO model from randomly initialized weights is non-trivial and requires a large dataset as well as lot of computation, we used previously trained model parameters in this exercise. If you wish, you can also try fine-tuning the YOLO model with your own dataset, though this would be a fairly non-trivial exercise.
微信捐赠
支付宝捐赠