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閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容