学习笔记-MATLAB的函数bwconncomp算法解析

一、说明。

  bwconncomp函数的作用是在一个二值图像中找出每一个连通分量,并返回一个结构体CC,CC中包含了图像及连通分量的一些属性。
  笔者使用的MATLAB版本为2018a,且在该版本中,函数bwconncomp有两个(如下图所示),本篇文章分析的是第一项的bwconncomp.m文件,第三项的bwconncomp.m文件是默认第一执行的(即在MATLAB search path中的靠前位置),但是由于此函数里的部分函数是mexw64后缀名的文件,本文便未作分析。bwconncomp文件路径
  由于笔者水平有限,文章中不免有错误之处,欢迎读者指出讨论。同时受限于文笔与个人对该函数的理解程度,文章详略分布可能不当,建议读者在阅读时可以选择适当跳过非核心部分。在文章末尾附了相关函数和本人阅读时加的一些便于个人理解的注释的文件链接。
 

二、最终效果

  每一个连通区域内部的标记值都是一样的,不连通区域的标记值各不相同,标记值从1开始,依次增加到图像中连通区域的数量(即图像中若有5个连通区域,则标记值为1,2,3,4,5)。
  最终的输出包括了图像的Connectivity(是4邻域还是8邻域),图像的大小ImageSize,图像的连通区域的数量NumObjects,各连通区域的像素点个数RegionLengths,各连通区域包含像素点的位置RegionIndices(从第一列的第一个像素开始往下数,数完一列,再从下一列开始数到当前像素点)。
  例如:假设某一幅二值图像数据如下:
1|  1 0 1 0 0
2|  0 1 1 1 0
3|  1 0 1 0 1
4|  1 1 1 0 1
5|  0 1 0 1 0
  若我们采用4邻域来计算,则Connectivity = 4;ImageSize = [5 5];NumObjects = 4;RegionLengths = [1 10 1 2];RegionIndices = [1  3 4 7 9 10 11 12 13 14 17   20   23 24];

三、函数算法分析

  笔者将该函数按d功能分为三个部分,第一部分是输入参数的检测,第二部分是连通分量的初步计算,第三部分是连通分量的完善处理并计算图像及连通分量的一些属性。本文的分析侧重于连通分量的计算的算法思想部分,不拘泥于每一行代码的具体实现。

(一)输入参数的检测

  在MATLAB的自带函数中,最开始的部分往往都是对输入参数的检测与处理,以尽早地发现用户给定的输入参数可能存在的问题,不用等到执行出错再停止,并将允许的多种输入进行处理,方便函数之后的计算。

处理步骤
  1. 通过 narginchk(1,2) 限制输入参数的数量在1至2之间。
  2. 通过 validateattributes 函数判断输入的第一个参数类型为‘logical’(逻辑值)或‘numeric’(数值)中的一种,且满足’2d’(二维矩阵), ‘real’(实数), ‘nonsparse’(非稀疏矩阵)1的要求。
  3. 若第一个输入参数为非logical类型(即numeric类型),则将其中的非零元素对应为TRUE(1),零元素对应为FALSE(0)来进行转换。
  4. 如果只有一个输入参数,则默认为8邻域。
  5. 如果第二个输入参数为矩阵
    0 1 0       1 1 1
    1 1 1   或   1 1 1
    0 1 0       1 1 1
    则将变量conn对应赋值为4或赋值为8。否则将其强制转换成double型赋给conn。
  6. 最后检测conn(1)是否为4或8。(其中加了“(1)”是为了处理输入为矩阵这样的输入,将第一个元素作为邻域的表示)
(二)连通分量的初步计算

  该部分是通过调用函数images.internal.coder.intermediateLabelRuns(后文中简称为 intermediateLabelRuns函数 )完成的(该函数代码也附在文末)。

[startRow,endRow,startCol,labelForEachRun,numRuns] = images.internal.coder.intermediateLabelRuns(BW,conn);

  函数intermediateLabelRuns是对二值图片中的每一列的连续像素进行操作的,其中有一个像素为非零值也算作一个区块(根据变量的名称,本文后续称在一列中连续的区块为run)。
例如:假设某一幅二值图像数据如下:
1|  1 0 1 0 0
2|  0 1 1 1 0
3|  1 0 1 0 1
4|  1 1 1 0 1
5|  0 1 0 1 0
则第一列的第一个元素算作一个run,第一列的第3、4个元素共同算作一个run,而第一列的第四个元素和第二列的第四个元素则不算作同一个run,第二列的第五个元素和第三列的第一个元素也不算作同一个run。

intermediateLabelRuns函数输入输出参数解释

  理解了前面的例子,就应该能比较容易地理解5个输出参数的内容表达了。前4个输出参数均为同样大小的列向量,且该大小为第五个参数numRuns的值。如果将这4个列向量排成一个二维矩阵,那么每一行则对应一个run的数据。

  • BW: 是二值图像在bwconncomp函数中转换之后的logical类型的二维矩阵。
  • conn: conn(1)为4或8表示4邻域或8邻域。
  • startRow: 表示每个run的第一行所在的行数组成的列向量。
  • endRow: 表示每个run的最后一行所在的行数组成的列向量。。
  • startCol: 表示每个run所在列数组成的列向量。。(由上面的例子容易知道,每个run只会在某一列。)
  • labelForEachRun: 每个run的标记值组成的列向量。(若有几个run的标记值相同,则代表这几个run是相连的。但不同run的标记值不同却不能断定run之间不相连,这个会在后面解释。)标记值是正整数。
  • numRuns: 数值大小表示二值图内run的数量,且等于上面四个输出参数的长度。
处理步骤
  1. 检测输入参数,同函数第一部分中第2,6两个步骤。
  2. 计算二值图像中run的数量赋给numRuns。
    先用size函数计算得到二值图像的行数M和列数N,如果M和N都不为0则从第一列到第N列,对每一列进行如下处理:
    (1)如果是第一行,若像素值不为0,则run计数加一。
    (2)从第二行到第M行,如果满足当前像素值不为0,且上一行的像素值为0,则可以确定这是一个新的run,则run计数加一。
  3. 判断numRuns是否为0,若为0,即代表整个二值图像像素全为0,则将前四个输出参数赋为空的 0×1 double类型的列向量,最后一个输出参数numRuns赋为0。程序返回,不执行后续的代码。
  4. 若numRuns不为0 ,则继续执行后续的代码。根据numRuns的值为startRow、endRow、startCol分配numRuns × 1大小的列向量的空间,但不赋值。在执行该操作后,仅预分配了内存,但不会导致初始化内存的开销(有兴趣的读者可以搜索coder.nullcopy的相关资料)。
  5. 再次扫描二值图像矩阵,计算得到startRow、endRow、startCol的数据。
    (1)在计算中,row代表当前行,col代表当前列,runCounter是对处理过的run进行计数,以实现将第n个run的数据存入startRow、endRow、startCol的第n个位置。
    (2)处理同样按列进行,在每一列数据处理的开始,row从第一行开始往下逐行递增。若找到了非零元素,则表示到了一个新的run,当前行(row的值)即代表startRow,当前列(col的值)即代表startCol,此时继续往下查找,直到走完了这个当前run。走完当前run有两种情况,情况a——第row行为零元素,则上一行即为endRow;或情况b——到了第M+1行(此时超出了图像底端),那么第row行的上一行也是endRow。
      重复执行以上过程,直到到达图像最后一行,然后再进行下一列的类似处理。
    下面举一个例子:假设某一幅二值图像数据如下:
    1|  1 0 1 0 0
    2|  0 1 1 1 0
    3|  1 0 1 0 1
    4|  1 1 1 0 1
    5|  0 1 0 1 0
      则处理完第一列后,startRow = [1 3 ~]; endRow = [1 4 ~]; startCol = [1 1 ~]; runCounter = 3;(注:其中的”~“代表后续未赋值的数据)。处理完整个图像后,startRow = [1 3 2 4 1 2 5 3]; endRow = [1 4 2 5 4 2 5 4]; startCol = [1 1 2 2 3 4 4 5]。
  6. 上面仅计算了每一列中连通区块run的runstartRow、endRow、startCol,并没有判断不同列之间的连通关系,接下来便初步判断不同列之间的连通关系。
      这一步的判断是找到当前列的当前run与前一列的所有run之间是否存在直接连通关系。在这一过程中有几个比较重要的变量firstRunOnThisColumn(表示当前列的第一个run是全部run中的第几个),firstRunOnPreviousColumn(表示上一列第一个run是全部run中的第几个),lastRunOnPreviousColumn(表示上一列最后一个run是全部run中的第几个),且如果上一列没有run,则这两个变量的值为-1。
    这一步骤的操作为从第1个run开始每个run(以下成为第k个run)执行以下过程:
    (a)先判断当前的这第k个run是否在新的一列,有三种结果。 结果1——进入了上一个run所在列的后一列,则将firstRunOnThisColumn,firstRunOnPreviousColumn,lastRunOnPreviousColumn三个变量对应赋好值。 结果2——进入了与上一列不相连的后面列,这时说明上一列没有run的存在,则就firstRunOnPreviousColumn和lastRunOnPreviousColumn赋为-1,firstRunOnThisColumn的值即赋k。 结果3——仍在当前列,没有进入新列,此时不做任何操作。
    (b)如果上一列没有run,则将第k个run的标记值赋为k,我们可称之为最根源的run,即它是它所在连通区域(此时的连通区域是在二值图像上而言的)中最靠前的run。这个最根源的run在后面的处理会用到,如下面(c)中的第一个例子的第1个run则是它所属区域的最根源的run,最根源run的特征是它的标记值与它在所有run中的它的序号相同,即第k个run的也为k。
    (c)如果上一列有run,则将上一列的第一个run开始至上一列的最后一个run(以下称为第p个run)进行判断,比较第p个run的最后一行与第k个run第一行,第p个run的第一行与第k个run的最后一行的关系(即是否满足前面选定的4邻域或8邻域的关系)。如果满足,则判断当前这第k个run是否已被标记。
      结果1——如果没有被标记,则说明当前第p个run是上一列与第k个run第一个相连的run,那么将第p个run的标记复制给第k个run。
      举一个例子:假设某一幅二值图像数据如下:
    1|  1 1 1 0 0
    2|  0 1 1 1 0
    3|  1 0 1 0 1
    4|  1 1 1 0 1
    5|  0 1 0 1 0
    处理第二列的run之前,标记向量labelForEachRun为[1 2 ~],这里只赋了第一列两个run的标记。在处理第3个run时,发现第3个run与第1个run相连,则第3个run的标记值也赋为1。则标记向量labelForEachRun变为[1 2 1 ~]。
     
      结果2——如果被标记则说明前一列第p个run之前已经有run与第k个run相连,此时就需要比较第p个run和第k个run的标记值是否相等了。
      如果相等那么说明在我们之前的处理中,已经识别到这两个run是相连的,那么我们不需要对它进行处理。
      如果这两个标记值不相等,那么说明这两个相连的run之前被我们当作了不相连的,那么我们就要处理它们。我们先找到这两个run的标记对应的最根源的run(找最根源run的方法是根据这个第m个run的标记值也为m的特征)。如果这两个run对应根源run——root_p和root_k的标记值相的那么就把第k个run的标记值直接赋为root_k(同root_p)。如果root_p和root_k的标记值不相等,则将第k,p个的标记值改为root_p和root_k中的最小值。
      举一个例子:假设某一幅二值图像和它的等效标记情况如下(我们暂时只看前三列,4邻域):
    1|  1 1 0 0 0
    2|  0 0 0 1 0
    3|  0 1 1 0 1
    4|  0 0 1 0 1
    5|  0 1 1 1 0
    处理到第5个run与第4个run的比较之前,该图像的等效标记情况如下:
    1|  1 1 0 0 0
    2|  0 0 0 0 0
    3|  0 3 3 0 0
    4|  0 0 3 0 0
    5|  0 4 3 0 0
    此时第5个run的标记为3,是因为在跟第3个run比较时得到的标记值。当与第4个run比较时,得到root_p和root_k的标记值分别3,4,那么我们将第4,5个run的标记值都取为3,得到结果如下:
    1|  1 1 0 0 0
    2|  0 0 0 0 0
    3|  0 3 3 0 0
    4|  0 0 3 0 0
    5|  0 3 3 0 0

