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方法實現上)。

[cpp] view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling By Two-Pass Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9.   
  10. #include <opencv2/imgproc/imgproc.hpp>  
  11. #include <opencv2/highgui/highgui.hpp>  
  12.   
  13.   
  14. void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  15. {  
  16.     // connected component analysis (4-component)  
  17.     // use two-pass algorithm  
  18.     // 1. first pass: label each foreground pixel with a label  
  19.     // 2. second pass: visit each labeled pixel and merge neighbor labels  
  20.     //   
  21.     // foreground pixel: _binImg(x,y) = 1  
  22.     // background pixel: _binImg(x,y) = 0  
  23.   
  24.   
  25.     if (_binImg.empty() ||  
  26.         _binImg.type() != CV_8UC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     // 1. first pass  
  32.   
  33.     _lableImg.release() ;  
  34.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  35.   
  36.     int label = 1 ;  // start by 2  
  37.     std::vector<int> labelSet ;  
  38.     labelSet.push_back(0) ;   // background: 0  
  39.     labelSet.push_back(1) ;   // foreground: 1  
  40.   
  41.     int rows = _binImg.rows - 1 ;  
  42.     int cols = _binImg.cols - 1 ;  
  43.     for (int i = 1; i < rows; i++)  
  44.     {  
  45.         int* data_preRow = _lableImg.ptr<int>(i-1) ;  
  46.         int* data_curRow = _lableImg.ptr<int>(i) ;  
  47.         for (int j = 1; j < cols; j++)  
  48.         {  
  49.             if (data_curRow[j] == 1)  
  50.             {  
  51.                 std::vector<int> neighborLabels ;  
  52.                 neighborLabels.reserve(2) ;  
  53.                 int leftPixel = data_curRow[j-1] ;  
  54.                 int upPixel = data_preRow[j] ;  
  55.                 if ( leftPixel > 1)  
  56.                 {  
  57.                     neighborLabels.push_back(leftPixel) ;  
  58.                 }  
  59.                 if (upPixel > 1)  
  60.                 {  
  61.                     neighborLabels.push_back(upPixel) ;  
  62.                 }  
  63.   
  64.                 if (neighborLabels.empty())  
  65.                 {  
  66.                     labelSet.push_back(++label) ;  // assign to a new label  
  67.                     data_curRow[j] = label ;  
  68.                     labelSet[label] = label ;  
  69.                 }  
  70.                 else  
  71.                 {  
  72.                     std::sort(neighborLabels.begin(), neighborLabels.end()) ;  
  73.                     int smallestLabel = neighborLabels[0] ;    
  74.                     data_curRow[j] = smallestLabel ;  
  75.   
  76.                     // save equivalence  
  77.                     for (size_t k = 1; k < neighborLabels.size(); k++)  
  78.                     {  
  79.                         int tempLabel = neighborLabels[k] ;  
  80.                         int& oldSmallestLabel = labelSet[tempLabel] ;  
  81.                         if (oldSmallestLabel > smallestLabel)  
  82.                         {                             
  83.                             labelSet[oldSmallestLabel] = smallestLabel ;  
  84.                             oldSmallestLabel = smallestLabel ;  
  85.                         }                         
  86.                         else if (oldSmallestLabel < smallestLabel)  
  87.                         {  
  88.                             labelSet[smallestLabel] = oldSmallestLabel ;  
  89.                         }  
  90.                     }  
  91.                 }                 
  92.             }  
  93.         }  
  94.     }  
  95.   
  96.     // update equivalent labels  
  97.     // assigned with the smallest label in each equivalent label set  
  98.     for (size_t i = 2; i < labelSet.size(); i++)  
  99.     {  
  100.         int curLabel = labelSet[i] ;  
  101.         int preLabel = labelSet[curLabel] ;  
  102.         while (preLabel != curLabel)  
  103.         {  
  104.             curLabel = preLabel ;  
  105.             preLabel = labelSet[preLabel] ;  
  106.         }  
  107.         labelSet[i] = curLabel ;  
  108.     }  
  109.   
  110.   
  111.     // 2. second pass  
  112.     for (int i = 0; i < rows; i++)  
  113.     {  
  114.         int* data = _lableImg.ptr<int>(i) ;  
  115.         for (int j = 0; j < cols; j++)  
  116.         {  
  117.             int& pixelLabel = data[j] ;  
  118.             pixelLabel = labelSet[pixelLabel] ;   
  119.         }  
  120.     }  
  121. }</span>  




2)Seed-Filling種子填充方法

說明:

基於OpenCV和C++實現;領域:4-領域。


