目标检测与位姿估计(七):PyTorch-YOLOv4的使用

该工程还未完全完成!之后随着项目的进行将逐渐完善,如果您在参考博客的过程中遇到解决不了的问题欢迎直接与我联系!

YOLO V4出来也几天了,论文大致看了下,然后看到大量的优秀者实现了各个版本的YOLOV4了。

Yolo v4 论文: https://arxiv.org/abs/2004.10934

AB大神Darknet版本的源码实现: https://github.com/AlexeyAB/darknet

本文针对Pytorch版本实现的YOLOV4进行分析,感谢Tianxiaomo 分享的工程:Pytorch-YoloV4


作者分享的权重文件,下载地址:

该权重文件yolov4.weights 是在coco数据集上训练的,目标类有80种,当前工程支持推理,不包括训练~

我的测试环境是anaconda配置的环境,pytorch1.0.1, torchvision 0.2.1;


工程目录如下:

终端运行指令:

  1. # 指令需要传入cfg文件路径,权重文件路径,图像路径

  2. >>python demo.py cfg/yolov4.cfg yolov4.weights data/dog.jpg

运行结果会生成一张检测后的图:predictions.jpg

 接下来对源码做分析:

其中demo.py中,主要调用了函数detect(),其代码如下:

def detect(cfgfile, weightfile, imgfile):
    m = Darknet(cfgfile) #穿件Darknet模型对象m
    m.print_network() # 打印网络结构
    m.load_weights(weightfile) #加载权重值
    print('Loading weights from %s... Done!' % (weightfile))
    num_classes = 80

    if num_classes == 20:
        namesfile = 'data/voc.names'
    elif num_classes == 80:
        namesfile = 'data/coco.names'
    else:
        namesfile = 'data/names'
use_cuda = 0 # 是否使用cuda,工程使用的是cpu执行
if use_cuda:
m.cuda() # 如果使用cuda则将模型对象拷贝至显存,默认GUP ID为0;
img = Image.open(imgfile).convert('RGB') # PIL打开图像
sized = img.resize((m.width, m.height))
for i in range(2):
    start = time.time()
    boxes = do_detect(m, sized, 0.5, 0.4, use_cuda) # 做检测,返回的boxes是昨晚nms后的检测框;
    finish = time.time()
    if i == 1:
        print('%s: Predicted in %f seconds.' % (imgfile, (finish - start)))
        class_names = load_class_names(namesfile) # 加载类别名

plot_boxes(img, boxes, 'predictions.jpg', class_names)# 画框,并输出检测结果图像文件;

在创建Darknet()对象过程中,会根据传入的cfg文件做初始化工作,主要是cfg文件的解析,提取cfg中的每个block;网络结构的构建;(如下图)


 现在先说下根据cfg文件是如何解析网络结果吧,主要调用了tool/cfg.py的parse_cfg()函数,它会返回blocks,网络结果是长这个样子的(使用Netron网络查看工具 打开cfg文件,完整版请自行尝试):


 创建网络模型是调用了darknet2pytorch.py中的create_network()函数,它会根据解析cfg得到的blocks构建网络,先创建个ModuleList模型列表,为每个block创建个Sequential(),将每个block中的卷积操作,BN操作,激活操作都放到这个Sequential()中;可以理解为每个block对应一个Sequential();

构建好的的ModuleList模型列表大致结构如下:


返回demo.py 的detect()函数,构件好Darknet对象后,打印网络结构图,然后调用darknet2pytorch.py中的load_weights()加载权重文件,这里介绍下这个权重文件中的数值分别是什么以及怎么排序的。

对于没有bias的模型数据,从yolov4.weights加载的模型数据,其数值排列顺序为先是BN的bias(gamma),然后是BN的weight(alpha)值,然后是BN的mean,然后是BN的var, 最后是卷积操作的权重值,如下图,buf是加载后的yolov4.weights数据内容;网络第一个卷积核个数为32个,其对应的BN2操作的bias也有32个,而卷积核参数为3x3x3x32 =864 (含义分别是输入通道是3,因为图像是三通道的,3x3的卷积核大小,然后输出核个数是32个);

 

 而如下几个block类型在训练过程中是不会生成权重值的,所以不用从yolov4.weights中取值;

