【代码解读】yolov3测试过程

夙愿已清 提交于 2019-12-11 05:32:42

yolov3项目完整的代码_tensorflow版本

一 分析yolov3的测试部分

首先看一下整体的测试代码:Image_demo.py

写了一些注释:

import cv2
import numpy as np
import core.utils as utils
import tensorflow as tf
from PIL import Image

###############——————————第1部分:参数定义——————————————##############
return_elements = ["input/input_data:0", "pred_sbbox/concat_2:0", "pred_mbbox/concat_2:0", "pred_lbbox/concat_2:0"]
#加载pb文件的时候,需要  输入节点和输出节点的  tensor返回值,
#参考:https://blog.csdn.net/fu6543210/article/details/80343345
pb_file         = "./yolov3_coco.pb"
image_path      = "./docs/images/road.jpeg"
num_classes     = 80
input_size      = 416
graph           = tf.Graph() #计算图,主要用于构建网络,本身不进行任何实际的计算。
#Tensorflow中,用计算图来构建网络,用会话来具体执行网络。

###############——————————第2部分:处理原图——————————————##############
original_image = cv2.imread(image_path) #读取一张原图 1152*1060
original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB) #颜色转换
original_image_size = original_image.shape[:2] #[1152,1060]
image_data = utils.image_preporcess(np.copy(original_image), [input_size, input_size])
# 预处理图片,处理之后的图片 shape(416,416,3)
image_data = image_data[np.newaxis, ...]
# 增加了一个维度,shape为(1,416,416,3)值没有发生任何变化,其实就是又加了一个[ ],加了一个维度

##############——————————第3部分:计算图的定义——————————————#############
return_tensors = utils.read_pb_return_tensors(graph, pb_file, return_elements)
# #读取pb文件时候需要注意的是,若要获取对应的张量必须用“tensor_name:0”的形式,这是tensorflow默认的。
# return_elements:为“tensor_name:0”的形式

##############——————————第4部分:运行图——————————————#############
# 开始运行图吧
with tf.Session(graph=graph) as sess:
    pred_sbbox, pred_mbbox, pred_lbbox = sess.run(
        [return_tensors[1], return_tensors[2], return_tensors[3]],
                feed_dict={ return_tensors[0]: image_data})
    #首先pb文件,需要喂入数据image_data,然后经过计算,返回输出的tensor

pred_bbox = np.concatenate([np.reshape(pred_sbbox, (-1, 5 + num_classes)),
                            np.reshape(pred_mbbox, (-1, 5 + num_classes)),
                            np.reshape(pred_lbbox, (-1, 5 + num_classes))], axis=0)
# axis=0 表示第0维  拼接
# 原本pred_sbbox的shape为:(batch_size,13,13,3,5+类别)
# 先把pred_sbbox这些框进行reshape,每5+类别个数值组成一维度,shape变为了[-1,5+类别]
# 第0维进行拼接,然后就得到所有的anchor预测出来的框,大概应该有
'''
pred_sbbox reshape以后:[ [5+类别],[5+类别],[5+类别],.....[5+类别] ]
pred_mbbox reshape以后:[ [5+类别],[5+类别],[5+类别],.....[5+类别] ]
pred_lbbox reshape以后:[ [5+类别],[5+类别],[5+类别],.....[5+类别] ]
np.concatenate按照axis=0,按照第0维度拼接;shape[(-1)+(-1)+(-1)  ,  5+类别]
结果:[ [5+类别],[5+类别],[5+类别],[5+类别],[5+类别],[5+类别].....[5+类别]......,[5+类别] ]
大概有:(13*13+26*26+52*52)*3个 [5+类别]的tensor
'''

bboxes = utils.postprocess_boxes(pred_bbox, original_image_size, input_size, 0.3)
# def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
# score_threshold就是置信度阈值
# bboxes:shape(-1,6)  6:x,y,w,h,score,classes索引
bboxes = utils.nms(bboxes, 0.45, method='nms')
image = utils.draw_bbox(original_image, bboxes)
image = Image.fromarray(image) #array转换成image
image.show()

