1. 程式人生 > >影象分析:二值影象連通域標記2-基於輪廓的標記

影象分析:二值影象連通域標記2-基於輪廓的標記

一、前言

二值影象,顧名思義就是影象的亮度值只有兩個狀態:黑(0)和白(255)。二值影象在影象分析與識別中有著舉足輕重的地位,因為其模式簡單,對畫素在空間上的關係有著極強的表現力。在實際應用中,很多影象的分析最終都轉換為二值影象的分析,比如:醫學影象分析、前景檢測、字元識別,形狀識別。二值化+數學形態學能解決很多計算機識別工程中目標提取的問題。

二值影象分析最重要的方法就是連通區域標記,它是所有二值影象分析的基礎,它通過對二值影象中白色畫素(目標)的標記,讓每個單獨的連通區域形成一個被標識的塊,進一步的我們就可以獲取這些塊的輪廓、外接矩形、質心、不變矩等幾何引數。

下面是一個二值影象被標記後,比較形象的顯示效果,這就是我們這篇文章的目標。

image

二、連通域

在我們討論連通區域標記的演算法之前,我們先要明確什麼是連通區域,怎樣的畫素鄰接關係構成連通。在影象中,最小的單位是畫素,每個畫素周圍有8個鄰接畫素,常見的鄰接關係有2種:4鄰接與8鄰接。4鄰接一共4個點,即上下左右,如下左圖所示。8鄰接的點一共有8個,包括了對角線位置的點,如下右圖所示。

image        image

如果畫素點A與B鄰接,我們稱A與B連通,於是我們不加證明的有如下的結論:

如果A與B連通,B與C連通,則A與C連通。

在視覺上看來,彼此連通的點形成了一個區域,而不連通的點形成了不同的區域。這樣的一個所有的點彼此連通點構成的集合,我們稱為一個連通區域。

下面這符圖中,如果考慮4鄰接,則有3個連通區域;如果考慮8鄰接,則有2個連通區域。(注:影象是被放大的效果,影象正方形實際只有4個畫素)。

image

三、連通區域的標記

連通區域標記演算法有很多種,有的演算法可以一次遍歷影象完成標記,有的則需要2次或更多次遍歷影象。這也就造成了不同的演算法時間效率的差別,在這裡我們介紹2種演算法。

第一種演算法是現在matlab中連通區域標記函式bwlabel中使的演算法,它一次遍歷影象,並記下每一行(或列)中連續的團(run)和標記的等價對,然後通過等價對對原來的影象進行重新標記,這個演算法是目前我嘗試的幾個中效率最高的一個,但是演算法裡用到了稀疏矩陣與Dulmage-Mendelsohn分解演算法用來消除等價對,這部分原理比較麻煩,所以本文裡將不介紹這個分解演算法,取而代這的用圖的深度優先遍歷來替換等價對。

第二種演算法是現在開源庫cvBlob中使用的標記演算法,它通過定位連通區域的內外輪廓來標記整個影象,這個演算法的核心是輪廓的搜尋演算法,這個我們將在文章中詳細介紹。這個演算法相比與第一種方法效率上要低一些,但是在連通區域個數在100以內時,兩者幾乎無差別,當連通區域個數到了103

103數量級時,上面的演算法會比該演算法快10倍以上

五、基於輪廓的標記

在這裡我還是先給出演算法描述:

1,從上至下,從左至右依次遍歷影象。

2,如下圖A所示,A為遇到一個外輪廓點(其實上遍歷過程中第一個遇到的白點即為外輪廓點),且沒有被標記過,則給A一個新的標記號。我們從A點出發,按照一定的規則(這個規則後面詳細介紹)將A所在的外輪廓點全部跟蹤到,然後回到A點,並將路徑上的點全部標記為A的標號。

3,如下圖B所示,如果遇到已經標記過的外輪廓點AA′,則從AA′向右,將它右邊的點都標記為AA′的標號,直到遇到黑色畫素為止。

