RoboMaster視覺教程(3)視覺識別程序整體框架

概覽

RoboMaster 視覺識別是一個比較大的項目了,綜合性太強。這裏從程序框架的角度來粗略講一下需要怎麼做。比較好的框架有官方開源的視覺程序,東南大學開源的視覺程序,其中東南大學開源的程序可以認爲是官方開源程序的加強版。他們的程序層次分明清晰易讀非常具有擴展性,在其基礎上可以很好地修改和擴展功能。

多線程

官方的開源程序和東南的開源程序最大的特點就是類封裝和多線程。通過類封裝讓程序結構分明各部分功能清晰,而多線程通過併發執行增加 cpu 利用率提高算法速度。

一般我們在學習 opencv 的時候寫程序通常的流程就是讀入圖片/視頻/攝像頭,對每張圖片進行處理,處理後進行輸出(顯示圖片/顯示識別結果等等)。這種模式基本上是一條線走下來的,前一個步驟沒有完成就無法進行下一步的操作。

而使用多線程後就可以把每個步驟拆分開,用單獨的線程來完成對應的操作。有人會說不管怎麼拆不還是需要先讀圖再處理再輸出嘛。

但是由於使用了多線程就可以進行流水線作業,這樣在圖像處理線程處理本張圖片的時候圖像讀取線程可以讀入下一張圖片。

一般而言圖像讀取速度會比圖像處理慢得多,一個 120fps 的攝像頭平均一幀所花費的時間是 8ms ,而圖像處理所花的時間則在 1~3ms 左右(只識別裝甲板燈條),這時候平均每幀的處理時間就是 8ms 。如果換 330fps 的攝像頭則平均每幀的處理時間就是 3ms 。通過多線程可以極大地減少算法的用時,提高效率。

除了多線程,還可使用多進程

之前看到 https://wzq.io/?p=345 這篇文章,他通過兩個進程來實現圖像的獲取和處理,我試過他的方法,這樣做效率沒有多線程高,獲取圖片時會增加 1ms 左右的延時,但是靈活性很強穩定性更高,可以讓多個 client 進程通過 server 調取攝像頭圖片。

如果需要對同一張圖片分別做各種處理,這種方式就很靈活簡潔了,比如一個圖像處理進程、一個圖像傳輸進程就可以實現邊處理圖片邊發送圖片達到圖傳與處理同時進行的效果。

圖像獲取與處理通過進程來實現可以確保任意一方掛了對另一方沒有影響,通過看門狗實現崩潰重啓繼續工作。由於時間關係在備賽中我沒有采用這個方案,不過可以作爲參考。

接下來以東南大學的開源程序爲例講一下他們的整體架構

東南大學2018年視覺開源程序 GitHub 地址:https://github.com/SEU-SuperNova-CVRA/Robomaster2018-SEU-OpenSource

他們的代碼寫得非常規範,而且也有很詳細的註釋和說明,現在把代碼開源的隊伍有很多,但像他們這樣做得如此規範的隊伍少有,很多隊伍把代碼開源後就不管了也沒有註釋什麼的。

通過讀他們的代碼可以學到很多東西,首先他們的代碼是通過 GitHub 來協作編寫的,用 GitHub 的好處是多人編寫代碼時不會亂套。

我17年參加比賽時當時負責視覺和部分電控,當時代碼協作就是靠優盤拷,經常會發生代碼中的一些參數沒改雲臺瘋了之類的情況。

用 GitHub 的另一個好處是代碼每個版本都有備份,可以隨心所欲的寫代碼,不用擔心之前的代碼找不回來的窘境。 qtcreator 內置有一個git插件,只需要簡單設置就可以圖形化地使用 GIt 。

其次他們用面向對象的設計思路用若干個類來組織代碼提高了程序的可讀性和可維護性,他們將每個功能劃分成類然後在調用的時候通過指針來實例化並調用相應的成員函數實現功能。

下面進入正題

GitHub 上下載源碼解壓後可以看到如下文件和文件夾,我在圖上標註了用途,由於今年大符大改,以往的代碼都作廢所以裝甲識別可以沿用並改進原有代碼,大符的要自己寫。在 README.md 中有項目說明和算法介紹,寫得很好。
seu2

項目配置文件概覽

在 qtcreator 中打開項目後可以看到項目全貌,雙擊 .pro 文件可以查看項目的配置情況,CONFIG += c++ 14是配置 qmake 支持 c++14 如果在妙算上用則由於版本問題該行失效,需要使用 QMAKE_CXXFLAGS += -std=c++1y 來支持 c++14 ,下面 CUDA 的部分可以刪掉,因爲今年視覺不需要用到顯卡加速,無論是風車還是裝甲識別算法都很簡單不需要用到深度學習。 V4L2 是攝像頭驅動,
seu3

Darknet 是 yolo 作者寫的一個開源深度學習框架,不需要使用可以刪除。接下來是頭文件和源文件,這些是添加相應文件後 qtcreator 自動生成的,當然也可以手動添加或者手動註釋。如果不需要編譯某個文件在前面加一個#就可以了,如果要跨行,需要在末尾添加一個\,否則會出錯。
seu4

ImgProdCons 類

該類可以看作是對整個系統的抽象,其成員函數包含了程序參數初始化,生產者消費者線程函數等,通過智能指針來調用其他類。

