iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0

一、介紹

在上一章中,我們使用了cv::findContours()函數來尋找影像的輪廓點向量。輪廓的幾何運算可以從輪廓中提取像是中心點、角度、圓心...等有用的資訊,並進行進一步的分析。

以下是一些常見的輪廓幾何運算:

  1. 凸包(Convex Hull):凸包是一個凸多邊形,用來包圍輪廓中的所有點。
  2. 邊界矩形(Bounding Rectangle):邊界矩形是一個矩形,完全包圍了給定輪廓。
  3. 最小面積矩形(Minimum Area Rectangle):最小面積矩形是一個矩形,完全包圍了給定輪廓,並且具有最小的面積。可用於檢測物體的方向。
  4. 最小包覆圓(Minimum Enclosing Circle):最小包覆圓,通常稱為「最小外接圓」,是一個圓形,其半徑最小,但仍然能夠包含住輪廓中的所有點。

二、原理

1. 凸包(Convex hull)

凸包是一個用來包圍輪廓中的所有點的凸多邊形。凸多邊形的一個特點是,其中任意三個連續頂點所形成的內角都小於180度,凸多邊形的所有內角都是銳角或者是直角,不存在鈍角。
https://ithelp.ithome.com.tw/upload/images/20230927/20161732uJoU5SgAfN.png

2. 邊界矩形(Bounding Rectangle)

邊界矩形是一個矩形,它包含了輪廓中的所有點。這個矩形的邊界就是輪廓中所有點的座標在水平(x)和垂直(y)方向上的最小和最大值所形成的。簡單來說,它就像是一個框框,將輪廓包裹住,確保沒有任何一個輪廓點在外面。

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

3. 最小面積矩形(Minimum Area Rectangle)

對於一個已經旋轉的矩形,當我們嘗試尋找邊界矩形並計算其面積時,我們會發現邊界矩形的面積不會與實際矩形的面積相符。這是因為邊界矩形會包含原始矩形外部的一些空白區域,導致其面積比原始矩形要大。

為了解決這個問題,我們可以使用最小面積矩形的方法,這個方法可以找到一個包含所有輪廓點的矩形,同時保證這個矩形是所有可能的矩形中面積最小的。最小面積矩形的特點是它會考慮到輪廓的旋轉角度,可以確保不會包含多餘的空白區域。

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

在最小面積矩形的計算結果中,除了我們可以獲得最小面積矩形的四個角落座標點外,還能夠得知這個矩形的旋轉角度。

不過,這個角度到底是相對於哪一個軸的角度呢?讓我們來看下圖,下圖展示了一個圖片坐標系。你可以觀察到,θ' 是矩形右下角邊和 X' 軸的夾角而 θ 則是和矩形長邊平行,並穿過矩形中心點的直線與圖片座標系的 X 軸之間的夾角。θ' 和θ有90°的角度差,當矩形沒有旋轉時,θ 等於 90°,因為此時 θ' 為 0°。
https://ithelp.ithome.com.tw/upload/images/20230927/20161732mJdhGae8Z6.png

4. 最小包覆圓(Minimum Enclosing Circle)

最小包覆圓,通常稱為「最小外接圓」,是一個圓形,其半徑最小,但仍然能夠包含住輪廓中的所有點。換句話說,這個圓擁有最小的半徑大小容納所有輪廓點,而不會多餘的擴張。
https://ithelp.ithome.com.tw/upload/images/20230927/20161732c1n7yeaabX.png

二、程式碼

這段程式碼是一個使用OpenCV的示例,用於處理一幅灰度影像中的輪廓和幾何特性。以下是程式碼的主要功能和操作:

  1. 載入一幅灰度影像。
  2. 建立一個名為"Output"的視窗,用於顯示處理後的影像。設定滑鼠點擊事件的回調函數,當滑鼠在影像上點擊時,會觸發onClick()這個函數。
  3. 對輸入影像進行二值化處理。
  4. 使用cv::findContours函數,尋找二值化影像中的輪廓,並存儲在contours向量中。
  5. 對每個輪廓進行以下操作:
    • 計算輪廓的凸包邊界矩形最小面積矩形最小包覆圓
    • 繪製凸包邊界矩形最小包覆圓Output視窗,並標示最小面積矩形的旋轉角度。
  6. 程式顯示最終的影像,當點擊Output視窗時,觸發onClick()這個函數,分析滑鼠點的位置,並確定滑鼠點的座標是否在輪廓內。滑鼠點可以位於輪廓的邊緣上、輪廓的內部或輪廓的外部。

1. 逐行解釋

1) 計算凸包

計算給定輪廓 contours[i] 的凸包,並將凸包的頂點儲存在 convex_hull_contours 中。

cv::convexHull(contours[i], convex_hull_contours);

2) 計算邊界矩形

計算給定輪廓 contours[i] 的邊界矩形,並將結果儲存在 bounding_rect 中。

cv::Rect 是 OpenCV 中用來表示矩形的類別(Class)。描述了矩形的位置和大小資訊。

cv::Rect 類別包含以下主要成員變數:

  • x:矩形的左上角 x 座標。
  • y:矩形的左上角 y 座標。
  • width:矩形的寬度。
  • height:矩形的高度。
cv::Rect bounding_rect=cv::boundingRect(contours[i]);

