Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> ORB_SLAM2在Android上的移植過程

ORB_SLAM2在Android上的移植過程

編輯:關於android開發

ORB_SLAM2在Android上的移植過程


一直沒時間寫博客,最近抽時間寫了些關於在ORB_SLAM2在Android上的移植過程,也算是點經驗吧。

寫完後一個手賤點了個鏈接,瞬間1/3工作量沒了,深夜弄完也是醉了。。。

正文開始


這篇博客講述如何在Android平台上移植ORB_SLAM2,講述過程包括基本的Android環境的搭建和NDK環境的配置,Android下移植的基本概念,ORB的具體移植步驟等。

Android平台搭建和NDK環境配置


系統:windows7 32bit
IDE:Eclipse Luna
環境工具: ADT24.0.2、Android SDK、NDK r10b

PS:不推薦使用集成了ADT環境的Eclipse版本,因為在NDK編譯的時候可能會報各種莫名其妙的錯。同時默認JDK環境已經裝好。

組件下載地址:百度網盤(包括ADT、NDK。SDK太大,請自行下載)

步驟:

安裝Eclipse,注意區分Eclipse是32位還是64位的,下載對應版本安裝即可。下載地址在前面給出。 下載ADT,不需要解壓。打開安裝好的Eclipse,在菜單欄點擊Help–>Install new software,彈出如下界面: 這裡寫圖片描述
點擊右上角Add,出現:
這裡寫圖片描述
第一欄隨便填個名字,例如ADT,第二欄點擊Archive,選擇你下載的ADT壓縮包,確定。在出現的列表中,全部選中,並取消勾選Contact all update sites….。點擊next,則開始了ADT的安裝,大概需要10分鐘。可能還需要accept 協議什麼的,這裡略過不表。安裝完成後提示重啟Eclipse即安裝成功。 下載Android SDK,並解壓(路徑不要有中文)。在Eclipse中選擇Window–>preferences。在左邊欄選中Android,在SDK Location中填入你解壓文件夾的根目錄,點擊apply和OK,則Android環境配置基本完成。這是你的菜單欄應該有了這兩項
這裡寫圖片描述 更新SDK。點擊Window–>Android SDK manager,彈出如下界面:
這裡寫圖片描述
可能由於牆的問題會彈出列表無法加載,需要用代理或鏡像,當然如果你有梯子也可以。這裡介紹些鏡像吧:
g.cn:80,在SDK Manager 中點擊tools–>options:
這裡寫圖片描述
按照上圖設置即可。其他的鏡像源還有北京化工大學鏡像站,請自行谷狗。設置好後點擊Packages–>reload即可得到對應的鏡像。有默認選中的一些選項,一般選中一個sdk tools,一份sdk build-platform-tools,一個sdk build tools,然後一個Android版本即可(一般4.0以上)。選中後點擊Install就會從鏡像中下載對應的SDK,這個過程有長有短,視網絡而定。
當上述步驟都完成後,Android開發環境就配置好了。 NDK環境配置:從上面給的百度網盤中下載ndk,解壓(規則同sdk),打開Eclipse,點擊Window–>Preferences,選擇Android–>NDK,在右邊界面中填入NDK Location即可。
這裡寫圖片描述

Android移植基礎


NDK是集成的Android中調用C++代碼的工具包,核心是JNI(Java Native Interface)技術,具體這裡略過不表。只說說NDK開發的基本步驟:
1. 編寫Java代碼:在Java中定義一個類,比如說叫NDKHelper吧,裡面定義幾個java的方法,只需要聲明,不需要實現,如下所示:

public class NDKHelper {
    //NDK示例方法1
    public static native void ndkOne(int a,long b);

    //NDK示例方法2
    public static native int ndkTwo(String a,String b);
}

