深度篇——目标检测史(七) 细说 YOLO-V3目标检测 之 代码详解

这篇博客详细解析了YOLO-V3目标检测的代码实现,包括代码结构、数据准备、网络构建、模型训练、模型冻结及测试过程。通过README.md、config.py配置文件,以及darknet53 backbone、yolov3网络模型等关键部分的介绍,帮助读者理解YOLO-V3的工作原理。
摘要由CSDN通过智能技术生成

返回主目录

返回 目标检测史 目录

上一章:深度篇——目标检测史(六) 细说 YOLO-V3目标检测

下一章:深度篇——目标检测史(八) 细说 CornerNet-Lite 目标检测

 

论文地址:《YOLO-V3》

代码地址:tf_yolov3_pro

 

本小节,细说 YOLO-V3目标检测 之 代码详解,下一小节细说 CornerNet-Lite 目标检测

 

八. YOLO-V3 目标检测 之 代码详解

之所以将 yolo-v3 的代码详解放在这边,是因为,如果放在上一小节的话,篇幅就很长了,读者估计阅读很不方便,而且容易疲劳,而且,还可以暴走骂人了。这代码,和前面的理论,是紧密结合的。下面的代码,我做了很多注释,对于相关理论,对代码的实现 的相关讲解。

 

1. 代码结构图形如下:

具体情况,可以参考 README.md

# [tf_yolov3_pro](https://github.com/wandaoyi/tf_yolov3_pro)
tensorflow 版本的 yolov3 目标检测项目 2020-03-18

- [论文地址](https://pjreddie.com/media/files/papers/YOLOv3.pdf)
- [我的 CSDN 博客](https://blog.csdn.net/qq_38299170) 
- 环境依赖(其实版本要求并不严格,你的版本要是能跑起来,那也是OK的):
```bashrc
pip install easydict
pip install numpy==1.16
conda install tensorflow-gpu==1.13.1
pip install tqdm
pip install opencv-python
```
- 对应数据来说,用户可以用自己的数据来跑,当然,也可以到网上去下载开源数据
- 下面是开源数据的链接:
```bashrc
$ wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
$ wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
$ wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
```
- 将数据放到指定的文件目录下(config.py 文件):
```bashrc
# 图像路径
__C.COMMON.IMAGE_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "data/images")
# xml 路径
__C.COMMON.ANNOTATION_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "data/annotations")
```
- 其实,做好依赖,拿到数据,就仔细看看 config.py 文件,里面全是配置。配好路径或一些超参,基本上,后面就是一键运行就 OK 了。
- 对 config.py 进行配置设置。

## 数据生成
训练模型之前,我们需要做数据,即是将数据做成我们网络所需要的样子
- 按上面的提示,将原始数据放到指定路径下,或将 config.py 里面的路径指向你的原始数据路径。还有,就是就是目标数据路径。然后,一键运行 prepare.py 文件,就可以生成 .txt 的目标数据。

## 训练模型
- 上面 config.py 调好了,而且数据也已经生成了,那,是驴是马,跑起来再说。还是一键运行 yolo_train.py。
- 在训练的过程中,最好 batch_size 不要太小,不然,loss 不好收敛。比方说,你 batch_size = 1 和 batch_size = 8 效果是不一样的。
- 在训练中,可以根据 loss 和 日志 进行人为的选择模型。

## 模型冻结
- 将上面训练得到的 .ckpt 模型文件,冻结成 .pb 文件。一键运行 model_freeze.py 文件
- 冻结模型,会对模型一定程度上的压缩,而且精度几乎不损。

## 图像预测
- 一键运行 yolo_test.py 文件(可以运行 prepare.py 来生成自己想要的数据,当然,前提是配置 config.py 文件)

## 视频预测
- 一键运行 yolo_video.py 文件

## 本项目的优点
- 就是方便,很多东西,我已经做成傻瓜式一键操作的方式。里面的路径,如果不喜欢用相对路径的,可以在 config.py 里面选择 绝对路径
- 本人和唠叨,里面的代码,基本都做了注解,就怕有人不理解,不懂,我只是希望能给予不同的你,一点点帮助。

## 本项目的缺点
- 没做 mAP
- 没做多线程(这些,以后有机会,会在博客中再详解)

看完 README.md 就看 config.py 配置文件,做好配置之后,后面,基本上都是一键操作

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# ============================================
# @Time     : 2020/03/07 14:09
# @Author   : WanDaoYi
# @FileName : config.py
# ============================================

import os
from easydict import EasyDict as edict


__C = edict()
# Consumers can get config by: from config import cfg
cfg = __C

# common options 公共配置文件
__C.COMMON = edict()
# windows 获取文件绝对路径, 方便 windows 在黑窗口 运行项目
__C.COMMON.BASE_PATH = os.path.abspath(os.path.dirname(__file__))
# # 获取当前窗口的路径, 当用 Linux 的时候切用这个,不然会报错。(windows也可以用这个)
# __C.COMMON.BASE_PATH = os.getcwd()

# 相对路径 当前路径
__C.COMMON.RELATIVE_PATH = "./"

# class 文件路径
__C.COMMON.CLASS_FILE_PATH = os.path.join(__C.COMMON.BASE_PATH, "infos/classes/voc_class.txt")
# anchor 文件路径
__C.COMMON.ANCHOR_FILE_PATH = os.path.join(__C.COMMON.BASE_PATH, "infos/anchors/coco_anchors.txt")

# iou 损失的 阈值
__C.COMMON.IOU_LOSS_THRESH = 0.5

# 超参
__C.COMMON.ALPHA = 1.0
__C.COMMON.GAMMA = 2.0

# 每个尺度最多允许有 几个 bounding boxes
__C.COMMON.MAX_BBOX_PER_SCALE = 150
# 衰减率的 移动平均值,用来控制模型的更新速度
# decay设置为接近1的值比较合理,
# 通常为:0.999,0.9999等,decay越大模型越稳定,
# 因为decay越大,参数更新的速度就越慢,趋于稳定
__C.COMMON.MOVING_AVE_DECAY = 0.9995

# 图像路径
__C.COMMON.IMAGE_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "data/images")
# xml 路径
__C.COMMON.ANNOTATION_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "data/annotations")

