import cv2
import cv2.ximgproc.segmentation as seg
import matplotlib.pyplot as plt
%config InlineBackend.figure_format = 'retina'11.1 Region-based CNN (R-CNN)
在前面的章节中,我们已经看到,卷积神经网络在图像分类任务上取得了非常好的效果。给定一张图片,CNN 可以判断这张图里是不是有猫、狗或者汽车。但在很多实际任务中,我们关心的并不是图里有没有某种物体,而是物体在哪里。
例如,在自动驾驶中,系统不仅要知道图像里有行人和车辆,还需要知道它们在图像中的具体位置;在医学影像中,我们不仅要判断是否存在病灶,还需要定位病灶的区域。这类问题被称为 目标检测(Object Detection)。
和图像分类相比,目标检测多了一个关键难点:模型不仅要预测类别,还要预测位置。这意味着,一个简单的分类网络已经不够用了。因为 CNN 通常只接收整张图像作为输入,并输出一个类别概率,它并不会告诉我们目标出现在图像的哪个位置。
那么,一个很直接的想法是:既然 CNN 擅长做分类,我们能不能把检测问题转化成很多次分类问题?我们可以在图像中选取许多可能包含物体的区域,把每个区域裁剪出来,然后分别送进 CNN 做分类。如果某个区域被分类为“汽车”,那就说明汽车可能出现在这个位置。
这个思路听起来非常简单,但它其实正是早期深度学习目标检测方法的核心思想。R-CNN (Region-based Convolutional Neural Network) (Girshick et al. 2014) 就是第一个系统地实现这一思路的模型。它的基本流程可以概括为三步:
- 在图像中生成一系列候选区域(Region Proposals);
- 将每个候选区域裁剪并送入 CNN 提取特征;
- 使用分类器判断该区域属于哪一类物体,并输出对应的边界框。
在接下来的内容中,我们将详细介绍 R-CNN 是如何把目标检测问题拆解成一系列分类问题的,以及这种方法为什么既有效,又存在明显的效率问题。
11.1.1 候选区域生成
在开篇,我们提出了一个直观的思路:既然 CNN 擅长做分类,我们能不能在图像中选出很多可能包含物体的区域,然后把这些区域分别送入 CNN 做分类?
这个思路看起来简单,但如果我们真的去实现,很快就会遇到一个非常现实的问题:这些候选区域从哪里来?
最直接的方法是 滑动窗口(Sliding Window)。我们可以在图像上用不同大小的窗口不断滑动,每一个窗口都当作一个候选区域。例如,我们在整张图像上从左到右、从上到下扫描,使用不同尺寸的窗口(比如 64x64、128x128、256x256 等),每个窗口都被视为一个候选区域。然后,我们把每个窗口都裁剪出来,送入 CNN 做一次分类。这个方法的优点是简单直接,但问题也非常明显:窗口数量会非常多。
假设一张图像大小为 1000x1000,如果我们在多个尺度上进行密集滑动,很容易就会产生几十万甚至上百万个候选窗口。如果每一个窗口都要单独跑一次 CNN,那么计算成本几乎是不可接受的。更重要的是,大部分窗口其实都是背景区域,并不包含任何目标。也就是说,我们花了大量计算,只是在重复判断这里没有物体。
因此,一个更合理的问题是:我们能不能先找出可能包含物体的区域,再让 CNN 去判断它们的类别?这就是 候选区域生成(Region Proposals) 的核心思想。
Region Proposal 的目标不是直接识别物体,而是从整张图像中找出一小部分可能包含物体的区域。理想情况下,这些区域应该满足两个条件:
- 高召回率:候选区域应该尽可能覆盖所有真实的目标。也就是说,我们不希望漏掉任何一个真正的物体。
- 低数量:候选区域的数量应该尽可能少,以减少后续分类的计算负担。
在 R-CNN 中,作者采用了一种经典的算法:选择性搜索(Selective Search)。
Selective Search (Uijlings et al. 2013) 认为,物体通常由一组相似的像素组成。如果我们能够根据颜色、纹理等信息,把相似区域逐步合并,就有可能得到对应物体的区域。Selective Search 首先用图像分割算法把图像切分成许多小区域,我们称为超像素(Superpixels)。然后根据颜色、纹理、大小和形状等特征,逐步合并这些超像素,形成一系列候选区域。在整个合并过程中,每一次生成的区域都会被记录下来,作为一个 Region Proposal。最终,我们通常会得到大约 2000 个候选区域。相比滑动窗口产生的几十万个候选框,这已经是一个大幅的减少。
image = cv2.imread('figures/bus.jpg')
assert image is not None, 'Image not found'
H, W = image.shape[:2]
img_area = H * W
ss = seg.createSelectiveSearchSegmentation()
ss.setBaseImage(image)
ss.switchToSelectiveSearchFast()
rects = ss.process()
print('Total proposals:', len(rects))
filtered = []
for x, y, w, h in rects:
area = w * h
if 0.01 * img_area <= area <= 0.5 * img_area:
filtered.append((x, y, w, h))
print('Filtered proposals:', len(filtered))
output = image.copy()
for x, y, w, h in filtered[:100]:
cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), thickness=1)
output = cv2.cvtColor(output, cv2.COLOR_BGR2RGB)
fig = plt.figure(1, figsize=(6, 6))
ax = fig.add_subplot(1, 1, 1)
ax.imshow(output)
ax.set_title('Top 100 Proposals')
ax.axis('off')
plt.show()Total proposals: 9340
Filtered proposals: 2607

