选择搜索(
Selective Search
)算法是一种通过分割图像为小块,然后逐步合并这些小块以获取所需要的子块的启发式算法
在目标检测的经典模型R-CNN
中,选择搜索算法被用于生成模型的候选区域,十分重要
选择搜索算法的思路很简单,就是输入一个图像,然后通过一些图像分割算法将其分割为很多个小块,这些小块组成一个集合R
。在R
中对所有相邻的块求相似度,得到新的集合S
。对集合S
中相似度最高的两个块R1, R2
进行合并可以得到新的块R_new
,加入R
中,同时删除S
中所有与R1
或R2
有关的相似度,然后计算R_new
和所有相邻区域的相似度,加入S
。如此重复迭代计算,直到集合S
中不再包含任何元素即可。
在算法主要思路中提到的图像分割算法,这里采用基于图的图像分割算法(Felzenszwalb
算法),其论文与C++
实现可以在下面的链接查看:基于图的图像分割算法
function selective_search(image):
R = Felzenszwalb(image) # 基于图的图像分割算法,将图像划分为小块
S = {} # 用于存储相似度的集合
for R_1, R_2 in Neighbor(R, R):
S = S.add(Similarity(R_1, R_2)) # 计算相邻区域相似度,存入集合S
while S is not Empty:
R_1, R_2, S_12 = Max_Similarity(S) # 查找出相似度最高的两个区域
R_new = Merge(R_1, R_2) # 合并R_1,R_2
S = Remove_Similarity_About(S, R_1) # 删除与R_1有关的相似度
S = Remove_Similarity_About(S, R_2) # 删除与R_2有关的相似度
for R_new, R_i in Neighbor(R_new, R):
S = S.add(Similarity(R_new, R_i)) # 计算R_new与其相邻的区域的相似度并存入S
R = R.add(R_new)
区域集生成可以考虑使用基于图的图像分割算法Felzenszwalb
。在skimage
库的segmentation
模块中实现了此算法,此处对Felzenszwalb
进行二次封装可以得到如下函数:
# 基于图的图像分割
def Felzenszwalb(img, scale, sigma, min_size):
# img: [h, w, 3]
# mask: [h, w]
mask = felzenszwalb(img_as_float(img), scale=scale, sigma=sigma, min_size=min_size)
# mask_layer: [h, w, 1]
mask_layer = np.zeros(img.shape[:2])[:, :, np.newaxis]
img_mask = np.concatenate([img, mask_layer], axis=2)
img_mask[:, :, 3] = mask
return img_mask
felzenszwalb
函数对图像进行分割后返回了一个和原图大小相同的掩码,将其存入原图的第四个通道并进行返回。
主要需要提取图像的纹理特征和颜色特征,这两种特征的提取方式都是计算各自特征值的频数。
图像纹理特征的特征值的一种计算方法就是局部二值模式算法local binary pattern, LBP
。此算法在skimage
库的feature
模块中已经进行了实现。下面简单说明一下LBP
算法的原理:
假设有一个像素的值为5
,其周围像素从左上角顺时针方向像素值分别为1, 3, 8, 7, 9, 2, 4, 6
。我们将周围像素值大于中心像素的设置为1
,小于的设置为0
,则可得0, 0, 1, 1, 1, 0, 0, 1
。将此0/1
序列视为一个二进制串,转换为十进制后便可用于表示此中心像素周围的纹理特征。如下所示:
[[1, 3, 8], [[0, 0, 1],
[6, 5, 7], => [1, 5, 1], => [0, 0, 1, 1, 1, 0, 0, 1] => 00111001 => 57
[4, 2, 9]] [0, 0, 1]
对skimage.feature.local_binary_pattern
进行二次封装可以得到如下代码:
# 使用局部二值模式算法提取纹理特征
def texture_feature(img):
texture = np.zeros(img.shape)
for c in range(3):
# P: 选择中心像素周围像素点数
# R: 选择像素距离中心像素的最大半径
# R=8, R=1: 选择中心像素8个方向各一个像素点
texture[:, :, c] = LBP(img[:, :, c], P=8, R=1)
return texture
如上代码用于从图像中提取出每个像素点的纹理特征值,下面还需要统计纹理特征值频数以表达整个图像区域的纹理特征,实现代码如下:
# 计算纹理特征频数
def texture_hist(texture, bins=10):
# texture: [<=(h * w), 4]
# 参数 texture 存储的是通过 mask 筛选后属于指定区域的像素的纹理特征值
hist = []
for c in range(3):
hist.append(np.histogram(texture[:, c], bins=bins)[0])
# hist: [3 * bins]
hist = np.concatenate(hist)
# L1 标准化
hist = hist / texture.shape[0]
return hist
图像的颜色特征值就是图像的RGB
像素值,可以直接统计频数用于衡量图像区域颜色特征。与计算纹理特征频数相似,区域颜色特征频数计算函数实现如下:
# 计算颜色特征频数
def color_hist(img, bins=25):
# img: [<=(h * w), 4]
# 参数 img 存储的是通过 mask 筛选后属于指定区域的像素的颜色特征值
hist = []
for c in range(3):
hist.append(np.histogram(img[:, c], bins=bins)[0])
# hist: [3 * bins]
hist = np.concatenate(hist)
# L1 normalize
hist = hist / img.shape[0]
return hist
R
集合R
数据结构为一个字典,其初始key
是存储于图像第四通道的mask
值,其value
是一个存储区域信息的字典。
图像区域字典属性如下:
min_x
:区域横坐标最小值max_x
:区域横坐标最大值min_y
:区域纵坐标最小值max_y
:区域纵坐标最大值region
:列表,存储属于此区域的所有mask
值,区域合并步骤前只有一个size
:区域大小,即像素数量,用于计算图像大小相似度和填充相似度hist_t
:区域纹理特征hist_c
:区域颜色特征 生成区域集R
的过程即是初始化如上数据结构的过程,代码实现如下:
# 获取R集
def get_R(img_mask):
R = {}
# img_mask: [h, w, 4]
# 遍历每一个像素, 并根据 mask 进行归类
for y, w4 in enumerate(img_mask):
for x, (r, g, b, mask) in enumerate(w4):
if mask not in R:
# 将 x_min, y_min 设置为最大值,将 x_max, y_max 设置为最小值, 以便后续的比较
# mask 用于标识像素点是否属于同一区域
R[mask] = {
"min_x": 0xffff, "max_x": 0, "min_y": 0xffff, "max_y": 0,
"region": [mask]
}
if R[mask]["min_x"] > x:
R[mask]["min_x"] = x
if R[mask]["max_x"] < x:
R[mask]["max_x"] = x
if R[mask]["min_y"] > y:
R[mask]["min_y"] = y
if R[mask]["max_y"] < y:
R[mask]["max_y"] = y
# 提取图像区域纹理特征
texture = texture_feature(img_mask)
for k, v in list(R.items()):
# 提取出每个通道中符合掩码的纹理特征值
# [h, w, 4] => [<=(h * w), 4]
texture_mask = texture[img_mask[:, :, 3] == k]
# 存储图像区域大小
R[k]["size"] = texture_mask.shape[0]
# 存储图像区域纹理特征频数
R[k]["hist_t"] = texture_hist(texture_mask)
# 提取出每个通道中符合掩码的像素颜色特征
# [h, w, 4] => [<=(h * w), 4]
color_mask = img_mask[img_mask[:, :, 3] == k]
# 存储图像区域颜色特征频数
R[k]["hist_c"] = color_hist(color_mask)
return R
图像区域相似度的衡量主要考虑四个方面:
计算如上四种相似度,然后进行相加,即可得到两个图像区域的相似度。代码实现如下:
# 计算相似度
def similarity(r1, r2, img_size):
# 纹理相似度
texture_sim = 0
for r1_ht, r2_ht in zip(r1["hist_t"], r2["hist_t"]):
texture_sim += min(r1_ht, r2_ht)
# 颜色相似度
color_sim = 0
for r1_hc, r2_hc in zip(r1["hist_c"], r2["hist_c"]):
color_sim += min(r1_hc, r2_hc)
# 大小相似度
size_sim = 1 - (r1["size"] + r2["size"]) / img_size
# 填充相似度
w_box = (max(r1["max_x"], r2["max_x"]) - min(r1["min_x"], r2["min_x"]))
h_box = (max(r1["max_y"], r2["max_y"]) - min(r1["min_y"], r2["min_y"]))
fill_sim = 1 - (w_box * h_box - r1["size"] - r2["size"]) / img_size
return texture_sim + color_sim + size_sim + fill_sim
判断相邻区域的方法较为简单,对于区域r1,r2
,只需要判断区域r2
的四个角(左上、右上、左下、右下)是否有像素在区域r1
内。如果有,则r1,r2
为相邻区域,否则则不是相邻区域。代码实现如下:
# 判断图像区域是否相邻
def isneighbor(r1, r2):
# r2 在 r1 的左上角
left_top = (r1["min_x"] <= r2["max_x"] <= r1["max_x"]) and (r1["min_y"] <= r2["max_y"] <= r1["max_y"])
# r2 在 r1 的右上角
right_top = (r1["min_x"] <= r2["min_x"] <= r1["max_x"]) and (r1["min_y"] <= r2["max_y"] <= r1["max_y"])
# r2 在 r1 的左下角
left_bottom = (r1["min_x"] <= r2["max_x"] <= r1["max_x"]) and (r1["min_y"] <= r2["min_y"] <= r1["max_y"])
# r2 在 r1 的右下角
right_bottom = (r1["min_x"] <= r2["min_x"] <= r1["max_x"]) and (r1["min_y"] <= r2["min_y"] <= r1["max_y"])
return left_top or right_top or left_bottom or right_bottom
能够判断两个区域是否相邻后,便可以通过遍历区域来查找到所有的相邻区域对。代码实现如下:
# 获取相邻区域对
def neighbors(R):
R = list(R.items())
N = []
# 遍历所有区域(除了最后一个)r1
for i, (k1, r1) in enumerate(R[:-1]):
# 遍历位于 r1 之后的所有区域
for k2, r2 in R[i + 1:]:
if isneighbor(r1, r2):
N.append([(k1, r1), (k2, r2)])
return N
区域字典中各属性的合并方法如下:
min_x
:取两个区域的min_x
中较小的一个max_x
:取两个区域的max_x
中较大的一个min_y
:取两个区域的min_y
中较小的一个max_y
:取两个区域的max_y
中较大的一个size
:两个区域的size
相加region
:合并两个区域的region
列表hist_t
:以区域size
为权求hist_t
的加权平均hist_c
:以区域size
为权求hist_c
的加权平均具体代码实现如下:
# 合并区域
def merge(r1, r2):
r_new = {"min_x": min(r1["min_x"], r2["min_x"]), "max_x": max(r1["max_x"], r2["max_x"]),
"min_y": min(r1["min_y"], r2["min_y"]), "max_y": max(r1["max_y"], r2["max_y"]),
"size": r1["size"] + r2["size"], "region": r1["region"] + r2["region"]}
r_new["hist_t"] = (r1["hist_t"] * r1["size"] + r2["hist_t"] * r2["size"]) / r_new["size"]
r_new["hist_c"] = (r1["hist_c"] * r1["size"] + r2["hist_c"] * r2["size"]) / r_new["size"]
return r_new
根据前面介绍的选择搜索算法的主要思想和伪代码,按步骤便可以实现选择搜索算法。代码如下:
# 选择搜索算法
def selective_search(img, scale, sigma, min_size):
img_size = img.shape[0] * img.shape[1]
# 图像分割
img_mask = Felzenszwalb(img, scale, sigma, min_size)
R = get_R(img_mask)
# 初始化 S 集
S = {}
for (k1, r1), (k2, r2) in neighbors(R):
S[(k1, k2)] = similarity(r1, r2, img_size)
while S:
print(f"R:{len(R)} S:{len(S)}")
# 查找到相似度最高的两个区域
k1, k2 = max(list(S.items()), key=lambda x:x[1])[0]
# 合并生成新区域 r_new, 并将其存入 R 集
r_new = merge(r1, r2)
k_new = max(R.keys()) + 1
R[k_new] = r_new
# 从 S 集查找与 r1, r2 有关的区域
related = []
for k, v in list(S.items()):
if (k1 in k) or (k2 in k):
related.append(k)
# 从 S 集删除与 r1, r2 有关的相似度
for k in related:
S.pop(k)
# 与 r1, r2 相邻和区域也会与 r_new 相邻, 计算新的相似度加入 S 集
for k in [k for k in related if k != (k1, k2)]:
# k_other 为与 r1, r2 相邻的区域的 key
k_other = k[1] if k[0] in (k1, k2) else k[0]
S[(k_new, k_other)] = similarity(R[k_new], R[k_other], img_size)
candidate_regions = []
for k, v in list(R.items()):
candidate_regions.append({
"box": (v["min_x"], v["min_y"], v["max_x"] - v["min_x"], v["max_y"] - v["min_y"]),
"size": v["size"],
"region": v["region"]
})
return candidate_regions