上一章:深度篇——目标检测史(六) 细说 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 _*_
# =======================================