图像连通域分析

2023-05-16

转自:https://blog.csdn.net/tiandijun/article/details/51279643,转载仅为方便学习。

一、前言

二值图像的图像的亮度值只有两个状态:黑(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

三、连通区域分析

连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。连通区域分析(Connected Component Analysis,Connected Component Labeling)是指将图像中的各个连通区域找出并标记。

连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。


四、连通区域的标记

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

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

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

1)基于行程的标记

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

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)写入等价对。

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

下面我们用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);
        }
    }
}  

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 && rowRun[i] == rowRun[j] + 1)
            {
                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++;
        }

    }
}  

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

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

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

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

3-7-9

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)。

image    image    image

下面是这个过程的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];
    }
}  

2)基于轮廓的标记

算法描述:

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

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

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

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

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

6,结束。

 

image

整个算法步骤,我们只扫描了一次图像,同时我们对图像中的像素进行标记,要么赋予一个新的标号,要么用它同行的左边的标号去标记它,下面是算法更细的描述

对于一个需要标记的图像,我们定义一个与它对应的标记图像,用来保存标记信息,开始我们把L上的所有值设置为0,同时我们有一个标签变量,初始化为1。然后我们开始扫描图像I,遇到白色像素时,设这个点为点,我们需要按下面不同情况进行不同的处理:

情况1:如果点是一个白色像素,在图像上这个位置没有被标记过,而且点的上方为黑色,则P是一个新的外轮廓的点,这时候我们将C的标签值标记给L图像上P点的位置,即,接着我们沿着P点开始做轮廓跟踪,并把把轮廓上的点对应的L上都标记为C,完成整个轮廓的搜索与标记后,回到了P点。最后不要忘了把C的值加1。这个过程如下面图像S1中所示。

image

 

情况2:如果P点的下方的点是unmarked点(什么是unmark点,情况3介绍完就会给出定义),则P点一定是内轮廓上的点,这时候有两种情况,一种是P点在L上已经被标记过了,说明这个点同时也是外轮廓上的点;另一种情况是P点在L上还没有被标记过,那如果是按上面步骤来的,P点左边的点一定被标记了(这一处刚开始理解可能不容易,不妨画一个简单的图,自己试着一个点一个点标记试试,就容易理解了),所以这时候我们采用P点左边点的标记值来标记P,接着从P点开始跟踪内轮廓把内轮廓上的点都标记为P的标号。

下面图像显示了上面分析的两种P的情况,左图的P点既是外轮廓上的点也是内轮廓上的点。

image    image

情况3:如果一个点P,不是上面两种情况之一,那么P点的左边一定被标记过(不理解,就手动去标记上面两幅图像),我们只需要用它左边的标号去标记L上的P点。

现在我们只剩下一个问题了,就是什么是unmarked点,我们知道内轮廓点开始点P的下方一定是一个黑色像素,是不是黑色像素就是unmarked点呢,显然不是,如下图像的Q点,它的下面也是黑色像素,然而它却不是内轮廓上的点。

实际上在我们在轮廓跟踪时,我们我轮廓点的周围做了标记,在轮廓点周围被查找过的点(查找方式见下面的轮廓跟踪算法)在L上被标记了一个负值(如下面右图所示),所以Q点的下方被标记为了负值,这样Q的下方就不是一个unmarked点,unmarked点,即在L上的标号没有被修改过,即为0。

image      image

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

我们对一点像素点周围的8个点分析作一个标号0-7,因为我们在遍历图像中第一个遇到的点肯定是外轮廓点,所以我们先来确定外轮廓的搜索策略,对于外轮廓的点P,有两种情况:

image

1)如果P是外轮廓的起点,也就是说我们是从P点开始跟踪的,那么我们从7号(右上角)位置开始,看7号是不是白色点,如果是,则把这个点加入外轮廓点中,并将它标记与P点相同,如果7号点是黑色点,则按顺时针7-0-1-2-3-4-5-6这个顺序搜索直到遇到白点为止,把那个点确定为,加入外轮廓,并把这个点的标号设置与P点相同。这里很重要一步就是,假设我们2号点才是白点,那么7,0,1这三个位置我们都搜索过,所以我们要把这些点在L上标记为一个负值。如下图所示,其中右图像标记的结果。

