【ORB-SLAM2】探索之旅 第 3 天 : 深入解析影像金字塔 ( Image Pyramid ) 的實作與應用

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 個小步驟 :

  1. 初始化影像金字塔基礎參數 :
    這一步驟很容易理解,首先我們要設定影像金字塔最重要的 3 個基礎參數,即「影像金字塔層數、欲提取的特徵點數、縮放因子」,如此一來,我們後續才可以好好地從影像金字塔中提取品質好的特徵點。

  2. 初始化資料結構大小以符合影像金字塔層數 :
    這一步對 C++ 新手來說可能有一點不直觀。在 C++ 的世界中,如果我們能夠精確地控制記憶體空間的分配,將會大大地提高程式運作的效率與速度。以 Vector 為例,如果我們已經事先知道我們要儲存多少筆資料,那就可以直接使用 resize 這個 Function,幫助我們直接調整 Vector 的大小,以符合我們要儲存的資料筆數。

    以影像金字塔為例,如果我們已經知道金字塔的層數為 3,那其成員變數 mvnFeaturesPerLevelmvfScaleFactorsmvfInvScaleFactorsmvImages 的層數都可以直接設定為 3。如此一來,也可以幫助我們把下一個步驟的程式碼邏輯簡化 !

  3. 初始化影像金字塔中,每一層的縮小倍數與放大倍數 :
    如同剛才所說的,我們已經得知 mvfScaleFactorsmvfInvScaleFactors 的大小,接下來只要簡單地根據金字塔層數,遍歷這兩個 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 倍。

  4. 計算影像金字塔中每一層應提取的特徵點數量 ( 根據公式 ) :
    在此步驟中,我們也一經得知 mvnFeaturesPerLevel 的大小,接下來只要簡單地根據金字塔層數,遍歷這個 Vector 即可。首先,我們要根據 《 第 2 天 : 影像金字塔 ( Image Pyramid ) 》這篇文章中提及的公式,來合理分配每一層影像中應提取的特徵點數,除了最後一層。分配完從第 1 層 ~ 倒數第 2 層的特徵點數後,剩下的特徵點數就分配給最後一層,這是最直接、最乾淨的做法。

    為什麼最後一層要額外處理呢 ? 因為如果連最後一層都套用公式,如圖(五)所示,最後每一層加起來的待提取特徵點數,並不會那麼剛好等於我們所要的總量,可能超過也可能缺少 !

    【注意】假設我們今天設 Scale Factor 為 1.2,那公式的 s 就等於 1.0/1.2 = 0.83,而不是 1.2 ,這一點千萬不要搞混,否則待提取特徵點數會變成負數 !

  5. 輸出影像金字塔相關資訊 :
    最後,我們只要打印出影像金字塔的相關資訊即可,這一步是最簡單也最單純的 !
合理分配每一層影像中應提取的特徵點數
圖(六)、分配每一層影像中應提取之特徵點數的公式

測試影像金字塔

#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; // 程式執行成功
}
影像金字塔縮小圖片
圖(七)、將一張影像設定到影像金字塔後,顯示每一層的影像。可以發現,越上層的影像以等比例縮小。

1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *