AS上利用NDK——CMake方法移植ORB SLAM算法到Android

概述

本文参考:http://blog.csdn.net/martin20150405/article/details/54692386
我将要移植的ORB SLAM算法是一种基于Eigen/ g2o/ DBoW2/ OpenCV外部库,在VS上通过C++11编写的算法。

相对于参考文的方法,我要实现在工程中只添加算法本身的cpp代码,不添加相关库的.cpp/.h代码,即只添加.a/.so文件。

鉴于我上篇文章已实现了将OpenCV算法移植到Android端,剩下的难点将包括以下3部分:
1、Eigen/ g2o/ DBoW2等外部库的移植;
2、.jpg/ .yaml/ .txt等初始化读取
3、C++算法程序为适应Jni进行的改写。
接下来分别解决这3个部分。

1、外部库移植

要移植Eigen/ g2o/ DBoW2这三个库,因为g2o使用了Eigen,而DBoW2使用了OpenCV,故应先移植Eigen。首先回顾一下openCV是怎么移植到Android上去的。

回顾分析opencv

opencv官方网址:https://opencv.org/releases.html
官网上专门提供了Andorid的安装包,不过那主要是为了在Andorid上通过java编写opencv,速度比较慢。要想速度快,就必须用native (C++)编写。打开 …\OpenCV-android-sdk\sdk\native\libs\arm64-v8a,可以看到其为native方法准备的库。
AS上利用NDK——CMake方法移植ORB SLAM算法到Android_第1张图片
其中.a文件时静态库,.so文件是动态库,一般的,用动态库的apk,体积小但运行速度慢;用静态库的apk,体积大但运行速度快。
在CMakeList.txt文件中有以下句子:

....
set(OpenCV_DIR D:/OpenCV/cv33/opencv-3.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni)
find_package(OpenCV REQUIRED)
....

在D:/OpenCV/cv33/opencv-3.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni路径下的文件中有OpenCVConfig.cmake、“include”、“abi-${ANDROID_NDK_ABI_NAME}”这三个文件/文件夹,
AS上利用NDK——CMake方法移植ORB SLAM算法到Android_第2张图片
分别表示CMake的编译方式、include头文件、链接的库文件。所以我估计,

其他外部库添加到android NDK端使用都可以在CMakeList文件中组织:a/ include 本地电脑上的头文件;b/ 链接.a/ .so库文件。

Eigen移植

Eigen官方网址:http://eigen.tuxfamily.org/index.php?title=Main_Page
参考:http://blog.csdn.net/u010154424/article/details/50975711
去官网、及相应博客上观看可知,Eigen只有.h头文件,可以在任何平台使用(包括Android)。故只需要在CMakeList添加
include_directories(“Eigen本地电脑的安装目录”)
即可。
由于Eigen中可能用到C++11语言,需要在app的build.gradle中添加

android{
...
    defaultConfig{
        ...
        externalNativeBuild{
            cmake{
                cppFlags "-frtti -fexceptions -std=c++11"
                }
            }
        }
    }

g2o移植

g2o官方网址: https://github.com/RainerKuemmerle/g2o
可以看到下载的g2o库都是源码,需要你再本地CMake编译相关的库。因为在其路径下有g2o/script/android.toolchain.cmake,所以可以通过Cmake控制台输入

mkdir build
cd build
cmake -G"Visual Studio 12 2013" -DCMAKE_TOOLCHAIN_FILE=../script/android.toolchain.cmake -DANDROID_NDK=D:/Android/AndroidStudio/sdk/ndk-bundle -DCMAKE_BUILD_TYPE=Release -DANDROID_ABI="arm64-v8a" -DEIGEN3_INCLUDE_DIR="D:/SLAM_ThirdLib/eigen3/software" -DEIGEN3_VERSION_OK=ON .. && cmake --build .

来生成.a和.so的库。但其对于-G<你的编译器>参数比较难设置,对于Visual Studio 12 2013 的IDE可能还需要NVIDIA Nsight Tegra Visual Studio Edition编译器。
我发现一种更简单地生成android库文件的方法——直接在Android Studio中编辑CMakeList来生成。

AS中使用CMakeList生成.a/ .so

先将g2o的源码(一个文件夹G2O)复制到AS工程的cpp文件夹下src/main/cpp/ThirdParty/G2O。
这样就可以直接在CMakeList中添加

#include Eigen的头文件
include_directories(D:/SLAM_ThirdLib/eigen3/software)
#include g2o的头文件
include_directories(src/main/cpp/ThirdParty/G2O
       src/main/cpp/ThirdParty/G2O/g2o)
