工作流

RCNN

RCNN(region-based convolutional network)的工作流如上图所示,下面我们将逐一介绍各个步骤。

Selective search

Selective Search 算法来自于 Selective Search for Object Recognition (IJCV 2013),其大致原理为先采取过分割的手段,将图像分割成小区域,再通过规则进行合并,最后生成约 2000 个候选框。选择合并区域的主要规则如下:

  • 选择 颜色(颜色直方图)相近的两个区域;
  • 选择 纹理(梯度直方图)相近的两个区域;
  • 选择合并后 总面积较小 的两个区域,防止某个大区域不断吞并其他区域;
  • 选择合并后 总面积占其 Bounding Box 的比例较大 的两个区域,尽量保证合并后的状态规则。

RCNN 正是使用了该算法为每张图片都提取约 2000 个候选框(Region Proposal),此时不考虑类别。

2. 预处理

Bounding box transformation for RCNN]

由于 AlexNet 接收的输入大小为 $227 \times 227$,因此在这里需要将长宽不一的矩形候选框都转换为该形状。作者提出了 6 种具体方法,如上图所示,介绍如下:

  • 遇到图像边界时,都以候选框像素的均值填充。
  • 上面一行表示不带 context padding,下面一行表示带 $16$ 个像素的 context padding;
  • 第一列:使用能包住提取的候选框的最小正方形,然后各向同性变形;
  • 第二列:使用提取的候选框,然后各向同性变形;
  • 第三列:使用提取的候选框,然后各向异性变形。

作者的实验结果显示效果最好的是上图的第二行第三列,即 context padding 为 16 的各向异性变形。然后再将每个候选框进行 0 均值操作。

3. 特征提取(CNN)

  • 使用 AlexNet 提取每个候选框的特征,得到 $4096$ 维向量($pool_5$ 层),concat 起来得到 $2000 \times 4096$ 维特征;
  • 保存提取的特征到硬盘,用于后面的 BBR。

4. 分类(SVM)

  • 假设一共有 $N$ 个目标类别,那么使用提前训练好的 $N$ 个二分类 SVM,每个 SVM 的参数为 $4096 \times 1$。
  • 将 $2000 \times 4096$ 维特征与 $4096 \times N$ 维 SVM 参数相乘,获得 $2000 \times N$ 维类别分数。

5. 非极大值抑制(NMS)

NMS

如果直接用 $2000 \times N$ 维进行后续操作,一方面候选框太多,后续计算量太大;另一方面,很有可能同一个物体会被多个相差不大的候选框框中,导致后面的大量的重复预测。因此非极大值抑制(non-maximum suppression,NMS)被用于压制覆盖同一个区域的多个相近的候选框中的非极大值,在每个类别都要执行一次

一般使用的 NMS 有两种,分别是 Hard-NMSSoft-NMS

Hard-NMS

Hard-NMS 的步骤如下(对于每个类别):

  1. 将 2000 个候选框按照该类别的 score 排序;
  2. 选择 score 最大的候选框,计算它与其他所有候选框的 IoU(intersection over union),舍弃所有 IoU 小于某个预定义好的阈值的候选框;
  3. 然后从剩余的候选框中,挑选 score 最大的候选框,重复进行第 2 步。

PyTorch 实现 Hard-NMS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def nms(self, bboxes, scores, thresh=0.5):

    x1 = bboxes[:,0]
    y1 = bboxes[:,1]
    x2 = bboxes[:,2]
    y2 = bboxes[:,3]
    areas = (x2-x1+1)*(y2-y1+1)
    _, order = scores.sort(0, descending=True)
    keep = []

    while order.numel() > 0:
        if order.numel() == 1:
            i = order.item()
            keep.append(i)
            break
        else:
            i = order[0].item()
            keep.append(i)
        xx1 = x1[order[1:]].clamp(min=x1[i])
        yy1 = y1[order[1:]].clamp(min=y1[i])
        xx2 = x2[order[1:]].clamp(max=x2[i])
        yy2 = y2[order[1:]].clamp(max=y2[i])
        inter = (xx2-xx1).clamp(min=0) * (yy2-yy1).clamp(min=0)
        iou = inter / (areas[i]+areas[order[1:]]-inter)
        idx = (iou <= threshold).nonzero().squeeze()
        if idx.numel() == 0:
            break
        order = order[idx+1]

    return torch.LongTensor(keep)

Soft-NMS

Hard-NMS 存在一些问题,如果相似的目标离得较近,就直接被没有任何余地地干掉了,因此:

  • 得分高的不一定更准,直接干掉太武断了;
  • 物体较为密集时,召回率不行;
  • 难以确定˙阈值。

