yolov3 原理代码复现3

yolov3代码复现之4预测

在前两篇文章中,我们已经知道网络的结构和怎么求loss,这就代表我们可以来训练模型了,模型训练完后,我们怎么利用现有的模型来预测一张图片呢?这就是我们这篇文章要解决的问题。

首先,我们已经知道了整个网络最后会输出三个尺度的数据,[b, 255, 13, 13],[b, 255, 26, 26], [b, 255, 52, 52],在训练模型的时候,我们需要将这三个尺度分开处理,但是在这,在预测这块,我们不再需要这样做,我们可以将这三个尺度的数据进行合并,数据维度处理流程为:[b, 255, 13, 13]->[b, 3, 13, 13, 85]->[b, 3x13x13, 85],[b, 255, 26, 26]->[b, 3, 26, 26, 85]->[b, 3x26x26,85],以及 [b, 255, 52, 52]->[b, 3, 52,52,85]->[b, 3x52x52,85],当我们将数据转化为这种形式之后就进行维度合并,使用torch,cat这个函数,最后合并后的维度为[b, 3x13x13+3x26x26+3x52x52,85],然后我们在对这个数据进行删选,删选的原则:置信度那一列小于阈值的的行去掉,然后对置信度进行排序,从高到低,然后对于80个列表,我们选择最大的那个,再对重复的边框进行处理。基本思想就是这样,接下来我们看看具体的处理。

在前篇文章中,我们知道85中的x,y,w,h只是我们默认的设置,并不是真实的x,y,w,h,那么我们首先就要将这些数据转化为真实的预测边框的x,y,w,h,大家看看下面这张图:
在这里插入图片描述
这就是我们的转化函数,指的注意的是,在上一篇文章对于dx函数的计算的时候,我们是将pw*dx当成85个数据中的第一列数据来计算的,所以最后处理数据时候的x就等于中心坐标对应的小框框左上角数据加上x这行的网络计算的数据,具体代码如下:

anchors = [(116, 90), (156, 198), (373, 326), (30, 61), (62, 45), (59, 119), (10, 13), (16, 30), (33, 23)]

def predict(prediction, inp_dim=416, num_class=80, cuda=False):
    '''this code was desinged by nike hu
    auchors:默认的auchor尺寸,
    #第一次yolo层,x为13*13*255,
    anchormask= [6, 7, 8] anchors= [(116, 90), (156, 198), (373, 326)]
    第二次yolo层,x为26*26*255,
    mask= [3, 4, 5] anchors= [(30, 61), (62, 45), (59, 119)]
    第三次yolo层,x为52*52*255,
    mask= [0, 1, 2] anchors= [(10, 13), (16, 30), (33, 23)]
    '''
    batchsize = prediction.shape[0] # 这个代表输入有多少张照片
    stridehw = inp_dim // prediction.shape[2] # 这个代表在原图尺寸为446的基础上,现在的特征图要缩小的倍数
    grid_size = inp_dim // stridehw # 这个代表最后处理的时候,要扩大的倍数
    bbox_attrs = 5 + num_class # 5代表x,y,w,h,和置信度,num_class代表类别总数,coco数据集是80类
    judgeAchors = prediction.shape[2] # 根据输入特征图的尺寸来选择achores
    if judgeAchors == 13:
        anchor = anchors[0:3]
    if judgeAchors == 26:
        anchor = anchors[3:6]
    if judgeAchors == 52:
        anchor = anchors[6:]
    num_anchors = len(anchor)
    anchor = [(a[0] / stridehw, a[1] / stridehw) for a in anchor]
    prediction = prediction.view(batchsize, bbox_attrs*num_anchors, grid_size*grid_size)
    # [b, 255, 13, 13]=>[b, 85*3, 13*13]
    prediction = prediction.transpose(1, 2).contiguous()
    # ->[b, 13*13, 85*3],之所以要加conti,,,,交换维度,交换后数据就不再联系,要使用contiguous将数据连续起来
    prediction = prediction.view(batchsize, grid_size*grid_size*num_anchors, bbox_attrs)
    #->[b, 13*13*3, 85]
    '''接下来对第一列,第二列,第五列进行sigmod,这几列分别对应坐标
    x,y,以及置信度'''
    prediction[:,:,0] = torch.sigmoid(prediction[:,:,0])
    prediction[:,:,1] = torch.sigmoid(prediction[:,:,1])
    prediction[:,:,4] = torch.sigmoid(prediction[:,:,4])
    '''接下来对坐标进行赋值'''
    grid_len = np.arange(grid_size) # 生成连续的数据
    a, b = np.meshgrid(grid_len, grid_len) # 生成网格数据
    x_offset = torch.from_numpy(a).view(-1, 1) # 维度为(13*13, 1)
    y_offset = torch.from_numpy(b).view(-1, 1)
    device = torch.device('cuda')
    if cuda:
        x_offset = x_offset.to(device)
        y_offset = y_offset.to(device)
    x_y_offset = torch.cat([x_offset, y_offset], dim=1).repeat(1, num_anchors).view(-1, 2).unsqueeze(0)
    # 之所以repeat那里是复制num_anchors次,是由于最后是生成num_anchors个anchors,最后维度为(13*13*3, 2),跟predict维度对应上
    # prediction = prediction.to(device)
    prediction[:, :, :2] += x_y_offset # 对坐标进行赋值,将网格偏移添加到中心坐标预测中:
    anchor = torch.Tensor(anchor)
    if cuda:
        anchor = anchor.to(device)
    anchor = anchor.repeat(grid_size*grid_size, 1).unsqueeze(0)
    prediction[:,:,2:4] = torch.exp(prediction[:,:,2:4])*anchor
    # 这里是对宽和高进行赋值,利用到了公式,论文中有说明
    prediction[:,:,5:] = torch.sigmoid(prediction[:,:,5:])
    # 对每一类都进行sigmod,,是因为数据集标签中存在复合类型,即能够匹配多个标签,softmax必须保证标签两两互斥。
    prediction[:,:,:4] *= stridehw
    # 将检测图的大小调整到和最初输入图像一致
    print('最后的维度->', prediction.shape)
    return prediction

