Image Pyramid 類別的 Source 檔撰寫
以下程式碼是 Image Pyramid 的完整 Source 檔,該檔案也收錄在我的 Github 上,有興趣的讀者歡迎點進來看。接下來,我會對這份 Source 檔多做一些說明,首先我會介紹實踐建構子的每一個步驟,再介紹我們該如何設定影像到影像金字塔中 :
#include "myORB-SLAM2/ImagePyramid.h"
namespace my_ORB_SLAM2 {
/*
@brief 影像金字塔
@param[in] nLevels 影像金字塔的層數
@param[in] fScaleFactor 每層之間的縮放因子 (例如 1.2)
@param[in] nFeatures 總共需要提取的特徵點數量 */
ImagePyramid::ImagePyramid(int nLevels, float fScaleFactor, int nFeatures) {
// 初始化影像金字塔基礎參數
mnLevels = nLevels;
mnFeatures = nFeatures;
mfScaleFactor = fScaleFactor;
// 初始化資料結構大小以符合影像金字塔層數
mvnFeaturesPerLevel.resize(mnLevels);
mvfScaleFactors.resize(mnLevels);
mvfInvScaleFactors.resize(mnLevels);
mvImages.resize(mnLevels);
// 初始化影像金字塔中,每一層的縮小倍數與反向縮放倍數
mvfScaleFactors[0] = 1.0f;
mvfInvScaleFactors[0] = 1.0f;
for(int level = 1; level < mnLevels; level++) {
mvfScaleFactors[level] = mvfScaleFactors[level-1] * mfScaleFactor;
mvfInvScaleFactors[level] = 1.0f / mvfScaleFactors[level];
}
// 計算影像金字塔中每一層應提取的特徵點數量 (根據公式)
int sumFeatures = 0;
float factor = 1.0f / mfScaleFactor;
float nDesiredFeaturesPerScale = (mnFeatures*(1-factor)) / (1-(float)pow((double)factor, (double)mnLevels));
for(int level = 0; level < mnLevels-1; level++) {
mvnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);
sumFeatures += mvnFeaturesPerLevel[level];
nDesiredFeaturesPerScale *= factor;
}
mvnFeaturesPerLevel[mnLevels-1] = max(mnFeatures-sumFeatures, 0);
info(); // 輸出影像金字塔相關資訊
}
/*
@brief 設定影像 : 將不同縮放倍率的影像依序放入影像金字塔中
@param[in] image 影像金字塔的影像*/
void ImagePyramid::setImage(const Mat &image) {
mvImages[0] = image;
for (int level = 1; level < mnLevels; ++level) {
float scale = mvfInvScaleFactors[level];
Size size(cvRound(image.cols*scale), cvRound(image.rows*scale));
resize(mvImages[level-1], mvImages[level], size, 0, 0, INTER_NEAREST);
}
}
}
建構子的實踐
建構子的程式碼位於第 10 ~ 42 行之間。基本上,建構子的內容可以分為 5 個小步驟 :
- 初始化影像金字塔基礎參數 :
這一步驟很容易理解,首先我們要設定影像金字塔最重要的 3 個基礎參數,即「影像金字塔層數、欲提取的特徵點數、縮放因子」,如此一來,我們後續才可以好好地從影像金字塔中提取品質好的特徵點。 - 初始化資料結構大小以符合影像金字塔層數 :
這一步對 C++ 新手來說可能有一點不直觀。在 C++ 的世界中,如果我們能夠精確地控制記憶體空間的分配,將會大大地提高程式運作的效率與速度。以 Vector 為例,如果我們已經事先知道我們要儲存多少筆資料,那就可以直接使用resize這個 Function,幫助我們直接調整 Vector 的大小,以符合我們要儲存的資料筆數。
以影像金字塔為例,如果我們已經知道金字塔的層數為 3,那其成員變數mvnFeaturesPerLevel、mvfScaleFactors、mvfInvScaleFactors、mvImages的層數都可以直接設定為 3。如此一來,也可以幫助我們把下一個步驟的程式碼邏輯簡化 ! - 初始化影像金字塔中,每一層的縮小倍數與放大倍數 :
如同剛才所說的,我們已經得知mvfScaleFactors、mvfInvScaleFactors的大小,接下來只要簡單地根據金字塔層數,遍歷這兩個 Vector 即可。首先,我們將這兩個 Vector 的第一層 ( Index = 0 ) 都初始化為 1.0 倍,因為第一層影像是沒有經過縮小或放大的 ( 縮放倍率為 1.0 )。接下來,每一層縮放倍率的計算,我們只要取前一層的數值,乘以和除以縮放因子即可。
假設我們今天設 Scale Factor 為1.2,那mvfScaleFactors就等於{ 1.000000, 1.200000, 1.440000 },每一層等於前一層的1.2倍。以影像來說,每一層影像的尺寸等於前一層影像尺寸除以1.2;而mvfInvScaleFactors就等於{ 1.000000, 0.833333, 0.694444 },它和mvfScaleFactors是相反的概念,以影像來說,每一層影像的尺寸等於前一層影像尺寸乘以0.83倍。 - 計算影像金字塔中每一層應提取的特徵點數量 ( 根據公式 ) :
在此步驟中,我們也一經得知mvnFeaturesPerLevel的大小,接下來只要簡單地根據金字塔層數,遍歷這個 Vector 即可。首先,我們要根據 《 第 2 天 : 影像金字塔 ( Image Pyramid ) 》這篇文章中提及的公式,來合理分配每一層影像中應提取的特徵點數,除了最後一層。分配完從第 1 層 ~ 倒數第 2 層的特徵點數後,剩下的特徵點數就分配給最後一層,這是最直接、最乾淨的做法。
為什麼最後一層要額外處理呢 ? 因為如果連最後一層都套用公式,如圖(五)所示,最後每一層加起來的待提取特徵點數,並不會那麼剛好等於我們所要的總量,可能超過也可能缺少 !
【注意】假設我們今天設 Scale Factor 為1.2,那公式的s就等於1.0/1.2 = 0.83,而不是1.2,這一點千萬不要搞混,否則待提取特徵點數會變成負數 ! - 輸出影像金字塔相關資訊 :
最後,我們只要打印出影像金字塔的相關資訊即可,這一步是最簡單也最單純的 !

