在目標檢測時,爲了定位到目標的具體位置,通常會把圖像分成許多子塊,然後把子塊作爲輸入,送到目標識別的模型中。分子塊的最直接方法叫滑動窗口法。滑動窗口的方法就是按照子塊的大小在整幅圖像上窮舉所有子圖像塊。這種方法產生的數據量想想都頭大。和滑動窗口法相對的是另外一類基於區域(region proposal)的方法。selective search就是其中之一!
首先通過簡單的區域劃分算法,將圖片劃分成很多小區域,再通過相似度和區域大小(小的區域先聚合,這樣是防止大的區域不斷的聚合小區域,導致層次關係不完全)不斷的聚合相鄰小區域,類似於聚類的思路。這樣就能解決object層次問題。
最後對於計算速度來說,只能說這個思路在相對窮舉,大大減少了後期分類過程中的計算量。
算法流程:
step0:生成區域集R,具體參見論文《Efficient Graph-Based Image Segmentation》
step1:計算區域集R裏每個相鄰區域的相似度S={s1,s2,…}
step2:找出相似度最高的兩個區域,將其合併爲新集,添加進R
step3:從S中移除所有與step2中有關的子集
step4:計算新集與所有子集的相似度
step5:跳至step2,直至S爲空
爲了保證能夠劃分的完全,對於相似度,作者提出了可以多樣化的思路,不但使用多樣的顏色空間(RGB,Lab,HSV等等),還有很多不同的相似度計算方法。
- 顏色空間變換:將原始色彩空間轉換到多達八中的色彩空間。然後通過多樣性的距離計算方式,綜合顏色、紋理等所有的特徵。
- 距離計算方式需要滿足兩個條件,其一速度得快啊,因爲畢竟我們這麼多的區域建議還有這麼多的多樣性。其二是合併後的特徵要好計算,因爲我們通過貪心算法合併區域,如果每次都需要重新計算距離,這個計算量就大太多了。
1.顏色距離就是對各個通道計算顏色直方圖,然後取各個對應bins的直方圖最小值。這樣做的話兩個區域合併後的直方圖也很好計算,直接通過直方圖大小加權區域大小然後除以總區域大小就好了。
2.紋理距離計算方式和顏色距離幾乎一樣,我們計算每個區域的快速sift特徵,其中方向個數爲8,3個通道,每個通道bins爲10,對於每幅圖像得到240維的紋理直方圖
3.優先合併小的區域,如果僅僅是通過顏色和紋理特徵合併的話,很容易使得合併後的區域不斷吞併周圍的區域,後果就是多尺度只應用在了那個局部,而不是全局的多尺度。因此我們給小的區域更多的權重,這樣保證在圖像每個位置都是多尺度的在合併。
4.區域的合適度度距離,不僅要考慮每個區域特徵的吻合程度,區域的吻合度也是重要的,吻合度的意思是合併後的區域要儘量規範,不能合併後出現斷崖的區域,這樣明顯不符合常識,體現出來就是區域的外接矩形的重合面積要大。因此區域的合適度距離定義爲:
5.綜合各種距離,通過多種策略去得到區域建議,最簡單的方法當然是加權:
6.參數初始化多樣性,我們基於基於圖的圖像分割得到初始區域,而這個初始區域對於最終的影響是很大的,因此我們通過多種參數初始化圖像分割,也算是擴充了多樣性。
給區域打分
通過上述的步驟我們能夠得到很多很多的區域,但是顯然不是每個區域作爲目標的可能性都是相同的,因此我們需要衡量這個可能性,這樣就可以根據我們的需要篩選區域建議個數啦。
這篇文章做法是,給予最先合併的圖片塊較大的權重,比如最後一塊完整圖像權重爲1,倒數第二次合併的區域權重爲2以此類推。但是當我們策略很多,多樣性很多的時候呢,這個權重就會有太多的重合了,排序不好搞啊。文章做法是給他們乘以一個隨機數,畢竟3分看運氣嘛,然後對於相同的區域多次出現的也疊加下權重,畢竟多個方法都說你是目標,也是有理由的嘛。這樣我就得到了所有區域的目標分數,也就可以根據自己的需要選擇需要多少個區域了。
python實現:
# -*- coding: utf-8 -*-
from __future__ import division
import skimage.io
import skimage.feature
import skimage.color
import skimage.transform
import skimage.util
import skimage.segmentation
import numpy
# "Selective Search for Object Recognition" by J.R.R. Uijlings et al.
#
# - Modified version with LBP extractor for texture vectorization
def _generate_segments(im_orig, scale, sigma, min_size):
"""
segment smallest regions by the algorithm of Felzenswalb and
Huttenlocher
"""
# open the Image
im_mask = skimage.segmentation.felzenszwalb(
skimage.util.img_as_float(im_orig), scale=scale, sigma=sigma,
min_size=min_size)
# merge mask channel to the image as a 4th channel
im_orig = numpy.append(
im_orig, numpy.zeros(im_orig.shape[:2])[:, :, numpy.newaxis], axis=2)
im_orig[:, :, 3] = im_mask
return im_orig
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"])])
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"])])
def _sim_size(r1, r2, imsize):
"""
calculate the size similarity over the image
"""
return 1.0 - (r1["size"] + r2["size"]) / imsize
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
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))
def _calc_colour_hist(img):
"""
calculate colour histogram for each region
the size of output histogram will be BINS * COLOUR_CHANNELS(3)
number of bins is 25 as same as [uijlings_ijcv2013_draft.pdf]
extract HSV
"""
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
def _calc_texture_gradient(img):
"""
calculate texture gradient for entire image
The original SelectiveSearch algorithm proposed Gaussian derivative
for 8 orientations, but we use LBP instead.
output will be [height(*)][width(*)]
"""
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
def _calc_texture_hist(img):
"""
calculate texture histogram for each region
calculate the histogram of gradient for each colours
the size of output histogram will be
BINS * ORIENTATIONS * COLOUR_CHANNELS(3)
"""
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
def _extract_regions(img):
R = {}
# get hsv image
hsv = skimage.color.rgb2hsv(img[:, :, :3])
# pass 1: count pixel positions
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
# pass 2: calculate texture gradient
tex_grad = _calc_texture_gradient(img)
# pass 3: calculate colour histogram of each region
for k, v in list(R.items()):
# colour histogram
masked_pixels = hsv[:, :, :][img[:, :, 3] == k]
R[k]["size"] = len(masked_pixels / 4)
R[k]["hist_c"] = _calc_colour_hist(masked_pixels)
# texture histogram
R[k]["hist_t"] = _calc_texture_hist(tex_grad[:, :][img[:, :, 3] == k])
return 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 = list(regions.items())
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
def _merge_regions(r1, r2):
new_size = r1["size"] + r2["size"]
rt = {
"min_x": min(r1["min_x"], r2["min_x"]),
"min_y": min(r1["min_y"], r2["min_y"]),
"max_x": max(r1["max_x"], r2["max_x"]),
"max_y": max(r1["max_y"], r2["max_y"]),
"size": new_size,
"hist_c": (
r1["hist_c"] * r1["size"] + r2["hist_c"] * r2["size"]) / new_size,
"hist_t": (
r1["hist_t"] * r1["size"] + r2["hist_t"] * r2["size"]) / new_size,
"labels": r1["labels"] + r2["labels"]
}
return rt
def selective_search(
im_orig, scale=1.0, sigma=0.8, min_size=50):
'''Selective Search
Parameters
----------
im_orig : ndarray
Input image
scale : int
Free parameter. Higher means larger clusters in felzenszwalb segmentation.
sigma : float
Width of Gaussian kernel for felzenszwalb segmentation.
min_size : int
Minimum component size for felzenszwalb segmentation.
Returns
-------
img : ndarray
image with region label
region label is stored in the 4th value of each pixel [r,g,b,(region)]
regions : array of dict
[
{
'rect': (left, top, width, height),
'labels': [...],
'size': component_size
},
...
]
'''
assert im_orig.shape[2] == 3, "3ch image is expected"
# load image and get smallest regions
# region label is stored in the 4th value of each pixel [r,g,b,(region)]
img = _generate_segments(im_orig, scale, sigma, min_size)
if img is None:
return None, {}
imsize = img.shape[0] * img.shape[1]
R = _extract_regions(img)
# extract neighbouring information
neighbours = _extract_neighbours(R)
# calculate initial similarities
S = {}
for (ai, ar), (bi, br) in neighbours:
S[(ai, bi)] = _calc_sim(ar, br, imsize)
# hierarchal search
while S != {}:
# get highest similarity
i, j = sorted(S.items(), key=lambda i: i[1])[-1][0]
# merge corresponding regions
t = max(R.keys()) + 1.0
R[t] = _merge_regions(R[i], R[j])
# mark similarities for regions to be removed
key_to_delete = []
for k, v in list(S.items()):
if (i in k) or (j in k):
key_to_delete.append(k)
# remove old similarities of related regions
for k in key_to_delete:
del S[k]
# calculate similarity set with the new region
for k in [a for a in key_to_delete if a != (i, j)]:
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 list(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
# -*- coding: utf-8 -*-
from __future__ import (
division,
print_function,
)
import skimage.data
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import selectivesearch
def main():
# loading astronaut image
img = skimage.data.astronaut()
# perform selective search
img_lbl, regions = selectivesearch.selective_search(
img, scale=500, sigma=0.9, min_size=10)
candidates = set()
for r in regions:
# excluding same rectangle (with different segments)
if r['rect'] in candidates:
continue
# excluding regions smaller than 2000 pixels
if r['size'] < 2000:
continue
# distorted rects
x, y, w, h = r['rect']
if w / h > 1.2 or h / w > 1.2:
continue
candidates.add(r['rect'])
# draw rectangles on the original image
fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
ax.imshow(img)
for x, y, w, h in candidates:
print(x, y, w, h)
rect = mpatches.Rectangle(
(x, y), w, h, fill=False, edgecolor='red', linewidth=1)
ax.add_patch(rect)
plt.show()
if __name__ == "__main__":
main()