image    image

2)那么如果P是不是外轮廓的起点,即P是外轮廓路径上的一个点,那么它肯定是由一个点进入的,我们设置为点,点的位置为,那么P点从这个位置开始寻找下一步的路径,是加2取模的意思,它反映在图像就是从P-1点按顺时针数2个格子的位置。确定搜索起点后,按照上面一种情况进行下面的步骤。

外轮廓点的跟踪方式确定了后,内轮廓点的跟踪方式大同小异,只是如果P是内轮廓的第一个点,则它的开始搜索位置不是7号点而是3号点。其他的与外轮廓完全一致。

如要上面搜索方式,你不是很直观的理解,不妨尝试着去搜索下面这幅图像,你应该有能有明确的了解了。一个路径搜索结束的条件是,回到原始点S,则S周围不存在unmarked点。

如下边中间图像所示,从S点开始形成的路径是STUTSVWV。

   image 

在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像素
}  

五  、连通区域分析的算法

从连通区域的定义可以知道,一个连通区域是由具有相同像素值的相邻像素组成像素集合,因此,我们就可以通过这两个条件在图像中寻找连通区域,对于找到的每个连通区域,我们赋予其一个唯一的标识(Label),以区别其他连通区域。

连通区域分析有基本的算法,也有其改进算法,本文介绍其中的两种常见算法:

1)Two-Pass法;2)Seed-Filling种子填充法;

Note:

a、这里的扫描指的是按行或按列访问以便图像的所有像素,本文算法采用的是按行扫描方式;

b、图像记为B,为二值图像:前景像素(pixel value = 1),背景像素(pixel value = 0)

c、label从2开始计数;

d、像素相邻关系:4-领域、8-领域,本文算法采用4-邻域;

                                     

4—领域图例                                                     8—领域图例


1)Two-Pass(两遍扫描法)

两遍扫描法,正如其名,指的就是通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。

思路:

第一遍扫描时赋予每个像素位置一个label,扫描过程中同一个连通区域内的像素集合中可能会被赋予一个或多个不同label,因此需要将这些属于同一个连通区域但具有不同值的label合并,也就是记录它们之间的相等关系;

第二遍扫描就是将具有相等关系的equal_labels所标记的像素归为一个连通区域并赋予一个相同的label(通常这个label是equal_labels中的最小值)。


下面给出Two-Pass算法的简单步骤:

(1)第一次扫描:

访问当前像素B(x,y),如果B(x,y) == 1:

a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:

label += 1, B(x,y) = label;

b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors:

1)将Neighbors中的最小值赋予给B(x,y):

B(x,y) = min{Neighbors} 

2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;

 labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都属于同一个连通区域(注:这里可以有多种实现方式,只要能够记录这些具有相等关系的label之间的关系即可)

(2)第二次扫描:

访问当前像素B(x,y),如果B(x,y) > 1:

a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);

完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。

下面这张图动态地演示了Two-pass算法:


2)Seed Filling(种子填充法)

种子填充方法来源于计算机图形学,常用于对某个图形进行填充。思路:选取一个前景像素点作为种子,然后根据连通区域的两个基本条件(像素值相同、位置相邻)将与种子相邻的前景像素合并到同一个像素集合中,最后得到的该像素集合则为一个连通区域。


下面给出基于种子填充法的连通区域分析方法:

(1)扫描图像,直到当前像素点B(x,y) == 1:

a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;

b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;

c、重复b步骤,直到栈为空;

此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;

(2)重复第(1)步,直到扫描结束;

扫描结束后,就可以得到图像B中所有的连通区域;

下面这张图动态地演示了Seed-Filling算法:


在Two-pass连通域标记中,第一次标记(first pass)时从左向右,从上向下扫描,会将各个有效像素置一个label值,判断规则如下(以4邻域为例):

1)         当该像素的左邻像素和上邻像素为无效值时,给该像素置一个新的label值,label ++;

2)         当该像素的左邻像素或者上邻像素有一个为有效值时,将有效值像素的label赋给该像素的label值;

3)         当该像素的左邻像素和上邻像素都为有效值时,选取其中较小的label值赋给该像素的label值。

此时,还需维护一个关系表,记录哪些label值属于同一个连通域。这个关系表通常用union-find数据结构来实现。

