目标检测RCNN系列的候选区选择算法——selective search(原理+Python实现)

原论文:《Selective Search for Object Recognition》
关键字:分层分组算法、初始化区域集、相似度计算
keywords: Hierarchical Grouping Algorithm, Obtaining Initial Regions, Calculating Similarity

一直想总结下目标检测的一系列算法,分为两个主要路线,一个是从RCNN发展起来的两阶段算法,另一个是以YOLO发展的一阶段算法。本篇属于RCNN算法中重要的一部分,用来解决生成候选区域的问题,算是基础,也很重要,尽管该算法已经过时了,但是掌握其中的思想用来解决可以问题还是不错的。本文中涉及到的代码和数据集,请关注公众号CV市场, 回复“RCNN”获取。

简述

作用:在原图片上,以尽可能地生成可能包含目标的候选块。换句话说,避免了穷举法的计算量大且无图像本身信息的缺点。

解决问题:

这位知乎网友说的很好,这块直接引用过来了,原文链接
在这里插入图片描述由于我们事先不知道需要检测哪个类别,因此第一张图的桌子、瓶子、餐具都是一个个候选目标,而餐具包含在桌子这个目标内,勺子又包含在碗内。这张图展示了目标检测的层级关系以及尺度关系,那我们如何去获得这些可能目标的位置呢。常规方法是通过穷举法,就是在原始图片上进行不同尺度不同大小的滑窗,获取每个可能的位置。而这样做的缺点也显而易见,就是计算量实在是太大了,而且由于不可能每个尺度都兼顾到,因此得到的目标位置也不可能那么准。那么我们能不能通过视觉特征去减少这种分类的可能性并提高精确度呢。这就是本文想做的事情。

可用的特征有很多,到底什么特征是有用的呢?我们看第二副图片的两只猫咪,他们的纹理是一样的,因此纹理特征肯定不行了。而如果通过颜色则能很好区分。但是第三幅图变色龙可就不行了,这时候边缘特征、纹理特征又显得比较有用。而在最后一幅图中,我们很容易把车和轮胎看作是一个整体,但是其实这两者的特征差距真的很明显啊,无论是颜色还是纹理或是边缘都差的太远了。而这这是几种情况,自然图像辣么多,我们通过什么特征去区分?应该区分到什么尺度?

selective search的策略是,既然是不知道尺度是怎样的,那我们就尽可能遍历所有的尺度好了,但是不同于暴力穷举,我们可以先得到小尺度的区域,然后一次次合并得到大的尺寸就好了,这样也符合人类的视觉认知。既然特征很多,那就把我们知道的特征都用上,但是同时也要照顾下计算复杂度,不然和穷举法也没啥区别这就是整篇文章的思路。

所以,为了解决纹理特征(梯度信息)不行的问题,使用了颜色特征;为了解决颜色特征不行的问题,使用了纹理特征;为了解决尺度问题,使用了候选区的面积互补性

解决办法(大致思想):

  1. selective search首先使用 Felzenswalb图像分割算法将图像分割成多个不同的块,每一个块代表分割算法得到的一个类,根据每个块的横纵坐标的最大值和最小值来初步产生一个候选区
  2. 上述分割结果为RGB空间,转成HSV空间,计算HSV空间下的颜色直方图
  3. 分割结果使用局部梯度算法(LBP)得到梯度信息,统计梯度直方图
  4. 分别根据颜色直方图和梯度直方图计算分割得到的那些发生重叠的候选区的颜色相似性和梯度相似性,还包括面积相似性、互补相似性
  5. 根据相似性合并这些重叠的候选区
  6. 计算候选区与ground truth的IOU,小于阈值的作为负样本,大于阈值的将真实类别作为其标签

小结: 关键词包括图像分割算法、颜色直方图、梯度直方图、图像梯度计算、相似度计算等,下面会一一详细地介绍这些关键词的思路、方法和Python实现。同时我也在我的博客中,对比分析了使用不同分割算法、梯度计算方法等对模型的影响。

步骤一:使用分割算法分割图像,初步得到候选区

目的: 得到初始候选区

思路: 因为区域级比像素级信息多,所以作者想尽可能地使用基于图片区域特征的分割方法,为了获得一组不跨越多个目标对象的小起始区域,使用了Felzenswalb图像分割算法在一张图片上分割出多个目标。

代码:
下面的代码中,对比了三种不同的felzenszwalb算法:

  1. skimage自带的felzenszwalb算法
  2. skimage自带的felzenszwalb算法cython版转Python代码,更改了高斯模糊
  3. felzenszwalb算法的实现,相比于上一种,区别主要在四邻域和颜色还原

代码中包括魔改的_felzenszwalb_cy.py,想要此部分代码请关注公众w号,回复 999 获取

