OpenCV—輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與重心標註(C++版)

OpenCV—輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與重心標註(C++版)

輪廓是定義或限定形狀或對象的邊或線,是機器視覺中的常用的概念,多用於目標檢測、識別等任務。關於OpenCV輪廓操作,尤其是級別及如何使用輪廓級別進行篩選等問題,相關文章比較少,正好最近用到,因此將其總結成文。本文主要介紹OpenCV的查找輪廓函數findContours()繪製函數drawContours(),及其輪廓級別參數hierarchy,涉及到預處理、輪廓篩選等內容,並提供全部源代碼,希望能幫助大家理解基本概念並能借鑑示例代碼編寫自己的算法。

本文代碼:C++

本文包括如下內容:

  • 基本概念

1.查找和繪製輪廓函數findContous(),drawContours()

2.輪廓參數:輪廓級別、輪廓長度

3.輪廓的形狀描述子:最小覆蓋矩形、圓、多邊形逼近、凸包

  • 編程實戰

1.如何篩選輪廓:按輪廓級別和長度篩選

2.如何繪製輪廓的外接形狀

3.如何獲取輪廓的重心座標並標註

本文的目標:

1.從原始圖像中找到2架可回收火箭

2.標註目標的位置與重心座標

閱讀完成後,將能從原始圖像中找到2架火箭,並標註其位置與座標。如下圖所示:

目錄

OpenCV輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與標註

1.查找、繪製輪廓函數

findContours()

drawContours()

2.預處理

3.查找輪廓

4.繪製輪廓

5.篩選輪廓

5.1 hierarchy輪廓級別詳解

contours與hierarchy的關係

什麼是層次結構hierarchy?

5.2 OpenCV中的層次結構表示

Next

Previous

First_Child

Parent

5.3 按hierarchy篩選輪廓

5.4 按長度篩選輪廓

6.聯通域分析

7.標註輪廓重心


1.查找、繪製輪廓函數


findContours()

void cv::findContours     (
        InputOutputArray         image,
        OutputArrayOfArrays   contours,
        OutputArray                 hierarchy,
        int                                 mode,
        int                                 method,
        Point                             offset = Point()
    )   

函數參數:

image

輸入:源圖像,一個8位單通道圖像,注意一定是CV_8UC1的單通道圖像,否則報錯。 非零像素被視爲1。 零像素保持爲0,因此圖像被視爲二進制。可以使用compare,inRange,threshold,adaptiveThreshold,Canny等來從灰度或彩色圖像中創建二進制圖像。如果mode爲RETR_CCOMP或RETR_FLOODFILL,則輸入也可以是標籤的32位整數圖像(CV_32SC1)。

contours

輸出:檢測到的輪廓。每個輪廓都存儲爲點向量(例如std :: vector <std :: vector <cv :: Point >>)。即由若干個cv::Point類型的點組成了單個輪廓std :: vector <cv :: Point >,再由若干個輪廓組成輸入圖像中的全部輪廓std::vector<std :: vector <cv :: Point >>

hierarchy

輸出:輪廓級別信息。Hierarchy爲可選輸出變量,是std::vector<cv::Vec4i>類型的向量(每個元素都是一個4個int值構成的向量)。包含有關圖像拓撲的信息。它具有與輪廓數量一樣多的元素。例如,第i個輪廓, hierarchy[i][0],hierarchy[i][1],hierarchy[i][2]和hierarchy[i][3]依次爲:第i個輪廓的[Next, Previous, First_Child, Parent],即輪廓i相同等級的下一輪廓、前一輪廓,第一個子輪廓和父輪廓(上一級輪廓)的索引號(即contours向量中的輪廓序號)。如果輪廓i沒有下一個,前一個,父級或嵌套輪廓,則層次結構[i]的相應元素將爲負數。這個參數我們將在下文中重點介紹。
mode 輸入:輪廓檢索模式, 詳見 RetrievalModes
method 輸入:輪廓近似法, 詳見ContourApproximationModes
offset 輸入:每個輪廓點移動的偏移量,可選參數,cv::Point()類型。如果從整幅圖像的某個ROI中提取輪廓,然後又在整個圖像中分析輪廓(將ROI中的輪廓座標恢復到整幅圖像中的座標),這個偏移量非常有用,可以免去我們自己寫代碼轉換座標系的麻煩。

