这里给一个labelimg软件的传送门https://pan.baidu.com/s/1tuIQmuyedRHP1WeGVVSx_Q 提取码: ejgx 。
使用的工具是LabelImg,由于YOLOV3的label标注的一行五个数分别代表类别:编号,BoundingBox 中心 X 坐标,中心 Y 坐标,宽,高。这些坐标都是 0~1 的相对坐标。由于该标注工具默认生成.xml文件属性,这时我们可以通过LabelImg修改生成文件属性。点击如下红框可直接生成yolov3算法需求的文件(.txt)
import os
def rename(file_path):
"""
遍历文件并且进行重命名
:param file_path:文件的路径(绝对)
:return:
"""
filelist = os.listdir(file_path)
total_num = len(filelist)
i = 1
for item in filelist:
# if item.endswith('.jpg'):
if item.endswith('.txt'):
src = os.path.join(os.path.abspath(file_path), item)
str1 = str(i)
# dst = os.path.join(os.path.abspath(file_path), str1.zfill(6) + '.jpg') # str1.zfill(x),x为一共几位数,用0补齐,如001000
dst = os.path.join(os.path.abspath(file_path), str1.zfill(6) + '.txt') # str1.zfill(x),x为一共几位数,用0补齐,如001000
try:
os.rename(src, dst)
print('converting %s to %s ...' % (src, dst))
i = i + 1
except:
continue
# file_path = 'D:\\ImageLabel\\TargetData\\output\\' # .jpg图片的存储路径
file_path = 'D:\\ImageLabel\\SaveTxt' # .txt图片的存储路径
rename(file_path)
为了规划自己的数据,减少出错的可能性,最好自己先给自己的图片编一个合理的序号,比如0001~0999。
利用软件把自己的数据标注好。每一个图片名对应的有一个相应名字的label.xml。
xml中的数据如下所示
在目录下新建VOC2007,并在VOC2007下新建Annotations,ImageSets和JPEGImages三个文件夹。在ImageSets下新建Main文件夹。文件目录如下所示:
将自己的数据集图片拷贝到JPEGImages目录下。将数据集label文件拷贝到Annotations目录下。在VOC2007下新建test.py文件夹,将下面代码拷贝进去运行,准备训练、验证、测试数据,生成四个文件:train.txt,val.txt,test.txt和trainval.txt。
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
即使用 k-means 算法对训练集上的 boudnding box 尺度做聚类。此外,考虑到训练集上的图片尺寸不一,因此对此过程进行归一化处理。
k-means 聚类算法有个坑爹的地方在于,类别的个数需要事先指定。这就带来一个问题,先验框 anchor 的数目等于多少最合适?一般来说,anchor 的类别越多,那么 YOLO 算法就越能在不同尺度下与真实框进行回归,但是这样就导致模型的复杂度更高,网络的参数量更庞大。
在上面这张图里,作者发现 k=5 时就能较好的实现高召回率和模型复杂度之间的平衡。由于在 YOLOV3 算法里一种有 3 个尺度预测,因此只能是 3 的倍数,所以最终选择了 9 个先验框。这里还有个问题需要解决,k-means 度量距离的选取很关键。距离度量如果使用标准的欧氏距离,大框框就会比小框产生更多的错误。在目标检测领域,我们度量两个边界框之间的相似度往往以 IOU 大小作为标准。因此,这里的度量距离也和 IOU 有关。需要特别注意的是,这里的IOU计算只用到了 boudnding box 的长和宽。在作者的代码里,是认为两个先验框的左上角位置是相重合的。(其实在这里偏移至哪都无所谓,因为聚类的时候是不考虑 anchor 框的位置信息的。)
如果两个边界框之间的IOU值越大,那么它们之间的距离就会越小。
def kmeans(boxes, k, dist=np.median,seed=1):
"""
Calculates k-means clustering with the Intersection over Union (IoU) metric.
:param boxes: numpy array of shape (r, 2), where r is the number of rows
:param k: number of clusters
:param dist: distance function
:return: numpy array of shape (k, 2)
"""
rows = boxes.shape[0]
distances = np.empty((rows, k)) ## N row x N cluster
last_clusters = np.zeros((rows,))
np.random.seed(seed)
# initialize the cluster centers to be k items
clusters = boxes[np.random.choice(rows, k, replace=False)]
while True:
# 为每个点指定聚类的类别(如果这个点距离某类别最近,那么就指定它是这个类别)
for icluster in range(k): # I made change to lars76's code here to make the code faster
distances[:,icluster] = 1 - iou(clusters[icluster], boxes)
nearest_clusters = np.argmin(distances, axis=1)
# 如果聚类簇的中心位置基本不变了,那么迭代终止。
if (last_clusters == nearest_clusters).all():
break
# 重新计算每个聚类簇的平均中心位置,并它作为聚类中心点
for cluster in range(k):
clusters[cluster] = dist(boxes[nearest_clusters == cluster], axis=0)
last_clusters = nearest_clusters
return clusters,nearest_clusters,distances
YOLO V3算法的基本思想可以分成两部分:
1、按一定规则在图片上产生一系列的候选区域,然后根据这些候选区域与图片上物体真实框之间的位置关系对候选区域进行标注。跟真实框足够接近的那些候选区域会被标注为正样本,同时将真实框的位置作为正样本的位置目标。偏离真实框较大的那些候选区域则会被标注为负样本,负样本不需要预测位置或者类别。
2、使用卷积神经网络提取图片特征并对候选区域的位置和类别进行预测。这样每个预测框就可以看成是一个样本,根据真实框相对它的位置和类别进行了标注而获得标签值,通过网络模型预测其位置和类别,将网络预测值和标签值进行比较,就可以建立起损失函数。
1、如上图所示:在训练过程中对于每幅输入图像,yolov3会预测三个不同大小的3D tensor,对应着三个不同的scale,设计这三个scale的目的是为了能检测出不同大小的物体。
2、这里以13 * 13的tensor为例,对于这个scale,原始输入图像会被分割成13 × 13的grid cell,每个grid cell对应着3D tensor中的1x1x255这样一个长条形voxel。255是由3x(4+1+80)而来,由上图可知,公式NxNx[3x(4+1+80)]中NxN表示的是scale size例如上面提到的13x13。3表示的是each grid predict 3 boxes。4表示的是坐标值即(tx,ty,tw,th)。1表示的是置信度,80表示的是类别数目。
3、如果训练集中某一个ground truth对应的bounding box中心恰好落在了输入图像的摸一个grid cell中,那么这个grid cell就负责预测此物体的bounding box,于是这个grid cell所对应的置信度为1,其他grid cell的置信度为0.每个grid cell都会被赋予3个不同大小的prior box,学习过程中,这个grid cell会学会如何选择哪个大小的prior box。作者定义的是只选择与ground truth bounding box的IOU重合度最高的prior box。
4、上面说到的三个预设的不同大小的prior box,这三个大小是如何计算的,首先在训练前,将coco数据集中的所有bbox使用k-means clustering分成9个类别,每3个类别对应一个scale,故总共3个scale,这种关于box大小的先验信息帮助网络准确预测每个box的offset和coordinate。从直观上,大小合适的box会使得网络更精准的学习。
产生候选区域
如何产生候选区域,是检测模型的核心设计方案。目前大多数基于卷积神经网络的模型所采用的方式大体如下:
1、按一定的规则在图片上生成一系列位置固定的锚框,将这些锚框看作是可能的候选区域
2、对锚框是否包含目标物体进行预测,如果包含目标物体,还需要预测所包含物体的类别,以及预测框相对于锚框位置需要调整的幅度。
生成锚框
让我们思考下面一个例子,其中输入图像大小是 416×416,网络的步幅是 32。如之前所述,特征图的维度会是 13×13。随后,我们将输入图像分为 13×13 个网格。
生成预测框
下面的公式描述了网络输出是如何转换,以获得边界框预测结果的
其中,Cx,Cy是feature map中grid cell的左上角坐标,在yolov3中每个grid cell在feature map中的宽和高均为1。如下图1的情形时,这个bbox边界框的中心属于第二行第二列的grid cell,它的左上角坐标为(1,1),故Cx=1,Cy=1.公式中的Pw、Ph是预设的anchor box映射到feature map中的宽和高(anchor box原本设定是相对于416416坐标系下的坐标,在yolov3.cfg文件中写明了,代码中是把cfg中读取的坐标除以stride如32映射到feature map坐标系中)。
最终得到的边框坐标值是bx,by,bw,bh即边界框bbox相对于feature map的位置和大小,是我们需要的预测输出坐标。但我们网络实际上的学习目标是tx,ty,tw,th这4个offsets,其中tx,ty是预测的坐标偏移值,tw,th是尺度缩放,有了这4个offsets,自然可以根据之前的公式去求得真正需要的bx,by,bw,bh4个坐标。
那么4个坐标tx,ty,tw,th是怎么求出来的呢?对于训练样本,在大多数文章里需要用到ground truth的真实框来求这4个坐标:
上面这个公式是faster-rcnn系列文章用到的公式,Px,Py在faster-rcnn系列文章是预设的anchor box在feature map上的中心点坐标。 Pw、Ph是预设的anchor box的在feature map上的宽和高。至于Gx、Gy、Gw、Gh自然就是ground truth在这个feature map的4个坐标了(其实上面已经描述了这个过程,要根据原图坐标系先根据原图纵横比不变映射为416416坐标下的一个子区域如416312,取 min(w/img_w, h/img_h)这个比例来缩放成416312,再填充为416416,坐标变换上只需要让ground truth在416312下的y1,y2(即左上角和右下角纵坐标)加上图2灰色部分的一半,y1=y1+(416-416/768576)/2=y1+(416-312)/2,y2同样的操作,把x1,x2,y1,y2的坐标系的换算从针对实际红框的坐标系(416312)变为416*416下了,这样保证bbox不会扭曲,然后除以stride得到相对于feature map的坐标)。
但是在yolov3中与faster-rcnn系列文章用到的公式在前两行是不同的,yolov3里Px和Py就换为了feature map上的grid cell左上角坐标Cx,Cy了,即在yolov3里是Gx,Gy减去grid cell左上角坐标Cx,Cy。x,y坐标并没有针对anchon box求偏移量,所以并不需要除以Pw,Ph。
也就是说是tx = Gx - Cx
ty = Gy - Cy
这样就可以直接求bbox中心距离grid cell左上角的坐标的偏移量。
tw和th的公式yolov3和faster-rcnn系列是一样的,是物体所在边框的长宽和anchor box长宽之间的比率,不管Faster-RCNN还是YOLO,都不是直接回归bounding box的长宽而是尺度缩放到对数空间,是怕训练会带来不稳定的梯度。因为如果不做变换,直接预测相对形变tw和th,那么要求tw,th>0,因为你的框的宽高不可能是负数。这样,是在做一个有不等式条件约束的优化问题,没法直接用SGD来做。所以先取一个对数变换,将其不等式约束去掉,就可以了。
这里就有个重要的疑问了,一个尺度的feature map有三个anchors,那么对于某个ground truth框,究竟是哪个anchor负责匹配它呢?和YOLOv1一样,对于训练图片中的ground truth,若其中心点落在某个cell内,那么该cell内的3个anchor box负责预测它,具体是哪个anchor box预测它,需要在训练中确定,即由那个与ground truth的IOU最大的anchor box预测它,而剩余的2个anchor box不与该ground truth匹配。YOLOv3需要假定每个cell至多含有一个grounth truth,而在实际上基本不会出现多于1个的情况。与ground truth匹配的anchor box计算坐标误差、置信度误差(此时target为1)以及分类误差,而其它的anchor box只计算置信度误差(此时target为0)。
边框回归为何只能微调?当输入的 Proposal 与 Ground Truth 相差较小时,,即IOU很大时(RCNN 设置的是 IoU>0.6), 可以认为这种变换是一种线性变换, 那么我们就可以用线性回归(线性回归就是给定输入的特征向量 X, 学习一组参数 W, 使得经过线性回归后的值跟真实值 Y(Ground Truth)非常接近. 即Y≈WX )来建模对窗口进行微调, 否则会导致训练的回归模型不work(当 Proposal跟 GT 离得较远,就是复杂的非线性问题了,此时用线性回归建模显然就不合理了)
那么训练时用的ground truth的4个坐标去做差值和比值得到tx,ty,tw,th,测试时就用预测的bbox就好了,公式修改就简单了,把Gx和Gy改为预测的x,y,Gw、Gh改为预测的w,h即可。
1、中心坐标
注意:我们使用 sigmoid 函数进行中心坐标预测。这使得输出值在 0 和 1 之间。原因如下:
正常情况下,YOLO 不会预测边界框中心的确切坐标。它预测:
与预测目标的网格单元左上角相关的偏移;
使用特征图单元的维度(1)进行归一化的偏移。
以我们的图像为例。如果中心的预测是 (0.4, 0.7),则中心在 13 x 13 特征图上的坐标是 (6.4, 6.7)(红色单元的左上角坐标是 (6,6))。
但是,如果预测到的 x,y 坐标大于 1,比如 (1.2, 0.7)。那么中心坐标是 (7.2, 6.7)。注意该中心在红色单元右侧的单元中,或第 7 行的第 8 个单元。这打破了 YOLO 背后的理论,因为如果我们假设红色框负责预测目标狗,那么狗的中心必须在红色单元中,不应该在它旁边的网格单元中。
因此,为了解决这个问题,我们对输出执行 sigmoid 函数,将输出压缩到区间 0 到 1 之间,有效确保中心处于执行预测的网格单元中。
边界框的维度
2、边界框的维度
我们对输出执行对数空间变换,然后乘锚点,来预测边界框的维度。
得出的预测 bw 和 bh 使用图像的高和宽进行归一化。即,如果包含目标(狗)的框的预测 bx 和 by 是 (0.3, 0.8),那么 13 x 13 特征图的实际宽和高是 (13 x 0.3, 13 x 0.8)。
3、锚框优劣的评定
如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。我们知道,Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合 A 和 B,它们的Jaccard系数即二者交集大小除以二者并集大小:
实际上,我们可以把边界框内的像素区域看成是像素的集合。可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度。
通常将Jaccard系数称为交并比(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如图所示。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。
这里使用 GIoU 来衡量检测框和 GT bbox 之间的差距
4、怎么样训练锚框
在训练集中,我们将每个锚框视为一个训练样本。需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。
大致思路:在目标检测时,
Objectness 分数
Object 分数表示目标在边界框内的概率。红色网格和相邻网格的 Object 分数应该接近 1,而角落处的网格的 Object 分数可能接近 0。
objectness 分数的计算也使用 sigmoid 函数,因此它可以被理解为概率
类别置信度
类别置信度表示检测到的对象属于某个类别的概率(如狗、猫、香蕉、汽车等)。在 v3 之前,YOLO 需要对类别分数执行 softmax 函数操作。
但是,YOLO v3 舍弃了这种设计,作者选择使用 sigmoid 函数。因为对类别分数执行 softmax 操作的前提是类别是互斥的。简言之,如果对象属于一个类别,那么必须确保其不属于另一个类别。这在我们设置检测器的 COCO 数据集上是正确的。但是,当出现类别「女性」(Women)和「人」(Person)时,该假设不可行。这就是作者选择不使用 Softmax 激活函数的原因。
置信度是每个bounding box输出的其中一个重要参数,作者对他的作用定义有两重:一重是代表当前box是否有对象的概率,注意,是对象,不是某个类别的对象,也就是说它用来说明当前box内只是个背景(backgroud)还是有某个物体(对象);另一重表示当前的box有对象时,它自己预测的box与物体真实的box可能的的值,注意,这里所说的物体真实的box实际是不存在的,这只是模型表达自己框出了物体的自信程度。以上所述,也就不难理解作者为什么将其称之为置信度了,因为不管哪重含义,都表示一种自信程度:框出的box内确实有物体的自信程度和框出的box将整个物体的所有特征都包括进来的自信程度。经过以上的解释,其实我们也就可以用数学形式表示置信度的定义了
在不同尺度上的预测
YOLO v3 在 3 个不同尺度上进行预测。检测层用于在三个不同大小的特征图上执行预测,特征图步幅分别是 32、16、8。这意味着,当输入图像大小是 416 x 416 时,我们在尺度 13 x 13、26 x 26 和 52 x 52 上执行检测。
该网络在第一个检测层之前对输入图像执行下采样,检测层使用步幅为 32 的层的特征图执行检测。随后在执行因子为 2 的上采样后,并与前一个层的特征图(特征图大小相同)拼接。另一个检测在步幅为 16 的层中执行。重复同样的上采样步骤,最后一个检测在步幅为 8 的层中执行。
在每个尺度上,每个单元使用 3 个锚点预测 3 个边界框,锚点的总数为 9(不同尺度的锚点不同)。
作者称这帮助 YOLO v3 在检测较小目标时取得更好的性能,而这正是 YOLO 之前版本经常被抱怨的地方。上采样可以帮助该网络学习细粒度特征,帮助检测较小目标。
1、小目标检测
2、中等尺度目标检测
输出处理
对于大小为 416 x 416 的图像,YOLO 预测 ((52 x 52) + (26 x 26) + 13 x 13)) x 3 = 10647 个边界框。但是,我们的示例中只有一个对象——一只狗。那么我们怎么才能将检测次数从 10647 减少到 1 呢?
目标置信度阈值:首先,我们根据它们的 objectness 分数过滤边界框。通常,分数低于阈值的边界框会被忽略。
非极大值抑制:非极大值抑制(NMS)可解决对同一个图像的多次检测的问题。例如,红色网格单元的 3 个边界框可以检测一个框,或者临近网格可检测相同对象
通俗的说:每个框都有一个所属类别的概率值,根据概率值先确定一个框的类别,然后把与这个框相交、且概率值大于阈值的框全都扔掉。最后剩下的框就是没有重复的预测边界框了
(1)其中的build_network()函数,建立网络模型
import numpy as np
import tensorflow as tf
import core.utils as utils
import core.common as common
import core.backbone as backbone
from config import cfg
class YoloV3(object):
def __init__(self, input_data, training_flag):
# 超参
self.alpha = cfg.COMMON.ALPHA
self.gamma = cfg.COMMON.GAMMA
# 是否是在训练模式下返回输出 True 为返回
self.training_flag = training_flag
# 获取 classes
self.classes = utils.read_class_names(cfg.COMMON.CLASS_FILE_PATH)
# 获取 classes 的种类数
self.num_class = len(self.classes)
# 获取 YOLOV3 的 3 个尺度
self.strides = np.array(cfg.YOLO.STRIDES)
# 获取 anchors
self.anchors = utils.get_anchors(cfg.COMMON.ANCHOR_FILE_PATH)
# 每个尺度有 3 个 anchors
self.anchor_per_scale = cfg.YOLO.ANCHOR_PER_SCALE
# iou 损失的 阈值
self.iou_loss_thresh = cfg.COMMON.IOU_LOSS_THRESH
# 上采样的方式
self.up_sample_method = cfg.YOLO.UP_SAMPLE_METHOD
try:
# 获取 yolov3 网络 小、中、大 三个尺度的 feature maps
self.conv_lbbox, self.conv_mbbox, self.conv_sbbox = self.structure_network(input_data)
pass
except:
raise NotImplementedError("Can not structure yolov3 network!")
pass
# anchors list [[116., 90.], [156., 198.], [373., 326.]],
# 小尺度的 feature maps 使用 大尺度的 anchors 值,用于检测大尺度目标
with tf.variable_scope('pred_lbbox'):
self.pred_lbbox = self.pred_conv_bbox(self.conv_lbbox, self.anchors[2], self.strides[2])
pass
# anchors list [[30., 61.], [62., 45.], [59., 119.]], 用于检测中等尺度目标
with tf.variable_scope('pred_mbbox'):
self.pred_mbbox = self.pred_conv_bbox(self.conv_mbbox, self.anchors[1], self.strides[1])
pass
# anchors list [[10., 13.], [16., 30.], [33., 23.]],
# 大尺度的 feature maps 使用 相对小点尺度的 anchors 值,用于检测小尺度目标
with tf.variable_scope('pred_sbbox'):
self.pred_sbbox = self.pred_conv_bbox(self.conv_sbbox, self.anchors[0], self.strides[0])
pass
pass
# 构建网络
def structure_network(self, input_data):
# 调取 darknet53 网络,获取 3 个尺度的返回值
route_1, route_2, input_data = backbone.darknet53(input_data, self.training_flag)
# conv set 操作 conv: 1 x 1 -> 3 x 3 -> 1 x 1 -> 3 x 3 -> 1 x 1
input_data = common.convolutional(input_data, (1, 1, 1024, 512), self.training_flag, 'conv52')
input_data = common.convolutional(input_data, (3, 3, 512, 1024), self.training_flag, 'conv53')
input_data = common.convolutional(input_data, (1, 1, 1024, 512), self.training_flag, 'conv54')
input_data = common.convolutional(input_data, (3, 3, 512, 1024), self.training_flag, 'conv55')
input_data = common.convolutional(input_data, (1, 1, 1024, 512), self.training_flag, 'conv56')
# scale one 小尺度的 feature maps: conv set -> conv 3 x 3 -> 1 x 1
conv_lobj_branch = common.convolutional(input_data, (3, 3, 512, 1024),
self.training_flag, name='conv_lobj_branch')
# 3 * (self.num_class + 5) 表示为: 每个尺度有 3 个 anchors,每个 anchor 有 5 + num_class 个值
# 5 = 4 + 1, 4个 坐标值,1 个置信度,num_class 表示分类的数量
conv_lbbox = common.convolutional(conv_lobj_branch, (1, 1, 1024, 3 * (self.num_class + 5)),
training_flag=self.training_flag, name='conv_lbbox',
activate=False, bn=False)
# 进入另一个分支点: conv set -> 1 x 1 -> up_sampling -> concatenate
input_data = common.convolutional(input_data, (1, 1, 512, 256), self.training_flag, 'conv57')
input_data = common.up_sample(input_data, name='up_sample0', method=self.up_sample_method)
with tf.variable_scope('route_1'):
input_data = tf.concat([input_data, route_2], axis=-1)
# conv set 操作 conv: 1 x 1 -> 3 x 3 -> 1 x 1 -> 3 x 3 -> 1 x 1
input_data = common.convolutional(input_data, (1, 1, 768, 256), self.training_flag, 'conv58')
input_data = common.convolutional(input_data, (3, 3, 256, 512), self.training_flag, 'conv59')
input_data = common.convolutional(input_data, (1, 1, 512, 256), self.training_flag, 'conv60')
input_data = common.convolutional(input_data, (3, 3, 256, 512), self.training_flag, 'conv61')
input_data = common.convolutional(input_data, (1, 1, 512, 256), self.training_flag, 'conv62')
# scale two 中尺度的 feature maps: conv set -> conv 3 x 3 -> 1 x 1
conv_mobj_branch = common.convolutional(input_data, (3, 3, 256, 512),
self.training_flag, name='conv_mobj_branch')
conv_mbbox = common.convolutional(conv_mobj_branch, (1, 1, 512, 3 * (self.num_class + 5)),
training_flag=self.training_flag, name='conv_mbbox',
activate=False, bn=False)
# 进入另一个分支点: conv set -> 1 x 1 -> up_sampling -> concatenate
input_data = common.convolutional(input_data, (1, 1, 256, 128), self.training_flag, 'conv63')
input_data = common.up_sample(input_data, name='up_sample1', method=self.up_sample_method)
with tf.variable_scope('route_2'):
input_data = tf.concat([input_data, route_1], axis=-1)
# conv set 操作 conv: 1 x 1 -> 3 x 3 -> 1 x 1 -> 3 x 3 -> 1 x 1
input_data = common.convolutional(input_data, (1, 1, 384, 128), self.training_flag, 'conv64')
input_data = common.convolutional(input_data, (3, 3, 128, 256), self.training_flag, 'conv65')
input_data = common.convolutional(input_data, (1, 1, 256, 128), self.training_flag, 'conv66')
input_data = common.convolutional(input_data, (3, 3, 128, 256), self.training_flag, 'conv67')
input_data = common.convolutional(input_data, (1, 1, 256, 128), self.training_flag, 'conv68')
# scale three 大尺度的 feature maps: conv set -> conv 3 x 3 -> 1 x 1
conv_sobj_branch = common.convolutional(input_data, (3, 3, 128, 256),
self.training_flag, name='conv_sobj_branch')
conv_sbbox = common.convolutional(conv_sobj_branch, (1, 1, 256, 3 * (self.num_class + 5)),
training_flag=self.training_flag, name='conv_sbbox',
activate=False, bn=False)
# 将 3 个尺度 小、中、大 的 feature maps 类似于 13 x 13 大小的那种 feature maps
return conv_lbbox, conv_mbbox, conv_sbbox
pass
# 对 yolov3 网络 3 个尺度的 feature maps 进行预测
def pred_conv_bbox(self, conv_bbox, anchors, strides):
"""
:param conv_bbox: yolov3 network 返回的 feature maps
:param anchors: anchors 例如: [[10., 13.], [16., 30.], [33., 23.]],
[10., 13.] 用来表示 anchor box 为 10 x 13 大小的先验值
:param strides: 缩放步幅的尺度 例如: [8, 16, 32] 中的一个值,(原图 416 x 416)
如 32 表示使用 步幅为 32 的尺度进行操作,得到 13 x 13 大小 的feature maps,
相当于 缩放为 原图的 1/32 大小,另外的 8 和 16 的操作同理。
:return:
"""
# 获取 conv_bbox 的形状结构
conv_bbox_shape = tf.shape(conv_bbox)
# 获取 conv_bbox 的批量大小
batch_size = conv_bbox_shape[0]
# 获取 conv_bbox 的大小
conv_bbox_size = conv_bbox_shape[1]
# 获取每个尺度 anchors 的数量
anchor_per_scale = len(anchors)
# 将 conv_bbox 构建 目标张量,方便取值
conv_obj = tf.reshape(conv_bbox, (batch_size, conv_bbox_size,
conv_bbox_size, anchor_per_scale,
5 + self.num_class))
# 获取 中心点 坐标
conv_raw_dxdy = conv_obj[:, :, :, :, 0:2]
# 获取 width 和 high
conv_raw_dwdh = conv_obj[:, :, :, :, 2:4]
# 获取 置信度 即 前景或背景 的概率
conv_raw_conf = conv_obj[:, :, :, :, 4:5]
# 获取 c 类 对应的 概率值
conv_raw_prob = conv_obj[:, :, :, :, 5:]
# 张量操作, 构建一个 y 轴方向 (conv_bbox_size, conv_bbox_size) 大小的 张量,
# 并填入对应的正数值,用来表示它的绝对位置
y = tf.tile(tf.range(conv_bbox_size, dtype=tf.int32)[:, tf.newaxis], [1, conv_bbox_size])
# 张量操作, 构建一个 x 轴方向 (conv_bbox_size, conv_bbox_size) 大小的 张量,
# 并填入对应的正数值,用来表示它的绝对位置
x = tf.tile(tf.range(conv_bbox_size, dtype=tf.int32)[tf.newaxis, :], [conv_bbox_size, 1])
# 将 (conv_bbox_size, conv_bbox_size) 大小的 张量 根据 通道数 cancat 起来,
# 得到 (conv_bbox_size, conv_bbox_size, 2) 大小的张量, 这样,就得到对应 feature maps 每个格子的 绝对位置的数值
xy_grid = tf.concat([x[:, :, tf.newaxis], y[:, :, tf.newaxis]], axis=-1)
# 张量操作: 构建成 (batch_size, conv_bbox_size, conv_bbox_size, anchor_per_scale, 2) 结构
xy_grid = tf.tile(xy_grid[tf.newaxis, :, :, tf.newaxis, :], [batch_size, 1, 1, anchor_per_scale, 1])
# 将数据转为浮点型
xy_grid = tf.cast(xy_grid, tf.float32)
# 获取 x、y 预测值 映射到 原图 的中心点 位置 坐标; (偏移量 + 左上角坐标值) * 缩放值
pred_xy = (tf.sigmoid(conv_raw_dxdy) + xy_grid) * strides
# 获取 w、h 预测值 映射到 原图 的 width 和 high
# 论文中的公式为: b_w = p_w * e ^ (t_w); 然后再乘以 缩放度,则映射回原图
# p_w 为 先验 w 的大小,即为 anchor box 中 w 的大小。
pred_wh = (tf.exp(conv_raw_dwdh) * anchors) * strides
# 将 中心点 和 长 高 合并
pred_xywh = tf.concat([pred_xy, pred_wh], axis=-1)
# 计算置信度
pred_conf = tf.sigmoid(conv_raw_conf)
# 计算 c 类 概率
pred_prob = tf.sigmoid(conv_raw_prob)
# 返回 [batch_size, conv_bbox_size, conv_bbox_size, anchor_per_scale, 4 + 1 + class_num] 的 feature map
# 4 + 1 + class_num 代表为: pred_xywh + pred_conf + pred_prob
# 靠近 anchors 的 pred_conf 值为 1,远离的则 pred_conf 值为 0
# 靠近 anchors 的 pred_prob 值接近 1,远离的则 pred_prob 值接近 0
return tf.concat([pred_xywh, pred_conf, pred_prob], axis=-1)
pass
def compute_loss(self, label_sbbox, label_mbbox, label_lbbox, true_sbbox, true_mbbox, true_lbbox):
"""
:param label_sbbox: label 相对应的信息 包含 5 + classes
:param label_mbbox:
:param label_lbbox:
:param true_sbbox: 为 batch_size image 对应 strides 尺度的 ground truth boxes
[batch_size, ground_truth_num, xywh]; ground_truth_num 为每张图里面打有几个框
:param true_mbbox:
:param true_lbbox:
:return:
"""
# 分别计算三个尺度的损失函数
with tf.name_scope('smaller_box_loss'):
loss_sbbox = self.layer_loss(self.conv_sbbox, self.pred_sbbox,
label_sbbox, true_sbbox,
stride=self.strides[0])
with tf.name_scope('medium_box_loss'):
loss_mbbox = self.layer_loss(self.conv_mbbox, self.pred_mbbox,
label_mbbox, true_mbbox,
stride=self.strides[1])
with tf.name_scope('bigger_box_loss'):
loss_lbbox = self.layer_loss(self.conv_lbbox, self.pred_lbbox,
label_lbbox, true_lbbox,
stride=self.strides[2])
# 对三个尺度的损失函数进行相加
with tf.name_scope('giou_loss'):
giou_loss = loss_sbbox[0] + loss_mbbox[0] + loss_lbbox[0]
with tf.name_scope('conf_loss'):
conf_loss = loss_sbbox[1] + loss_mbbox[1] + loss_lbbox[1]
with tf.name_scope('prob_loss'):
prob_loss = loss_sbbox[2] + loss_mbbox[2] + loss_lbbox[2]
return giou_loss, conf_loss, prob_loss
def layer_loss(self, conv_bbox, pred_bbox, label_bbox, true_bbox, stride):
"""
:param conv_bbox: yolov3 网络得到的其中一个尺度的输出 feature maps
:param pred_bbox: 对 一个尺度输出的 feature maps 预测值
:param label_bbox: ground truth 对应的信息
:param true_bbox: ground truth 对应 anchor 尺度下的真实 box 值
:param stride: 缩放尺度 stride = [8, 16, 32] 中的一个值
:return:
"""
conv_shape = tf.shape(conv_bbox)
batch_size = conv_shape[0]
conv_bbox_size = conv_shape[1]
input_size = stride * conv_bbox_size
conv_bbox = tf.reshape(conv_bbox, (batch_size, conv_bbox_size, conv_bbox_size,
self.anchor_per_scale, 5 + self.num_class))
conv_raw_conf = conv_bbox[:, :, :, :, 4:5]
conv_raw_prob = conv_bbox[:, :, :, :, 5:]
# [batch_size, conv_bbox_size, conv_bbox_size, anchor_per_scale, 4 + 1 + class_num] 的 feature map
pred_xywh = pred_bbox[:, :, :, :, 0:4]
pred_conf = pred_bbox[:, :, :, :, 4:5]
label_xywh = label_bbox[:, :, :, :, 0:4]
respond_bbox = label_bbox[:, :, :, :, 4:5]
label_prob = label_bbox[:, :, :, :, 5:]
# 计算 预测框 与 label 框 的 GIOU
giou = tf.expand_dims(self.bbox_giou(pred_xywh, label_xywh), axis=-1)
input_size = tf.cast(input_size, tf.float32)
# 计算 giou 的损失函数,在这里 使用 1 < bbox_loss_scale < 2 为 giou_loss 的惩罚系数
# 当 bbox 相对于整张图像较小时,这时预测的准确率相对于较大的图像要小,需要用较大的 loss 来
# 对 目标 训练的准确率进行调整。因为当 loss 很小,而准确率不高的情况下,
# 是很难通过降低 loss 来调高 准确率的。而如 loss 相对大些,则容易通过降低 loss 来调高准确率。
# 这个 1 < bbox_loss_scale < 2 也是作者通过试验,测出来较好的值
bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (input_size ** 2)
# 在这里乘上一个置信度,因为背景是没有 giou_loss 的
giou_loss = respond_bbox * bbox_loss_scale * (1 - giou)
# 预测框 和 ground truth box 的 iou
# [batch_size, conv_bbox_size, conv_bbox_size, anchor_per_scale, ground_truth_box_num, xywh]
# ground_truth_box_num: 表示一张图 打有 几个 框
# 比如 pred_xywh 为 13 x 13 个格子,ground_truth_box_num 为 2。
# 每个格子中的坐标 与 ground_truth_box_num 这两个框 的坐标 的 IOU 结果,
# 这个 iou 用于 获取 下面 获取负样本 数
iou = self.bbox_iou(pred_xywh[:, :, :, :, np.newaxis, :],
true_bbox[:, np.newaxis, np.newaxis, np.newaxis, :, :])
# tf.reduce_max(iou, axis=-1) 获取最后一个维度 最大的 iou 值;
# expand_dims 可以用来增加一个维度,比如 [1, 2, 3] --> [[1], [2], [3]]
max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)
# 获取 负样本 系数
respond_bgd = (1.0 - respond_bbox) * tf.cast(max_iou < self.iou_loss_thresh, tf.float32)
# Focal loss: 为 交叉熵 的优化损失函数,减少 负样本 对损失函数对模型的影响
# Focal_loss = -(respond_bbox - pred_conf) ^ gamma * log(pred_conf)
# conf_focal 为 负样本 惩罚项系数
conf_focal = self.alpha * tf.pow(tf.abs(respond_bbox - pred_conf), self.gamma)
# respond_bbox 这里为正样本系数,因为它 的负样本 对应的值 为 0
# respond_bgd 为负样本系数
# 置信度损失函数
conf_loss = conf_focal * (
respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
+
respond_bgd * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
)
# c 类 概率损失函数
prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)
# 对各类损失函数累加再求均值
giou_loss = tf.reduce_mean(tf.reduce_sum(giou_loss, axis=[1, 2, 3, 4]))
conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1, 2, 3, 4]))
prob_loss = tf.reduce_mean(tf.reduce_sum(prob_loss, axis=[1, 2, 3, 4]))
return giou_loss, conf_loss, prob_loss
# bounding boxes giou
def bbox_giou(self, boxes1, boxes2):
# (x, y, w, h) --> (xmin, ymin, xmax, ymax)
boxes1 = tf.concat([boxes1[..., :2] - boxes1[..., 2:] * 0.5,
boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)
boxes2 = tf.concat([boxes2[..., :2] - boxes2[..., 2:] * 0.5,
boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)
# 获取 框 的 左上角 和 右下角 的坐标值
boxes1 = tf.concat([tf.minimum(boxes1[..., :2], boxes1[..., 2:]),
tf.maximum(boxes1[..., :2], boxes1[..., 2:])], axis=-1)
boxes2 = tf.concat([tf.minimum(boxes2[..., :2], boxes2[..., 2:]),
tf.maximum(boxes2[..., :2], boxes2[..., 2:])], axis=-1)
# 计算 框 的面积
boxes1_area = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] - boxes1[..., 1])
boxes2_area = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] - boxes2[..., 1])
# 计算交集的 左上角 和 右下角 坐标
left_up = tf.maximum(boxes1[..., :2], boxes2[..., :2])
right_down = tf.minimum(boxes1[..., 2:], boxes2[..., 2:])
# 判断 两个 框 是否相交
inter_section = tf.maximum(right_down - left_up, 0.0)
# 计算 交集 的面积
inter_area = inter_section[..., 0] * inter_section[..., 1]
# 计算 并集 的面积
union_area = boxes1_area + boxes2_area - inter_area
# 计算 IOU
iou = inter_area / union_area
# 计算最小密封框 的 左上角 坐标
enclose_left_up = tf.minimum(boxes1[..., :2], boxes2[..., :2])
# 计算最小密封框 的 右下角 坐标
enclose_right_down = tf.maximum(boxes1[..., 2:], boxes2[..., 2:])
# 计算最小密封框 的 high 和 width
enclose = tf.maximum(enclose_right_down - enclose_left_up, 0.0)
# 计算最小密封框 的 面积
enclose_area = enclose[..., 0] * enclose[..., 1]
# 计算 GIOU
giou = iou - 1.0 * (enclose_area - union_area) / enclose_area
return giou
# bounding boxes iou
def bbox_iou(self, boxes1, boxes2):
# 计算 框 的面积
boxes1_area = boxes1[..., 2] * boxes1[..., 3]
boxes2_area = boxes2[..., 2] * boxes2[..., 3]
# (x, y, w, h) --> (xmin, ymin, xmax, ymax)
boxes1 = tf.concat([boxes1[..., :2] - boxes1[..., 2:] * 0.5,
boxes1[..., :2] + boxes1[..., 2:] * 0.5], axis=-1)
boxes2 = tf.concat([boxes2[..., :2] - boxes2[..., 2:] * 0.5,
boxes2[..., :2] + boxes2[..., 2:] * 0.5], axis=-1)
# 计算交集的 左上角 和 右下角 坐标
left_up = tf.maximum(boxes1[..., :2], boxes2[..., :2])
right_down = tf.minimum(boxes1[..., 2:], boxes2[..., 2:])
# 判断 两个 框 是否相交
inter_section = tf.maximum(right_down - left_up, 0.0)
# 计算 交集 的面积
inter_area = inter_section[..., 0] * inter_section[..., 1]
# 计算 并集 的面积
union_area = boxes1_area + boxes2_area - inter_area
# 计算 IOU
iou = 1.0 * inter_area / union_area
return iou
(A)其中的darknet53()函数,建立基本的卷积网络,获取大中小3个等级的特征图:
import core.common as common
import tensorflow as tf
def darknet53(input_data, training_flag):
with tf.variable_scope('darknet'):
input_data = common.convolutional(input_data, filters_shape=(3, 3, 3, 32),
training_flag=training_flag, name='conv0')
input_data = common.convolutional(input_data, filters_shape=(3, 3, 32, 64),
training_flag=training_flag, name='conv1', down_sample=True)
for i in range(1):
input_data = common.residual_block(input_data, 64, 32, 64,
training_flag=training_flag, name='residual%d' % (i + 0))
input_data = common.convolutional(input_data, filters_shape=(3, 3, 64, 128),
training_flag=training_flag, name='conv4', down_sample=True)
for i in range(2):
input_data = common.residual_block(input_data, 128, 64, 128, training_flag=training_flag,
name='residual%d' % (i + 1))
input_data = common.convolutional(input_data, filters_shape=(3, 3, 128, 256),
training_flag=training_flag, name='conv9', down_sample=True)
for i in range(8):
input_data = common.residual_block(input_data, 256, 128, 256, training_flag=training_flag,
name='residual%d' % (i + 3))
route_1 = input_data
input_data = common.convolutional(input_data, filters_shape=(3, 3, 256, 512),
training_flag=training_flag, name='conv26', down_sample=True)
for i in range(8):
input_data = common.residual_block(input_data, 512, 256, 512, training_flag=training_flag,
name='residual%d' % (i + 11))
route_2 = input_data
input_data = common.convolutional(input_data, filters_shape=(3, 3, 512, 1024),
training_flag=training_flag, name='conv43', down_sample=True)
for i in range(4):
input_data = common.residual_block(input_data, 1024, 512, 1024, training_flag=training_flag,
name='residual%d' % (i + 19))
return route_1, route_2, input_data
(B). 网络模块工具 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
©.utils.py
import cv2
import random
import colorsys
import numpy as np
import tensorflow as tf
from config import cfg
# 获取类别
def read_class_names(class_file_name):
"""
:param class_file_name: class 文件路径
:return:
"""
names = {}
with open(class_file_name, 'r') as data:
# 获取类名和下标,用于数值和类之间的转换
for ID, name in enumerate(data):
names[ID] = name.strip('\n')
return names
# 获取 anchor
def get_anchors(anchors_path):
"""
:param anchors_path: anchor 文件路径
:return:
"""
with open(anchors_path) as f:
anchors = f.readline()
anchors = np.array(anchors.split(','), dtype=np.float32)
# anchors = anchors.reshape(-1, 2)
anchors = anchors.reshape(3, 3, 2)
# print("anchors.reshape:{}".format(anchors))
return anchors
# 读取 image data 的路径
def read_data_path(file_path):
image_path_list = []
with open(file_path) as file:
line_list = file.readlines()
for line_info in line_list:
data_info = line_info.strip()
image_path = data_info.split()[0]
image_path_list.append(image_path)
pass
pass
return image_path_list
pass
# 读取数据
def read_data_line(file_path):
data_line_list = []
with open(file_path) as file:
line_list = file.readlines()
for line_info in line_list:
data_info = line_info.strip().split()
data_line_list.append(data_info)
return data_line_list
# 图像预处理
def image_preporcess(image, target_size, gt_boxes=None):
"""
:param image: 图像信息
:param target_size: 目标尺寸
:param gt_boxes: ground truth
:return:
"""
# 颜色空间转换
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)
# 目标边长 比如 416 x 416
ih, iw = target_size
# 图像的真实边长 比如 632 x 900
h, w, _ = image.shape
# 将图像resize 成 最大边 为目标的边长,另一个边小于或等于目标边长
# 以保障最大的边没超出对应的边长。
# 通过上面的比如值,计算得到 0.4622222222222222
scale = min(iw / w, ih / h)
# 得到新的边长为 416
nw = int(scale * w)
# 得到新的边长为 292
nh = int(scale * h)
# 将上面的真实图像 resize 到 416 x 292 大小
image_resized = cv2.resize(image, (nw, nh))
# 创建一张 416 x 416 大小的三通道图,并全部填充 128 值
image_paded = np.full(shape=[ih, iw, 3], fill_value=128.0)
# 为了将上面的 resize 的真实图像放置在 目标图像 416 x 416 大小的中间
# 必须先根据 resize 的真实图像的边长,计算出与 目标图像 边长的差距
# 然后这些差距 除以 2,就在 目标图像 416 x 416 的中间了
# 根据上面的比如值 计算得 0
dw = (iw - nw) // 2
# 根据上面的比如值 计算得 62
dh = (ih - nh) // 2
# 将 resize 值放置 目标图像中间
# 比如值得 [62: 292 + 62, 0: 416 + 0, :]
# 这样,得到的图像 3 个通道中,h 上面和下面分别有 62 个格子是空白的
# 确保了真实图像放缩到了目标图像的中间位置。这一步对卷积提取特征非常重要。
image_paded[dh: nh + dh, dw: nw + dw, :] = image_resized
# 对得到的图像进行归一化处理
image_paded = image_paded / 255.
if gt_boxes is None:
return image_paded
else:
# 真实图像进行了缩放,ground truth 也相对的进行 缩放映射
gt_boxes[:, [0, 2]] = gt_boxes[:, [0, 2]] * scale + dw
gt_boxes[:, [1, 3]] = gt_boxes[:, [1, 3]] * scale + dh
return image_paded, gt_boxes
# 打框
def draw_bbox(image, bboxes, classes=None, show_label=True):
"""
:param image:
:param bboxes: [x_min, y_min, x_max, y_max, probability, cls_id] format coordinates.
:param classes: index and class dic
:param show_label:
:return:
"""
# 获取 class 的 index 和 name
if classes is None:
classes = read_class_names(cfg.COMMON.CLASS_FILE_PATH)
pass
class_num = len(classes)
image_h, image_w, _ = image.shape
# 生成 与 类别数量的 3 通道 hsv 颜色元组
hsv_tuples = [(1.0 * x / class_num, 1., 1.) for x in range(class_num)]
# 颜色空间变换,将 hsv 转 rgb
colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
# 将归一化的颜色值还原到 0~255 之间
colors = list(map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)), colors))
# 设置随机种子
random.seed(32)
# 打框颜色 洗牌
random.shuffle(colors)
random.seed(None)
for i, bbox in enumerate(bboxes):
# 获取 bbox 的坐标值
coor = np.array(bbox[:4], dtype=np.int32)
font_scale = 0.5
# 获取得分
score = bbox[4]
# 获取类别的 index
class_ind = int(bbox[5])
# 根据类别的 index 获取 bbox 打框的颜色
bbox_color = colors[class_ind]
# bbox 打框 的线条宽度
bbox_thick = int(0.6 * (image_h + image_w) / 600)
# 获取左上角和右下角坐标
c1 = (coor[0], coor[1])
c2 = (coor[2], coor[3])
# 打框
cv2.rectangle(image, c1, c2, bbox_color, bbox_thick)
# 对 label 进行打框标注
if show_label:
# 根据 index 获取 class 的名字
bbox_mess = '%s: %.2f' % (classes[class_ind], score)
t_size = cv2.getTextSize(bbox_mess, 0, font_scale, thickness=bbox_thick // 2)[0]
cv2.rectangle(image, c1, (c1[0] + t_size[0], c1[1] - t_size[1] - 3), bbox_color, -1) # filled
cv2.putText(image, bbox_mess, (c1[0], c1[1] - 2), cv2.FONT_HERSHEY_SIMPLEX,
font_scale, (0, 0, 0), bbox_thick // 2, lineType=cv2.LINE_AA)
return image
# (x, y, w, h) --> (xmin, ymin, xmax, ymax)
def bbox_xywh_dxdy(boxes):
boxes = np.array(boxes)
boxes_dxdy = np.concatenate([boxes[..., :2] - boxes[..., 2:] * 0.5,
boxes[..., :2] + boxes[..., 2:] * 0.5], axis=-1)
return boxes_dxdy
pass
# (xmin, ymin, xmax, ymax) --> (x, y, w, h)
def bbox_dxdy_xywh(boxes):
boxes = np.array(boxes)
# 转换 [xmin, ymin, xmax, ymax] --> [x, y, w, h] bounding boxes 结构
bbox_xywh = np.concatenate([(boxes[2:] + boxes[:2]) * 0.5,
boxes[2:] - boxes[:2]], axis=-1)
return bbox_xywh
pass
# IOU
def bboxes_iou(boxes1, boxes2):
boxes1 = np.array(boxes1)
boxes2 = np.array(boxes2)
# 计算 面积
boxes1_area = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] - boxes1[..., 1])
boxes2_area = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] - boxes2[..., 1])
# 交集的 左上角坐标
left_up = np.maximum(boxes1[..., :2], boxes2[..., :2])
# 交集的 右下角坐标
right_down = np.minimum(boxes1[..., 2:], boxes2[..., 2:])
# 计算交集矩形框的 high 和 width
inter_section = np.maximum(right_down - left_up, 0.0)
# 两个矩形框的 交集 面积
inter_area = inter_section[..., 0] * inter_section[..., 1]
# 两个矩形框的并集面积
union_area = boxes1_area + boxes2_area - inter_area
# 计算 iou
ious = np.maximum(1.0 * inter_area / union_area, np.finfo(np.float32).eps)
return ious
# 获取 .pb 模型
def read_pb_return_tensors(graph, pb_file, return_elements):
with tf.gfile.FastGFile(pb_file, 'rb') as f:
frozen_graph_def = tf.GraphDef()
frozen_graph_def.ParseFromString(f.read())
with graph.as_default():
return_elements = tf.import_graph_def(frozen_graph_def,
return_elements=return_elements)
return return_elements
# 非极大值抑制
def nms(bboxes, iou_threshold, sigma=0.3, method='nms'):
"""
:param bboxes: (xmin, ymin, xmax, ymax, score, class)
:param iou_threshold: iou 阈值
:param sigma:
:param method: 方法
:return:
"""
# 获取 bbox 中类别种类的 list
classes_in_img = list(set(bboxes[:, 5]))
best_bboxes = []
for cls in classes_in_img:
# 构建一个 bbox batch size 大小的 list,
# 类别与 cls 相同的为 list内容为 True,不同的额外 False
# 比如 bboxes = [[12, 12, 94, 94, 0.78, 0],
# [34, 34, 64, 64, 0.88, 1],
# [78, 78, 124, 124, 0.98, 0],
# [52, 52, 74, 74, 0.78, 1]
# ]
# 第一次遍历得到 [True, False, True, False] 这样的 cls_mask list
# 第二次遍历得到 [False, True, False, True] 这样的 cls_mask list
cls_mask = (bboxes[:, 5] == cls)
# 第一次遍历得到 [[12, 12, 94, 94, 0.78, 0], [78, 78, 124, 124, 0.98, 0]] 这样的 cls_bboxes list
# 第二次遍历得到 [[34, 34, 64, 64, 0.88, 1], [52, 52, 74, 74, 0.78, 1]] 这样的 cls_bboxes list
cls_bboxes = bboxes[cls_mask]
while len(cls_bboxes) > 0:
# 获取最大概率值的下标
max_ind = np.argmax(cls_bboxes[:, 4])
# 概率值最大的 bbox 为最佳 bbox
best_bbox = cls_bboxes[max_ind]
# 将所有 最好的 bbox 放到一个 list 中
best_bboxes.append(best_bbox)
# 将概率最大的那个 bbox 移除后 剩下的 bboxes
cls_bboxes = np.concatenate([cls_bboxes[: max_ind], cls_bboxes[max_ind + 1:]])
# 计算 best bbox 与剩下的 bbox 之间的 iou
iou = bboxes_iou(best_bbox[np.newaxis, :4], cls_bboxes[:, :4])
# 构建一个 长度为 len(iou) 的 list,并填充 1 值
weight = np.ones((len(iou),), dtype=np.float32)
assert method in ['nms', 'soft-nms']
if method == 'nms':
# 将大于阈值的 iou,其对应 list 的值设置为 0,用于下面对该值进行移除
iou_mask = iou > iou_threshold
weight[iou_mask] = 0.0
if method == 'soft-nms':
weight = np.exp(-(1.0 * iou ** 2 / sigma))
# 移除 大于阈值 的 bboxes,如此重复,直至 cls_bboxes 为空
# 将大于阈值的 bbox 概率设置为 零值
cls_bboxes[:, 4] = cls_bboxes[:, 4] * weight
# 保留概率 大于 零值 的 bbox
score_mask = cls_bboxes[:, 4] > 0.
cls_bboxes = cls_bboxes[score_mask]
return best_bboxes
# 处理后的盒子
def postprocess_boxes(pred_bbox, org_img_shape, input_size, score_threshold):
"""
:param pred_bbox: 预测的 bbox
:param org_img_shape: 原始图像的 shape
:param input_size: 输入的大小
:param score_threshold: 得分阈值
:return:
"""
valid_scale = [0, np.inf]
pred_bbox = np.array(pred_bbox)
# bbox 坐标
pred_xywh = pred_bbox[:, 0:4]
# bbox 置信度
pred_conf = pred_bbox[:, 4]
# bbox 概率
pred_prob = pred_bbox[:, 5:]
# (1) (x, y, w, h) --> (xmin, ymin, xmax, ymax)
pred_coor = np.concatenate([pred_xywh[:, :2] - pred_xywh[:, 2:] * 0.5,
pred_xywh[:, :2] + pred_xywh[:, 2:] * 0.5], axis=-1)
# (2) (xmin, ymin, xmax, ymax) -> (xmin_org, ymin_org, xmax_org, ymax_org)
org_h, org_w = org_img_shape
resize_ratio = min(input_size / org_w, input_size / org_h)
dw = (input_size - resize_ratio * org_w) / 2
dh = (input_size - resize_ratio * org_h) / 2
# 将预测的 x 的坐标(xmin, xmax) pred_coor[:, 0::2] 减去空白区域 dw 后,
# 除以缩放比率,得到原图 x 方向的大小
pred_coor[:, 0::2] = 1.0 * (pred_coor[:, 0::2] - dw) / resize_ratio
# 将预测的 y 的坐标(ymin, ymax) pred_coor[:, 1::2] 减去空白区域 dh 后,
# 除以缩放比率,得到原图 y 方向的大小
pred_coor[:, 1::2] = 1.0 * (pred_coor[:, 1::2] - dh) / resize_ratio
# (3) clip some boxes those are out of range 处理那些超出原图大小范围的 bboxes
pred_coor = np.concatenate([np.maximum(pred_coor[:, :2], [0, 0]),
np.minimum(pred_coor[:, 2:], [org_w - 1, org_h - 1])], axis=-1)
# 处理不正常的 bbox
invalid_mask = np.logical_or((pred_coor[:, 0] > pred_coor[:, 2]), (pred_coor[:, 1] > pred_coor[:, 3]))
pred_coor[invalid_mask] = 0
# (4) discard some invalid boxes 丢弃无效的 bbox
bboxes_scale = np.sqrt(np.multiply.reduce(pred_coor[:, 2:4] - pred_coor[:, 0:2], axis=-1))
# np 的 逻辑 and
scale_mask = np.logical_and((valid_scale[0] < bboxes_scale), (bboxes_scale < valid_scale[1]))
# (5) discard some boxes with low scores 丢弃分值过低的 bbox
classes = np.argmax(pred_prob, axis=-1)
scores = pred_conf * pred_prob[np.arange(len(pred_coor)), classes]
score_mask = scores > score_threshold
mask = np.logical_and(scale_mask, score_mask)
coors, scores, classes = pred_coor[mask], scores[mask], classes[mask]
return np.concatenate([coors, scores[:, np.newaxis], classes[:, np.newaxis]], axis=-1)
(D)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")
参考文章:
[1]YOLOv3: An Incremental Impro
[2]Deep Residual Learning for Image Recognition
[3]YOLOv3源码解析