本文概述了论文《Efficient Graph-Based Image Segmentation》所述的基于图的图像分割方法。
参考:
- 《Efficient Graph-Based Image Segmentation》代码及论文 http://cs.brown.edu/people/pfelzens/segment/
1、快速开始
(1) 作者给出的C++代码
首先下载《Efficient Graph-Based Image Segmentation》的代码 http://cs.brown.edu/people/pfelzens/segment/,解压下载的文件segment.zip,执行make编译之后,就可以进行图像分割了。分割命令为 “segment sigma k min input output”,这里的输入输出是ppm格式的图像,ppm格式的介绍可以访问这里https://blog.csdn.net/kinghzkingkkk/article/details/70226214。
先把图片转为ppm格式
import cv2
im = cv2.imread("car.png")
cv2.imwrite("car.ppm",im)
然后执行下面的命令完成图像分割。
./segment 0.5 500 20 car.ppm car_seg.ppm
结果如下图所示,左侧是原图,右侧是分割的图。
(2) skimage中的python接口
# encoding:utf-8
import cv2
from skimage import segmentation
import skimage
import numpy
im_orig = cv2.imread("len_std.jpg")
scale = 500
sigma = 0.5
min_size = 20
im_orig = numpy.array(im_orig)
im_mask = segmentation.felzenszwalb(skimage.util.img_as_float(im_orig),
scale=scale,
sigma=sigma,
min_size=min_size)
rect = {}
min_l = numpy.min(im_mask)
max_l = numpy.max(im_mask)
for l in range(int(min_l),int(max_l)+1):
y_arr, x_arr = numpy.where(im_mask==l)
rect[l] = {"min_x": x_arr.min(), "min_y": y_arr.min(),
"max_x": x_arr.max(), "max_y": y_arr.max(), "labels": l}
for l, v in rect.items():
area = (v["max_x"]-v["min_x"]) * (v["max_y"]-v["min_y"])
ratio = (v["max_x"]-v["min_x"]) / (v["max_y"]-v["min_y"])
# 分割的区域太多,这里画出像素数量在(5500,6000),并且宽高比在(0.7,1.3)的区域边框
if area>5500<6000 and ratio>0.7<1.3:
cv2.rectangle(im_orig,(v["min_x"],v["min_y"]),(v["max_x"],v["max_y"]),color=(0,255,0),thickness=2)
# 随机上色
rgb = {}
for l in rect.keys():
# randint参数high为None的时候,生成的是[0,low)的随机整数
rgb[l] = numpy.random.randint(256,size=(3,))
im_tintage = numpy.empty(shape=im_orig.shape, dtype=numpy.uint8)
for height in range(im_orig.shape[0]):
for row in range(im_orig.shape[1]):
im_tintage[height, row, :] = rgb[im_mask[height, row]]
gap = 255*numpy.ones([im_orig.shape[0],50,3],dtype=numpy.uint8)
im_display = numpy.hstack([im_orig, gap,im_tintage])
cv2.imshow("",im_display)
cv2.waitKey(0)
结果如图所示,这里没有画出所有的边框,可以看到中间的边框近似的框住了人脸,但是从右边的染色图来看整个人脸并不是被分割在同一个区域中。
2、算法流程
在上述的算法流程中,G是一个无向图,图G中每个顶点是一个像素点,边E是像素点之间的不相似度,即第3步中。但是实际中不会去计算所有像素点对之间的不相似度,论文中讨论了两种构建图G的情形,grid-graph和Nearest Neighbor Graphs。
第一种情形是grid-graph,这种情况下只计算每个像素点周围8个点的不相似度,因此图像上相近的像素点在图G上也相近。在灰度图中,按照下式来计算两个像素点的不相似度。
其中是像素点的灰度值。如果是彩色图像的话,论文中分别在RGB这3个通道上进行分割,然后将3个通道的分割结果相交,也就是说,在3个通道图里,某两个像素点都属于同一个component,那这两个像素点就属于同一个component。
第二种情形是Nearest Neighbor Graphs,这种情形下将像素点映射到特征空间中再去计算不相似度,论文中提到的方法是将像素点(r,g,b)映射到(x,y,r,g,b),其中x和y是像素点的坐标,以特征空间中的欧式距离作为图G中两个点之间的不相似度。通过ANN算法(Approximate nearest neighbor searching)可以找到每个像素点相邻的点,那么每个像素点要和几个相邻点计算不相似度呢?论文中给出了两个思路,一个是固定相邻点的数量,另外也可以设定一个半径,将此半径范围内的相邻点纳入计算。
在Nearest Neighbor Graphs情形下,图像上离得较远的两个像素点也可能是图G中相邻的点(假如这两个像素点的颜色特别相似的话),因此图像上不相邻的区域可能会划分到同一个component,这在第一种情形grid-graph中是不会出现的。
在分割开始的时候把每个像素点当作一个component,然后逐渐融合。融合过程中,对于某条边,如果这条边的两个顶点不在同一个component,将这两个component记为和,此时如果这条边权值小于或者等于阈值,则将这两个点所属的component进行融合,阈值的计算公式如下:
上式中的是最小生成树中最大的权值,函数计算方法如下,其中k是一个超参数。
3、源码分析
这一部分对http://cs.brown.edu/people/pfelzens/segment/ 给出的源码进行分析,分割的主要过程写在segment-graph.h中,下面对主要代码段进行解释。
(1) 输入
image<rgb> *segment_image(image<rgb> *im, float sigma, float c, int min_size,
int *num_ccs)
(2) 高斯模糊
进行图像分割之前,首先将图像进行平滑(高斯模糊),论文中对此解释说:
We always use a Gaussian with σ = 0.8, which does not produce any visible change to the image but helps remove artifacts.
方差为0.8的高斯模糊示意图如下,左边是原图,右边是平滑的图,细节虽然不是很清楚了,但是整体的色块没有受影响(直观的看这样更容易进行分割)。
int width = im->width();
int height = im->height();
// 将rgb三个通道分离
image<float> *r = new image<float>(width, height);
image<float> *g = new image<float>(width, height);
image<float> *b = new image<float>(width, height);
// smooth each color channel
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
imRef(r, x, y) = imRef(im, x, y).r;
imRef(g, x, y) = imRef(im, x, y).g;
imRef(b, x, y) = imRef(im, x, y).b;
}
}
// 这里的smooth方法实现了高斯模糊
image<float> *smooth_r = smooth(r, sigma);
image<float> *smooth_g = smooth(g, sigma);
image<float> *smooth_b = smooth(b, sigma);
delete r;
delete g;
delete b;
(3) 建立连接图G
连接图中计算了每个像素点相邻的8个点之间的连接权值,边缘点有5个相邻点,角点有3个相邻点。
// build graph
edge *edges = new edge[width * height * 4];
int num = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// 对所有不在最后一列的点,计算该点和右侧点之间的不相似度
if (x < width - 1)
{
edges[num].a = y * width + x;
edges[num].b = y * width + (x + 1);
edges[num].w = diff(smooth_r, smooth_g, smooth_b, x, y, x + 1, y);
num++;
}
// 对所有不在最后一行的点,计算该点和正下方点之间的不相似度
if (y < height - 1)
{
edges[num].a = y * width + x;
edges[num].b = (y + 1) * width + x;
edges[num].w = diff(smooth_r, smooth_g, smooth_b, x, y, x, y + 1);
num++;
}
// 对所有不在最后一行也不在最后一列的点,计算该点和右下方点之间的不相似度
if ((x < width - 1) && (y < height - 1))
{
edges[num].a = y * width + x;
edges[num].b = (y + 1) * width + (x + 1);
edges[num].w = diff(smooth_r, smooth_g, smooth_b, x, y, x + 1, y + 1);
num++;
}
// 对所有不在最后一列也不在第一行的点,计算该点和右上方点之间的不相似度
if ((x < width - 1) && (y > 0))
{
edges[num].a = y * width + x;
edges[num].b = (y - 1) * width + (x + 1);
edges[num].w = diff(smooth_r, smooth_g, smooth_b, x, y, x + 1, y - 1);
num++;
}
}
}
delete smooth_r;
delete smooth_g;
delete smooth_b;
不相似度计算方法如下,这里是用rgb空间的欧式距离作为两个像素点的不相似度,即考虑了像素颜色的相似性。
// dissimilarity measure between pixels
static inline float diff(image<float> *r, image<float> *g, image<float> *b,
int x1, int y1, int x2, int y2)
{
return sqrt(square(imRef(r, x1, y1) - imRef(r, x2, y2)) +
square(imRef(g, x1, y1) - imRef(g, x2, y2)) +
square(imRef(b, x1, y1) - imRef(b, x2, y2)));
}
(4) 分割
图G建好后,利用segment_graph方法分割图G,该方法定义在segment-graph.h中。
// segment
universe *u = segment_graph(width * height, num, edges, c);
segment_graph方法如下所示,该方法将图G中所有的顶点分为多个disjoint-set,组成一个disjoint-set forest(由于每个disjoint-set是一个树结构,多个树就组成了一个森林),然后迭代融合,在disjoint-set forest的实现中使用了union by rank(按秩合并)和path compression(路径压缩)两种优化方法。
/*
* Segment a graph
*
* Returns a disjoint-set forest representing the segmentation.
*
* num_vertices: number of vertices in graph.
* num_edges: number of edges in graph
* edges: array of edges.
* c: constant for treshold function.
*/
universe *segment_graph(int num_vertices, int num_edges, edge *edges,
float c)
{
// sort edges by weight
std::sort(edges, edges + num_edges);
// make a disjoint-set forest 并查集
universe *u = new universe(num_vertices);
// init thresholds
float *threshold = new float[num_vertices];
for (int i = 0; i < num_vertices; i++)
// 论文中的阈值包括两部分 Int(C)+\tau(C)
// 因为开始的时候每个集合只有一个元素,因此Int(C)=0
// 下面的c就是论文中的k,由于目前一个点是一类,因此集合元素数量|C|=1
// 所以初始的阈值就是这里的c(论文中的k)
threshold[i] = THRESHOLD(1, c);
// for each edge, in non-decreasing weight order...
for (int i = 0; i < num_edges; i++)
{
edge *pedge = &edges[i];
// components conected by this edge
int a = u->find(pedge->a); // 当前这条边的起始点所在集合的root节点序号
int b = u->find(pedge->b); // 当前这条边的终止点所在集合的root节点序号
if (a != b)
{
if ((pedge->w <= threshold[a]) && // 权重w是按照欧式距离计算的
(pedge->w <= threshold[b]))
{
u->join(a, b); // 将a和b两个点所在的集合合并
a = u->find(a); // 合并之后将新集合的root节点序号赋给a
// 两个集合合并了之后更新阈值,阈值包括两部分 Int(C)+\tau(C)
// 计算\tau(C)用到了此时集合的元素数量
// Int(C)指的是最小生成树中最大的权值,由于之前将所有的权值升序排列
// 所以当前权值pedge->w就是Int(C)
// 理解这一部分可以参考最小生成树的Kruskal algorithm
threshold[a] = pedge->w + THRESHOLD(u->size(a), c);
}
}
}
// free up
delete threshold;
return u;
}
(5) 合并较小的component
图G分割完成之后,会将包含像素较少的相邻component合并在一起,这里引入了一个超参数k,论文里对k的解释如下:
Thus k effectively sets a scale of observation, in that a larger k causes a preference for larger components.
// post process small components
for (int i = 0; i < num; i++)
{
int a = u->find(edges[i].a);
int b = u->find(edges[i].b);
if ((a != b) && ((u->size(a) < min_size) || (u->size(b) < min_size)))
u->join(a, b);
}
delete[] edges;
*num_ccs = u->num_sets();
(6) 随机上色
图像分割完成之后,对像素点进行随机上色,输出上色的图像。
image<rgb> *output = new image<rgb>(width, height);
// pick random colors for each component
rgb *colors = new rgb[width * height];
for (int i = 0; i < width * height; i++)
colors[i] = random_rgb();
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int comp = u->find(y * width + x);
imRef(output, x, y) = colors[comp];
}
}
delete[] colors;
delete u;
return output;