# coding:utf8
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

    # rescale scale to behave like in reference implementation
    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))

    # 使用RGB距离,计算四邻域
    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)   # root
            self.s = np.ones((n))  # 存储像素点的个数
            self.t = np.ones((n)) * k  # 存储不相似度
            self.k = k

        def find(self, x):    # Find root of node x
            if x == self.f[x]:
                return x
            return self.find(self.f[x])

        def join(self, a, b):  # Join two trees containing nodes n and m
            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]))):
            # 更新类标号:将的类a,b标号统一为的标号a。更新该类的不相似度阈值为:k / (u.s[a]+u.s[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)):
            # 分割后会有很多小区域,当区域像素点的个数小于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")
    # skimage自带的felzenszwalb算法
    seg1 = felzenszwalb(image, scale=K, sigma=sigma, min_size=min_size)
    # skimage自带的felzenszwalb算法cython版转Python代码,更改了高斯模糊
    seg2 = _felzenszwalb_cython(image, scale=K, sigma=sigma, kernel=kernel,min_size=min_size)
    # felzenszwalb算法的实现,相比于上一种,区别主要在四邻域和颜色还原
    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获取)

# pass 1: 根据分割类别,找到分割结果的每个类别像素位置的左上坐标和右下坐标
    for y, i in enumerate(img):
        for x, (r, g, b, l) in enumerate(i):
            # initialize a new region
            if l not in R:
                R[l] = {
                    "min_x": 0xffff, "min_y": 0xffff,
                    "max_x": 0,      "max_y": 0, "labels": [l]}
            # bounding box
            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个初始候选区,接下来就需要对这些区进行合并、删除等。

步骤二:根据分割类别计算颜色直方图梯度直方图,用于后续的候选区删除和合并

目的: 为计算两个直方图,为合并重叠候选区做准备

简述里面已经说过了,既要利用颜色信息,又要利用纹理特征,所以就分别计算了颜色直方图和梯度直方图。

  1. RGB空间计算梯度直方图
# pass 2: 计算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,大致思想就是统计整张图片的梯度,作为图片的纹理信息。
在这里插入图片描述有了整张图像的梯度信息后,按照之前分割算法得到的像素级的分类结果,按照像素的类别计算同类下的梯度直方图

# texture histogram
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):
        # mask by the colour channel
        fd = img[:, colour_channel]
        # calculate histogram for each orientation and concatenate them all
        # and join to the result
        hist = numpy.concatenate([hist] + [numpy.histogram(fd, BINS, (0.0, 1.0))[0]])
    # L1 Normalize
    hist = hist / len(img)
    return hist

下图简单演示了下,分割结果为0类下的梯度直方图:
在这里插入图片描述
2. HSV空间计算颜色直方图

为什么在计算颜色直方图时,把RGB空间转成了HSV空间?在 HSV 颜色空间下,比 BGR 更容易跟踪某种颜色的物体,常用于分割指定颜色的物体,换句话说就是直观和明显(这块我不确定,欢迎指正)

# RGB空间转成HSV空间
hsv = skimage.color.rgb2hsv(img[:, :, :3])
def _calc_colour_hist(img):
    BINS = 25
    hist = numpy.array([])
    for colour_channel in (0, 1, 2):
        # extracting one colour channel
        c = img[:, colour_channel]
        # calculate histogram for each colour and join to the result
        hist = numpy.concatenate(
            [hist] + [numpy.histogram(c, BINS, (0.0, 255.0))[0]])
    # L1 normalize
    hist = hist / len(img)
    return hist

效果展示如下图最后一行:
在这里插入图片描述

步骤三:计算有重叠的候选区的相似性

目的: 在步骤一中,得到了一些候选区,这些候选区中存在大量重叠的情况。这些重叠的候选区的特征根据相似性是可以合并的,有效地减少了候选区的数量

思路: 步骤二得到了每一个候选区的颜色直方图和梯度直方图,那么就计算每一对有重叠的候选区的相似性(相似性的计算是selective search算法的重点),迭代循环合并这些有重合候选区中相似性较高的区域。

具体方法:

1. 得到有重叠部分的候选区
# extract neighbouring information
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
2. 计算这些有重叠候选区两两之间的相似性
# calculate initial similarities
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=1nmin(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=325=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=1nmin(tik,tjk)
和上面计算颜色相似度的方法很相似,就是计算这两个候选区下统计的梯度直方图的最小值,累加作为梯度相似度

插曲—关于n的取值:HSV也是三通道,每个通道计算8个方向的梯度,取10个bin的话, n = 3 ∗ 8 ∗ 10 = 240 n=3 * 8 *10=240 n=3810=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)=1size(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)=1size(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 fill gaps: if ri is contained in rj it is logical to merge these first 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)=a1Scolour(ri,rj)+a2Stexture(ri,rj)+a3Ssize(ri,rj)+a4Sfill(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))
3.合并
大致思路是遍历相似度对集合S,每次遍历都取两个候选区相似度值最大的做合并,合并后从S中删除这两个候选区,再计算新合并后的候选区与其他候选区的相似度,直到S为空为止。
# hierarchal search
  while S != {}:
      # 每次取S中相似度最大的两个候选区
      i, j = sorted(list(S.items()), key = lambda a: a[1])[-1][0]
      # 合并这两个候选区
      t = max(R.keys()) + 1.0
      # 合并结果添加到R中,用于计算与其他未被合并的候选区的相似度
      R[t] = _merge_regions(R[i], R[j])
      # 从S中删除已经被合并的两个候选区
      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”获取。

未完待续……

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值