在计算机视觉领域里,图像分割(Segmentation)是指将数字图像分割成一些列像素集合的过程。这些集合也被成为超像素。因为分割是基于一定标准进行像素划分,归类的,所以形成的超像素中的像素一般具有位置相邻且颜色、亮度、纹理等特征相似等特点。这些小区域大多保留了进一步进行图像处理的有效信息,且一般不会破坏图像中物体的边界信息。因此超像素分割可以作为一些算法的前处理。接下来介绍一种常用的超像素分割算法SLIC(simple linear iterativeclustering)即简单的线性迭代聚类。
SLIC算法流程是:
- 将彩色图像转化为LAB颜色空间。
- 接着,由LAB颜色空间向量和图像本身的位置(row, col) 向量构建新的5维特征向量(即 new_vector[l, a, b, row, col])。
- 然后,根据针对5维向量构建的距离标准来聚类。
聚类算法流程如下:
初始化种子点(聚类中心):按照设定的超像素个数,在图像内均匀的分配种子点。假设图片总共有 N 个像素点,预分割为 K 个相同尺寸的超像素,那么每个超像素的大小为N/ K ,则相邻种子点的距离(步长)近似为。
在种子点的n*n邻域内重新选择种子点(一般取n=3)。具体方法为:计算该邻域内所有像素点的梯度值,将种子点移到该邻域内梯度最小的地方。这样做的目的是为了避免种子点落在梯度较大的轮廓边界上,以免影响后续聚类效果。
在每个种子点周围的邻域内为每个像素点分配类标签(即属于哪个聚类中心)。和标准的k-means在整张图中搜索不同,SLIC的搜索范围限制为,可以加速算法收敛。在此注意一点:期望的超像素尺寸为,但是搜索的范围是。
距离度量。包括颜色距离和空间距离。对于每个搜索到的像素点,分别计算它和该种子点的距离。距离计算方法如下:
其中,dc代表颜色距离,ds代表空间距离,Ns是类内最大空间距离,定义为,适用于每个聚类。最大的颜色距离Nc既随图片不同而不同,也随聚类不同而不同,一般取值范围是取值范围[1,40]。D'就是最后5维向量的距离,是由颜色距离和空间距离构成的2维向量的欧氏距离。
由于每个像素点都会被多个种子点搜索到,所以每个像素点都会有一个与周围种子点的距离,取最小值对应的种子点作为该像素点的聚类中心。
- 迭代优化。理论上上述步骤不断迭代直到误差收敛(可以理解为每个像素点聚类中心不再发生变化为止),实践发现10次迭代对绝大部分图片都可以得到较理想效果,所以一般迭代次数取10。
下面是简易的python实现:
import math
from skimage import io, color
import numpy as np
class Cluster(object):
cluster_index = 1
def __init__(self, row, col, l=0, a=0, b=0):
self.row = row
self.col = col
self.l = l
self.a = a
self.b = b
self.pixels = []
self.no = self.cluster_index
Cluster.cluster_index += 1
def update(self, row, col, l, a, b):
self.row = row
self.col = col
self.l = l
self.a = a
self.b = b
class SLICProcessor(object):
@staticmethod
def open_image(path):
rgb = io.imread(path)
lab_arr = color.rgb2lab(rgb)
return lab_arr
@staticmethod
def save_lab_image(path, lab_arr):
rgb_arr = color.lab2rgb(lab_arr)
io.imsave(path, rgb_arr)
def make_cluster(self, row, col):
row=int(row)
col=int(col)
return Cluster(row, col,
self.data[row][col][0],
self.data[row][col][1],
self.data[row][col][2])
# K是超像素的个数,M是用于计算lab空间颜色向量距离的参数
def __init__(self, filename, K, M):
self.K = K
self.M = M
self.data = self.open_image(filename)
self.rows = self.data.shape[0]
self.cols = self.data.shape[1]
self.N = self.rows * self.cols
self.S = int(math.sqrt(self.N / self.K))
self.clusters = []
self.label = {}
self.dis = np.full((self.rows, self.cols), np.inf)
def init_clusters(self):
row = self.S / 2
col = self.S / 2
while row < self.rows:
while col < self.cols:
self.clusters.append(self.make_cluster(row, col))
col+= self.S
col = self.S / 2
row += self.S
def get_gradient(self, row, col):
if col + 1 >= self.cols:
col = self.cols - 2
if row + 1 >= self.rows:
row = self.rows - 2
gradient = (self.data[row + 1][col][0] + self.data[row][col+1][0] - 2 * self.data[row][col][0]) + \
(self.data[row + 1][col][1] + self.data[row][col+1][1] - 2 * self.data[row][col][1]) + \
(self.data[row + 1][col][2] + self.data[row][col+1][2] - 2 * self.data[row][col][2])
return gradient
def move_clusters(self):
for cluster in self.clusters:
cluster_gradient = self.get_gradient(cluster.row, cluster.col)
new_row = cluster.row
new_col = cluster.col
for dh in range(-1, 2):
for dw in range(-1, 2):
_row = cluster.row + dh
_col = cluster.col + dw
new_gradient = self.get_gradient(_row, _col)
if new_gradient < cluster_gradient:
cluster_gradient = new_gradient
new_row = _row
new_col = _col
cluster.update(new_row, new_col, self.data[new_row][new_col][0], self.data[new_row][new_col][1], self.data[new_row][new_col][2])
def assignment(self):
for cluster in self.clusters:
for h in range(cluster.row - 2 * self.S, cluster.row + 2 * self.S):
if h < 0 or h >= self.rows: continue
for w in range(cluster.col - 2 * self.S, cluster.col + 2 * self.S):
if w < 0 or w >= self.cols: continue
L, A, B = self.data[h][w]
Dc = math.sqrt(
math.pow(L - cluster.l, 2) +
math.pow(A - cluster.a, 2) +
math.pow(B - cluster.b, 2))
Ds = math.sqrt(
math.pow(h - cluster.row, 2) +
math.pow(w - cluster.col, 2))
D = math.sqrt(math.pow(Dc / self.M, 2) + math.pow(Ds / self.S, 2))
if D < self.dis[h][w]:
if (h, w) not in self.label:
self.label[(h, w)] = cluster
cluster.pixels.append((h, w))
else:
self.label[(h, w)].pixels.remove((h, w))
self.label[(h, w)] = cluster
cluster.pixels.append((h, w))
self.dis[h][w] = D
def update_cluster(self):
for cluster in self.clusters:
sum_h = sum_w = number = 0
for p in cluster.pixels:
sum_h += p[0]
sum_w += p[1]
number += 1
_h =int(sum_h / number)
_w =int(sum_w / number)
cluster.update(_h, _w, self.data[_h][_w][0], self.data[_h][_w][1], self.data[_h][_w][2])
def save_current_image(self, name):
image_arr = np.copy(self.data)
for cluster in self.clusters:
for p in cluster.pixels:
image_arr[p[0]][p[1]][0] = cluster.l
image_arr[p[0]][p[1]][1] = cluster.a
image_arr[p[0]][p[1]][2] = cluster.b
# 迭代后的聚合点 -- 如果要显示请反注释下面的代码
# image_arr[cluster.row][cluster.col][0] = 0
# image_arr[cluster.row][cluster.col][1] = 0
# image_arr[cluster.row][cluster.col][2] = 0
self.save_lab_image(name, image_arr)
def iterates(self, output_path):
self.init_clusters()
self.move_clusters()
#考虑到效率和效果,折中选择迭代10次
for i in range(10):
self.assignment()
self.update_cluster()
self.save_current_image(output_path)
if __name__ == '__main__':
p = SLICProcessor("path of the image you want to process", 200, 40)
p.iterates("path of the image you want to save")
效果如下: