基于OpenCV的Android软件开发

    最近在做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.containernew 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 btnNDKbtnRestore;  
    @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++代码开发配置方法





posted @ 2014-10-11 15:59  了凡春秋  阅读(653)  评论(0编辑  收藏  举报