#生成g2o的静态库
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/G2O/g2o/core g2oCore)
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/G2O/g2o/solvers g2oSolvers)
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/G2O/g2o/stuff g2oStuff)
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/G2O/g2o/types g2oTypes)
add_library(g2o STATIC
            ${g2oCore} ${g2oSolvers} ${g2oStuff} ${g2oTypes})
#确定生成静态库的路径(静态库ARCHIVE_OUTPUT_DIRECTORY  动态库LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(g2o PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../libs2/${ANDROID_ABI})
set_target_properties(g2o PROPERTIES CLEAN_DIRECT_OUTPUT 1)

然后在native-lib.cpp文件中随便写一个使用g2o库的程序测试一下,然后点击run。
这样就能在app/libs2的文件夹下找到g2o的静态库。
AS上利用NDK——CMake方法移植ORB SLAM算法到Android_第3张图片

AS通过CMake使用.a/ .so

打开Project目录树,在app下有个libs文件夹(和CMakeList在同一目录),便可将之前生成在libs2文件夹内的libg2o.a库文件复制到libs文件夹下(当然,如果你之前直接就将库文件生成在libs文件夹下,就更方便了)。然后在CMakeList文件中添加

#include Eigen和g2o的头文件
include_directories(D:/SLAM_ThirdLib/eigen3/software
                    D:/SLAM_ThirdLib/g2o)
#添加g2o库文件
add_library(g2o STATIC IMPORTED)
set_target_properties(g2o PROPERTIES IMPORTED_LOCATION ${libs}/libg2o.a)
...
#将g2o库链接起来
target_link_libraries( native-lib
                       g2o
                       ${log-lib})

这样就能直接使用.a/.so库了。

DBoW2移植

DBoW2官方网址:https://github.com/dorian3d/DBoW2 ; http://webdiis.unizar.es/~dorian/index.php?p=32
同g2o类似,不过因其依赖opencv,所以需要先链接opencv库,然后才能生成DBoW2库。代码如下:

CMakeList生成.a/ .so

#先添加OpenCV库,已自动include其头文件
set(OpenCV_STATIC ON)
set(OpenCV_DIR D:/OpenCV/cv33/opencv-3.3.0-android-sdk/OpenCV-android-sdk/sdk/native/jni)
find_package(OpenCV REQUIRED)

#include DBoW2的头文件
include_directories(src/main/cpp/ThirdParty/DBoW)
...
#在libs2文件夹下生成DBoW2的动态库
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/DBoW/DBoW2 DBoW2)
AUX_SOURCE_DIRECTORY(src/main/cpp/ThirdParty/DBoW/DUtils DUtils)
add_library(DBoW SHARED ${DBoW2} ${DUtils})
target_link_libraries(DBoW ${OpenCV_LIBS})#链接opencv库
set_target_properties(DBoW PROPERTIES
                      LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/libs2/${ANDROID_ABI}
                      ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})
set_target_properties(DBoW PROPERTIES CLEAN_DIRECT_OUTPUT 1)

CMake使用.a/ .so

#include DBoW2的头文件
include_directories(D:/SLAM_ThirdLib/DBoW2)
...
#添加DBoW2库文件
add_library(DBoW STATIC IMPORTED)
set_target_properties(DBoW PROPERTIES
        IMPORTED_LOCATION  ${libs}/libDBoW.a)
...
#将DBoW2库链接起来
target_link_libraries( native-lib
                       slam ...)

至此ORB SLAM所需要的外部库已全部移植到Android平台上了。

2、初始化文件读取

初始化文件包括一个jpg、一个txt、一个yaml,除yaml外,其他两个2文件均大于2MB,故不能使用res/raw的方法读取。

读取资源的2种方式

有2种方式:res/raw 和 Assets。

1、通过res/raw方式

在编译阶段就直接读取了,可通过R.raw.XXX方式读取,但不能读取大小超过>1MB的文件。例如:
File yamlFile= new File(dir,”ins2.yaml”);
InputStream yamlInput=getResources().openRawResource(R.raw.ins);

int length=yamlInput.available();
byte[] bufffer1=new byte[length];
yamlInput.read(bufffer1);
OutputStream out=new FileOutputStream(yamlFile);
out.write(bufffer1);
out.flush();
out.close();
yamlInput.close();

2、通过Assets方式