代码的整体理解:
首先,需要对输入的原图进行预处理,将原图resize成416416416*416,同时进行色域转换,并加上batch_size这一个维度(为了保证和训练的维度一样)。
然后用一组向量去接收,计算图的结果。
最终对结果进行后处理,以及NMS去除重复的框,然后将框画在原图上。

1. 参数的定义

这一部分没什么好说的,就是一些变量的定义,后续会使用到,唯一需要注意的就是第一个变量return_elements这个变量是用来接收计算图的结果的,要写成向量的形式,即加上:0表示是向量。

2. 输入的原图进行预处理

首先,通过cv2.imread读取一张图片,但是读取的图片是BGR形式的,需要通过cv2.cvtColor手动转为RGB色域。
假设我们输入的图片大小为115210601152*1060,但是训练的时候为了统一,我们将所有的训练图片变为416416416*416,所以测试的时候也需要将图片resize416416416*416
如何resize??通过函数utils.image_preporcess。解析这个函数:

def image_preporcess(image, target_size, gt_boxes=None): #预处理图片的过程
    #输入:需要预处理的图片,以及将图片变为多大的目标size需要是一个数组[416,416]

    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)
    #转颜色   同时变为浮点型
    #astype:转换数组的数据类型。

    #按照比例缩放原图为416*416
    #现在假设输入:1152*1060
    ih, iw    = target_size # 416  416
    h,  w, _  = image.shape #1152  1060

    scale = min(iw/w, ih/h) #min(0.39,0.36)  scale=0.36
    nw, nh  = int(scale * w), int(scale * h) #381  414
    image_resized = cv2.resize(image, (nw, nh)) #把输入图片resize成 381*414
    #还没有达到416*416   后面需要补0操作,填充为416*416

    image_paded = np.full(shape=[ih, iw, 3], fill_value=128.0)
    #np.full:构造一个数组,用指定值填充其元素,先填充一个416*416*3的数组,用128填充,
    # 然后后面在把resize之后的图片的像素,覆盖这416*416*3的数组
    dw, dh = (iw - nw) // 2, (ih-nh) // 2
    #" // "表示整数除法。 (416-381)//2 左边从这个值开始填充
    #为了保证图片尽量位于中间
    image_paded[dh:nh+dh, dw:nw+dw, :] = image_resized
    image_paded = image_paded / 255.  #归一化处理

    if gt_boxes is None: #测试的过程利用这个默认值None
        return image_paded
        #gt_boxes默认为None,返回处理完的图片

    else: # 训练的时候,需要用到  把真实的目标大小转换到416*416的大小上
        gt_boxes[:, [0, 2]] = gt_boxes[:, [0, 2]] * scale + dw  # x
        # 传入的gt_boxes:[ [48 240 95 371 11] , [8 12 352 498 14] ]
        # 转换到416*416大小上,再得到416大小 上的目标
        gt_boxes[:, [1, 3]] = gt_boxes[:, [1, 3]] * scale + dh
        return image_paded, gt_boxes
    # 训练的时候最终返回的是,resize之后的原图416和resize之后的目标

这个image_preporcess函数的作用:
test:其实就是将原图片115210601152*1060按照比例不变resize成416416416*416所以部分地方需要用其他颜色来填充。
train:将原图片115210601152*1060按照比例不变resize成416416416*416,同时将目标的框进行resize为416大小的相应尺寸。
但是在测试阶段,只是考虑,将115210601152*1060按照比例resize为416416416*416

resize之后需要增加一个维度image_data = image_data[np.newaxis, ...],保证的训练的时候维度相同,方便代码书写以及处理。

3. 计算图的定义

这一部分,就是通过utils.read_pb_return_tensors读取pb文件,然后将得到的向量返回,都是固定形式,没什么好说的。

