目录
- 原理
- 二值图像
- 连通区域(Connected Component)
- 连通区域分析(Connected Component Analysis,Connected Component Labeling)
- 算法:Two-Pass(两遍扫描法)
- 思路:
- Two-Pass算法的简单步骤:
- 源码
- 效果
- 示例一
- 示例二
平台:Windows 10 20H2
Visual Studio 2015
OpenCV 4.5.3
本文摘自OpenCV-二值图像连通域分析 —— 青雲-吾道乐途
原理
二值图像
顾名思义就是图像的亮度值只有两个状态:黑(0)和白(255)。二值图像在图像分析与识别中有着举足轻重的地位,因为其模式简单,对像素在空间上的关系有着极强的表现力。在实际应用中,很多图像的分析最终都转换为二值图像的分析,比如:医学图像分析、前景检测、字符识别,形状识别。二值化+数学形态学能解决很多计算机识别工程中目标提取的问题。
二值图像分析最重要的方法就是连通区域标记,它是所有二值图像分析的基础,它通过对二值图像中白色像素(目标)的标记,让每个单独的连通区域形成一个被标识的块,进一步的我们就可以获取这些块的轮廓、外接矩形、质心、不变矩等几何参数。
连通区域(Connected Component)
一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。
在图像中,最小的单位是像素,每个像素周围有8个邻接像素,常见的邻接关系有2种:4邻接与8邻接。4邻接一共4个点,即上下左右,如下左图所示。8邻接的点一共有8个,包括了对角线位置的点,如下右图所示。
如果像素点A与B邻接,我们称A与B连通,于是我们不加证明的有如下的结论:
如果A与B连通,B与C连通,则A与C连通。
在视觉上看来,彼此连通的点形成了一个区域,而不连通的点形成了不同的区域。这样的一个所有的点彼此连通点构成的集合,我们称为一个连通区域。
下面这符图中,如果考虑4邻接,则有3个连通区域;如果考虑8邻接,则有2个连通区域。(注:图像是被放大的效果,图像正方形实际只有4个像素)
连通区域分析(Connected Component Analysis,Connected Component Labeling)
是指将图像中的各个连通区域找出并标记。
连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:
OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、
视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、
医学图像处理(感兴趣目标区域提取)等等。
也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。
算法:Two-Pass(两遍扫描法)
从连通区域的定义可以知道,一个连通区域是由具有相同像素值的相邻像素组成像素集合,因此,我们就可以通过这两个条件在图像中寻找连通区域,对于找到的每个连通区域,我们赋予其一个唯一的标识(Label),以区别其他连通区域。
两遍扫描法指的就是通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。
思路:
第一遍扫描时赋予每个像素位置一个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算法:
源码
#include <iostream>
#include "opencv2/opencv.hpp"using namespace std;
using namespace cv;//------------------------------【两步法新改进版】----------------------------------------------
// 对二值图像进行连通区域标记,从1开始标号
void Two_PassNew(const Mat &bwImg, Mat &labImg)
{assert(bwImg.type() == CV_8UC1);labImg.create(bwImg.size(), CV_32SC1); //bwImg.convertTo( labImg, CV_32SC1 );labImg = Scalar(0);labImg.setTo(Scalar(1), bwImg);assert(labImg.isContinuous());const int Rows = bwImg.rows - 1, Cols = bwImg.cols - 1;int label = 1;vector<int> labelSet;labelSet.push_back(0);labelSet.push_back(1);//the first passint *data_prev = (int*)labImg.data; //0-th row : int* data_prev = labImg.ptr<int>(i-1);int *data_cur = (int*)(labImg.data + labImg.step); //1-st row : int* data_cur = labImg.ptr<int>(i);for (int i = 1; i < Rows; i++){data_cur++;data_prev++;for (int j = 1; j<Cols; j++, data_cur++, data_prev++){if (*data_cur != 1)continue;int left = *(data_cur - 1);int up = *data_prev;int neighborLabels[2];int cnt = 0;if (left>1)neighborLabels[cnt++] = left;if (up > 1)neighborLabels[cnt++] = up;if (!cnt){labelSet.push_back(++label);labelSet[label] = label;*data_cur = label;continue;}int smallestLabel = neighborLabels[0];if (cnt == 2 && neighborLabels[1]<smallestLabel)smallestLabel = neighborLabels[1];*data_cur = smallestLabel;// 保存最小等价表for (int k = 0; k<cnt; k++){int tempLabel = neighborLabels[k];int& oldSmallestLabel = labelSet[tempLabel]; //这里的&不是取地址符号,而是引用符号if (oldSmallestLabel > smallestLabel){labelSet[oldSmallestLabel] = smallestLabel;oldSmallestLabel = smallestLabel;}else if (oldSmallestLabel<smallestLabel)labelSet[smallestLabel] = oldSmallestLabel;}}data_cur++;data_prev++;}//更新等价队列表,将最小标号给重复区域for (size_t i = 2; i < labelSet.size(); i++){int curLabel = labelSet[i];int prelabel = labelSet[curLabel];while (prelabel != curLabel){curLabel = prelabel;prelabel = labelSet[prelabel];}labelSet[i] = curLabel;}//second passdata_cur = (int*)labImg.data;for (int i = 0; i < Rows; i++){for (int j = 0; j < bwImg.cols - 1; j++, data_cur++)*data_cur = labelSet[*data_cur];data_cur++;}
}//---------------------------------【颜色标记程序】-----------------------------------
//彩色显示
cv::Scalar GetRandomColor()
{uchar r = 255 * (rand() / (1.0 + RAND_MAX));uchar g = 255 * (rand() / (1.0 + RAND_MAX));uchar b = 255 * (rand() / (1.0 + RAND_MAX));return cv::Scalar(b, g, r);
}void LabelColor(const cv::Mat& labelImg, cv::Mat& colorLabelImg)
{int num = 0;if (labelImg.empty() ||labelImg.type() != CV_32SC1){return;}std::map<int, cv::Scalar> colors;int rows = labelImg.rows;int cols = labelImg.cols;colorLabelImg.release();colorLabelImg.create(rows, cols, CV_8UC3);colorLabelImg = cv::Scalar::all(0);for (int i = 0; i < rows; i++){const int* data_src = (int*)labelImg.ptr<int>(i);uchar* data_dst = colorLabelImg.ptr<uchar>(i);for (int j = 0; j < cols; j++){int pixelValue = data_src[j];if (pixelValue > 1){if (colors.count(pixelValue) <= 0){colors[pixelValue] = GetRandomColor();num++;}cv::Scalar color = colors[pixelValue];*data_dst++ = color[0];*data_dst++ = color[1];*data_dst++ = color[2];}else{data_dst++;data_dst++;data_dst++;}}}printf("color num : %d \n", num);
}//------------------------------------------【测试主程序】-------------------------------------
int main()
{cv::Mat binImage = cv::imread("D:\\Work\\OpenCV\\Workplace\\Test_1\\Test.jpg", 0);cv::threshold(binImage, binImage, 127, 255, THRESH_BINARY);cv::imshow("原图", binImage);cv::Mat labelImg;double time;time = getTickCount();Two_PassNew(binImage, labelImg);time = 1000 * ((double)getTickCount() - time) / getTickFrequency();cout << std::fixed << time << "ms" << endl;//彩色显示cv::Mat colorLabelImg;LabelColor(labelImg, colorLabelImg);cv::imshow("colorImg", colorLabelImg);double minval, maxval;minMaxLoc(labelImg, &minval, &maxval);cout << "minval" << minval << endl;cout << "maxval" << maxval << endl;cv::waitKey(0);return 0;
}
效果
示例一
示例二