目标:
1. 学习使用YOLO(you only look once)算法
修改【参考文章】的代码,使用tensorflow2实现
参考自:
1.【中文】【吴恩达课后编程作业】Course 4 - 卷积神经网络 - 第三周作业 - 车辆识别
2.吴恩达深度学习课后作业tensorflow2实现
确保先安装了keras,tf.keras不顶用
pip install keras
实际上,为了方便数据的操作,最后CNN输出的是(m,19,19,425)的向量
每个小格子预测一种类型的物体,将置信度p*每个分类的概率c,然后取p*c的最大值,就得到了该小格子最大可能是什么物体。
(1)box_confidnce:(m,19,19,5,1),包含每个小格子的置信度 p
(2)boxes:(m,19,19,5,4),包含每个格子预测物体的 位置 ( p x , p y , p h , p w ) (p_x,p_y,p_h,p_w) (px,py,ph,pw)
(3)box_classes_probs:(m,19,19,5,80),包含每个格子预测物体种类的概率
def yolo_filter_boxes(box_confidence,boxes,box_class_probs,threshold=0.6):
"""
通过阈值来过滤对象和分类的置信度。
参数:
box_confidence - tensor类型,维度为(19,19,5,1),包含19x19单元格中每个单元格预测的5个锚框中的所有的锚框的pc (一些对象的置信概率)。
boxes - tensor类型,维度为(19,19,5,4),包含了所有的锚框的(px,py,ph,pw )。
box_class_probs - tensor类型,维度为(19,19,5,80),包含了所有单元格中所有锚框的所有对象( c1,c2,c3,···,c80 )检测的概率。
threshold - 实数,阈值,如果分类预测的概率高于它,那么这个分类预测的概率就会被保留。
返回:
scores - tensor 类型,维度为(None,),包含了保留了的锚框的分类概率。
boxes - tensor 类型,维度为(None,4),包含了保留了的锚框的(b_x, b_y, b_h, b_w)
classess - tensor 类型,维度为(None,),包含了保留了的锚框的索引
注意:"None"是因为你不知道所选框的确切数量,因为它取决于阈值。
比如:如果有10个锚框,scores的实际输出大小将是(10,)
"""
# 第一步,计算锚框的得分
box_scores = box_confidence * box_class_probs
# out: (19,19,5,80)
# 第二步,找到最大值锚框的索引,[以及]对应最大值锚框的分数
box_classes = tf.argmax(box_scores,axis=-1)
# out: (19,19,5)
box_class_scores = tf.reduce_max(box_scores,axis=-1)
# out: (19,19,5)
# 第三步,根据阈值 创建掩码
filtering_mask = box_class_scores >= threshold
# out: (19,19,5)的boolean数组
# 对scores,boxes以及classes使用掩码
# boolean_mask如果axis不写,则从第一个维度开始
# boolean_mask每次执行的结果,放进一个数组
scores = tf.boolean_mask(box_class_scores,filtering_mask)
# out: (1786,) 消失了一部分数据
boxes = tf.boolean_mask(boxes,filtering_mask)
# out: (1786,4)
classes = tf.boolean_mask(box_classes,filtering_mask)
# out: (1786,)
return scores,boxes,classes
测试:
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].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.shape))
print("boxes.shape = " + str(boxes.shape))
print("classes.shape = " + str(classes.shape))
输出:
scores[2] = 16.064978
boxes[2] = [ 1.0273384 -2.1178942 4.8872733 -4.0143332]
classes[2] = 78
scores.shape = (1786,)
boxes.shape = (1786, 4)
classes.shape = (1786,)
由于非最大值抑制输入的boxes的格式是对角线的两个顶点作为定位的,这里需要转换一下。
原来的图像可能也不是608x608的,这里也需要缩放一下boxes,然后再进行非最大值抑制。
对上面的输出进行非最大值抑制/非极大值抑制NMS(Non-Maximum Suppression)
IoU = 交集的面积/并集的面积
非最大值抑制
流程:
(所以,非最大值抑制,还是可以有部分交集的,但是不能太多)
def yolo_non_max_suppression(scores,boxes,classes,max_boxes=10,iou_threshold=0.5):
"""
为锚框实现非最大值抑制( Non-max suppression (NMS))
参数:
scores - tensor类型,维度为(None,),yolo_filter_boxes()的输出,每个小框的分数
boxes - tensor类型,维度为(None,4),yolo_filter_boxes()的输出,每个小框的位置
classes - tensor类型,维度为(None,),yolo_filter_boxes()的输出,每个小框预测的物体类别
max_boxes - 整数,预测的锚框数量的最大值
iou_threshold - 实数,交并比阈值。
返回:
scores - tensor类型,维度为(None,),每个锚框的预测的可能值
boxes - tensor类型,维度为(None,4),预测的锚框的坐标
classes - tensor类型,维度为(None,),每个锚框的预测的分类
注意:"None"是明显小于max_boxes的,这个函数也会改变scores、boxes、classes的维度,这会为下一步操作提供方便。
"""
# 使用使用tf.image.non_max_suppression()来获取与我们保留的框相对应的索引列表
# 返回分数最高的max_boxes个边框的【索引值】
nms_indices = tf.image.non_max_suppression(boxes=boxes,
scores=scores,
max_output_size=max_boxes,
iou_threshold=iou_threshold)
# 使用keras.backend.gather()根据索引选择对应的分数、位置和分类
scores = tf.keras.backend.gather(reference=scores,indices=nms_indices)
boxes = tf.keras.backend.gather(boxes,nms_indices)
classes = tf.keras.backend.gather(classes,nms_indices)
return scores,boxes,classes
测试:
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].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.numpy().shape))
print("boxes.shape = " + str(boxes.numpy().shape))
print("classes.shape = " + str(classes.numpy().shape))
输出:
scores[2] = 6.938395
boxes[2] = [ 3.4738503 -0.2052151 1.9243622 1.6695945]
classes[2] = 0.59752893
scores.shape = (10,)
boxes.shape = (10, 4)
classes.shape = (10,)
将上面的操作,合并一下
def yolo_eval(yolo_outputs,image_shape=(720.,1280.),max_boxes=10,
score_threshold=0.6,iou_threshold=0.5):
"""
将YOLO编码的输出(很多锚框)转换为预测框以及它们的分数,框坐标和类。
参数:
yolo_outputs - 编码模型的输出(对于维度为(608,608,3)的图片),包含4个tensors类型的变量:
box_confidence : tensor类型,维度为(19, 19, 5, 1)
box_xy : tensor类型,维度为(19, 19, 5, 2)
box_wh : tensor类型,维度为(19, 19, 5, 2)
box_class_probs: tensor类型,维度为(19, 19, 5, 80)
image_shape - tensor类型,维度为(2,),包含了输入的图像的维度,这里是(608.,608.)
max_boxes - 整数,预测的锚框数量的最大值
score_threshold - 实数,可能性阈值。
iou_threshold - 实数,交并比阈值。
返回:
scores - tensor类型,维度为(None,4),每个锚框的预测的可能值
boxes - tensor类型,维度为(None,4),预测的锚框的坐标
classes - tensor类型,维度为(None,),每个锚框的预测的分类
"""
# 获取YOLO CNN模型的输出
box_confidence,box_xy,box_wh,box_class_probs = yolo_outputs
# 中心点转换为边角
boxes = yolo_utils.yolo_boxes_to_corners(box_xy,box_wh)
# 可信度分值过滤
scores,boxes,classes = yolo_filter_boxes(box_confidence,
boxes,
box_class_probs,
score_threshold)
# 缩放锚框,以适应图像
boxes = yolo_utils.scale_boxes(boxes,image_shape)
# 使用非最大值抑制
scores,boxes,classes = yolo_non_max_suppression(scores,
boxes,
classes,
max_boxes,
iou_threshold)
return scores,boxes,classes
测试:
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].numpy()))
print("boxes[2] = " + str(boxes[2].numpy()))
print("classes[2] = " + str(classes[2].numpy()))
print("scores.shape = " + str(scores.numpy().shape))
print("boxes.shape = " + str(boxes.numpy().shape))
print("classes.shape = " + str(classes.numpy().shape))
输出:
scores[2] = 138.6871
boxes[2] = [-3737.2234 2206.7576 -1381.3837 2188.9182]
classes[2] = 60
scores.shape = (10,)
boxes.shape = (10, 4)
classes.shape = (10,)
def predict(yolo_model,image_file,is_show_info=True,is_plot=True):
"""
运行存储在sess的计算图以预测image_file的边界框,打印出预测的图与信息。
参数:
sess - 包含了YOLO计算图的TensorFlow/Keras的会话。
image_file - 存储在images文件夹下的图片名称
返回:
out_scores - tensor类型,维度为(None,),锚框的预测的可能值。
out_boxes - tensor类型,维度为(None,4),包含了锚框位置信息。
out_classes - tensor类型,维度为(None,),锚框的预测的分类索引。
"""
class_names = yolo_utils.read_classes("model_data/coco_classes.txt")
anchors = yolo_utils.read_anchors("model_data/yolo_anchors.txt")
image_shape = (720., 1280.)
# 图像预处理
image, image_data = yolo_utils.preprocess_image("images/"+image_file,model_image_size=(608,608))
# 预测图像,结果为(None,19,19,425)
yolo_model_output = yolo_model.predict(image_data)
print("yolo_model_output.shape",yolo_model_output.shape)
# yolo_head将yolo模型的输出进行转换为各个各自种每个锚框的(坐标、宽高、预测值、分类值)
yolo_outputs = yolo_head(yolo_model_output,anchors,len(class_names))
scores, boxes, classes = yolo_eval(yolo_outputs, image_shape)
# 打印预测信息
if is_show_info:
print("在" + str(image_file) + "中找到了" + str(len(boxes)) + "个锚框。")
# 指定要绘制的边框的颜色
colors = yolo_utils.generate_colors(class_names)
# 绘制边界并保存图片
yolo_utils.draw_boxes(image,scores,boxes,classes,class_names,colors)
image.save(os.path.join("out",image_file),quality=100)
# 打印出已经绘制了边界框的图
if is_plot:
output_image = plt.imread(os.path.join("out",image_file))
plt.imshow(output_image)
plt.show()
return scores,boxes,classes
yolo_utils.preprocess_image
进行图片的预处理:缩放图片,归一化图片
yolo_head
将CNN的输出(1,19,19,425),转换成
yolo_outputs:{
box_confidence(19,19,5,1) 置信度
box_xy(19,19,5,2) 方框的xy坐标
box_wh(19,19,5,2) 方框的宽度w和高度h
box_class_probs(19,19,5,80) 方框每个锚框的每个物体种类的预测概率
}
ps: 将model当作参数传入后,在批量predict的时候,就不用重复加载了
def batchPredict():
# 获得模型
yolo_model = tf.keras.models.load_model("model_data/yolo.h5")
for i in range(1,121):
num_fill = int(len("0000") - len(str(1))) + 1
filename = str(i).zfill(num_fill) + ".jpg"
print("当前文件:" + str(filename))
# 开始预测
predict(yolo_model=yolo_model,
image_file=filename,
is_show_info=False,
is_plot=False)
print("预测完成")
tf.boolean_mask(
tensor,
mask=,
axis=None,
name,
)
boolean_mask如果axis不写,则从第一个维度开始
boolean_mask每次执行的结果,放进一个数组
tf.image.non_max_suppression(
boxes, 由边框对角线的两个坐标(y1,x1,y2,x2)组成
scores, 每个边框的分数
max_output_size, 最多输出多少个边框
iou_threshold=0.5, 交并比的阈值
)
传入索引值数组,获得对应的元素列表
tf.keras.backend.gather(
reference, 原数组
indices 索引值数组
)
var = tf.keras.backend.variable([[1, 2, 3], [4, 5, 6]])
var_gathered = tf.keras.backend.gather(var, [0,1,0])
输出:
var_gathered = array([[1., 2., 3.],
[4., 5., 6.],
[1., 2., 3.]], dtype=float32)
load_model时候发生错误,参考
关于吴恩达车辆识别代码出错的问题
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, feats.dtype) # 原本是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