native標識符表示該函數將會利用C++代碼完成實現。
接下來在工程上右鍵,Android Tools–>Add native support,出現如下界面:
這裡寫圖片描述
名字就是最後我們要生成的庫的名字,隨便填,可修改。點擊確定就會給你的工程添加C++編譯支持,菜單欄會多了個小錘子:
這裡寫圖片描述
這個是用來編譯C++的快捷鍵。在你的工程目錄下會新建jni目錄和obj目錄,其中jni目錄用來存放和C++代碼有關的東西,obj則存放C++進行編譯時產生的中間件,最後生成的library會寫入到libs文件夾下。
在jni文件夾中生成了如下文件,一個.cpp,一個Android.mk,其中.cpp是自動生成的,是用來編寫C++部分的,而Android.mk類似C++裡面的CMakeList,用來指定需要編譯的文件和編譯生成的模塊名,一個最簡單的Android.mk文件如下所示:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := NDKTest
LOCAL_SRC_FILES := NDKTest.cpp
include $(BUILD_SHARED_LIBRARY)

LOCAL_PATH :=$(call my-dir)表示包含當前目錄。
include $(CLEAR_VARS)表示清除全部非系統變量和部分系統變量;
LOCAL_MODULE := NDKTest 表示當前生成的模塊名,最終會生成libNDKTest.so文件
LOCAL_SRC_FILES := NDKTest.cpp 表示當前需要編譯的cpp文件;
include $(BUILD_SHARED_LIBRARY) 表示生成共享庫,需要生成靜態庫請修改成BUILD_STATIC_LIBRARY。

其他基礎命令:
LOCAL_C_INCLUDES:= 表示添加頭文件進入編譯環境
LOCAL_LDLIBS:= 表示添加系統靜態庫
LOCAL_SHARED_LIBRARIES:= 表示添加共享庫
其他命令請自行查看API文檔。

這裡指定了進行編譯時的各項條件,如果需要指定編譯器版本和編譯目標平台等信息,則需要在jni目錄下新建Application.mk文件,基本語句如下:

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions 
NDK_TOOLCHAIN_VERSION := 4.8
APP_ABI :=armeabi-v7a

APP_STL :=表示使用stl庫,APP_CPPFLAGS表示一些CPP編譯參數,NDK_TOOLCHAIN_VERSION 表示NDK使用的編譯器版本,APP_ABI表示編譯的目標平台,可以指定多個平台,平台之間用空格隔開,或者指定all則為全平台編譯(armeabi,armeabi-v7a,mips,x86)。其他命令請自行查看API。

接下來編寫對應的C++文件。
打開eclipse,點擊Project–>build Project(若build automatically已勾選則會自動編譯)打開命令行,cd到你的工程文件夾下的bin–>classes文件夾下,輸入如下命令:
這裡寫圖片描述

javah com.example.ndktest.NDKHelper

回車,則在你的classes文件夾下會生成對應的頭文件。這裡com.example.ndktest是你的package名字,NDKHelper是你的NDK函數的類名。
生成的頭文件如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_example_ndktest_NDKHelper */

#ifndef _Included_com_example_ndktest_NDKHelper
#define _Included_com_example_ndktest_NDKHelper
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_ndktest_NDKHelper
 * Method:    ndkOne
 * Signature: (IJ)V
 */
JNIEXPORT void JNICALL Java_com_example_ndktest_NDKHelper_ndkOne
  (JNIEnv *, jclass, jint, jlong);