4,如下圖C所示,如果遇到了一個已經被標記的點B,且是內輪廓的點(它的正下方畫素為黑色畫素且不在外輪廓上),則從B點開始,跟蹤內輪廓,路徑上的點都設定為B的標號,因為B已經被標記過與A相同,所以內輪廓與外輪廓將標記相同的標號。

5,如下圖D所示,如果遍歷到內輪廓上的點,則也是用輪廓的標號去標記它右側的點,直到遇到黑色畫素為止。

6,結束。

image

整個演算法步驟,我們只掃描了一次影象,同時我們對影象中的畫素進行標記,要麼賦予一個新的標號,要麼用它同行的左邊的標號去標記它,下面是演算法更細的描述

對於一個需要標記的影象II,我們定義一個與它對應的標記影象LL,用來儲存標記資訊,開始我們把L上的所有值設定為0,同時我們有一個標籤變數CC,初始化為1。然後我們開始掃描影象I,遇到白色畫素時,設這個點為PP點,我們需要按下面不同情況進行不同的處理:

情況1:如果P(i,j)P(i,j)點是一個白色畫素,在LL影象上這個位置沒有被標記過,而且PP點的上方為黑色,則P是一個新的外輪廓的點,這時候我們將C的標籤值標記給L影象上P點的位置(x,y)(x,y),即L(x,y)=CL(x,y)=C,接著我們沿著P點開始做輪廓跟蹤,並把把輪廓上的點對應的L上都標記為C,完成整個輪廓的搜尋與標記後,回到了P點。最後不要忘了把C的值加1。這個過程如下面影象S1中所示。

image

情況2:如果P點的下方的點是unmarked點(什麼是unmark點,情況3介紹完就會給出定義),則P點一定是內輪廓上的點,這時候有兩種情況,一種是P點在L上已經被標記過了,說明這個點同時也是外輪廓上的點;另一種情況是P點在L上還沒有被標記過,那如果是按上面步驟來的,P點左邊的點一定被標記了(這一處剛開始理解可能不容易,不妨畫一個簡單的圖,自己試著一個點一個點標記試試,就容易理解了),所以這時候我們採用P點左邊點的標記值來標記P,接著從P點開始跟蹤內輪廓把內輪廓上的點都標記為P的標號。

下面影象顯示了上面分析的兩種P的情況,左圖的P點既是外輪廓上的點也是內輪廓上的點。

image    image

情況3:如果一個點P,不是上面兩種情況之一,那麼P點的左邊一定被標記過(不理解,就手動去標記上面兩幅影象),我們只需要用它左邊的標號去標記L上的P點。

現在我們只剩下一個問題了,就是什麼是unmarked點,我們知道內輪廓點開始點P的下方一定是一個黑色畫素,是不是黑色畫素就是unmarked點呢,顯然不是,如下影象的Q點,它的下面也是黑色畫素,然而它卻不是內輪廓上的點。

實際上在我們在輪廓跟蹤時,我們我輪廓點的周圍做了標記,在輪廓點周圍被查詢過的點(查詢方式見下面的輪廓跟蹤演算法)在L上被標記了一個負值(如下面右圖所示),所以Q點的下方被標記為了負值,這樣Q的下方就不是一個unmarked點,unmarked點,即在L上的標號沒有被修改過,即為0。

image      image

顯然,這個演算法的重點在於輪廓的查詢與標記,而對於輪廓的查詢,就是確定搜尋策略的問題,我們下面給內輪廓與外輪廓定義tracker規則。

我們對一點畫素點周圍的8個點分析作一個標號0-7,因為我們在遍歷影象中第一個遇到的點肯定是外輪廓點,所以我們先來確定外輪廓的搜尋策略,對於外輪廓的點P,有兩種情況:

image

