连通域标记是二值图像分析中非常重要的一种方法,也是其他二值图像处理的基础与前提。
所谓连通域标记,就是将一副二值图像中的每个白色像素进行标记,属于同一个连通域的白色像素标记相同,不同连通域的白色像素有不同的标记,从而能将图像中每个连通域提取出来。
连通域标记的算法有很多种,这里介绍其中一种基于一次遍历图像,记录等价对的标记方法,这种方法效率很高。
在介绍算法前,我们先了解一下什么算是连通。如果两个白色像素邻接,则我们认为这两个像素是连通的;同时,若像素 A 与像素 B 连通,像素 B 与像素 C 连通,则像素 A 与 C 也是连通的。
因此,我们需要定义什么样的两个像素点算是邻接的。常见的邻接关系有两种:4 邻接与 8 邻接。如下图所示:
如果另一个像素点在上图中黑点的位置,则表示这个像素点与 X 点邻接。
算法描述:
for 二值图像中的每一行: //列也可以
1. 记录此行白色像素的每一个序列的的起始位置,终止位置;
2. 除第一行以外(第一行直接标记),判断是否与上一行序列有重叠:
如果没有重叠,则分配一个新的标记;
如果有一个重叠,则用上一行序列的标记进行标记;
如果有一个以上的重叠,则用上一行重叠序列中最小的进行标记,同时将后面几个标记与此标记记为等价对;
end
如上图所示,我们采用 4 邻接,行遍历来说明一下上面的操作
第一行: 我们记录一个序列:(1,4)标记为 1;
第二行:我们记录两个序列:(0,3),与上一行的序列重叠,因此标记为 1,(6,8),与上一行没有重叠,标记为 2;
第三行:一个序列:(2,6),与上一行两个序列重叠,标记为上一行较小的标记,即 1,同时记录等价对 <1,2>;
第四行:两个序列:(1,2),标记为 1,(6,8)标记为 1。
在上一步中,我们得到了若干个等价对,每一个等价对 表示被标记 a 区域与被标记 b 的区域是连通的,因此,我们希望将每个等价对中的标记更新为同一个标记。用图的遍历即可做到这一点。
我们将每一个标记看作一个图的结点,每一个等价对看作是图的边,我们要做的就是通过图的遍历来寻找属于同一个最大连通子图的结点。这些结点锁表示的标记是等价的。
最后,我们将之前的标记更新为新的标记,如上图所示,之前标记为 1,2,5 的像素标记为 1;标记为 6 3 7 9 8 的像素重新标记为 2;标记为 4 的像素重新标记为 3。至此,拥有相同标记的像素点就构成了一个连通域,而标记的最大值就是连通域的个数。
#include
#include
#include
#include
#include
#include
#include
#include
#define HEIGHBOR_4 4
#define HEIGHBOR_8 8
#define UPWARDFIND 1
#define DOWNWARDFIND 2
using namespace std;
using namespace cv;
struct Group {
int start;
int end;
int tag;
};
//寻找一个group与上一行的group是否有重叠,返回值若为-1,则表示没有重叠,若为其他值,则返回值是这个group的标记,同时记录等价对
int findEqualPair(vectorint ,int>> &_equal_pairs, Group _group,const vector &_pre_groups,int _mode)
{
vector<int> tags;
if (_mode == HEIGHBOR_4)
{
for (auto i : _pre_groups)
{
if (!(i.start > _group.end || _group.start > i.end))
tags.push_back(i.tag);
}
}
if (_mode == HEIGHBOR_8)
{
for (auto i : _pre_groups)
{
if (!(i.start+1 > _group.end && _group.start+1 > i.end))
tags.push_back(i.tag);
}
}
if (tags.size() == 0)
{
return -1;
}
sort(tags.begin(), tags.end());
tags.erase(unique(tags.begin(), tags.end()), tags.end());
int min_tag = tags[0];
for (int i =1;iint, int>(min_tag, tags[i]));
}
return min_tag;
}
//连通域标记
int tagDomain(Mat &_image,Mat &_result,int _mode)
{
int width = _image.cols;
int height = _image.rows;
_result = Mat(_image.size(),CV_16UC1, Scalar::all(0));
vector<vector > groups;//用于存团
vectorint , int>> equal_pairs;//存储等价对
int tag_count = 0;
//第一次遍历
clock_t time1, time2;
for (int i = 0; i < height; i++)
{
vector tmp_groups;
uchar* row = _image.ptr(i);
int j = 0;
time1 = clock();
while (j < width)
{
if (row[j] == 255)
{
Group group;
group.start = j;
j++;
while (row[j] == 255 && j < width)
{
j++;
continue;
}
group.end = j - 1;
if (i != 0)
{
int tmp_tag = findEqualPair(equal_pairs, group, groups[i - 1], _mode);
if (tmp_tag != -1)
group.tag = tmp_tag;
else
group.tag = ++tag_count;
}
else
{
group.tag = ++tag_count;
}
tmp_groups.push_back(group);
//cout << "(" << group.first << "," << group.end << "):" << group.tag << endl;
}
else
{
j++;
}
}
time2 = clock();
groups.push_back(tmp_groups);
}
//消除等价对(图的遍历)
//消除重复的等价对
sort(equal_pairs.begin(), equal_pairs.end());
equal_pairs.erase(unique(equal_pairs.begin(), equal_pairs.end()), equal_pairs.end());
//构建图
int size = tag_count+1;
vector<list<int>> graph(size);
vector<int> updata_tag(size,0);
for (auto equal_pair : equal_pairs)
{
graph[equal_pair.first].push_front(equal_pair.second);
graph[equal_pair.second].push_front(equal_pair.first);
}
//遍历
tag_count = 1;
int first_node;
bool finished = false;
while (true)
{
finished = true;
for (int i = 1; i < size; i++)
{
if (updata_tag[i] == 0)
{
finished = false;
first_node = i;
break;
}
}
if (finished)
break;
//图的广度优先遍历
queue<int> q;
updata_tag[first_node] = tag_count;
q.push(first_node);
while (!q.empty())
{
int tmp_node = q.front();
q.pop();
for (auto i : graph[tmp_node])
{
if (updata_tag[i] == 0)
{
updata_tag[i] = tag_count;
q.push(i);
}
}
}
tag_count++;
}
//
for (int i = 0; i < height; i++)
{
ushort* data = _result.ptr(i);
for (auto j : groups[i])
{
int tmp_tag = updata_tag[j.tag];
for (int n = j.start; n <= j.end; n++)
{
data[n] = tmp_tag;
}
}
}
return tag_count-1;
}
//随机取色
Scalar random_color(RNG &_rng)
{
int icolor = (unsigned)_rng;
return Scalar(icolor & 0xFF, (icolor >> 8) & 0xFF, (icolor >> 16) & 0xFF);
}
//给连通域涂上不同的颜色
Mat drawDomain(Mat &_domain_tag, int _num, RNG &_rng)
{
Mat result(_domain_tag.size(), CV_8UC3, Scalar::all(0));
vector colors;
for (int i = 0; i < _num; i++)
{
colors.push_back(random_color(_rng));
}
int height = result.rows;
int width = result.cols;
for (int i = 0; i < height; i++)
{
Vec3b* data = result.ptr(i);
ushort* tag_data = _domain_tag.ptr(i);
for (int j = 0; j < width; j++)
{
Scalar color = colors[tag_data[j]-1];
data[j][0] = color[0];
data[j][1] = color[1];
data[j][2] = color[2];
}
}
return result;
}
int main()
{
Mat image = imread("images\\3.bmp");
Mat binary;
cvtColor(image, binary, CV_BGR2GRAY);//图像本身是二值图,转下灰度就可以了
clock_t time1, time2;
time1 = clock();
Mat result;
int num = tagDomain(binary, result, HEIGHBOR_4);
Point seed1, seed2;
seed1 = Point(2000, 2150);
seed2 = Point(2000, 350);
Point seam_point_down = findSeam(binary, seed1, DOWNWARDFIND);
Point seam_point_up = findSeam(binary, seed2, UPWARDFIND);
cout << num << endl;
Mat draw_result = drawDomain(result, num, RNG(123));
imwrite("images\\draw_result.bmp", draw_result);
imshow("draw_result", draw_result);
waitKey(0);
return 0;
}