基於內容的圖像分析的重點是提取出圖像中具有代表性的特征,而線條、輪廓、塊往往是最能體現特征的幾個元素,這篇文章就針對於這幾個重要的圖像特征,研究它們在OpenCV中的用法,以及做一些簡單的基礎應用。
一、Canny檢測輪廓
在上一篇文章中有提到sobel邊緣檢測,並重寫了soble的C++代碼讓其與matlab中算法效果一致,而soble邊緣檢測是基於單一閾值的,我們不能兼顧到低閾值的豐富邊緣和高閾值時的邊緣缺失這兩個問題。而canny算子則很好的彌補了這一不足,從目前看來,canny邊緣檢測在做圖像輪廓提取方面是最優秀的邊緣檢測算法。
canny邊緣檢測采用雙閾值值法,高閾值用來檢測圖像中重要的、顯著的線條、輪廓等,而低閾值用來保證不丟失細節部分,低閾值檢測出來的邊緣更豐富,但是很多邊緣並不是我們關心的。最后采用一種查找算法,將低閾值中與高閾值的邊緣有重疊的線條保留,其他的線條都刪除。
本篇文章中不對canny的算法原理作進一步說明,稍后會在圖像處理算法相關的文章中詳細介紹。
下面我們用OpenCV中的Canny函數來檢測圖像邊緣
1 int main() 2 { 3 Mat I=imread("../cat.png"); 4 cvtColor(I,I,CV_BGR2GRAY); 5 6 Mat contours; 7 Canny(I,contours,125,350); 8 threshold(contours,contours,128,255,THRESH_BINARY); 9 10 namedWindow("Canny"); 11 imshow("Canny",contours); 12 waitKey(); 13 return 0; 14 }
顯示效果如下:
二、直線檢測
直線在圖像中出現的頻率非常之高,而直線作為圖像的特征對於基本內容的圖像分析有着很重要的作用,本文通過OpenCV中的hough變換來檢測圖像中的線條。
我們先看最基本的Hough變換函數HoughLines,它的原型如下:
1 void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0 );
它的輸入是一個二值的輪廓圖像,往往是邊緣檢測得到的結果圖像;它的輸出是一個包含多個Vec2f點的數組,數組中的每個元素是一個二元浮點數據對<rou,theta>,rou代表直線離坐標原點的距離,theta代表角度。第3和第4個參數代表步長,因為Hough變換實際上是一個窮舉的算法,rho表示距離的步長,theta代表角度的步長。第5個參數是一個閾值設置直接的最低投票個數,知道Hough原理的,這個參數應該很容易理解。
從這個函數的輸出結果我們可以看出,得到的直線並沒有指定在圖像中的開始點與結束點,需要我們自己去計算,如果我們想把直接顯示在圖像中就會比較麻煩,而且會有很多角度接近的直線,其實它們是重復的,為了解決上面這些問題,OpenCV又提供了一個函數HoughLinesP()。它的輸出是一個Vector of Vec4i。Vector每一個元素代表一條直線,是由一個4元浮點數組構成,前兩個點一組,后兩個點一組,代表了在圖像中直線的起始和結束點。
1 void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta,int threshold, double minLineLength=0, double maxLineGap=0 );
解釋一下最后兩個參數,minLineLength指定了檢測直線中的最小寬度,如果低於最小寬度則舍棄掉,maxLineGap指定通過同一點的直線,如果距離小於maxLineGap就會進行合並。
下面是一個用HoughLinesP檢測直線的例子:
1 int main() 2 { 3 Mat image=imread("../car.png"); 4 Mat I; 5 cvtColor(image,I,CV_BGR2GRAY); 6 7 Mat contours; 8 Canny(I,contours,125,350); 9 threshold(contours,contours,128,255,THRESH_BINARY); 10 11 vector<Vec4i> lines; 12 // 檢測直線,最小投票為90,線條不短於50,間隙不小於10 13 HoughLinesP(contours,lines,1,CV_PI/180,80,50,10); 14 drawDetectLines(image,lines,Scalar(0,255,0)); 15 16 namedWindow("Lines"); 17 imshow("Lines",image); 18 waitKey(); 19 return 0; 20 }
上面程序將檢測到的線條保存在lines變量內,我們需要進一步將它們畫在圖像上:
1 void drawDetectLines(Mat& image,const vector<Vec4i>& lines,Scalar & color) 2 { 3 // 將檢測到的直線在圖上畫出來 4 vector<Vec4i>::const_iterator it=lines.begin(); 5 while(it!=lines.end()) 6 { 7 Point pt1((*it)[0],(*it)[1]); 8 Point pt2((*it)[2],(*it)[3]); 9 line(image,pt1,pt2,color,2); // 線條寬度設置為2 10 ++it; 11 } 12 }
實際上Hough變換可以檢測很多固定的形狀,比如:圓、正方形等。它們的原理基本相同,都是構造一個投票矩陣。OpenCV里提供了檢測圓的函數HoughCircles,它的輸出是一個Vector of Vec3i,Vector的每個元素包含了3個浮點數,前2個是圓的中心坐標,最后一個是半徑。
三、輪廓的提取與描述
在目標識別中我們首先要把感興趣的目標提取出來,而一般常見的步驟都是通過顏色或紋理提取出目標的前景圖(一幅黑白圖像,目標以白色顯示在圖像中),接下來我們要對前景圖進行分析進一步地把目標提取出來,而這里常常用到的就是提取目標的輪廓。
OpenCV里提取目標輪廓的函數是findContours,它的輸入圖像是一幅二值圖像,輸出的是每一個連通區域的輪廓點的集合:vector<vector<Point>>。外層vector的size代表了圖像中輪廓的個數,里面vector的size代表了輪廓上點的個數。下面我們通過實例來看函數的用法。
1 int main() 2 { 3 using namespace cv; 4 5 Mat image=imread("../shape.png"); 6 cvtColor(image,image,CV_BGR2GRAY); 7 vector<vector<Point>> contours; 8 // find 9 findContours(image,contours,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_NONE); 10 // draw 11 Mat result(image.size(),CV_8U,Scalar(0)); 12 drawContours(result,contours,-1,Scalar(255),2); 13 14 namedWindow("contours"); 15 imshow("contours",result); 16 waitKey(); 17 return 0; 18 }
上面程序中包含了2個函數,第一個是查找輪廓函數,它的第三個參數說明查找輪廓的類型,這里我們使用的是外輪廓,還可以查找所有輪廓,即包括一些孔洞的部分,像圖像人物胳膊與腰間形成的輪廓。第4個參數說明了輪廓表示的方法,程序中的參數說明輪廓包括了所有點,也可以用其他參數讓有點直線的地方,只保存直線起始與終點的位置點,具體參數用法可以參考手冊里函數的介紹。
第二個函數drawContours是一個畫輪廓的函數,它的第3個參數程序里設置-1表示所有的輪廓都畫,你也可以指定要畫的輪廓的序號。
提取到輪廓后,其實我們更關心的是如果把這些輪廓轉換為可以利用的特征,也就是涉及到輪廓的描述問題,這時就有多種方法可以選擇,比如矢量化為多邊形、矩形、橢圓等。OpenCV里提供了一些這樣的函數。
1 // 輪廓表示為一個矩形 2 Rect r = boundingRect(Mat(contours[0])); 3 rectangle(result, r, Scalar(255), 2); 4 5 // 輪廓表示為一個圓 6 float radius; 7 Point2f center; 8 minEnclosingCircle(Mat(contours[1]), center, radius); 9 circle(result, Point(center), static_cast<int>(radius), Scalar(255), 2); 10 11 // 輪廓表示為一個多邊形 12 vector<Point> poly; 13 approxPolyDP(Mat(contours[2]), poly, 5, true); 14 vector<Point>::const_iterator itp = poly.begin(); 15 while (itp != (poly.end() - 1)) 16 { 17 line(result, *itp, *(itp + 1), Scalar(255), 2); 18 ++itp; 19 } 20 line(result, *itp, *(poly.begin()), Scalar(255), 2); 21 // 輪廓表示為凸多邊形 22 vector<Point> hull; 23 convexHull(Mat(contours[3]), hull); 24 vector<Point>::const_iterator ith = hull.begin(); 25 while (ith != (hull.end() - 1)) 26 { 27 line(result, *ith, *(ith + 1), Scalar(255), 2); 28 ++ith; 29 } 30 line(result, *ith, *(hull.begin()), Scalar(255), 2);
程序中我們依次畫了矩形、圓、多邊形和凸多邊形。最終效果如下:
對連通區域的分析到此遠遠沒有結束,我們可以進一步計算每一個連通區域的其他屬性,比如:重心、中心矩等特征,這些內容以后有機會展開來寫。
以下幾個函數可以嘗試:minAreaRect:計算一個最小面積的外接矩形,contourArea可以計算輪廓內連通區域的面積;pointPolygenTest可以用來判斷一個點是否在一個多邊形內。mathShapes可以比較兩個形狀的相似性,相當有用的一個函數。