在union-find结构中,属于同一个连通域的label值被存储到同一个树形结构中,如图1所示,{1,2,3,4,8}属于同一个连通域,{5,6,7}属于同一个连通域。同时,树形结构的数据存储到一个vector或array中,vector的下标为label值,vector的存储值为该label的父节点label值,当vector的存储值为0时,说明该label是根节点。这样,对于任意一个label,我们都可以寻找其根节点,作为所在连通域的label值,即某一连通域所有像素都被置为同一个label值,即根节点label值。对给定的任意一个label,我们可以通过find算法寻找其根节点,如图2所示。

 

如果知道两个label同属于一个连通域,要如何在vector中实现其存储关系呢?首先,各自寻找两个label的根节点,如果二者的根节点相同,则二者已经属于同一个连通域;如果二者的根节点不同,那么把其中一个根节点作为另一个的父节点即可,即将两个label划入同一个连通域,或者叫做连通域合并,算法如图3所示。

那么这个存储label值的vector要如何初始化呢?将vector初始化为0即可,即各个label值都是根节点,大家互不相连。同时注意,vector[0]不使用。

实现代码如下:

[cpp]  view plain  copy
  1. const int max_size = 100;  
  2. int parent[100] = {0};  
  3.   
  4. // 找到label x的根节点  
  5. int find(int x, int parent[]){  
  6.     int i = x;  
  7.     while(0 != parent[i])  
  8.         i = parent[i];  
  9.     return i;  
  10. }  
  11.   
  12. // 将label x 和 label y合并到同一个连通域  
  13. void union_label(int x, int y, int parent[]){  
  14.     int i = x;  
  15.     int j = y;  
  16.     while(0 != parent[i])  
  17.         i = parent[i];  
  18.     while(0 != parent[j])  
  19.         j = parent[j];  
  20.     if(i != j)  
  21.         parent[i] = j;  
  22. }  
参考:http://blog.csdn.net/lichengyu/article/details/13986521

六、实验演示


1)前景二值图像


2)连通区域分析方法标记后得到的label图像


Two-pass算法:



Seed-filling算法:


注:为了显示方便,将像素值乘以了一个整数进行放大。


3)color后的label图像

Two-pass算法:


Seed-filling算法:



注:颜色是随机生成的。


七、代码


1)Two-pass算法的一种实现

说明:基于OpenCV和C++实现,领域:4-领域。实现与算法描述稍有差别(具体为记录具有相等关系的label方法实现上)。

