作者:Frank
转载请注明出处

一直没时间写博客,最近抽时间写了些关于在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太大,请自行下载)

步骤:

  1. 安装Eclipse,注意区分Eclipse是32位还是64位的,下载对应版本安装即可。下载地址在前面给出。
  2. 下载ADT,不需要解压。打开安装好的Eclipse,在菜单栏点击Help–>Install new software,弹出如下界面:
  3. 这里写图片描述
    点击右上角Add,出现:
    这里写图片描述
    第一栏随便填个名字,例如ADT,第二栏点击Archive,选择你下载的ADT压缩包,确定。在出现的列表中,全部选中,并取消勾选Contact all update sites….。点击next,则开始了ADT的安装,大概需要10分钟。可能还需要accept 协议什么的,这里略过不表。安装完成后提示重启Eclipse即安装成功。
  4. 下载Android SDK,并解压(路径不要有中文)。在Eclipse中选择Window–>preferences。在左边栏选中Android,在SDK Location中填入你解压文件夹的根目录,点击apply和OK,则Android环境配置基本完成。这是你的菜单栏应该有了这两项
    这里写图片描述
  5. 更新SDK。点击Window–>Android SDK manager,弹出如下界面:
    这里写图片描述
    可能由于墙的问题会弹出列表无法加载,需要用代理或镜像,当然如果你有梯子也可以。这里介绍些镜像吧:
    g.cn:80,在SDK Manager 中点击tools–>options:
    ORB_SLAM2在Android上的移植过程_第1张图片
    按照上图设置即可。其他的镜像源还有北京化工大学镜像站,请自行谷狗。设置好后点击Packages–>reload即可得到对应的镜像。有默认选中的一些选项,一般选中一个sdk tools,一份sdk build-platform-tools,一个sdk build tools,然后一个Android版本即可(一般4.0以上)。选中后点击Install就会从镜像中下载对应的SDK,这个过程有长有短,视网络而定。
    当上述步骤都完成后,Android开发环境就配置好了。
  6. 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,出现如下界面:
ORB_SLAM2在Android上的移植过程_第2张图片
名字就是最后我们要生成的库的名字,随便填,可修改。点击确定就会给你的工程添加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    := NDKTestLOCAL_SRC_FILES := NDKTest.cppinclude $(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_staticAPP_CPPFLAGS := -frtti -fexceptions NDK_TOOLCHAIN_VERSION := 4.8APP_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 __cplusplusextern "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_SLAM2在Android上的移植过程_第3张图片
分为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.mkLOCAL_PATH := $(MAINDIR)include $(CLEAR_VARS)MAINDIR:= $(LOCAL_PATH)LOCAL_MODULE:= lapackLOCAL_SHORT_COMMANDS := trueLOCAL_STATIC_LIBRARIES := tmglib clapack blas f2cLOCAL_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官网进行下载。其目录结构如下:
    ORB_SLAM2在Android上的移植过程_第4张图片
    其中sdk为核心部分,opencv4Android包含两个版本,一个是opencv为java做的本地化sdk,另一个是opencv利用ndk编译C++版本得到的库工程。我们将opencv4android解压后放置到ORB_SLAM2项目的同级目录下,如下所示:
    这里写图片描述
    之后在jni目录下的Android.mk中需要引用到OpenCV的地方加入如下代码:
OPENCV_LIB_TYPE:=STATICifeq ("$(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.mkelse  include $(OPENCV_MK_PATH)  endif 

这里opencv.mk我给的是绝对地址,其实相对地址也是可以的。上面这段引用会将opencv进行编译并引入到当前的工作模块上来,这里就完成了opencv库的基本调用。如果为了方便还可以将opencv自身单独编译成一个库工程并开放给其他模块引用。

其他libraries的编译过程和上述工程大同小异,其主要步骤可以概括如下:

  1. 将当前库复制到jni的特定目录下;
  2. 在Android.mk中新建一个模块并对模块进行命名;
  3. LOCAL_C_INCLUDE引入库的头文件,LOCAL_SRC_FILES引入库的cpp文件;
  4. LOCAL_LDLIBS/LOCAL_SHARED_LIBRARIES/LOCAL_STATIC_LIBRARIES引入依赖库;
  5. 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方法将其转换成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为bitmapint[] pix = new int[w * h];tmp.getPixels(pix, 0, w, 0, 0, w, h);

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为函数传递的图像参数}

C++部分:

const cv::Mat *im = (cv::Mat *) addr;//addr为传入的图像参数

结尾


当上述步骤都完成后,我们会得到最终生成的sdk。Android部分的布局文件和对应activity文件在这里也略过不表。当得到最终生成的apk后,我们如果要测试Camera模式,需要先将opencv4Android中apk文件夹中对应类型的opencv manager安装到手机中并预先打开才能使用,否则会提示找不到opencv的支持库;若只需测试Dataset模式则无需上述步骤。

水平有限,如有错误请不吝指正,谢谢。

更多相关文章

  1. Android通知MediaScanner扫描指定的文件
  2. android中XML文件系列(一)—Drawable中的XML
  3. android XMLPullParser读取xml文件
  4. Android压缩文件(压缩目录)
  5. android 从文件制定位置读取数据
  6. Android 文件缓存方法
  7. android http上传文件
  8. 深入分析android中用SAX解析XML文件并纠错

随机推荐

  1. 【Gradle】Android(安卓)Gradle 插件
  2. Android开发现状分析(2020版)
  3. mac下下载安装Android(安卓)Studio教程
  4. Android中对Handle机制的理解
  5. Android(安卓)数据保存
  6. MAC上使用maven打android的包,报错:No And
  7. Android--布局方式(LinearLayout)学习
  8. Android(安卓)数据存储(二) 文件的使用
  9. android browser 的几个小feature (五) A
  10. Android(安卓)笔试/面试,常见问题整理