最近在做Android软件开发,手头有一些C、OpenCV版本的代码想移植到手机中,于是调查了OpenCV在Android中的使用方法,总结如下。
我使用的Android软件开发环境为Android ADT(Android Developer Tools),它包含了Android软件开发必备的开发插件,下载下来解压就能用。对于编译C/C++ Android Native代码开发,需要NDK,也是下载下来解压,在eclipse里配置一下路径即可,如下图(Window->Preferences)。
我用的最新的android-ndk-r10版本。对于Android开发环境的配置,网上介绍很多,这里就不详说了。
以开发边缘检测App为例,首先新建一个空工程
使用ADT Eclipse默认创建的新工程的onCreate函数如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.container, new PlaceholderFragment())
.commit();
}
}
工程的res/layout下还有一个fragment_main.xml,activity_main与fragment_main貌似是嵌套关系,使用上面原始代码有时候会出莫名其妙的问题,如在OnCreate中让一个ImageView显示一张图片
imgView.setImageBitmap(img);
程序运行到此就崩溃。刚接触Android开发不到2个月,不清楚这是什么原因,不过可以如下修改
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_main);
}
这样后面的程序就运行正常了。
创建好工程后,在界面上添加两个按钮和一个ImageView,fragment_main.xml修改如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.opencvhello.MainActivity$PlaceholderFragment" >
<Button
android:id="@+id/btnNDK"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="imgBtnClick"
android:text="Prosecc by C++ OpenCV" />
<Button
android:id="@+id/btnRestore"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="imgBtnClick"
android:text="Restore" />
<ImageView
android:id="@+id/ImageView01"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
在res/drawable下添加图片资源lena.jpg,然后添加如下代码
private static String TAG = "MainActivity";
ImageView imgView;
Button btnNDK, btnRestore;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_main);
// 设置标题
setTitle("使用NDK转换灰度图");
// 按钮句柄
btnRestore = (Button) findViewById(R.id.btnRestore);
btnNDK = (Button) findViewById(R.id.btnNDK);
// 图片句柄,显示图片
imgView = (ImageView) findViewById(R.id.ImageView01);
Bitmap img = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();
imgView.setImageBitmap(img);
Log.i(TAG, "onCreate---");
}
// 按钮响应
public void imgBtnClick(View view) {
switch (view.getId()) {
case R.id.btnNDK:
break;
case R.id.btnRestore:
break;
}
}
这时运行程序到手机,结果如下
下面添加JNI-OpenCV代码:
首先添加JNI支持,如下操作,右键单击工程,选择Android Tools->Add Native Support...
根据自己的意愿,输入名称,如EdgeFun
这时,在工程树中会添加jni文件夹,EdgeFun.cpp中可以编写自己的C/C++代码,Android.mk是编译选项,如下图
对于JNI、Android.mk等的介绍,可以在网上查一查,资料很多。
对于OpenCV在Android的使用,需要先下载OpenCV For Android,可以从OpenCV官网下载,我使用的是OpenCV 2.4.9版本的,如下图
下载后解压,可以看到4个文件夹:
apk:这是一些apk安装包,如果使用OpenCV提供给Android的Java接口,需要先安装OpenCV Manager,此文件夹中包含了对不同硬件支持的OpenCV Manager;
doc:一些文档介绍;
samples:一些例子;
sdk:在手机上开发OpenCV程序的必备sdk。里面的java文件夹是OpenCV提供给Android的java接口,如果使用JNI开发OpenCV程序,则要包含native文件夹中的内容。
这里我们使用JNI开发OpenCV程序,所以把native文件夹拷贝到刚才新建工程的jni文件夹下,改名为opencv,刷新工程,结果如下
值得注意的是,opencv文件夹有160多M,实际上里面很多东西是我们没有用到的,如OpenCV for Android是对armeabi、armeabi-v7a、mips和x86都支持的,而我们上面开发的程序是基于armeabi-v7a的,所以opencv文件夹里涉及其他3个的文件夹可以删掉;除此之外,opencv/jni下的.cmake文件也可以删掉。删掉后的工程树如下
另外,opencv/libs/armeabi-v7a下一些没用到的库也可以删掉,这个根据自己软件的情况而定了。如程序中没有用到摄像头,可以删掉camera库,如下
工程JNI配置好了,OpenCV也配置好了,就可以编写代码了。
首先在Java里声明native函数,为了方便起见,我们把native函数专门封装到一个类里。新建一个NativeClass类:
其中添加代码如下
package com.example.opencvhello;
public class NativeClass {
// 加载库
static {
System.loadLibrary("EdgeFun");
}
// 导出函数在java中的声明
public static native int[] ImgFun(int[] buf, int w, int h);
}
然后添加C++代码,在EdgeFun.cpp中添加如下代码
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <android/log.h>
#include <opencv2/opencv.hpp>
using namespace cv;
// 可以使用LOGI输出调试信息
#define SHOW_DEBUG_INFO 1 // 设置为1,可输出调试信息
#if SHOW_DEBUG_INFO
#define LOG_TAG "libEdgeFun"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#else
#define LOGI(...)
#define LOGE(...)
#endif
// 导出函数,供Android调用
extern "C" {
JNIEXPORT jintArray JNICALL Java_com_example_opencvhello_NativeClass_ImgFun(
JNIEnv* env, jobject obj, jintArray buf, int w, int h);
}
// 图像转化
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;
}
// 边缘提取函数
JNIEXPORT jintArray JNICALL Java_com_example_opencvhello_NativeClass_ImgFun(
JNIEnv* env, jobject obj, jintArray buf, int w, int h) {
LOGI("ImgFun begin----");
// 利用env调用java函数提取java数组中的图像信息到c数组
jint *cbuf;
cbuf = env->GetIntArrayElements(buf, false);
if (cbuf == NULL) {
return 0;
}
// 创建OpenCV图像对象
Mat myimg(h, w, CV_8UC4, (unsigned char*) cbuf);
IplImage image=IplImage(myimg);
// 图像转化
IplImage* image3channel = change4channelTo3InIplImage(&image);
// Canny边缘提取
IplImage* pCannyImage=cvCreateImage(cvGetSize(image3channel),IPL_DEPTH_8U,1);
cvCanny(image3channel,pCannyImage,50,150,3);
// 提取OpenCV处理结果到C数组
int* outImage=new int[w*h];
for(int i=0;i<w*h;i++)
outImage[i]=(int)pCannyImage->imageData[i];
// C数组结果保存到java数组中返回
int size = w * h;
jintArray result = env->NewIntArray(size);
env->SetIntArrayRegion(result, 0, size, outImage);
// 释放C数组
env->ReleaseIntArrayElements(buf, cbuf, 0);
delete [] outImage;
LOGI("ImgFun end----");
return result;
}
值得注意的是native函数的命名规则,介绍JNI的资料里都会有说明,简单来说就是:我的工程包是package com.example.opencvhello,java中声明native函数的类名为NativeClass,函数名是ImgFun,于是导出函数名字为
Java_com_example_opencvhello_NativeClass_ImgFun
这样,在Java中就可以调用ImgFun函数实现边缘检测了。由于ImgFun是static函数,所以在其他地方就可以使用NativeClass.ImgFun调用了边缘提取了,如在MainActivity中添加按钮响应新代码:
// 按钮响应
public void imgBtnClick(View view) {
switch (view.getId()) {
case R.id.btnNDK:
// 计时
long current = System.currentTimeMillis();
// 加载图片
Bitmap img1 = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();
// 从Bitmap到int数组
int w = img1.getWidth(), h = img1.getHeight();
int[] pix = new int[w * h];
img1.getPixels(pix, 0, w, 0, 0, w, h);
// 调用边缘提取函数
int[] resultInt = NativeClass.ImgFun(pix, w, h);
// 从int数组到Bitmap
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);
setTitle("w:" + String.valueOf(img1.getWidth())
+ ",h:" + String.valueOf(img1.getHeight()) + "NDK耗时"
+ String.valueOf(performance) + " 毫秒");
break;
case R.id.btnRestore:
Bitmap img2 = ((BitmapDrawable) getResources().getDrawable(R.drawable.lena)).getBitmap();
imgView.setImageBitmap(img2);
MainActivity.this.setTitle("使用OpenCV进行图像处理");
break;
}
}
要运行以上代码,还要对Android.mk做出调整:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
OPENCV_LIB_TYPE := STATIC
include $(LOCAL_PATH)/opencv/jni/OpenCV.mk
LOCAL_MODULE := EdgeFun
LOCAL_SRC_FILES := EdgeFun.cpp
include $(BUILD_SHARED_LIBRARY)
同时添加一个Application.mk文件
内容为
APP_STL:=gnustl_static
APP_CPPFLAGS:=-frtti -fexceptions
APP_ABI:=armeabi-v7a
以上都完成后,就可以运行程序了,点击按钮Process by C++ OpenCV的结果如下
cpp代码中的LOGI输出如下信息
之前初次接触JNI的时候,真是问题多多,有时候莫名其妙在eclipse里打开.cpp文件会报很多错误,后来发现这个是C/C++的语法检查引起的,可以关掉相关检查,如下图关掉Syntax and Semantic Errors
参考:
[原]Android(安卓)开发通过NDK调用JNI,使用opencv做本地c++代码开发配置方法