Android(安卓)开发通过NDK调用JNI,使用opencv做本地c++代码开发配置方法 边缘检测 范例代码

来源:互联网 发布:知金教育集团 编辑:程序博客网 时间:2024/06/05 02:03

update by 24/1/2015

这篇文章写了一年多了,看到很多朋友在NDK/JNI开发环境的时候还是遇到了很多问题,所以今天抽了个时间,自己重新在ubuntu上配置了一下,更新一下本文中的错误。

并且,我把项目代码放在了github上,可以自己看看: https://github.com/weixsong/libra

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


以前写过两个Android开发配置文档,使用NDK进行JNI开发,这样能够利用以前已经写好的C++代码。

前两篇博客地址:

 http://blog.csdn.net/watkinsong/article/details/8829072

http://blog.csdn.net/watkinsong/article/details/8829235


但是这两篇配置介绍中,多少的有些错误,这里重新整理这些错误以及要注意的问题,作为勘误文。


简介:本系列博客介绍了安卓开发环境的配置,和在安卓开发中,通过JNI调用本地C++代码,使用opencv进行开发处理,本地代码通过NDK进行编译。


参考链接:http://www.cnblogs.com/ldr213/archive/2012/02/20/2359262.html 我最早学习是参考这个链接的,但是教程比较老,而且OPENCV现在都2.4.5的版本了,所以想总结一下分享给需要的朋友。(勘误:上面的连接博客中,提到你的工程的目录必须按照怎么样的目录结构存放,开始我做的时候也是按照他的那种存放方式放工程目录的,但是后来发现最新的NDK以及opencv没有必要这么做,局限性太大,只要你能配置环境把需要的文件找到即可。)


之所以需要使用NDK进行JNI java本地调用的原因是,很多实验室或者公司以前大部分的工作都是利用c/c++进行开发的,如果把这些代码使用java重写是不现实的,所以需要利用NDK调用公司已经存在的大量的c/c++代码。


目前OPENCV已经提供了Android 版本的API,如果你的工程完全是新的, 没有任何需要使用以前c/c++代码,那么还是建议你直接使用opencv Android版本的java API,使用NDK的效率并不一定会提高。本文主要是讲怎么利用NDK调用编译本地的c/c++代码。


请文明转载,声明出处。by watkins.song


有一种方式不需要自己配置所有的Sun JDK, Android SDK以及NDK,Eclipse等设置,使用已经配置好的开发套件就可以进行直接的开发,由NVIDIA开发的开发套件Tegra Android Development Pack能够直接设置好所有的开发环境,而且最新的版本还包含了OPENCV,不想自己配置的朋友可以直接下载这个套件。但是我本人没有尝试过使用这个套件,怎么使用也不明白,所以如果不想自己配置环境的话还是需要自己去看看这个套件的使用。



1. Sun JDK


首先需要安装java开发环境,这里必须使用Sun JDK,不能使用Opencv JDK. 安卓开发不支持Opencv JDK.

JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html

建议下载稳定版本的J2SE.

安装好 Sun JDK后需要按照java JDK的安装方式配置环境变量。

设置JDK系统环境变量:

在环境变量中添加如下内容

JAVA_HOME= C:\Program Files\Java\jdk1.X.XXX
Path=…..; %JAVA_HOME%\bin




2. Android SDK


安装安卓开发用的SDK,可以从  http://developer.android.com/sdk/index.html  这里下载最新的SDK。下载完毕后解压缩到一个不包含空格的目录即可。建议使清晰明了的目录,以后还要用。

建议将SDK安装到独立的文件夹中,文件夹名不要有空格,也不要起中文名字。

Android SDK 不用配置系统环境变量,在Eclipse中创建Android的工程时候或者安装完ADT(Android Development Tools)之后会提示配置Android  SDK 目录。这里只要保证目录名字不包含空格就可以了。


3. Eclipse


下载Eclipse作为开发用的IDE

下载地址:http://www.eclipse.org/downloads/

提示:最新发现下载的ADT中包含了最新版本Eclipse,可以不用下载。


4. Android Development Tools(ADT)


下载安卓开发工具包,包含一些常用的开发工具。

也可以直接使用Eclipse在线安装,但下载后再装比较方便,速度快。

下载地址:http://developer.android.com/sdk/eclipse-adt.html 


下载完ADT后,给Eclipse安装ADT组件。

在Eclipse中:菜单Help-->Install new Software




安装ADT时的截图如下:




这个时候会看到两类组件,一类是Develop Tools,还有就是NDT Plugins,NDT Plugins是本地编程编译工具,也就是用来编译本地C++代码的,建议将两组工具都全部安装。

特别说明:如果你需要做本地C++开发的话,一定要把NDT Plugins勾选上。(注释:安装的时候务必选择NDK Plugins)




5. 配置Eclipse


ADT安装完毕,应该可以在Eclipse工具栏和Window菜单上找到Android SDK管理器的图标




点击Preferences开始设置Eclipse的Android开发环境……




设置安卓开发的SDK目录,这里需要将SDK目录指定到刚才我们下载的Android SDK目录的根目录。




在Eclipse中选择Windows->Android SDK Manager,可以管理下载的SDK,也可以下载最新的SDK,用于不同的SDK平台开发。

选择你所需要开发的平台的SDK(我最早下载的那个SDK包含了很多版本的SDK,但是最新下载的最新的SDK,结果只包含了很好的Android 4.3的API,很多都需要自己下载)




6. 创建虚拟机


使用Android Virtual Device Manager管理和创建虚拟机,用于调试。

(配置虚拟机的时候,有的虚拟机配置可以选择是否模拟GPU,建议根据自己的配置需要进行测试,我有一次使用了模拟GPU,结果模拟器的图像显示完全不正常)








————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

上面部分主要介绍了Android开发环境的基本配置步骤,下面将要通过示例,讲解如何配置NDK进行本地JNI调用。


++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


