源码解读YOLO v3 - 06 测试

Posted monologuesmw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码解读YOLO v3 - 06 测试相关的知识,希望对你有一定的参考价值。

  技术图片

  在实际预测的过程中,主要包括两个部分:

  • 输入图像的标准化处理
  • 从模型输出的y1,y2,y3中进行分类和定位

  虽然会先生成yolo的对象,即预测评估的运算过程。

 输入图像的处理

1 def detect_img(yolo):
2     path = "VOCdevkit/VOC2007/SegmentationClass/*.jpg"
3     outdir = "./VOCdevkit/VOC2007/SegmentationObject"
4     for jpgfile in glob.glob(path):
5         img = Image.open(jpgfile)
6         img = yolo.detect_image(img)   # (731*575)
7         img.save(os.path.join(outdir, os.path.basename(jpgfile)))
8     yolo.close_session()

  在代码的第6行yolo.detect_image(img)中对输入的图像进行处理。

1 def detect_image(self, image):

A. 要获得图像数据image_data:

  • 判断设置的输入尺寸是不是能够整除32的,
  • 将输入的图片等比缩放至设定的model_image_size的大小中(此处为416*416)

  -- letterbox_image()中, 过程与y_true的等比缩放的过程相同(详见:https://www.cnblogs.com/monologuesmw/p/12794278.html),放在灰片上也会使图像放置在正中央的位置, 只不过此时没有标定框的事宜。

1 if self.model_image_size != (None, None):   # 判断是否设定图像输入模型的尺寸范围
2     assert self.model_image_size[0] % 32 == 0, Multiples of 32 required    # 这个是关于模型设定的尺寸的大小是否符合规范,
3     assert self.model_image_size[1] % 32 == 0, Multiples of 32 required    # 而不是图像的大小(对图像没有要求)
4     boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))    # 把输入图像缩放至规定的方位内
5 else:
6     new_image_size = (image.width - (image.width % 32),
7                       image.height - (image.height % 32))
8     boxed_image = letterbox_image(image, new_image_size)
9 image_data = np.array(boxed_image, dtype=float32) 

B. 图像数据归一化,并将其维度调整至运算过程中可用的维度

  扩展数组的形状,在第0维的位置多1维。 即 416*416*3--》1*416*416*3

  #通过上述的扩展转换,使得输入的尺寸符合网络输入格式 [batch_size, width, high, channels]

1 image_data /= 255.  # 归一化了
2 image_data = np.expand_dims(image_data, 0)  # Add batch dimension. 

C. 通过喂数据,获得边框、类别信息

  该部分的主干在于yolo初始化的generate过程,这个时候返回的检测每个物体的图像仅有一个。(即已经经过了nms等过滤系统)

1 out_boxes, out_scores, out_classes = self.sess.run(
2     [self.boxes, self.scores, self.classes],
3     feed_dict={
4         self.yolo_model.input: image_data,     # 图像的数据
5         self.input_image_shape: [image.size[1], image.size[0]],    # 原始图像的宽高
6         K.learning_phase(): 0    # 学习模式, 0: 测试模型, 1: 训练模型
7     })     # 该部分的目的在于求解generate的过程,

D. 将获得的边框,类别、类别概率信息绘制在原图上

  • 进行字体、厚度的设置
1 font = ImageFont.truetype(font=font/FiraMono-Medium.otf,
2             size=np.floor(3e-2 * image.size[1] + 0.5).astype(int32))   # 字体
3 thickness = (image.size[0] + image.size[1]) // 300  # 厚度
  • 按照输出的类别进行循环,并进行绘制
1 for i, c in reversed(list(enumerate(out_classes))):

  获得框、类等信息

1 predicted_class = self.class_names[c]  # 预测的类别
2 box = out_boxes[i]   # 框信息
3 score = out_scores[i]   # 框的得分(IOU)

  获取预测边框的尺寸

1 top, left, bottom, right = box
2 top = max(0, np.floor(top + 0.5).astype(int32))
3 left = max(0, np.floor(left + 0.5).astype(int32))
4 bottom = min(image.size[1], np.floor(bottom + 0.5).astype(int32))
5 right = min(image.size[0], np.floor(right + 0.5).astype(int32))

  上、左会+0.5 并且向下取整;

  下、右会+0.5 并且向下取整,再和原图的宽高对比取最小

1 print(label, (left, top), (right, bottom))    # 边框坐标

技术图片

  根据预测框的位置,调整文本的位置label_size--》[110, 20]

