1. 程式人生 > >影象處理(五)——連通域

影象處理(五)——連通域

連通區域(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;
}