/*
 * Class:     com_example_ndktest_NDKHelper
 * Method:    ndkTwo
 * Signature: (Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_example_ndktest_NDKHelper_ndkTwo
  (JNIEnv *, jclass, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif

其他不用管,我們關注中間的兩個函數聲明:

JNIEXPORT void JNICALL Java_com_example_ndktest_NDKHelper_ndkOne(JNIEnv *, jclass, jint, jlong);

這個函數就是NDKHelper類中ndkOne函數對應的C++版本,其中JNIEXPORT和JNICALL是固定字段,void是函數返回值,函數名由Java字段+包名+類名+函數名組成,參數則多了幾個JNI的系統參數JNIEnv 和jclass,其他的就是NDKHelper類中的對應參數,ndk會對該函數進行解析和鏈接,實現java和C++的對接。
將生成的.h頭文件復制到jni目錄下,新建對應的cpp文件,將該頭文件include進來並對對應函數進行實現,實現過程就視函數功能而定。
這些工作完成後需要修改你的Android.mk文件,將剛剛新建的cpp和h文件包括進來。
然後點擊開始那個小錘子或者直接項目右鍵RunAs–>Android Application,則C++部分會開始編譯,編譯具體過程可以在Eclipse下方Console窗口看到(如果沒有Console窗口則點擊Window–>Show Views,選擇Console確定即可)。
編譯完成後會生成對應的庫存放在libs目錄下,則你可以開始在Java裡面調用剛才定義的ndkOne和ndkTwo函數實現具體的功能。

NDK基礎到此為止,更深入的學習可以下載Android官方給的ndk samples.

ORB_SLAM2的移植


不想知道移植過程的童鞋可以直接下載我的Github源碼:https://github.com/FangGet/ORB_SLAM2_Android 直接按照步驟進行即可。
移植過程
先看目錄:
這裡寫圖片描述
分為ORB和ThirdParty,其中ThirdParty包括boost clapack DBow2 g2o eigen3。
clapack和eigen來自於一個github的開源庫:https://github.com/simonlynen/android_libs 這裡集成了一些經典的C++庫的ndk版本,下載即可使用。g2o和DBoW2則來自於ORB_SLAM2原作者的github地址,Boost是自己編譯的lib,這裡只介紹clapack和opencv的庫配置。
clapack配置
從前述的開源庫中將clapack目錄拷貝到Thirdparty的對應目錄下,clapack中已經包含了對當前目錄極其子目錄的編譯過程,我們在jni目錄下的Android.mk文件中加入如下內容:

include $(CLEAR_VARS)
MAINDIR:= $(LOCAL_PATH)
include $(MAINDIR)/Thirdparty/clapack/Android.mk
LOCAL_PATH := $(MAINDIR)

include $(CLEAR_VARS)
MAINDIR:= $(LOCAL_PATH)
LOCAL_MODULE:= lapack
LOCAL_SHORT_COMMANDS := true
LOCAL_STATIC_LIBRARIES := tmglib clapack blas f2c

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
LOCAL_PATH := $(MAINDIR)
include $(BUILD_SHARED_LIBRARY)

這裡的基本命令之前都已經講過了,只補充如下幾點內容:

LOCAL_SHORT_COMMANDS是為了防止Windows對G++編譯命令長度的限制而設置的參數,該參數會拖慢整個編譯過程,因此請謹慎使用; LOCAL_EXPORT_C_INCLUDES表示將當前庫的頭文件EXPORT給系統,讓程序代碼中能實現<>的調用過程,若不設置這一參數則在cpp文件中可能無法引用該庫; LOCAL_STATIC_LIBRARIES := tmglib clapack blas f2c是引用lapack子目錄中編譯好的一些依賴模塊
這裡會編譯出一個名為lapack的庫工程,該工程就可以作為依賴項被ORB所引用。
OpenCV的編譯
opencv4Android是opencv官網為了對Android的支持而推出的一個工具集,可以在opencv官網進行下載。其目錄結構如下:
這裡寫圖片描述
其中sdk為核心部分,Z喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcGVuY3Y0QW5kcm9pZLD8uqzBvbj2sOaxvqOs0ru49srHb3BlbmN2zqpqYXZh1/a1xLG+tdi7r3Nka6Oswe3Su7j2ysdvcGVuY3bA+9PDbmRrseDS60MrK7Dmsb61w7W9tcS/4rmks8yho87Sw8e9q29wZW5jdjRhbmRyb2lkveLRubrzt8XWw7W9T1JCX1NMQU0yz+7Ev7XEzay8tsS/wrzPwqOsyOfPwsv5yr6jujxiciAvPg0KPGltZyBhbHQ9"這裡寫圖片描述" src="http://www.bkjia.com/uploads/allimg/160417/041415Nb-11.png" title="\" />
之後在jni目錄下的Android.mk中需要引用到OpenCV的地方加入如下代碼:
OPENCV_LIB_TYPE:=STATIC
ifeq ("$(wildcard $(OPENCV_MK_PATH))","")  
#try to load OpenCV.mk from default install location  
include E:/ORB_SLAM2/OpenCV-2.4.9-android-sdk/sdk/native/jni/OpenCV.mk
else  
include $(OPENCV_MK_PATH)  
endif 

這裡opencv.mk我給的是絕對地址,其實相對地址也是可以的。上面這段引用會將opencv進行編譯並引入到當前的工作模塊上來,這裡就完成了opencv庫的基本調用。如果為了方便還可以將opencv自身單獨編譯成一個庫工程並開放給其他模塊引用。

其他libraries的編譯過程和上述工程大同小異,其主要步驟可以概括如下:

將當前庫復制到jni的特定目錄下; 在Android.mk中新建一個模塊並對模塊進行命名; LOCAL_C_INCLUDE引入庫的頭文件,LOCAL_SRC_FILES引入庫的cpp文件; LOCAL_LDLIBS/LOCAL_SHARED_LIBRARIES/LOCAL_STATIC_LIBRARIES引入依賴庫; LOCAL_C_FLAGS設置編譯參數;

ORB_SLAM2的編譯
這裡我們將ORB_SLAM2的源文件也編譯為一個library以供調用,其編譯過程和上面雷同,需要注意的是,由於pangolin編譯有問題,我拆了源文件的pangolin部分並注釋了對應的部分代碼,同時引入了opengl es 來進行map和pose的繪制。同時,為了完成特征檢測圖像的回調,我改變了System.cc中TrackMonocular的返回值,將其返回值改成了Mat。
當上述過程完成後,我們的C++編譯工作就基本完成了,最後也是最重要的一步是為Java中定義的native方法做C++的實現,在JAVA中,我定義了如下native函數:

    /**
     * jni中初始化SLAM系統
     * @param VOCPath
     * @param calibrationPath
     */
    public static native void initSystemWithParameters(String VOCPath,String calibrationPath);

    /**
     * Dataset模式中ORB系統的start函數
     * @param curTimeStamp
     * @param data
     * @param w
     * @param h
     * @return
     */
    public static native int[] startCurrentORB(double curTimeStamp,int[] data,int w,int h);
    /**
     * Camera模式中ORB系統的start
     * @param curTimeStamp
     * @param addr
     * @param w
     * @param h
     * @return
     */
    public native static int[] startCurrentORBForCamera(double curTimeStamp,long addr,int w,int h);
    /**
     * Opengl es 的初始化
     */
    public native static void glesInit();  
    /**
     * opengl es繪制更新
     */
    public native static void glesRender();  
    /**
     * 防止opengl es窗口resize帶來的影響
     * @param width
     * @param height
     */
    public native static void glesResize(int width, int height);