mode參數:

RETR_EXTERNAL 

Python: cv.RETR_EXTERNAL

僅檢索極端外輪廓。 它爲所有輪廓設置hierarchy [i][2] = hierarchy [i][3] = - 1。

RETR_LIST 

Python: cv.RETR_LIST

檢索所有輪廓而不建立任何層次關係。

RETR_CCOMP 

Python: cv.RETR_CCOMP

檢索所有輪廓並將它們組織成兩級層次結構。在頂層輪廓是外部輪廓。在第二層輪廓是“洞”的輪廓。如果連接組件的洞內有另一個輪廓,它的級別仍然認定爲頂層。

RETR_TREE 

Python: cv.RETR_TREE

檢索所有輪廓並重建嵌套輪廓的完整層次結構。

RETR_FLOODFILL 

Python: cv.RETR_FLOODFILL

 

drawContours()

繪製輪廓輪廓或填充輪廓。

void cv::drawContours     (
        InputOutputArray       image,
        InputArrayOfArrays    contours,
        int                               contourIdx,
        const Scalar &            color,
        int                               thickness = 1,
        int                               lineType = LINE_8,
        InputArray                  hierarchy = noArray(),
        int                               maxLevel = INT_MAX,
        Point                           offset = Point()
    )       

函數參數:

image

輸入:源圖像。單通道或3通道圖像。

contours

輸入:待繪製的輪廓。std :: vector <std :: vector <cv :: Point >>類型。
contourIdx 輸入:待繪製的輪廓序號。例如:0爲繪製第1個輪廓contours[0];1爲繪製第2個輪廓contours[1],依次類推;-1爲繪製所有輪廓。
color 輸入:輪廓顏色。cv::Scalar變量,例如:cv::Scalar(0,0,255)爲紅色輪廓,cv::Scalar::all(0)爲黑色輪廓
thickness 輸入:輪廓粗細。int型變量,默認爲1,值越大越粗
lineType 輸入:繪製輪廓的線型。默認LINE_8,8聯通線型(下一個點連接上一個點的邊或角)

hierarchy

輸入:待繪製的輪廓級別。std::vector<cv::Vec4i>類型的向量(每個元素都是一個4個int值構成的向量)。下一輪廓、前一輪廓,第一個子輪廓和父輪廓(上一級輪廓)的索引號。
maxLevel 輸入:待繪製的輪廓最大級別。
method 輸入:輪廓近似法, 詳見ContourApproximationModes
offset 輸入:每個輪廓點移動的偏移量,可選參數。

2.預處理


預處理目的是爲輪廓查找提供高質量的輸入源圖像。

預處理的主要步驟包括:

  • 灰度化:使用cv::cvtColor()
  • 圖像去噪:使用高斯濾波cv::Gaussian()
  • 二值化:使用cv::Threshold()
  • 形態學處理:cv::morphologyEx()

其中灰度化可以將3通道圖像轉化爲單通道圖像,以便進行二值化門限分割;去噪可以有效剔除圖像中的異常獨立噪點;二值化是爲輪廓查找函數提供單通道圖像;形態學的某些處理通常可以剔除細小輪廓,聯通斷裂的輪廓。

讀取圖像代碼如下:

// 1.載入圖像
	cv::Mat image = cv::imread("spaceX2.jpg",1);
	cv::imshow("original", image);
	cv::waitKey();
// 2.預處理
	cv::Mat gray, binary, element; // 臨時變量
	cv::cvtColor(image, gray, CV_BGR2GRAY);
	cv::imshow("gray", gray);

	GaussianBlur(gray, gray, Size(3, 3), 1);
	cv::threshold(gray, binary, 80, 255, CV_THRESH_BINARY_INV);
	cv::imshow("binary", binary);
	
	element = getStructuringElement(MORPH_RECT, Size(3, 3));//3*3全1結構元素
	cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, element);
	cv::imshow("morphology", binary);
	cv::waitKey();

 


使用findContours函數查找輪廓

