一 分析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成,同时进行色域转换,并加上batch_size这一个维度(为了保证和训练的维度一样)。
然后用一组向量去接收,计算图的结果。
最终对结果进行后处理,以及NMS去除重复的框,然后将框画在原图上。
1. 参数的定义
这一部分没什么好说的,就是一些变量的定义,后续会使用到,唯一需要注意的就是第一个变量return_elements
这个变量是用来接收计算图的结果的,要写成向量的形式,即加上:0
表示是向量。
2. 输入的原图进行预处理
首先,通过cv2.imread
读取一张图片,但是读取的图片是BGR形式的,需要通过cv2.cvtColor
手动转为RGB色域。
假设我们输入的图片大小为,但是训练的时候为了统一,我们将所有的训练图片变为,所以测试的时候也需要将图片resize为。
如何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:其实就是将原图片按照比例不变resize成所以部分地方需要用其他颜色来填充。
train:将原图片按照比例不变resize成,同时将目标的框进行resize为416大小的相应尺寸。
但是在测试阶段,只是考虑,将按照比例resize为。
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得到的相对于上的所有预测框组成的向量。 - 通过拼接函数
np.concatenate
将得到三个尺度合为一个尺度上,得到pred_bbox
,shape是(, 5+类别),有个[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.然后我们送进网络的图片大小是,输出的目标也是相对于的目标,现在需要将图片和目标进行扩增扩大到原图的大小的大小。
3.将超过原图宽和高的坐标,设置为原图的宽和高 减1
检查xmin和xmax大小是否合乎情理,ymin和ymax大小是否合乎情理,有一个不合情理,坐标就置为0。
4.的值是否合理,不合理也置为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
- 最后,将框和目标名称写在画在原图上就可以了。
- 然后可以现实出来。
来源:CSDN
作者:dlut_yan
链接:https://blog.csdn.net/weixin_43384257/article/details/103463604