編譯環境:
操作系統:Win8.1 64位
IDE平臺:Visual Studio 2013 Ultimate
OpenCV:2.4.8
一、連通域
在圖像中,最小的單位是像素,每個像素周圍有8個鄰接像素,常見的鄰接關係有2種:4鄰接與8鄰接。4鄰接一共4個點,即上下左右,如下左圖所示。8鄰接的點一共有8個,包括了對角線位置的點,如下右圖所示。
如果像素點A與B鄰接,我們稱A與B連通,於是我們不加證明的有如下的結論:
如果A與B連通,B與C連通,則A與C連通。
在視覺上看來,彼此連通的點形成了一個區域,而不連通的點形成了不同的區域。這樣的一個所有的點彼此連通點構成的集合,我們稱爲一個連通區域。
下面這符圖中,如果考慮4鄰接,則有3個連通區域;如果考慮8鄰接,則有2個連通區域。(注:圖像是被放大的效果,圖像正方形實際只有4個像素)。
二、連通區域的標記
1)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都屬於同一個連通區域(注:這裏可以有多種實現方式,只要能夠記錄這些具有相等關係的label之間的關係即可)
(2)第二次掃描:
訪問當前像素B(x,y),如果B(x,y) > 1:
a、找到與label = B(x,y)同屬相等關係的一個最小label值,賦予給B(x,y);
b、完成掃描後,圖像中具有相同label值的像素就組成了同一個連通區域。
2)Seed Filling(種子填充法)
種子填充方法來源於計算機圖形學,常用於對某個圖形進行填充。思路:選取一個前景像素點作爲種子,然後根據連通區域的兩個基本條件(像素值相同、位置相鄰)將與種子相鄰的前景像素合併到同一個像素集合中,最後得到的該像素集合則爲一個連通區域。
下面給出基於種子填充法的連通區域分析方法:
(1)掃描圖像,直到當前像素點B(x,y) == 1:
a、將B(x,y)作爲種子(像素位置),並賦予其一個label,然後將該種子相鄰的所有前景像素都壓入棧中;
b、彈出棧頂像素,賦予其相同的label,然後再將與該棧頂像素相鄰的所有前景像素都壓入棧中;
c、重複b步驟,直到棧爲空;
此時,便找到了圖像B中的一個連通區域,該區域內的像素值被標記爲label;
(2)重複第(1)步,直到掃描結束;
掃描結束後,就可以得到圖像B中所有的連通區域;
三、程序代碼
#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>
using namespace std;
void Seed_Filling(const cv::Mat& binImg, cv::Mat& lableImg) //種子填充法
{
// 4鄰接方法
if (binImg.empty() ||
binImg.type() != CV_8UC1)
{
return;
}
lableImg.release();
binImg.convertTo(lableImg, CV_32SC1);
int label = 1;
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; // 沒有重複的團,開始新的標籤
while (!neighborPixels.empty())
{
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));
}
}
}
}
}
}
void Two_Pass(const cv::Mat& binImg, cv::Mat& lableImg) //兩遍掃描法
{
if (binImg.empty() ||
binImg.type() != CV_8UC1)
{
return;
}
// 第一個通路
lableImg.release();
binImg.convertTo(lableImg, CV_32SC1);
int label = 1;
std::vector<int> labelSet;
labelSet.push_back(0);
labelSet.push_back(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); // 不連通,標籤+1
data_curRow[j] = label;
labelSet[label] = label;
}
else
{
std::sort(neighborLabels.begin(), neighborLabels.end());
int smallestLabel = neighborLabels[0];
data_curRow[j] = smallestLabel;
// 保存最小等價表
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;
} ;
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];
}
}
}
//彩色顯示
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)
{
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();
}
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()
{
cv::Mat binImage = cv::imread("test.jpg", 0);
cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV);
cv::Mat labelImg;
Two_Pass(binImage, labelImg, num);
//Seed_Filling(binImage, labelImg);
//彩色顯示
cv::Mat colorLabelImg;
LabelColor(labelImg, colorLabelImg);
cv::imshow("colorImg", colorLabelImg);
/* //灰度顯示
cv::Mat grayImg;
labelImg *= 10;
labelImg.convertTo(grayImg, CV_8UC1);
cv::imshow("labelImg", grayImg);
*/
cv::waitKey(0);
return 0;
}
四、演示結果
原圖:
效果圖:
參考文章:
http://www.cnblogs.com/ronny/p/img_aly_01.html
http://blog.csdn.net/icvpr/article/details/10259577