elif block['type'] == 'maxpool':
    pass
elif block['type'] == 'reorg':
    pass
elif block['type'] == 'upsample':
    pass
elif block['type'] == 'route':
    pass
elif block['type'] == 'shortcut':
    pass
elif block['type'] == 'region':
    pass
elif block['type'] == 'yolo':
    pass
elif block['type'] == 'avgpool':
    pass
elif block['type'] == 'softmax':
    pass
elif block['type'] == 'cost':
    pass

完成cfg文件的解析,模型的创建与权重文件的加载之后,现在要做的就是执行检测操作了,主要调用了utils/utils.py中的do_detect()函数,在demo.py中就是这行代码:boxes = do_detect(m, sized, 0.5, 0.4, use_cuda)

模型forward后输出结果存在list_boxes中,因为有3个yolo输出层,所以这个列表list_boxes中又分为3个子列表;

其中list_boxes[0]中存放的是第一个yolo层输出,其特征图大小对于原图缩放尺寸为8,即strides[0], 对于608x608图像来说,该层的featuremap尺寸为608/8=76;则该层的yolo输出数据维度为[batch, (classnum+4+1)*num_anchors, feature_h, feature_w] , 对于80类的coco来说,测试图像为1,每个yolo层每个特征图像点有3个锚点,该yolo层输出是[1,255,76,76];对应锚点大小为[1.5,2.0,2.375,4.5,5.0,3.5]; (这6个数分别是3个锚点的w和h,按照w1,h1,w2,h2,w3,h3排列);

同理第二个yolo层检测结果维度为[1,255,38,38],对应锚点大小为:[2.25,4.6875,4.75,3.4375,4.5,9.125],输出为 [1,255,38,38]

第三个yolo层检测维度为[1,255,19,19],对应锚点大小为:[4.4375,3.4375,6.0,7.59375,14.34375,12.53125],输出为 [1,255,19,19];


do_detect()函数中主要是调用了get_region_boxes1(output, conf_thresh, num_classes, anchors, num_anchors, only_objectness=1, validation=False) 这个函数对forward后的output做解析并做nms操作;

