基于图的图像分割方法 Efficient Graph-Based Image Segmentation

本文概述了论文《Efficient Graph-Based Image Segmentation》所述的基于图的图像分割方法。

参考:

  1. 《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步中w(o_q)。但是实际中不会去计算所有像素点对之间的不相似度,论文中讨论了两种构建图G的情形,grid-graph和Nearest Neighbor Graphs。

第一种情形是grid-graph,这种情况下只计算每个像素点周围8个点的不相似度,因此图像上相近的像素点在图G上也相近。在灰度图中,按照下式来计算两个像素点的不相似度。

w((v_i,v_j))=|I(p_i)-I(p_j)|

其中I(p_i)是像素点p_i的灰度值。如果是彩色图像的话,论文中分别在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记为C_1C_2,此时如果这条边权值小于或者等于阈值MInt(C_1,C_2),则将这两个点所属的component进行融合,阈值的计算公式如下:

MInt(C_1,C_2)=min(Int(C_1)+\tau(C_1),Int(C_2)+\tau(C_2))

上式中的Int(C_1)C_1最小生成树中最大的权值,函数\tau(C)计算方法如下,其中k是一个超参数。

\tau (C) = \frac{k}{|C|}

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;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值