Android(安卓)开发通过NDK调用JNI,使用opencv做本地c++代码开发配置方法 边缘检测 范例代码
以前写过两个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中。
9. 安装Cygwin(可以选择性安装,通过命令行进行编译C++代码, 不建议使用)
在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; @Override public 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文件夹下建立一个"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;
}
上面的代码中#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)
这个函数名,必须与java代码中的包名以及类名,函数名完全一致,
Java_com_testopencv_haveimgfun_LibImgFun_ImgFun
分别表示了包,类,函数名,中间用_分开,这个是非常重要的,否则会提示找不到函数的异常 错误。
2. eclipse很奇怪,可能是我的配置问题,我本来已经配置好了opencv的目录,但是如果不配置eclipse工程的包含目录,是找不到opencv头文件的。
#include <opencv2/opencv.hpp>
这行代码,如果不配置eclipse工程中的包含目录,找不到系统环境变量中的opencv目录,这个如果各位有解决办法,还请多多指教。
如果 给工程添加包含目录,只有添加了包含目录,才能找到对应的头文件:
这里包含的头文件的目录既可以是opencv4android的c++头文件目录,也可以是以前你已经配置好的opencv目录
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:=STATIC ifeq ("$(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 := ImgFun LOCAL_SRC_FILES := ImgFun.cpp include $(BUILD_SHARED_LIBRARY)
Application.mk:
APP_STL:=gnustl_static
APP_CPPFLAGS:=-frtti -fexceptions
APP_ABI:=armeabi armeabi-v7a
在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放到哪里,只要用绝对或者相对目录加载进来就可以。
然后需要使用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 版本(否则编译提示出错)