以上的处理初步完成了二值图像标记的初步处理,但在该处理中,看似将连通区域找到了,但是遗留了一点小问题(这个小问题就到了第三部分再处理),读者可以看以下的例子。
某二值图像等效标记数据(进行到第5列的第一个run与第4列的第2个run)如下:
1|  1 1 1 1 1
2|  0 0 0 0 1
3|  0 4 4 4 1
4|  2 0 4 0 1
5|  2 0 4 0 0
根据以上的做法,这幅图像最后的等效标记数据为:
1|  1 1 1 1 1
2|  0 0 0 0 1
3|  0 1 4 4 1
4|  2 0 4 0 1
5|  2 0 4 0 0
与上面的不同在于,将第4个run的标记值4改成了1,但是其他的4并没有改动,这也解释了前面提到的问题,标记值不相同的run不一定不在同一片区域,即存在相互等效的标记(如上面的1和4就是等效的)。并且,细心的读者也可以发现,这些标记值之间并不是连续的。

(三)连通分量的完善处理并计算图像及连通分量的一些属性

  经过了第二部分的预处理,已经得到了startRow、endRow、startCol和初步的labelForEachRun,并又返回到了bwconncomp这个函数。如同上面所述,此时的labelForEachRun存在着标记值之间的等效和不连续的问题,这个问题在这一部分得到处理。这一部分同时还完成最后属性数据的输出。
  这个时候,我们准备给run重新进行标记,得到labelsRenumbered。标记方法为,从第1个run到最后一个run进行如下处理:在处理第k个run时,若第k个run的标记值为k(即它为根源run),则我们令其标记值为numComponents,numComponents为初始为0,且只在重新标记根源run前加1。而对于其他的run,我们将它的标记值赋为labelsRenumbered中的第它原来的标记值的那一个元素的标记值(有点绕口,看下面的例子)。numComponents最后的值即为图像的连通区域数NumObjects。
  例如在这个之前给出的等效标记数据:
1|  1 1 1 1 1
2|  0 0 0 0 1
3|  0 1 4 4 1
4|  2 0 4 0 1
5|  2 0 4 0 0
  第1,3,4,5,7,9个run的重新标记值仍然为1,第二个run的重新标记值仍然为2,而第6,8个run的标记值则改成了labelsRenumbered(4)即为1。
最终结果为:
1|  1 1 1 1 1
2|  0 0 0 0 1
3|  0 1 1 1 1
4|  2 0 1 0 1
5|  2 0 1 0 0
  可以看出,经过了以上处理,便解决了标记值之间的等效和不连续的问题。
  接下来,我们就可以根据标记值相同的run来统计图像中连通区域的像素点个数regionLengths。容易知道,每一个run的endRow - startRow + 1的结果即为这个run的像素点个数,便只需要在循环里将同样的的标记的run的像素点个数累加,即可以得到每个连通区域的像素点个数,最后形成列向量regionLengths。
  接下来我们还要计算一个数据——列向量pixelIdxList。这个列向量的长度为列向量regionLengths的所有元素求和的值。而它前(regionLengths(1))个数据依次记录了了第1个连通区域的所有像素点的位置,第(regionLengths(1)+ 1)个数据至(regionLengths(1)+ regionLengths(2))个数据记录了第2个连通区域的所有像素点的位置,依此类推。其中每个像素点的位置的表示方法在前面最终效果部分已说明。
  其他的几个输出属性在最终效果部分也容易看出,此处不详细讲述。至此,bwconncomp函数算法解析完毕。

四、文件百度网盘链接

链接:https://pan.baidu.com/s/1NBiWxCgXM9y0C-a8Ng_GgA
提取码:r4ua
复制这段内容后打开百度网盘手机App,操作更方便哦


  1. 稀疏矩阵:矩阵中非零元素的个数远远小于矩阵元素的总数,并且非零元素的分布没有规律,通常认为矩阵中非零元素的总数比上矩阵所有元素总数的值小于等于0.05时,则称该矩阵为稀疏矩阵(sparse matrix),该比值称为这个矩阵的稠密度。 ↩︎

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值