我們已經學會如何擷取影像的輪廓並進行幾何運算。接下來,我們要討論輪廓的實際應用,其中最常見的應用之一就是輪廓比對。透過輪廓比對,我們可以判定影像上的物體是否為我們尋找的目標,輪廓比對需要一種量化的方式來進行,而這就是我們今天要談的主題 - "矩"。
矩是一種數學工具,用於描述輪廓的高階幾何特徵,例如質心、面積、方向等。在輪廓比對中,我們使用矩來量化的表示物體的形狀和結構,讓得我們可以進行辨識和比對。
空間矩可以用來計算兩種輸入,分別為輪廓點或是單通道影像。矩由以下的數學公式表達:
零階矩m00數學的定義是將非零的像素點加總起來,這個矩的數學定義會根據你的輸入改變,但仍可以使用相同的公式:
一階矩包含了m10和m01,和零階矩同理,將p、q其中一方帶入1就是一階矩。一階矩描述的是座標的加總值,m10描述的是圖片中相素值不為0或輪廓點的X座標值加總,m01則是描述Y座標值加總。
如果分別對m10、m01除以m00,會得到物體的x、y座標平均值,即為物體的中心點。
假設今天有兩個一樣的物體,其中一個被平移到不同的位置時中心點的座標會改變,造成空間矩改變,影響兩物體比對的結果。我們希望求出來的矩不會受平移關係影響。中心矩為了解決這個問題,先求出物體的中心座標,並將物體的中心平移到原點O(0,0),中心矩的數學公式如下:
一階中心矩會將物體的質心移到坐標原點O(0,0),因此後續中心矩的計算不再受到平移的影響。這是中心矩的一個重要特性,使物體的形狀特徵不受位置的影響,這對於物體、輪廓辨識非常有用。
零階中心矩的輸出會等於m00,因為因素p、q為0,消掉了後面的算式。
只要是一階中心矩如μ10、μ01,由於質心坐標 x̄ 和 ȳ 是通過計算物體輪廓上所有點的坐標並求平均值得到的。在前面的敘述我們知道了一階矩描述的是座標的加總值。然而在計算中心矩時,實際上已經將物體的中心移到了坐標原點O(0,0),所以輪廓點座標的總和為0。
舉個例子,假設我們有一顆公平的五面骰,骰到的點數的機率平均,我們使用P(X=1)這個機率質量函數來表示骰到1的機率為多少。如果我們對這個機率質量函數做一階中心矩,會發生什麼事情呢?沒有錯,一階的中心矩結果為0。
我們求出了中心矩來避免物體發生平移時矩的改變。然而,假設今天有兩個一樣的物體,其中一個被放大或縮小時中心矩會改變。我們希望求出來的矩不會受縮放關係影響,因此需要對中心矩進行正規化。可以看到下列數學公式,正規中心矩是基於中心矩除以m00的因素p、q次方,來達到正規化:
最後,我們解決了上述各種操作對矩的影響,包括平移、縮放。然而我們還沒有考慮旋轉、鏡射對矩的影響,為了解決旋轉、鏡射對矩的影響,Hu不變矩因此誕生。Hu不變矩是正規中心矩的線性組合,以下公式為Hu矩的數學定義:
下圖是一張測試圖,圖片中字母經過了旋轉、平移、縮放。對照下表Hu矩輸出結果,你會發現其實同一個字母的Hu矩差異不大。
輪廓 | hu0 | hu1 | hu2 | hu3 | hu4 | hu5 | hu6 |
---|---|---|---|---|---|---|---|
2 | 0.84 | 0.26 | 0.00 | 0.00 | 0.00 | -0.00 | 0.00 |
8 | 0.82 | 0.25 | 0.01 | 0.00 | 0.00 | 0.00 | 0.00 |
11 | 0.74 | 0.20 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
12 | 0.76 | 0.21 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
14 | 0.74 | 0.20 | 0.00 | 0.00 | -0.00 | 0.00 | 0.00 |
15 | 0.73 | 0.20 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
輪廓 | hu0 | hu1 | hu2 | hu3 | hu4 | hu5 | hu6 |
---|---|---|---|---|---|---|---|
0 | 0.96 | 0.61 | 0.27 | 0.04 | -0.00 | -0.00 | -0.00 |
3 | 0.93 | 0.57 | 0.36 | 0.08 | 0.01 | 0.03 | -0.01 |
4 | 0.85 | 0.47 | 0.23 | 0.04 | 0.00 | 0.01 | -0.00 |
5 | 0.84 | 0.46 | 0.19 | 0.03 | 0.00 | 0.00 | -0.00 |
6 | 0.83 | 0.45 | 0.23 | 0.04 | 0.00 | 0.01 | -0.00 |
7 | 1.07 | 0.80 | 0.51 | 0.15 | 0.03 | 0.09 | -0.02 |
輪廓 | hu0 | hu1 | hu2 | hu3 | hu4 | hu5 | hu6 |
---|---|---|---|---|---|---|---|
1 | 0.95 | 0.88 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
9 | 0.76 | 0.55 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
10 | 0.76 | 0.55 | 0.00 | 0.00 | 0.00 | 0.00 | -0.00 |
13 | 0.95 | 0.88 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
計算給定輪廓contours[i]
的矩(moments),包含了不同階的矩的值,例如零階矩、一階矩、二階矩...等等。
cv::Moments moments=cv::moments(contours[i]);
cv::moments(contours[i])
:計算輪廓contours[i]
的矩(moments)。函式的輸出式cv::Moments
用於儲存輸出結果,空間矩、中心矩、正規中心矩的結果都會被存放到這個類(Class)裡面。
這段程式碼使用OpenCV函式庫來計算影像的Hu不變矩。可以看到,我們用一個double
陣列hu
去接Hu矩的結果。
double hu[7];
cv::HuMoments(moments,hu);
moments
:這是一個包含影像矩的數據結構,由前面cv::moments
函式計算得知。hu
:這是一個數組,用於存儲計算出的Hu不變矩的值。cv::imread
函式讀取了一張灰階影像,並將其存儲在image
變數中。cv::threshold
函式對image
進行了二值化處理,使用了Otsu閾值化方法,結果存儲在binary
變數中。contours_image
和moments_image
,這兩個影像將用於繪製輪廓和輪廓的重心。cv::findContours
函式找到了影像中的外部輪廓,這些輪廓存儲在contours
向量中。for
迴圈遍歷所有找到的輪廓。
#include <iostream>
#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 image = cv::imread("C:\\Users\\vince\\Downloads\\test_image5.jpg",cv::IMREAD_GRAYSCALE);
cv::Mat binary;
cv::threshold(image, binary, 0, 255, cv::THRESH_OTSU);
cv::Mat contours_image=cv::Mat::zeros(cv::Size(binary.cols,binary.rows),CV_8UC3);
cv::Mat moments_image=cv::Mat::zeros(cv::Size(binary.cols,binary.rows),CV_8UC3);
vector<vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_KCOS);
for (int i = 0;i < contours.size();i++) {
cv::Rect bounding_rect=cv::boundingRect(contours[i]);
cv::putText(contours_image, to_string(i), cv::Point(bounding_rect.x, bounding_rect.y - 5),cv::FONT_HERSHEY_COMPLEX,0.5,cv::Scalar(0,255,0));
cv::drawContours(contours_image, contours, i, cv::Scalar(0,255,0));
cv::Moments moments=cv::moments(contours[i]);
cv::Point center=cv::Point(moments.m10/moments.m00,moments.m01/moments.m00);
cv::circle(contours_image, center, 3, cv::Scalar(0, 255, 255), -1);
double hu[7];
cv::HuMoments(moments,hu);
printf("|%d|%.2f|%.2f|%.2f|%.2f|%.2f|%.2f|%.2f|\n",i,hu[0],hu[1],hu[2],hu[3],hu[4],hu[5],hu[6]);
}
cv::imshow("Contours",contours_image);
cv::waitKey(0);
return 0;
}
輸出的影像會畫出物體的邊緣並加上數字標號,並用黃色點標出字母的中心。在Console端會看到每個輪廓的Hu矩。
|0|0.96|0.61|0.27|0.04|-0.00|-0.00|-0.00|
|1|0.95|0.88|0.00|0.00|0.00|0.00|0.00|
|2|0.84|0.26|0.00|0.00|0.00|-0.00|0.00|
|3|0.93|0.57|0.36|0.08|0.01|0.03|-0.01|
|4|0.85|0.47|0.23|0.04|0.00|0.01|-0.00|
|5|0.84|0.46|0.19|0.03|0.00|0.00|-0.00|
|6|0.83|0.45|0.23|0.04|0.00|0.01|-0.00|
|7|1.07|0.80|0.51|0.15|0.03|0.09|-0.02|
|8|0.82|0.25|0.01|0.00|0.00|0.00|0.00|
|9|0.76|0.55|0.00|0.00|0.00|0.00|0.00|
|10|0.76|0.55|0.00|0.00|0.00|0.00|-0.00|
|11|0.74|0.20|0.00|0.00|0.00|0.00|0.00|
|12|0.76|0.21|0.00|0.00|0.00|0.00|0.00|
|13|0.95|0.88|0.00|0.00|0.00|0.00|0.00|
|14|0.74|0.20|0.00|0.00|-0.00|0.00|0.00|
|15|0.73|0.20|0.00|0.00|0.00|0.00|0.00|