目标检测和图像分类
- 图像分类:识别图像中的目标。输入图片,输出图像中存在的目标类别,以及对应类别的概率。
- 目标检测:识别+定位图片中的目标。输入图片,输出图像中存在的目标类别,以及置信度,还输出边界框(x, y, width, height)表示目标在图片中的位置。
可见目标检测算法的核心之一也是图像分类。假设我们训练了一个图像分类模型,它可以识别图像中的目标,但是不知道目标的位置,要定位目标,我们需要选择图像的子区域(patches),然后将图像分类算法应用于这些 patches。目标的位置是由返回的类别概率较高的 patches 的位置给出。
因此,如何生成 patches? 生成较小的子区域最简单方法是滑动窗口 (Sliding Window) 算法。然而,滑动窗口方法有一些局限。“区域提名 (Region Proposal)” 算法可以克服这些。选择性搜索 (Selective Search) 算法是最流行的区域提名算法之一。
滑动窗口算法
在滑动窗口方法中,将一个窗口滑动到图像上以选择一个 patch,并使用分类模型对窗口覆盖的每个 patch 进行分类。为了尽可能框到图像中的目标。需要滑动搜索图像中所有可能的位置,相邻窗口会出现很多重叠地方,此外,由于图像中目标的大小,形状不确定,还必须用不同 size 的窗口依次遍历图像,例如改变窗口的大小再遍历,改变窗口的宽高比 (ratio) 再遍历。可见,滑动窗口简单易理解,但是利用各种大小的窗口在图像中全局搜索,导致分类模型要对数万个 patches 进行分类。
图像是三维到二维投影。根据拍摄角度,某些物体的长宽比和形状有很大的不同。当使用滑动窗口时,计算 very expensive。滑动窗口适用于宽高比变化相对稳定的目标,如脸部,行人。
区域提名算法
Region Proposal 的算法会选出图像中所有可能包含目标的框。这些区域方案可能会很嘈杂、重叠,也可能无法完美地包含目标,但在这些提名区域中,会有非常接近图像中的实际目标的 patch。然后利用分类,将概率分数高的区域输出。
Region Proposal 算法使用分割来识别目标中的预期对象。在分割中,我们根据颜色、纹理等标准对彼此相似的相邻区域进行分组。滑动窗口是通过所有像素的位置,各种大小框来遍历寻找图像中的目标,区域提名算法是通过将像素分组为较少数量的 segments 来工作。因此,最终生成的 proposals 比滑动窗口方法少很多倍。
另外,Region Proposal 算法一个重要特性是具有很高的召回率 (Recall = TP/(TP+FN))。这是由于真阳性 (TP) 较高,而假阴性 (FN) 较低。在提名的区域列表中尽可能包含我们正在查找的目标的区域。为了实现这一点,提名的区域列表中最终可能会包含许多不包含任何目标的区域。换句话说,Region Proposal 算法要想查找到所有的真阳性,就可以可能产生大量的假阳性 (FP)。这些假阳性区域大多会被目标识别算法 pass 掉。因此较多的假阳性对精度有只有轻微影响,只是检测所需的时间增加。可见高召回率确实是这类算法很好的一个特性,如果丢失了包含实际物体的区域 (FN),就会严重影响了检测率。
已经提出的几个 Region Proposal 算法
-
[Constrained Parametric Min-Cuts for Automatic Object Segmentation]
J. Carreira and C. Sminchisescu, “Constrained parametric min-cuts for automatic object segmentation,” in CVPR, 2010.
-
[Category Independent Object Proposals]
I. Endres and D. Hoiem, “Category independent object proposals,” in ECCV, 2010.
-
Randomized Prim:
S. Manén, M. Guillaumin, and L. Van Gool, “Prime object proposals with randomized prim’s algorithm,” in ICCV, 2013.
目前,在所有这些区域提名算法中,选择性搜索算法 (Selective Search) 最常用,因为它速度快,召回率高。
选择性搜索算法
选择性搜索 (Selective Search) 算法基于颜色、纹理、大小和形状的兼容性来对相似区域层级分组 (Hierarchical grouping)。算法从图像 over-segment 后开始, over-segment 是基于像素强度,可以使用 Felzenszwalb 和 Huttenlocher 基于图形 (Graph-based) 的分割方法。
那么可以将此图像中的分割部分用作提名区域吗?答案是否定的,两点原因:
-
原始图像中的大多数实际目标包含2个或更多分割部分 (segmented parts)
-
图像中可能有物体被遮挡,这样被遮挡的部分很可能没有被分割到该物体的区域中
选择性搜索算法将那些 over-segments 作为初始输入,执行以下步骤
-
将所有 segmented parts 对应的边界框添加到提名区域列表中
-
基于相似性对相邻段进行分组
-
转到步骤1
在每次迭代中,将形成更大的 segments,并添加到提名区域列表中。如下图,该算法是自下而上,从较小 segments 到较大 segments,来创建提名区域的。这就是上文提到的层级分组 (Hierarchical grouping) 的意思了。
Hierarchical Segmentation
相似度计算
如何计算两个区域之间的相似度。SS 算法根据颜色、纹理、尺寸和匹配程度这 4 种度量来计算相似度。
- Color Similarity
- Texture Similarity
- Size similarity
- Shape Compatibility
颜色相似度
对图像的每个通道计算 bins=25 的颜色直方图,所有通道的直方图构成 25×3=75 维的颜色描述符 (descriptor)。
两个区域
(
r
i
,
r
j
)
(r_{i},r_{j})
(ri,rj) 的颜色相似度是基于直方图交叉,,
c
i
k
c_{i}^k
cik 表示第
k
t
h
b
i
n
k^th bin
kthbin 的直方图:
S
c
o
l
o
r
(
r
i
,
r
j
)
=
∑
k
=
1
n
m
i
n
(
c
i
k
,
c
j
k
)
S_{color}(r_{i},r_{j})=\sum_{k=1}^nmin(c_{i}^k,c_{j}^k)
Scolor(ri,rj)=k=1∑nmin(cik,cjk)
纹理相似度
通过提取每个通道 8 个方向上的高斯导数来计算纹理特征。对于每个方向和每个颜色通道,计算 bins=10 的直方图,因此生成一个10x8x3=240 维的特征描述符。
两个区域的纹理相似度也通过直方图交叉点来计算:
S
t
e
x
t
u
r
e
(
r
i
,
r
j
)
=
∑
k
=
1
n
m
i
n
(
t
i
k
,
t
j
k
)
S_{texture}(r_{i},r_{j})=\sum_{k=1}^nmin(t_{i}^k,t_{j}^k)
Stexture(ri,rj)=k=1∑nmin(tik,tjk)
尺寸相似度
尺寸相似度鼓励较小的区域尽早合并。它确保在图像的所有部分形成所有规模的 region proposals。这种相似性度量,避免单个区域一个接一个地吞没相邻较小的区域,举个不太恰当例子,很早之前的贪食蛇游戏,一条蛇不断吃,一个点一个点的吃,现在有那种大概叫蛇蛇大作战的游戏,多条蛇同时并存,不断壮大,可能独立存在,或者相互融合。尺寸相似性定义为:
S
s
i
z
e
(
r
i
,
r
j
)
=
1
−
s
i
z
e
(
r
i
)
+
s
i
z
e
(
r
j
)
s
i
z
e
(
i
m
)
S_{size}(r_{i},r_{j})=1-\frac{size(r_{i})+size(r_{j})}{size(im)}
Ssize(ri,rj)=1−size(im)size(ri)+size(rj)
形状匹配程度
测量两个区域 ( r i , r j ) (r_{i},r_{j}) (ri,rj) 彼此之间的匹配程度。希望合并它们以填补空白,如果 r i r_{i} ri和 r j r_{j} rj 之间几乎没有接触,很可能会形成一个奇怪的区域,不应该合并。将 B B i j BB_{ij} BBij 定义为紧紧围绕且包含 r i r_{i} ri 和 r j r_{j} rj 的边界框。因此如下, S f i l l ( r i , r j ) S_{fill(r_{i},r_{j})} Sfill(ri,rj) 是 B B i j BB_{ij} BBij 中不包含 r i r_{i} ri 和 r j r_{j} rj 的区域。如果它们甚至没有相互接触,就不应该合并。
形状兼容性定义为:
S
f
i
l
l
(
r
i
,
r
j
)
=
1
−
B
B
i
j
−
s
i
z
e
(
r
i
)
−
s
i
z
e
(
r
j
)
s
i
z
e
(
i
m
)
S_{fill(r_{i},r_{j})}=1-\frac{BB_{ij}-size(r_{i})-size(r_{j})}{size(im)}
Sfill(ri,rj)=1−size(im)BBij−size(ri)−size(rj)
总的相似度
四个相似度的线性组合:
S
(
r
i
,
r
j
)
=
a
1
S
c
o
l
o
r
+
a
2
S
t
e
x
t
u
r
e
+
a
3
S
s
i
z
e
+
a
4
S
f
i
l
l
S(r_{i},r_{j})=a_{1}S_{color}+a_{2}S_{texture}+a_{3}S_{size}+a_{4}S_{fill}
S(ri,rj)=a1Scolor+a2Stexture+a3Ssize+a4Sfill
其中
a
i
∈
0
,
1
a_{i} \in 0,1
ai∈0,1.
https://www.koen.me/research/pub/vandesande-iccv2011-poster.pdf
选择性搜索算法代码
C++代码
#include "opencv2/ximgproc/segmentation.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
#include <ctime>
using namespace cv;
using namespace cv::ximgproc::segmentation;
static void help() {
std::cout << std::endl <<
"Usage:" << std::endl <<
"./ssearch input_image (f|q)" << std::endl <<
"f=fast, q=quality" << std::endl <<
"Use l to display less rects, m to display more rects, q to quit" << std::endl;
}
int main(int argc, char** argv) {
// If image path and f/q is not passed as command
// line arguments, quit and display help message
if (argc < 3) {
help();
return -1;
}
// speed-up using multithreads
setUseOptimized(true);
setNumThreads(4);
// read image
Mat im = imread(argv[1]);
// resize image
int newHeight = 200;
int newWidth = im.cols*newHeight/im.rows;
resize(im, im, Size(newWidth, newHeight));
// create Selective Search Segmentation Object using default parameters
Ptr<SelectiveSearchSegmentation> ss = createSelectiveSearchSegmentation();
// set input image on which we will run segmentation
ss->setBaseImage(im);
// Switch to fast but low recall Selective Search method
if (argv[2][0] == 'f') {
ss->switchToSelectiveSearchFast();
}
// Switch to high recall but slow Selective Search method
else if (argv[2][0] == 'q') {
ss->switchToSelectiveSearchQuality();
}
// if argument is neither f nor q print help message
else {
help();
return -2;
}
// run selective search segmentation on input image
std::vector<Rect> rects;
ss->process(rects);
std::cout << "Total Number of Region Proposals: " << rects.size() << std::endl;
// number of region proposals to show
int numShowRects = 100;
// increment to increase/decrease total number
// of reason proposals to be shown
int increment = 50;
while(1) {
// create a copy of original image
Mat imOut = im.clone();
// itereate over all the region proposals
for(int i = 0; i < rects.size(); i++) {
if (i < numShowRects) {
rectangle(imOut, rects[i], Scalar(0, 255, 0));
}
else {
break;
}
}
// show output
imshow("Output", imOut);
// record key press
int k = waitKey();
// m is pressed
if (k == 109) {
// increase total number of rectangles to show by increment
numShowRects += increment;
}
// l is pressed
else if (k == 108 && numShowRects > increment) {
// decrease total number of rectangles to show by increment
numShowRects -= increment;
}
// q is pressed
else if (k == 113) {
break;
}
}
return 0;
}
python 代码
opencv3.3
#!/usr/bin/env python
'''
Usage:
./ssearch.py input_image (f|q)
f=fast, q=quality
Use "l" to display less rects, 'm' to display more rects, "q" to quit.
'''
import sys
import cv2
if __name__ == '__main__':
# If image path and f/q is not passed as command
# line arguments, quit and display help message
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
# speed-up using multithreads
cv2.setUseOptimized(True);
cv2.setNumThreads(4);
# read image
im = cv2.imread(sys.argv[1])
# resize image
newHeight = 200
newWidth = int(im.shape[1]*200/im.shape[0])
im = cv2.resize(im, (newWidth, newHeight))
# create Selective Search Segmentation Object using default parameters
ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()
# set input image on which we will run segmentation
ss.setBaseImage(im)
# Switch to fast but low recall Selective Search method
if (sys.argv[2] == 'f'):
ss.switchToSelectiveSearchFast()
# Switch to high recall but slow Selective Search method
elif (sys.argv[2] == 'q'):
ss.switchToSelectiveSearchQuality()
# if argument is neither f nor q print help message
else:
print(__doc__)
sys.exit(1)
# run selective search segmentation on input image
rects = ss.process()
print('Total Number of Region Proposals: {}'.format(len(rects)))
# number of region proposals to show
numShowRects = 100
# increment to increase/decrease total number
# of reason proposals to be shown
increment = 50
while True:
# create a copy of original image
imOut = im.copy()
# itereate over all the region proposals
for i, rect in enumerate(rects):
# draw rectangle for region proposal till numShowRects
if (i < numShowRects):
x, y, w, h = rect
cv2.rectangle(imOut, (x, y), (x+w, y+h), (0, 255, 0), 1, cv2.LINE_AA)
else:
break
# show output
cv2.imshow("Output", imOut)
# record key press
k = cv2.waitKey(0) & 0xFF
# m is pressed
if k == 109:
# increase total number of rectangles to show by increment
numShowRects += increment
# l is pressed
elif k == 108 and numShowRects > increment:
# decrease total number of rectangles to show by increment
numShowRects -= increment
# q is pressed
elif k == 113:
break
# close image show window
cv2.destroyAllWindows()