每个yolo层输出数据分析,对于第一个yolo层,输出维度为[1,85*3,76,76 ]; 会将其reshape为[85, 1*3*76*76],即有1*3*76*76个锚点在预测,每个锚点预测信息有80个类别的概率和4个位置信息和1个是否包含目标的置信度;下图是第一个yolo输出层的数据(实际绘制网格数量不正确,此处只是做说明用

 每个输出的对应代码实现为:

继续结合上面的图,分析对于某一个yolo层输出的数据是怎么排列的,其示意图如下:

 

 如果置信度满足阈值要求,则将预测的box保存到列表(其中id是所有output的索引,其值在0~batch*anchor_num*h*w范围内)

    def do_detect(model, img, conf_thresh, nms_thresh, use_cuda=1):
    model.eval() #模型做推理
    t0 = time.time()
    if isinstance(img, Image.Image):
    width = img.width
    height = img.height
    img = torch.ByteTensor(torch.ByteStorage.from_buffer(img.tobytes()))
    img = img.view(height, width, 3).transpose(0, 1).transpose(0, 2).contiguous() # CxHxW
    img = img.view(1, 3, height, width) # 对图像维度做变换,BxCxHxW
    img = img.float().div(255.0) # [0-255] --> [0-1]
    elif type(img) == np.ndarray and len(img.shape) == 3: # cv2 image
    img = torch.from_numpy(img.transpose(2, 0, 1)).float().div(255.0).unsqueeze(0)
    elif type(img) == np.ndarray and len(img.shape) == 4:
    img = torch.from_numpy(img.transpose(0, 3, 1, 2)).float().div(255.0)
    else:
    print("unknow image type")
    exit(-1)
    if use_cuda:
    img = img.cuda()
    img = torch.autograd.Variable(img)
    list_boxes = model(img) # 主要是调用了模型的forward操作,返回三个yolo层的输出
    anchors = [12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401]
    num_anchors = 9 # 3个yolo层共9种锚点
    anchor_masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
    strides = [8, 16, 32] # 每个yolo层相对输入图像尺寸的减少倍数分别为8,16,32
    anchor_step = len(anchors) // num_anchors
    boxes = []
    for i in range(3):
    masked_anchors = []
    for m in anchor_masks[i]:
    masked_anchors += anchors[m * anchor_step:(m + 1) * anchor_step]
    masked_anchors = [anchor / strides[i] for anchor in masked_anchors]
    boxes.append(get_region_boxes1(list_boxes[i].data.numpy(), 0.6, 80, masked_anchors, len(anchor_masks[i])))
    # boxes.append(get_region_boxes(list_boxes[i], 0.6, 80, masked_anchors, len(anchor_masks[i])))
    if img.shape[0] > 1:
    bboxs_for_imgs = [
    boxes[0][index] + boxes[1][index] + boxes[2][index]
    for index in range(img.shape[0])]
    # 分别对每一张图像做nms
    boxes = [nms(bboxs, nms_thresh) for bboxs in bboxs_for_imgs]
    else:
    boxes = boxes[0][0] + boxes[1][0] + boxes[2][0]
    boxes = nms(boxes, nms_thresh)
    return boxes # 返回nms后的boxes

对于3个yolo层先是简单的对每个yolo层输出中是否含有目标做了过滤(含有目标的概率大于阈值);然后就是对三个过滤后的框合并到一个list中作NMS操作了;涉及的代码如下:

    def nms(boxes, nms_thresh):
    if len(boxes) == 0:
    return boxes
    det_confs = torch.zeros(len(boxes))
    for i in range(len(boxes)):
    det_confs[i] = 1 - boxes[i][4]
    _, sortIds = torch.sort(det_confs) # sort是按照从小到大排序,那么sortlds中是按照有目标的概率由大到小排序
    out_boxes = []
    for i in range(len(boxes)):
    box_i = boxes[sortIds[i]]
    if box_i[4] > 0:
    out_boxes.append(box_i) # 取出有目标的概率最大的box放入out_boxes中;
    for j in range(i + 1, len(boxes)): #然后将剩下的box_j都和这个box_i进行IOU计算,若与box_i重叠率大于阈值,则将box_j的包含目标概率值置为0(即不选它)
    box_j = boxes[sortIds[j]]
    if bbox_iou(box_i, box_j, x1y1x2y2=False) > nms_thresh:
    # print(box_i, box_j, bbox_iou(box_i, box_j, x1y1x2y2=False))
    box_j[4] = 0
    return out_boxes

补充:

论文中提到的mish激活函数

公式是这样的(其中x是输入)

对应的图是:

##Pytorch中的代码实现为:

class Mish(torch.nn.Module):

def __init__(self):

super().__init__()

def forward(self, x):

x = x * (torch.tanh(torch.nn.functional.softplus(x)))

return x

#--------------------------------------------------------------#

Tensorflow的代码实现为:

import tensorflow as tf

from tensorflow.keras.layers import Activation

from tensorflow.keras.utils import get_custom_objects

class Mish(Activation):

def __init__(self, activation, **kwargs):

super(Mish, self).__init__(activation, **kwargs)

self.__name__ = 'Mish'

def mish(inputs):

return inputs * tf.math.tanh(tf.math.softplus(inputs))

get_custom_objects().update({'Mish': Mish(mish)})

#使用方法

x = Activation('Mish')(x)

文中提到的SPP结构大致是:

Pytorch指定运行的GPUID号的方法,https://www.cnblogs.com/jfdwd/p/11434332.html

训练自己的权重

https://blog.csdn.net/Msjiangmei/article/details/107919386

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白 AI 日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值