以下部分来自于对原论文的翻译
一、引言
目前的检测系统通过重用分类器来执行检测。为了检测目标,这些系统为该目标提供一个分类器,在测试图像的不同的位置和不同的尺度上对其进行评估。像deformable parts models(DPM)这样的系统使用滑动窗口方法,其分类器在整个图像上均匀间隔的位置上运行。最近的方法,如R-CNN使用region proposal策略,首先在图像中生成潜在的边界框(bounding box),然后在这些框上运行分类器。在分类之后,执行用于细化边界框的后处理,消除重复的检测,并根据场景中的其它目标为边界框重新打分。这些复杂的流程是很慢,很难优化的,因为每个独立的部分都必须单独进行训练。
我们将目标检测看作是一个单一的回归问题,直接从图像像素得到边界框坐标和类别概率。使用我们的系统——You Only Look Once(YOLO),便能得到图像上的物体是什么和物体的具体位置。YOLO非常简单(见下图),它仅用单个卷积网络就能同时预测多个边界框和它们的类别概率。YOLO在整个图像上训练,并能直接优化检测性能。与传统的目标检测方法相比,这种统一的模型下面所列的一些优点。
第一,YOLO速度非常快。由于我们将检测视为回归问题,所以我们不需要复杂的流程。测试时,我们在一张新图像上简单的运行我们的神经网络来预测检测结果。在Titan X GPU上不做批处理的情况下,YOLO的基础版本以每秒45帧的速度运行,而快速版本运行速度超过150fps。这意味着我们可以在不到25毫秒的延迟内实时处理流媒体视频。此外,YOLO实现了其它实时系统两倍以上的平均精度。
第二,YOLO是在整个图像上进行推断的。与基于滑动窗口和候选框的技术不同,YOLO在训练期间和测试时都会顾及到整个图像,所以它隐式地包含了关于类的上下文信息以及它们的外观。Fast R-CNN是一种很好的检测方法,但由于它看不到更大的上下文,会将背景块误检为目标。与Fast R-CNN相比,YOLO的背景误检数量少了一半。
第三,YOLO能学习到目标的泛化表征。把在自然图像上进行训练的模型,用在艺术图像进行测试时,YOLO大幅优于DPM和R-CNN等顶级的检测方法。由于YOLO具有高度泛化能力,因此在应用于新领域或碰到意外的输入时不太可能出故障。
YOLO在精度上仍然落后于目前最先进的检测系统。虽然它可以快速识别图像中的目标,但它在定位某些物体尤其是小的物体上精度不高。我们在实验中会进一步探讨精度/时间的权衡。我们所有的训练和测试代码都是开源的,而且各种预训练模型也都可以下载。
论文地址:点击打开链接
二、检测
我们将目标检测的独立部分整合到单个神经网络中。我们的网络使用整个图像的特征来预测每个边界框。它还可以同时预测一张图像中的所有类别的所有边界框。这意味着我们的网络对整张图像和图像中的所有目标进行推断。YOLO设计可实现端到端训练和实时的速度,同时保持较高的平均精度。我们的系统将输入图像分成S×S的网格。如果目标的中心落入某个网格单元中,那么该网格单元就负责检测该目标。
每个网格单元都会预测B个边界框和这些框的置信度分数(confidence scores)。这些置信度分数反映了该模型对那个框内是否包含目标的信心,以及它对自己的预测的准确度的估量。在形式上,我们将置信度定义为 。如果该单元格中不存在目标,则置信度分数应为零。否则,我们希望置信度分数等于预测框(predict box)与真实标签框(ground truth)之间联合部分的交集(IOU)。每个边界框包含5个预测:x,y,w,h和置信度。(x,y)坐标表示边界框的中心相对于网格单元的边界的值,而宽度和高度则是相对于整张图像来预测的。置信度预测表示预测框与任意实际边界框之间的IOU。
每个网格单元还预测C个条件类别概率 ,这些概率以包含目标的网格单元为条件。不管边界框的的数量B是多少,每个网格单元我们只预测一组类别概率。在测试时,我们把条件类概率和每个框的预测的置信度值相乘,
它给出了每个框特定类别的置信度分数。这些分数体现了该类出现在框中的概率以及预测框拟合目标的程度。为了在Pascal VOC上评估YOLO,我们使用S=7,B=2。Pascal VOC有20个标注类,所以C=20。我们最终的预测是7×7×30的张量。
2.1 网络设计
我们将此模型作为卷积神经网络来实现,并在Pascal VOC检测数据集上进行评估。网络的初始卷积层从图像中提取特征,而全连接层负责预测输出概率和坐标。我们的网络架构受图像分类模型GoogLeNet的启发。我们的网络有24个卷积层,后面是2个全连接层。我们只使用1×1降维层,后面是3×3卷积层,这与Lin等人类似,而不是GoogLeNet使用的Inception模块。完整的网络如图所示。我们还训练了快速版本的YOLO,旨在推动快速目标检测的界限。快速YOLO使用具有较少卷积层(9层而不是24层)的神经网络,在这些层中使用较少的滤波器。除了网络规模之外,基本版YOLO和快速YOLO的所有训练和测试参数都是相同的。我们网络的最终输出是7×7×30的预测张量。
2.2 训练
我们在ImageNet的1000类竞赛数据集上预训练我们的卷积层。对于预训练,我们使用图3中的前20个卷积层,接着是平均池化层和全连接层。我们对这个网络进行了大约一周的训练,并且在ImageNet 2012验证集上获得了单一裁剪图像88%的top-5准确率,与Caffe模型池中的GoogLeNet模型相当。我们使用Darknet框架进行所有的训练和推断。然后我们转换模型来执行检测训练。Ren等人表明,预训练网络中增加卷积层和连接层可以提高性能[29]。按照他们的方法,我们添加了四个卷积层和两个全连接层,这些层的权重都用随机值初始化。检测通常需要细粒度的视觉信息,因此我们将网络的输入分辨率从224×224改为448×448。
模型的最后一层预测类概率和边界框坐标。我们通过图像宽度和高度来规范边界框的宽度和高度,使它们落在0和1之间。我们将边界框x和y坐标参数化为特定网格单元位置的偏移量,所以它们的值被限定在在0和1之间。模型的最后一层使用线性激活函数,而所有其它的层使用下面的leaky rectified activation:
我们对模型输出的平方和误差进行优化。我们选择使用平方和误差,是因为它易于优化,但是它并不完全符合最大化平均精度(average precision)的目标。它给分类误差与定位误差的权重是一样的,这点可能并不理想。另外,每个图像都有很多网格单元并没有包含任何目标,这将这些单元格的“置信度”分数推向零,通常压制了包含目标的单元格的梯度。这可能导致模型不稳定,从而导致训练在早期就发散。为了弥补平方和误差的缺陷,我们增加了边界框坐标预测的损失,并减少了不包含目标的框的置信度预测的损失。 我们使用两个参数λcoord和λnoobj来实现这一点。 我们设定λcoord= 5和λnoobj= .5。
平方和误差对大框和小框的误差权衡是一样的,而我们的错误指标应该要体现出,大框的小偏差的重要性不如小框的小偏差的重要性。为了部分解决这个问题,我们直接预测边界框宽度和高度的平方根,而不是宽度和高度。YOLO为每个网格单元预测多个边界框。在训练时,每个目标我们只需要一个边界框预测器来负责。若某预测器的预测值与目标的实际值的IOU值最高,则这个预测器被指定为“负责”预测该目标。这导致边界框预测器的专业化。每个预测器可以更好地预测特定大小,方向角,或目标的类别,从而改善整体召回率。在训练期间,我们优化以下多部分损失函数:
其中 表示目标是否出现在网格单元i中, 表示单元格i中的第j个边界框预测器“负责”该预测。注意,如果目标存在于该网格单元中(前面讨论的条件类别概率),则损失函数仅惩罚分类错误。如果预测器“负责”实际边界框(即该网格单元中具有最高IOU的预测器),则它也仅惩罚边界框坐标错误。
我们用Pascal VOC 2007和2012的训练集和验证数据集进行了大约135个迭代的网络训练。因为我们仅在Pascal VOC 2012上进行测试,所以我们的训练集里包含了Pascal VOC 2007的测试数据。在整个训练过程中,我们使用的批量大小是64,动量为0.9,衰减率是0.0005。我们的学习率计划如下:在第一个迭代周期,我们将学习率从 慢慢地提高到 。如果从大的学习率开始训练,我们的模型通常会由于不稳定的梯度而发散。我们继续以 进行75个周期的训练,然后以 进行30个周期的训练,最后以 进行30个周期的训练。为避免过拟合,我们使用了Dropout和大量的数据增强。 在第一个连接层之后的dropout层的丢弃率设置为0.5,以防止层之间的相互适应。 对于数据增强,我们引入高达20%的原始图像大小的随机缩放和平移。我们还在HSV色彩空间中以高达1.5的因子随机调整图像的曝光度和饱和度。
2.3 推断
就像在训练中一样,预测测试图像的检测只需要一次网络评估。在Pascal VOC上,每张图像上网络预测98个边界框和每个框的类别概率。YOLO在测试时非常快,因为它只需要一次网络评估,这与基于分类器的方法不同。网格设计强化了边界框预测中的空间多样性。通常一个目标落在哪一个网格单元中是很明显的,而网络只能为每个目标预测一个边界框。然而,一些大的目标或接近多个网格单元的边界的目标能被多个网格单元定位。非极大值抑制可以用来修正这些多重检测。非最大抑制对于YOLO的性能的影响不像对于R-CNN或DPM那样重要,但也能增加2−3%的mAP。
2.4 缺陷
YOLO给边界框预测强加空间约束,因为每个网格单元只预测两个框和只能有一个类别。这个空间约束限制了我们的模型可以预测的邻近目标的数量。我们的模型难以预测群组中出现的小物体(比如鸟群)。由于我们的模型学习是从数据中预测边界框,因此它很难泛化到新的、不常见的长宽比或配置的目标。我们的模型也使用相对较粗糙的特征来预测边界框,因为输入图像在我们的架构中历经了多个下采样层。
最后,我们的训练基于一个逼近检测性能的损失函数,这个损失函数无差别地处理小边界框与大边界框的误差。大边界框的小误差通常是无关要紧的,但小边界框的小误差对IOU的影响要大得多。我们的主要错误来自于不正确的定位。
三、实验
此次我们复现的是yolo-small 模型,也就是dark-net19。话不多说,代码如下:
# -*- coding: utf-8 -*-
import tensorflow as tf
import numpy as np
import cv2
# leaky_relu激活函数
def leaky_relu(x, alpha=0.1):
return tf.maximum(alpha * x, x)
class Yolo(object):
def __init__(self, weights_file, input_image, verbose=True):
# 后面程序打印描述功能的标志位
self.verbose = verbose
# 检测超参数
self.S = 7 # cell数量
self.B = 2 # 每个网格的边界框数
self.classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
"bus", "car", "cat", "chair", "cow", "diningtable",
"dog", "horse", "motorbike", "person", "pottedplant",
"sheep", "sofa", "train", "tvmonitor"]
self.C = len(self.classes) # 类别数
self.x_offset = np.transpose(np.reshape(np.array([np.arange(self.S)] * self.S * self.B),
[self.B, self.S, self.S]), [1, 2, 0])
self.y_offset = np.transpose(self.x_offset, [1, 0, 2]) # 改变数组的shape
self.threshold = 0.2 # 类别置信度分数阈值
self.iou_threshold = 0.4 # IOU阈值,小于0.4的会过滤掉
self.max_output_size = 10 # NMS选择的边界框的最大数量
self.sess = tf.Session()
self._build_net() # 【1】搭建网络模型(预测):模型的主体网络部分,这个网络将输出[batch,7*7*30]的张量
self._build_detector() # 【2】解析网络的预测结果:先判断预测框类别,再NMS
self._load_weights(weights_file) # 【3】导入权重文件
self.detect_from_file(image_file=input_image) # 【4】从预测输入图片,并可视化检测边界框、将obj的分类结果和坐标保存成txt。
# 【1】搭建网络模型(预测):模型的主体网络部分,这个网络将输出[batch,7*7*30]的张量
def _build_net(self):
# 打印状态信息
if self.verbose:
print("Start to build the network ...")
# 输入、输出用占位符,因为尺寸一般不会改变
self.images = tf.placeholder(tf.float32, [None, 448, 448, 3]) # None表示不确定,为了自适应batchsize
# 搭建网络模型
net = self._conv_layer(self.images, 1, 64, 7, 2)
net = self._maxpool_layer(net, 1, 2, 2)
net = self._conv_layer(net, 2, 192, 3, 1)
net = self._maxpool_layer(net, 2, 2, 2)
net = self._conv_layer(net, 3, 128, 1, 1)
net = self._conv_layer(net, 4, 256, 3, 1)
net = self._conv_layer(net, 5, 256, 1, 1)
net = self._conv_layer(net, 6, 512, 3, 1)
net = self._maxpool_layer(net, 6, 2, 2)
net = self._conv_layer(net, 7, 256, 1, 1)
net = self._conv_layer(net, 8, 512, 3, 1)
net = self._conv_layer(net, 9, 256, 1, 1)
net = self._conv_layer(net, 10, 512, 3, 1)
net = self._conv_layer(net, 11, 256, 1, 1)
net = self._conv_layer(net, 12, 512, 3, 1)
net = self._conv_layer(net, 13, 256, 1, 1)
net = self._conv_layer(net, 14, 512, 3, 1)
net = self._conv_layer(net, 15, 512, 1, 1)
net = self._conv_layer(net, 16, 1024, 3, 1)
net = self._maxpool_layer(net, 16, 2, 2)
net = self._conv_layer(net, 17, 512, 1, 1)
net = self._conv_layer(net, 18, 1024, 3, 1)
net = self._conv_layer(net, 19, 512, 1, 1)
net = self._conv_layer(net, 20, 1024, 3, 1)
net = self._conv_layer(net, 21, 1024, 3, 1)
net = self._conv_layer(net, 22, 1024, 3, 2)
net = self._conv_layer(net, 23, 1024, 3, 1)
net = self._conv_layer(net, 24, 1024, 3, 1)
net = self._flatten(net)
net = self._fc_layer(net, 25, 512, activation=leaky_relu)
net = self._fc_layer(net, 26, 4096, activation=leaky_relu)
net = self._fc_layer(net, 27, self.S * self.S * (self.B * 5 + self.C))
# 网络输出,[batch,7*7*30]的张量
self.predicts = net
# 【2】解析网络的预测结果:先判断预测框类别,再NMS
def _build_detector(self):
# 原始图片的宽和高
self.width = tf.placeholder(tf.float32, name='img_w')
self.height = tf.placeholder(tf.float32, name='img_h')
# 网络回归[batch,7*7*30]:
idx1 = self.S * self.S * self.C
idx2 = idx1 + self.S * self.S * self.B
# 1.类别概率[:,:7*7*20] 20维
class_probs = tf.reshape(self.predicts[0, :idx1], [self.S, self.S, self.C])
# 2.置信度[:,7*7*20:7*7*(20+2)] 2维
confs = tf.reshape(self.predicts[0, idx1:idx2], [self.S, self.S, self.B])
# 3.边界框[:,7*7*(20+2):] 8维 -> (x,y,w,h)
boxes = tf.reshape(self.predicts[0, idx2:], [self.S, self.S, self.B, 4])
# 将x,y转换为相对于图像左上角的坐标
# w,h的预测是平方根乘以图像的宽度和高度
boxes = tf.stack([(boxes[:, :, :, 0] + tf.constant(self.x_offset, dtype=tf.float32)) / self.S * self.width,
(boxes[:, :, :, 1] + tf.constant(self.y_offset, dtype=tf.float32)) / self.S * self.height,
tf.square(boxes[:, :, :, 2]) * self.width,
tf.square(boxes[:, :, :, 3]) * self.height], axis=3)
# 类别置信度分数:[S,S,B,1]*[S,S,1,C]=[S,S,B,类别置信度C]
scores = tf.expand_dims(confs, -1) * tf.expand_dims(class_probs, 2)
scores = tf.reshape(scores, [-1, self.C]) # [S*S*B, C]
boxes = tf.reshape(boxes, [-1, 4]) # [S*S*B, 4]
# 只选择类别置信度最大的值作为box的类别、分数
box_classes = tf.argmax(scores, axis=1) # 边界框box的类别
box_class_scores = tf.reduce_max(scores, axis=1) # 边界框box的分数
# 利用类别置信度阈值self.threshold,过滤掉类别置信度低的
filter_mask = box_class_scores >= self.threshold
scores = tf.boolean_mask(box_class_scores, filter_mask)
boxes = tf.boolean_mask(boxes, filter_mask)
box_classes = tf.boolean_mask(box_classes, filter_mask)
# NMS (不区分不同的类别)
# 中心坐标+宽高box (x, y, w, h) -> xmin=x-w/2 -> 左上+右下box (xmin, ymin, xmax, ymax),因为NMS函数是这种计算方式
_boxes = tf.stack([boxes[:, 0] - 0.5 * boxes[:, 2], boxes[:, 1] - 0.5 * boxes[:, 3],
boxes[:, 0] + 0.5 * boxes[:, 2], boxes[:, 1] + 0.5 * boxes[:, 3]], axis=1)
nms_indices = tf.image.non_max_suppression(_boxes, scores,
self.max_output_size, self.iou_threshold)
self.scores = tf.gather(scores, nms_indices)
self.boxes = tf.gather(boxes, nms_indices)
self.box_classes = tf.gather(box_classes, nms_indices)
# 【3】导入权重文件
def _load_weights(self, weights_file):
# 打印状态信息
if self.verbose:
print("Start to load weights from file:%s" % (weights_file))
# 导入权重
saver = tf.train.Saver() # 初始化
saver.restore(self.sess, weights_file) # saver.restore导入/saver.save保存
# 【4】从预测输入图片,并可视化检测边界框、将obj的分类结果和坐标保存成txt。
# image_file是输入图片文件路径;
# deteted_boxes_file="boxes.txt"是最后坐标txt;detected_image_file="detected_image.jpg"是检测结果可视化图片
def detect_from_file(self, image_file, imshow=True, deteted_boxes_file="boxes.txt",
detected_image_file="detected_image.jpg"):
# read image
image = cv2.imread(image_file)
img_h, img_w, _ = image.shape
scores, boxes, box_classes = self._detect_from_image(image)
predict_boxes = []
for i in range(len(scores)):
# 预测框数据为:[概率,x,y,w,h,类别置信度]
predict_boxes.append((self.classes[box_classes[i]], boxes[i, 0],
boxes[i, 1], boxes[i, 2], boxes[i, 3], scores[i]))
self.show_results(image, predict_boxes, imshow, deteted_boxes_file, detected_image_file)
################# 对应【1】:定义conv/maxpool/flatten/fc层#############################################################
# 卷积层:x输入;id:层数索引;num_filters:卷积核个数;filter_size:卷积核尺寸;stride:步长
def _conv_layer(self, x, id, num_filters, filter_size, stride):
# 通道数
in_channels = x.get_shape().as_list()[-1]
# 均值为0标准差为0.1的正态分布,初始化权重w;shape=行*列*通道数*卷积核个数
weight = tf.Variable(
tf.truncated_normal([filter_size, filter_size, in_channels, num_filters], mean=0.0, stddev=0.1))
bias = tf.Variable(tf.zeros([num_filters, ])) # 列向量
# padding, 注意: 不用padding="SAME",否则可能会导致坐标计算错误
pad_size = filter_size // 2 # 除法运算,保留商的整数部分
pad_mat = np.array([[0, 0], [pad_size, pad_size], [pad_size, pad_size], [0, 0]])
x_pad = tf.pad(x, pad_mat)
conv = tf.nn.conv2d(x_pad, weight, strides=[1, stride, stride, 1], padding="VALID")
output = leaky_relu(tf.nn.bias_add(conv, bias))
# 打印该层信息
if self.verbose:
print('Layer%d:type=conv,num_filter=%d,filter_size=%d,stride=%d,output_shape=%s'
% (id, num_filters, filter_size, stride, str(output.get_shape())))
return output
# 池化层:x输入;id:层数索引;pool_size:池化尺寸;stride:步长
def _maxpool_layer(self, x, id, pool_size, stride):
output = tf.layers.max_pooling2d(inputs=x,
pool_size=pool_size,
strides=stride,
padding='SAME')
if self.verbose:
print('Layer%d:type=MaxPool,pool_size=%d,stride=%d,out_shape=%s'
% (id, pool_size, stride, str(output.get_shape())))
return output
# 扁平层:因为接下来会连接全连接层,例如[n_samples, 7, 7, 32] -> [n_samples, 7*7*32]
def _flatten(self, x):
tran_x = tf.transpose(x, [0, 3, 1, 2]) # [batch,行,列,通道数channels] -> [batch,通道数channels,列,行]
nums = np.product(x.get_shape().as_list()[1:]) # 计算的是总共的神经元数量,第一个表示batch数量所以去掉
return tf.reshape(tran_x, [-1, nums]) # [batch,通道数channels,列,行] -> [batch,通道数channels*列*行],-1代表自适应batch数量
# 全连接层:x输入;id:层数索引;num_out:输出尺寸;activation:激活函数
def _fc_layer(self, x, id, num_out, activation=None):
num_in = x.get_shape().as_list()[-1] # 通道数/维度
# 均值为0标准差为0.1的正态分布,初始化权重w;shape=行*列*通道数*卷积核个数
weight = tf.Variable(tf.truncated_normal(shape=[num_in, num_out], mean=0.0, stddev=0.1))
bias = tf.Variable(tf.zeros(shape=[num_out, ])) # 列向量
output = tf.nn.xw_plus_b(x, weight, bias)
# 正常全连接层是leak_relu激活函数;但是最后一层是liner函数
if activation:
output = activation(output)
# 打印该层信息
if self.verbose:
print('Layer%d:type=Fc,num_out=%d,output_shape=%s'
% (id, num_out, str(output.get_shape())))
return output
######################## 对应【4】:可视化检测边界框、将obj的分类结果和坐标保存成txt#########################################
def _detect_from_image(self, image):
"""Do detection given a cv image"""
img_h, img_w, _ = image.shape
img_resized = cv2.resize(image, (448, 448))
img_RGB = cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB)
img_resized_np = np.asarray(img_RGB)
_images = np.zeros((1, 448, 448, 3), dtype=np.float32)
_images[0] = (img_resized_np / 255.0) * 2.0 - 1.0
scores, boxes, box_classes = self.sess.run([self.scores, self.boxes, self.box_classes],
feed_dict={self.images: _images, self.width: img_w,
self.height: img_h})
return scores, boxes, box_classes
def show_results(self, image, results, imshow=True, deteted_boxes_file=None,
detected_image_file=None):
"""Show the detection boxes"""
img_cp = image.copy()
if deteted_boxes_file:
f = open(deteted_boxes_file, "w")
# draw boxes
for i in range(len(results)):
x = int(results[i][1])
y = int(results[i][2])
w = int(results[i][3]) // 2
h = int(results[i][4]) // 2
if self.verbose:
print("class: %s, [x, y, w, h]=[%d, %d, %d, %d], confidence=%f"
% (results[i][0], x, y, w, h, results[i][-1]))
# 中心坐标 + 宽高box(x, y, w, h) -> xmin = x - w / 2 -> 左上 + 右下box(xmin, ymin, xmax, ymax)
cv2.rectangle(img_cp, (x - w, y - h), (x + w, y + h), (0, 255, 0), 2)
# 在边界框上显示类别、分数(类别置信度)
cv2.rectangle(img_cp, (x - w, y - h - 20), (x + w, y - h), (125, 125, 125), -1) # puttext函数的背景
cv2.putText(img_cp, results[i][0] + ' : %.2f' % results[i][5], (x - w + 5, y - h - 7),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
if deteted_boxes_file:
# 保存obj检测结果为txt文件
f.write(results[i][0] + ',' + str(x) + ',' + str(y) + ',' +
str(w) + ',' + str(h) + ',' + str(results[i][5]) + '\n')
if imshow:
cv2.imshow('YOLO_small detection', img_cp)
cv2.waitKey(1)
if detected_image_file:
cv2.imwrite(detected_image_file, img_cp)
if deteted_boxes_file:
f.close()
if __name__ == '__main__':
yolo_net = Yolo(weights_file='D:/Python/YOLOv1-Tensorflow-master/YOLO_small.ckpt',
input_image='D:/Python/YOLOv1-Tensorflow-master/car.jpg')
以上的代码我已经注释的比较详细,慢慢看还是容易懂的,结果如下:
由结果看还是很不错得,赶紧动手自己实验一次吧,欢迎大家批评指正。