其對應的C++代碼為:

/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    initSystemWithParameters
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_initSystemWithParameters
(JNIEnv * env, jclass cls, jstring VOCPath, jstring calibrationPath) {
    const char *calChar = env->GetStringUTFChars(calibrationPath, JNI_FALSE);
    const char *vocChar = env->GetStringUTFChars(VOCPath, JNI_FALSE);
    // use your string
    std::string voc_string(vocChar);
    std::string cal_string(calChar);
    env->GetJavaVM(&jvm);
    jvm->AttachCurrentThread(&env, NULL);
    s=new ORB_SLAM2::System(voc_string,cal_string,ORB_SLAM2::System::MONOCULAR,true);
    env->ReleaseStringUTFChars(calibrationPath, calChar);
    env->ReleaseStringUTFChars(VOCPath, vocChar);
    init_end=true;
}

/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    startCurrentORB
 * Signature: (DDD[I)[I
 */
JNIEXPORT jintArray JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_startCurrentORB(
        JNIEnv * env, jclass cls, jdouble curTimeStamp, jintArray buf, jint w,
        jint h) {
    jint *cbuf;
    cbuf = env->GetIntArrayElements(buf, false);
    if (cbuf == NULL) {
        return 0;
    }
    int size = w * h;
    cv::Mat myimg(h, w, CV_8UC4, (unsigned char*) cbuf);
    cv::Mat ima = s->TrackMonocular(myimg, curTimeStamp);
    jintArray resultArray = env->NewIntArray(ima.rows * ima.cols);
    jint *resultPtr;
    resultPtr = env->GetIntArrayElements(resultArray, false);
    for (int i = 0; i < ima.rows; i++)
        for (int j = 0; j < ima.cols; j++) {
            int R = ima.at < Vec3b > (i, j)[0];
            int G = ima.at < Vec3b > (i, j)[1];
            int B = ima.at < Vec3b > (i, j)[2];
            resultPtr[i * ima.cols + j] = 0xff000000 + (R << 16) + (G << 8) + B;
        }
    env->ReleaseIntArrayElements(resultArray, resultPtr, 0);
    env->ReleaseIntArrayElements(buf, cbuf, 0);
    return resultArray;
}
/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    glesInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_glesInit
(JNIEnv *env, jclass cls) {
    // 啟用陰影平滑
    glShadeModel(GL_SMOOTH);
    // 黑色背景
    glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
    // 設置深度緩存
    glClearDepthf(1.0f);
    // 啟用深度測試
    glEnable(GL_DEPTH_TEST);
    // 所作深度測試的類型
    glDepthFunc(GL_LEQUAL);
    // 告訴系統對透視進行修正
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
}

/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    glesRender
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_glesRender
(JNIEnv * env, jclass cls) {
    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode (GL_MODELVIEW);
    glLoadIdentity ();
    if(init_end)
    s->drawGL();
}

/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    glesResize
 * Signature: (II)V
 */
JNIEXPORT void JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_glesResize
(JNIEnv *env, jclass cls, jint width, jint height) {
    //圖形最終顯示到屏幕的區域的位置、長和寬
    glViewport (0,0,width,height);
    //指定矩陣
    glMatrixMode (GL_PROJECTION);
    //將當前的矩陣設置為glMatrixMode指定的矩陣
    glLoadIdentity ();
    glOrthof(-2, 2, -2, 2, -2, 2);
}

/*
 * Class:     orb_slam2_android_nativefunc_OrbNdkHelper
 * Method:    readShaderFile
 * Signature: (Landroid/content/res/AssetManager;)V
 */
JNIEXPORT jintArray JNICALL Java_orb_slam2_android_nativefunc_OrbNdkHelper_startCurrentORBForCamera
(JNIEnv *env, jclass cls,jdouble timestamp, jlong addr,jint w,jint h) {
    const cv::Mat *im = (cv::Mat *) addr;
    cv::Mat ima = s->TrackMonocular(*im, timestamp);
    jintArray resultArray = env->NewIntArray(ima.rows * ima.cols);
    jint *resultPtr;
    resultPtr = env->GetIntArrayElements(resultArray, false);
    for (int i = 0; i < ima.rows; i++)
    for (int j = 0; j < ima.cols; j++) {
        int R = ima.at < Vec3b > (i, j)[0];
        int G = ima.at < Vec3b > (i, j)[1];
        int B = ima.at < Vec3b > (i, j)[2];
        resultPtr[i * ima.cols + j] = 0xff000000 + (R << 16) + (G << 8) + B;
    }
    env->ReleaseIntArrayElements(resultArray, resultPtr, 0);
    return resultArray;
}

這裡解釋下Dataset和Camera模式下start方法的區別。其實就是圖像參數傳遞的方式不一樣。在DataSet模式中,我們是用ImageView顯示圖片,用Bitmap讀取文件中的圖片,而非基本類型的數據都是不能被jni接口所接受的因此我們需要利用Bitmap的getPixels方法將其轉換成intp[型數據進行傳遞,在jni中int[]對應的數據類型為jintArray,我們可以在獲取到數據後將jintArray轉換成Mat進行後續處理;而在Camera模式中我們是利用opencv android sdk中的cvCameraView 來直接進行攝像頭的調用和圖像的顯示。其onCameraFrame(CvCameraViewFrame inputFrame)中的inputfram可以通過rgba()方法轉換成Mat類型數據,而Mat類型同樣不被jni識別,因此需要利用Mat的getNativeObjAddr方法獲取Mat數據的long型指針傳遞到jni中進行處理。

結尾


當上述步驟都完成後,我們會得到最終生成的sdk。Android部分的布局文件和對應activity文件在這裡也略過不表。當得到最終生成的apk後,我們如果要測試Camera模式,需要先將opencv4Android中apk文件夾中對應類型的opencv manager安裝到手機中並預先打開才能使用,否則會提示找不到opencv的支持庫;若只需測試Dataset模式則無需上述步驟。

水平有限,如有錯誤請不吝指正,謝謝。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved