編輯:關於android開發
一直沒時間寫博客,最近抽時間寫了些關於在ORB_SLAM2在Android上的移植過程,也算是點經驗吧。
寫完後一個手賤點了個鏈接,瞬間1/3工作量沒了,深夜弄完也是醉了。。。
正文開始
這篇博客講述如何在Android平台上移植ORB_SLAM2,講述過程包括基本的Android環境的搭建和NDK環境的配置,Android下移植的基本概念,ORB的具體移植步驟等。
系統: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,彈出如下界面: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.
不想知道移植過程的童鞋可以直接下載我的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子目錄中編譯好的一些依賴模塊
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模式則無需上述步驟。
水平有限,如有錯誤請不吝指正,謝謝。
Android surfaceview詳解(一) surface,這個單詞的意思是浮在表面的,那麼surfaceview就是浮在表面的view了。如果真的這樣解釋,估計有
Android Activity生命周期以及Fragment生命周期的區別與分析,androidfragmentAndroid Fragment生命周期圖: Activ
Android Material Design 兼容庫的使用,androidmaterialAndroid Material Design 兼容庫的使用 mecury
Android Eclipse 導入 AS Gradle AAR 庫手冊 序言 這是一篇半技術類文章。眾所周知現在Google主推Android Studio開發工具,而