[cpp] view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling By Seed-Filling Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14.   
  15. void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  16. {  
  17.     // connected component analysis (4-component)  
  18.     // use seed filling algorithm  
  19.     // 1. begin with a foreground pixel and push its foreground neighbors into a stack;  
  20.     // 2. pop the top pixel on the stack and label it with the same label until the stack is empty  
  21.     //   
  22.     // foreground pixel: _binImg(x,y) = 1  
  23.     // background pixel: _binImg(x,y) = 0  
  24.   
  25.   
  26.     if (_binImg.empty() ||  
  27.         _binImg.type() != CV_8UC1)  
  28.     {  
  29.         return ;  
  30.     }  
  31.   
  32.     _lableImg.release() ;  
  33.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  34.   
  35.     int label = 1 ;  // start by 2  
  36.   
  37.     int rows = _binImg.rows - 1 ;  
  38.     int cols = _binImg.cols - 1 ;  
  39.     for (int i = 1; i < rows-1; i++)  
  40.     {  
  41.         int* data= _lableImg.ptr<int>(i) ;  
  42.         for (int j = 1; j < cols-1; j++)  
  43.         {  
  44.             if (data[j] == 1)  
  45.             {  
  46.                 std::stack<std::pair<int,int>> neighborPixels ;     
  47.                 neighborPixels.push(std::pair<int,int>(i,j)) ;     // pixel position: <i,j>  
  48.                 ++label ;  // begin with a new label  
  49.                 while (!neighborPixels.empty())  
  50.                 {  
  51.                     // get the top pixel on the stack and label it with the same label  
  52.                     std::pair<int,int> curPixel = neighborPixels.top() ;  
  53.                     int curX = curPixel.first ;  
  54.                     int curY = curPixel.second ;  
  55.                     _lableImg.at<int>(curX, curY) = label ;  
  56.   
  57.                     // pop the top pixel  
  58.                     neighborPixels.pop() ;  
  59.   
  60.                     // push the 4-neighbors (foreground pixels)  
  61.                     if (_lableImg.at<int>(curX, curY-1) == 1)  
  62.                     {// left pixel  
  63.                         neighborPixels.push(std::pair<int,int>(curX, curY-1)) ;  
  64.                     }  
  65.                     if (_lableImg.at<int>(curX, curY+1) == 1)  
  66.                     {// right pixel  
  67.                         neighborPixels.push(std::pair<int,int>(curX, curY+1)) ;  
  68.                     }  
  69.                     if (_lableImg.at<int>(curX-1, curY) == 1)  
  70.                     {// up pixel  
  71.                         neighborPixels.push(std::pair<int,int>(curX-1, curY)) ;  
  72.                     }  
  73.                     if (_lableImg.at<int>(curX+1, curY) == 1)  
  74.                     {// down pixel  
  75.                         neighborPixels.push(std::pair<int,int>(curX+1, curY)) ;  
  76.                     }  
  77.                 }         
  78.             }  
  79.         }  
  80.     }  
  81. }</span>  



3)顏色標記(用於顯示)


[cpp] view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling -- Color Labeling   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. cv::Scalar icvprGetRandomColor()  
  15. {  
  16.     uchar r = 255 * (rand()/(1.0 + RAND_MAX));  
  17.     uchar g = 255 * (rand()/(1.0 + RAND_MAX));  
  18.     uchar b = 255 * (rand()/(1.0 + RAND_MAX));  
  19.     return cv::Scalar(b,g,r) ;  
  20. }  
  21.   
  22.   
  23. void icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)   
  24. {  
  25.     if (_labelImg.empty() ||  
  26.         _labelImg.type() != CV_32SC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     std::map<int, cv::Scalar> colors ;  
  32.   
  33.     int rows = _labelImg.rows ;  
  34.     int cols = _labelImg.cols ;  
  35.   
  36.     _colorLabelImg.release() ;  
  37.     _colorLabelImg.create(rows, cols, CV_8UC3) ;  
  38.     _colorLabelImg = cv::Scalar::all(0) ;  
  39.   
  40.     for (int i = 0; i < rows; i++)  
  41.     {  
  42.         const int* data_src = (int*)_labelImg.ptr<int>(i) ;  
  43.         uchar* data_dst = _colorLabelImg.ptr<uchar>(i) ;  
  44.         for (int j = 0; j < cols; j++)  
  45.         {  
  46.             int pixelValue = data_src[j] ;  
  47.             if (pixelValue > 1)  
  48.             {  
  49.                 if (colors.count(pixelValue) <= 0)  
  50.                 {  
  51.                     colors[pixelValue] = icvprGetRandomColor() ;  
  52.                 }  
  53.                 cv::Scalar color = colors[pixelValue] ;  
  54.                 *data_dst++   = color[0] ;  
  55.                 *data_dst++ = color[1] ;  
  56.                 *data_dst++ = color[2] ;  
  57.             }  
  58.             else  
  59.             {  
  60.                 data_dst++ ;  
  61.                 data_dst++ ;  
  62.                 data_dst++ ;  
  63.             }  
  64.         }  
  65.     }  
  66. }  
  67. </span>  




4)測試程序


[cpp] view plain copy
  1. <span style="font-size:12px">//  Connected Component Analysis/Labeling -- Test code  
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. int main(int argc, char** argv)  
  15. {  
  16.     cv::Mat binImage = cv::imread("../icvpr.com.jpg", 0) ;  
  17.     cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV) ;  
  18.   
  19.     // connected component labeling  
  20.     cv::Mat labelImg ;  
  21.     icvprCcaByTwoPass(binImage, labelImg) ;  
  22.     //icvprCcaBySeedFill(binImage, labelImg) ;  
  23.   
  24.     // show result  
  25.     cv::Mat grayImg ;  
  26.     labelImg *= 10 ;  
  27.     labelImg.convertTo(grayImg, CV_8UC1) ;  
  28.     cv::imshow("labelImg", grayImg) ;  
  29.   
  30.     cv::Mat colorLabelImg ;  
  31.     icvprLabelColor(labelImg, colorLabelImg) ;  
  32.     cv::imshow("colorImg", colorLabelImg) ;  
  33.     cv::waitKey(0) ;  
  34.   
  35.     return 0 ;  
  36. }</span>  




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



注: 原文鏈接http://blog.csdn.net/icvpr/article/details/10259577

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章