1.任务 构建YOLO算法的各个模块:Bounding Box预测、交并比(IoU)、非极大值抑制、Anchor Boxes。
然后利用构建的YOLO算法进行目标识别(汽车检测)。
2.导入依赖包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import argparseimport osimport matplotlib.pyplot as pltfrom matplotlib.pyplot import imshowimport scipy.ioimport scipy.miscimport numpy as npimport pandas as pdimport PILimport tensorflow as tffrom keras import backend as Kfrom keras.layers import Input, Lambda, Conv2Dfrom keras.models import load_model, Modelfrom yolo_utils import read_classes, read_anchors, generate_colors, preprocess_image, draw_boxes, scale_boxesfrom yad2k.models.keras_yolo import yolo_head, yolo_boxes_to_corners, preprocess_true_boxes, yolo_loss, yolo_body
3.YOLO 3.1Model Details 首先我们知道:
输入 是一批形状为 (m, 608, 608, 3) 的图像
输出 是一系列边界框以及识别出的类别。如上所述,每个边界框由6个数字 表示。如果将 扩展为一个80维向量,那么每个边界框将由85个数字表示。
我们将使用5个锚框。因此,你可以将YOLO架构想象为以下流程:图像 (m, 608, 608, 3) -> 深度CNN -> 编码 (m, 19, 19, 5, 85)。
让我们更详细地了解这种编码代表什么。
如果一个物体的中心/中点落在一个网格单元中,那么该网格单元负责检测该物体。
由于我们使用了5个锚框,因此每个19x19的单元格都包含了5个框的信息。锚框仅由其宽度和高度定义。
为了简化,我们将展平形状的最后两个维度 (19, 19, 5, 85) 编码。所以深度CNN的输出是 (19, 19, 425)。
现在,对于每个单元格的每个框,我们将计算以下逐元素乘积,并提取框包含某个类别的概率。
以下是一种可视化YOLO在图像上预测的方式:
对于19x19网格的每个单元格,找出概率分数的最大值(在5个锚框和不同类别中取最大值)。
根据该网格单元格最可能考虑的对象对该网格单元进行着色。
这样做的结果是这样的图片:
请注意,这种可视化并不是YOLO算法本身用于进行预测的核心部分;它只是一种可视化算法中间结果的好方法。 另一种可视化YOLO输出的方式是绘制它输出的边界框。这样做的结果是这样的可视化:
在上图中,我们只绘制了模型分配了高概率的框,但这仍然是太多的框。你希望将算法的输出过滤到更少的检测到的物体。为此,你将使用非最大抑制。具体来说,你将执行以下步骤:
去除分数低的框(意味着,该框对检测类别的置信度不高)
当几个框重叠在一起并检测到同一个物体时,只选择一个框。
3.2使用类别得分阈值进行过滤 你将通过设置阈值来应用第一个过滤器。你希望去除那些类别“得分”低于选定阈值的框。
模型给出了总共19x19x5x85个数字,每个框由85个数字描述。将(19,19,5,85)(或(19,19,425))维度的张量重新排列成以下变量会更方便:
box_confidence
:形状为 的张量,包含每个19x19单元格中预测的5个框的 (存在某物体的置信概率)。
boxes
:形状为 的张量,包含每个单元格的5个框的 。
box_class_probs
:形状为 的张量,包含每个单元格的5个框的80个类别的检测概率 。
练习 :实现yolo_filter_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 38 39 40 41 42 43 44 45 46 def yolo_filter_boxes (box_confidence, boxes, box_class_probs, threshold = .6 ): """ 通过阈值处理来过滤YOLO的边框,基于物体和类别的置信度。 参数: box_confidence -- 形状为 (19, 19, 5, 1) 的张量,表示边框的置信度 boxes -- 形状为 (19, 19, 5, 4) 的张量,表示边框的坐标 box_class_probs -- 形状为 (19, 19, 5, 80) 的张量,表示各个类别的概率 threshold -- 实数值,如果[最高类别概率得分 < 阈值],则丢弃对应的边框 返回值: scores -- 形状为 (None,) 的张量,包含选中边框的类别概率得分 boxes -- 形状为 (None, 4) 的张量,包含选中边框的(b_x, b_y, b_h, b_w)坐标 classes -- 形状为 (None,) 的张量,包含被选中边框检测到的类别索引 注意: "None"是因为你不知道选中的边框的确切数量,这取决于阈值。 例如,如果有10个边框,scores的实际输出大小将是(10,)。 """ box_scores = box_confidence * box_class_probs box_classes = K.argmax(box_scores, axis=-1 ) box_class_scores = K.max (box_scores, axis=-1 , keepdims=False ) filtering_mask = box_class_scores > threshold scores = tf.boolean_mask(box_class_scores, filtering_mask) boxes = tf.boolean_mask(boxes, filtering_mask) classes = tf.boolean_mask(box_classes, filtering_mask) return scores, boxes, classes
部署:
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 2 3 4 5 6 scores[2 ] = 10.750582 boxes[2 ] = [ 8.426533 3.2713668 -0.5313436 -4.9413733 ] classes[2 ] = 7 scores.shape = (?,) boxes.shape = (?, 4 ) classes.shape = (?,)
3.3非极大值抑制 即使通过对类别分数进行阈值过滤,最终仍会出现大量重叠的方框。第二种筛选方法是非极大值抑制(NMS)。
在上图中,模型预测了 3 辆车,但实际上是对同一辆车的 3 次预测。运行非最大抑制 (NMS) 将只选择 3 个方框中最准确(概率最高)的一个。
非极大值抑制使用一个非常重要的函数,称为“交集与并集” ,或者IoU。
练习 :实现iou()函数。一些提示:
在这个练习中,我们使用两个角点(左上和右下)来定义一个盒子:(x1, y1, x2, y2),而不是中点和高度/宽度。
要计算矩形的面积,你需要将其高度(y2 - y1)乘以宽度(x2 - x1)
你还需要找到两个盒子相交的坐标(xi1, yi1, xi2, yi2)。记住:
xi1 = 两个盒子的x1坐标的最大值
yi1 = 两个盒子的y1坐标的最大值
xi2 = 两个盒子的x2坐标的最小值
yi2 = 两个盒子的y2坐标的最小值
在这段代码中,我们采用的惯例是(0,0)是图像的左上角,(1,0)是右上角,(1,1)是右下角。
代码:
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 iou (box1, box2 ): """ 实现 box1 和 box2 之间的交并比(IoU)。 参数: box1 -- 第一个边框,列表对象,坐标为 (x1, y1, x2, y2) box2 -- 第二个边框,列表对象,坐标为 (x1, y1, x2, y2) """ 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) 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 iou = inter_area / union_area return iou
部署:
1 2 3 box1 = (2 , 1 , 4 , 3 ) box2 = (1 , 2 , 3 , 4 ) print ("iou = " + str (iou(box1, box2)))
输出:
1 iou = 0.14285714285714285
现在已经准备好实现非极大值抑制的前提条件了。
关键步骤如下:
选择得分最高的盒子。
计算它与所有其他盒子的重叠部分,并移除那些与它重叠超过iou_threshold
的盒子。
返回第1步并迭代,直到没有更低分数的盒子。 这将移除所有与选定盒子重叠较大的盒子。只留下“最佳”盒子。
代码:
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 def yolo_non_max_suppression (scores, boxes, classes, max_boxes = 10 , iou_threshold = 0.5 ): """ 对一组边框应用非极大值抑制(NMS)。 参数: scores -- 形状为 (None,) 的张量,yolo_filter_boxes() 的输出 boxes -- 形状为 (None, 4) 的张量,yolo_filter_boxes() 的输出,已缩放到图像大小(见后文) classes -- 形状为 (None,) 的张量,yolo_filter_boxes() 的输出 max_boxes -- 整数,你希望预测的最大边框数量 iou_threshold -- 实数值,用于NMS过滤的“交并比”阈值 返回值: scores -- 形状为 (, None) 的张量,每个边框的预测得分 boxes -- 形状为 (4, None) 的张量,预测的边框坐标 classes -- 形状为 (, None) 的张量,每个边框的预测类别 注意:输出张量的 "None" 维度显然必须小于 max_boxes。还要注意,这个函数会转置 scores, boxes, classes 的形状。这是为了方便处理。 """ nms_indices = tf.image.non_max_suppression(boxes, scores, max_boxes, iou_threshold, name=None ) scores = K.gather(scores, nms_indices) boxes = K.gather(boxes, nms_indices) classes = K.gather(classes, nms_indices) return scores, boxes, classes
部署:
1 2 3 4 5 6 7 8 9 10 11 with tf.Session() as test_b: scores = tf.random_normal([54 ,], mean=1 , stddev=4 , seed = 1 ) boxes = tf.random_normal([54 , 4 ], mean=1 , stddev=4 , seed = 1 ) classes = tf.random_normal([54 ,], mean=1 , stddev=4 , seed = 1 ) scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes) 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.eval ().shape)) print ("boxes.shape = " + str (boxes.eval ().shape)) print ("classes.shape = " + str (classes.eval ().shape))
输出:
1 2 3 4 5 6 scores[2 ] = 6.938395 boxes[2 ] = [-5.299932 3.1379814 4.450367 0.95942086 ] classes[2 ] = -2.2452729 scores.shape = (10 ,) boxes.shape = (10 , 4 ) classes.shape = (10 ,)
3.4结束过滤 利用深度 CNN 的输出(19x19x5x85 维编码)实现一个函数,并使用刚才实现的函数对所有方框进行过滤。
练习 :
实现 **yolo_eval()**,获取 YOLO 编码的输出,并使用分数阈值和 NMS 过滤方框。还有最后一个实现细节你必须知道。有几种表示方框的方法,例如通过角或通过中点和高/宽。YOLO 会使用以下函数(依赖包提供了这些函数)在不同时间转换几种此类格式:
1 boxes = yoloo_boxes_too_corners(box_xy, box_wh)
将 yolo 方框坐标(x,y,w,h)转换为方框角坐标(x1, y1, x2, y2),以适应 yoloo_filter_boxes
的输入。
1 boxes = scale_boxes(boxes, image_shape)
YOLO 的网络是在 608x608 的图像上训练运行的。如果您要在不同尺寸的图像上测试这些数据,例如,汽车检测数据集的图像尺寸为 720x1280,那么这一步将重新缩放方框,以便将它们绘制在原始的 720x1280 图像之上。
代码:
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 def yolo_eval (yolo_outputs, image_shape = (720. , 1280. ), max_boxes=10 , score_threshold=.6 , iou_threshold=.5 ): """ 将YOLO编码的输出(大量边框)转换为预测的边框及其分数、边框坐标和类别。 参数: yolo_outputs -- 编码模型的输出(对于 (608, 608, 3) 的图像形状),包含4个张量: box_confidence: 形状为 (None, 19, 19, 5, 1) 的张量 box_xy: 形状为 (None, 19, 19, 5, 2) 的张量 box_wh: 形状为 (None, 19, 19, 5, 2) 的张量 box_class_probs: 形状为 (None, 19, 19, 5, 80) 的张量 image_shape -- 形状为 (2,) 的张量,包含输入形状,在本笔记本中我们使用 (608., 608.)(必须是 float32 数据类型) max_boxes -- 整数,你希望预测的最大边框数量 score_threshold -- 实数值,如果[最高类别概率得分 < 阈值],则丢弃对应的边框 iou_threshold -- 实数值,用于NMS过滤的“交并比”阈值 返回值: scores -- 形状为 (None, ) 的张量,每个边框的预测得分 boxes -- 形状为 (None, 4) 的张量,预测的边框坐标 classes -- 形状为 (None,) 的张量,每个边框的预测类别 """ box_confidence, box_xy, box_wh, box_class_probs = yolo_outputs boxes = yolo_boxes_to_corners(box_xy, box_wh) scores, boxes, classes = yolo_filter_boxes(box_confidence, boxes, box_class_probs, score_threshold) boxes = scale_boxes(boxes, image_shape) scores, boxes, classes = yolo_non_max_suppression(scores, boxes, classes, max_boxes, iou_threshold) return scores, boxes, classes
部署:
1 2 3 4 5 6 7 8 9 10 11 12 with tf.Session() as test_b: yolo_outputs = (tf.random_normal([19 , 19 , 5 , 1 ], mean=1 , stddev=4 , seed = 1 ), tf.random_normal([19 , 19 , 5 , 2 ], mean=1 , stddev=4 , seed = 1 ), tf.random_normal([19 , 19 , 5 , 2 ], mean=1 , stddev=4 , seed = 1 ), tf.random_normal([19 , 19 , 5 , 80 ], mean=1 , stddev=4 , seed = 1 )) scores, boxes, classes = yolo_eval(yolo_outputs) 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.eval ().shape)) print ("boxes.shape = " + str (boxes.eval ().shape)) print ("classes.shape = " + str (classes.eval ().shape))
输出:
1 2 3 4 5 6 scores[2 ] = 138.79124 boxes[2 ] = [1292.3297 -278.52167 3876.9893 -835.56494 ] classes[2 ] = 54 scores.shape = (10 ,) boxes.shape = (10 , 4 ) classes.shape = (10 ,)