利用 Selective Search 算法,我们成功把候选框数量降低到约 2600 个。同时,通过图片中框的位置,我们可以看到这些候选框已经覆盖了图像中的大部分物体区域。下方人行道背景区域的候选框较少,而行人、汽车、背景楼房的窗户等目标区域的候选框较多,这说明 Selective Search 在生成候选区域时,确实能够更好地覆盖潜在的目标。
所以,Selective Search 相当于一个“粗筛选”步骤,它负责在整张图像中找到可能存在物体的位置,而 CNN 则负责完成真正的识别任务。当然,这种设计也带来了一个新的问题:每一个候选区域都需要单独跑一次 CNN。当候选区域达到 2000 个时,这仍然意味着 2000 次 CNN 前向计算。这也是 R-CNN 最大的效率瓶颈之一。
在了解了 Selective Search 算法之后,我们来看看 R-CNN 的完整流程。
11.1.2 R-CNN 的整体流程
有了候选区域之后,我们就可以把目标检测问题转化成一系列分类问题。那么,一个完整的 R-CNN 模型到底是如何工作的?
R-CNN 的核心思想其实非常直接:先找区域,再做分类。
整个流程可以分为四个主要阶段:
- 生成候选区域;
- 使用 CNN 提取区域特征;
- 使用分类器和边界框回归进行预测;
- 非极大值抑制(Non-Maximum Suppression)去除重复检测。
下面我们逐步来看。
1. 生成候选区域
首先,对输入图像运行 Selective Search,生成大约 2000 个候选区域。这些区域并不一定精确对应物体,但它们很可能覆盖图像中的目标。每一个候选区域通常用一个 边界框(Bounding Box) 表示:
\[ (x, y, w, h) \]
其中,\((x, y)\) 是区域的左上角坐标,\(w\) 和 \(h\) 分别是区域的宽度和高度。
接下来,我们把这些区域从原始图像中裁剪出来,经过预处理(例如缩放到固定大小),然后送入 CNN 进行特征提取。
2. CNN 特征提取
CNN 通常要求输入图像具有固定大小,而候选区域的尺寸各不相同。因此,R-CNN 会先把每个区域缩放到统一尺寸(例如 224x224),然后送入一个预训练的 CNN(例如 AlexNet)。
需要注意的是,这个 CNN 是作为一个特征提取器使用的。也就是说,我们并不直接使用 CNN 的输出类别概率,而是取 CNN 的某一层(通常是全连接层)的输出作为该区域的特征表示。这些特征向量可以看作是对该区域内容的高层次抽象描述。
3. 分类器和边界框回归
得到特征向量之后,R-CNN 会对每个区域做两件事情:分类和边界框回归。
对于分类,R-CNN 使用 SVM 分类器来判断区域属于哪个类别。如果一个区域被分类为某个物体类别,那么就认为这个区域包含该物体。在原始 R-CNN 论文中,作者使用 CNN 来提取特征,而使用 SVM 进行分类,这意味着整个训练流程并不是端到端的。
至于为什么要用边界框回归呢?因为候选区域通常并不完全准确地覆盖目标物体。例如,框可能稍微偏左,也可能略大了一点。因此,R-CNN 会训练一个边界框回归器来微调预测框的位置。回归器的输入是 CNN 提取的特征,输出是对边界框的调整量,例如 \((\Delta x, \Delta y, \Delta w, \Delta h)\),用来修正原始候选框的位置和大小,使其更准确地覆盖目标物体。
相信你可能会有一个疑问,为什么 R-CNN 不直接使用 CNN 的 softmax 输出做分类,而是额外训练一个 SVM?因为在当时,作者尝试直接用 softmax 进行分类,但在 VOC 2007 上 mAP 从 54.2% 降到 50.9%,所以他们保留了 SVM 作为分类器。但是现在,现在随着深度学习的不断发展,端到端训练的检测模型(例如 Faster R-CNN)已经成为主流,直接使用 CNN 的分类头来做分类已经非常普遍了。
4. 非极大值抑制
最后,我们还需要解决一个问题:同一个物体往往会对应多个高分候选框。例如,一辆车周围可能有好几个候选框,它们都和真实目标有较大重叠,而且都被分类器判为“car”。如果我们把这些框全部保留下来,最终输出中就会出现很多重复检测结果。非极大值抑制(Non-Maximum Suppression, NMS) 就是用来解决这个问题的。
NMS 的基本思路很简单:对于同一类别的所有预测框,我们先按置信度从高到低排序,选择分数最高的那个框;然后计算它与其他框的重叠程度,通常用 交并比(Intersection over Union, IoU) 来衡量。IoU 是两个框的相交面积与相并面积之比,IoU 越高,说明两个框的重叠程度越高。如果某些框和当前最高分框的 IoU 超过设定阈值,就认为它们实际上检测的是同一个物体,于是将这些重叠框删除。接着,再从剩下的框中继续选择分数最高的框,重复这个过程,直到没有多余框为止。
这样做的结果就是,对于同一个目标,我们最终只保留一个最有代表性的预测框,从而让检测结果更干净,也更符合直觉。
11.1.3 R-CNN 的伪代码
下面我们用伪代码来总结一下 R-CNN 的整体流程,来更清晰地理解每个步骤是如何衔接在一起的。
R-CNN peudo-code
# R-CNN inference pipeline
image = load_image()
# Step 1: generate region proposals
proposals = selective_search(image)
results = []
for region in proposals:
# Step 2: warp region to fixed size
warped_region = warp(region, size=(227, 227))
# Step 3: extract CNN features
feature = cnn_extractor(warped_region)
# Step 4: classify with class-specific SVMs
scores = svm_classifiers(feature)
# Step 5: refine box with bounding-box regression
refined_box = bbox_regressor(feature, region)
results.append((refined_box, scores))
# Step 6: remove duplicate detections
final_detections = nms_filter(results)总的来说,R-CNN 的步骤分为两大块:第一块是生成候选区域并提取特征,第二块是分类和边界框回归。由于现在几乎没有人会再使用 R-CNN 了,所以我们就不深入展开每个部分的细节。感兴趣的读者可以参考原始论文 (Girshick et al. 2014),里面有非常详细的实现细节和实验结果分析。
11.1.3 R-CNN 的关键问题
R-CNN 的提出是目标检测领域的重要突破,因为它第一次成功地把 深度学习引入目标检测。但是,它也有一个非常明显的问题:计算效率非常低。
我们来算一笔账:假设一张图像上有 2000 个候选区域,每个区域都要送入 CNN 做一次前向计算。如果我们使用一个比较轻量级的 CNN(例如 AlexNet),每次前向计算可能需要 10ms,那么单张图像的处理时间就达到了 20 秒!这对于实际应用来说是完全不可接受的。更重要的是,R-CNN 的训练过程也非常复杂。因为它使用了 SVM 作为分类器,所以需要先训练 CNN 提取特征,然后再用这些特征训练 SVM,这个过程并不是端到端的,训练起来也比较麻烦。
在下一节中,我们将看到 Fast R-CNN 是如何解决 R-CNN 中的重复计算问题,并且实现端到端训练的。