// 3.查找輪廓
	std::vector<std::vector<cv::Point>> contours;
	std::vector<cv::Vec4i> hierarchy;
	cv::findContours(
		binary,               // 輸入二值圖
		contours,             // 存儲輪廓的向量
		hierarchy,            // 輪廓層次信息
		RETR_TREE,            // 檢索所有輪廓並重建嵌套輪廓的完整層次結構
		CHAIN_APPROX_NONE);   // 每個輪廓的全部像素
	printf("find %d contours", contours.size());

4.繪製輪廓


爲了方便查看輪廓查找結果,使用drawContours()來繪製輪廓並顯示。編寫函數drawMyContours()函數用於在白色背景中或者原圖上查看輪廓。

函數參數:

  • 窗口名字;
  • 原始圖像以及輪廓變量
  • 白色背景上還是在原圖上繪製輪廓的標誌位
// 4. 繪製輪廓函數
void drawMyContours(string winName, Mat &image, std::vector<std::vector<cv::Point>> contours, bool draw_on_blank)
{
	cv::Mat temp;
	if (draw_on_blank) // 在白底上繪製輪廓
	{
		temp = cv::Mat(image.size(), CV_8U, cv::Scalar(255));
		cv::drawContours(
			temp,
			contours,
			-1,//畫全部輪廓
			0, //用黑色畫
			2);//寬度爲2
	}		
	else // 在原圖上繪製輪廓
	{
		temp = image.clone();
		cv::drawContours(
			temp,
			contours,
			-1,//畫全部輪廓
			cv::Scalar(0,0,255), //用red畫
			1);//寬度爲2
	}
	cv::imshow(winName, temp);
	cv::waitKey();
}

main函數中調用drawMyContours()如下:

// 4.繪製原始輪廓
	drawMyContours("contours", image, contours, true);

首次查找的輪廓變量contours中有16個向量,即找到16個輪廓。

展開contours變量,可以看到每個元素都是一個由一系列輪廓上的點組成的向量,其size就是每個輪廓的長度。

通過繪製輪廓,可以看到這16個輪廓,除了兩個目標之外,還有云、地面背景以其中的“洞”輪廓。

5.篩選輪廓


查找到大輪廓顯然有許多不符合要求,因此可以通過某些準則進行輪廓篩選。通過觀察,發現上圖中有許多輪廓包含“洞”,即子輪廓,而這些子輪廓顯然也有父級輪廓,因此我們可以使用findContours的hierarchy輪廓級別參數刪除那些有子輪廓也有父輪廓的輪廓

5.1 hierarchy輪廓級別詳解

contourshierarchy的關係

使用findContours()函數將返回contours輪廓向量以及對應的hierarchy輪廓級別向量(可選項)。兩者有相同的長度即contours.size() = hierarchy.size(),並且向量的序號表示找到的輪廓索引,且一一對應。

什麼是層次結構hierarchy

通常我們使用findContours()函數來檢測圖像中的對象。有時對象位於不同的位置。但在某些情況下,某些形狀在其他形狀內(類似嵌套)。在這種情況下,我們將外部輪廓稱爲父級輪廓,將內部輪廓稱爲子輪廓。這樣,圖像中的輪廓彼此之間存在某種關係。並且我們可以指定一個輪廓如何相互連接,例如,它是某個其他輪廓的子項,還是父項等。此關係的表示就稱爲層次結構hierarchy。

考慮下面的示例圖片:

在上圖中,有一些形狀,我們從0-5編號這5個形狀。圖中2和2a表示最外側矩形的外部和內部輪廓。

輪廓0,1,2是最外部輪廓,三者爲同一級別。我們可以說,它們在層次結構0中,或者只是它們處於相同的層次結構級別。

接下來是輪廓-2a可以被認爲是輪廓-2的子輪廓(或者相反,輪廓-2是輪廓-2a的父級輪廓)所以讓它在層次結構-1中。 類似地,輪廓-3是輪廓-2a的子輪廓,它進入下一層次。 最後,輪廓4,5是輪廓-3a的子輪廓,它們位於最後的層次結構級別。 從編號框的方式,可以說輪廓-4是輪廓-3a的第一個子輪廓,當然輪廓-5也是輪廓-3a的子輪廓。

如果上面的描述看着頭暈,不要緊,一開始都這樣。

5.2 OpenCV中的層次結構表示

