一、前言

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

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

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

二、连通域

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

        

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

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

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

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

三、连通区域的标记

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

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

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

四、基于行程的标记

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

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

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

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

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

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

6,结束。

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

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

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

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

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

下面我们用C++实现上面的过程,即步骤2,分两个进行:

1)fillRunVectors函数完成所有团的查找与记录;

void fillRunVectors(const Mat& bwImage, int& NumberOfRuns, vector<int>& stRun, vector<int>& enRun, vector<int>& rowRun)
{
    for (int i = 0; i < bwImage.rows; i++)
    {
        const uchar* rowData = bwImage.ptr<uchar>(i);

if (rowData[0] == 255)
        {
            NumberOfRuns++;
            stRun.push_back(0);
            rowRun.push_back(i);
        }
        for (int j = 1; j < bwImage.cols; j++)
        {
            if (rowData[j - 1] == 0 && rowData[j] == 255)
            {
                NumberOfRuns++;
                stRun.push_back(j);
                rowRun.push_back(i);
            }
            else if (rowData[j - 1] == 255 && rowData[j] == 0)
            {
                enRun.push_back(j - 1);
            }
        }
        if (rowData[bwImage.cols - 1])
        {
            enRun.push_back(bwImage.cols - 1);
        }
    }
}

//fillRunVectors

2)firstPass函数完成团的标记与等价对列表的生成。相比之下第二个函数要稍微难理解一些。

void firstPass(vector<int>& stRun, vector<int>& enRun, vector<int>& rowRun, int NumberOfRuns,
    vector<int>& runLabels, vector<pair<int, int>>& equivalences, int offset)
{
    runLabels.assign(NumberOfRuns, 0);
    int idxLabel = 1;
    int curRowIdx = 0;
    int firstRunOnCur = 0;
    int firstRunOnPre = 0;
    int lastRunOnPre = -1;
    for (int i = 0; i < NumberOfRuns; i++)
    {
        if (rowRun[i] != curRowIdx)
        {
            curRowIdx = rowRun[i];
            firstRunOnPre = firstRunOnCur;
            lastRunOnPre = i - 1;
            firstRunOnCur = i;

}
        for (int j = firstRunOnPre; j <= lastRunOnPre; j++)
        {
            if (stRun[i] <= enRun[j] + offset && enRun[i] >= stRun[j] - offset)
            {
                if (runLabels[i] == 0) // 没有被标号过
                    runLabels[i] = runLabels[j];
                else if (runLabels[i] != runLabels[j])// 已经被标号            
                    equivalences.push_back(make_pair(runLabels[i], runLabels[j])); // 保存等价对
            }
        }
        if (runLabels[i] == 0) // 没有与前一列的任何run重合
        {
            runLabels[i] = idxLabel++;
        }

}
}

//firstPass

接下来是我们的重点,即等价对的处理,我们需要将它转化为若干个等价序列。比如有如下等价对:

(1,2),(1,6),(3,7),(9-3),(8,1),(8,10),(11,5),(11,8),(11,12),(11,13),(11,14),(15,11)

我们需要得到最终序列是:

list1:  1-2-5-6-8-10-11-12-13-14-15

list2:  3-7-9

list3:  4

一个思路是将1-15个点都看成图的结点,而等价对(1,2)说明结点1与结点2之间有通路,而且形成的图是无向图,即(1,2)其实包含了(2,1)。我们需要遍历图,找出其中的所有连通图。所以我们采用了图像深入优先遍历的原理,进行等价序列的查找。

从结点1开始,它有3个路径1-2,1-6,1-8。2和6后面都没有路径,8有2条路径通往10和11,而10没有后续路径,11则有5条路径通往5,12,13,14,15。等价表1查找完毕。

第2条等价表从3开始,则只有2条路径通向7和9,7和9后面无路径,等价表2查找完毕。

最后只剩下4,它没有在等价对里出现过,所以单儿形成一个序列(这里假设步骤2中团的最大标号为15)。

        

下面是这个过程的C++实现,每个等价表用一个vector<int>来保存,等价对列表保存在map<pair<int,int>>里。

