iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0

一、介紹

上一章節,我們介紹了如何使用各種運算子進行邊緣檢測,檢測出來的結果是一張帶有邊緣的影像。然而,我們仍不知道要如何從邊緣影像中取得輪廓中每一個點的座標位置,像是取得感興趣區域(ROI)、凸包、最小外接圓等運算,都需要先知道輪廓中的點。在這一章的內容會教你如何使用OpenCV在邊緣影像中提取輪廓,以及如何取得每個輪廓點的座標位置。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732J3IerQWWUH.jpg

二、原理

1. 輪廓(Contour)

輪廓是由多個點組成的連續曲線或多邊形,這些點會包圍物體或形狀的邊界。輪廓描述了物體在影像中的外形和輪廓的形狀。

看到下圖為一張黑白的影像,我們先將影像中每個輪廓都進行標號,注意如果形狀不是實心的,在形狀的內側也會有輪廓,像是輪廓0還有一個內側的輪廓1,反之輪廓2和輪廓3因為是實心的所以只有一個。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732krh56ADWRz.jpg

2. 輪廓階層(Hierarchy)

輪廓階層(Contour Hierarchy)用於描述和組織影像中多個輪廓之間的關係。當一張影像中有多個物體或形狀的輪廓存在時,輪廓階層的作用就像是一個家譜,讓我們可以清晰的了解這些輪廓之間的層次關係。

輪廓階層可以被視為一個樹狀結構,其中包含以下三個主要元素:

  1. 父輪廓(Parent Contour):父輪廓是某個輪廓的直接上層,就像家譜中的父母一樣。例如在上個圖例中,輪廓1是輪廓4的父輪廓,而輪廓4則是輪廓5的父輪廓。
  2. 子輪廓(Child Contour):子輪廓是某個輪廓的直接下層,就像家譜中的孩子一樣。例如在上個圖例中,輪廓4是輪廓1的子輪廓,同理,輪廓5是輪廓4的子輪廓。
  3. 同層輪廓(Sibling Contour):同層輪廓是指位於相同父輪廓下的輪廓,就像家譜中的兄弟姐妹一樣。例如在上個圖例中,輪廓2和輪廓4可以是同層輪廓,因為它們都有相同的父輪廓1。同樣地,輪廓0和輪廓6也可以被視為同層輪廓,即使他們沒有任何的父輪廓,可以把它想像成,因為它們都沒有明確的父輪廓,可以把它歸在同一類。

下圖就是這張圖片完整的輪廓樹狀圖,可以把它想像成輪廓的祖譜,這張圖完美的詮釋了輪廓的層次結構,誰是父親、兒子和兄弟姊妹一目了然。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732PTvnF3YqdQ.jpg

3. 輪廓階層表示方式

在OpenCV上,一張圖片具有多個輪廓,這些輪廓的階層會被OpenCV包裝成一個vector<cv::Vec4i>,可以想像成一個cv::Vec4i的陣列。當然,這樣講不精確,vector<>其實是C語言向量的表示方式,只不過你也可以把它當成一個可以任意增加、刪除元素的陣列,在開發OpenCV時會經常使用到,概念類似Java的List<T>,或是python的List[T]

cv::Vec4i又是什麼東西?cv::Vec4i是一個有號整數四維的向量,他不像前面講的vector<>具有增加、刪除元素的特性。反之,這個向量的元素數量固定是4,而且這個型別是OpenCV特有的向量表示法,可以參與一些矩陣運算。如果你想要了解更多有關Vec4i的用法,可以參考 Basic Structures Vec

https://ithelp.ithome.com.tw/upload/images/20230927/20161732FnSj9QG0Ov.jpg

當你透過OpenCV尋找輪廓以後,OpenCV會輸出輪廓階層輪廓點的向量,輪廓階層會被包裝成Vec4i,這個向量裡的四個元素分別代表不同的資訊。

  • Vec4i[0] Next: 位於同層的下一個輪廓,以上圖的輪廓樹狀圖為例,假設目前的輪廓是0,那和他同層的輪廓6就會是下一個。
  • Vec4i[1] Previous: 位於同層的上一個輪廓,同樣以上圖為例,假設目前的輪廓是6,那和他同層的輪廓1就會是上一個。
  • Vec4i[2] First Child: 目前輪廓的第一個子輪廓,同樣以上圖為例,假設目前的輪廓是1,那他的第一個子輪廓就是。
  • Vec4i[4] Parent: 目前輪廓的父輪廓,同樣以上圖為例,假設目前的輪廓是4,那他的父輪廓就是1。

使用OpenCV樹狀的檢索模式,完整的輪廓階層樹狀圖輸出:

0 ->[6, -1, 1, -1]
1 ->[-1, -1, 2, 0]
2 ->[4, -1, 3, 1]
3 ->[-1, -1, -1, 2]
4 ->[-1, 2, 5, 1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 0, -1, -1]

4. 輪廓檢索模式(Contour Retrieval Mode)

輪廓檢索模式主要影響輪廓檢測後,如何組織和儲存檢測到的輪廓。