7.  安装CDT(CDT plugin for Eclipse



Eclipse的CDT插件是用来在Eclipse进行C++开发的工具,如果你在配置安卓开发环境的时候安装ADT的过程中,已经选择了NDK Plugins,那么就不需要再进行安装了,因为NDK Plugins已经包含了CDT,如下图:


如果在安装的时候没有选择NDK插件,那么需要再次安装CDT。





8. Android NDK



访问 http://developer.android.com/sdk/ndk/index.html 

下载最新的Android NDK,是一个ZIP解压包,只需解压到某个路径即可,例如"F:\android-ndk-r8b-windows\android-ndk-r8b",再把这个路径添加到系统的环境变量PATH中。

update: 目前我使用的版本是:android-ndk-r10d


9. 安装Cygwin(可以选择性安装,通过命令行进行编译C++代码, 不建议使用)


如果你是在windows上作开发,可以选择安装,如果是在ubuntu上,则根本不需要安装。


在http://blog.csdn.net/watkinsong/article/details/8829235 这篇博客中,第三部分,介绍了使用Cygwin的方法,但是这里不推荐使用,所以如果你想使用的话,请参考上面链接中的博客配置方式。



10. OpenCV For Android



下载最新的opencv for android,

下载地址:http://sourceforge.net/projects/opencvlibrary/files/opencv-android/

安装完以后最好配置环境变量。


(不配置环境变量也可以,可以直接在eclipse中指定opencv头文件的包含目录)

注释: 最近仔细看了下opencv for android与opencv的区别,opencv4android也包含了opencv中的c++的头文件,所以如果你以前的c/c++代码使用了opencv的头文件,那么不用原来的opencv 也可以,因为opencv4android也有c/c++的头文件,只要你的工程配置能够找到这些头文件即可。另外,opencv4android中主要包含的是java版本的API, 都是.so链接库,.so 链接库是linux用的链接库文件。


***************

opencv4android中还包含了opencv.mk这样的一个make文件,这个文件对于编译本地opencv代码是非常重要的,如果你不想用opencv4android的SDK,但是也要把这个SDK中的opencv.mk这个文件复制到你的opencv目录或者其他目录,将来在 Android程序中配置NDK本地编译的时候需要使用这个文件。非常重要。

**********


11. 在Android中使用Opencv


使用opencv有两种方式,一种是使用opencv的java版本的API,但是这种方式不是通过本地调用实现的,全部都是java代码,所以这里先不讲,另外一种方式就是使用opencv的c++版本的API,将本地c++代码编译成.so链接库,然后在安卓开发中进行调用,本地cpp代码使用NDK进行编译。


11.1 安卓java代码


下面给出一个使用Canny算子检测边缘的本地代码调用的使用方式。

新建安卓项目,配置使用安卓API等信息,这里我的项目名称为HaveImgFun

然后修改界面控制文件res->layout->activity_have_img_fun.xml


<?xml version="1.0" encoding="utf-8"?>      <LinearLayout   xmlns:android="http://schemas.android.com/apk/res/android"       android:orientation="vertical"       android:layout_width="fill_parent"        android:layout_height="fill_parent"      >      <Button android:layout_height="wrap_content"           android:layout_width="fill_parent"           android:id="@+id/btnNDK"           android:text="使用C++ OpenCV进行处理" />      <Button android:layout_height="wrap_content"           android:layout_width="fill_parent"           android:id="@+id/btnRestore"           android:text="还原" />       <ImageView android:id="@+id/ImageView01"       android:layout_width="fill_parent"       android:layout_height="fill_parent" />        </LinearLayout>

在文件夹src下的com.testopencv.haveimgfun包中新建一个类用于包装使用了opencv c++代码的动态库的导出函数,类名为LibImgFun。
Eclipse会为你创建一个新的文件LibImgFun.java,将里面的内容改为:


package com.testopencv.haveimgfun;  public class LibImgFun {   static {            System.loadLibrary("ImgFun");           }          /**              * @param width the current view width              * @param height the current view height  */      public static native int[] ImgFun(int[] buf, int w, int h);   }

从上面的代码可以得知,我们的动态库名字应该为“libImgFun.so”,注意"public static native int[] ImgFun(int[] buf, int w, int h)"中的native关键字,表明这个函数来自native code。static表示这是一个静态函数,这样就可以直接用类名去调用。


修改功能代码,修改HaveImgFun.java的代码,代码内容如下:


package com.testopencv.haveimgfun;import android.app.Activity;import android.graphics.Bitmap;import android.graphics.Bitmap.Config;import android.graphics.drawable.BitmapDrawable;import android.os.Bundle;import android.widget.Button;import android.view.View;import android.widget.ImageView;public class HaveImgFun extends Activity {/** Called when the activity is first created. */ImageView imgView;Button btnNDK, btnRestore;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_have_img_fun);this.setTitle("使用NDK转换灰度图");btnRestore = (Button) this.findViewById(R.id.btnRestore);btnRestore.setOnClickListener(new ClickEvent());btnNDK = (Button) this.findViewById(R.id.btnNDK);btnNDK.setOnClickListener(new ClickEvent());imgView = (ImageView) this.findViewById(R.id.ImageView01);Bitmap img = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();imgView.setImageBitmap(img);}class ClickEvent implements View.OnClickListener {public void onClick(View v) {if (v == btnNDK) {long current = System.currentTimeMillis();Bitmap img1 = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();int w = img1.getWidth(), h = img1.getHeight();int[] pix = new int[w * h];img1.getPixels(pix, 0, w, 0, 0, w, h);int[] resultInt = LibImgFun.ImgFun(pix, w, h);Bitmap resultImg = Bitmap.createBitmap(w, h, Config.RGB_565);resultImg.setPixels(resultInt, 0, w, 0, 0, w, h);long performance = System.currentTimeMillis() - current;imgView.setImageBitmap(resultImg);HaveImgFun.this.setTitle("w:" + String.valueOf(img1.getWidth())+ ",h:" + String.valueOf(img1.getHeight()) + "NDK耗时"+ String.valueOf(performance) + " 毫秒");} else if (v == btnRestore) {Bitmap img2 = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();imgView.setImageBitmap(img2);HaveImgFun.this.setTitle("使用OpenCV进行图像处理");}}}}

