物体检测比图像分类的难度大得多,过程也复杂了许多。所以希望自己能将自己的学习过程记录下来,总结过程中也许会有不一样的体会。

边界框

  目标检测中,通常不止需要我们识别出物体的类别,还需要我们检测出物体的具体位置,所以我们常用边界框来描述物体的具体位置,具体来说通常情况下,我们用物体的左上角 $x,y$ 坐标和右上角 $x,y$ 来标记一个物体的位置,即 $(x_l,y_l,x_r,y_r)$ 来标记物体的位置。
  具体实现:其实边界框就是一个矩形框,我们可以使用matplotlib绘制一个矩形框然后显示就行。总结步骤如下

  • 读入图片,例如使用MXNet API mx.image.imread(path).asnumpy(),记住后面需要转换为numpy数据格式,因为matplotlib只支持numpy格式数据,而不支持ndarray。如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    * 生成边界框坐标,一般用一个数组来保存,如`dog_box,cat_box = [60, 45, 378, 516], [400, 112, 655, 493]`,其坐标按顺序分别为$(x_l,y_l,x_r,y_r)$ 
    * 使用plt绘制出矩形,获得一个矩形框对象,使用`plt.Rectangle(xy, width, height, angle=0.0, **kwargs)`函数进行绘制,所以我们需要将上述的坐标转换为左上角坐标+长宽,具体代码为`dog_bbox=plt.Rectangle((dog_box[0],dog_box[1]),width=(dog_bbox[2]-dog_bbox[0]),height=(dog_bbox[3]-dog_bbox[1]),
    fill = False,edgecolor = 'r',linewidth=2)`
    * 最后一步,将我们上述生成的矩形框对象加入到显示的图像中即可,使用`fig.axes.add_patch()`函数实现。

    ```python
    img =image.imread('../img/catdog.jpg').asnumpy()
    dog_bbox,cat_bbox = [60, 45, 378, 516], [400, 112, 655, 493]
    dog_bbox = plt.Rectangle((dog_bbox[0],dog_bbox[1]),
    width=dog_bbox[2]-dog_bbox[0],height=dog_bbox[3]-dog_bbox[1],
    fill = False,edgecolor = 'r',linewidth=2)
    cat_bbox = plt.Rectangle((cat_bbox[0],cat_bbox[1]),
    width=cat_bbox[2]-cat_bbox[0],height=cat_bbox[3]-cat_bbox[1],
    fill = False,edgecolor = 'r',linewidth=2)

    fig = plt.imshow(img)
    fig.axes.add_patch(dog_bbox)
    fig.axes.add_patch(cat_bbox)

    图片失效啦


锚框

定义

  目标检测历史中,RCNN,Fast-RCNN采用的是启发式搜索(selective search)来生成我们感兴趣的目标区域(RoI),并且在这些区域的基础上提取特征,最后进行分类和回归预测。

  之后的方法如Faster-RCNN、YOLO、SSD等检测框架中,放弃了启发式搜索方法,而是改用 锚框 来生成我们的RoI。

  具体就是,目标检测通常会以每个像素为中心生成多个大小和宽高比不同的边界框。这些边界框就成为锚框,之后的特征提取、分类、回归预测都是基于这些锚框的。

生成方式

  通常我们会选取多组大小比例 $s \in (0,1)$ 和宽高比 $r > 0$ 的锚框,生成的锚框大小为 $ws \sqrt r$和 $hs \sqrt r$。
  假设我们分别设定好了一组大小比例 $s_1,s_2…,s_n$,以及一组宽高比$r_1,r_2,…,r_m$。如果我们以每个像素为中心都使用所有大小的宽高比组合,那么输入图像一共会得到$whnm$ 个锚框。

  • RCNN中 的确是如此做,直接使用生成的锚框
  • SSD中为了降低计算复杂度,只使用包含 $r_1$ 或 $s_1$ 的大小和宽高比组合,所以一共有$n+m-1$个锚框。

下面记录一下,MXNet中生成锚框的方式,MXNet中使用contrib.nd.MultiBoxPrior(data=None, sizes=_Null, ratios=_Null, clip=_Null, steps=_Null, offsets=_Null, out=None, name=None, **kwargs)函数来生成锚框。解释一下其中的重要参数:

  • data:输入数据,形状为(批量大小,通道数,宽,高),函数会基于宽、高为每个像素生成锚框,其中输入数据的前两维 (批量大小,通道数) 对输出没有影响。
  • sizes:大小比例,即上面说的 $s$ 一般输入为一个列表,表示不同的大小比例
  • ratios:宽高比例,即上面说的 $w$ 一般输入也为一个列表,表示不同的宽高比例

返回值为 ( 1,生成的锚框总数,4 ) 形状的张量。其中锚框总数为 $hw(n+m-1)$ 并且返回的锚框的坐标值均除以了宽和高。
(P.S. 教程中说的是会返回(批量大小,生成的锚框总数,4)形状的锚框,但是我测试了一下发现无论批量大小为多少,其第一维均为1)
图片失效啦


绘制锚框的方式

那么我们如何绘制所有锚框呢?这里首先总结一下matpoltlib中常用的绘图加标注的函数:
|函数|作用|
|:—-:|:——:|
|plt.xlabel()|给整幅图像的X轴添加文本标签说明|
|plt.ylabel()|给整幅图像的Y轴添加文本标签说明|
|plt.title()|给整幅图形添加文本标题说明|
|plt.text()|在图像中任意位置添加文本标签说明|
|plt.annotation()|在图像中任意位置添加带箭头的文本标签说明|

  这里我们需要用到的是 plt.text()函数,用于给我们绘制的锚框添加标签,一般我们最后在显示的时候会为每个锚框显示预测类别置信度和类别的标签。下面详细说明一下 plt.text() 中我们需要用到的参数

参数 作用
x 显示位置的x坐标
y 显示位置的y坐标
fontsize 显示文字标签的字体大小
color 显示文字标签的字体颜色
bbox 用于给文字标签生成边界框

其中bbox常用参数如下

bbox参数 作用
boxstyle 边框外形
facecolor 背景颜色
edgecolor 边框颜色
edgewidth 边框线条大小

常用格式如下axes.text(rect.xy[0], rect.xy[1], labels[i],va='center', ha='center', fontsize=9, color=text_color,bbox=dict(facecolor=color, lw=0))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def show_bboxes(axes,bboxes,labels=None,colors=None):
def _make_list(obj,defaultvalue=None):
if obj is None:
obj = defaultvalue
elif not isinstance(obj,(list,tuple)):
obj = [obj]
return obj

labels = _make_list(labels)
colors = _make_list(colors,['b','g','r','m','c'])
#下面开始绘制图像
for i,bbox in enumerate(bboxes):
#获取bbox背景颜色
color = colors[i%len(colors)]
rect = bbox_to_rect(bbox.asnumpy(),color)
axes.add_patch(rect)
#添加标签
if labels and len(labels)>i:
text_color = 'k' if color=='w' else 'w'
axes.text(x=rect.xy[0],y=rect.xy[1],s=labels[i],
va='center',ha='center',fontsize=9,color=text_color,
bbox=dict(facecolor=color,lw=0))

注意,当显示通过 contrib.nd.MultiBoxPrior 生成的锚框时,需要将生成的坐标乘以宽和高,这样才是生成坐标的绝对坐标。


交并比

   交并比(Intersection over Union) 作用主要是用来衡量生成的锚框与真实物体框之间的相似度,也就是“距离”。
  物体分类中,衡量预测类别和真实类别之间的相似度很简单,直接看预测的概率就行。那么锚框有四个坐标,是一个多标签的区域,我们一般采用交并比来衡量两个区域之间的相似度,具体定义如下:

图片失效啦
所以我们可以通过交并比,来衡量锚框与真实物框、以及锚框与锚框之间的相似度(用于后面的非极大值抑制)。


如何为每个锚框标注其匹配的真实边界框

  图像分类时,我们会将每一张图片都看作一个训练样本,为其标注真实的物体标签(猫、狗、飞机、…..)。这样我们就可以通过预测的标签和真实标签之间的差值,来计算损失函数。
  同理在目标检测中,我们将每个锚框都看作一个训练样本,所以我们需要为每个锚框打上标记,为其分配真实标签:

  • 锚框的物体类别(背景、猫、狗、….)
  • 锚框与真实物体框之间的偏移量(如果是背景锚框则不关心,计算偏移量的损失函数的时候并不将其计算在内)

  问题:我们如何为一个锚框分配真实边框呢?一张图片中真实物体边框的个数肯定远远小于我们生成的锚框数,也就是说大部分锚框应该都是背景框,给定一个锚框,我们如何为其分配最相似的真实物体框呢?
  不妨假设图片中我们生成的锚框为$A_1,A_2,…,A_{n_a}$,真实的物体框为$B_1,B_2,…,B_{n_b}$,显然$n_a>n_b$。定义矩阵 $X \in \mathbb R^{n_an_b}$ ,其中第$i$行,第$j$列的元素$x_{ij}$定义为:锚框$A_i$和真实边界框$B_j$的*交并比,那么标记每个锚框的具体步骤如下:

  1. 首先找出矩阵所有元素的最大值,也就是找出所有锚框和所有真实物体框中交并比最大的那个,不妨假设该元素的行和列索引为$i_1,j_1$,那么我们为锚框$A_{i_1}$分配真实边界框$B_{j_1}$,之后我们丢弃第$i_11$行所有元素,以及第$j_1$列的所有元素。
  2. 重复上一个步骤,我们找到剩余元素中的最大值,假设为行和列索引分别为$i_2,j_2$,那么我们为锚框$A_{i_2}$分配真实物体框$B_{j_2}$,之后我们丢弃第$i_2$行所有元素,以及第$j_2$列的所有元素。
  3. 重复上述步骤,直到矩阵的所有$n_b$列都被丢弃为止,那么我们现在已经分配了$n_b$个锚框和真实边界框匹配。只剩下$n_a-n_b$个锚框还未分配。
  4. 最后一步,我们遍历剩余的$n_a-n_b$个锚框,也就是我们对剩余的每个锚框$A_i$遍历其所在的行,找出最大的元素,也就是找出与其交并比最大的真实物体框$B_j$,之后如果满足 最大的元素 $>$ 设定的阈值,那么我们为其分配真实边界框$B_j$,否则,将其标记背景锚框(也就是不分配锚框)。

过程如图所示:$x_{23}、x_{71}、x_{54}、x_{92}$分别为每次选出的最大值:

图片失效啦

  MXNet中已经有函数可以帮我们实现这些功能,这个函数为contrib.nd.MultiBoxTarget(),下面总结一下这个函数各个重要参数说明:
参数|作用
———-|———-
anchor |输入的锚框,一般为MultiBoxPrior生成的锚框,形状为(1,锚框总数,4),各通道共享这些锚框
label |真实的物体框标记,一般形状为(批量大小,最多的真实物体框数,5),第二维为一个训练集中最多的真实物体框数,如果某一张图像没有这么多物体框,将会为其填充-1表示是负样本,这里的第三维为(真实物体框类别+四个边界边界坐标)如[0, 0.1, 0.08, 0.52, 0.92]
cls_pred|预测的类别,一般形状为(批量大小,预测的总类别数+1,锚框总数),这个输入的作用是为了负采样(negative_mining),如果不设置负采样,那么这个输入并不影响输出
overlap_threshold|前面说的设定的IoU的阈值
negative_mining_ratio|负采样的比例,设置了负采样之后,输出只会选择置信度最小的一些负类锚框进行训练,而未选择的负类锚框将标记为-1
输入如下图所示

图片失效啦

图片失效啦

下面说明一下输出,输出为一个list,其中有三个元素,第一个元素为每个锚框标记的四个偏移量,形状为(批量大小,锚框个数*4),其中负类锚框的偏移量标注为0

图片失效啦

第二个元素为掩码(mask),形状为(批量大小,锚框个数*4),与上面生成的每个锚框的四个偏移量一一对应。由于我们不关心对背景的检测,所以负类的偏移量不应该影响目标函数。我们可以通过按元素乘法,从而可以在计算目标函数之前过滤掉负类的偏移量。

图片失效啦

最后一个元素为对应锚框标记的类别数,其中0表示背景,然后依次类推,输出的形状为(批量大小,锚框个数)。注意如果我们设置了负采样,那么有一些负类会标记为-1,表示我们丢弃了这些负类样本,在计算损失函数时,应该将其过滤,使用SoftmaxCrossEnrtopy()函数的sample_weight可以实现过滤。

图片失效啦


输出预测边界框

  模型预测时,我们会为一张图片生成大量的锚框,并且一一为这些锚框预测类别和偏移量。问题:这些锚框可能会有大量的重复区域,比如所一个物体被框了很多次。解决办法:我们采用非极大值抑制(NMS)来去除重复相似的锚框。
  对一个预测边界框$B$,模型会预测它属于各个类别的概率。设其中的最大值为$p$,该概率所对应的类别即为$B$所预测的类别,我们也将$p$称为预测边界框$B$的置信度。
  那么具体步骤如下:

  1. 在同一张图像上,我们将预测类别非背景的预测边界框置信度,按照从高到低的顺序排序,得到列表。$L$
  2. 在列表$L$中选择置信度最大的预测边界框$B_1$,并且将所有与$B_1$交并比大于设定阈值的非基准边界框,从$L$中移除。
  3. 接下来,从$L$中选择置信度第二高的边界框$B_2$作为基准,并且重复上一步的操作
  4. 最后直到$L$中所有的边界框都曾经作为基准。此时$L$中剩余的任意一对预测边界框的交并比都小于设定阈值

MXNet中实现这个功能的函数为contrib.ndarray.MultiBoxDetection()下面总结一下其常用的参数
参数|作用
——-|———-
cls_prob|预测各个锚框的概率值,也就是说要经过$\boldsymbol {softmax}$运算,注意这里的形状为(批量大小,总类别数,锚框总数),和前面有点不一样
anchor|预测的所有锚框,形状为(批量大小,锚框总数,4)
loc_pred|预测出来的偏移量,形状为(批量大小,锚框总数*4)

输出:输出的形状为(批量大小,锚框总数,6),其中最后一维第一个元素表示锚框的类别,其中-1表示该锚框为背景框或者NMS过程中被删除,最后一维第二各元素为该锚框的置信度,最后一维的最后四个元素为锚框的四个坐标
图片失效啦

图像NMS之前如下图所示

图片失效啦

进行NMS处理之后,如下图所示

图片失效啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def show_nms_bbox(axes,nms_bboxes,colors=None):
bboxes = []
labels = []
for nms_bbox in nms_bboxes[0].asnumpy():
if(nms_bbox[0]>=0):
if nms_bbox[0]==0 :
label = 'dog'
else:
label = 'cat'
bboxes.append(nms_bbox[2:])
labels.append(label+str(nms_bbox[1]))

bboxes = nd.array(bboxes)
print(bboxes)
show_bboxes(axes,bboxes*img_scale,labels)

plt.figure(figsize=(10,10))
fig=plt.imshow(img)
show_nms_bbox(fig.axes,outputs)

总结一下关于锚框常用的函数:

  1. contrib.nd.MultiBoxPrior用来生成锚框。
  2. contrib.nd.MultiBoxTarget用来为生成的锚框分配真实边界框和标记偏移量。
  3. contrib.nd.MultiBoxDetection用来去掉重复的锚框,NMS。
  4. 所有的锚框坐标都除以了宽和高 ,归一化到了(0,1),绘制边界框的时候记得要乘以原来图像的尺寸

参考资料:动手深度学习