VR/AR动手玩(三):Android使用jni调用opencv

虽然网上有不少资料,但大多是老版本的,新版本Android Studio对ndk作了改进,采用cmake构建,简单了许多。下面示例基于Android Studio2.3版本,关于如何编译请参考上一篇http://blog.csdn.net/efanlee/article/details/69944267

一、新建工程

1、创建CvNative工程,注意要选上include C++ support
VR/AR动手玩(三):Android使用jni调用opencv_第1张图片
在定制Activity时,我习惯把下面这个勾去掉,直接采用简单的Activity基类。
VR/AR动手玩(三):Android使用jni调用opencv_第2张图片
C++设置页面中,将下面两项勾选,否则构建会失败。当然,后面再改CMakeLists.txt也是等效的。
VR/AR动手玩(三):Android使用jni调用opencv_第3张图片

2、不同于java方式需要导入一个module,jni方式只要把libs目录拷到项目里。将编译好的opencv4android/sdk/native/libs目录整个复制到CvNative\app\src\main目录下,并改名为jniLibs
这里写图片描述

3、将opencv4android/sdk/native/jni/include目录整个复制到CvNative下
VR/AR动手玩(三):Android使用jni调用opencv_第4张图片

4、修改app的gradle,在defaultConfig内添加ndk的配置(ndk部分),主要是指定编译哪些abi:

externalNativeBuild {
  cmake {
    cppFlags "-frtti -fexceptions"
  }
  ndk {
    abiFilters "armeabi", "armeabi-v7a"
  }
}

注意刚才勾选的rtti和exceptions在这里体现了,如果需要,还可以加上-std=gnu++11。修改完后,同步一下配置。

5、修改CMakeLists.txt文件,如果是Android视图,应该在External Build Files下。
这一步主要是为了增加头文件路径,以及库文件。
在cmake_minimum_required之后,增加下面配置:

include_directories(../include)

由于CMakeLists.txt文件在CvNative/app目录下,所以../include表示CvNative/include目录,即上面步骤3中复制的include路径——如果你把include目录复制到其他地方,请修改这里。
继续添加