def read_pb_return_tensors(graph, pb_file, return_elements):

    with tf.gfile.FastGFile(pb_file, 'rb') as f: #打开pb文件
        #获取文本操作句柄,类似于python提供的文本操作open()函数,
        # filename是要打开的文件名,mode是以何种方式去读写,将会返回一个文本操作句柄。
        frozen_graph_def = tf.GraphDef() ##计算图,主要用于构建网络,本身不进行任何实际的计算。
        frozen_graph_def.ParseFromString(f.read())

    with graph.as_default(): #读取pb文件并设置为默认的计算图
        #https://blog.csdn.net/fu6543210/article/details/80343345
        return_elements = tf.import_graph_def(frozen_graph_def,## 导入计算图
                                              return_elements=return_elements)
    return return_elements
#读取pb文件时候需要注意的是,若要获取对应的张量必须用“tensor_name:0”的形式,这是tensorflow默认的。

迄今为止,我们都只是在定义输入的图片,定义图,但是还没有开始真正的运行图,没有发生计算呢。现在开始运行图,开始预测。

4. 运行图

  • 通过with tf.Session(graph=graph) as sess开始运行图,注意这里多了一个(graph=graph)表示运行刚才的计算图。
  • 通过计算图,我们得到pred_sbbox, pred_mbbox, pred_lbbox,这就是网络进行前向计算得到conv,然后进行decode得到的相对于416416416*416上的所有预测框组成的向量。
  • 通过拼接函数np.concatenate将得到三个尺度合为一个尺度上,得到pred_bbox,shape是(1313+2626+52523(13*13+26*26+52*52)*3, 5+类别),有1313+2626+52523(13*13+26*26+52*52)*3个[5+类别]的tensor。
  • 将得到的box进行初步的处理,通过函数utils.postprocess_boxes,这个代码处理过程有些复杂,附上代码解析:
def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
# pred_bbox: [ [5+类别],[5+类别],[5+类别],[5+类别],[5+类别],[5+类别].....[5+类别]......,[5+类别] ]
# 针对416*416中的预测值

    valid_scale=[0, np.inf]  # np.inf无穷大 python的数组就是一个list而已[0, inf]
    pred_bbox = np.array(pred_bbox)  # np.array:将输入转为矩阵格式
    # np.array:https://blog.csdn.net/xiaomifanhxx/article/details/82498176
    # np.array:https://blog.csdn.net/fu6543210/article/details/83240024
    # np.array之后才为tensor,以前的可以理解为是数组,经过np.array之后才是矩阵,是tensor
    '''
    Python中提供了list容器,可以当作数组使用。但列表中的元素可以是任何对象,
    因此列表中保存的是对象的指针,这样一来,为了保存一个简单的列表[1,2,3]。
    就需要三个指针和三个整数对象。对于数值运算来说,这种结构显然不够高效。
    而且,python中的list进行相加的时候表示的是拼接,并不是数值计算,
    总之python中一旦超过一维就表示的不是计算了。
    Python虽然也提供了array模块,但其只支持一维数组,不支持多维数组(在TensorFlow里面偏向于矩阵理解),
    也没有各种运算函数。因而不适合数值运算。所以要转为numpy的格式,才方便我们的计算。
    '''
    # 具体例子可以查看test中的---4----

    pred_xywh = pred_bbox[:, 0:4]  # 0,1,2,3表示的是:x,y,w,h,
    # 因为取出来0,1,2,3这四个数就组成了1个维度,在把第0维所有的 : 取出来,就构成了第2个维度
    # 所以pred_xywh是2维向量[[1,5,6,7],[1,12,45,52],[2,3,6,9]....]
    pred_conf = pred_bbox[:, 4]  # 置信度  取出来以后是一个1维向量
    # 因为只有一个数字,就是将第4位取出来,只1个维度
    # 直接一个维度就可以了  [0.1  0.2  0.3  0.2  0.5...]
    # pred_conf = pred_bbox[:, 4:5] # 取出2个维度,因为原来就是两个维度
    pred_prob = pred_bbox[:, 5:] # 各个类别的概率
    # #####具体看例子test中的-----8-----
    # 预测的时候,只需要2个维度,[:, 0:4]这样取取出来两个维度也是因为原来就2个维度
    # 测试的时候,就需要5个维度,所以[:,:,:,:,0:4]这样取的时候,也是取出5个维度
    # 表示,第?个batch中的第?行第?列的格子的第?个anchor的[0:4]的元素,