/*
* @Brief:   This class aims at separating reading(producing) images and consuming(using)
*           images into different threads. New images read from the camera are stored
*           into a circular queue. New image will replace the oldest one.
*/
class ImgProdCons
{
public:
    ImgProdCons();
    ~ImgProdCons() {}

    /*
     * @Brief: Initialize all the modules
     */
	void init();

    /*
     * @Brief: Receive self state from the serail port, update task mode if commanded
     */
    void sense();

	/*
    * @Brief: keep reading image from the camera into the buffer
	*/
	void produce();

	/*
    * @Brief: run tasks
	*/
	void consume();

private:
    /*
    * To prevent camera from dying!
    */
    static bool _quit_flag;
    static void signal_handler(int);
    void init_signals(void);

    /* Camera */
    std::unique_ptr<RMVideoCapture> _videoCapturePtr;
    FrameBuffer _buffer;

    /* Serial */
    std::unique_ptr<Serial> _serialPtr;

    /* Angle solver */
    std::unique_ptr<AngleSolver> _solverPtr;

    /* Armor detector */
    std::unique_ptr<ArmorDetector> _armorDetectorPtr;

    /*Rune detector*/
    std::unique_ptr<RuneDetector> _runeDetectorPtr;

    /* @See: 'Serial::TaskMode' */
    volatile uint8_t _task;

	void updateFeelings();
};

如果自己寫了算法,比如寫一個打風車大符的算法也可以仿照類似的方式用智能指針來指向它,其實用普通指針也可以,不過用智能指針unique_ptr可以在管理資源同時保證安全性,創建對象的時候使用make_unique可以安全地創建對象,由於make_unique是 c++14 才支持的所以要用 gcc5 來編譯。讀東南大學的開源代碼經常會看到一些最新的特性的使用和一些技巧,學到了很多,很佩服。

主函數

主函數主要作爲一個入口,創建 ImgProdCons 類,執行初始化成員函數,創建線程,之後主函數就完成了使命。這點和qt的編程很像,如果創建一個qt widget 應用,則會自動生成一個 MainWindow 類,自動生成的 main 中會幫寫好初始化和顯示的代碼,只需在qt設計師中設計窗口添加槽函數補充 MainWindow 類就能很容易寫出一個簡單的qt程序。

int main()
{
    rm::ImgProdCons imgProdCons;

    imgProdCons.init();

    std::thread produceThread(&rm::ImgProdCons::produce, &imgProdCons);
    std::thread consumeThread(&rm::ImgProdCons::consume, &imgProdCons);
    std::thread senseThread(&rm::ImgProdCons::sense, &imgProdCons);

    produceThread.join();
    consumeThread.join();
    senseThread.join();

    return 0;
}

produceThread 負責獲取圖像保存到緩存隊列中,consumeThread 負責對圖圖片的處理和指令的發送,而 senseThread 用於接收數據。

其實除了這幾個線程還可以添加一個保存視頻線程和一個串口發送線程,經實測將串口發送也用線程來做可以省 1~2ms 左右的時間,保存視頻的線程可以用來保存場上的比賽資料,現在的 minipc 的容量大的嚇人隨便來一個就有100多G的固態硬盤容量,就算是妙算也有10個G可以用,把視頻保存下來後可以針對場上發生的各種情況來進行改進,這種第一視角的視頻可以看到很多被忽略的細節。

用類來包裝算法

在寫一個檢測算法的時候我往往是怎麼簡單怎麼來,很多要調的參數什麼的就直接原樣寫在程序中了,整個算法也都堆在 main 中。

這麼搞對於自己寫着玩沒問題,但是如果要把寫好的算法拿來用那就要適當地包裝一下自己的算法了。

通過合理地拆解自己的算法按照面向對象的方式組成類可以提高程序的可讀性和健壯性。開源代碼中的裝甲檢測、大符、位姿解算和串口通信都提供了很好的範例,照樣畫葫蘆做就可以了。我本身也不太懂面向對象的東西,大學在課堂中只學過 c 語言,用到的一些 c++ 的東西都是現學現賣的。

現在以裝甲檢測舉例,分析一下他們是如何用類來包裝的。

在 ArmorDetector.h 中,有一個 ArmorParam 結構體和三個類 LightDescriptor 、 ArmorDescriptor 、 ArmorDetector 。

其中 ArmorParam 結構體用來存放一些算法用到的常量並用構造函數初始化, LightDescriptor 類用來描述檢測到的燈條, ArmorDescriptor 類用來描述裝甲板, ArmorDetector 是最終用來檢測裝甲的類。

可以依識別步驟先將燈條抽象出來建一個類,用自定義的類來描述燈條,每個燈條都會有寬高面積角度等屬性。

再將裝甲板抽象出來,裝甲板也有寬高面積傾斜角度以及裝甲的種類等屬性。

而整個識別過程也通過類來描述,識別的整個流程包括初始化參數、定義敵方顏色、加載圖像、檢測裝甲、返回裝甲信息等,將這些步驟每一步都用成員函數來封裝,可以讓代碼更易讀易改。

申請了一個自己的公衆號 江達小記 ,打算將自己的學習研究的經驗總結下來幫助他人也方便自己。感興趣的朋友可以關注一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章