图像分析:二值图像连通域标记-基于行程的标记方法

一、前言

二值图像,顾名思义就是图像的亮度值只有两个状态:黑(0)和白(255)。二值图像在图像分析与识别中有着举足轻重的地位,因为其模式简单,对像素在空间上的关系有着极强的表现力。在实际应用中,很多图像的分析最终都转换为二值图像的分析,比如:医学图像分析、前景检测、字符识别,形状识别。二值化+数学形态学能解决很多计算机识别工程中目标提取的问题。

二值图像分析最重要的方法就是连通区域标记,它是所有二值图像分析的基础,它通过对二值图像中白色像素(目标)的标记,让每个单独的连通区域形成一个被标识的块,进一步的我们就可以获取这些块的轮廓、外接矩形、质心、不变矩等几何参数。

下面是一个二值图像被标记后,比较形象的显示效果,这就是我们这篇文章的目标。

image

二、连通域

在我们讨论连通区域标记的算法之前,我们先要明确什么是连通区域,怎样的像素邻接关系构成连通。在图像中,最小的单位是像素,每个像素周围有8个邻接像素,常见的邻接关系有2种:4邻接与8邻接。4邻接一共4个点,即上下左右,如下左图所示。8邻接的点一共有8个,包括了对角线位置的点,如下右图所示。

image        image

如果像素点A与B邻接,我们称A与B连通,于是我们不加证明的有如下的结论:

如果A与B连通,B与C连通,则A与C连通。

在视觉上看来,彼此连通的点形成了一个区域,而不连通的点形成了不同的区域。这样的一个所有的点彼此连通点构成的集合,我们称为一个连通区域。

下面这符图中,如果考虑4邻接,则有3个连通区域;如果考虑8邻接,则有2个连通区域。(注:图像是被放大的效果,图像正方形实际只有4个像素)。

image

三、连通区域的标记

连通区域标记算法有很多种,有的算法可以一次遍历图像完成标记,有的则需要2次或更多次遍历图像。这也就造成了不同的算法时间效率的差别,在这里我们介绍2种算法。

第一种算法是现在matlab中连通区域标记函数bwlabel中使的算法,它一次遍历图像,并记下每一行(或列)中连续的团(run)和标记的等价对,然后通过等价对对原来的图像进行重新标记,这个算法是目前我尝试的几个中效率最高的一个,但是算法里用到了稀疏矩阵与Dulmage-Mendelsohn分解算法用来消除等价对,这部分原理比较麻烦,所以本文里将不介绍这个分解算法,取而代这的用图的深度优先遍历来替换等价对。

第二种算法是现在开源库cvBlob中使用的标记算法,它通过定位连通区域的内外轮廓来标记整个图像,这个算法的核心是轮廓的搜索算法,这个我们将在文章中详细介绍。这个算法相比与第一种方法效率上要低一些,但是在连通区域个数在100以内时,两者几乎无差别,当连通区域个数到了 103 103数量级时,上面的算法会比该算法快10倍以上。

四、基于行程的标记

我们首先给出算法的描述,然后再结合实际图像来说明算法的步骤。

1,逐行扫描图像,我们把每一行中连续的白色像素组成一个序列称为一个团(run),并记下它的起点start、它的终点end以及它所在的行号。

2,对于除了第一行外的所有行里的团,如果它与前一行中的所有团都没有重合区域,则给它一个新的标号;如果它仅与上一行中一个团有重合区域,则将上一行的那个团的标号赋给它;如果它与上一行的2个以上的团有重叠区域,则给当前团赋一个相连团的最小标号,并将上一行的这几个团的标记写入等价对,说明它们属于一类。

3,将等价对转换为等价序列,每一个序列需要给一相同的标号,因为它们都是等价的。从1开始,给每个等价序列一个标号。

4,遍历开始团的标记,查找等价序列,给予它们新的标记。

5,将每个团的标号填入标记图像中。

6,结束。

我们来结合一个三行的图像说明,上面的这些操作。

image

第一行,我们得到两个团:[2,6]和[10,13],同时给它们标记1和2。

第二行,我们又得到两个团:[6,7]和[9,10],但是它们都和上一行的团有重叠区域,所以用上一行的团标记,即1和2。

