最近在听Andrew Ng讲解目标检测的视频,包括目标定位,特征点检测,卷积的滑动窗口的实现,Bounding Box预测,交并比,非极大值抑制,AnchorBoxes,YOLO算法以及候选区域,并通过查阅相关的资料,对以上内容有了初步的理解,趁热打铁,总结如下。
一、目标定位(Object Localization)
图片分类:图片分类问题已经不陌生了,例如,输入一张图片到多层卷积神经网络,它会输出一个特征向量,并反馈给softmax单元来预测图片的类型。
定位分类问题:不仅要用算法判断出图中是否有一辆汽车,而且还要标注汽车的位置,一般用边框(Bounding Box)来标注。通常只有一个较大的对象位于图片的中间位置,我们要对它进识别和定位。
目标分类(Object Classification) 目标定位(Object Localization) 目标检测(Object Detection)对象检测问题:图片可以含有多个对象 ,甚至单张图片会有多个不同的分类对象。因此图像分类的思路可以帮助我们学习对象定位,而对象定位的思路又可以帮助我们学习对象检测。
举个栗子(example):
如果你正在构建汽车自动驾驶系统,那么对象可以包括以下几类:行人、汽车、摩托车和背景,背景就是图片中没有行人、汽车、摩托车三类对象,输出结果便会是背景对象,这四个对象就是softmax函数可能输出的结果,这就是目标分类的过程。但是如果你还想定位图片中的位置,该怎么做呢?我们可以让神经网络多输出几个单元,输出一个边界框。具体说就是让神经网络再多输出 4 个数字,标记,,,这四个数字是被检测对象的边界框的参数化表示。
图片的左上角是(0,0),右下角是(1,1),要确定边界框的具体位置,需要指定红色方框的中心点,这个点表示为 ( ,),边界框的高度为,宽度为。因此训练集不仅包含神经网络要预测的对象分类标签,还要包含表示边界框的这四个数字,接着采用监督学习算法,输出一个分类标签,还有四个参数值,从而给出检测对象的边框位置。此例中, 的理想值是 0.5,大约是 0.7,约为 0.3,约为 0.4。
目标标签y的定义如下:
它是一个向量,第一个组件表示是否含有对象,如果对象属于前三类(行人,汽车,摩托车),则 = 1,如果是背景,则图片中没有要检测的对象,则 = 0,我们可以这样理解,它表示被检测对象属于某一分类的概率,背景分类除外。,,表示对象属于1-3类的哪一类,是行人,汽车,还是摩托车。神经网络中的损失函数,其参数为类别y和网络输出,如果采用平方误差策略,则:损失值等于每个元素相应差值的平方和。当 = 1时,平方误差策略可以减少这 8 个元素预测值和实际输出结果之间差值的平方。当时,y矩阵中的后 7 个元素都不用考虑,只需要考虑神经网络评估y1y1(即pcpc)的准确度。
实际应用中,你可以对,,和 softmax 激活函数应用对数损失函数,并输出其中一个元素值,通常做法是对边界框坐标应用平方差或类似方法,对应用逻辑回归函数,甚至采用平方预测误差也是可以的。
二、特征点检测
上面,我们讲了如何利用神经网络进行对象定位,即通过输出四个参数值,,和给出图片中对象的边界框。更概括地说,神经网络可以通过输出图片上特征点的(x,y)坐标来实现对目标特征的识别,我们看几个例子。
假设你正在构建一个人脸识别应用,出于某种原因,你希望算法可以给出眼角的具体位置。眼角坐标为(x,y),你可以让神经网络的最后一层多输出两个数字和,作为眼角的坐标值。如果你想知道两只眼睛的四个眼角的具体位置,那么从左到右,依次用四个特征点来表示这四个眼角。对神经网络稍做些修改,输出第一个特征点(,)第二个特征点(,),依此类推,这四个脸部特征点的位置就可以通过神经网络输出了。
也许除了这四个特征点,你还想得到更多的特征点输出值,这些(图中眼眶上的红色特征点)都是眼睛的特征点,你还可以根据嘴部的关键点输出值来确定嘴的形状,从而判断人物是在微笑还是皱眉,也可以提取鼻子周围的关键特征点。为了便于说明,你可以设定特征点的个数,假设脸部有 64 个特征点,有些点甚至可以帮助你定义脸部轮廓或下颌轮廓。选定特征点个数,并生成包含这些特征点的标签训练集,然后利用神经网络输出脸部关键特征点的位置。
具体做法是,准备一个卷积网络和一些特征集,将人脸图片输入卷积网络,输出1或0,1表示有人脸,0表示没有人脸,然后输出(,) ……直到(,)。这里我用l代表一个特征,这里有 129个输出单元,其中1表示图片中有人脸,因为有 64个特征,64×2=128,所以最终输出 128+1=129 个单元,由此实现对图片的人脸检测和定位。这只是一个识别脸部表情的基本构造模块,如果你玩过 Snapchat 或其它娱乐类应用,你应该对 AR(增强现实)过滤器多少有些了解, Snapchat 过滤器实现了在脸上画皇冠和其他一些特殊效果。检测脸部特征也是计算机图形效果的一个关键构造模块,比如实现脸部扭曲,头戴皇冠等等。当然为了构建这样的网络,你需要准备一个标签训练集,也就是图片和标签的集合,这些点都是人为辛苦标注的。
最后一个例子,如果你对人体姿态检测感兴趣,你还可以定义一些关键特征点,如胸部的中点,左肩,左肘,腰等等。然后通过神经网络标注人物姿态的关键特征点,再输出这些标注过的特征点,就相当于输出了人物的姿态动作。当然,要实现这个功能,你需要设定这些关键特征点,从胸部中心点(,)一直往下,直到(,)。
三、目标检测(多个目标)
学过了对象定位和特征点检测,我们来构建一个对象检测算法。我们将学习如何通过卷积网络进行对象检测,采用的是基于滑动窗口的目标检测算法。
假如你想构建一个汽车检测算法,步骤是,首先创建一个标签训练集,也就是x和y表示适当剪切的汽车图片样本,这张图片(编号 1)x是一个正样本. ,因为它是一辆汽车图片,这几张图片(编号 2、 3)也有汽车,但这两张(编号 4、 5)没有汽车。出于我们对这个训练集的期望,你一开始可以使用适当剪切的图片,就是整张图片几乎都被汽车占据,你可以照张照片,然后剪切,剪掉汽车以外的部分,使汽车居于中间位置,并基本占据整张图片。有了这个标签训练集,你就可以开始训练卷积网络了,输入这些适当剪切过的图片(编号 6),卷积网络输出y, 0 或 1 表示图片中有汽车或没有汽车。训练完这个卷积网络,就可以用它来实现滑动窗口目标检测,具体步骤如下:
假设这是一张测试图片,首先选定一个特定大小的窗口,比如图片下方这个窗口,将这个红色小方块输入卷积神经网络,卷积网络开始进行预测,即判断红色方框内有没有汽车。
滑动窗口目标检测算法接下来会继续处理第二个图像,即红色方框稍向右滑动之后的区域,并输入给卷积网络,因此输入给卷积网络的只有红色方框内的区域,再次运行卷积网络,然后处理第三个图像,依次重复操作,直到这个窗口滑过图像的每一个角落,这就是所谓的图像滑动窗口操作。
注意:滑动窗口目标检测算法也有很明显的缺点,就是计算成本,因为你在图片中剪切出太多小方块,卷积网络要一个个地处理。如果你选用的步幅很大,显然会减少输入卷积网络的窗口个数,但是粗糙间隔尺寸可能会影响性能。反之,如果采用小粒度或小步幅,传递给卷积网络的小窗口会特别多,这意味着超高的计算成本。
在上述我们已经说过滑动窗口目标检测算法也有很明显的缺点,如何解决这个算法的缺点呢?在卷积层上应用滑动窗口目标检测算法可以明显的提高这个算法的效率。为了构造滑动窗口的卷积应用,首先要知道如何把神经网络的全连接层转化为卷积层。下图可以直观的解释全连接层向卷积层转化的过程。
第一部分(上面那部分)是全连接层的过程,第二部分(下面那部分)是全连接层向卷积层的转化 。下面举一个例子,解释一下在卷积层上应用滑动窗口检测的算法过程,如下图所示。
在上图中假设图像大小为16×16×3,窗口的大小为14×14×3,假设步长为2,那么窗口要经过4次才能遍历整个图像,也就是在卷积层里进行4次运算,这样也会使算法的效率很低。我们的解决办法是,直接将整个图像经过卷积层、池化层这样1次操作,在上图中可以看出,最后输出的2×2×4的结果便是经过4次的结果,例如2×2×4图像的左上角区域,对应着图像的左上角的滑动窗口。
四、Bounding Box预测
在上述的在卷积层应用窗口检测算法虽然效率很高,但是其输出的边框的精准度不是很高(如下图展示,不能很精准的输出汽车的边框),如何才能使输出的边框精准度变高呢?用YOLO算法(You Only Look Once)便可解决这个问题。
YOLO 算法做的就是,将图像分成n×n的网格(下图是分成3×3的网格),取两个对象的中点,然后将这个对象分配给包含对象中点的格子。所以左边的汽车就分配到绿色框标记的格子上,右边的汽车就分配到黄色框标记的格子上。即使中心格子同时有两辆车的一部分,我们就假装中心格子没有任何我们感兴趣的对象。
五、交并比
如评价目标检测算法的好坏呢?这里便可引入交并比。
在上图中假设红色区域是真实的汽车边框,蓝色区域是应用目标检测算法所检测的边框,那么交并比(LoU)就可以如上图所表示,即黄色区域 /绿色区域 。如果LoU>=0.5就可以认定所检测输出的边框合格,这里的0.5称为阀门(人工定义的),也可以是0.6, 0.7等等。
六、非极大值抑制
到目前为止我们学习到的目标检测算法仍然存在一个问题,就是当我们使用目标检测算法时,我们对同一个对象可能检验多次,如同下面这个图所示,同一对象可能检测出多次。
我们分步介绍一下非极大值抑制是怎么起效的,因为你要在 361 个格子上都运行一次图像检测和定位算法,那么可能很多格子都会举手说我的Pc ,我这个格子里有车的概率很高,而不是 361 个格子中仅有两个格子会报告它们检测出一个对象。所以当你运行算法的时候,最后可能会对同一个对象做出多次检测,所以非极大值抑制做的就是清理这些检测结果。这样一辆车只检测一次,而不是每辆车都触发多次检测。非极大值抑制可以确保我们对一个对象只检验一次,非极大值抑制算法的具体细节如下图所示。
非极大值抑制算法步骤:
1、将所有的输出的进行排序,选出最高的及其对应输出框。
2、遍历其余的输出框,如果和当前最高的输出框的重叠面积(IOU)大于一定阈值,我们就将框删除。
3、从未处理的输出框中继续选一个最高的,重复上述过程。
七、Anchor Boxes
到目前为止我们所熟悉的目标检测算法每个格子只能检测出一个对象,如果想让一个格子检测出多个对象,那么便可引出Anchor Boxes这个概念,举个栗子:
对于上面那个图像,我们将它分割为3×3,那么可以清楚的看到,人的中点和车的中点落到了同一个格子里,如果按照上述的目标检测算法,便不知道要输出哪个结果,怎么解决这个问题呢?
此时我们可以预先定义两个不同形状的Anchor Box ,并把预测结果与这两个Anchor Box相关联,此时的y可以定义如下:
前8个的结果可以与Anchor Box1相关联,后8个的结果可以与Anchor Box2相关联。下图是无Anchor Box和有Anchor Box的对比过程。
八、YOLO算法
由上面所学习到的知识组装起来构成YOLO对象检测算法。
假设我们要在图片中检测三种目标:行人、汽车和摩托车,同时使用两种不同的Anchor box。
(1)训练集:
- 输入X:同样大小的完整图片;
- 目标Y:使用
- 对不同格子中的小图,定义目标输出向量Y。
(2)模型预测:
输入与训练集中相同大小的图片,同时得到每个格子中不同的输出结果:
(3)运行非最大值抑制(NMS):
九、python实现YOLO目标检测
我们将在这篇博客使用在COCO数据集上预训练好的YOLOv3模型。COCO 数据集包含80类,有people (人),bicycle(自行车),car(汽车)......,详细类别可查看链接:https://github.com/pjreddie/darknet/blob/master/data/coco.names。
# -*- coding: utf-8 -*-
# 载入所需库
import cv2
import numpy as np
import os
import time
def yolo_detect(pathIn='',
pathOut=None,
label_path='./cfg/coco.names',
config_path='./cfg/yolov3.cfg',
weights_path='./cfg/yolov3.weights',
confidence_thre=0.5,
nms_thre=0.3,
jpg_quality=80):
'''
pathIn:原始图片的路径
pathOut:结果图片的路径
label_path:类别标签文件的路径
config_path:模型配置文件的路径
weights_path:模型权重文件的路径
confidence_thre:0-1,置信度(概率/打分)阈值,即保留概率大于这个值的边界框,默认为0.5
nms_thre:非极大值抑制的阈值,默认为0.3
jpg_quality:设定输出图片的质量,范围为0到100,默认为80,越大质量越好
'''
# 加载类别标签文件
LABELS = open(label_path).read().strip().split("\n")
nclass = len(LABELS)
# 为每个类别的边界框随机匹配相应颜色
np.random.seed(42)
COLORS = np.random.randint(0, 255, size=(nclass, 3), dtype='uint8')
# 载入图片并获取其维度
base_path = os.path.basename(pathIn)
img = cv2.imread(pathIn)
(H, W) = img.shape[:2]
# 加载模型配置和权重文件
print('从硬盘加载YOLO......')
net = cv2.dnn.readNetFromDarknet(config_path, weights_path)
# 获取YOLO输出层的名字
ln = net.getLayerNames()
ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()]
# 将图片构建成一个blob,设置图片尺寸,然后执行一次
# YOLO前馈网络计算,最终获取边界框和相应概率
blob = cv2.dnn.blobFromImage(img, 1 / 255.0, (416, 416), swapRB=True, crop=False)
net.setInput(blob)
start = time.time()
layerOutputs = net.forward(ln)
end = time.time()
# 显示预测所花费时间
print('YOLO模型花费 {:.2f} 秒来预测一张图片'.format(end - start))
# 初始化边界框,置信度(概率)以及类别
boxes = []
confidences = []
classIDs = []
# 迭代每个输出层,总共三个
for output in layerOutputs:
# 迭代每个检测
for detection in output:
# 提取类别ID和置信度
scores = detection[5:]
classID = np.argmax(scores)
confidence = scores[classID]
# 只保留置信度大于某值的边界框
if confidence > confidence_thre:
# 将边界框的坐标还原至与原图片相匹配,记住YOLO返回的是
# 边界框的中心坐标以及边界框的宽度和高度
box = detection[0:4] * np.array([W, H, W, H])
(centerX, centerY, width, height) = box.astype("int")
# 计算边界框的左上角位置
x = int(centerX - (width / 2))
y = int(centerY - (height / 2))
# 更新边界框,置信度(概率)以及类别
boxes.append([x, y, int(width), int(height)])
confidences.append(float(confidence))
classIDs.append(classID)
# 使用非极大值抑制方法抑制弱、重叠边界框
idxs = cv2.dnn.NMSBoxes(boxes, confidences, confidence_thre, nms_thre)
# 确保至少一个边界框
if len(idxs) > 0:
# 迭代每个边界框
for i in idxs.flatten():
# 提取边界框的坐标
(x, y) = (boxes[i][0], boxes[i][1])
(w, h) = (boxes[i][2], boxes[i][3])
# 绘制边界框以及在左上角添加类别标签和置信度
color = [int(c) for c in COLORS[classIDs[i]]]
cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)
text = '{}: {:.3f}'.format(LABELS[classIDs[i]], confidences[i])
(text_w, text_h), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)
cv2.rectangle(img, (x, y - text_h - baseline), (x + text_w, y), color, -1)
cv2.putText(img, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2)
# 输出结果图片
if pathOut is None:
cv2.imwrite('with_box_' + base_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), jpg_quality])
else:
cv2.imwrite(pathOut, img, [int(cv2.IMWRITE_JPEG_QUALITY), jpg_quality])
pathIn = '../image/yolo_test1.jpg'
pathOut = '../out/yolo_test1.jpg'
yolo_detect(pathIn,pathOut)
测试一:
原图:
经过YOLO目标检测之后result:
测试二:
原图:
经过YOLO目标检测之后result:
十、 候选区域(region proposals)
R-CNN(Regions with convolutional networks),会在我们的图片中选出一些目标的候选区域,从而避免了传统滑动窗口在大量无对象区域的无用运算。所以在使用了R-CNN后,我们不会再针对每个滑动窗口运算检测算法,而是只选择一些候选区域的窗口,在少数的窗口上运行卷积网络。
具体实现:运用图像分割算法,将图片分割成许多不同颜色的色块,然后在这些色块上放置窗口,将窗口中的内容输入网络,从而减小需要处理的窗口数量。
更快的算法:
(1)R-CNN:给出候选区域,对每个候选区域进行分类识别,输出对象 标签 和 bounding box,从而在确实存在对象的区域得到更精确的边界框,但速度慢;
(2)Fast R-CNN:给出候选区域,使用滑动窗口的卷积实现去分类所有的候选区域,但得到候选区的聚类步骤仍然非常慢;
(3)Faster R-CNN:使用卷积网络给出候选区域。
以上是我对吴恩达讲解目标检测知识的总结,如果有什么理解不到位的地方,还请指点改正