注意:这里由于不同的项目名以及类名,可能在运行程序的时候提示某个类找不到,这就需要查看AndroidManifest.xml这个文件了, AndroidMainfest.xml代码示例:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.haveimgfun"    android:versionCode="1"    android:versionName="1.0" >    <uses-sdk        android:minSdkVersion="8"        android:targetSdkVersion="18" />    <application        android:allowBackup="true"        android:icon="@drawable/ic_launcher"        android:label="@string/app_name"        android:theme="@style/AppTheme" >        <activity            android:name="com.example.haveimgfun.HaveImgFun"            android:label="@string/app_name" >            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

上面的代码中指定了程序运行时需要实例化的类,
android:name="com.example.haveimgfun.HaveImgFun"

上面这句代码需要根据不同的项目名称以及类名进行修改,有时候会出现类找不到的错误提示。


11.2 C++代码


在项目中新建一个jni文件,用于放置该项目的所有cpp代码。
在jni文件夹下建立一个"ImgFun.cpp"的文件,内容改为下面所示:


#include <jni.h>#include <stdio.h>#include <stdlib.h>#include <opencv2/opencv.hpp>using namespace cv;IplImage * change4channelTo3InIplImage(IplImage * src);extern "C" {JNIEXPORT jintArray JNICALL Java_com_testopencv_haveimgfun_LibImgFun_ImgFun(JNIEnv* env, jobject obj, jintArray buf, int w, int h);JNIEXPORT jintArray JNICALL Java_com_testopencv_haveimgfun_LibImgFun_ImgFun(JNIEnv* env, jobject obj, jintArray buf, int w, int h) {jint *cbuf;cbuf = env->GetIntArrayElements(buf, false);if (cbuf == NULL) {return 0;}Mat myimg(h, w, CV_8UC4, (unsigned char*) cbuf);IplImage image=IplImage(myimg);IplImage* image3channel = change4channelTo3InIplImage(&image);IplImage* pCannyImage=cvCreateImage(cvGetSize(image3channel),IPL_DEPTH_8U,1);cvCanny(image3channel,pCannyImage,50,150,3);int* outImage=new int[w*h];for(int i=0;i<w*h;i++){outImage[i]=(int)pCannyImage->imageData[i];}int size = w * h;jintArray result = env->NewIntArray(size);env->SetIntArrayRegion(result, 0, size, outImage);env->ReleaseIntArrayElements(buf, cbuf, 0);return result;}}IplImage * change4channelTo3InIplImage(IplImage * src) {if (src->nChannels != 4) {return NULL;}IplImage * destImg = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 3);for (int row = 0; row < src->height; row++) {for (int col = 0; col < src->width; col++) {CvScalar s = cvGet2D(src, row, col);cvSet2D(destImg, row, col, s);}}return destImg;}


在上面的代码中,给出了简单的Canny算子检测边缘的代码,并且返回检测后的图像显示。
上面的代码中#include <jni.h>是必须要包含的头文件,#include <opencv2/opencv.hpp>是opencv要包含的头文件。


注释: 1. 

JNIEXPORT jintArray JNICALL Java_com_testopencv_haveimgfun_LibImgFun_ImgFun(JNIEnv* env, jobject obj, jintArray buf, int w, int h)</span></span>


这个函数名,必须与java代码中的包名以及类名,函数名完全一致,

Java_com_testopencv_haveimgfun_LibImgFun_ImgFun

分别表示了包,类,函数名,中间用_分开,这个是非常重要的,否则会提示找不到函数的异常 错误。


2.  eclipse很奇怪,可能是我的配置问题,我本来已经配置好了opencv的目录,但是如果不配置eclipse工程的包含目录,是找不到opencv头文件的。

#include <opencv2/opencv.hpp>

这行代码,如果不配置eclipse工程中的包含目录,找不到系统环境变量中的opencv目录,这个如果各位有解决办法,还请多多指教。

如果 给工程添加包含目录,只有添加了包含目录,才能找到对应的头文件:

这里包含的头文件的目录既可以是opencv4android的c++头文件目录,也可以是以前你已经配置好的opencv目录


下面的截图中,是我的项目的配置,这里需要包括NDK中的若干最新版本的头文件,以及一些标准的c/c++的头文件,其中,标准的c/c++的头文件,会在将android项目转换为c/cpp项目的过程中自动添加。请大家根据自己的系统配置以及文件存放目录对应的修改。原来的配置说明中使用的版本都太古老了。。。(下面第一张图是最新的配置截图,第二张图是原先的配置截图,放在这里仅供参考对比。)

特别注意:所以的配置中,都要让你的项目找到opencv的jni的目录,这样才能使用opencv的c/c++ 对应的头文件。





11.3 配置文件


然后再在jni下新建两个文件"Android.mk"文件和"Application.mk"文件,这两个文件事实上就是简单的Makefile文件。
使用NDK进行编译的时候,需要使用Android.mk和Application.mk两个文件。
Android.mk:

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  OPENCV_LIB_TYPE:=STATICifeq ("$(wildcard $(OPENCV_MK_PATH))","")  #try to load OpenCV.mk from default install location  include E:\java\OpenCV-2.4.5-android-sdk\sdk\native\jni\OpenCV.mk else  include $(OPENCV_MK_PATH)  endif  LOCAL_MODULE    := ImgFunLOCAL_SRC_FILES := ImgFun.cpp  include $(BUILD_SHARED_LIBRARY) 

Application.mk:

APP_STL:=gnustl_static  APP_CPPFLAGS:=-frtti -fexceptions  APP_ABI:=armeabi armeabi-v7a </span>

在Android.mk文件中,需要主要修改的代码是如下一行:

include E:\java\OpenCV-2.4.5-android-sdk\sdk\native\jni\OpenCV.mk

这里要指定到opencv.mk这个文件,否则在NDK进行编译本地c/c++ 文件得时候会提示你找不到opencv.mk这个文件。不用你把opencv.mk放到哪里,只要用绝对或者相对目录加载进来就可以。

特别注意:这里一定要在Android.mk文件中包含正确的opencv中的OpenCV.mk文件,因为这个OpenCV.mk文件配置了如何使用opencv中的动态链接库。并且通过这个文件把opencv中的c/cpp的链接库文件复制到了android项目中,在anroid项目运行的时候才能找到本地的native code对应的函数。也就是说,通过OpenCV.mk文件,在编译本地c/cpp文件的时候才能找到本地c/cpp代码所使用的opencv函数的链接库。

但是,因为我之前是在Windows上作开发,所以混合使用native code和 opencv 的 java SDK是没有问题的,不用手动copy opencv4android 的libopencv_java.so文件也能自动的把这个文件复制过去,但是目前我在ubuntu上配置的时候,发现最大的一个问题就是当混合使用native code和java sdk的时候, 没有自动的给android项目添加 libopencv_java.so这个库文件,而且当手动把文件添加到android项目的libs目录后NDK在 build本地代码的时候会自动把手动添加的库文件删了。。。WTF, 这个问题困扰了我好久,也找了很多资料,但是都不是太理想,最后还是看NDK的文档把问题解决了,这里真心不知道为什么在Ubuntu下配置使用opencv java SDK会有问题,不会自动的复制.so文件。。。

目前,仅仅使用native code不需要关心上面提出的问题,上面的问题会单独写一个blog给出解决方法。


然后需要使用LOCAL_SRC_FILES包含需要编译的文件。所有的c/c++ 文件都要分别列出来。

LOCAL_MODULE    := ImgFun
上面一行代码用来指定生成的链接库的名称。


11.4 编译本地C++代码


编译本地C++代码可以使用Cygwin进行编译,cd 到项目目录,然后运行ndk-build
也可以使用windows控制台进行编译,同样cd到项目目录,运行ndk-build
还可以使用Eclipse进行编译,建议配置使用Eclipse进行编译,这样当项目的本地cpp代码发生变化的时候就可以实现自动的cpp代码编译,不用每次都在命令行中手动的进行编译,虽然使用黑乎乎的命令行手动编译,输出一堆信息显着很牛逼的样子。


