OPENCV輪廓提取findContours和drawContours
最近用OPENCV的輪廓提取函式,總結一下。
void findContours//提取輪廓,用於提取影象的輪廓
(
InputOutputArray image,//輸入影象,必須是8位單通道影象,並且應該轉化成二值的
OutputArrayOfArrays contours,//檢測到的輪廓,每個輪廓被表示成一個point向量
OutputArray hierarchy,//可選的輸出向量,包含影象的拓撲資訊。其中元素的個數和檢測到的輪廓的數量相等
int mode,//說明需要的輪廓型別和希望的返回值方式
int method,//輪廓近似方法
Point offset = Point()
)
void drawContours//繪製輪廓,用於繪製找到的影象輪廓
(
InputOutputArray image,//要繪製輪廓的影象
InputArrayOfArrays contours,//所有輸入的輪廓,每個輪廓被儲存成一個point向量
int contourIdx,//指定要繪製輪廓的編號,如果是負數,則繪製所有的輪廓
const Scalar& color,//繪製輪廓所用的顏色
int thickness = 1, //繪製輪廓的線的粗細,如果是負數,則輪廓內部被填充
int lineType = 8, /繪製輪廓的線的連通性
InputArray hierarchy = noArray(),//關於層級的可選引數,只有繪製部分輪廓時才會用到
int maxLevel = INT_MAX,//繪製輪廓的最高級別,這個引數只有hierarchy有效的時候才有效
//maxLevel=0,繪製與輸入輪廓屬於同一等級的所有輪廓即輸入輪廓和與其相鄰的輪廓
//maxLevel=1, 繪製與輸入輪廓同一等級的所有輪廓與其子節點。
//maxLevel=2,繪製與輸入輪廓同一等級的所有輪廓與其子節點以及子節點的子節點
Point offset = Point()
)
注意:findContours()執行的時候,這個影象會被直接塗改,因此如果是將來還有用的影象,應該複製之後再傳給findContours()。
接下來總結一下我在書上看到的解釋和自己實驗的結果。
一《學習opencv》中給的例子。
圖1 輸入的測試圖(上圖)和得到的輪廓圖(下圖)
傳遞給findContours()測試圖(上圖),得到輪廓圖(下圖)。得到的輪廓只有兩種,外部輪廓(橙色虛線)或者孔(藍色點線)。
1.引數mode的意義
mode的值決定把找到的輪廓如何掛到輪廓樹節點變數(h_prev, h_next, v_prev, v_next)上。圖2展示了四種可能的mode值所得到的結果的拓撲結構。
圖2 輪廓連線方法
每種情況下,結構都可以看成是被橫向連線(h_prev, h_next)聯絡和被縱向連線(v_prev, v_next)不同層次。
CV_RETR_EXTERNAL 只檢測出最外輪廓即c0。圖2中第一個輪廓指向最外的序列,除此之外沒有別的連線。
CV_RETR_LIST 檢測出所有的輪廓並將他們儲存到表(list)中,圖2中描繪了這個表,被找到的9條輪廓相互之間由h_prev和h_next連線。這裡並沒有表達出縱向的連線關係,沒有使用v_prev和v_next.
CV_RETR_COMP 檢測出所有的輪廓並將他們組織成雙層的結構,第一層是外部輪廓邊界,第二層邊界是孔的邊界。從圖2可以看到5個輪廓的邊界,其中3個包含孔。最外層邊界c0有兩個孔,c0之間的所有孔相互間由h_prev和h_next指標連線。
CV_RETR_TREE 檢測出所有輪廓並且重新建立網狀的輪廓結構。圖2中,根節點是最外層的邊界c0,c0之下是孔h00,在同一層中與另一個孔h01相連線。同理,每個孔都有子節點(相對於c000和c010),這些子節點和父節點被垂直連線起來。這個步驟一直持續到影象最內層的輪廓,這些輪廓會成為樹葉節點。
2. method的五個值
CV_CHAIN_CODE 用freeman鏈碼輸出輪廓,其他方法輸出多邊形(頂點的序列)。
CV_CHAIN_APPROX_NONE將鏈碼編碼中的所有點轉換為點。
CV_CHAIN_APPROX_SIMPLE壓縮水平,垂直或斜的部分,只儲存最後一個點。
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_QPPROX_TC89_KCOS使用Teh-Chin鏈逼近演算法中的一個。
CV_LINK_RUNS與上述的演算法完全不同,連線所有的水平層次的輪廓。
二 實驗部分
我用跟上面的例子結構一樣的圖作為測試圖片(這是我照著上面的圖自己畫的)。
圖3 原圖
測試程式碼如下:
#include "opencv/highgui.h"
#include "opencv/cv.h"
using namespace cv;
void main()
{
Mat image = imread("test.png");
Mat gray;
cvtColor(image, gray, CV_BGR2GRAY);
GaussianBlur(gray, gray, Size(3, 3), 3, 3);
Mat img;
threshold(gray, img, 0, 255, THRESH_OTSU);
Mat img1;
img.copyTo(img1);
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(img, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);
Mat resultImage = Mat ::zeros(img.size(),CV_8U);
drawContours(resultImage, contours, -1, Scalar(255, 0, 255));
return;
}
1. mode的四種方法
下圖是分別用mode的四種方法得到的輪廓的結果
圖4 四種mode方式分別得到的輪廓結果 從圖中可以看出,圖4(a)得到的只有一個最外的輪廓,hierarchy的值為{-1,-1,-1,-1},表示除此之外沒有別的連線。其他三種方式都可以得到所有的輪廓,而hierarchy的值是不同的,反映了不同的輪廓連線方法即結果的拓撲機構。 2.method的五種方法 CV_CHAIN_CODE是用Freeman鏈碼輸出輪廓,在Freeman鏈碼中,多邊形被表示成一系列位移,每一個位移有8個方向,使用整數0~7表示。關於Freeman鏈碼的具體內容《學習opencv》這本書裡有詳細的介紹。通過試驗發現用這種方式表示的輪廓不可以通過drawContours()繪製出檢測到的輪廓。 CV_CHAIN_APPROX_NONE 是將鏈碼編碼中的所有點轉換為點,也就是引數contours中的每個輪廓是用構成該輪廓的所有畫素點表示的。 CV_CHAIN_APPROX_NONE是輸出多邊形頂點的序列,也就是說引數contours中的每個輪廓是用該輪廓的所有頂點表示的。 圖5分別畫出了這兩種方式下的contours中的點,用紅色的點表示。 圖5 輪廓的表達方式 其中(a)中由於畫素之間沒有間隔已經練成線了。 圖6是CV_CHAIN_APPROX_TC89_L1和CV_LINK_RUNS兩種方式下contours的結果。 圖6 輪廓的表達方式 從圖中可以看出,CV_CHAIN_APPROX_TC89_L1的結果也是儲存的輪廓的頂點,但是仔細看可以看出輪廓線是有斷的感覺不是連貫的,這可能是輪廓逼近過程中的誤差,具體情況就不清楚了,也有可能這種方法對具有某種特徵的圖逼近效果比較好。 書上說CV_LINK_RUNS是和上述演算法完全不同的演算法,連線所有水平層次的輪廓,但結果畫出來的是一條條垂直的線,也不清楚具體怎麼回事,不過應該也肯定是有它存在的意義的。另外,在mode=CV_RETR_EXTERNAL的前提下,選用這種方式也是可以畫出所有輪廓的,而其他的方式只能畫出最外層的輪廓。還有書上說此方法只可與 CV_RETR_LIST搭配使用。我試了其他的方法,也是可以的。 所以method這個引數決定了輪廓的表達方式,這要根據自己提取輪廓後的應用選擇合適的輪廓描述方法來決定用哪一種。 3.drawContours()函式中的引數thinkness thinkness=CV_FILLED可以填充輪廓,opencv官網對這個引數的解釋原文是“If it is negative (for example,thickness=CV_FILLED ), the contour interiors are drawn”,圖7給出了這個thinkness=CV_FILLED時的繪製結果,可以看出我們得到了跟原圖一致的結果。 圖7 輪廓繪製結果 關於更復雜的情況就沒研究了,我要的就是圖7這樣的結果。有不對的地方希望大家批評指正!