选择搜索(Selective Search)算法介绍与Python实现解释

文章目录

        • 一、算法的主要思路
        • 二、伪代码
        • 三、Python实现与解释
          • 3.1 划分区域
          • 3.2 特征提取
            • 3.2.1 纹理特征
            • 3.2.2 颜色特征
          • 3.3 生成区域集`R`
          • 3.4 相似度计算
          • 3.5 判断并获取相邻区域
          • 3.6 合并区域
          • 3.7 选择搜索算法

选择搜索(Selective Search)算法是一种通过分割图像为小块,然后逐步合并这些小块以获取所需要的子块的启发式算法
在目标检测的经典模型R-CNN中,选择搜索算法被用于生成模型的候选区域,十分重要

一、算法的主要思路

  选择搜索算法的思路很简单,就是输入一个图像,然后通过一些图像分割算法将其分割为很多个小块,这些小块组成一个集合R。在R中对所有相邻的块求相似度,得到新的集合S。对集合S中相似度最高的两个块R1, R2进行合并可以得到新的块R_new,加入R中,同时删除S中所有与R1R2有关的相似度,然后计算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)
三、Python实现与解释
3.1 划分区域

  区域集生成可以考虑使用基于图的图像分割算法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函数对图像进行分割后返回了一个和原图大小相同的掩码,将其存入原图的第四个通道并进行返回。

3.2 特征提取

  主要需要提取图像的纹理特征颜色特征,这两种特征的提取方式都是计算各自特征值的频数

3.2.1 纹理特征

  图像纹理特征的特征值的一种计算方法就是局部二值模式算法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
3.2.2 颜色特征

  图像的颜色特征值就是图像的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
3.3 生成区域集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
3.4 相似度计算

  图像区域相似度的衡量主要考虑四个方面:

  • 纹理相似度: S = ∑ k = 1 n min ⁡ ( t i k , t j k ) S=\sum^{n}_{k=1}{\min(t^k_i, t^k_j)} S=k=1nmin(tik,tjk)
  • 颜色相似度: S = ∑ k = 1 n min ⁡ ( c i k , c j k ) S=\sum^{n}_{k=1}{\min(c^k_i, c^k_j)} S=k=1nmin(cik,cjk)
  • 大小相似度: S = 1 − s i z e i + s i z e j s i z e i m a g e S=1-\frac{size_i+size_j}{size_{image}} S=1sizeimagesizei+sizej
  • 填充相似度: S = 1 − s i z e b o x − s i z e i − s i z e j s i z e i m a g e , s i z e b o x = ( x m a x − x m i n ) ∗ ( y m a x − y m i n ) S=1-\frac{size_{box}-size_i-size_j}{size_{image}},\quad size_{box}=(x_{max}-x_{min})*(y_{max}-y_{min}) S=1sizeimagesizeboxsizeisizej,sizebox=(xmaxxmin)(ymaxymin)

  计算如上四种相似度,然后进行相加,即可得到两个图像区域的相似度。代码实现如下:

# 计算相似度
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
3.5 判断并获取相邻区域

  判断相邻区域的方法较为简单,对于区域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
3.6 合并区域

  区域字典中各属性的合并方法如下:

  • 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
3.7 选择搜索算法

  根据前面介绍的选择搜索算法的主要思想和伪代码,按步骤便可以实现选择搜索算法。代码如下:

# 选择搜索算法
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

你可能感兴趣的:(计算机视觉,算法,python,计算机视觉)