
本文章將介紹如何實作影像金字塔類別,以及成員變數、函數有哪些 ? 如何實踐 ? 有興趣了解影像金字塔的朋友,可以前往 《【ORB-SLAM2】探索之旅 第 2 天:影像金字塔 (Image Pyramid) Part 1》 觀看。
Table of Contents
本章節能夠帶給你甚麼好處 ?
學完本章,你將能夠自己設計一個影像金字塔。把影像設定到影像金字塔後,金字塔會自動計算各層的參數,例如縮放因子、待提取特徵點數、縮放後的影像等,這些參數有利於我們後續提取影像中的特徵點,實現特徵點的「尺度不變性」!

題外話
需要特別說明的是,本系列文章並不是單純照抄原作,而是我在理解其理論與程式碼後,重新消化、吸收,再加上自己的想法所整理出來的版本,因此在實作方式上可能與原作有很大的差異。
例如在原作者的程式碼中,影像金字塔只佔了短短幾行,並沒有強調影像金字塔的存在。儘管如此,影像金字塔的概念仍然很重要,為了加強影像金字塔的存在感,我幫它獨立設計了一個 ImagePyramid 類別。這種習慣也是為了讓我能夠更清楚每個不同模組的功能,同時讓每個模組之間更加獨立、降低彼此的依賴度,使程式碼更容易維護 !
後續,我們會為更多,其它不同的功能,分別設計一個類別,方便我們管理「程式碼」或 「Debug」。
Image Pyramid 的類別設計 & Header 檔撰寫
以下程式碼是 Image Pyramid 的完整 Header 檔,該檔案也收錄在我的 Github 上,有興趣的讀者歡迎點進來看。接下來,我會對這份 Header 檔多做一些說明 :
#ifndef IMAGEPYRAMID_H
#define IMAGEPYRAMID_H
#include <vector>
#include <list>
#include <cstdio>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
using namespace cv;
using namespace std;
namespace my_ORB_SLAM2 {
class ImagePyramid {
    public :
        /*
        @brief 影像金字塔
        
        @param[in] nLevels 影像金字塔的層數
        @param[in] fScaleFactor 每層之間的縮放因子 (例如 1.2) 
        @param[in] nFeatures 總共需要提取的特徵點數量 */
        ImagePyramid (int nLevels, float fScaleFactor, int nFeatures);
        ~ImagePyramid () {};
        // 設定影像 : 將不同縮放倍率的影像依序放入影像金字塔中
        void setImage(const Mat &image);
        int mnLevels; // 影像金字塔的層數
        int mnFeatures; // 總共需要提取的特徵點數量
        float mfScaleFactor; // 每層之間的縮放係數
        vector<int> mvnFeaturesPerLevel; // 儲存每一層影像中應提取的「特徵點數」
        vector<float> mvfScaleFactors; // 儲存每一層影像相較於第一層影像的「縮小倍數」
        vector<float> mvfInvScaleFactors; // 儲存每一層影像恢復為第一層影像大小所需的「縮放倍數」
        vector<Mat> mvImages; // 儲存每一層影像的矩陣
    
    private :
        // 輸出影像金字塔相關資訊
        void info() {
            printf("Image Pyramid Information: \n");
            printf(" - Levels: %d\n", mnLevels);
            printf(" - Scale Factor: %f\n", mfScaleFactor);
            printf(" - Number of Features: %d\n", mnFeatures);
            
            printf("\n - Features Per Level: \n");
            printf(" { ");
            for(int i = 0; i < mnLevels-1; i++) { printf("%d, ", mvnFeaturesPerLevel[i]); }
            printf("%d", mvnFeaturesPerLevel[mnLevels-1]);
            printf(" }\n");
            printf("\n - Scale Factors: \n");
            printf(" { ");
            for(int i = 0; i < mnLevels-1; i++) { printf("%f, ", mvfScaleFactors[i]); }
            printf("%f", mvfScaleFactors[mnLevels-1]);
            printf(" }\n");
            printf("\n - Inv Scale Factors: \n");
            printf(" { ");
            for(int i = 0; i < mnLevels-1; i++) { printf("%f, ", mvfInvScaleFactors[i]); }
            printf("%f", mvfInvScaleFactors[mnLevels-1]);
            printf(" }\n");
        }
};
}
#endif
為什麼把 Variables 都放在 Public 區塊?
關於這份 Header 檔,值得說明的部分是,為了簡單性,我把所有成員變數都放在 Public 的區塊,這樣如果我想把某個變數輸入到其它 Function 作為參數,會更方便。
為什麼不設計一些 Getter Function ?
你可能會好奇 :「只是多寫一些 Getter 應該沒那麼困難吧? 只不過是 Return 一些基礎型別 (Integer、Float 等) 的資料而已。」但是對我來說,使用 Getter 反而會降低一點可讀性。
以上面 ImagePyramid 的成員變數,mnLevels 為例,光是看到這個變數名稱就可以告訴我很多資訊,前綴的小寫字母 m 代表「成員 (Member)」、n 代表「幾個,且型別是 Interger」。所以 mnLevels 這個變數想傳達的就是 :「我是 ImagePyramid 物件的成員變數,這個影像金字塔總共有幾層。」這樣的概念。
為什麼使用 Getter 反而難以閱讀 ?
使用 Getter 的話,Function 名稱取「getLevels」,這樣是代表要取得每一層影像 ? 還是每一層的特徵點 ? 還是每一層相對於第一層的縮放係數 ? 難以一眼看出該 Function 的用途或目的。就算名稱取「getMNLevels」,看起來也很怪,很難閱讀,乾脆不使用哈 !
放棄使用 Getter,允許直接存取成員變數
這樣寫程式的時候腦子就不會亂掉,不必回去查這個變數是「屬於誰 ?」、「浮點數還是整數 ?」,非常方便,計算時也不容易因為型別問題而出錯。但這同樣付出了一些安全性上的代價,例如其它物件或 Function 能夠輕易修改這些成員變數的值,當然我不會讓這種事情亂發生 !
安全性與可讀性方面的折衷
考慮到這系列文章只是一個學習、研究 ORB SLAM2 的過程,只要程式碼能動,且其概念、架構也能夠傳達清楚,有以上這幾點就很棒了。所以,安全性的部分我就不那麼鑽牛角尖囉 !



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