void replaceSameLabel(vector<int>& runLabels, vector<pair<int, int>>&
    equivalence)
{
    int maxLabel = *max_element(runLabels.begin(), runLabels.end());
    vector<vector<bool>> eqTab(maxLabel, vector<bool>(maxLabel, false));
    vector<pair<int, int>>::iterator vecPairIt = equivalence.begin();
    while (vecPairIt != equivalence.end())
    {
        eqTab[vecPairIt->first - 1][vecPairIt->second - 1] = true;
        eqTab[vecPairIt->second - 1][vecPairIt->first - 1] = true;
        vecPairIt++;
    }
    vector<int> labelFlag(maxLabel, 0);
    vector<vector<int>> equaList;
    vector<int> tempList;
    cout << maxLabel << endl;
    for (int i = 1; i <= maxLabel; i++)
    {
        if (labelFlag[i - 1])
        {
            continue;
        }
        labelFlag[i - 1] = equaList.size() + 1;
        tempList.push_back(i);
        for (vector<int>::size_type j = 0; j < tempList.size(); j++)
        {
            for (vector<bool>::size_type k = 0; k != eqTab[tempList                [j] - 1].size(); k++)
            {
                if (eqTab[tempList[j] - 1][k] && !labelFlag[k])
                {
                    tempList.push_back(k + 1);
                    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];
    }
}

//replaceSameLabel

五、基于轮廓的标记

在这里我还是先给出算法描述:

1,从上至下,从左至右依次遍历图像。

2,如下图A所示,A为遇到一个外轮廓点(其实上遍历过程中第一个遇到的白点即为外轮廓点),且没有被标记过,则给A一个新的标记号。我们从A点出发,按照一定的规则(这个规则后面详细介绍)将A所在的外轮廓点全部跟踪到,然后回到A点,并将路径上的点全部标记为A的标号。

3,如下图B所示,如果遇到已经标记过的外轮廓点A ′   ,则从A ′   向右,将它右边的点都标记为A ′   的标号,直到遇到黑色像素为止。

4,如下图C所示,如果遇到了一个已经被标记的点B,且是内轮廓的点(它的正下方像素为黑色像素且不在外轮廓上),则从B点开始,跟踪内轮廓,路径上的点都设置为B的标号,因为B已经被标记过与A相同,所以内轮廓与外轮廓将标记相同的标号。

5,如下图D所示,如果遍历到内轮廓上的点,则也是用轮廓的标号去标记它右侧的点,直到遇到黑色像素为止。

6,结束。

所以总结起来,这个算法步骤非常简单:

1)找到所有外轮廓及与之对应的内轮廓(如果有的话),并给轮廓上的点标上标号。

2)遍历标记的图像,如果这个点在原来二值图像上为白色点,且在标记图像上没有标记过,则给它一个标号且等于它左边点的标号。

显然,这个算法的重点在于轮廓的查找与标记,而对于轮廓的查找,就是确定搜索策略的问题,我们下面给内轮廓与外轮廓定义tracker规则。

我们对一点像素点周围的8个位置作一个记号,对于外轮廓上的点A,如果它是起始点,则从位置7开始按顺时针方向查找,直到遇到白色像素,则按那个方向搜索一步。如果不是起始点,那它有一个进入点位置(路径传过来的位置)N,我们则从d+2(mod8)  的位置开始搜过,直到遇到白色像素点为止,确定为下一步的方向,这里可能从哪里进来,还从那个方向出去。

这里特别要做的一步是,一个点在它周围搜索过程中,在找到白色的路上,把它们都标记为负值,说明已经搜索过。

如下边中间图像所示,从S点开始形成的路径是STUTSVWVS。最右边图像显示了P点的搜索路径,完成搜索后,轮廓点的周围都被标记为了负值。

     

在OpenCV中查找轮廓的函数已经存在了,而且可以得到轮廓之间的层次关系。这个函数按上面的算法实现起来并不困难,所以这里就不再实现这个函数,有兴趣的可以看OpenCV的源码(contours.cpp)。

void bwLabel(const Mat& imgBw, Mat & imgLabeled)
{
    // 对图像周围扩充一格
    Mat imgClone = Mat(imgBw.rows + 1, imgBw.cols + 1, imgBw.type(), Scalar(0));
    imgBw.copyTo(imgClone(Rect(1, 1, imgBw.cols, imgBw.rows)));

imgLabeled.create(imgClone.size(), imgClone.type());
    imgLabeled.setTo(Scalar::all(0));

vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(imgClone, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);

vector<int> contoursLabel(contours.size(), 0);
    int numlab = 1;
    // 标记外围轮廓
    for (vector<vector<Point>>::size_type i = 0; i < contours.size(); i++)
    {
        if (hierarchy[i][3] >= 0) // 有父轮廓
        {
            continue;
        }
        for (vector<Point>::size_type k = 0; k != contours[i].size(); k++)
        {
            imgLabeled.at<uchar>(contours[i][k].y, contours[i][k].x) = numlab;
        }
        contoursLabel[i] = numlab++;
    }
    // 标记内轮廓
    for (vector<vector<Point>>::size_type i = 0; i < contours.size(); i++)
    {
        if (hierarchy[i][3] < 0)
        {
            continue;
        }
        for (vector<Point>::size_type k = 0; k != contours[i].size(); k++)
        {
            imgLabeled.at<uchar>(contours[i][k].y, contours[i][k].x) = contoursLabel[hierarchy[i][3]];
        }
    }
    // 非轮廓像素的标记
    for (int i = 0; i < imgLabeled.rows; i++)
    {
        for (int j = 0; j < imgLabeled.cols; j++)
        {
            if (imgClone.at<uchar>(i, j) != 0 && imgLabeled.at<uchar>(i,j) == 0)
            {
                imgLabeled.at<uchar>(i, j) = imgLabeled.at<uchar>(i, j-1);
            }
        }
    }
    imgLabeled = imgLabeled(Rect(1, 1, imgBw.cols, imgBw.rows)).clone(); // 将边界裁剪掉1像素
}