OpenCV中每個輪廓都有自己的信息,關於它是什麼層次結構,誰是它的子輪廓,誰是它的父輪廓等.OpenCV將它表示爲四個int值的數組,類型爲cv::Vec4i(4個int值):

[Next,Previous,First_Child,Parent]

Next

Next表示同一級別的下一個輪廓索引。例如,在我們的圖片中取出輪廓-0。同一水平的下一個輪廓是輪廓-1。 所以簡單地說Next = 1。類似地,對於輪廓-1,next是輪廓-2。 所以Next = 2。

輪廓-2的同一級別沒有下一個輪廓,所以輪廓-2的Next = -1。輪廓-4呢?它與輪廓-5處於同一水平。所以它的下一個輪廓是輪廓-5,所以輪廓-4的Next = 5。

Previous

Previous表示同一級別的上一個輪廓索引。例如,輪廓-1的上一個輪廓在同一級別中爲輪廓-0。 類似地,對於輪廓-2,它的上一個輪廓是輪廓-1。而對於輪廓-0,沒有先前的,所以把它的Previous = -1。

First_Child

First_Child表示當前輪廓的第一個子輪廓索引。例如,對於輪廓-2,子輪廓是輪廓-2a。因此輪廓-2的First_Child爲輪廓-2a的相應索引值。輪廓-3a呢?它有兩個子輪廓。但hierarchy參數只記錄第一個子輪廓,因此它是輪廓-4的索引值。因此,對於輪廓-3a,First_Child = 4。

Parent

Parent表示當前輪廓的父輪廓索引。對於輪廓-4和輪廓-5,它們的父輪廓都是輪廓-3a。對於輪廓-3a,它的父輪廓是輪廓-3,依此類推。

注意:

  • Previous表示同一層級的前一個輪廓的索引;
  • Parent表示其父輪廓的索引;
  • 如果某個輪廓沒有子輪廓項或父輪廓,則對應的字段=-1.

5.3hierarchy篩選輪廓

有了上述對輪廓層級的理解,下面就可以根據需要篩選輪廓了。例如本文任務是找到兩個火箭,而首次查找輪廓有許多中間有空洞的輪廓不符合要求,下面就通過遍歷每一個輪廓的hierarchy級別參數的第3第4個參數來找到那些有子輪廓或者有父輪廓的輪廓,並刪除之。注意向量迭代器的使用,刪除後會返回下一個向量的指針;此外,contours與hierarchy元素需要同步刪除和並遞增迭代器,以保持編號對應關係,否則會刪錯。

// 5.篩選輪廓
	// 初始化迭代器
	std::vector<std::vector<cv::Point>>::iterator itc = contours.begin();
	std::vector<cv::Vec4i>::iterator itc_hierarchy = hierarchy.begin();
	
	// 5.1使用層級結構篩選輪廓
	int i = 0;
	while(itc_hierarchy != hierarchy.end())
	{
		//驗證輪廓大小
		if (hierarchy[i][2] > 0 || hierarchy[i][3] > 0) // 存在子輪廓/父輪廓
		{
			itc = contours.erase(itc);
			itc_hierarchy = hierarchy.erase(itc_hierarchy);
		}
		else
		{
			++i;
			++itc;
			++itc_hierarchy;
		}
	}
	printf("%d contours remaining after hierarchy filtering", contours.size());
	// 繪製級別篩選後的輪廓
	drawMyContours("contours after hierarchy filtering", image, contours, true);

篩選過後的輪廓如下圖所示,左上角和右下角的雲層與地面的帶有空洞的輪廓都被刪除了。

5.4 按長度篩選輪廓

儘管上一個步驟已經剔除了天空與地面背景輪廓,但仍然殘留這一些細小的輪廓。下一步可以使用輪廓長度contours[i].size()來濾除過小或過大的輪廓:

// 5.2使用輪廓長度濾波
	int min_size = 20;
	int max_size = 500;

	// 針對所有輪廓
	itc = contours.begin();
	itc_hierarchy = hierarchy.begin();
	while (itc != contours.end()) 
	{
		//驗證輪廓大小
		if (itc->size() < min_size || itc->size() > max_size)
		{
			itc = contours.erase(itc);
			itc_hierarchy = hierarchy.erase(itc_hierarchy);
		}			
		else
		{
			++itc;
			++itc_hierarchy;
		}			
	}
	printf("%d contours remaining after length filtering", contours.size());
	// 繪製長度篩選後的輪廓
	drawMyContours("contours after length filtering", image, contours, true);