第三行,两个:[2,4]和[7,8]。[2,4]这个团与上一行没有重叠的团,所以给它一个新的记号为3;而[2,4]这个团与上一行的两个团都有重叠,所以给它一个两者中最小的标号,即1,然后将(1,2)写入等价对。

全部图像遍历结束,我们得到了很多个团的起始坐标,终止坐标,它们所在的行以及它们的标号。同时我们还得到了一个等价对的列表。

按照此算法写代码如下:

void bwLabelFunc(Mat bwFrame)
{
  int width = bwFrame.cols;
  int height= bwFrame.rows;
  vector<int> stRun, endRun, rowRun, runLabelInit;
  int totalRuns = 0;
  int lastLineStIdx = 0, lastLineEndIdx = 0;
  vector<pair<int,int>> equivalences;

  
  lastLineStIdx  = 0;
  lastLineEndIdx = 0;

  for (int i=0; i<height; i++)
  {
    for (int j=0; j<width; j++)
    {
      uchar pelVal = bwFrame.at<uchar>(i,j);
      uchar pelValPre, pelValNext; // Get the Pre and Next Pixel Value.
      if (j==0) pelValPre = 255;
      else      pelValPre = bwFrame.at<uchar>(i,j-1);

      if (j<width-1) pelValNext = bwFrame.at<uchar>(i,j+1);
      else           pelValNext = 255;

      if (pelValPre==255 && pelVal==0) // start Valid.
      {
        stRun.push_back(j);
        rowRun.push_back(i);
      }

      if (pelVal==0 && pelValNext==255) // End Valid.
      {
        endRun.push_back(j);

        // Get Label.
        int curLabel = -1;
        for (int m=lastLineStIdx; m<lastLineEndIdx; m++) // Last Line Valid. 
        {
          int startRunLastLine = stRun[m];
          int endRunLastLine   = endRun[m];

          int startRunCur  = stRun[stRun.size()-1];
          int endRunCur    = endRun[stRun.size()-1];
          
          if (startRunLastLine<=endRunCur+1 && endRunLastLine+1>=startRunCur) // Connection Label.
          {
            if (curLabel==-1)
            {
              curLabel = runLabelInit[m];
            }
            else // Set Connection Equal Label.
            {
              equivalences.push_back(make_pair(curLabel, runLabelInit[m]));
            }
          }
        }
        if (curLabel==-1)
        {
          curLabel = totalRuns;
          totalRuns++;
        }

        runLabelInit.push_back(curLabel);
      }
    }
    lastLineStIdx  = lastLineEndIdx; // Update Last Line's Start/End Run.
    lastLineEndIdx = endRun.size();
  }

  // DAG; Find the Same Connection Label.
  int maxLabel = *max_element(runLabelInit.begin(), runLabelInit.end());
  vector<vector<bool>> eqTab(totalRuns, vector<bool>(totalRuns, false)); // graph Init.
  // Construct Graph.
  vector<pair<int, int>>::iterator vecPairIt = equivalences.begin();
  while (vecPairIt != equivalences.end())
  {
    eqTab[vecPairIt->first][vecPairIt->second] = true;
    eqTab[vecPairIt->second][vecPairIt->first] = true;
    vecPairIt++;
  }

  vector<int> labelFlag(totalRuns, 0);
  vector<vector<int>> equaList;
  vector<int> tempList;
  // cout << maxLabel << endl;
  for (int i = 0; i <= maxLabel; i++)
  {
    if (labelFlag[i])
    {
      continue;
    }
    labelFlag[i] = equaList.size() + 1;
    tempList.push_back(i);

    // BFS Search Algorithm.
    for (vector<int>::size_type j = 0; j < tempList.size(); j++)
    {
      for (vector<bool>::size_type k = 0; k != eqTab[tempList[j]].size(); k++)
      {
        if (eqTab[tempList[j]][k] && !labelFlag[k])
        {
          tempList.push_back(k);
          labelFlag[k] = equaList.size() + 1;
        }
      }
    }
    equaList.push_back(tempList);
    tempList.clear();
  }
  /*cout << equaList.size() << endl;
  for (vector<int>::size_type i = 0; i != runLabels.size(); i++)
  {
    runLabels[i] = labelFlag[runLabels[i] - 1];
  }*/
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值