我们输入两张照片的数据,经过对这两数据进行处理,输入维度变为[2, 3,416,416],经过神经网络的计算,然后使用上面的predict函数,注意是单独的将神经网络每一层的数据输入到predict,最后结果为:

最后的维度-> torch.Size([2, 507, 85])
最后的维度-> torch.Size([2, 2028, 85])
最后的维度-> torch.Size([2, 8112, 85])

然后我们使用下面的函数将这三个经过处理的数据进行合并:

x = torch.cat((predict(x1), predict(x2), predict(x3)), dim=1)

最后输出结果的维度为[bachsize, 10647, 85]。

好了,我们在经过上面的处理之后,我们就要开始对[bachsize, 10647, 85]这数据进行处理了,在这些数据里面,肯定有很多重合度很高的边框,那么我们就要开始处理这些重复的边框。这时候,我们就要想办法处理了,不知道大家是否还记得上篇文章中所提到的Iou的概念,在上篇文章中,iou只是针对两个边框,这两个边框的中心坐标是没有定的,但是,我们预测出来的边框是确定了中心点的,所以,这时候的Iou处理的方式跟上一篇还是有些区别。

首先,我们要将中心坐标转化为左上角右下角的坐标,转化函数如下:

# 接下来将x,y,w,h转化为左上角和右下角
def change_to_conner(image_array):
    change_array = torch.zeros_like(image_array)
    change_array[:,:,0] = image_array[:,:,0] - image_array[:,:,3] / 2 # 左上角x
    change_array[:,:,1] = image_array[:,:,1] - image_array[:,:,4] / 2 # 左上角y
    change_array[:,:,2] = image_array[:,:,0] + image_array[:,:,3] / 2 # 右下角x
    change_array[:,:,3] = image_array[:,:,1] + image_array[:,:,4] / 2 # 右下角y
    change_array[:,:,4:] = image_array[:,:,4:]
    return change_array

坐标转化之后,计算两个边框的Iou,代码如下:

def get_iou(box1, box2):
    box1x1, box1y1, box1x2, box1y2 = box1[:,0], box1[:,1], box1[:,2], box1[:,3] # 参考坐标的左上角和右下角坐标
    box2x1, box2y1, box2x2, box2y2 = box2[:,0], box2[:,1], box2[:,2], box2[:,3] # 对比框的左下角和右下角
    box_max_x1 = torch.max(box1x1, box2x1)
    box_max_y1 = torch.max(box1y1, box2y1)
    box_max_x2 = torch.min(box1x2, box2x2)
    box_max_y2 = torch.min(box1y2, box2y2) # 将各自的最大值求出,返回的是一个矩阵
    # 求交叉的面积
    crossing_area = (box_max_x2 - box_max_x1 + 1) * (box_max_y2 - box_max_y1 + 1)
    # print('维度->', crossing_area.shape)
    # 总的面积
    sum_area = (box1x2 - box1x1 + 1) * (box1y2 - box1y1 + 1) + (box2x2 - box2x1 + 1) * (box2y2 - box2y1 + 1) - crossing_area
    # iou
    iou = abs(crossing_area) / abs(sum_area)
    return iou

介绍了这连个概念之后,我们开始理一下处理的流程:对比置信地阈值->将小于阈值的那些行全部变为0->处理类别的那80列,将里面最大的值和这个值对应的下标记录下来->统计预测到的类别->针对每一个类别进行删选边框->置信度排序->边框筛选,基本流程就是这样,我们下面看看代码:

