keras-yolov3代码解读

文章目录

  • model.py
    • yolo_head函数
    • preprocess_true_boxes函数
    • yolo_loss函数

看了几天,大概看懂了工程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

你可能感兴趣的:(计算机视觉)