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

来源:互联网 发布:oc中往数组添加元素 编辑:程序博客网 时间:2024/05/15 01:44

一、前言

二值图像,顾名思义就是图像的亮度值只有两个状态:黑(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

两种连通区域

 四连通区域:从区域内一点出发,可通过上、下、左、右四个方向的移动组合,在不越出区域的前提下,能到达区域内的任意像素


 八连通区域:从区域内每一像素出发,可通过八个方向,即上、下、左、右、左上、右上、左下、右下移动的组合,在不越出区域的前提下,能到达区域内的任意像素。



基本原理

从多边形区域内部的某一像素点(称为种子)开始,由此出发找到区域内的其它所有像素。


采用的边界定义

区域边界上所有像素均具有某个特定的颜色值,区域内部所有像素均不取这一特定颜色,而边界外的像素则可具有与边界相同的颜色值。


算法的执行过程:

从(x,y)开始,先检测该点的颜色,若它与边界色和填充色均不相同,则用填充色填充该点。然后检测相邻位置,以确定它们是否是边界色和填充色,若不是,则填充该相邻点。直到检测完区域边界范围内的所有像素为止。
 从当前点检测相邻像素的方法:四连通或八连通
 从四个方向寻找下一个像素,称为四向算法(只能填充四连通区域);
 从八个方向寻找下一个像素,称为八向算法(可以填充八连通区域和四连通区域)。


四连通区域的种子填充递归算法:

[cpp] view plain copy
  1. void ZhongZiTC4 (int seedx, int seedy, int fcolor, int bcolor)  
  2. {      
  3.      int current = getpixel (seedx, seedy);  
  4.      if ((current != bcolor) && (current != fcolor))  
  5.      {   putpixel (seedx, seedy, fcolor);  
  6.      ZhongZiTC4 (seedx+1, seedy, fcolor, bcolor);  //右  
  7.      ZhongZiTC4 (seedx–1, seedy, fcolor, bcolor);  //左  
  8.      ZhongZiTC4 (seedx, seedy+1, fcolor, bcolor);  //上  
  9.      ZhongZiTC4 (seedx, seedy–1, fcolor, bcolor);  //下  
  10.      }  
  11. }   


三、连通区域的标记

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

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

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

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

按照此算法写代码如下:

[cpp] view plain copy
  1. void bwLabelFunc(Mat bwFrame)  
  2. {  
  3.   int width = bwFrame.cols;  
  4.   int height= bwFrame.rows;  
  5.   vector<int> stRun, endRun, rowRun, runLabelInit;  
  6.   int totalRuns = 0;  
  7.   int lastLineStIdx = 0, lastLineEndIdx = 0;  
  8.   vector<pair<int,int>> equivalences;  
  9.   
  10.     
  11.   lastLineStIdx  = 0;  
  12.   lastLineEndIdx = 0;  
  13.   
  14.   for (int i=0; i<height; i++)  
  15.   {  
  16.     for (int j=0; j<width; j++)  
  17.     {  
  18.       uchar pelVal = bwFrame.at<uchar>(i,j);  
  19.       uchar pelValPre, pelValNext; // Get the Pre and Next Pixel Value.  
  20.       if (j==0) pelValPre = 255;  
  21.       else      pelValPre = bwFrame.at<uchar>(i,j-1);  
  22.   
  23.       if (j<width-1) pelValNext = bwFrame.at<uchar>(i,j+1);  
  24.       else           pelValNext = 255;  
  25.   
  26.       if (pelValPre==255 && pelVal==0) // start Valid.  
  27.       {  
  28.         stRun.push_back(j);  
  29.         rowRun.push_back(i);  
  30.       }  
  31.   
  32.       if (pelVal==0 && pelValNext==255) // End Valid.  
  33.       {  
  34.         endRun.push_back(j);  
  35.   
  36.         // Get Label.  
  37.         int curLabel = -1;  
  38.         for (int m=lastLineStIdx; m<lastLineEndIdx; m++) // Last Line Valid.   
  39.         {  
  40.           int startRunLastLine = stRun[m];  
  41.           int endRunLastLine   = endRun[m];  
  42.   
  43.           int startRunCur  = stRun[stRun.size()-1];  
  44.           int endRunCur    = endRun[stRun.size()-1];  
  45.             
  46.           if (startRunLastLine<=endRunCur+1 && endRunLastLine+1>=startRunCur) // Connection Label.  
  47.           {  
  48.             if (curLabel==-1)  
  49.             {  
  50.               curLabel = runLabelInit[m];  
  51.             }  
  52.             else // Set Connection Equal Label.  
  53.             {  
  54.               equivalences.push_back(make_pair(curLabel, runLabelInit[m]));  
  55.             }  
  56.           }  
  57.         }  
  58.         if (curLabel==-1)  
  59.         {  
  60.           curLabel = totalRuns;  
  61.           totalRuns++;  <li style="box-sizing: border-box; margin-left: 40px; list-style: decimal; border-top: none; border-right: none; border-bottom: none; border-left: 3px solid rgb(108, 226, 108); border-image: initial; background-color: rgb(248, 248, 248); line-height: 22.5px; margin-top: 0px !important; margin-right: 0px !important; margin-bottom: 0px !
阅读全文
0 0
原创粉丝点击