1 if top - label_size[1] >= 0:   # 101-20>=0
2     text_origin = np.array([left, top - label_size[1]])  # 
3 else:
4     text_origin = np.array([left, top + 1])

  绘制

1 for i in range(thickness):
2     draw.rectangle(
3         [left + i, top + i, right - i, bottom - i],
4         outline=self.colors[c])
5 draw.rectangle(
6     [tuple(text_origin), tuple(text_origin + label_size)],
7     fill=self.colors[c])
8 draw.text(text_origin, label, fill=(0, 0, 0), font=font)

2. 模型预测

  始于YOLO()类的初始化。

1 detect_img(YOLO())

  关于模型的预测,全系于代码第8行generate中。

1 def __init__(self, **kwargs):
2     self.__dict__.update(self._defaults)  # set up default values
3     self.__dict__.update(kwargs)   # and update with user overrides
4     self.class_names = self._get_class()   # 通过路径获取class_names的值
5     self.anchors = self._get_anchors()   # 获取边界的宽高, 并将原始数据转化为两列
6     self.sess = K.get_session()
7     self.boxes, self.scores, self.classes = self.generate()

  generate内部包含处理的函数如下思维导图所示:

  • yolo_head: 将模型最后一层的输出转化为中心点坐标、宽、高、框置信度、类别概率。即那些公式的计算。
  • yolo_correct_boxes: 转换成真实图像上的坐标
  • yolo_boxes_and_scores:中除了包含两个函数以外,还会进行框的得分的计算:

   框的得分=框的置信度*类别置信度

  并且返回框的信息和框的得分

  • yolo_eval:中除了包含上述的函数,还会根据框的得分和阈值进行过滤,并进行NMS抑制被标定多次的框。

技术图片

  接下来将从最底层往顶层扒~

A. yolo_head()

1 def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
2     """Convert final layer features to bounding box parameters."""

  获得anchors的个数,并初始化一个相关的张量

1 num_anchors = len(anchors)
2 # Reshape to batch, height, width, num_anchors, box_params.
3 anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])

  创建网格

  这个网格用于后续将偏移量转化为每个中心点的坐标

  • 会包含x轴的网格和y轴的网格 两部分
  • 每部分每行都是从0到12的网格数
  • 最后将其合并在一起,组成了一个13*13*1*2的矩阵
1 grid_shape = K.shape(feats)[1:3]  # height, width   获取网格的尺寸 eg:13*13
2 grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
3     [1, grid_shape[1], 1, 1])   # 0~12的 从代码上看,只有grid_shape[1]能够体现reshape的这个功能 一个像素一个格子?
4 grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
5     [grid_shape[0], 1, 1, 1])   #  用于生成网格grid
6 grid = K.concatenate([grid_x, grid_y])
7 grid = K.cast(grid, K.dtype(feats))  #  -》(13,13,1,2)

  将最后一位展开,网格info与其它分离

1 feats = K.reshape(
2     feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])    

  进行计算

  • 中心点坐标
  • 宽高
  • 框的置信度
  • 类别概率
1 box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
2 box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
3 box_confidence = K.sigmoid(feats[..., 4:5])  # 框置信度
4 box_class_probs = K.sigmoid(feats[..., 5:])   # 类别置信度    # 可以一次性的把所有网格包含的内容都算出来

中心点的坐标是相对于网格尺寸的位置

宽高是相对于416*416尺寸的位置

B. yolo_correct_boxes()

1 def yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape):
2     ‘‘‘转化成真实的坐标‘‘‘

  进行坐标的翻转

1 box_yx = box_xy[..., ::-1]
2  box_hw = box_wh[..., ::-1]

  获得图像经过等比缩放以后的尺寸(没有灰边的)--- new_shape

1 input_shape = K.cast(input_shape, K.dtype(box_yx))
2 image_shape = K.cast(image_shape, K.dtype(box_yx))
3 new_shape = K.round(image_shape * K.min(input_shape/image_shape))

将box的中心点、宽高调整至原图尺寸

1 offset = (input_shape-new_shape)/2./input_shape
2 scale = input_shape/new_shape
3 box_yx = (box_yx - offset) * scale
4 box_hw *= scale

  将中心点、宽高信息转化为四个坐标点 xmin ymin xmax ymax

1 box_mins = box_yx - (box_hw / 2.)
2 box_maxes = box_yx + (box_hw / 2.)
3 boxes =  K.concatenate([
4     box_mins[..., 0:1],  # y_min
5     box_mins[..., 1:2],  # x_min
6     box_maxes[..., 0:1],  # y_max
7     box_maxes[..., 1:2]  # x_max
8 ])# 即转化为真实坐标
9 return boxes     ---- 此处的boxes已经是原图上的了  