set(libs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")

add_library(libopencv_java3 SHARED IMPORTED )
set_target_properties(libopencv_ java3 PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_ java3.so")

add_library(libopencv_world STATIC IMPORTED )
set_target_properties(libopencv_world PROPERTIES
    IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_world.a")

add_library(libopencv_contrib_world STATIC IMPORTED )
set_target_properties(libopencv_contrib_world PROPERTIES
IMPORTED_LOCATION "${libs}/${ANDROID_ABI}/libopencv_contrib_world.a")

解释一下,首先是设置libs变量为jniLibs的路径,然后增加libopencv_java3、libopencv_world和libopencv_contrib_world三个库,并指定类型分别为共享和静态库,指定位置为相应的.so及.a文件。

这里有两点需要注意:
首先,第一行设置了libs变量,虽然这里看似可以设置任意的目录存放库文件,但实际使用中,必须使用src/main/jniLibs。否则虽然能够编译通过,但在运行时会提示找不到库。
其次,经过测试,libopencv_java3.so是必须的。缺少libopencv_java3.so支持时,链接时会报错,提示找不到carotene引用。网上说需要libtegra_hal.a,这个文件在3rdparty里,但单独引用此库并不能解决问题(链接问题能解决,但运行时缺少so)。目前测试只有提供libopencv_java3才管用。
但之前编译时提到,如果勾了world选项,就不会编译java组件。为解决这个问题,可以每种架构编译两遍,一次是用world选项,复制libopencv_world.a及libopencv_contrib_world.a两个文件,第二遍不选world,只复制libopencv_java3.so。但这样非常麻烦,时间和空间都要增加1倍,而且与PC版不同,Android版的world编译并不能有效减少占用空间,仅仅是方便在CMakeLists.txt少写几句配置(如果不用world,则每个lib库文件都要add一遍),是否值得,还要权衡一下

接下来在原来target_link_libraries(通常在文件末尾)的基础上,添加刚才add进去的三个lib文件libopencv_java3、libopencv_world及libopencv_contrib_world:

target_link_libraries( # Specifies the target library.
                       native-lib
                       -ljnigraphics
                       libopencv_java3 libopencv_world libopencv_contrib_world

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

新增的-ljnigraphics是为了后面引用AndroidBitmap添加的,否则会报链接错误。
修改完后,同步一下配置。如果一切顺利,同步配置后能看到BUILD SUCCESSFUL提示。

补充说明:
后来看了一下资料,其实应该使用find_library而不是add_library。因为add_library主要是用于本项目的代码生成library,而find_library才是使用准备好的库(通常是第三方)。但是测试时使用find_library一直未能成功,后来找到似乎是Android做过改动,要加一个CMAKE_FIND_ROOT_PATH_BOTH参数,例如:

find_library(OPENCV_JAVA NAMES libopencv_java3.so PATHS "${libs}/${ANDROID_ABI}" CMAKE_FIND_ROOT_PATH_BOTH)

然而libopencv_java3.so的问题虽然解决了,但引入libopencv_world.a的时候又会报找不到carotene库的错误,只好放弃。


二、编写测试代码
上面步骤自动生成的程序模板,自带一个native-lib.cpp,并编译为native-lib模块(参考CMakeLists.txt配置)。在MainActivity中,通过stringFromJNI方法获取”hello world”字符串并显示出来。
参考这种方式,稍作修改,测试opencv的调用。

1、 修改MainActivity.onCreate()如下:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ImageView imgView = new ImageView(this);
    setContentView(imgView);

    int w = 100, h = 100;
    Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    int[] pixels = new int[w * h];
    bmp.getPixels(pixels, 0, w, 0, 0, w, h);
    int[] result = cvprocess(pixels, w, h);
    Bitmap resultBmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    resultBmp.setPixels(result, 0, w, 0, 0, w, h);
    imgView.setImageBitmap(resultBmp);
}

2、 紧跟着stringFromJNI()后面,添加新的jni方法

    private native int[] cvprocess(int[] pixels, int w, int h);

并在native-lib.cpp实现如下:

extern "C"
JNIEXPORT jintArray JNICALL
Java_efan_cvnative2_MainActivity_cvprocess(JNIEnv *env,
                       jobject instance, jintArray pixels_, jint w, jint h) {

    jint *pixels = env->GetIntArrayElements(pixels_, NULL);
    cv::Mat img(w, h, CV_8UC4, pixels);
    cv::circle(img, cv::Point(w/2, h/2), w * 0.4, cv::Scalar(255, 0, 0, 255));
    jintArray result = env->NewIntArray(w * h);
    env->SetIntArrayRegion(result, 0, w * h, pixels);
    env->ReleaseIntArrayElements(pixels_, pixels, 0);
    return result;
}

3、 编译运行,可以看到屏幕上出现一个蓝色的小圆
VR/AR动手玩(三):Android使用jni调用opencv_第5张图片


三、效率对比
从刚才简单的测试代码来看,每次显示需要创建两个Bitmap对象,在java层需要new int[w * h]数组,JNI层还要再用env->NewIntArray(w*h),效率是比较低的。
实际使用时,从Camera.PreviewCallback接口获取byte[] data数据,要先转成YUV格式,再转成BGR,进行处理,最后转成RGBA格式的bmp,耗时更多。未经优化时,效率甚至比不上直接使用opencv封装好的java接口(最终也是调用JNI)。

于是参考了opencv自带java封装的实现(在Utils.matToBitmap方法),发现调用了AndroidBitmap_lockPixels和unlockPixels,可以节省一些内存的申请。修改代码如下:

1、 java部分(实现Camera.PreviewCallback的接口)

public void onPreviewFrame(byte[] data, Camera camera) {
    Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    jniProcess(data, bmp);
    imgView.setImageBitmap(bmp);
}

2、 JNI部分

extern "C"
JNIEXPORT void JNICALL
Java_com_example_efanlee_cvnative_CameraActivity_jniProcess(JNIEnv * env,
                                  jobject instance,
                                  jbyteArray buf, jobject bitmap,
                                  jint width, jint height) {

    jbyte *cbuf = env->GetByteArrayElements(buf, NULL);
    cv::Mat yuv(height + height / 2, width, CV_8UC1, cbuf);
    cv::Mat bgr;
    cv::cvtColor(yuv, bgr, CV_YUV2BGR_NV21); //转成BGR
    //TODO 处理bgr
    void *pixels;
    AndroidBitmap_lockPixels(env, bitmap, &pixels);
    cv::Mat result(height, width, CV_8UC4, pixels);
    cv::cvtColor(bgr, result, CV_BGR2RGBA);
    AndroidBitmap_unlockPixels(env, bitmap);
    env->ReleaseByteArrayElements(buf, cbuf, 0);
}

以轮廓、背景、keypoint三种场景下进行不严谨的对比测试,JNI比java接口有小幅度提升(测试手机是我的烂红米)。

场景 轮廓(fps) 背景(fps) 关键点(fps)
CvJava 12.188 8.127 2.626
CvNative 15.426 8.637 3.293
提升 +25.6% +6.3% +25.4%

这点提升很难令人满意,可能还是有一些优化没有做好(比如中间转换过程太多)。从理论上分析,应该是中间步骤越多,JNI提升越明显,可能需要测试混合场景才能体现出来。
VR/AR动手玩(三):Android使用jni调用opencv_第6张图片
VR/AR动手玩(三):Android使用jni调用opencv_第7张图片
VR/AR动手玩(三):Android使用jni调用opencv_第8张图片

但不管怎样,帧数实在太低,如果换成高配手机,显然无法实现廉价的方案,下一步要试一下树莓派。

参考资料:
http://blog.csdn.net/martin20150405/article/details/53284442
http://blog.csdn.net/sbsujjbcy/article/details/49520791(这里用的是make的方式,资料有点旧,仅参考)

你可能感兴趣的:(VR/AR动手玩(三):Android使用jni调用opencv)