(以下内容,如果使用cygwin进行编译,则不需要进行操作,直接使用cygwin或者命令行进行编译,保证编译通过以后即可运行程序,如果选择使用Eclipse自动进行编译,则参考以下内容进行配置)

首先需要将该项目转换到C++项目,使得该项目具有C++代码属性,如下所述。
点击项目,右击,New -> Other -> C/C++ -> Convert to a C/C++ Project.








配置Eclipse对cpp代码进行编译:
首先需要给当前项目添加一个编译环境变量
如下目录
open Eclipse menu Window -> Preferences -> C/C++ -> Build -> Environment,
点击Add... 
添加一个NDKROOT,并且设置值为NDK的根目录。
然后设置编译的一些参数
Project Properties -> C/C++ Build, uncheck Use default build command, replace “Build command” text from "make" to
"${NDKROOT}/ndk-build.cmd" on Windows,
"${NDKROOT}/ndk-build" on Linux and MacOS.



然后修改Behaviour选项卡,设置编译配置(配置在保存代码的时候进行自动编译):




点击确定,然后确认NDK(ndk-build)编译能够正常进行编译,
可以看到下图:



这个时候,会在C++代码中,看到非常多的错误提示,遍地都是错误提示,这里不要慌,这里只是假的错误提示,编译cpp代码能够编译通过,但是运行程序是不行的,会提示你代码有错误,需要解决这些问题。




打开工程属性,Project Properties -> C/C++ General -> Paths and Symbols

为GNC C++编译器添加如下路径:(这里添加的路径就是NDK 中的c/c++ 头文件的路径)


# for NDK r8 and prior:${NDKROOT}/platforms/android-9/arch-arm/usr/include${NDKROOT}/sources/cxx-stl/gnu-libstdc++/include${NDKROOT}/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/include${ProjDirPath}/../../sdk/native/jni/include

# for NDK r8b and later:${NDKROOT}/platforms/android-9/arch-arm/usr/include${NDKROOT}/sources/cxx-stl/gnu-libstdc++/4.6/include${NDKROOT}/sources/cxx-stl/gnu-libstdc++/4.6/libs/armeabi-v7a/include${ProjDirPath}/../../sdk/native/jni/include

然后就会看到所有的错误都消失了,这样重新编译本地cpp代码,然后就可以运行工程了。


注释:上面的头文件配置路径都是比较古老的了,最新的配置请看下图:



终于可以运行程序了,可以看到本程序的截图如下:(由于使用的虚拟机,所以运行速度比较慢)







**************************************************************************************************************************************************************************************

注释:上面的说明都是用的opencv  的c/c++版本的头文件以及代码,如果你用opencv4android中提供的例子,例子里面都用到opencv4android的java版本的API,这样你需要给工程配置Library,才能编译通过,我在最初的尝试中,都指定了API,但是一会API那个路径就变成叉叉了,后来发现,eclipse中必须要把libray那个工程加入进去,才能正确的加载library, 这样,你的eclipse必须把opencv4android中的Android版本的library那个工程加进去才可以。我用的opencv4android 2.4.6的版本, 这个版本的library工程名称为OpenCV Library - 2.4.6,必须在eclipse中把这个工程导入才可以成功的引用opencv4android 的java版本SDK

所有的引用 import org.opencv.core.Rect; 这种类型的包的文件,都说明这个工程包含了opencv4android的java版本的API,需要配置library.

而且还需要配置Android SDK 版本(否则编译提示出错)



Q & A:


在博客评论中,困扰很多人的问题有:

1. 很多人提到了cbuf = env->GetIntArrayElements(buf, false); 编译不过,这个应该是NDK版本的问题,其实我也没有仔细去查资料,到底是什么问题,总之把问题解决了就可以了,我在重新配置的时候也遇到了这个问题,无非就是传入的参数和函数可以接收的参数不匹配,那么给它传一个匹配的就好了。

现在改成了:
cbuf = env->GetIntArrayElements(buf, NULL);
这样就可以编译通过了
原创粉丝点击