測試影像金字塔
#include "myORB-SLAM2/ImagePyramid.h" // 引入自定義的 ImagePyramid 類別
#include <opencv2/opencv.hpp> // 引入 OpenCV 的核心功能和影像處理功能
#include <chrono> // 提供高精度時間測量功能
using namespace my_ORB_SLAM2; // 使用自定義命名空間 my_ORB_SLAM2
using namespace std; // 使用標準命名空間
using namespace cv; // 使用 OpenCV 的命名空間
// test cmd: ./testImagePyramid_00 ../../images_00/000000.png
// 測試命令:執行程式並傳入影像路徑作為參數
int main(int argc, char **argv) {
string path = argv[1]; // 從命令列參數中獲取影像路徑
Mat image = imread(path); // 使用 OpenCV 的 imread 函式讀取影像
// 建立影像金字塔物件,設定層數為 3,每層縮放係數為 1.2,待提取的總特徵點數量為 2000
std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
ImagePyramid imagePyramid(3, 1.2, 2000);
std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
std::chrono::duration<double> time_used =
std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
printf("\nSetting Parameters costs: %f\n", time_used.count());
// 將讀取的影像設置為影像金字塔的第一層,並生成其他層
t1 = std::chrono::steady_clock::now();
imagePyramid.setImage(image);
t2 = std::chrono::steady_clock::now();
time_used = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
printf("Setting Images costs: %f\n", time_used.count());
// 獲取影像金字塔的所有層影像
vector<Mat> images = imagePyramid.mvImages;
// 遍歷影像金字塔的每一層,並顯示影像
int level = 0;
for(Mat &image : images) {
string name = "image"; // 設定視窗名稱的前綴
name.append(to_string(level++)); // 為每層影像添加層數後綴
imshow(name, image); // 使用 OpenCV 的 imshow 函式顯示影像
}
// 等待使用者按下任意鍵後關閉所有視窗
waitKey(0);
destroyAllWindows();
return 0; // 程式執行成功
}




[…] 如同我們在《 第 3 天 : 深入解析影像金字塔 ( Image Pyramid ) 的實作與應用 》這篇文章提到的,關鍵點 ( 或稱角點 ) 其實就是指影像中,顯著的角落處,這些地方被認為是比較具有辨識度的。 […]