copy

  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling By Two-Pass Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9.   
  10. #include <opencv2/imgproc/imgproc.hpp>  
  11. #include <opencv2/highgui/highgui.hpp>  
  12.   
  13.   
  14. void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  15. {  
  16.     // connected component analysis (4-component)  
  17.     // use two-pass algorithm  
  18.     // 1. first pass: label each foreground pixel with a label  
  19.     // 2. second pass: visit each labeled pixel and merge neighbor labels  
  20.     //   
  21.     // foreground pixel: _binImg(x,y) = 1  
  22.     // background pixel: _binImg(x,y) = 0  
  23.   
  24.   
  25.     if (_binImg.empty() ||  
  26.         _binImg.type() != CV_8UC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     // 1. first pass  
  32.   
  33.     _lableImg.release() ;  
  34.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  35.   
  36.     int label = 1 ;  // start by 2  
  37.     std::vector<int> labelSet ;  
  38.     labelSet.push_back(0) ;   // background: 0  
  39.     labelSet.push_back(1) ;   // foreground: 1  
  40.   
  41.     int rows = _binImg.rows - 1 ;  
  42.     int cols = _binImg.cols - 1 ;  
  43.     for (int i = 1; i < rows; i++)  
  44.     {  
  45.         int* data_preRow = _lableImg.ptr<int>(i-1) ;  
  46.         int* data_curRow = _lableImg.ptr<int>(i) ;  
  47.         for (int j = 1; j < cols; j++)  
  48.         {  
  49.             if (data_curRow[j] == 1)  
  50.             {  
  51.                 std::vector<int> neighborLabels ;  
  52.                 neighborLabels.reserve(2) ;  
  53.                 int leftPixel = data_curRow[j-1] ;  
  54.                 int upPixel = data_preRow[j] ;  
  55.                 if ( leftPixel > 1)  
  56.                 {  
  57.                     neighborLabels.push_back(leftPixel) ;  
  58.                 }  
  59.                 if (upPixel > 1)  
  60.                 {  
  61.                     neighborLabels.push_back(upPixel) ;  
  62.                 }  
  63.   
  64.                 if (neighborLabels.empty())  
  65.                 {  
  66.                     labelSet.push_back(++label) ;  // assign to a new label  
  67.                     data_curRow[j] = label ;  
  68.                     labelSet[label] = label ;  
  69.                 }  
  70.                 else  
  71.                 {  
  72.                     std::sort(neighborLabels.begin(), neighborLabels.end()) ;  
  73.                     int smallestLabel = neighborLabels[0] ;    
  74.                     data_curRow[j] = smallestLabel ;  
  75.   
  76.                     // save equivalence  
  77.                     for (size_t k = 1; k < neighborLabels.size(); k++)  
  78.                     {  
  79.                         int tempLabel = neighborLabels[k] ;  
  80.                         int& oldSmallestLabel = labelSet[tempLabel] ;  
  81.                         if (oldSmallestLabel > smallestLabel)  
  82.                         {                             
  83.                             labelSet[oldSmallestLabel] = smallestLabel ;  
  84.                             oldSmallestLabel = smallestLabel ;  
  85.                         }                         
  86.                         else if (oldSmallestLabel < smallestLabel)  
  87.                         {  
  88.                             labelSet[smallestLabel] = oldSmallestLabel ;  
  89.                         }  
  90.                     }  
  91.                 }                 
  92.             }  
  93.         }  
  94.     }  
  95.   
  96.     // update equivalent labels  
  97.     // assigned with the smallest label in each equivalent label set  
  98.     for (size_t i = 2; i < labelSet.size(); i++)  
  99.     {  
  100.         int curLabel = labelSet[i] ;  
  101.         int preLabel = labelSet[curLabel] ;  
  102.         while (preLabel != curLabel)  
  103.         {  
  104.             curLabel = preLabel ;  
  105.             preLabel = labelSet[preLabel] ;  
  106.         }  
  107.         labelSet[i] = curLabel ;  
  108.     }  
  109.   
  110.   
  111.     // 2. second pass  
  112.     for (int i = 0; i < rows; i++)  
  113.     {  
  114.         int* data = _lableImg.ptr<int>(i) ;  
  115.         for (int j = 0; j < cols; j++)  
  116.         {  
  117.             int& pixelLabel = data[j] ;  
  118.             pixelLabel = labelSet[pixelLabel] ;   
  119.         }  
  120.     }  
  121. }</span>  

2)Seed-Filling种子填充方法

说明:基于OpenCV和C++实现;领域:4-领域。