'''this code was designed by nike hu'''
def write_result(prediction, confidence=0.5, num_class=80, nms_conf=0.5):
    batchsize = prediction.size(0) # 代表总共的照片数量
    conf_mask = (prediction[:,:,4] > confidence).float().unsqueeze(-1)
    #[b, 10647, 85]=>[b, 10647]=>[b, 10647, 1]
    prediction = prediction * conf_mask#这里就会将小于置信度阈值的行清零
    print('赋零后的数据->', prediction.shape)
    class_max, class_max_id = torch.max(prediction[:,:,5:], dim=2)#获得最大分类的分数以及下标,输出维度为[batchsize, 10647]
    class_max, class_max_id = class_max.unsqueeze(-1), class_max_id.unsqueeze(-1).float()#增加维度 变为[batchsize,10647,1],特别注意返回的下标矩阵的数据类型
    print('类别最大删选后维度->', class_max.shape, class_max_id.shape)
    pred_class = torch.cat((prediction[:,:,:5], class_max, class_max_id), dim=2)#第三个维度就变为x,y,w,h,置信度,类别分数,下标
    print('合并之后的维度->', pred_class.shape)
    pred_change_class = change_to_conner(pred_class)
    print('转化为左上角右下角的维度->', pred_change_class.shape)
    '''对于一个多维的数据,如果要统计某一列非零的下标,使用torch.nonzero的话貌似只能对两维进行某一列统计'''
    last_image_pre = torch.zeros((1, 8)) # 这个参数用来统计最后满足要求的行
    for image_id in range(batchsize):
        chirld_image = pred_change_class[image_id] # [batchsize, 10647, 7]->[10647, 7]
        print('子照片维度->', chirld_image.shape)
        '''接下来将置信度为0的直接去掉,而不是像上面只是赋零'''
        zero_index = torch.nonzero(chirld_image[:,4]) # 统计置信度那一列非零的下标,返回的是非零的行数,类似[3071, 1]
        chirld_image = chirld_image[zero_index[:,0]] # ->类似[4649,7]
        print('非零的行数->', chirld_image.shape)
        classes = torch.unique(chirld_image[:, -1]) # 统计一张照片找到的类别总数
        # print('这种照片对应的类别总数->', classes, classes.shape)
        for image_class in classes:
            # print('最初的维度->', chirld_image.shape)
            image_in = torch.eq(chirld_image[:,-1], image_class).float().unsqueeze(-1).expand(chirld_image.size(0), 7) # 找到照片中跟类别一样的下标,维度为[n,7]
            # print('下标为->', image_in.shape)
            chirld_image_then = (chirld_image * image_in) # 这个参数需要随时更新
            # print('合并后维度->', chirld_image_then.shape)
            chirld_image_id = torch.nonzero(chirld_image_then[:, -1])
            # print('非零的行数', chirld_image_id)
            chirld_image_then = chirld_image_then[chirld_image_id[:,0]] # 去掉为0的行数, 维度类似[4197, 7]
            # print('去掉行数为0的维度->', chirld_image_then.shape)
            chirld_image_sort_id = torch.sort(chirld_image_then[:,4], descending=True)[1] # 依旧会得到从大到小的排序的数值和下标
            chirld_image_then = chirld_image_then[chirld_image_sort_id]
            # print('排序后的下标->', chirld_image_then.shape)
            for i in range(chirld_image_then.size(0)): # 这里就是同一类别可能出现的cell
                try:
                    iou = get_iou(chirld_image_then[i].unsqueeze(0), chirld_image_then[i+1:]) # 获取iou矩阵
                except IndexError: # 这个是下标超界,因为矩阵会越来越小
                    break
                # print('iou->', iou)
                compare_id = (iou < nms_conf).float().unsqueeze(-1) # 注意这里是小于而不是大于
                # print(compare_id.shape)
                chirld_image_then[i+1:] *= compare_id # 将iou大于阈值的部分清零
                classif_id = torch.nonzero(chirld_image_then[:,-1])
                # print('删选和-.', classif_id)
                chirld_image_then = chirld_image_then[classif_id[:,0]] # 这里将不符合要求的边框去掉了
            imageid = torch.full([chirld_image_then.size(0)], image_id).view(-1, 1) # 一行全为图片的编号
            # print('之前->', chirld_image_then.shape)
            chirld_image_then = torch.cat((imageid, chirld_image_then), dim=1)
            # print('之后->',chirld_image_then.shape)
            last_image_pre = torch.cat((last_image_pre, chirld_image_then), dim=0)
            # print('.......................')
    print('最后返回->', last_image_pre[1:20, 0], last_image_pre[1:20, 1])
    return last_image_pre[1:,:]#第一行全为0,返回的时候要排除

好了,处理预测数据基本就这样了,经过上面的处理,我们可以得到预测类别可能性最大的那一行,我们可以通过那一行的x,y,w,h来进行边框的作图。对于作图这块,我一般习惯性的用opencv来作图,但是openv作图,特别是在原图中标注,只能标注英文,不能标注中文,为了解决这个问题,只能退而求其次,用pil这个库来操作,大家感兴趣的话可以自行了解或者我之后再写一篇这块的内容。

感谢大家的阅读,希望我的讲解能为大家减少一些弯路,我的Yolov3的讲解就到此为止了,至于如果加载数据,如何显示预测的图片这些东西,我就不再累述了,因为我也没真打算将我的这些代码用在实际工程中,有兴趣的同学,可以自行将我没有写出来的代码写出来,再次感谢大家的支持。

2020 4.14

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值