本文将根据 Mask R-CNN 和 Faster R-CNN 论文以及 TensorFlow 实现的 目标检测 来详细的解析通过 R-CNN 方法来进行目标检测的原理(使用 TensorFlow 训练目标检测器请参考 TensorFlow 训练自己的目标检测器)。
如下图所示,我们想要知道图像中有哪些目标(人、风筝等),知道了存在的目标之后,还想知道这些目标在图像中的哪个位置(左上角、右下角等)。知道这些信息,在实际中的某些任务上很有帮助,比如,在自动驾驶中,通过摄像头传回的图像,可以检测出前方是否存在行人、车辆等障碍物,而且可以计算出离他们的距离。目标检测的目的即是从一张给定的图像中检测出固定种类的目标以及定位这些目标在图像的位置(如图中的绿色边框、橙色边框)。因此,目标检测包含两个子任务:分类和定位,既要识别目标的类别,又要定位目标的位置。
上图充分说明了目标检测的难度。主要难度有:1.一张图像中的可识别的目标可能很多,很难在识别准确的基础上不漏掉其中一些目标;2.目标尺度变化很大,很难兼顾大尺度目标和小尺度目标;3.目标之间可能存在重叠、遮挡、形变等复杂关系。相比之下,下图的检测过程就容易很多,因为只包含一个目标,而且目标清晰,但即便如此,最后的定位(黑框)精度也不甚理想。
我们知道识别目标属于哪个类别是一个分类任务,最终只需要赋予这个目标一个类标号(0, 1, 2, 3, ...)或类名(人、车、猫、狗、......)即可。现在我们想知道的是:要怎么来表达定位结果?正如你看到(想到)的一样,用一个包含目标的边框来表达目标的定位位置,通常可以用边框的左上角、右下角的坐标,即 (ymin, xmin)、(ymax, xmax),或者中心坐标以及宽高,即 (xc, yc)、w、h 这两种方式来表示目标的位置(见上面两图)。
目标检测不同于分类任务的关键在于:所面临的图像中可识别的目标可能不止一个,而且还可以是零个,这就使得通常的分类网络不能直接应用于目标检测,但使用分类网络进行特征提取却是通用的技巧。当前,常用的目标检测算法包括:1.单阶段算法:YOLO 系列,SSD 系列;2.双阶段算法:R-CNN系列。本文主要讲解 R-CNN 系列,特别是 Faster R-CNN。
一、Faster R-CNN 思想
我们已经知道目标检测包含两个任务,分别是定位和分类。因为一张图像中可能包含 0 个或 多 个目标,因此目标检测与单纯的分类任务存在本质的区别。但如果先从图像中把所有的目标都单独分割出来,形成分割出来的图像中有且仅有一个目标,则此时就将原任务转化成了一个单纯的分类问题。这就是 R-CNN 论文最原始的思想。因为具有逻辑上的先后关系:先分割出目标,然后再对分割出的目标分类,因此 R-CNN 算法是一个两阶段的算法。此时,目标检测的核心问题在于:1.怎么将目标从图像中分割出来;2.怎么对分割的目标进行识别。因为 2 是一个单纯的分类问题,已经有很多成熟的算法,所以目标检测的关键问题是:怎么从图像中分割目标?
分割目标其实不是一件容易的事,原因有二:1.目标数量、尺度和位置都是任意变化的,分割要做到不遗漏,特别是要做到不加入无效的背景区域很困难。这里涉及到一个技术问题,即:从图像中分割出的一个区域要怎么判定它包含可识别的目标(即目标区域)或不包含目标(即背景区域)?一个下意识的想法也许是:这不是一个二分类任务吗?确实是一个二分类任务!把所有可识别的目标都泛化成一个大类,比如叫正类(类标号为 1),所有背景为另一个类,比如负类(类标号为 0),判断一个分割区域是否为目标区域就成了一个典型的二分类问题。分类问题都是简单的,因此目标检测的关键问题再次被简化,现在的关键问题是:怎么从图像中获取分割区域?这就引出了分割目标困难的第二个原因:2.要高效的对图像进行区域分割,也很困难,困难之处在于高效,即:在分割区域数量尽可能少的情况下,不遗漏所有可识别的目标。一个最原始的分割区域的方式是滑动窗口算法,几乎是以一种穷竭的方法把所有子区域都从图像中分割出来了,但是显然它不够高效。
R-CNN 最先使用的分割方法是选择搜索(Selective Search),基本思想是:先用图像分割的方法把图像分割成充分多的足够小的区域,然后根据相邻区域的相似性(包括颜色、纹理、尺寸、空间交叠等相似性度量)不断的合并小区域,直至所有的小区域合并成整张图像为止,记录下此过程中的所有中间结果(包括最开始的小区域),这些区域就是最终用于二分类判定是否是目标区域的数据集合。这个过程依旧很慢,但比起滑动窗口算法还是快很多。
现在可以来纵观一下上面所述的目标检测算法的整个流程了(如下图所示):对给定的一张图像,首先使用 选择搜索 算法分割出数量较多的子区域(包括 目标区域 和 背景区域),然后使用 CNN 二分类算法从所有子区域中分离出所有(疑似)目标区域,接着使用 CNN 对所有(疑似)目标区域做最后的分类,得到检测结果。你是否会问,以上过程只是处理了分类任务,那么定位任务呢?实际上,定位在分割这一步隐式的实现了,因为当你从原图像中分割一个矩形子区域时,势必已经知道这个矩形区域相对于原图像的像素位置(比如,你必须知道这个矩形区域的左上角,以及宽高,才能裁剪出这个区域)。(之所以带 【疑似】 两字,是因为分类算法都不是 100% 准确的)
以上过程实际上可以做一些简化,合并分割之后的 二分类 和 分类 这两步,就是 R-CNN 论文的整体检测框架。二分类这一步实际上是不需要的,因为就算做了这一步也无法保证所有判定的目标区域就都是真的目标区域(而不会完全不存在噪声的背景区域),因此后续的 分类 过程,还是必须引入除了 可识别目标类别 之外的一个额外类别:背景,还不如直接对所有分割出来的矩形区域进行 可识别目标类别 + 背景类别 分类。这样,R-CNN 算法的步骤为:1.从图像中分割矩形区域;2.对矩形区域分类(类别数量 = 可识别目标类别数量 + 1,多出来的一个类别为背景)。这里,之所以画蛇添足引入一个二分类过程,除了逻辑上自然之外,还因为后面重点讲解的 Faster R-CNN 算法中存在这一步。
接下来,我们从上述流程来分析一下 R-CNN 算法的缺点。以上分析所知,大体上,R-CNN 算法分两步:1.分割子区域;2.对子区域分类。基于此,明显的缺点如下:
- 不是端到端的模型。R-CNN 算法中,分割子区域使用的是传统的图像分割方法,所有子区域分割出来后,统一缩放到固定尺寸,然后使用卷积神经网络对这些区域进行分类。显然,以上两步的处理是割裂的,分别使用了两种不同的模型(算法),并不是一个端到端的模型(指从输入一步到位到最终的输出)。另外,第一步的传统图像分割方法是在 CPU 上进行的,非常耗时(相对于后续的卷积神经网络运行在 GPU 上),也是一个大缺陷。
- 需要存储中间结果。这个缺点是由第一个缺点直接导致的,因为前后步骤是割裂的,因此为了进行后续的操作,势必要存储前一阶段的结果,所造成的直接影响是必须要花费巨大的存储空间。
- 重复提取卷积特征。这是因为第一步分割出来的矩形区域,很多都是相互重叠的,即每个矩形区域都或多或少的与其他很多矩形区域存在共同的部分,当对它们进行分类时,这些公共部分被重复的送进卷积神经网络,从而相同的卷积特征被重复的提取,造成巨大的计算消耗。
聪明的读者想必已经想到,改进 R-CNN 可以从两点入手:1.改造 R-CNN 使其成为端到端的模型;2.减少,甚至避免特征被重复提取。按照 R-CNN 系列论文发表顺序来看,2 要比 1 简单,因为 2 对应的 Fast R-CNN 要比 1 对应的 Faster R-CNN 发表更早。
接下来来看 Fast R-CNN。Fast R-CNN 承袭了 R-CNN 的框架,旨在减少重复提取图像特征。R-CNN 特征提取重复的原因是直接把从图像中分割的区域传入卷积神经网络,这样当分割的区域超过一定数量之后(R-CNN 实际裁剪数量约为 2000),区域与区域之间的重叠程度会很严重,即同一区域被包含在很多分割区域内,直接导致同一区域的特征被不断的反复提取。问题的关键在于:R-CNN 在分割图像后,对所有子区域直接提取特征:即先从图像中分割子区域,然后将子区域送入 CNN 中,这样只要子区域与子区域有交集,这个交集就会先后两次进入 CNN 造成重复。解决的办法是:先对全图一次性提取特征,然后分割出各子区域对应的特征,即先将整张图像送入 CNN 中提取特征,然后根据子区域相对于全图像的位置来计算对应区域在全图像特征中的位置,直接从全图像特征中裁剪出子区域对应的特征,然后使用这个特征进行第二阶段的分类。
Fast R-CNN 在处理第二阶段的分类任务时显然更合理,计算量得到非常大的削减,计算速度得到非常大的提升。此时,因为分类的卷积神经网络是 GPU 上运行的,耗时极少,整个目标检测过程中时间基本消耗在第一阶段的图像分割上。为了解决耗时问题,一个很直接的方法是:将运行在 CPU 上的图像分割过程迁移到 GPU 上,这样只需要工程手段即可,但显然这不是研究人员想要的。