[cpp]  view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling By Seed-Filling Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14.   
  15. void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  16. {  
  17.     // connected component analysis (4-component)  
  18.     // use seed filling algorithm  
  19.     // 1. begin with a foreground pixel and push its foreground neighbors into a stack;  
  20.     // 2. pop the top pixel on the stack and label it with the same label until the stack is empty  
  21.     //   
  22.     // foreground pixel: _binImg(x,y) = 1  
  23.     // background pixel: _binImg(x,y) = 0  
  24.   
  25.   
  26.     if (_binImg.empty() ||  
  27.         _binImg.type() != CV_8UC1)  
  28.     {  
  29.         return ;  
  30.     }  
  31.   
  32.     _lableImg.release() ;  
  33.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  34.   
  35.     int label = 1 ;  // start by 2  
  36.   
  37.     int rows = _binImg.rows - 1 ;  
  38.     int cols = _binImg.cols - 1 ;  
  39.     for (int i = 1; i < rows-1; i++)  
  40.     {  
  41.         int* data= _lableImg.ptr<int>(i) ;  
  42.         for (int j = 1; j < cols-1; j++)  
  43.         {  
  44.             if (data[j] == 1)  
  45.             {  
  46.                 std::stack<std::pair<int,int>> neighborPixels ;     
  47.                 neighborPixels.push(std::pair<int,int>(i,j)) ;     // pixel position: <i,j>  
  48.                 ++label ;  // begin with a new label  
  49.                 while (!neighborPixels.empty())  
  50.                 {  
  51.                     // get the top pixel on the stack and label it with the same label  
  52.                     std::pair<int,int> curPixel = neighborPixels.top() ;  
  53.                     int curX = curPixel.first ;  
  54.                     int curY = curPixel.second ;  
  55.                     _lableImg.at<int>(curX, curY) = label ;  
  56.   
  57.                     // pop the top pixel  
  58.                     neighborPixels.pop() ;  
  59.   
  60.                     // push the 4-neighbors (foreground pixels)  
  61.                     if (_lableImg.at<int>(curX, curY-1) == 1)  
  62.                     {// left pixel  
  63.                         neighborPixels.push(std::pair<int,int>(curX, curY-1)) ;  
  64.                     }  
  65.                     if (_lableImg.at<int>(curX, curY+1) == 1)  
  66.                     {// right pixel  
  67.                         neighborPixels.push(std::pair<int,int>(curX, curY+1)) ;  
  68.                     }  
  69.                     if (_lableImg.at<int>(curX-1, curY) == 1)  
  70.                     {// up pixel  
  71.                         neighborPixels.push(std::pair<int,int>(curX-1, curY)) ;  
  72.                     }  
  73.                     if (_lableImg.at<int>(curX+1, curY) == 1)  
  74.                     {// down pixel  
  75.                         neighborPixels.push(std::pair<int,int>(curX+1, curY)) ;  
  76.                     }  
  77.                 }         
  78.             }  
  79.         }  
  80.     }  
  81. }</span>  

3)颜色标记(用于显示)

[cpp]  view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling -- Color Labeling   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. cv::Scalar icvprGetRandomColor()  
  15. {  
  16.     uchar r = 255 * (rand()/(1.0 + RAND_MAX));  
  17.     uchar g = 255 * (rand()/(1.0 + RAND_MAX));  
  18.     uchar b = 255 * (rand()/(1.0 + RAND_MAX));  
  19.     return cv::Scalar(b,g,r) ;  
  20. }  
  21.   
  22.   
  23. void icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)   
  24. {  
  25.     if (_labelImg.empty() ||  
  26.         _labelImg.type() != CV_32SC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     std::map<int, cv::Scalar> colors ;  
  32.   
  33.     int rows = _labelImg.rows ;  
  34.     int cols = _labelImg.cols ;  
  35.   
  36.     _colorLabelImg.release() ;  
  37.     _colorLabelImg.create(rows, cols, CV_8UC3) ;  
  38.     _colorLabelImg = cv::Scalar::all(0) ;  
  39.   
  40.     for (int i = 0; i < rows; i++)  
  41.     {  
  42.         const int* data_src = (int*)_labelImg.ptr<int>(i) ;  
  43.         uchar* data_dst = _colorLabelImg.ptr<uchar>(i) ;  
  44.         for (int j = 0; j < cols; j++)  
  45.         {  
  46.             int pixelValue = data_src[j] ;  
  47.             if (pixelValue > 1)  
  48.             {  
  49.                 if (colors.count(pixelValue) <= 0)  
  50.                 {  
  51.                     colors[pixelValue] = icvprGetRandomColor() ;  
  52.                 }  
  53.                 cv::Scalar color = colors[pixelValue] ;  
  54.                 *data_dst++   = color[0] ;  
  55.                 *data_dst++ = color[1] ;  
  56.                 *data_dst++ = color[2] ;  
  57.             }  
  58.             else  
  59.             {  
  60.                 data_dst++ ;  
  61.                 data_dst++ ;  
  62.                 data_dst++ ;  
  63.             }  
  64.         }  
  65.     }  
  66. }  
  67. </span>  

4)测试程序

[cpp]  view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling -- Test code  
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. int main(int argc, char** argv)  
  15. {  
  16.     cv::Mat binImage = cv::imread("../icvpr.com.jpg", 0) ;  
  17.     cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV) ;  
  18.   
  19.     // connected component labeling  
  20.     cv::Mat labelImg ;  
  21.     icvprCcaByTwoPass(binImage, labelImg) ;  
  22.     //icvprCcaBySeedFill(binImage, labelImg) ;  
  23.   
  24.     // show result  
  25.     cv::Mat grayImg ;  
  26.     labelImg *= 10 ;  
  27.     labelImg.convertTo(grayImg, CV_8UC1) ;  
  28.     cv::imshow("labelImg", grayImg) ;  
  29.   
  30.     cv::Mat colorLabelImg ;  
  31.     icvprLabelColor(labelImg, colorLabelImg) ;  
  32.     cv::imshow("colorImg", colorLabelImg) ;  
  33.     cv::waitKey(0) ;  
  34.   
  35.     return 0 ;  
  36. }</span>  