3) 計算最小面積矩形

這行程式碼的作用是計算指定輪廓 (contours[i]) 的最小面積矩形,並將結果存儲在 min_area_rect 變數中。

cv::RotatedRect 是 OpenCV 中用來表示旋轉矩形的類型,描述了旋轉矩形的重要資訊。

cv::RotatedRect 類別包含以下主要成員變數:

  • center: 表示旋轉矩形的中心點座標。
  • size: 表示旋轉矩形的尺寸,其中 size.width 表示矩形的寬度,size.height 表示矩形的高度。
  • angle: 表示旋轉矩形相對於水平軸(X軸)的旋轉角度,以度為單位,於0~90度之間。
cv::RotatedRect min_area_rect = cv::minAreaRect(contours[i]);

4) 計算最小包覆圓

計算給定輪廓 contours[i] 的最小包覆圓的圓心座標和半徑。

  • center:用於儲存計算結果的圓心座標。
  • radius:用於儲存計算結果的半徑。
cv::minEnclosingCircle(contours[i],center,radius);

5) 幾何測試

計算一個點 (x, y) 與指定輪廓 contours[i] 之間的距離,並根據距離判斷這個點的位置關係。

double dist = cv::pointPolygonTest(contours[i],cv::Point2f(x,y),false);
  • contours[i]:輪廓 i,也就是要進行距離計算的輪廓。
  • cv::Point2f(x, y):表示要計算距離的目標點,這裡使用 cv::Point2f 來表示二維座標 (x, y)
  • measureDist:如果設為 false,則不計算最短距離,只會回傳**+1**、0-1,分別代表點在輪廓內輪廓上輪廓外。如果設為 true,則計算最短距離並回傳。如果點在輪廓上,距離為0。

2. 完整程式碼

#include <iostream>
#include <opencv2/opencv.hpp>
#include "opencv2/core/utils/logger.hpp"

using namespace std;

vector<vector<cv::Point>> contours;
void onClick(int event, int x, int y, int flags, void* param)
{
    if (event & cv::EVENT_LBUTTONDOWN)
    {
		for (int i = 0;i < contours.size();i++) {
			double dist = cv::pointPolygonTest(contours[i],cv::Point2f(x,y),false);
			if (dist == 0.0) {
				printf("f(%d,%d)在輪廓%d邊緣上\n",x,y,i);
			}
			else if(dist == -1.0){
				printf("f(%d,%d)在輪廓%d外\n",x,y,i);
			}
			else if (dist == 1.0) {
				printf("f(%d,%d)在輪廓%d內\n",x,y,i);
			}
		}

		printf("--------------------\n");
    }
}
int main()
{
	
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
	cv::Mat image = cv::imread("C:\\Users\\vince\\Downloads\\test_image4.jpg",cv::IMREAD_GRAYSCALE);

	cv::namedWindow("Output",cv::WindowFlags::WINDOW_NORMAL);
	cv::resizeWindow("Output", cv::Size(512.0 * ((float)image.cols / image.rows), 512));
	cv::setMouseCallback("Output", onClick);

	cv::Mat dst;
	cv::threshold(image, dst,0,255,cv::THRESH_OTSU);
	cv::findContours(dst, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
	cv::Mat output=cv::Mat::zeros(cv::Size(image.cols,image.rows),CV_8UC3);
	cv::drawContours(output, contours, -1, cv::Scalar(0, 255, 0));
	for (int i = 0;i < contours.size();i++) {
		vector<cv::Point> convex_hull_contours;
		cv::convexHull(contours[i], convex_hull_contours);

		cv::Rect bounding_rect=cv::boundingRect(contours[i]);
		cv::RotatedRect min_area_rect = cv::minAreaRect(contours[i]);

		cv::Point2f center;
		float radius;
		cv::minEnclosingCircle(contours[i],center,radius);
		cv::circle(output, center,radius,cv::Scalar(0, 255, 255));

		cv::polylines(output, convex_hull_contours, true, cv::Scalar(0, 0, 255));
		cv::rectangle(output, bounding_rect, cv::Scalar(255, 0, 0));
		cv::putText(output, to_string(i), cv::Point(bounding_rect.x, bounding_rect.y - 5),cv::FONT_HERSHEY_COMPLEX,0.5,cv::Scalar(0,255,0));
		
		printf("%d -> angle:%.2f\n", i, min_area_rect.angle);
	}
	cv::imshow("Output",output);
	cv::waitKey(0);
	return 0;
}

3. 測試圖

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

4. 測試結果

圖片中的顏色分別代表:原始輪廓(綠色);凸包(紅色);邊界矩形(藍色);最小包覆圓(黃色)。

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

可以看到Console端輸出了每個輪廓最小面積矩形的角度。你可以透過點擊視窗,了解你目前點擊的座標是否位於輪廓內。

0 -> angle:0.45
1 -> angle:24.44
2 -> angle:90.00
3 -> angle:81.25
4 -> angle:90.00
f(414,252)在輪廓0外
f(414,252)在輪廓1外
f(414,252)在輪廓2內
f(414,252)在輪廓3外
f(414,252)在輪廓4外
--------------------

上一篇
【Day22】OpenCV 邊緣檢測後處理:尋找輪廓
下一篇
【Day24】使用OpenCV求出輪廓矩(Moments)
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言