1)如果P是外輪廓的起點,也就是說我們是從P點開始跟蹤的,那麼我們從7號(右上角)位置P1P1開始,看7號是不是白色點,如果是,則把這個點加入外輪廓點中,並將它標記與P點相同,如果7號點是黑色點,則按順時針7-0-1-2-3-4-5-6這個順序搜尋直到遇到白點為止,把那個點確定為P1P1,加入外輪廓,並把這個點的標號設定與P點相同。這裡很重要一步就是,假設我們2號點才是白點,那麼7,0,1這三個位置我們都搜尋過,所以我們要把這些點在L上標記為一個負值。如下圖所示,其中右影象標記的結果。

image    image

2)那麼如果P是不是外輪廓的起點,即P是外輪廓路徑上的一個點,那麼它肯定是由一個點進入的,我們設定為P1P−1點,P1P−1點的位置為x(0<=x<=7)x(0<=x<=7),那麼P點從(x+2)mod8(x+2)mod8這個位置開始尋找下一步的路徑,(x+2)mod8(x+2)mod8是加2取模的意思,它反映在影象就是從P-1點按順時針數2個格子的位置。確定搜尋起點後,按照上面一種情況進行下面的步驟。

外輪廓點的跟蹤方式確定了後,內輪廓點的跟蹤方式大同小異,只是如果P是內輪廓的第一個點,則它的開始搜尋位置不是7號點而是3號點。其他的與外輪廓完全一致。

如要上面搜尋方式,你不是很直觀的理解,不妨嘗試著去搜索下面這幅影象,你應該有能有明確的瞭解了。一個路徑搜尋結束的條件是,回到原始點S,則S周圍不存在unmarked點。

如下邊中間影象所示,從S點開始形成的路徑是STUTSVWV。

   image 

在OpenCV中查詢輪廓的函式已經存在了,而且可以得到輪廓之間的層次關係。這個函式按上面的演算法實現起來並不困難,所以這裡就不再實現這個函式,有興趣的可以看OpenCV的原始碼(contours.cpp)。

void bwLabelContour(const Mat& imgBw, Mat & imgLabeled)
{
  // Padding a Pixel around the pixel. 
  Mat imgClone = Mat(imgBw.rows + 1, imgBw.cols + 1, imgBw.type(), Scalar(0));
  imgBw.copyTo(imgClone(Rect(1, 1, imgBw.cols, imgBw.rows)));

  imgLabeled.create(imgClone.size(), imgClone.type());
  imgLabeled.setTo(Scalar::all(0));

  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  findContours(imgClone, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);

  vector<int> contoursLabel(contours.size(), 0);
  int numlab = 1;
  // Outer Nested Contours
  for (vector<vector<Point>>::size_type i = 0; i < contours.size(); i++)
  {
    if (hierarchy[i][3] >= 0) // Nested Contours.
    {
      continue;
    }
    for (vector<Point>::size_type k = 0; k != contours[i].size(); k++)
    {
      imgLabeled.at<uchar>(contours[i][k].y, contours[i][k].x) = numlab;
    }
    contoursLabel[i] = numlab++;
  }
  // Inner Nested Contours
  for (vector<vector<Point>>::size_type i = 0; i < contours.size(); i++)
  {
    if (hierarchy[i][3] < 0) // Not Nested Contours.
    {
      continue;
    }
    for (vector<Point>::size_type k = 0; k != contours[i].size(); k++)
    {
      imgLabeled.at<uchar>(contours[i][k].y, contours[i][k].x) = contoursLabel[hierarchy[i][3]];
    }
  }
  // Assign to the remaining non-contour Samples.
  for (int i = 0; i < imgLabeled.rows; i++)
  {
    for (int j = 0; j < imgLabeled.cols; j++)
    {
      if (imgClone.at<uchar>(i, j) != 0 && imgLabeled.at<uchar>(i, j) == 0)
      {
        imgLabeled.at<uchar>(i, j) = imgLabeled.at<uchar>(i, j - 1);
      }
    }
  }
  // Discard the Neighboring 1 Samples.
  imgLabeled = imgLabeled(Rect(1, 1, imgBw.cols, imgBw.rows)).clone(); 
}