OpenCV_連通區域分析(Connected Component Analysis-Labeling)
OpenCV_連通區域分析(Connected Component Analysis/Labeling)
【摘要】
本文主要介紹在CVPR和影象處理領域中較為常用的一種影象區域(Blob)提取的方法——連通性分析法(連通區域標記法)。文中介紹了兩種常見的連通性分析的演算法:1)Two-pass;2)Seed-Filling種子填充,並給出了兩個演算法的基於OpenCV的C++實現程式碼。
一、連通區域分析
連通區域(Connected Component)一般是指影象中具有相同畫素值且位置相鄰的前景畫素點組成的影象區域(Region,Blob)。連通區域分析(Connected Component Analysis,Connected Component Labeling
)是指將影象中的各個連通區域找出並標記。
連通區域分析是一種在CVPR和影象分析處理的眾多應用領域中較為常用和基本的方法。例如:OCR識別中字元分割提取(車牌識別、文字識別、字幕識別等)、視覺跟蹤中的運動前景目標分割與提取(行人入侵檢測、遺留物體檢測、基於視覺的車輛檢測與跟蹤等)、醫學影象處理(感興趣目標區域提取)、等等。也就是說,在需要將前景目標提取出來以便後續進行處理的應用場景中都能夠用到連通區域分析方法,通常連通區域分析處理的物件是一張二值化後的影象。
二、連通區域分析的演算法
從連通區域的定義可以知道,一個連通區域是由具有相同畫素值的相鄰畫素組成畫素集合,因此,我們就可以通過這兩個條件在影象中尋找連通區域,對於找到的每個連通區域,我們賦予其一個唯一的標識(Label),以區別其他連通區域。
連通區域分析有基本的演算法,也有其改進演算法,本文介紹其中的兩種常見演算法:
1)Two-Pass法;2)Seed-Filling種子填充法;
Note:
a、這裡的掃描指的是按行或按列訪問以便影象的所有畫素,本文演算法採用的是按行掃描方式;
b、影象記為B,為二值影象:前景畫素(pixel value = 1),背景畫素(pixel value = 0)
c、label從2開始計數;
d、畫素相鄰關係:4-領域、8-領域,本文演算法採用4-鄰域;
4—領域圖例 8—領域圖例
1)Two-Pass(兩遍掃描法)
兩遍掃描法,正如其名,指的就是通過掃描兩遍影象,就可以將影象中存在的所有連通區域找出並標記。思路:第一遍掃描時賦予每個畫素位置一個label,掃描過程中同一個連通區域內的畫素集合中可能會被賦予一個或多個不同label,因此需要將這些屬於同一個連通區域但具有不同值的label合併,也就是記錄它們之間的相等關係;第二遍掃描就是將具有相等關係的equal_labels所標記的畫素歸為一個連通區域並賦予一個相同的label(通常這個label是equal_labels中的最小值)。
下面給出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);
完成掃描後,影象中具有相同label值的畫素就組成了同一個連通區域。
下面這張圖動態地演示了Two-pass演算法:
2)Seed Filling(種子填充法)
種子填充方法來源於計算機圖形學,常用於對某個圖形進行填充。思路:選取一個前景畫素點作為種子,然後根據連通區域的兩個基本條件(畫素值相同、位置相鄰)將與種子相鄰的前景畫素合併到同一個畫素集合中,最後得到的該畫素集合則為一個連通區域。
下面給出基於種子填充法的連通區域分析方法:
(1)掃描影象,直到當前畫素點B(x,y) == 1:
a、將B(x,y)作為種子(畫素位置),並賦予其一個label,然後將該種子相鄰的所有前景畫素都壓入棧中;
b、彈出棧頂畫素,賦予其相同的label,然後再將與該棧頂畫素相鄰的所有前景畫素都壓入棧中;
c、重複b步驟,直到棧為空;
此時,便找到了影象B中的一個連通區域,該區域內的畫素值被標記為label;
(2)重複第(1)步,直到掃描結束;
掃描結束後,就可以得到影象B中所有的連通區域;
下面這張圖動態地演示了Seed-Filling演算法:
三、實驗演示
1)前景二值影象
2)連通區域分析方法標記後得到的label影象
Two-pass演算法:
Seed-filling演算法:
注:為了顯示方便,將畫素值乘以了一個整數進行放大。
3)color後的label影象
Two-pass演算法:
Seed-filling演算法:
注:顏色是隨機生成的。
四、程式碼
1)Two-pass演算法的一種實現
說明:
基於OpenCV和C++實現,領域:4-領域。實現與演算法描述稍有差別(具體為記錄具有相等關係的label方法實現上)。
// 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 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 ; } } } } } } // 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] ; pixelLabel = labelSet[pixelLabel] ; } } }
2)Seed-Filling種子填充方法
說明:
基於OpenCV和C++實現;領域:4-領域。
// Connected Component Analysis/Labeling By Seed-Filling Algorithm // Author: www.icvpr.com // Blog : http://blog.csdn.net/icvpr #include <iostream> #include <string> #include <list> #include <vector> #include <map> #include <stack> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg) { // connected component analysis (4-component) // use seed filling algorithm // 1. begin with a foreground pixel and push its foreground neighbors into a stack; // 2. pop the top pixel on the stack and label it with the same label until the stack is empty // // foreground pixel: _binImg(x,y) = 1 // background pixel: _binImg(x,y) = 0 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)) ; // pixel position: <i,j> ++label ; // begin with a new label while (!neighborPixels.empty()) { // get the top pixel on the stack and label it with the same label std::pair<int,int> curPixel = neighborPixels.top() ; int curX = curPixel.first ; int curY = curPixel.second ; _lableImg.at<int>(curX, curY) = label ; // pop the top pixel neighborPixels.pop() ; // push the 4-neighbors (foreground pixels) if (_lableImg.at<int>(curX, curY-1) == 1) {// left pixel neighborPixels.push(std::pair<int,int>(curX, curY-1)) ; } if (_lableImg.at<int>(curX, curY+1) == 1) {// right pixel neighborPixels.push(std::pair<int,int>(curX, curY+1)) ; } if (_lableImg.at<int>(curX-1, curY) == 1) {// up pixel neighborPixels.push(std::pair<int,int>(curX-1, curY)) ; } if (_lableImg.at<int>(curX+1, curY) == 1) {// down pixel neighborPixels.push(std::pair<int,int>(curX+1, curY)) ; } } } } } }
3)顏色標記(用於顯示)
// Connected Component Analysis/Labeling -- Color Labeling // Author: www.icvpr.com // Blog : http://blog.csdn.net/icvpr #include <iostream> #include <string> #include <list> #include <vector> #include <map> #include <stack> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> 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++ ; } } } }
4)測試程式
// Connected Component Analysis/Labeling -- Test code // Author: www.icvpr.com // Blog : http://blog.csdn.net/icvpr #include <iostream> #include <string> #include <list> #include <vector> #include <map> #include <stack> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> int main(int argc, char** argv) { cv::Mat binImage = cv::imread("../icvpr.com.jpg", 0) ; cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV) ; // connected component labeling cv::Mat labelImg ; icvprCcaByTwoPass(binImage, labelImg) ; //icvprCcaBySeedFill(binImage, labelImg) ; // show result cv::Mat grayImg ; 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 ; }
Reference
[1] http://en.wikipedia.org/wiki/Connected-component_labeling
[2] http://homepages.inf.ed.ac.uk/rbf/HIPR2/label.htm
[3] http://www.codeproject.com/Articles/336915/Connected-Component-Labeling-Algorithm