虽然网上有不少资料,但大多是老版本的,新版本Android Studio对ndk作了改进,采用cmake构建,简单了许多。下面示例基于Android Studio2.3版本,关于如何编译请参考上一篇http://blog.csdn.net/efanlee/article/details/69944267
一、新建工程
1、创建CvNative工程,注意要选上include C++ support
在定制Activity时,我习惯把下面这个勾去掉,直接采用简单的Activity基类。
C++设置页面中,将下面两项勾选,否则构建会失败。当然,后面再改CMakeLists.txt也是等效的。
2、不同于java方式需要导入一个module,jni方式只要把libs目录拷到项目里。将编译好的opencv4android/sdk/native/libs目录整个复制到CvNative\app\src\main目录下,并改名为jniLibs
3、将opencv4android/sdk/native/jni/include目录整个复制到CvNative下
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;
}
三、效率对比
从刚才简单的测试代码来看,每次显示需要创建两个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提升越明显,可能需要测试混合场景才能体现出来。
但不管怎样,帧数实在太低,如果换成高配手机,显然无法实现廉价的方案,下一步要试一下树莓派。
参考资料:
http://blog.csdn.net/martin20150405/article/details/53284442
http://blog.csdn.net/sbsujjbcy/article/details/49520791(这里用的是make的方式,资料有点旧,仅参考)