NDK/JNI demo ( 五 ) ORB_SLAM2在Android上的移植过程
Android平臺搭建和NDK環境配置
Android移植基礎
NDK是集成的Android中調用C++代碼的工具包,核心是JNI(Java Native Interface)技術,具體這里略過不表。只說說NDK開發的基本步驟:
1. 編寫Java代碼:在Java中定義一個類,比如說叫NDKHelper吧,里面定義幾個java的方法,只需要聲明,不需要實現,如下所示:
public class NDKHelper {//NDK示例方法1public static native void ndkOne(int a,long b);//NDK示例方法2public 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 <jni.h>
/* 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);
- 1
這個函數就是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 f2cLOCAL_EXPORT_C_INCLUDES := $(LOCAL_C_INCLUDES)
LOCAL_PATH := $(MAINDIR)
include $(BUILD_SHARED_LIBRA
這里的基本命令之前都已經講過了,只補充如下幾點內容:
- 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為核心部分,opencv4Android包含兩個版本,一個是opencv為java做的本地化sdk,另一個是opencv利用ndk編譯C++版本得到的庫工程。我們將opencv4android解壓后放置到ORB_SLAM2項目的同級目錄下,如下所示:
之后在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 stringstd::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方法將其轉換成int[]型數據進行傳遞,在jni中int[]對應的數據類型為jintArray,我們可以在獲取到數據后將jintArray轉換成Mat進行后續處理;而在Camera模式中我們是利用opencv android sdk中的cvCameraView 來直接進行攝像頭的調用和圖像的顯示。其onCameraFrame(CvCameraViewFrame inputFrame)中的inputfram可以通過rgba()方法轉換成Mat類型數據,而Mat類型同樣不被jni識別,因此需要利用Mat的getNativeObjAddr方法獲取Mat數據的long型指針傳遞到jni中進行處理。關鍵代碼如下: DataSet 的Java部分:
int w = tmp.getWidth(), h = tmp.getHeight();//其中tmp為bitmap
int[] pix = new int[w * h];
tmp.getPixels(pix, 0, w, 0, 0, w, h);3
C++部分:
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);
Camera的Java部分:
Mat im=inputFrame.rgba();
synchronized (im) {addr=im.getNativeObjAddr();//addr為函數傳遞的圖像參數
4
C++部分:
const cv::Mat *im = (cv::Mat *) addr;//addr為傳入的圖像參數1
結尾
當上述步驟都完成后,我們會得到最終生成的sdk。Android部分的布局文件和對應activity文件在這里也略過不表。當得到最終生成的apk后,我們如果要測試Camera模式,需要先將opencv4Android中apk文件夾中對應類型的opencv manager安裝到手機中并預先打開才能使用,否則會提示找不到opencv的支持庫;若只需測試Dataset模式則無需上述步驟。
總結
以上是生活随笔為你收集整理的NDK/JNI demo ( 五 ) ORB_SLAM2在Android上的移植过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《梦仙》第四十五句是什么
- 下一篇: 请问普鲁卡因多少钱