# 数据划分比例
__C.COMMON.TRAIN_PERCENT = 0.7
__C.COMMON.VAL_PERCENT = 0.2
__C.COMMON.TEST_PERCENT = 0.1

# 图像后缀名
__C.COMMON.IMAGE_EXTENSION = ".jpg"

# YOLO options
__C.YOLO = edict()
# YOLOV3 的 3 个尺度
__C.YOLO.STRIDES = [8, 16, 32]
# YOLOV3 上采样的方法
__C.YOLO.UP_SAMPLE_METHOD = "resize"
# YOLOV3 每个尺度包含 3 个 anchors
__C.YOLO.ANCHOR_PER_SCALE = 3

# Train options
__C.TRAIN = edict()
# 训练集数据
__C.TRAIN.TRAIN_DATA_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "infos/dataset/voc_train.txt")
__C.TRAIN.VAL_DATA_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "infos/dataset/voc_val.txt")
# 训练集 input size
__C.TRAIN.INPUT_SIZE_LIST = [320, 352, 384, 416, 448, 480, 512, 544, 576, 608]
__C.TRAIN.TRAIN_BATCH_SIZE = 1
__C.TRAIN.VAL_BATCH_SIZE = 2
# 学习率的范围
__C.TRAIN.LEARNING_RATE_INIT = 1e-3
__C.TRAIN.LEARNING_RATE_END = 1e-6
# 第一阶段的训练 epoch
__C.TRAIN.FIRST_STAGE_EPOCHS = 16
# 第二阶段的训练 epoch 用于表述,如果是预训练的话,第一阶段训练会冻结参数
__C.TRAIN.SECOND_STAGE_EPOCHS = 32
# 预热训练,即在预热之前,learning_rate 学习率简单的 人为缩小,即 前面 [: 2] 个 epochs
# 预热之后,则 learning_rate 随着训练次数  人为在缩小,
# 即 [2: FIRST_STAGE_EPOCHS + SECOND_STAGE_EPOCHS] 个 epochs
__C.TRAIN.WARM_UP_EPOCHS = 2

# 初始化模型
__C.TRAIN.INITIAL_WEIGHT = os.path.join(__C.COMMON.RELATIVE_PATH, "checkpoint/val_loss=4.4647.ckpt-5")
# 训练日志
__C.TRAIN.TRAIN_LOG = os.path.join(__C.COMMON.RELATIVE_PATH, "log/train_log")
# 验证日志
__C.TRAIN.VAL_LOG = os.path.join(__C.COMMON.RELATIVE_PATH, "log/val_log")

# FREEZE MODEL
__C.FREEZE = edict()
# ckpt 模型文件夹
__C.FREEZE.CKPT_MODEL_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "checkpoint/val_loss=4.4647.ckpt-5")
# pb 模型文件夹
__C.FREEZE.PB_MODEL_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "model_info/val_loss=4.4647.pb")
# YOLOV3 节点输出
__C.FREEZE.YOLO_OUTPUT_NODE_NAME = ["input/input_data",
                                    "pred_sbbox/concat_2",
                                    "pred_mbbox/concat_2",
                                    "pred_lbbox/concat_2"
                                    ]


# TEST options
__C.TEST = edict()
# 测试数据集
__C.TEST.TEST_DATA_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "infos/dataset/voc_test.txt")
# 测试 .pb 模型 文件路径 yolov3_model
__C.TEST.TEST_PB_MODEL_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "model_info/val_loss=4.4647.pb")
# test 输入尺度
__C.TEST.INPUT_SIZE = 544
# 输出 图像 文件夹
__C.TEST.OUTPUT_IMAGE_FILE = os.path.join(__C.COMMON.RELATIVE_PATH, "output/test_image")
# 输出 预测框信息 文件夹
__C.TEST.OUTPUT_BOX_INFO_FILE = os.path.join(__C.COMMON.RELATIVE_PATH, "output/test_box_info")
# 是否对预测打框后的图像进行保存,默认保存 True
__C.TEST.SAVE_BOXES_IMAGE_FLAG = True

__C.TEST.RETURN_ELEMENTS = ["input/input_data:0",
                            "pred_sbbox/concat_2:0",
                            "pred_mbbox/concat_2:0",
                            "pred_lbbox/concat_2:0"
                            ]

__C.TEST.VEDIO_PATH = os.path.join(__C.COMMON.RELATIVE_PATH, "data/video/test_video.mp4")

 

2. 准备训练、验证、测试数据

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# ============================================
# @Time     : 2020/03/15 02:59
# @Author   : WanDaoYi
# @FileName : prepare.py
# ============================================

import os
import random
from datetime import datetime
import xml.etree.ElementTree as ET
import core.utils as utils
from config import cfg


class Prepare(object):

    def __init__(self):
        # 图像路径
        self.image_path = cfg.COMMON.IMAGE_PATH
        # 图像的后缀名
        self.image_extension = cfg.COMMON.IMAGE_EXTENSION
        # xml 路径
        self.annotation_path = cfg.COMMON.ANNOTATION_PATH
        # 获取 c 类 字典型
        self.classes_dir = utils.read_class_names(cfg.COMMON.CLASS_FILE_PATH)
        self.classes_len = len(self.classes_dir)
        # 获取 c 类 list 型
        self.classes_list = [self.classes_dir[key] for key in range(self.classes_len)]

        # 数据的百分比
        self.test_percent = cfg.COMMON.TEST_PERCENT
        self.val_percent = cfg.COMMON.VAL_PERCENT

        # 各成分数据保存路径
        self.train_data_path = cfg.TRAIN.TRAIN_DATA_PATH
        self.val_data_path = cfg.TRAIN.VAL_DATA_PATH
        self.test_data_path = cfg.TEST.TEST_DATA_PATH

        pass

    def do_prepare(self):

        xml_file_list = os.listdir(self.annotation_path)
        xml_len = len(xml_file_list)
        # 根据百分比得到各成分 数据量
        n_test = int(xml_len * self.test_percent)
        n_val = int(xml_len * self.val_percent)
        n_train = xml_len - n_test - n_val

        if os.path.exists(self.train_data_path):
            os.remove(self.train_data_path)
            pass

        if os.path.exists(self.val_data_path):
            os.remove(self.val_data_path)
            pass

        if os.path.exists(self.test_data_path):
            os.remove(self.test_data_path)
            pass

        # 随机划分数据
        n_train_val = n_train + n_val
        train_val_list = random.sample(xml_file_list, n_train_val)
        train_list = random.sample(train_val_list, n_train)

        train_file = open(self.train_data_path, "w")
        val_file = open(self.val_data_path, "w")
        test_file = open(self.test_data_path, "w")

        for xml_name in xml_file_list:
            # 名字信息
            name_info = xml_name[: -4]
            # 图像名
            image_name = name_info + self.image_extension

            # 如果文件名在 训练 和 验证 文件划分中
            if xml_name in train_val_list:
                # 如果文件名在 训练数据划分中
                if xml_name in train_list:
                    self.convert_annotation(xml_name, image_name, train_file)
                    train_file.write('\n')
                    pass
                # 否则文件在 验证 文件
                else:
                    self.convert_annotation(xml_name, image_name, val_file)
                    val_file.write('\n')
                    pass
                pass
            # 否则文件名在 测试 文件
            else:
                self.convert_annotation(xml_name, image_name, test_file)
                test_file.write('\n')
                pass

        pass

    def convert_annotation(self, xml_name, image_name, file):
        xml_path = os.path.join(self.annotation_path, xml_name)
        image_path = os.path.join(self.image_path, image_name)
        file.write(image_path)

        # 打开 xml 文件
        xml_file = open(xml_path)
        # 将 xml 文件 转为树状结构
        tree = ET.parse(xml_file)
        root = tree.getroot()
        for obj in root.iter("object"):
            diff = obj.find("difficult").text
            cls = obj.find("name").text
            if cls not in self.classes_list or int(diff) == 1:
                continue

            cls_id = self.classes_list.index(cls)
            xml_box = obj.find("bndbox")
            b = (int(xml_box.find('xmin').text), int(xml_box.find('ymin').text),
                 int(xml_box.find('xmax').text), int(xml_box.find('ymax').text))
            file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id))
            pass
        pass


if __name__ == "__main__":
    # 代码开始时间
    start_time = datetime.now()
    print("开始时间: {}".format(start_time))

    demo = Prepare()
    demo.do_prepare()

    # 代码结束时间
    end_time = datetime.now()
    print("结束时间: {}, 训练模型耗时: {}".format(end_time, end_time - start_time))
    pass

 

3. 构建 yolo-v3 网络

   (1). 首先,当然是网络模块工具咯 common.py

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# ============================================
# @Time     : 2020/03/07 14:09
# @Author   : WanDaoYi
# @FileName : common.py
# ============================================


import tensorflow as tf


# 卷积
def convolutional(input_data, filters_shape, training_flag, name, down_sample=False, activate=True, bn=True):
    """
    :param input_data: 输入信息
    :param filters_shape: 卷积核的形状,如 (3, 3, 32, 64) 表示:3 x 3 大小的卷积核,输入32维,输出64维
    :param training_flag: 是否是在训练模式下返回输出
    :param name: 卷积的名称
    :param down_sample: 是否下采样,默认不下采样
    :param activate: 是否使用 ReLU 激活函数
    :param bn: 是否进行 BN 处理
    :return:
    """
    with tf.variable_scope(name):
        # 下采样
        if down_sample:
            pad_h, pad_w = (filters_shape[0] - 2) // 2 + 1, (filters_shape[1] - 2) // 2 + 1
            paddings = tf.constant([[0, 0], [pad_h, pad_h], [pad_w, pad_w], [0, 0]])
            input_data = tf.pad(input_data, paddings, 'CONSTANT')
            strides = (1, 2, 2, 1)
            padding = 'VALID'
        # 不下采样
        else:
            strides = (1, 1, 1, 1)
            padding = "SAME"

        weight = tf.get_variable(name='weight', dtype=tf.float32, trainable=True,
                                 shape=filters_shape, initializer=tf.random_normal_initializer(stddev=0.01))
        # 卷积操作
        conv = tf.nn.conv2d(input=input_data, filter=weight, strides=strides, padding=padding)
        # BN 处理
        if bn:
            conv = tf.layers.batch_normalization(conv,
                                                 beta_initializer=tf.zeros_initializer(),
                                                 gamma_initializer=tf.ones_initializer(),
                                                 moving_mean_initializer=tf.zeros_initializer(),
                                                 moving_variance_initializer=tf.ones_initializer(),
                                                 training=training_flag)
        # 添加 bias
        else:
            bias = tf.get_variable(name='bias', shape=filters_shape[-1], trainable=True,
                                   dtype=tf.float32, initializer=tf.constant_initializer(0.0))
            conv = tf.nn.bias_add(conv, bias)

        # 激活函数处理
        if activate:
            conv = tf.nn.leaky_relu(conv, alpha=0.1)

    return conv


# 残差模块
def residual_block(input_data, input_channel, filter_num1, filter_num2, training_flag, name):
    """
    :param input_data: 输入的 feature maps
    :param input_channel: 输入的 通道
    :param filter_num1: 卷积核数
    :param filter_num2: 卷积核数
    :param training_flag: 是否是在训练模式下返回输出
    :param name:
    :return:
    """

    # 用来做短路连接的 feature maps
    short_cut = input_data

    with tf.variable_scope(name):
        input_data = convolutional(input_data, filters_shape=(1, 1, input_channel, filter_num1),
                                   training_flag=training_flag, name='conv1')
        input_data = convolutional(input_data, filters_shape=(3, 3, filter_num1, filter_num2),
                                   training_flag=training_flag, name='conv2')
        # 残差值和短路值相加,得到残差模块
        residual_output = input_data + short_cut

    return residual_output


# concat 操作
def route(name, previous_output, current_output):
    with tf.variable_scope(name):
        concat_output = tf.concat([current_output, previous_output], axis=-1)

    return concat_output


# 上采样
def up_sample(input_data, name, method="deconv"):
    assert method in ["resize", "deconv"]

    if method == "resize":
        with tf.variable_scope(name):
            input_shape = tf.shape(input_data)
            up_sample_output = tf.image.resize_nearest_neighbor(input_data, (input_shape[1] * 2, input_shape[2] * 2))
        pass
    else:
        # 输入 filter 的数量
        filter_num = input_data.shape.as_list()[-1]
        up_sample_output = tf.layers.conv2d_transpose(input_data, filter_num, kernel_size=2,
                                                      padding='same', strides=(2, 2),
                                                      kernel_initializer=tf.random_normal_initializer())
        pass

    return up_sample_output

   (2). 其次,还是一些工具函数。毕竟,工欲善其事必先利其器 嘛 utils.py 精彩出场

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# =======================================
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值