参考:http://www.cnblogs.com/ronny/p/img_aly_01.html

         http://blog.csdn.net/sanwandoujiang/article/details/25734175

           http://blog.csdn.net/cooelf/article/details/26581539?utm_source=tuicool&utm_medium=referral

            二值图像连通域标记算法与代码

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

图像连通域分析 的相关文章

  • 软连接ln -s 创建以及删除

    在 usr local 创建软连接 链接到 usr local include test dst文件夹 phe 64 phe usr local sudo ln s usr local include test dst test sourc
  • CAN报文:数据帧详解

    CAN报文 xff1a 数据帧详解 CAN报文CAN帧类型数据帧帧起始 Start Of Frame 仲裁段控制段数据段CRC段ACK段帧结束 End Of Frame CAN报文 CAN使用的是两条差分信号线 xff0c 只能表达一个信号
  • I2C总线和SPI总线

    I2C串行总线一般有两根信号线 xff0c 一根是双向的数据线SDA xff0c 另一根是时钟线SCL I2C协议 2条双向串行线 xff0c 一条数据线SDA xff0c 一条时钟线SCL SDA传输数据是 大端传输 xff08 字节高位
  • APM飞控学习之路:2 四旋翼的工作原理与系统组成

    一叶障目 xff0c 不见泰山 在研究四旋翼飞行器之前 xff0c 有必要从整体介绍其工作原理 主要部件 技术名词等基础知识 不然就像羊入虎口 xff0c 陷入一大堆不同层次的资料 xff0c 难觅出口 接下我就抛砖引玉 xff0c 尽自己
  • APM飞控学习之路:4 源码裁剪与下载

    月盈则亏 xff0c 水满则溢 当博主编译完成 xff0c 以为离成功更近一步准备下载的时候 xff0c 殊不知陷阱也早已准备好 xff0c 等待我的踏入 连上USB线 xff0c 下载 xff0c timeout xff0c timeou
  • 详解STM32CubeIDE 中 HAL库的串口中断接收函数 HAL_UART_Receive_IT

    MX串口配置方法见 xff1a CubeIDE 利用自带HAL库 串口收发 一 代码自动生成以后的项目及代码结构 xff1a main c中 xff0c 调用了串口初始化 串口初始化函数赋值了串口的参数 相当于底层的初始化 xff0c 配置
  • RTK-Real Time kinematic实时动态

    目前 xff0c GNSSj接收机约99 的时间都用作RTK模式进行测量 xff0c 只有1 的时间用作静态测量做控制网等 所以 xff0c 大部分人都习惯把GNSS接收机喊成RTK了 不过除去GNSS接收机 xff0c 你知道RTK是什么
  • USB-PD3.0(Power Delivery)充电协议

    USB PD xff08 Power Delivery xff09 是基于USB Type C的一种电源供电标准 xff0c 最大供电功率可达100瓦 xff08 W xff09 xff1b 随着USB Type C的普及 xff0c 越来
  • Lua ---- LFS库的使用

    lfs attributes filepath aname 获取路径指定属性 lfs chdir path 改变当前工作目录 xff0c 成功返回true xff0c 失败返回nil加上错误信息 lfs currentdir 获取当前工作目
  • USB2.0实际传输速度

    USB2 0规范中传输速度是480 Mbps 即60 MB s 但是很多USB2 0设备在实际工作时的数据传输速度却与此相差甚远 xff0c 比如用PC用U盘拷个东西 xff0c 往往比60MB s慢很多 xff0c 这是为什么呢 xff1
  • Type-C协议-CC检测原理

    Type C协议简介 xff08 CC检测原理 xff09 1 简介 越来越多的手机开始采用Type C作为充电和通信端口 xff0c Type C连接器实物和PIN定义如下图 xff1a Type C连接器中有两个管脚CC1和CC2 xf
  • 基地址和偏移地址

    首先必须明白 cpu和内存的区别 cpu 中央处理器 内存是物理数据存放的地方 cpu不直接存放数据而是通过内存来存放数据 cpu和内存之间通过20条地址总线相连接 xff0c 地址总线就是cpu通过地址找到对应的内存的物理数据的传递工具
  • RZ、NRZ、NRZ1、曼彻斯特编码

    1 RZ Return Zero Code 编码 也称为归零码 xff0c 特性就是在一个周期内 xff0c 用二进制传输数据位 xff0c 在数据位脉冲结束后 xff0c 需要维持一段时间的低电平 RZ编码又分为两种 xff1a 单极性归
  • LTE中RB和RE、REG、CCE的定义

    一 RB RE REG CCE定义 xff1a 1 RB Resource Block xff1a 频率上连续12个子载波 xff0c 时域上一个slot xff0c 称为1个RB xff0c 即1RB 61 12个子载波 RB 61 12
  • UART 波形

    先介绍一些概念 起始位 先发出一个逻辑 0 的信号 xff0c 表示传输数据的开始 校验位 xff1a 数据位加上这一位后 xff0c 使得 1 的位数应为偶数 偶校验 或奇数 奇校验 xff0c 以此来校验数据传送的正确性 就比如传输 A
  • UART中的硬件流控RTS与CTS

    在RS232中本来CTS 与RTS 有明确的意义 xff0c 但自从贺氏 HAYES 推出了聪明猫 SmartModem 后就有点混淆了 xff0c 不过现在这种意义为主流意义的 xff0c 各大芯片制造厂家对UART控制器的流控基本采用H
  • AC-DC电源

    电源是什么 xff1f 电源是将来自能量源 xff08 如供电网 xff09 的电流转换为负载 xff08 如电机或电子设备 xff09 用电所需的电压和电流值的电气设备 电源的目的是以适当的电压和电流为负载供电 因此电流必须以受控的方 式
  • VYSOR-投屏软件

    前言 使用Vysor软件进行投屏 无需root 示 xff1a 以下是本篇文章正文内容 xff0c 下面案例可供参考 一 Vysor是什么 xff1f vysor是一个免root实现电脑控制手机的chrome插件 xff0c 不需要root
  • Camera ZSL

    camera的ZSL是什么 ZSL Zero Shutter Lag 零快门延迟 ubiFocus 高通对照片后期的一种处理技术 xff0c 可以利用多张照片来实现拍照不对焦 xff0c 拍好之后随便选择对焦点的功能 原文链接 xff1a
  • Unity3D中uGUI事件系统简述及使用方法总结

    Unity3D的uGUI系统的将UI可能触发的事件分为12个类型 xff0c 即EventTriggerType枚举的12个值 如下图所示 xff1a 先以PointerClick为例 这个是用于某点点击事件 其他事件都可以根据相同的办法调