1) RETR_EXTERNAL

最常見的輪廓檢索模式,這個模式會檢索最外層的輪廓,忽略所有內部輪廓。換句話說,它只檢索影像中最外層的物體邊界,其他子類通通忽略掉,經常被應用在抓出感興趣區域(ROI)的範圍。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732RU0sQMspnW.jpg

使用OpenCV的向量表示為:

0 ->[6, -1, -1, -1]
6 ->[-1, 0, -1, -1]

2) RETR_LIST

這種模式檢索所有的輪廓,模式會檢索所有輪廓並將它們存儲在列表中,但這種檢索方法無法表示輪廓的層次結構,因為所有檢測到的輪廓均為同層輪廓

https://ithelp.ithome.com.tw/upload/images/20230927/201617328XJVeATBGQ.jpg

使用OpenCV的向量表示為:

0 ->[1, -1, -1, -1]
1 ->[2, 0, -1, -1]
2 ->[3, 1, -1, -1]
3 ->[4, 2, -1, -1]
4 ->[5, 3, -1, -1]
5 ->[6, 4, -1, -1]
6 ->[-1, 5, -1, -1]

3) RETR_CCOMP

這種模式檢索所有輪廓,但將輪廓會被分為兩個層次,最外層的輪廓位於第一層,而內部的輪廓位於第二層。最上層的輪廓階層最多只能包含一個子類,且子類不會再有任何子類,所以要使用這個方法完整描述輪廓的層次還是有困難,但足以應付一些使用情境。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732OuaJOcgW40.jpg

使用OpenCV的向量表示為:

0 ->[2, -1, 1, -1]
1 ->[-1, -1, -1, 0]
2 ->[4, 0, 3, -1]
3 ->[-1, -1, -1, 2]
4 ->[6, 2, 5, -1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 4, -1, -1]

4) RETR_TREE

這種模式檢索所有的輪廓,並將它們組織成一個層次樹狀結構。每個輪廓都有一個父輪廓和零個或多個子輪廓,完美描述了輪廓的階層關係。

https://ithelp.ithome.com.tw/upload/images/20230927/20161732ez22IGtK5o.jpg

使用OpenCV的向量表示為:

0 ->[6, -1, 1, -1]
1 ->[-1, -1, 2, 0]
2 ->[4, -1, 3, 1]
3 ->[-1, -1, -1, 2]
4 ->[-1, 2, 5, 1]
5 ->[-1, -1, -1, 4]
6 ->[-1, 0, -1, -1]

三、程式碼

1. 逐行解釋

下面的程式碼展示了如何使用RETR_TREE檢索模式 尋找輪廓,執行的流程如下:

  1. 讀取一張灰度影像。
  2. 使用大津演算法進行二值化,強化邊緣。
  3. 尋找影像中的輪廓,使用cv::RETR_TREE作為輪廓檢索模式,以及cv::CHAIN_APPROX_TC89_KCOS作為輪廓近似方法。
  4. 計算每個輪廓的邊界矩形(Bounding Rect),繪製矩形框以包圍每個輪廓,並在矩形框上方用文字標記輪廓的索引。

1) 尋找輪廓

使用OpenCV的cv::findContours函數,該函數用於在二值化影像中尋找輪廓。採坑注意,傳入的圖片不需先做邊緣檢測(Canny、Sobel...等),否則檢測出來的輪廓數量會變成兩倍

cv::findContours(binary_img,contours,hierarchy,cv::RETR_TREE,cv::CHAIN_APPROX_TC89_KCOS);
  1. binary_img:輸入的二值化影像。
  2. contours:一個儲存檢測到的輪廓的向量,以vector<vector<cv::Point>>的形式存儲在這個向量中。
  3. hierarchy:一個存儲輪廓的階層結構的向量,以vector<cv::Vec4i>的形式存儲在這個向量中。每個cv::Vec4i表示一個輪廓的層次關係,包括父輪廓索引、子輪廓索引、同層輪廓索引。
  4. cv::RETR_TREE:輪廓檢索模式。在這裡被設置為cv::RETR_TREE,檢索所有輪廓。
  5. cv::CHAIN_APPROX_TC89_KCOS:這是輪廓近似方法的一個選項。設置為cv::CHAIN_APPROX_TC89_KCOS

2) 尋找邊界矩形

這行程式碼使用了OpenCV的cv::boundingRect函數,用於計算輪廓的外接矩形。以下是這行程式碼的解釋:

cv::Rect rect=cv::boundingRect(contours.at(i));
  1. contours.at(i):從contours向量中獲取第i個輪廓。

3) 繪製輪廓

使用OpenCV的cv::drawContours函數來在 output_img 影像上繪製輪廓。

cv::drawContours(output_img, contours,i, cv::Scalar(0,0,255), 1,cv::LINE_8);
  1. output_img:這是目標影像,也就是要在其上繪製輪廓的影像。
  2. contours: 包含多個輪廓,每個輪廓都是一個 vector<cv::Point>
  3. i:這是要繪製的輪廓的索引。在這行程式碼中,它被設置為 i,這表示我們將繪製 contours 中的第 i 個輪廓。
  4. cv::Scalar(0,0,255):這個參數指定了繪製輪廓的顏色,這裡使用(0,0,255)純紅色。
  5. cv::LINE_8:繪製輪廓線的類型。

