OpenCV—輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與重心標註(C++版)
輪廓是定義或限定形狀或對象的邊或線,是機器視覺中的常用的概念,多用於目標檢測、識別等任務。關於OpenCV輪廓操作,尤其是級別及如何使用輪廓級別進行篩選等問題,相關文章比較少,正好最近用到,因此將其總結成文。本文主要介紹OpenCV的查找輪廓函數findContours()繪製函數drawContours(),及其輪廓級別參數hierarchy,涉及到預處理、輪廓篩選等內容,並提供全部源代碼,希望能幫助大家理解基本概念並能借鑑示例代碼編寫自己的算法。
本文代碼:C++
本文包括如下內容:
- 基本概念
1.查找和繪製輪廓函數findContous(),drawContours()
2.輪廓參數:輪廓級別、輪廓長度
3.輪廓的形狀描述子:最小覆蓋矩形、圓、多邊形逼近、凸包
- 編程實戰
1.如何篩選輪廓:按輪廓級別和長度篩選
2.如何繪製輪廓的外接形狀
3.如何獲取輪廓的重心座標並標註
本文的目標:
1.從原始圖像中找到2架可回收火箭
2.標註目標的位置與重心座標
閱讀完成後,將能從原始圖像中找到2架火箭,並標註其位置與座標。如下圖所示:
目錄
OpenCV輪廓操作一站式詳解:查找/篩選/繪製/形狀描述與標註
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輪廓級別詳解
contours與hierarchy的關係
使用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.3 按hierarchy篩選輪廓
有了上述對輪廓層級的理解,下面就可以根據需要篩選輪廓了。例如本文任務是找到兩個火箭,而首次查找輪廓有許多中間有空洞的輪廓不符合要求,下面就通過遍歷每一個輪廓的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
轉載請註明出處。