# 每个第?个batch中的第?行第?列的格子的第?个anchor,都有这样的五个元素

        # pred_coor以后表示的都是 目标的左上角坐标 和右下角坐标
    # # (1) (x, y, w, h) --> (xmin, ymin, xmax, ymax)
    pred_coor = np.concatenate([pred_xywh[:, :2] - pred_xywh[:, 2:] * 0.5,
                                pred_xywh[:, :2] + pred_xywh[:, 2:] * 0.5], axis=-1)
    '''
    [:, :2]:0.1-----x,y      [:, 2:]:2,3-----w,h
    x -w *0.5 =xmin   y-h*0.5=ymin ....得到(xmin, ymin, xmax, ymax)
    [pred_xywh[:, :2] - pred_xywh[:, 2:] * 0.5:
    表示把第0维和第1维取出来组成了新的tensor,通过计算得到xmin和ymin
    pred_xywh[:, :2] + pred_xywh[:, 2:] * 0.5]
    表示把第0维和第1维取出来组成了新的tensor,通过计算得到xmax和ymax
    axis=-1 按照最后一个维度拼接起来,得到,[-1,2+2=4]
    '''

    # # (2) (xmin, ymin, xmax, ymax) -> (xmin_org, ymin_org, xmax_org, ymax_org)
    org_h, org_w = org_img_shape  # 原始图片的大小 1152*1060
    resize_ratio = min(input_size / org_w, input_size / org_h)  # input_size=416
    # mmin(0.36,0.39)=0.36

    dw = (input_size - resize_ratio * org_w) / 2  # 2   0.64
    dh = (input_size - resize_ratio * org_h) / 2  # 35   17.2

    pred_coor[:, 0::2] = 1.0 * (pred_coor[:, 0::2] - dw) / resize_ratio
    # 0::2:https://blog.csdn.net/Dontla/article/details/103010359
    # 切片,从地0位切到最后一位,每隔2位切一次,其实是拿到xmin和xmax
    pred_coor[:, 1::2] = 1.0 * (pred_coor[:, 1::2] - dh) / resize_ratio
    '''
    怎么把416*416上的目标切换到1152*1060上?那就反过来考虑,你是如何把1152*1060切换到416*416上的
    1152*1060------416*416?????
    首先求得一个缩放比例:416/1152=0.36  416/1060=0.39,那肯定缩放0.36倍,
    如果0.39倍,1152*0.39=449>416溢出,有些像素无法缩小了,所以0.36倍,
    1152----414    1060---381   
    剩下的416-414=2  416-381=35 分别除以2,416*416的图片上两边分别补0
    
    现在考虑,416*416------1152*1060???
    416------1152??首先,考虑1152---416,计算过程是:(1152*0.36)+2/1=416
    反过来计算:(416-2/1)/0.36
    
    416-------1060??首先考虑1060----416,计算过程:(1060*0.36)+35/2=416
    反过来计算:(416-35/2)/0.36
    '''

    # # (3) clip some boxes those are out of range
    pred_coor = np.concatenate([np.maximum(pred_coor[:, :2], [0, 0]),
                                np.minimum(pred_coor[:, 2:], [org_w - 1, org_h - 1])], axis=-1)
    # maximum:表示把xmin_org, ymin_org和0作比较,取大值
    # 也就是,将转换后的坐标,xmin_org, ymin_org小于0的,变为0
    # minimum:将xmax_org, ymax_org和原图的宽高作比较1152*1060作比较
    # 将超过原图宽和高的坐标 设置为原图的宽和高 减1
    invalid_mask = np.logical_or((pred_coor[:, 0] > pred_coor[:, 2]), (pred_coor[:, 1] > pred_coor[:, 3]))
    # np.logical_or:表示逻辑或一个为真则为真,全部为false才能为false
    # 检测是否合乎逻辑,也就是xmin>xmax,和 ymin>ymax 有一个为true 结果就为true,
    # 全部为false的时候结果才为false,全部为false的时候才合理,也就是结果为false的时候才是合理的
    # 意思:所有的box,坐标合理的为false,不合理的为true
    pred_coor[invalid_mask] = 0
    # numpy中布尔索引:https://blog.csdn.net/xsl15181685808/article/details/79734872
    # 布尔型数组长度必须跟被索引的轴长度一致.
    # 然后将为true,也就是不合理的box的四个坐标都设置为0
    # 执行的过程实际上是:将布尔索引为true的box取出来,然后赋值为0。
    # invalid_mask表示的实际上是一个索引,每个索引代表的是这个box是否合理
    # 例子可以看代码test中的---5---


    # #(4) discard some invalid boxes
    bboxes_scale = np.sqrt(np.multiply.reduce(pred_coor[:, 2:4] - pred_coor[:, 0:2], axis=-1))
    # np.sqrt(x) : 计算数组各元素的平方根
    # np.multiply.reduce: https://blog.csdn.net/shu15121856/article/details/76206891
    # 例子查看:test中的6
    # [:, 2:4]----[xmax,ymax]   [:, 0:2]-----[xmin,ymin]  相减:[xmax-xmin,ymax-ymin]
    # 求得是:(xmax-xmin)(ymax-ymin)再开根号
    scale_mask = np.logical_and((valid_scale[0] < bboxes_scale), (bboxes_scale < valid_scale[1]))
    # np.logical_and:逻辑和,一个为false则为false,两个均为true才为true
    # 所求的bboxes_scale大于0且小于inf才为true

    # # (5) discard some boxes with low scores
    classes = np.argmax(pred_prob, axis=-1) ##取出a中元素最大值所对应的索引
    #axis=-1表示最后一维,也就是[-1,类别]中的类别着一个维度
    scores = pred_conf * pred_prob[np.arange(len(pred_coor)), classes]
    # 查看test中的-----7-------
    # pred_prob[np.arange(len(pred_coor)),classes]:所有box中的,类别概率最大的那个类别的概率是多少
    # np.arange(len(pred_coor)),classes表示的是索引号,对应组合,
    # 第0个box和第0个box中类别概率最大的组合构成一个索引,得到第0个box的最大概率的值,是一个数
    # pred_conf就是1个维度的
    # 然后和置信度相乘 作为输出,得到所有box属于哪一个类别,并求得这个类别的分数
    # 一个box只有一个分数,所以scores最后的分数shape(-1)只有一个维度
    score_mask = scores > score_threshold
    # 大于阈值的  为true

    mask = np.logical_and(scale_mask, score_mask)
    # 逻辑与:两个都为true的才为true,
    # scale_mask为true表示:所求的bboxes_scale=(xmax-xmin)(ymax-ymin)再开根号,大于0且小于inf
    # score_mask为true表示:分数大于阈值
    coors, scores, classes = pred_coor[mask], scores[mask], classes[mask]
    # 表示:只有在mask问true的时候,也就是只有符合要求的box的坐标,分数,以及类别索引号才会输出进行赋值
    '''
    一个box有四个坐标,一个分数,以及一个类别
    四个坐标:xmin,ymin,xmax,ymax经过了两次筛选,
    第1次:将坐标超出范围的设置为0或者原图1152*1060的最大值
    第2次:将坐标xmin>xmax等这种不合规律的,四个坐标全部设置为0
    coors的shape(-1,4)
    
    一个分数:将每个box类别概率最大的作为这个box的类别,这个概率再乘以置信度,作为这个box的分数score
    scores的shape(-1),只有1个维度
    #pred_conf就是1个维度的
    
    一个类别:因为还要写出来类别的名字,所以要将每个box的最大类别的索引号输出
    classes就是1个维度的,classes就是取出2维的概率的每个box概率最大的索引号,
    每个box只有一个最大的索引号,就一个数,然后这些数组成一个1维的就可以了
    '''

    return np.concatenate([coors, scores[:, np.newaxis], classes[:, np.newaxis]], axis=-1)
    # 把这些全部拼接起来,维度不够的先生一个维度,然后将最后一个维度拼接起来
    # 组成[-1:6] 6:x,y,w,h,score,classes索引

