在上一章中,我們使用了cv::findContours()
函數來尋找影像的輪廓點向量。輪廓的幾何運算可以從輪廓中提取像是中心點、角度、圓心...等有用的資訊,並進行進一步的分析。
以下是一些常見的輪廓幾何運算:
凸包是一個用來包圍輪廓中的所有點的凸多邊形。凸多邊形的一個特點是,其中任意三個連續頂點所形成的內角都小於180度,凸多邊形的所有內角都是銳角或者是直角,不存在鈍角。
邊界矩形是一個矩形,它包含了輪廓中的所有點。這個矩形的邊界就是輪廓中所有點的座標在水平(x)和垂直(y)方向上的最小和最大值所形成的。簡單來說,它就像是一個框框,將輪廓包裹住,確保沒有任何一個輪廓點在外面。
對於一個已經旋轉的矩形,當我們嘗試尋找邊界矩形並計算其面積時,我們會發現邊界矩形的面積不會與實際矩形的面積相符。這是因為邊界矩形會包含原始矩形外部的一些空白區域,導致其面積比原始矩形要大。
為了解決這個問題,我們可以使用最小面積矩形的方法,這個方法可以找到一個包含所有輪廓點的矩形,同時保證這個矩形是所有可能的矩形中面積最小的。最小面積矩形的特點是它會考慮到輪廓的旋轉角度,可以確保不會包含多餘的空白區域。
在最小面積矩形的計算結果中,除了我們可以獲得最小面積矩形的四個角落座標點外,還能夠得知這個矩形的旋轉角度。
不過,這個角度到底是相對於哪一個軸的角度呢?讓我們來看下圖,下圖展示了一個圖片坐標系。你可以觀察到,θ' 是矩形右下角邊和 X' 軸的夾角,而 θ 則是和矩形長邊平行,並穿過矩形中心點的直線與圖片座標系的 X 軸之間的夾角。θ' 和θ有90°的角度差,當矩形沒有旋轉時,θ 等於 90°,因為此時 θ' 為 0°。
最小包覆圓,通常稱為「最小外接圓」,是一個圓形,其半徑最小,但仍然能夠包含住輪廓中的所有點。換句話說,這個圓擁有最小的半徑大小容納所有輪廓點,而不會多餘的擴張。
這段程式碼是一個使用OpenCV的示例,用於處理一幅灰度影像中的輪廓和幾何特性。以下是程式碼的主要功能和操作:
onClick()
這個函數。cv::findContours
函數,尋找二值化影像中的輪廓,並存儲在contours
向量中。Output
視窗,並標示最小面積矩形的旋轉角度。Output
視窗時,觸發onClick()
這個函數,分析滑鼠點的位置,並確定滑鼠點的座標是否在輪廓內。滑鼠點可以位於輪廓的邊緣上、輪廓的內部或輪廓的外部。計算給定輪廓 contours[i]
的凸包,並將凸包的頂點儲存在 convex_hull_contours
中。
cv::convexHull(contours[i], convex_hull_contours);
計算給定輪廓 contours[i]
的邊界矩形,並將結果儲存在 bounding_rect
中。
cv::Rect
是 OpenCV 中用來表示矩形的類別(Class)。描述了矩形的位置和大小資訊。
cv::Rect
類別包含以下主要成員變數:
x
:矩形的左上角 x 座標。y
:矩形的左上角 y 座標。width
:矩形的寬度。height
:矩形的高度。cv::Rect bounding_rect=cv::boundingRect(contours[i]);
這行程式碼的作用是計算指定輪廓 (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]);
計算給定輪廓 contours[i]
的最小包覆圓的圓心座標和半徑。
center
:用於儲存計算結果的圓心座標。radius
:用於儲存計算結果的半徑。cv::minEnclosingCircle(contours[i],center,radius);
計算一個點 (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。#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;
}
圖片中的顏色分別代表:原始輪廓(綠色);凸包(紅色);邊界矩形(藍色);最小包覆圓(黃色)。
可以看到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外
--------------------