连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。连通区域分析(Connected Component Analysis,Connected Component Labeling)是指将图像中的各个连通区域找出并标记。
连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。
而这次我要做的是实现图像的快速连通域算法,可以提取出图像中的连通域,并将不同连通域用不同颜色表示。
寻找图像中的连通域的算法有两个,一个是Two-Pass方法
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都属于同一个连通区域
图示为:
// 1. 第一次遍历
_lableImg.release();
_binImg.convertTo(_lableImg, CV_32SC1);
int label = 1; // start by 2
std::vector<int> labelSet;
labelSet.push_back(0); // background: 0
labelSet.push_back(1); // foreground: 1
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)
{
std::vector<int> neighborLabels;
neighborLabels.reserve(2);
int leftPixel = data_curRow[j - 1];
int upPixel = data_preRow[j];
if (leftPixel > 1)
{
neighborLabels.push_back(leftPixel);
}
if (upPixel > 1)
{
neighborLabels.push_back(upPixel);
}
if (neighborLabels.empty())
{
labelSet.push_back(++label); // assign to a new label
data_curRow[j] = label;
labelSet[label] = label;
}
else
{
std::sort(neighborLabels.begin(), neighborLabels.end());
int smallestLabel = neighborLabels[0];
data_curRow[j] = smallestLabel;
// save equivalence
for (size_t k = 1; k < neighborLabels.size(); k++)
{
int tempLabel = neighborLabels[k];
int& oldSmallestLabel = labelSet[tempLabel];
if (oldSmallestLabel > smallestLabel)
{
labelSet[oldSmallestLabel] = smallestLabel;
oldSmallestLabel = smallestLabel;
}
else if (oldSmallestLabel < smallestLabel)
{
labelSet[smallestLabel] = oldSmallestLabel;
}
}
}
}
}
}
(2)第二次扫描:
访问当前像素B(x,y),如果B(x,y) > 1:
a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。
图示为:
// 2. 第二遍扫描
for (int i = 0; i < rows; i++)
{
int* data = _lableImg.ptr<int>(i);
for (int j = 0; j < cols; j++)
{
int& pixelLabel = data[j];
pixelLabel = labelSet[pixelLabel];
}
}
}
另一个方法就是Seed-Filling方法
种子填充法的连通区域分析方法:
(1)扫描图像,直到当前像素点B(x,y) == 1:
a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;
b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;
// 推及到四个邻居
if (_lableImg.at<int>(curX, curY - 1) == 1)
{// 左边的像素
neighborPixels.push(std::pair<int, int>(curX, curY - 1));
}
if (_lableImg.at<int>(curX, curY + 1) == 1)
{// 右边的像素
neighborPixels.push(std::pair<int, int>(curX, curY + 1));
}
if (_lableImg.at<int>(curX - 1, curY) == 1)
{// 上面的像素
neighborPixels.push(std::pair<int, int>(curX - 1, curY));
}
if (_lableImg.at<int>(curX + 1, curY) == 1)
{// 下面的像素
neighborPixels.push(std::pair<int, int>(curX + 1, curY));
}
c、重复b步骤,直到栈为空;
此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;
(2)重复第(1)步,直到扫描结束;
扫描结束后,就可以得到图像B中所有的连通区域;
而我选择的是种子填充方法。
对下面这张图片做处理
得到结果为:
改变连通域颜色:
我用的方法是在刚开始的时候就随机设置三个RGB 值,然后为填充不同连通域(每个连通域的像素的RGB值都是随机的)。
cv::Scalar icvprGetRandomColor()
{
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);
}
代码自取
// CVE6.cpp: 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <stack>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <stdio.h>
using namespace cv;
//Two Pass方法
void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
/*
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都属于同一个连通区域
(2)第二次扫描:
访问当前像素B(x,y),如果B(x,y) > 1:
a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。
*/
if (_binImg.empty() ||
_binImg.type() != CV_8UC1)
{
return;
}
// 1. 第一次遍历
_lableImg.release();
_binImg.convertTo(_lableImg, CV_32SC1);
int label = 1; // start by 2
std::vector<int> labelSet;
labelSet.push_back(0); // background: 0
labelSet.push_back(1); // foreground: 1
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)
{
std::vector<int> neighborLabels;
neighborLabels.reserve(2);
int leftPixel = data_curRow[j - 1];
int upPixel = data_preRow[j];
if (leftPixel > 1)
{
neighborLabels.push_back(leftPixel);
}
if (upPixel > 1)
{
neighborLabels.push_back(upPixel);
}
if (neighborLabels.empty())
{
labelSet.push_back(++label); // assign to a new label
data_curRow[j] = label;
labelSet[label] = label;
}
else
{
std::sort(neighborLabels.begin(), neighborLabels.end());
int smallestLabel = neighborLabels[0];
data_curRow[j] = smallestLabel;
// save equivalence
for (size_t k = 1; k < neighborLabels.size(); k++)
{
int tempLabel = neighborLabels[k];
int& oldSmallestLabel = labelSet[tempLabel];
if (oldSmallestLabel > smallestLabel)
{
labelSet[oldSmallestLabel] = smallestLabel;
oldSmallestLabel = smallestLabel;
}
else if (oldSmallestLabel < smallestLabel)
{
labelSet[smallestLabel] = oldSmallestLabel;
}
}
}
}
}
}
// 更新标签
// 用每个连通域中最小的标签来表示这个连通域
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. 第二遍扫描
for (int i = 0; i < rows; i++)
{
int* data = _lableImg.ptr<int>(i);
for (int j = 0; j < cols; j++)
{
int& pixelLabel = data[j];
pixelLabel = labelSet[pixelLabel];
}
}
}
void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
/*
种子填充法的连通区域分析方法:
(1)扫描图像,直到当前像素点B(x,y) == 1:
a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;
b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;
c、重复b步骤,直到栈为空;
此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;
(2)重复第(1)步,直到扫描结束;
扫描结束后,就可以得到图像B中所有的连通区域;
*/
if (_binImg.empty() ||
_binImg.type() != CV_8UC1)
{
return;
}
_lableImg.release();
_binImg.convertTo(_lableImg, CV_32SC1);
int label = 1; // start by 2
int rows = _binImg.rows - 1;
int cols = _binImg.cols - 1;
for (int i = 1; i < rows - 1; i++)
{
int* data = _lableImg.ptr<int>(i);
for (int j = 1; j < cols - 1; j++)
{
if (data[j] == 1)
{
std::stack<std::pair<int, int>> neighborPixels;
neighborPixels.push(std::pair<int, int>(i, j)); // 像素坐标: <i,j>
++label; //从一个新label开始
while (!neighborPixels.empty())
{
// 栈中最上面的像素给予和与其连通的像素相同的label
std::pair<int, int> curPixel = neighborPixels.top();
int curX = curPixel.first;
int curY = curPixel.second;
_lableImg.at<int>(curX, curY) = label;
// 弹出最上面的像素
neighborPixels.pop();
// 推及到四个邻居
if (_lableImg.at<int>(curX, curY - 1) == 1)
{// 左边的像素
neighborPixels.push(std::pair<int, int>(curX, curY - 1));
}
if (_lableImg.at<int>(curX, curY + 1) == 1)
{// 右边的像素
neighborPixels.push(std::pair<int, int>(curX, curY + 1));
}
if (_lableImg.at<int>(curX - 1, curY) == 1)
{// 上面的像素
neighborPixels.push(std::pair<int, int>(curX - 1, curY));
}
if (_lableImg.at<int>(curX + 1, curY) == 1)
{// 下面的像素
neighborPixels.push(std::pair<int, int>(curX + 1, curY));
}
}
}
}
}
}
//为连通域加上颜色
cv::Scalar icvprGetRandomColor()
{
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 icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)
{
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] = icvprGetRandomColor();
}
cv::Scalar color = colors[pixelValue];
*data_dst++ = color[0];
*data_dst++ = color[1];
*data_dst++ = color[2];
}
else
{
data_dst++;
data_dst++;
data_dst++;
}
}
}
}
int main(int argc, char** argv)
{
cv::Mat binImage = cv::imread("E:/C++/CVE6/图片2.png", 0);
cv::imshow("img", binImage);
//cv::Mat binImage2;
cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV);
cv::Mat labelImg;
//icvprCcaByTwoPass(binImage, labelImg);
icvprCcaBySeedFill(binImage, labelImg) ;
// 展示结果
cv::Mat grayImg;
//结果*10,更突出
labelImg *= 10;
labelImg.convertTo(grayImg, CV_8UC1);
cv::imshow("labelImg", grayImg);
cv::Mat colorLabelImg;
//更改连通域颜色
icvprLabelColor(labelImg, colorLabelImg);
cv::imshow("colorImg", colorLabelImg);
cv::waitKey(0);
return 0;
}