原论文:《Selective Search for Object Recognition》 关键字:分层分组算法、初始化区域集、相似度计算 keywords: Hierarchical Grouping Algorithm, Obtaining Initial Regions, Calculating Similarity
一直想总结下目标检测的一系列算法,分为两个主要路线,一个是从RCNN发展起来的两阶段算法,另一个是以YOLO发展的一阶段算法。本篇属于RCNN算法中重要的一部分,用来解决生成候选区域的问题,算是基础,也很重要,尽管该算法已经过时了,但是掌握其中的思想用来解决可以问题还是不错的。本文中涉及到的代码和数据集,请关注公众号CV市场, 回复“RCNN”获取。
简述
作用:在原图片上,以尽可能快和好地生成可能包含目标的候选块。换句话说,避免了穷举法的计算量大且无图像本身信息的缺点。
解决问题:
这位知乎网友说的很好,这块直接引用过来了,原文链接 由于我们事先不知道需要检测哪个类别,因此第一张图的桌子、瓶子、餐具都是一个个候选目标,而餐具包含在桌子这个目标内,勺子又包含在碗内。这张图展示了目标检测的层级关系以及尺度关系,那我们如何去获得这些可能目标的位置呢。常规方法是通过穷举法,就是在原始图片上进行不同尺度不同大小的滑窗,获取每个可能的位置。而这样做的缺点也显而易见,就是计算量实在是太大了,而且由于不可能每个尺度都兼顾到,因此得到的目标位置也不可能那么准。那么我们能不能通过视觉特征去减少这种分类的可能性并提高精确度呢。这就是本文想做的事情。
可用的特征有很多,到底什么特征是有用的呢?我们看第二副图片的两只猫咪,他们的纹理是一样的,因此纹理特征肯定不行了。而如果通过颜色则能很好区分。但是第三幅图变色龙可就不行了,这时候边缘特征、纹理特征又显得比较有用。而在最后一幅图中,我们很容易把车和轮胎看作是一个整体,但是其实这两者的特征差距真的很明显啊,无论是颜色还是纹理或是边缘都差的太远了。而这这是几种情况,自然图像辣么多,我们通过什么特征去区分?应该区分到什么尺度?
selective search的策略是,既然是不知道尺度是怎样的,那我们就尽可能遍历所有的尺度好了,但是不同于暴力穷举,我们可以先得到小尺度的区域,然后一次次合并得到大的尺寸就好了,这样也符合人类的视觉认知。既然特征很多,那就把我们知道的特征都用上,但是同时也要照顾下计算复杂度,不然和穷举法也没啥区别这就是整篇文章的思路。
所以,为了解决纹理特征(梯度信息)不行的问题,使用了颜色特征;为了解决颜色特征不行的问题,使用了纹理特征;为了解决尺度问题,使用了候选区的面积和互补性。
解决办法(大致思想):
- selective search首先使用 Felzenswalb图像分割算法将图像分割成多个不同的块,每一个块代表分割算法得到的一个类,根据每个块的横纵坐标的最大值和最小值来初步产生一个候选区
- 上述分割结果为RGB空间,转成HSV空间,计算HSV空间下的颜色直方图
- 分割结果使用局部梯度算法(LBP)得到梯度信息,统计梯度直方图
- 分别根据颜色直方图和梯度直方图计算分割得到的那些发生重叠的候选区的颜色相似性和梯度相似性,还包括面积相似性、互补相似性
- 根据相似性合并这些重叠的候选区
- 计算候选区与ground truth的IOU,小于阈值的作为负样本,大于阈值的将真实类别作为其标签
小结: 关键词包括图像分割算法、颜色直方图、梯度直方图、图像梯度计算、相似度计算等,下面会一一详细地介绍这些关键词的思路、方法和Python实现。同时我也在我的博客中,对比分析了使用不同分割算法、梯度计算方法等对模型的影响。
步骤一:使用分割算法分割图像,初步得到候选区
目的: 得到初始候选区
思路: 因为区域级比像素级信息多,所以作者想尽可能地使用基于图片区域特征的分割方法,为了获得一组不跨越多个目标对象的小起始区域,使用了Felzenswalb图像分割算法在一张图片上分割出多个目标。
代码: 下面的代码中,对比了三种不同的felzenszwalb算法:
- skimage自带的felzenszwalb算法
- skimage自带的felzenszwalb算法cython版转Python代码,更改了高斯模糊
- felzenszwalb算法的实现,相比于上一种,区别主要在四邻域和颜色还原
代码中包括魔改的_felzenszwalb_cy.py,想要此部分代码请关注公众w号,回复 999 获取
import cv2
import numpy as np
from skimage import io as sio
from skimage.segmentation import felzenszwalb
import matplotlib.pyplot as plt
from _felzenszwalb_cy import _felzenszwalb_cython
def felzenszwalb_test(img,sigma,kernel,k, min_size):
img = np.asanyarray(img, dtype=np.float) / 255
k = float(k) / 255.
img = cv2.GaussianBlur(img, (kernel, kernel), sigma)
height, width = img.shape[:2]
num = height * width
edges = np.zeros(((height - 1) * width * 2 + height * (width - 1) * 2, 3))
index = np.array([i for i in range(height * width)])
index = index.reshape((height, width))
to_left = np.sqrt(((img[:, 1:] - img[:, :-1]) ** 2).sum(axis=2))
to_right = to_left
to_up = np.sqrt(((img[1:] - img[:-1]) ** 2).sum(axis=2))
to_down = to_up
last, cur = 0, 0
last, cur = cur, cur + (width - 1) * height
edges[last: cur, 0] = index[:, 1:].reshape(-1)
edges[last: cur, 1] = index[:, :-1].reshape(-1)
edges[last: cur, 2] = to_left.reshape(-1)
last, cur = cur, cur + (width - 1) * height
edges[last: cur, 0] = index[:, :-1].reshape(-1)
edges[last: cur, 1] = index[:, 1:].reshape(-1)
edges[last: cur, 2] = to_right.reshape(-1)
last, cur = cur, cur + (height - 1) * width
edges[last: cur, 0] = index[1:].reshape(-1)
edges[last: cur, 1] = index[:-1].reshape(-1)
edges[last: cur, 2] = to_up.reshape(-1)
last, cur = cur, cur + (height - 1) * width
edges[last: cur, 0] = index[:-1].reshape(-1)
edges[last: cur, 1] = index[1:].reshape(-1)
edges[last: cur, 2] = to_down.reshape(-1)
edges = [edges[i] for i in range(edges.shape[0])]
edges.sort(key=lambda x: x[2])
class universe(object):
def __init__(self, n, k):
self.f = np.array([i for i in range(n)])
self.r = np.zeros_like(self.f)
self.s = np.ones((n))
self.t = np.ones((n)) * k
self.k = k
def find(self, x):
if x == self.f[x]:
return x
return self.find(self.f[x])
def join(self, a, b):
if self.r[a] > self.r[b]:
self.f[b] = a
self.s[a] += self.s[b]
else:
self.f[a] = b
self.s[b] += self.s[a]
if self.r[a] == self.r[b]:
self.r[b] += 1
u = universe(num, k)
for edge in edges:
a, b = u.find(int(edge[0])), u.find(int(edge[1]))
if ((a != b) and (edge[2] <= min(u.t[a], u.t[b]))):
u.join(a, b)
a = u.find(a)
u.t[a] = edge[2] + k / u.s[a]
for edge in edges:
a, b = u.find(int(edge[0])), u.find(int(edge[1]))
if ((a != b) and ((u.s[a] < min_size) or u.s[b] < min_size)):
u.join(a, b)
dst = np.zeros_like(img)
def locate(index):
return index // width, index % width
avg_color = np.zeros((num, 3))
for i in range(num):
f = u.find(i)
x, y = locate(i)
avg_color[f, :] += img[x, y, :] / u.s[f]
for i in range(height):
for j in range(width):
f = u.find(i * width + j)
dst[i, j, :] = avg_color[f, :]
return dst
if __name__ == '__main__':
sigma = 0.9
kernel = 3
K, min_size = 500, 10
image = sio.imread("F:/learn/R-CNN-master/17flowers/jpg/0/image_0001.jpg")
seg1 = felzenszwalb(image, scale=K, sigma=sigma, min_size=min_size)
seg2 = _felzenszwalb_cython(image, scale=K, sigma=sigma, kernel=kernel,min_size=min_size)
seg3=felzenszwalb_test(image, sigma, kernel,K, min_size)
fig = plt.figure()
a = fig.add_subplot(221)
plt.imshow(image)
a.set_title("image")
a = fig.add_subplot(222)
plt.imshow(seg1)
a.set_title("seg1 nums:" + str(np.unique(seg1).shape[0]))
a = fig.add_subplot(223)
plt.imshow(seg2)
a.set_title("seg2 nums:" + str(np.unique(seg2).shape[0]))
a = fig.add_subplot(224)
plt.imshow(seg3)
a.set_title("seg3 nums:" + str(np.unique(seg3).shape[0]))
plt.show()
结果如下: 图1为原始图片,seg num是分割得到的分割类别数 分割得到的每一类分割结果都会有像素位置的横坐标的左侧最小值min_x、纵坐标的左侧最小值min_y、横坐标的右侧最大值max_x、纵坐标的右侧最大值max_y,即[min_x, min_y, max_x, max_y]作为当前分割结果的候选区(框)
(下面的代码可能和这篇文章上下文不符合,这块单独看应该 是看不明白,但是这一部分只是获取候选区的坐标,完整代码清关中公众号 ,回复 998获取)
for y, i in enumerate(img):
for x, (r, g, b, l) in enumerate(i):
if l not in R:
R[l] = {
"min_x": 0xffff, "min_y": 0xffff,
"max_x": 0, "max_y": 0, "labels": [l]}
if R[l]["min_x"] > x:
R[l]["min_x"] = x
if R[l]["min_y"] > y:
R[l]["min_y"] = y
if R[l]["max_x"] < x:
R[l]["max_x"] = x
if R[l]["max_y"] < y:
R[l]["max_y"] = y
比如在分割结果示意图中,seg2分割出334个类,那么又会有334个初始候选区,接下来就需要对这些区进行合并、删除等。
步骤二:根据分割类别计算颜色直方图梯度直方图,用于后续的候选区删除和合并
目的: 为计算两个直方图,为合并重叠候选区做准备
在简述里面已经说过了,既要利用颜色信息,又要利用纹理特征,所以就分别计算了颜色直方图和梯度直方图。
- RGB空间计算梯度直方图
tex_grad = _calc_texture_gradient(img)
def _calc_texture_gradient(img):
ret = numpy.zeros((img.shape[0], img.shape[1], img.shape[2]))
for colour_channel in (0, 1, 2):
ret[:, :, colour_channel] = skimage.feature.local_binary_pattern(img[:, :, colour_channel], 8, 1.0)
return ret
此处计算整张图像的梯度信息是使用的skimage自带的local_binary_pattern,大致思想就是统计整张图片的梯度,作为图片的纹理信息。 有了整张图像的梯度信息后,按照之前分割算法得到的像素级的分类结果,按照像素的类别计算同类下的梯度直方图
R[k]["hist_t"] = _calc_texture_hist(tex_grad[:, :][img[:, :, 3] == k])
def _calc_texture_hist(img):
BINS = 10
hist = numpy.array([])
for colour_channel in (0, 1, 2):
fd = img[:, colour_channel]
hist = numpy.concatenate([hist] + [numpy.histogram(fd, BINS, (0.0, 1.0))[0]])
hist = hist / len(img)
return hist
下图简单演示了下,分割结果为0类下的梯度直方图: 2. HSV空间计算颜色直方图
为什么在计算颜色直方图时,把RGB空间转成了HSV空间?在 HSV 颜色空间下,比 BGR 更容易跟踪某种颜色的物体,常用于分割指定颜色的物体,换句话说就是直观和明显(这块我不确定,欢迎指正)
hsv = skimage.color.rgb2hsv(img[:, :, :3])
def _calc_colour_hist(img):
BINS = 25
hist = numpy.array([])
for colour_channel in (0, 1, 2):
c = img[:, colour_channel]
hist = numpy.concatenate(
[hist] + [numpy.histogram(c, BINS, (0.0, 255.0))[0]])
hist = hist / len(img)
return hist
效果展示如下图最后一行:
步骤三:计算有重叠的候选区的相似性
目的: 在步骤一中,得到了一些候选区,这些候选区中存在大量重叠的情况。这些重叠的候选区的特征根据相似性是可以合并的,有效地减少了候选区的数量
思路: 步骤二得到了每一个候选区的颜色直方图和梯度直方图,那么就计算每一对有重叠的候选区的相似性(相似性的计算是selective search算法的重点),迭代循环合并这些有重合候选区中相似性较高的区域。
具体方法:
neighbours = _extract_neighbours(R)
def _extract_neighbours(regions):
def intersect(a, b):
if (a["min_x"] < b["min_x"] < a["max_x"] and a["min_y"] < b["min_y"] < a["max_y"]) or\
(a["min_x"] < b["max_x"] < a["max_x"] and a["min_y"] < b["max_y"] < a["max_y"]) or \
(a["min_x"] < b["min_x"] < a["max_x"] and a["min_y"] < b["max_y"] < a["max_y"]) or \
(a["min_x"] < b["max_x"] < a["max_x"] and a["min_y"] < b["min_y"] < a["max_y"]):
return True
return False
R = regions.items()
r = [elm for elm in R]
R = r
neighbours = []
for cur, a in enumerate(R[:-1]):
for b in R[cur + 1:]:
if intersect(a[1], b[1]):
neighbours.append((a, b))
return neighbours
S = {}
for (ai, ar), (bi, br) in neighbours:
S[(ai, bi)] = _calc_sim(ar, br, imsize)
其中相似性的代码如下:
def _calc_sim(r1, r2, imsize):
return (_sim_colour(r1, r2) + _sim_texture(r1, r2) + _sim_size(r1, r2, imsize) + _sim_fill(r1, r2, imsize))
可见,计算两个候选区的相似性不但用到了颜色直方图_sim_colour,也用到了梯度直方图_sim_texture
下面说下这两个相似性的计算方法: (1)颜色相似性:
S
c
o
l
o
u
r
(
r
i
,
r
j
)
=
∑
k
=
1
n
m
i
n
(
c
i
k
,
c
j
k
)
S_{colour}(r_i,r_j)=\sum_{k=1}^{n}min(c_{i}^{k},c_{j}^{k})
Scolour?(ri?,rj?)=k=1∑n?min(cik?,cjk?) 上式中
r
i
r_{i}
ri?和
r
j
r_{j}
rj?表示有重叠的两个候选区,
c
i
k
c_{i}^{k}
cik?和
c
j
k
c_{j}^{k}
cjk?分别表示这两个候选区的第k个颜色直方图值,取两者最小值累加的结果作为这两个候选区的颜色相似度。
插曲—关于n的取值:如果颜色直方图取25个bin的话,RGB图像每一个通道都会计算25个值,那么三通道加在一起就是75的值,即
n
=
3
?
25
=
75
n=3* 25 = 75
n=3?25=75。
def _sim_colour(r1, r2):
"""
calculate the sum of histogram intersection of colour
"""
return sum([min(a, b) for a, b in zip(r1["hist_c"], r2["hist_c"])])
(2)梯度相似度:
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}^{n}min(t_{i}^{k},t_{j}^{k})
Stexture?(ri?,rj?)=k=1∑n?min(tik?,tjk?) 和上面计算颜色相似度的方法很相似,就是计算这两个候选区下统计的梯度直方图的最小值,累加作为梯度相似度
插曲—关于n的取值:HSV也是三通道,每个通道计算8个方向的梯度,取10个bin的话,
n
=
3
?
8
?
10
=
240
n=3 * 8 *10=240
n=3?8?10=240。
def _sim_texture(r1, r2):
"""
calculate the sum of histogram intersection of texture
"""
return sum([min(a, b) for a, b in zip(r1["hist_t"], r2["hist_t"])])
(3)候选区的大小相似性:
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?)?
s
i
z
e
(
i
m
)
size(im)
size(im)是图片面积,其他的没啥可说的。保证合并操作的尺度较为均匀,避免一个大区域陆续“吃掉”其他小区域。
def _sim_size(r1, r2, imsize):
"""
calculate the size similarity over the image
"""
return 1.0 - (r1["size"] + r2["size"]) / imsize
(4)候选区的互补相似性:
S
f
i
l
l
(
r
i
,
r
j
)
=
1
?
s
i
z
e
(
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{size(BB_{ij})-size(r_{i})-size(r_{j})}{size(im)}
Sfill?(ri?,rj?)=1?size(im)size(BBij?)?size(ri?)?size(rj?)?
s
i
z
e
(
B
B
i
j
)
size(BB_{ij})
size(BBij?)为两个候选区的最大的横坐标减去最小的横坐标,再乘上最大的纵坐标减去最小的纵坐标,即:(x_max-x_min)* (y_max - y_min)(具体见代码) 原文说需要衡量这个值的原因:The idea is to ?ll gaps: if ri is contained in rj it is logical to merge these ?rst in order to avoid any holes. On the other hand, if ri and rj are hardly touching each other they will likely form a strange region and should not be merge。我的理解是避免没有太大重叠时发生合并,以防止生成对模型不利(噪声)的候选区
def _sim_fill(r1, r2, imsize):
"""
calculate the fill similarity over the image
"""
bbsize = (
(max(r1["max_x"], r2["max_x"]) - min(r1["min_x"], r2["min_x"])) * (max(r1["max_y"], r2["max_y"]) - min(r1["min_y"], r2["min_y"]))
)
return 1.0 - (bbsize - r1["size"] - r2["size"]) / imsize
(5)上述四种相似性求和:
s
(
r
i
,
r
j
)
=
a
1
S
c
o
l
o
u
r
(
r
i
,
r
j
)
+
a
2
S
t
e
x
t
u
r
e
(
r
i
,
r
j
)
+
a
3
S
s
i
z
e
(
r
i
,
r
j
)
+
a
4
S
f
i
l
l
(
r
i
,
r
j
)
s(r_{i},r_{j})=a_{1}S_{colour}(r_{i},r_{j})+a_{2}S_{texture}(r_i,r_j)+a_{3}S_{size}(r_{i},r_{j})+a_{4}S_{fill}(r_{i},r_{j})
s(ri?,rj?)=a1?Scolour?(ri?,rj?)+a2?Stexture?(ri?,rj?)+a3?Ssize?(ri?,rj?)+a4?Sfill?(ri?,rj?)
def _calc_sim(r1, r2, imsize):
return (_sim_colour(r1, r2) + _sim_texture(r1, r2) + _sim_size(r1, r2, imsize) + _sim_fill(r1, r2, imsize))
大致思路是遍历相似度对集合S,每次遍历都取两个候选区相似度值最大的做合并,合并后从S中删除这两个候选区,再计算新合并后的候选区与其他候选区的相似度,直到S为空为止。
while S != {}:
i, j = sorted(list(S.items()), key = lambda a: a[1])[-1][0]
t = max(R.keys()) + 1.0
R[t] = _merge_regions(R[i], R[j])
key_to_delete = []
for k, v in S.items():
if (i in k) or (j in k):
key_to_delete.append(k)
for k in key_to_delete:
del S[k]
for k in filter(lambda a: a != (i, j), key_to_delete):
n = k[1] if k[0] in (i, j) else k[0]
S[(t, n)] = _calc_sim(R[t], R[n], imsize)
regions = []
for k, r in R.items():
regions.append({
'rect': (
r['min_x'], r['min_y'],
r['max_x'] - r['min_x'], r['max_y'] - r['min_y']),
'size': r['size'],
'labels': r['labels']
})
return img, regions
总结
到此为止,selective search算法的大部分内容都算是说完了。下面简单看下候选框没有合并操作和有合并操作的区别(直观感受下),上图为未合并之前的,下图为合并之后的。 本文中涉及到的代码和数据集,请关注公众号CV市场, 回复“RCNN”获取。
未完待续……
|