感覺越來越接近真理了!

6.聯通域分析


連通區域通常代表了場景中的某個物體。爲了識別該物體,或將它與其他圖像元素比較,需要對此區域進行測量,以提取部分特徵。本節介紹opencv的形狀描述子,用於描述連通區域的形狀。OpenCV中用於形狀描述的函數有很多。我們把其中幾個用到上節提取到的區域。

(1)矩形框cv::Rect r0 = cv::boundingRect()

在表示和定位圖像中的區域方法中,邊界框可能是最簡潔的。它的定義是:能完整包含該形狀的最小垂直矩形。比較邊界框的高度和寬度,可以獲得物體在垂直和水平方向上的範圍。

(2)最小覆蓋圓cv::minEnclosingCircle()

最小覆蓋圓通常用在只需要區域尺寸和位置的近似值的情況。

(3)多邊形逼近cv::approxPolyDP()

如果要更緊湊地表示區域的形狀,可以採用多邊形逼近。在創建時需要設置精度參數,表示形狀與對應的簡化多邊形之間能接受的最大距離。它是cv::approxPolyDP(contours[1],poly,5,true)函數的第四個參數。返回結果是cv::Point類型的向量,表示多邊形頂點個數。在畫這個多邊形時,要迭代遍歷整個向量,並在頂點之間畫直線,把它們逐個連接起來。

(4)凸包cv::convexHull()

凸包是包含該形狀的最小凸多邊形。可以把它看作一條繞在區域周圍的橡皮筋。在形狀輪廓中凹進去的位置,凸包輪廓會與原始輪廓發生偏離。

下面使用上述分析方法中的凸包與最小覆蓋矩形兩種方法分別對2架火箭的提取輪廓進行分析。

// 6.形狀描述子
	// 最小覆蓋矩形
	cv::Mat result =image.clone();
	cv::Rect rect = cv::boundingRect(contours[0]);//輪廓1			
	cv::rectangle(result, rect, cv::Scalar(0,255,255), 1);//畫矩形
	// 凸包
	std::vector<cv::Point> hull;
	cv::convexHull(contours[1], hull);//輪廓2
	cv::polylines(result, hull, true, cv::Scalar(0, 255, 0), 1);//畫多邊形
	cv::imshow("bounding", result);
	cv::waitKey();

得到的結果如下圖所示:輪廓1爲最小覆蓋矩形(黃色線條),輪廓2爲凸包(綠色線條)

7.標註輪廓重心

終於到最後一步了。下面先求2個輪廓的重心,然後使用cv::Circle()與cv::putText()函數將重心位置與座標標註到畫面上。

// 7.計算輪廓矩,畫重心
	itc = contours.begin();
	while (itc != contours.end()) {
		// 計算全部輪廓矩
		cv::Moments mom = cv::moments(cv::Mat(*itc++));
		// 畫重心
		cv::Point pt = cv::Point(mom.m10 / mom.m00, mom.m01 / mom.m00);	//使用前三個矩m00, m01和m10計算重心	
		cv::circle(result, pt, 2, cv::Scalar(0, 0, 255), 2);//畫紅點
                // 標註重心座標值
		string text_x = std::to_string(pt.x);
		string text_y = std::to_string(pt.y);
		string text = "(" + text_x + ", " + text_y + ")";
		cv::putText(result, text, cv::Point(pt.x+10,pt.y+10), cv::FONT_HERSHEY_PLAIN, 1.5, cv::Scalar::all(255), 1, 8, 0);
	}
	cv::imshow("center", result);
	cv::waitKey();

最終結果如下圖所示。

參考鏈接:

https://docs.opencv.org/3.1.0/d9/d8b/tutorial_py_contours_hierarchy.html

https://docs.opencv.org/3.4.1/d3/dc0/group__imgproc__shape.html#ga17ed9f5d79ae97bd4c7cf18403e1689a

本文更新鏈接:https://blog.csdn.net/iracer/article/details/90260670

轉載請註明出處。

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