随机推荐

  • USB 2.0 A型、B型、Mini和Micro接口 type-c 定义及封装

    USB全 称Universal Serial Bus xff08 通用串行总线 xff09 xff0c 目前USB 2 0接口分为四种类型A型 B型 Mini型还有后来补充的Micro型接口 xff0c 每种接口都分插头和插座两个部分 xf
  • UVC摄像头开发(一)

    近期 xff0c 要做一个usb摄像头的开发 xff0c 然后看了韦东山老师的第三期视频 xff0c 讲UVC协议摄像头开发的教程 做一些记录 xff0c 以免忘记 1 当我们拿到一个摄像头 xff0c 怎么知道它的一些信息呢 xff1f
  • 分析锂电池充放电保护电路的特点及工作原理

    锂在元素周期表上位于第3位 xff0c 因外层电子数为1个 xff0c 容易失去从而形成稳定结构 xff0c 故锂是一种非常活泼的金属 由锂元素制成的锂离子电池 xff0c 具有放电电流大 内阻低 寿命长 无记忆效应等优点 xff0c 现已
  • vscode相关配置

    转载 作者 ifredom 原文链接 xff1a https blog csdn net win7583362 article details 79315055 配置详解 editor是针对vscode的风格设置 例如 tabSize xf
  • odroid x2安装ubuntu系统,HDMI显示,说说折腾事儿。

    安装的是Lubuntu14 04 xff0c odroid官网有下载 xff0c 烧写软件用官网下载的win32disk imager for odroid 不要用通用的win32disk image 可能不行 write烧写完毕之后记得v
  • VC http post 文件到服务器

    首先引入 include 34 afxinet h 34 其次准备好HTTP POST服务器 STDMETHODIMP CPostPDFToA4 t5PostPDFToA4 BSTR strFileName BSTR strFileFull
  • 宏参数

    一 可变参数宏 和 VA ARGS 1 1 直接替代 结果 xff1a weight 61 1 shipping 61 2 span class hljs comment define PR printf VA ARGS span PR s
  • 记Datax3.0解决MySQL抽数到HDFSNULL变为空字符的问题

    一 背景 使用Datax3 0 地址 xff1a https github com alibaba DataX 在HDFS读的时候开放了如下nullFormat选项 xff1a nullFormat 描述 xff1a 文本文件中无法使用标准
  • vector用法总结(定义,操作,方法,注意点)

    一 vector的基本概念 vector是同一种类型的对象的集合 xff0c 每个对象都有一个对应的整数索引值 和string对象一样 xff0c 标准库负责管理存储元素的相关内存 我们把vector称为容器 xff0c 是因为它可以包含其
  • windows c编写串口通信

    一 介绍 平时调试协议直接上其他平台不方便调试 xff0c 这里以windows平台的c语言实现串口通信 二 准备 1 串口调试助手 2 虚拟串口工具 3 dev c 43 43 4 vscode xff08 可以不用 xff09 三 代码
  • C++接口定义及实现举例

    C 43 43 接口定义及实现举例 一 接口的定义 有时候 xff0c 我们得提供一些接口给别人使用 接口的作用 xff0c 就是提供一个与其他系统交互的方法 其他系统无需了解你内部细节 xff0c 并且也无法了解内部细节 xff0c 只能
  • 2016年终总结

    关键点 xff1a 程序员修炼之道Java编程思想 xff08 Java并发 异常处理 xff09 代码大全2Bat批处理Shell编程正则表达式Shader之初体验Unity5 x资源打包和加载Android单机游戏 xff0c 医疗项目
  • 嵌入式debian没有lsusb命令解决

    问题 bash lsusb command not found 解决
  • Postman抓包教程

    目录 什么是抓包 xff1f 如何使用 Postman 进行抓包 查看历史抓包数据 使用抓包数据进行接口测试和开发 抓包技巧和注意事项 什么是抓包 xff1f 在计算机网络中 xff0c 抓包是指捕获网络流量的过程 抓包工具可以截获进出计算
  • Cmakelists 使用 gcc/g++

    转载 https blog csdn net afei article details 81201039 常用变量 预定义变量 PROJECT SOURCE DIR xff1a 工程的根目录 PROJECT BINARY DIR xff1a
  • ROS 环境配置问题

    如果 roslaunch beginner tutorials turtlemimic launch 发现 turtlemimic launch is neither a launch file in package beginner tu
  • getdate()函数

    定义和用法 getdate 函数取得日期 xff0f 时间信息 语法 getdate timestamp 参数 描述 timestamp 可选 规定 Unix 时间格式中的时间 说明 返回一个根据 timestamp 得出的包含有日期信息的
  • linux系统的嵌入式设备调试422串口

    1 登陆linux系统 xff0c 查看当前可用的设备 xff0c 在终端输入 xff1a dmesg grep ttyS 例如会显示如下 xff0c 当前ttyS0可用 2 可先使用linux系统中的串口调试工具 cutecom 检查线路
  • Windows核心编程之邮槽实现进程间通信

    邮槽是Windows系统提供的一种单向通信的机制 即进程中的一方只能写入或读取数据 xff0c 而另一方则只能读取或写入数据 通过邮槽 xff0c 用户可以实现一对多或跨网络的进程之间的通信 但是 xff0c 邮槽能传输的数据非常小 xff
  • 图像连通域分析

    转自 xff1a https blog csdn net tiandijun article details 51279643 xff0c 转载仅为方便学习 一 前言 二值图像的图像的亮度值只有两个状态 xff1a 黑 0 和白 255 二