因此后来 Soft-NMS (ICCV 2017)) 提出了不能粗暴地干掉 score 较小的候选框,而是根据 IoU 的多少降低其 score,具体来说就是从 $s_i = 0$ 改成了 $$ s_i = s_i \times (1 - \text{IoU}) $$

两者的 IoU 越大,socre 就降得越狠,但是不会完全抛弃这个 score 较小的候选框。

有关 NMS 的更新进展,可以参考 SFXiang:攻克目标检测难点秘籍二,非极大值抑制与回归损失优化之路,很强。

6. 边界框回归(BBR)

这一步使用 $N$ 个回归器(regressor)对剩下的所有候选框进行边界框回归(bounding box regression,BBR)。

假设某个候选框的中心坐标为 $(x, y)$,宽高分别为 $w$、$h$,提取的特征($4096$ 维)为 $\theta$,那么回归器设计为:

$$ \begin{cases} t_x = W_x^T \theta + b_x \\ t_y = W_y^T \theta + b_y \\ t_w = W_w^T \theta + b_w \\ t_h = W_h^T \theta + b_h \\ \end{cases} $$

变换之后的候选框为:

$$ \begin{cases} \hat{x} = wt_x + x \\ \hat{y} = ht_y + y \\ \hat{w} = w \exp(t_w) \\ \hat{h} = h \exp(t_h) \\ \end{cases} $$

这里要注意一下为什么这么设计:

  • 为什么 $t_x$、$t_y$ 要乘上原来的宽高再加中心坐标?

    首先偏移量与当前的中心坐标无关,然后为了尺度不变性,所以乘上原来的宽高。

  • 为什么 $t_w$、$t_h$ 要做指数操作再成原宽高?

    为了保证宽高乘的系数是正数。

训练目标如下:

$$ \hat{W} = \underset{W}{\arg\min}\sum_i^M(t^{(i)} - W^T\theta)^2 + \lambda||W||^2 $$

注意,这里的回归器是类别无关的,即所有类别共用同样的回归器。

训练过程

  1. 预训练 AlexNet 分类器。
  2. 微调 AlexNet:
    • 预处理方式与 RCNN 一样;
    • 分类器正负的 IoU 阈值为 $0.5$;
    • 每个 batch 包括 32 个 positive、96 个 negative
    • 最后使用 $(N + 1)$ 路的 Softmax,包括目标检测的 $N$ 个类别和一个 background 类别(VOC 中 $N = 20$,ILSVRC 中 $N = 200$)。
  3. 训练 SVM 分类器:
    • 使用 $4096$ 维输入(AlexNet 的 $pool_5$ 层输出);
    • 正样本使用 ground truth,负样本以 IoU 阈值 $0.3$ 来构造;
    • 使用难例挖掘(Hard mining)技巧
  4. 训练 BBR 回归器。

优缺点

  • (+) 第一次用 CNN 来做目标检测,引入了 Selective Search 来提取候选框,将目标检测问题转换成了分类问题,并在最后加上回归器实现更精准的回归。
  • (-) 不能端到端地训练,必须分成多个阶段(multi-stage)分别训练,包括特征提取器(CNN)、候选框分类器(SVM)、边界框回归器(regressor)三个子模型。
  • (-) 需要保存中间提取的特征,用于边界框回归(即同一份特征要需要经过 SVM 分类,也要经过 BBR 回归,二者是割裂的)。
  • (-) 速度很慢,具体原因包括几个方面:
    • Selective search 算法比较慢;
    • 由于对每个候选区域都做特征提取,某些区域的特征提取被重复多次,存在大量的重复运算。

代码片段

IoU

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def compute_iou(box1, box2):
    """计算 IoU。

    Args:
        box1 (list): 第一个框的左上点坐标、右下点坐标,具体来说是 [top, left, bottom, right],注意坐标轴从左上开始。
        box2 (list): 第二个框的左上点坐标、右下点坐标,具体来说是 [top, left, bottom, right]。

    Returns:
        iou (float): 两个框的 IoU。
    """
    box1 = [int(x) for x in box1]
    box2 = [int(x) for x in box2]

    # Area of intersect
    top_left_x = max(box1[0], box2[0])
    top_left_y = max(box1[1], box2[1])
    bottom_right_x = min(box1[2], box2[2])
    bottom_right_y = min(box1[3], box2[3])
    area_inter = max(0, bottom_right_x - top_left_x + 1) * max(0, bottom_right_y - top_left_y + 1)

    # Area of 2 boxes
    area_box1 = (box1[2] - box1[0] + 1) * (box1[3] - box1[1] + 1)
    area_box2 = (box2[2] - box2[0] + 1) * (box2[3] - box2[1] + 1)

    # Area of union
    area_union = float(area_box1 + area_box2 - area_inter)

    # IoU
    iou = area_inter / area_union

    return iou

参考