//bwLabel

来源: <http://www.blue1000.com/bkhtml/c17/2014-03/71515.htm>
来自为知笔记(Wiz)

转载于:https://www.cnblogs.com/fireae/p/3723785.html

寻找二值图像的连通域算法分析相关推荐

  1. 寻找二值图像连通域的算法分析

    本文为博客园转载文章,原文地址如下: https://www.cnblogs.com/fireae/p/3723785.html 一.前言 二值图像,顾名思义就是图像的亮度值只有两个状态:黑(0)和白 ...

  2. Matlab中寻找二值图像最大联通域的算法

    srcImage为二值图像,里面至少有一个联通域 bwlabel函数对每一个联通域进行标记,得到标记后的图像L,num为联通域的个数 [L,num]=bwlabel(srcImage);        ...

  3. 寻找连通域算法_FPGA实现的连通域识别算法升级

    代码在这:https://github.com/becomequantum/kryon 还做了个算法演示动画:https://www.bilibili.com/video/av26067000 < ...

  4. 【OpenCV 4开发详解】图像连通域分析

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

  5. cv2.error: opencv(4.4.0)_【OpenCV 4开发详解】图像连通域分析

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

  6. OpenCV——图像连通域

    图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来. 一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提 ...

  7. MATLAB二值图连通域快速标记算法

    (来点有用的)MATLAB二值图连通域快速标记算法 基本原理 算法实现 0. 变量解释 1.种子点搜索 2 区域生长 完整代码 实验 其他 by HPC_ZY 由于工程需要,用C++实现三维二值图像的 ...

  8. 图像的连通域检测的堆栈算法

    图像的连通域寻找在直觉上可使用递归的方法,进而可以使用堆栈数据结构进行改进.本文描述了一个图像寻找连通域的堆栈方法,四连通域和八联通域的选择可以使用一个参数来确定. 以下是代码: //根据种子点寻找8 ...

  9. matlab删除连通域面积_matlab连通域处理函数

    1. matlab函数bwareaopen──删除小面积对象 格式:BW2 = bwareaopen(BW,P,conn) 作用:删除二值图像BW中面积小于P的对象,默认情况下使用8邻域. 算法: ( ...

  10. matlab 读二值图片,二值图像

    二值图像(Binary Image)是指将图像上的每一个像素只有两种可能的取值或灰度等级状态,人们经常用黑白.B&W.单色图像表示二值图像. 二值图像是指在图像中,灰度等级只有两种,也就是说, ...

最新文章

  1. oracle索引本地打钩,Oracle
  2. CMM能力成熟度模型
  3. python十三:函数
  4. SAP gateway里一个控制缓存行为的开关 User parameter /UI2/PAGE_CACHE_OFF
  5. vue中如何实现全全全屏和退出全屏?
  6. Python:pyinstaller报错【A RecursionError maximum recursion depth exceeded occurred】
  7. 演练 玩游戏支付游戏币
  8. oracle cost小 比较慢,Oracle数据库中有关CBO优化的三个问题
  9. 小米冲击高端,这次能否成功?
  10. soapui 免费版 实现 datasource+ dataLoop
  11. nuxt 中 vuex 路由鉴权 keeplive
  12. 前言:智能车制作的那些事
  13. 计算机辅助电话访问优势,电话访问的优点缺点、优势不足、局限性
  14. 基于MobileNetv3实现人脸面部表情识别
  15. office ppt在线预览
  16. Power BI可视化之参数what-if
  17. elasticsearch2.4.2安装(2) --插件marvel
  18. QComboBox下拉选择框
  19. Box2D v2.1.0用户手册翻译 - 第06章 夹具(Fixtures)
  20. Unity学习笔记--超简单:两个游戏对象直接用线连接(UI和世界坐标下均可)

热门文章

  1. z-index终结者
  2. 使用自定义函数替代Linux rm命令
  3. 11. 数值的整数次方
  4. 每日一道剑指offer-二叉树的镜像
  5. 个人计算机主板的组成部分,简述计算机主板的基本组成部分
  6. mysql 存储过程使用参数_mysql 存储过程 使用参数
  7. 如何使用python提高办公效率-提升Python程序运行效率的6个方法
  8. linux关机方法有哪些?有何区别_茅台酒都有哪些容量?一升装的与一斤装的到底有何区别?...
  9. 软件需求最佳实践pdf_| 专家观点 | 彭瑜:PackML成功的秘诀 ——简论软件定义包装过程的最佳实践...
  10. matlab repmat_三行MATLAB实现动漫风格照片