OpenCV On Android最佳环境配置指南(Android Studio篇)

声明:由于简书写作不便,后续将在掘金上更新和发布文章,包括本文。

掘金账号:徒步青云

简介

本文是《OpenCV On Android最佳环境配置指南》 系列教程第二篇,也是配置系列的最后一篇,适合使用Android Studio的开发人员学习。

本教程是经过本人多次踩坑,并结合网上众多OpenCV On Android的配置教程总结而来,尽希望能帮助学习OpenCV的朋友们少走弯路。如果配置上遇到问题,可在评论中留言,我将尽力帮助解决。

如果您使用的是Eclipse,请参考上一章OpenCV On Android最佳环境配置指南(Eclipse篇)


环境

电脑:Windows10
Java:jdk1.8.0_172
Android Studio:Version 4.0.2
SDK:Android Studio 4.0.2自带的最新SDK(请不要与Eclipse同用一SDK,以免出错)
NDK:Android Studio 4.0.2自带的最新NDK
OpenCV:V4.4.0

注:以上配置向上兼容,读者可使用更新的版本,但低版本可能出现错误
(更新时间:2020-10-12)


配置前说明:

本次配置不像上篇介绍Eclipse配置环境那样编写多个Demo,本次将使用一个Demo,将OpenCV Java和NDK配置方式完全包含,尽可能帮助大家去理解,请大家不要跳跃式地阅读。
同时OpenCV Java库和NDK库的优缺点在上篇文章里面已经提及,本文就不再赘述。


一、安装必要组件

1、打开Android Studio。如果是欢迎界面,选择Configure->SDK Manager。如果是项目界面,选择Tools->Android->SDK Manager
2、将选项条切换到SDK Tools,勾上左下角的Show Package Details,然后勾选以下四项,然后OK,开始下载。

1.png

下载完后,就可以开始创建项目了。


二、创建Android Studio工程

Create New Projec,开始选择模板,这时选择最后一项Native C++,然后进入配置界面。

2.png

这一步需要注意两个地方

1、包名:请尽量与我保持一致,否则新手容易出错。
2、最小SDK:OpenCV 4.2.0要求最小SDK必须大于21。

下一步直接Finish,项目创建成功!

项目创建完成后,最好运行一下,确保基本环境没问题


三、OpenCV Java库使用指南

3.1、环境配置:

第一步、将OpenCV Java库作为Module导入。具体步骤为:File->New->Import Module,然后将OpenCV-android-sdk\sdk\java目录导入。如下图,然后Next->Finish

3.png

第二步、修改模块名
默认导入的模块名为java,为了方便区分,建议修改成opencv,步骤如下:
java模块右键Refactor->Rename

第二步、将导入的opencv模块从application改成library,步骤如下:

4

1、将文件预览方式切换至Android。
2、打开opencv的build.gradle文件。
3、将apply plugin: 'com.android.application'修改成apply plugin: 'com.android.library'
4、删除(或注释)掉defaultConfig内容
5、将Run/Debug Configurations从opencv切换到app
6、点击Sync Now使修改生效。

第三步、给项目添加opencv依赖
菜单File->Project Structure,在Dependencies中选择app,点击+,选择Module dependency,如下图:

5

勾选上opencv模块,点击OK即可!

3.2、代码编写:

在AndroidManifest.xml文件中添加权限:

....
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />
    <uses-feature
        android:name="android.hardware.camera.autofocus"
        android:required="false" />
....

将activity_main.xml内容修改为以下内容:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <org.opencv.android.JavaCameraView
        android:id="@+id/javaCameraView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:camera_id="back"
        app:show_fps="true" />
</FrameLayout>

将MainActivity.java改为以下内容:

