连通域是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域,一般使用二值图像表示。连通标记是指将图像中的各个连通区域找出并标记为响应的标号。
wo-Pass(两遍扫描法)
通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。
思路:第一遍扫描时赋予每个像素位置一个label,扫描过程中同一个连通区域内的像素集合中可能会被赋予一个或多个不同label,因此需要将这些属于同一个连通区域但具有不同值的label合并,也就是记录它们之间的相等关系;
第二遍扫描就是将具有相等关系的equal_labels所标记的像素归为一个连通区域并赋予一个相同的label(通常这个label是equal_labels中的最小值)。
下面给出Two-Pass算法的简单步骤:
I(x,y)为二值图像,L(x,y)连通域标记图像
(1)第一次扫描:
访问当前像素I(x,y),if I(x,y) == 1:
if: I(x,y)的已访问领域像素(四邻域为上、坐;八邻域加上左上)的值都为0,则赋予I(x,y)一个新的label:
label += 1, L(x,y) = label;
else I(x,y)的领域中有像素值 > 1的像素Neighbors:
1)将Neighbors中的最小值赋予给L(x,y):
L(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:
找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。
opencv 实现代码:
//算法原理:YUV空间融合,Y采用对比度和曝光适度融合,权值经过递归滤波处理
//UV采用饱和度融合
//问题:1、图像的平滑区域出现零散的黑点,图像的若细节增强的不够明显
//2、如何采用GUP编程实现,提高算法的实时性
// Connected Component Analysis/Labeling By Two-Pass Algorithm
// Author: www.icvpr.com
// Blog : http://blog.csdn.net/icvpr
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
// connected component analysis (4-component)
// use two-pass algorithm
// 1. first pass: label each foreground pixel with a label
// 2. second pass: visit each labeled pixel and merge neighbor labels
//
// foreground pixel: _binImg(x,y) = 1
// background pixel: _binImg(x,y) = 0
if (_binImg.empty() ||
_binImg.type() != CV_8UC1)
{
return;
}
// 1. first pass
_lableImg.release();
_binImg.convertTo(_lableImg, CV_32SC1);
int label = 1; // start by 2
std::vector<int> labelSet;
labelSet.push_back(0); // background: 0
int rows = _binImg.rows - 1;
int cols = _binImg.cols - 1;
for (int i = 1; i < rows; i++)
{
int* data_preRow = _lableImg.ptr<int>(i - 1);
int* data_curRow = _lableImg.ptr<int>(i);
for (int j = 1; j < cols; j++)
{
if (data_curRow[j] == 1)
{
int leftPixel = data_curRow[j - 1];
int upPixel = data_preRow[j];
if (leftPixel > 0 && upPixel > 0)
{
if (leftPixel > upPixel)
{
data_curRow[j] = upPixel;
int oldSmallestLabel = labelSet[leftPixel];
if (oldSmallestLabel > upPixel)
{
labelSet[oldSmallestLabel] = upPixel;
labelSet[leftPixel] = upPixel;
}
else if(oldSmallestLabel < upPixel)
{
labelSet[upPixel] = oldSmallestLabel;
}
}
else if(leftPixel < upPixel)
{
data_curRow[j] = leftPixel;
int oldSmallestLabel = labelSet[upPixel];
if (oldSmallestLabel > leftPixel)
{
labelSet[oldSmallestLabel] = leftPixel;
labelSet[upPixel] = leftPixel;
}
else if (oldSmallestLabel < leftPixel)
{
labelSet[leftPixel] = oldSmallestLabel;
}
}
}
else if (upPixel > 0)
{
data_curRow[j] = upPixel;
}
else if(leftPixel > 0)
{
data_curRow[j] = leftPixel;
}
else
{
label++;
labelSet.push_back(label); // assign to a new label
data_curRow[j] = label;
}
}
}
}
// update equivalent labels
// assigned with the smallest label in each equivalent label set
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;
}
// 2. second pass
for (int i = 0; i < rows; i++)
{
int* data = _lableImg.ptr<int>(i);
for (int j = 0; j < cols; j++)
{
int pixelLabel = data[j];
data[j] = labelSet[pixelLabel];
}
}
}
该方法将每一个连通域标记为独一无二的整数,因为第二步标号合并操作,导致标记图像中的标号不一定连续,即标号存在中断,这可能会对后续的连通域分析带来麻烦。为了 保证最终标记图像的标号是连续的,需要统计标号合并时,该标号之前被消灭的标号数目,故需要建立一个标号变换表,将初始标号的值投影为新标号的值,其包含两个调整:
(1)标号合并;
(2)消失标号后续标号的前进(减小,减的数值即为前面合并标号的数目)操作。