大致思想:
1.坐标本是:中心坐标以及宽和高,先将其转换为pred_coor(xmin,ymin,xmax,ymax)
2.然后我们送进网络的图片大小是416416416*416,输出的目标也是相对于416416416*416的目标,现在需要将图片和目标进行扩增扩大到原图的大小115210601152*1060的大小。
3.将超过原图宽和高的坐标,设置为原图的宽和高 减1
检查xmin和xmax大小是否合乎情理,ymin和ymax大小是否合乎情理,有一个不合情理,坐标就置为0。
4.(xmaxxmin)(ymaxymin)\sqrt{(x_{max}-x_{min})(y_{max}-y_{min})}的值是否合理,不合理也置为0,其实这一处理,我不是很理解。。。
5.将分数低的box丢掉。
coors, scores, classes = pred_coor[mask], scores[mask], classes[mask]其中pred_coor[mask], scores[mask], classes[mask]表示的是一连串的true false,将为true的位置的pred_coor的坐标赋值给coors,以此类推。然后在拼接起来作为返回值。

  • NMS 刚才已经去除了很多box,现在需要将一个目标多个重复的框进行去除,利用NMS,这部分代码不写了,网上都一样,给一个iou阈值就行。
  • 然后化框,调用函数utils.draw_bbox
def draw_bbox(image, bboxes, classes=read_class_names(cfg.YOLO.CLASSES), show_label=True):
    """
    bboxes: [x_min, y_min, x_max, y_max, probability, cls_id] format coordinates.
    """

    num_classes = len(classes)  # 4
    image_h, image_w, _ = image.shape # [1152,1060,3]
    #颜色:https://blog.csdn.net/weixin_40688204/article/details/89150010
    ## 生成绘制边框的颜色。
    hsv_tuples = [(1.0 * x / num_classes, 1., 1.) for x in range(num_classes)]
    #使用HSV / HSB / HSL颜色空间(三个名字或多或少相同的东西)。
    # 生成N个元组在色调空间中均匀分布,然后将它们转换为RGB。
    colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
    #转换为RGB
    colors = list(map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)), colors))
    ##hsv取值范围在【0,1】,而RBG取值范围在【0,255】,所以乘上255
    random.seed(0) #产生随机种子。固定种子为一致的颜色
    random.shuffle(colors) #产生随机种子。固定种子为一致的颜色
    random.seed(None) ##重置种子为默认

    for i, bbox in enumerate(bboxes):
        coor = np.array(bbox[:4], dtype=np.int32) #shape(-1,4)
        fontScale = 0.5
        score = bbox[4] #1维
        class_ind = int(bbox[5]) #1维
        bbox_color = colors[class_ind] #选取一种颜色
        bbox_thick = int(0.6 * (image_h + image_w) / 600) #矩形框的粗细
        c1, c2 = (coor[0], coor[1]), (coor[2], coor[3]) #两个点的坐标
        cv2.rectangle(image, c1, c2, bbox_color, bbox_thick) #画图
        # 在图片上画了矩形

        if show_label: #显示标签
            bbox_mess = '%s: %.2f' % (classes[class_ind], score)
            #需要写的内容:名称和分数
            t_size = cv2.getTextSize(bbox_mess, 0, fontScale, thickness=bbox_thick//2)[0]
            #获取一个文字的宽度和高度
            cv2.rectangle(image, c1, (c1[0] + t_size[0], c1[1] - t_size[1] - 3), bbox_color, -1)  # filled
            #在图片的附近化一个矩形框,写目标的标签
            cv2.putText(image, bbox_mess, (c1[0], c1[1]-2), cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale, (0, 0, 0), bbox_thick//2, lineType=cv2.LINE_AA)
            #各参数依次是:图片,添加的文字,左上角坐标,字体,字体大小,颜色,字体粗细

    return image
  • 最后,将框和目标名称写在画在原图上就可以了。
  • 然后可以现实出来。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!