public class MainActivity extends CameraActivity implements CameraBridgeViewBase.CvCameraViewListener2 {
    private JavaCameraView javaCameraView;
    private BaseLoaderCallback baseLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS: {
                    javaCameraView.enableView();
                }
                break;
                default:
                    super.onManagerConnected(status);
                    break;
            }
        }
    };

    @Override
    protected List<? extends CameraBridgeViewBase> getCameraViewList() {
        List<CameraBridgeViewBase> list = new ArrayList<>();
        list.add(javaCameraView);
        return list;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        javaCameraView = findViewById(R.id.javaCameraView);
        javaCameraView.setVisibility(SurfaceView.VISIBLE);
        javaCameraView.setCvCameraViewListener(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        if (javaCameraView != null)
            javaCameraView.disableView();
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, baseLoaderCallback);
        } else {
            baseLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }

    @Override
    public void onCameraViewStarted(int width, int height) {

    }

    @Override
    public void onCameraViewStopped() {

    }

    @Override
    public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
        return inputFrame.gray();
    }
}

这里注意一下,让我们自己的Activity继承至opencv的CameraActivity,是一个十分不好的做法,请读者务必看懂CameraActivity源码,做到活学活用。

做到这一步,我们已经能够编译出一个app,但是它并不能正常运行,这是因为我们只是将opencv的api模块导入,但具体代码没包含在apk中。
我们现在可通过以下两种方式来解决这个问题:

1、在手机中安装OpenCV Manager.apk
这种方式及其不好,原因有以下几点:

1、用户需要单独安装这个apk,且不同CPU架构的手机,apk也不一样。
2、Android高版本中,即使安装了这个apk,我们的程序也可能不会正常运行。这是因为一些手机厂商,为了防止应用通过相互唤醒来实现应用保活,所以对不同应用进程间调用进行了限制。比如华为手机管家中的启动管理。
3、OpenCV高版本现已不再提供OpenCV Manager.apk


2、将libopencv_java4.so导入到apk中
这种方式的缺点就是:麻烦!!!

如果我们已经确定了目标机型,这种方式无疑是比较好的。通常来说,如果是真机,导入armeabiarmeabi-v7a架构的so文件;如果是虚拟机,则一般选择x86架构的so文件。


方法2实现步骤如下:
1、将OpenCV库中的OpenCV-android-sdk\sdk\native\libs目录下4个子目录,copy到我们项目的libs目录下。
2、修改build.gradle文件,添加以下内容:

sourceSets{
    main{
        jniLibs.srcDirs = ["libs"];
    }
}

如图所示:

6

做完这一步,libopencv_java4.so将被自动打包进apk中,但是依旧不能正常运行,提示缺少c++_shared,这需要我们再次修改build.gradle文件,添加arguments:

android {
    //......
    defaultConfig {
        //......
        externalNativeBuild {
            cmake {
                cppFlags ""
                arguments "-DANDROID_STL=c++_shared"
            }
        }
    }
}

做完以上内容,基本上就OK了。

其实这里还有一个坑,由于我们从一开始就创建的是一个Native C++项目,所以通过在build.gradle文件中添加arguments参数,就能将c++_shared.so打包进apk,但是如果创建的是普通项目,此方式将无效,需要手动将c++_shared.so添加到libs对应的目录下


四、OpenCV NDK库使用指南

4.1、环境配置:

Android Studio配置OpenCV环境灰常简单(是的,没错),只需修改一个文件便能成功配置环境,什么Android.mk啊、Application.mk啊,全部滚蛋。
配置方式:打开CMakeLists.txt,内容修改如下,(将OpenCV_DIR设置为你的路径,注意分隔符,使用'/'或'\\')

cmake_minimum_required(VERSION 3.4.1)

# ##################### OpenCV 环境 ############################
#设置OpenCV-android-sdk路径
set( OpenCV_DIR D:\\OpenCV\\OpenCV-android-sdk\\sdk\\native\\jni )

find_package(OpenCV REQUIRED )
if(OpenCV_FOUND)
    include_directories(${OpenCV_INCLUDE_DIRS})
    message(STATUS "OpenCV library status:")
    message(STATUS "    version: ${OpenCV_VERSION}")
    message(STATUS "    libraries: ${OpenCV_LIBS}")
    message(STATUS "    include path: ${OpenCV_INCLUDE_DIRS}")
