看了几天,大概看懂了工程https://github.com/qqwweee/keras-yolo3的代码,记录一下
关于yolov3理论,推荐这篇博文: https://blog.csdn.net/leviopku/article/details/82660381 [1]
自己写的v1,v2: https://blog.csdn.net/qq_25800609/article/details/86683586 [2]
model.py
yolo_head函数
def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
"""Convert final layer features to bounding box parameters."""
"""
feat 是某一个尺度下的特征图 如13*13*255
"""
#将最后一层的特征 转换成box参数
num_anchors = len(anchors)
# Reshape to batch, height, width, num_anchors, box_params.
anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])
grid_shape = K.shape(feats)[1:3] # height, width #得到高宽
#下面在对grid进行什么操作,没看懂,挖坑……
grid_y = K.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
[1, grid_shape[1], 1, 1])
grid_x = K.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
[grid_shape[0], 1, 1, 1])
#拼接函数
grid = K.concatenate([grid_x, grid_y])
grid = K.cast(grid, K.dtype(feats))
#这里输出如果是小尺度就是 13*13*num_anchor*(num_classes+5)
#num_anchor 是3 表示有三个anchor 负责这层 也表示每个grid预测3个bounding box
feats = K.reshape(
feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
#调整预置到每个空间网格点和锚的大小
# Adjust preditions to each spatial grid point and anchor size.
#这里中心坐标x ,y和宽高wh的转换可以看 文献[2]
#把特征图的输出转换成 相对于特征图的比例 和坐标相对于整张图像的比例是等价的
box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
#把宽高 wh转换成相对于整张图像的比例
box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
#转换置信度和类别概率
box_confidence = K.sigmoid(feats[..., 4:5])
box_class_probs = K.sigmoid(feats[..., 5:])
#如果计算loss 为真 返回grid feats box_xy box_wh
if calc_loss == True:
return grid, feats, box_xy, box_wh
#返回 xy wh 置信度 类别概率
#此时xy wh都是相对于图像大小的比例值
return box_xy, box_wh, box_confidence, box_class_probs
preprocess_true_boxes函数
def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):
'''Preprocess true boxes to training input format
#将原始box 信息 转换成训练时输入的格式
Parameters
----------
true_boxes: array, shape=(m, T, 5)
Absolute x_min, y_min, x_max, y_max, class_id relative to input_shape.
表示m个图像 每个图像T个box ,每个box含有5种信息 x_min,y_min,x_max,y_max,class_id
即输入的这个true_boxes是最原始的txt中box信息
input_shape: array-like, hw, multiples of 32
anchors: array, shape=(N, 2), wh
num_classes: integer
Returns
-------
y_true: list of array, shape like yolo_outputs, xywh are reletive value
'''
#检查有无异常数据 即txt提供的box id 是否存在大于 num_class的情况
assert (true_boxes[..., 4]<num_classes).all(), 'class id must be less than num_classes'
num_layers = len(anchors)//3 # default setting
#不同尺度的anchor 分到不同尺度的 输出
anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers==3 else [[3,4,5], [1,2,3]]
true_boxes = np.array(true_boxes, dtype='float32')
input_shape = np.array(input_shape, dtype='int32')
#得到中心点坐标
boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
#得到box宽高
boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]
#中心坐标 和 宽高 都变成 相对于input_shape的比例
true_boxes[..., 0:2] = boxes_xy/input_shape[::-1]
true_boxes[..., 2:4] = boxes_wh/input_shape[::-1]
#现在true_boxes 中的数据成了 x,y,w,h
#这个m应该是batch的大小 即是输入图片的数量
m = true_boxes.shape[0]
grid_shapes = [input_shape//{0:32, 1:16, 2:8}[l] for l in range(num_layers)]
#grid_shape [13,13 ] [26,26] [52,52]
y_true = [np.zeros((m,grid_shapes[l][0],grid_shapes[l][1],len(anchor_mask[l]),5+num_classes),
dtype='float32') for l in range(num_layers)]
#y_true m*13*13*3*(5+num_clasess)
# m*26*26*3*(5+num_classes)
# m*52*52*3*(5+num_classes)
# Expand dim to apply broadcasting.
anchors = np.expand_dims(anchors, 0)
anchor_maxes = anchors / 2. #网格中心为原点(即网格中心坐标为 (0,0) ), 计算出anchor 右下角坐标
anchor_mins = -anchor_maxes #网格中心为原点 计算anchorr 左上角坐标
valid_mask = boxes_wh[..., 0]>0 #去掉异常数据
#遍历所有batch 个图片
for b in range(m):
# Discard zero rows.
#取出这张图片的 所有有效box_wh
wh = boxes_wh[b, valid_mask[b]]
if len(wh)==0: continue
# Expand dim to apply broadcasting.
wh = np.expand_dims(wh, -2)
box_maxes = wh / 2. # 假设 bouding box 的中心也位于网格的中心
box_mins = -box_maxes
#下面就是在计算 ground_true与anchor box的交并比
intersect_mins = np.maximum(box_mins, anchor_mins)
intersect_maxes = np.minimum(box_maxes, anchor_maxes)
intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
box_area = wh[..., 0] * wh[..., 1]
anchor_area = anchors[..., 0] * anchors[..., 1]
iou = intersect_area / (box_area + anchor_area - intersect_area)
#算boundinxg box 与 anchor的交并比
# Find best anchor for each true box
#对于每个真实box 找到最匹配的anchor best_anchor 的格式为 bounding_box id -> anchor_id
best_anchor = np.argmax(iou, axis=-1)
#遍历所有 匹配的anchor
#t是bounding box id,n是anchor_id
for t, n in enumerate(best_anchor):
#遍历anchor 尺寸 3个尺寸
#因为此时box 已经和一个anchor box匹配上,看这个anchor box属于那一层,小,中,大,然后将其box分配到那一层
for l in range(num_layers):
#anchor_mask [ 6,7,8 3,4,5 0,1,2]
#如果匹配的这个n即 anchor id在 l这一层,那么进行赋值数据
if n in anchor_mask[l]:
#np.floor 返回不大于输入参数的最大整数。 即对于输入值 x ,将返回最大的整数 i ,使得 i <= x。
#true_boxes x,y,w,h, 此时x y w h都是相对于整张图像的
#第b个图像 第 t个 bounding box的 x 乘以 第l个grid shap的x(grid shape 格式是hw,
#因为input_shape格式是hw)
#找到这个bounding box落在哪个cell的中心
i = np.floor(true_boxes[b,t,0]*grid_shapes[l][1]).astype('int32')
j = np.floor(true_boxes[b,t,1]*grid_shapes[l][0]).astype('int32')
#找到n 在 anchor_box的索引位置
k = anchor_mask[l].index(n)
#得到box的id
c = true_boxes[b,t, 4].astype('int32')
#第b个图像 第j行 i列 第k个anchor x,y,w,h,confindence,类别概率
y_true[l][b, j, i, k, 0:4] = true_boxes[b,t, 0:4]
y_true[l][b, j, i, k, 4] = 1 #置信度是1 因为含有目标
y_true[l][b, j, i, k, 5+c] = 1 #类别的one-hot编码
return y_true
yolo_loss函数
#yolo loss代码
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False):
'''Return yolo_loss tensor
Parameters
----------
yolo_outputs: list of tensor, the output of yolo_body or tiny_yolo_body
y_true: list of array, the output of preprocess_true_boxes
#第b个图像 第j行 i列 第k个anchor x,y,w,h,confindence,类别概率
anchors: array, shape=(N, 2), wh
num_classes: integer
ignore_thresh: float, the iou threshold whether to ignore object confidence loss
Returns
-------
loss: tensor, shape=(1,)
'''
num_layers = len(anchors)//3 # default setting
yolo_outputs = args[:num_layers] #这里存储的是输出
y_true = args[num_layers:] #这里存储的是ground truth
anchor_mask = [[6,7,8], [3,4,5], [0,1,2]] if num_layers == 3 else [[3,4,5], [1,2,3]]
input_shape = K.cast(K.shape(yolo_outputs[0])[1:3] * 32, K.dtype(y_true[0]))
grid_shapes = [K.cast(K.shape(yolo_outputs[l])[1:3], K.dtype(y_true[0])) for l in range(num_layers)]
loss = 0
m = K.shape(yolo_outputs[0])[0] # batch size, tensor
mf = K.cast(m, K.dtype(yolo_outputs[0]))
#遍历每个尺度
for l in range(num_layers):
object_mask = y_true[l][..., 4:5] #置信度
true_class_probs = y_true[l][..., 5:] # 类别概率
#这个yolo_head 因为calc_loss=True 返回 grid 特征图 xy wh,特征图是最原始输出,xy是相对于特征图,wh是相对于整张图像
grid, raw_pred, pred_xy, pred_wh = yolo_head(yolo_outputs[l],
anchors[anchor_mask[l]], num_classes, input_shape, calc_loss=True)
#将xy与wh进行拼接
pred_box = K.concatenate([pred_xy, pred_wh])
# Darknet raw box to calculate loss.
#darknet 原始盒子 来计算损失
#将y_ture转换成最原始 的 没有加经过处理的输出 是yolo_head函数中转换xy wh的逆过程
raw_true_xy = y_true[l][..., :2]*grid_shapes[l][::-1] - grid
#xy true一开始存储的是xy相对于整张图像的比例值大小 经过操作后 就变成 相对于相对于当前cell的偏移值了
raw_true_wh = K.log(y_true[l][..., 2:4] / anchors[anchor_mask[l]] * input_shape[::-1])
raw_true_wh = K.switch(object_mask, raw_true_wh, K.zeros_like(raw_true_wh)) # avoid log(0)=-inf
box_loss_scale = 2 - y_true[l][...,2:3]*y_true[l][...,3:4]
"""
大框给小权重,小框给大权重,因为大框的xywh不需要学得那么好,而小框则对xywh很敏感
为了调整不同大小的预测框所占损失的比重,真值框越小,
box_loss_scale越大,这样越小的框的损失占比越大,和v1,v2里采用sqrt(w)的目的一样
"""
# Find ignore mask, iterate over each of batch.
ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True)
#object 4:5 存储的是置信度 将其转换为bool类型
object_mask_bool = K.cast(object_mask, 'bool')
def loop_body(b, ignore_mask):
#这里看了下tf.boolean_mast 函数 将置信度为1 (即含有目标) 赋值给true_box
true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0])
#遍历第b(即mini_batch_size)个图像 这个图像上所有的预测box和 当前尺度下的所有gt做iou
iou = box_iou(pred_box[b], true_box)
best_iou = K.max(iou, axis=-1)
#如果一张图片的最大iou 都小于阈值 认为这张图片没有目标
ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
return b+1, ignore_mask
_, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])
"""
如果某个anchor不负责预测GT,且该anchor预测的框与图中所有GT的IOU都小于某个阈值,则让它预测背景,
如果大于阈值则不参与损失计算
"""
ignore_mask = ignore_mask.stack()
ignore_mask = K.expand_dims(ignore_mask, -1)
# K.binary_crossentropy is helpful to avoid exp overflow.
#现在raw_true_xy,raw_pred 都是特征图得直接输出 没有经过任何处理
#这里会对raw_pred进行sigmod操作 所以xyloss 输入交叉熵的都是相对于 当前cell左上角偏移值 的 一个交叉熵
xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[...,0:2], from_logits=True)
wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh-raw_pred[...,2:4])
confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True)+ \
(1-object_mask) * K.binary_crossentropy(object_mask, raw_pred[...,4:5], from_logits=True) * ignore_mask
#感觉这里体现了臭豆腐说的那句话
"""
1-object_mast 说明该这个anchor 不负责 预测GT Object_mask=y_ture 可以再仔细看下y_true的存储格式
如果某个anchor不负责预测GT,且该anchor预测的框与图中所有GT的IOU都小于某个阈值,则让它预测背景,
如果大于阈值则不参与损失计算
"""
class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[...,5:], from_logits=True)
xy_loss = K.sum(xy_loss) / mf
wh_loss = K.sum(wh_loss) / mf
confidence_loss = K.sum(confidence_loss) / mf
class_loss = K.sum(class_loss) / mf
loss += xy_loss + wh_loss + confidence_loss + class_loss
if print_loss:
loss = tf.Print(loss, [loss, xy_loss, wh_loss, confidence_loss, class_loss, K.sum(ignore_mask)], message='loss: ')
return loss