目录:
1,过程感慨;
2,运行环境;
3,准备工作;
4,编译 .so
5,遇到的关键问题及其解决方法
6,实现效果截图。
(原创:转载声明出处:http://www.cnblogs.com/linguanh/)
1,过程感慨(想直接看教程,请跳过此部分)
在写具体内容之前,我先说下我搞这个东西的过程,由于导师之前说过要搞个图像匹配的androi APP,具体就是匹配前后两张图片的相似度,类似 安卓5.0 引入的刷脸解锁。
当时觉得,要实现这样一个东西,肯定没现成的API 可供使用,第一时间想到的 无疑就是opencv,这个拥有一套强大的图像处理函数的库,它的开发语言主要是C++,但是,也有 jar 包可供android开发使用,如果单单是使用里面已经写好了的效果的话,肯定是不能完成图像匹配的。
也就是说,我必须要调用它里面的函数再结合自己算法重新去实现这样一个功能,再使用 ndk 环境去实现 jni 编程,把我自己写好的 c++ 代码,在生成 .so 动态库的基础上,引入并使用。
刚开始,思路很清晰,然后便着手百度 android studio(下面简称 as) 的 opencv jni编程使用教程,十分遗憾,所能搜到的,关于 as 和 opencv、jni 搭边的例子 几乎为0,很多的例子是 eclipse。没办法,只有自己亲手搞了。
刚动手的时候,很快地把所有装备工作都搞定了,.so 动态库文件(下面会介绍)也编译出来了,但是,就在此时,我遇到了一个 令我第一阶段切底放弃的 bug!!
这个 bug 是:(下面我会说明白,它的真实起因和解决方法)
fatal error: opencv2/opencv.hpp: No such file or directory, 意思是 我所要编译的 cpp文件中的 头文件 opencv2/opencv.hpp 找不到。当时,无论是自己请教别人、百度、google 还是查书,都无法解决,足足耗时 一星期!!
逐保留项目信息,放弃不搞。
直到 2 天前,开始决定重新尝试,并于今天正式解决后,现发表此文。
2,运行环境
win 7, 系统;
android studio 版本 0.8.0 beta,使用 build:gradle:0.12.+,tools版本:21.1.2,api 21;
opencv for android 包,我使用的版本是 OpenCV-3.0.0-android-sdk,2.4.9的也可以,可以到 opencv 官网下载,我这里提供个链接
http://downloads.sourceforge.net/project/opencvlibrary/opencv-android/3.0.0/OpenCV-3.0.0-android-sdk-1.zip?r=http%3A%2F%2Fopencv.org%2F&ts=1436167636&use_mirror=nchc
编译.so 动态库 使用 cygwin,安装了所有包,这里提示,不一定要用它,可以直接使用 cmd 进行编译;
ndk 为 android-ndk-r10d(强烈建议使用 r9 或 r10 系列,因为这两个能在 cmd 中编译出 .so),r10d 能够支持的 android api 最高到 21,如果你的是 22 的请修改,否则会有会编译不出 jni.h 头文件,或者其他的头文件,你会发现,别人的源码在你这编译不出了。
3,准备工作
1,---ndk 的下载、安装和配置,此部分不说,网上教程很多,很多可行。
2,---cygwin 的下载和安装, 参照 http://blog.csdn.net/asmcvc/article/details/9311573,我上面说了,不一定要用它,win 自带的 cmd 也可以编译。如果使用 cygwin,要做好心理准备,下载和安装它,非常非常的久,文件总体积 20 多G!!!!我是用了9个多小时。
3,---opencv for android 的sdk 下载完成后。打开 该文件夹,sdk/native/libs,里面有很多平台的文件夹,能在里面出现的,证明你能够在下面的 Application.mk 中设置生成对应的架构的 .so文件,我举个例子,我的是:
在下面介绍的 Application.mk 文件中有一句话 ,它是用来设置生成 对应架构的 .so 文件,我这里是armeabi-7a,如果要生所有的,写出 :=all,注意,这样很可能会报错,错误信息是,某种架构找不到,所以,我要你看清楚,上面文件夹里面有哪些架构,这些 坑是网上找不到,如果你要生成两种,可以轮着来编译,第二次的编译,不同的架构是不会覆盖的。现在打开 sdk/native/jni,如无意外,里面肯定有个 文件叫做 OpenCV.mk,它就是我们在 android.mk 脚本文件中要引入 opencv C++库所要参照的文件。请用记事本 或者Notepad++ 打开。
4,---了解 Android.mk 和 Application.mk 文件的基本内容信息:下面我使用默认的 Android.mk 来说明,和我的例子的 Application.mk 来说明。
它们都是脚本文件。
Android.mk
Application.mk
4,编译 .so
使用你的 as 创建一个新项目,然后在你的 项目的 main 目录下创建一个一个 jni 文件夹,这样创建:
创建好了之后,是这样的:
首先编译 项目的头文件 .h,一般编译出来后,它的名字结构是:包名_类名.h
编译命令如下,请在你的 as 下面的 Terminal 里面输入:
然后在你的jni 文件夹下面 分别创建 Android.mk 、Application.mk 和你要编译的 .cpp 或者.c 文件,前两个的 内容可以模仿我上面介绍的, .cpp 我这里提供一个。
Android.mk 、Application.mk 、ImgFuncpp 分别如下,util.c 是空文件,之所以创建它是为了避免另外一个 bug,这不说:
Android.mk 文件如下
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) OPENCV_LIB_TYPE:=STATIC ifeq ("$(wildcard $(OPENCV_MK_PATH))","") include E:\OpenCV-3.0.0-android-sdk-1\OpenCV-android-sdk\sdk\native\jni\OpenCV.mk else include $(OPENCV_MK_PATH) endif LOCAL_MODULE := ImgFun LOCAL_SRC_FILES := ImgFun.cpp LOCAL_LDLIBS += -lm -llog include $(BUILD_SHARED_LIBRARY)
Application.mk 文件如下
APP_STL := gnustl_static APP_CPPFLAGS := -frtti -fexceptions APP_ABI := armeabi-v7a #这句是设置生成的cpu指令类型,提示,目前绝大部分安卓手机支持armeabi,libs下太多类型,编译进去 apk 包会过大 APP_PLATFORM := android-8 #这句是设置最低安卓平台,可以不弄
ImgFun.cpp 文件如下
1 #include <io_github_froger_jni_MyActivity.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <opencv2/opencv.hpp> 5 using namespace cv; 6 IplImage * change4channelTo3InIplImage(IplImage * src); 7 8 extern "C" { 9 JNIEXPORT jintArray JNICALL Java_io_github_froger_jni_MyActivity_ImgFun( 10 JNIEnv* env, jobject obj, jintArray buf, int w, int h); 11 JNIEXPORT jintArray JNICALL Java_io_github_froger_jni_MyActivity_ImgFun( 12 JNIEnv* env, jobject obj, jintArray buf, int w, int h) { 13 14 jint *cbuf; 15 cbuf = env->GetIntArrayElements(buf, false); 16 if (cbuf == NULL) { 17 return 0; 18 } 19 20 Mat myimg(h, w, CV_8UC4, (unsigned char*) cbuf); 21 IplImage image=IplImage(myimg); 22 IplImage* image3channel = change4channelTo3InIplImage(&image); 23 24 IplImage* pCannyImage=cvCreateImage(cvGetSize(image3channel),IPL_DEPTH_8U,1); 25 26 cvCanny(image3channel,pCannyImage,50,150,3); 27 28 int* outImage=new int[w*h]; 29 for(int i=0;i<w*h;i++) 30 { 31 outImage[i]=(int)pCannyImage->imageData[i]; 32 } 33 34 int size = w * h; 35 jintArray result = env->NewIntArray(size); 36 env->SetIntArrayRegion(result, 0, size, outImage); 37 env->ReleaseIntArrayElements(buf, cbuf, 0); 38 return result; 39 } 40 } 41 42 IplImage * change4channelTo3InIplImage(IplImage * src) { 43 if (src->nChannels != 4) { 44 return NULL; 45 } 46 47 IplImage * destImg = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 3); 48 for (int row = 0; row < src->height; row++) { 49 for (int col = 0; col < src->width; col++) { 50 CvScalar s = cvGet2D(src, row, col); 51 cvSet2D(destImg, row, col, s); 52 } 53 } 54 55 return destImg; 56 }
上面 .cpp 文件的有几句话要说明下,注意 .c 文件和 .cpp 文件是不一样的:
1,请用 extern "C" { } 包住 你要你的 c++ 函数体的定义和里面的变量,函数声明可以在外面。
2,JNIEXPORT jintArray JNICALL Java_io_github_froger_jni_MyActivity_ImgFun(JNIEnv* env, jobject obj, jintArray buf, int w, int h);
3,jintArray 是你定义的函数的返回值,我这里的是int数组,它在类型的前面有一个 j ,如果是字符串,那么就是 jstring,数组加上Array;
4,JNICALL Java 这句不变,所有都一样,注意java的 j 是大写;
5,io_github_froger_jni 这里是你的包名;
6,MyActivity 你的类名,要引用这个这里C++函数的类名;
7,ImgFun 是你要在java中调用的函数名字,哪些不用直接被调用的,不用写;
8,JNIEnv* env, jobject obj, 这个固定不变,第一个的意思是虚拟机引用,第二个是项目;
9,jintArray buf, int w, int h 函数的参数。
好了,上面该介绍的已经介绍完了,接下来是编译 .so 的正式操作(我这里使用cmd做例子,因为它更简单操作,cygwin也可以)。
你可以在 as 的 cmd 中或者 系统的 cmd框中实现编译,首先使用命令进入到当前的 jni 文件夹的 目录,例如,我的是
D:asproject/JniDemo/app/main/jni,然后使用命令 ndk-build,(使用ndk-build命令这一步,需要你已经配置好了 ndk 环境,请参照百度上面的教程)然后回车,如无意外,将会生成如下文件:
其中的 .so 文件就是我们所需要的,现在打开你项目app下的 build.gradle 文件,在 android{} 里面加入:
sourceSets { main() { jniLibs.srcDirs = ['src/main/libs'] } }
这样是为了使用 .so文件,上面我们仅仅是生产!
OK,到这里基本大功告成了,不过,笔者我就是在这一步之后,运行程序的时候,出现的简单的致命的 bug,导致我找了近2星期,现在想起来真是蠢..............
5,遇到的关键问题及其解决方法
运行程序,出现,如下错误,这里声明下,不仅仅是 opencv2/opencv.hpp,还可能是其他的 hpp。
出现的原因:
原来是这样的,android studio 在我们编译完 .so 文件后,我们在Android.mk 文件中设置引入的opencv 函数库,是已经被编译进去.so 动态库里面了的,而我们编译所需要的 cpp 文件,它在 jni 文件夹呢,自然就没有 opencv 库可依赖,所以。
解决方法:
在你编译完.so 文件后,就可以把 cpp 或者 c 文件里面的内容 注释或者删除了,不然在你运行程序的时候就会抛出头文件找不到的错误,哎,真是辛酸泪,这样一个 bug 搞了我 那么多时间,不过还好,还是解决了。
6,实现效果截图。
1 package io.github.froger.jni; 2 3 import android.app.Activity; 4 import android.graphics.Bitmap; 5 import android.graphics.drawable.BitmapDrawable; 6 import android.os.Bundle; 7 import android.view.View; 8 import android.widget.Button; 9 import android.widget.ImageView; 10 11 public class MyActivity extends Activity { 12 /** Called when the activity is first created. */ 13 ImageView imgView; 14 Button btnNDK, btnRestore; 15 public static native int[] ImgFun(int[] buf, int w, int h); 16 static { 17 System.loadLibrary("ImgFun"); 18 } 19 @Override 20 public void onCreate(Bundle savedInstanceState) { 21 super.onCreate(savedInstanceState); 22 setContentView(R.layout.activity_my); 23 24 this.setTitle("使用NDK转换灰度图"); 25 btnRestore = (Button) this.findViewById(R.id.btnRestore); 26 //btnRestore.setText(ImgFun()); 27 btnRestore.setOnClickListener(new ClickEvent()); 28 btnNDK = (Button) this.findViewById(R.id.btnNDK); 29 btnNDK.setOnClickListener(new ClickEvent()); 30 imgView = (ImageView) this.findViewById(R.id.ImageView01); 31 Bitmap img = ((BitmapDrawable) getResources().getDrawable( 32 R.drawable.ic_launcher)).getBitmap(); 33 imgView.setImageBitmap(img); 34 } 35 36 class ClickEvent implements View.OnClickListener { 37 public void onClick(View v) { 38 //btnRestore.setText(ImgFun()); 39 if (v == btnNDK) { 40 long current = System.currentTimeMillis(); 41 Bitmap img1 = ((BitmapDrawable) getResources().getDrawable( 42 R.drawable.ic_launcher)).getBitmap(); 43 int w = img1.getWidth(), h = img1.getHeight(); 44 int[] pix = new int[w * h]; 45 img1.getPixels(pix, 0, w, 0, 0, w, h); 46 int[] resultInt = ImgFun(pix, w, h); 47 Bitmap resultImg = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565); 48 resultImg.setPixels(resultInt, 0, w, 0, 0, w, h); 49 long performance = System.currentTimeMillis() - current; 50 imgView.setImageBitmap(resultImg); 51 } else if (v == btnRestore) { 52 Bitmap img2 = ((BitmapDrawable) getResources().getDrawable( 53 R.drawable.ic_launcher)).getBitmap(); 54 imgView.setImageBitmap(img2); 55 } 56 } 57 } 58 59 60 }