else(OpenCV_FOUND)
    message(FATAL_ERROR "OpenCV library not found")
endif(OpenCV_FOUND)

# ###################### 项目原生模块 ###########################

add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp)

target_link_libraries( native-lib
                       ${OpenCV_LIBS}
                       log
                       jnigraphics)

OK,环境配置好了,嘿嘿嘿,接下来开始代码编写。

4.2、代码编写:

菜单File->New->Activity->Empty Activity,创建一个新的Activity,其命名下如图,并设置为启动页,Finish

7

为了分清楚桌面上两个程序入口,请在AndroidManifest.xml文件中给两个Activity指定label,如下图:

8

下面开始编写布局文件activity_native.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal">

        <Button
            android:id="@+id/show"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="show" />

        <Button
            android:id="@+id/process"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="process" />
    </LinearLayout>

</RelativeLayout>

NativeActivity.java内容如下:

public class NativeActivity extends AppCompatActivity implements View.OnClickListener {
    private ImageView imageView;

    static {//加载so库
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_native);
        imageView = findViewById(R.id.imageView);
        findViewById(R.id.show).setOnClickListener(this);
        findViewById(R.id.process).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.show) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
            imageView.setImageBitmap(bitmap);
        } else {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
            getEdge(bitmap);
            imageView.setImageBitmap(bitmap);
        }
    }

    //获得Canny边缘
    native void getEdge(Object bitmap);
}

将一张名为test.jpg的图片放置在drawable目录下,嘿嘿嘿!

9

应用层写好了,现在开始原生层操作:

第一步:生成头文件
打开Android Studio下方Terminal栏,输入cd app\src\main\java(所有应用都一样),回车。然后输入javah -encoding UTF-8 包名.类名(我们这里输入的是javah -encoding UTF-8 com.demo.opencv.NativeActivity,注意,类名后面不要加.java或.class),回车后,将在app\src\main\java目录下生成一个头文件,如果提示类文件不存在,则需先build,再执行上述操作。最后将该头文件移动到app\src\main\cpp目录下。

10

第二步:编写NDK代码
native-lib.cpp内容修改为:

#include "com_demo_opencv_NativeActivity.h"
#include <android/bitmap.h>
#include <opencv2/opencv.hpp>

using namespace cv;

extern "C" JNIEXPORT void
JNICALL Java_com_demo_opencv_NativeActivity_getEdge
        (JNIEnv *env, jobject obj, jobject bitmap) {
    AndroidBitmapInfo info;
    void *pixels;

    CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0);
    CV_Assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 ||
              info.format == ANDROID_BITMAP_FORMAT_RGB_565);
    CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0);
    CV_Assert(pixels);
    if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
        Mat temp(info.height, info.width, CV_8UC4, pixels);
        Mat gray;
        cvtColor(temp, gray, COLOR_RGBA2GRAY);
        Canny(gray, gray, 45, 75);
        cvtColor(gray, temp, COLOR_GRAY2RGBA);
    } else {
        Mat temp(info.height, info.width, CV_8UC2, pixels);
        Mat gray;
        cvtColor(temp, gray, COLOR_RGB2GRAY);
        Canny(gray, gray, 45, 75);
        cvtColor(gray, temp, COLOR_GRAY2RGB);
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}

注意,naive-lib.cpp中只有一个函数,这个函数名必须与生成的头文件中定义的一致
运行程序,点击SHOW按钮,效果如下:

11

点击PROCESS,效果如下:

12

完美,收工,回家吃饭!

五、总结

OpenCV On Android 系列配置教程就到此为止,写这两篇文章确实也不容易,修改了很多遍,尤其是这篇Android Studio,算是百忙之中抽空完成的吧,也拖了很久。自己在配置的过程中踩了无数的坑,希望我的经验能够帮助到大家少走弯路,同时也虚心接受大家的批评与指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容