4) 顯示輪廓階層

這行程式碼是用來處理輪廓階層(Hierarchy)中的一部分信息,主要用於分析和顯示輪廓之間的父子關係。以下是這段程式碼的解釋:

  1. 為了方便後續的輸出和顯示,使用cv::transpose函數將vec中的元素轉置到v中,這樣一來Vec4i的4x1的向量會被轉置成1x4的矩陣
  2. 顯示輪廓階層(Hierarchy)中的資訊,並顯示輪廓點的數量。
cv::Vec4i vec=hierarchy[i];
cv::Mat v;
cv::transpose(vec, v);
printf("%d ->",i);
print(v);
printf(" points:%d\n",(int)contours[i].size());

2. 完整程式碼

#include <iostream> 
#include <vector> 
#include <opencv2/opencv.hpp> 
#include "opencv2/core/utils/logger.hpp"
using namespace std;

int main()
{
	cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
	cv::Mat grayImage = cv::imread("C:\\Users\\vince\\Downloads\\test_image3.jpg",cv::IMREAD_GRAYSCALE);
	cv::namedWindow("Output", cv::WINDOW_NORMAL);
	cv::resizeWindow("Output", cv::Size(512*((float)grayImage.cols)/grayImage.rows,512));

	cv::Mat binary_img;
	cv::threshold(grayImage, binary_img, 0, 255, cv::THRESH_OTSU);
	
	vector<vector<cv::Point>> contours;
	vector<cv::Vec4i> hierarchy;
	cv::findContours(binary_img,contours,hierarchy,cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE);

	printf("counters found:%d\n",(int)contours.size());
	printf("\n");

	cv::Mat output_img=cv::Mat::zeros(cv::Size(binary_img.cols,binary_img.rows),CV_8UC3);

	for (int i = 0;i < hierarchy.size();i++) {
		cv::Vec4i vec=hierarchy[i];
		cv::Mat v;
		cv::transpose(vec, v);
		printf("%d ->",i);
		print(v);
		printf(" points:%d\n",(int)contours[i].size());

		cv::Rect rect=cv::boundingRect(contours.at(i));
		cv::rectangle(output_img, rect, cv::Scalar(0,255,0),1);
		cv::putText(output_img, std::to_string(i),cv::Point(rect.x, rect.y-10),cv::FONT_HERSHEY_COMPLEX,0.4,cv::Scalar(0, 255,0),1);
		cv::drawContours(output_img, contours,i, cv::Scalar(0,0,255), 1,cv::LINE_8);
	}

	cv::imshow("Output",output_img);
	cv::imwrite("output.jpg", output_img);
	cv::waitKey(0);
	return 0;
}

3. 測試圖

https://ithelp.ithome.com.tw/upload/images/20230927/20161732oFpLGvWkAT.jpg

4. 輸出結果

1) RETR_EXTERNAL

https://ithelp.ithome.com.tw/upload/images/20230927/20161732wxRydnBp0B.png

counters found:2

0 ->[1, -1, -1, -1] points:22
1 ->[-1, 0, -1, -1] points:4

2) RETR_LIST

https://ithelp.ithome.com.tw/upload/images/20230927/20161732HcAnihnnGk.png

counters found:7
0 ->[1, -1, -1, -1] points:8
1 ->[2, 0, -1, -1] points:4
2 ->[3, 1, -1, -1] points:8
3 ->[4, 2, -1, -1] points:22
4 ->[5, 3, -1, -1] points:8
5 ->[6, 4, -1, -1] points:22
6 ->[-1, 5, -1, -1] points:4

3) RETR_CCOMP

https://ithelp.ithome.com.tw/upload/images/20230927/20161732BklQK0GUfE.png

counters found:7

0 ->[2, -1, 1, -1] points:4
1 ->[-1, -1, -1, 0] points:8
2 ->[4, 0, 3, -1] points:22
3 ->[-1, -1, -1, 2] points:8
4 ->[6, 2, 5, -1] points:22
5 ->[-1, -1, -1, 4] points:8
6 ->[-1, 4, -1, -1] points:4

4) RETR_TREE

https://ithelp.ithome.com.tw/upload/images/20230927/20161732xW4IU86Hdg.png

counters found:7

0 ->[6, -1, 1, -1] points:22
1 ->[-1, -1, 2, 0] points:8
2 ->[4, -1, 3, 1] points:4
3 ->[-1, -1, -1, 2] points:8
4 ->[-1, 2, 5, 1] points:22
5 ->[-1, -1, -1, 4] points:8
6 ->[-1, 0, -1, -1] points:4

上一篇
【Day21】使用OpenCV進行邊緣檢測
下一篇
【Day23】使用OpenCV進行輪廓的幾何運算
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言