C. yolo_boxes_and_scores()

1 def yolo_boxes_and_scores(feats, anchors, num_classes, input_shape, image_shape):
2     ‘‘‘Process Conv layer output‘‘‘  处理卷积层输出的结果

  执行A.B

1 box_xy, box_wh, box_confidence, box_class_probs = yolo_head(feats,
2     anchors, num_classes, input_shape)
3 boxes = yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape)
4 boxes = K.reshape(boxes, [-1, 4])

  计算框的得分

   框的得分=框的置信度*类别概率

1 box_scores = box_confidence * box_class_probs   # 框的得分=框的置信度*类别置信度
2 box_scores = K.reshape(box_scores, [-1, num_classes])
3 return boxes, box_scores   --- 原图上的boxes  而且是四坐标

D. yolo_eval()

1 def yolo_eval(yolo_outputs,     # 网络神经元输出的feature map(last layer)
2               anchors,
3               num_classes,
4               image_shape,
5               max_boxes=20,
6               score_threshold=.6,
7               iou_threshold=.5):
8     """Evaluate YOLO model on given input and return filtered boxes."""

  相关使用的信息初始化:

1 num_layers = len(yolo_outputs)
2 anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]] # default setting
3 input_shape = K.shape(yolo_outputs[0])[1:3] * 32      # 模型最后一层有三个神经元---> (32,16,8)13-26-52  (下采样的时候的尺度缩放)
4 boxes = []                                            # 每个神经元会有3个anchor_box
5 box_scores = []

  input_shape 为 416*416的;

  逐层获得的边框信息和框的分的信息:

1 for l in range(num_layers):
2     _boxes, _box_scores = yolo_boxes_and_scores(yolo_outputs[l],
3         anchors[anchor_mask[l]], num_classes, input_shape, image_shape)   # 处理卷积层的输出 内含yolo_head
4     boxes.append(_boxes)
5     box_scores.append(_box_scores)    # 

  将boxes 和 box_scores的数据展平:

1 boxes = K.concatenate(boxes, axis=0)   # 将数据展平 (?,4)---四坐标(与标定一样)
2 box_scores = K.concatenate(box_scores, axis=0)

  获得了一个边界框的得分是否大于阈值的布尔向量,用于将低于阈值的边界框过滤掉

1 mask = box_scores >= score_threshold     # 对三种尺度的得分进行筛选(阈值) 得到一堆符合条件的布尔值

  初始化:

1 max_boxes_tensor = K.constant(max_boxes, dtype=int32)
2 boxes_ = []
3 scores_ = []
4 classes_ = []

  按类进行进一步的框框筛选:

1 for c in range(num_classes):

  过滤掉一些得分小于阈值的边框:

1 class_boxes = tf.boolean_mask(boxes, mask[:, c])   # 过滤掉一些得分小于阈值的边界框
2 class_box_scores = tf.boolean_mask(box_scores[:, c], mask[:, c])

  进行NMS处理(处理掉标定同一物体的多个标定的框)

1 nms_index = tf.image.non_max_suppression(
2     class_boxes, class_box_scores, max_boxes_tensor, iou_threshold=iou_threshold)  # 运行非最大抑止—用于排出同一目标被标注多次的框

 

1 class_boxes = K.gather(class_boxes, nms_index)   # 在class_boxes中搜寻nms_index下表的向量
2 class_box_scores = K.gather(class_box_scores, nms_index)
3 classes = K.ones_like(class_box_scores, int32) * c  
4 boxes_.append(class_boxes)
5 scores_.append(class_box_scores)
6 classes_.append(classes)

 

  从此结束循环

1 boxes_ = K.concatenate(boxes_, axis=0)
2 scores_ = K.concatenate(scores_, axis=0)
3 classes_ = K.concatenate(classes_, axis=0)
4 return boxes_, scores_, classes_

  generate中包含导入模型以及给不同的类分配一种颜色的框,然后就是yolo_eval().

以上是关于源码解读YOLO v3 - 06 测试的主要内容,如果未能解决你的问题,请参考以下文章

源码解读YOLO v3 训练 - 05 损失函数loss

yolo v2 损失函数源码解读

yolo v2 损失函数源码解读

yolo v2 损失函数源码解读

YOLOv5源码逐行超详细注释与解读——网络结构yolo.py

jQuery公司源码解读,v3.1.1中从菜鸟到大神之路