运行时再读取,通过getAssets()进行读取,大小没有限制,可通过inputStream.read(buffer)来进行分段读取。
InputStream inputStream=getAssets().open(yamlFile.toString);
OutputStream outputStream=new FileOutputStream(yamlFile);
byte[] buffer=new byte[2048];
int len;
while ((len=inputStream.read(buffer)) > 0){
outputStream.write(buffer,0,len);}
inputStream.close();
outputStream.flush();
outputStream.close();

故可以在main文件夹下新建assets文件夹
AS上利用NDK——CMake方法移植ORB SLAM算法到Android_第4张图片
然后把相应的文件放入该文件夹下。
由于Apk每次运行时都需要在Jni中读取,最好的办法就是直接把相关文件存在手机的外部存储路径中,然后由Jni中直接由路径读取。

将assets里的资源文件存入手机的外部存储中

//存储路径设置
File dir= getExternalFilesDir(null);
yamlFile= new File(dir,"ins.yaml");
txtFile= new File(dir,"ORBvoc.txt");
jpgFile= new File(dir,"stereotype.jpg");
if (!(yamlFile.exists() & txtFile.exists() & jpgFile.exists())){
   //apk内资源名称首字母不能大写,读取文件设置
   copyAssets("ins.yaml",yamlFile);
   copyAssets("namecard.jpg",jpgFile);
   copyAssets("orbvoc.txt",txtFile);
}
...
private void copyAssets(String fileName,File file){
        try {
            InputStream inputStream=getAssets().open(fileName);
            OutputStream outputStream=new FileOutputStream(file);
            byte[] buffer=new byte[2048];
            int len;
            while ((len=inputStream.read(buffer)) > 0){
                outputStream.write(buffer,0,len);
            }
            inputStream.close();
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
}

将路径传入Jni,Jni中直接读取文件

java文件中

static {System.loadLibrary("native-lib");}
public native void initialSLAM(String ymlPath,String txtPath,String jpgPath);

cpp文件中

JNIEXPORT void JNICALL
Java_eigeng2odbow_slamwjh_MyNDKOpencv_initialSLAM(JNIEnv *env, jobject instance,
                                                  jstring ymlPath_,
                                                  jstring txtPath_,
                                                  jstring jpgPath_) {
    const char *ymlPath = env->GetStringUTFChars(ymlPath_, 0);
    const char *txtPath = env->GetStringUTFChars(txtPath_, 0);
    const char *jpgPath = env->GetStringUTFChars(jpgPath_, 0);

    //可设置断点依次检查是否正确读取路径下文件
    cv::FileStorage fp;
    fp.open(ymlPath, cv::FileStorage::READ);
    cv::Mat cameraMatrix,distCoeffs;
    float error;
    fp["cameraMatrix"] >> cameraMatrix;
    fp["distcoeffs"] >> distCoeffs;
    fp["reprojection"] >> error;

    std::ifstream f;
    f.open(txtPath);
    std::string s;
    std::getline(f,s);
    f.close();

    cv::Mat image = cv::imread(jpgPath);

    env->ReleaseStringUTFChars(ymlPath_, ymlPath);
    env->ReleaseStringUTFChars(txtPath_, txtPath);
    env->ReleaseStringUTFChars(jpgPath_, jpgPath);
}

这样就能实现在程序中读取较大的初始化文件了。

C++程序jni化改写

由于C++算法程序涉及到保密,就不予以展示了。对于由VS的控制台C++程序的改写,大体上注意事项如下:
1、路径符号由’\’转为’/’。
2、注意消除main函数,将其.cpp文件改为.h。
3、编码为ANSI或 UTF-8无BOM。
4、注意全局变量的自动内存回收,最好使用指针,通过new来创建内存区域,然后通过delete来回收内存区域。

调试方法

LLDB

如果使用AS,其自带LLDB,可直接在cpp程序中设置断点,查看运行到该断点处时各变量值。

打印Log

使用AS的也可通过观察log来显示程序运行时各变量的变化,通过添加自定义的头文件(如JniLog.h)。

#ifndef SLAMWJH_JNILOG_H
#define SLAMWJH_JNILOG_H

#include <android/log.h>
#define TAG "native" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型


#endif //SLAMWJH_JNILOG_H

然后只要在需要打印log的cpp文件前面添加#include "JniLog.h",就能在程序中随意的添加如
LOGD("operator:nlevels=%d",nlevels);
LOGD("operator:mvInvScaleFactor[0]=%f",mvInvScaleFactor[0]);

这样的log显示指令了。程序运行时就可在Logcat中通过筛选“native”查到相应的log展示了。如果直接在手机上启动app,则可在Run中查看。
程序运行结果

你可能感兴趣的:(使用心得,OpenCV,ndk学习)