1. 程式人生 > >OpenCV On Android最佳環境配置指南(Android Studio篇)

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

簡介

本文是《OpenCV On Android最佳環境配置指南》系列教程第二篇,也是配置教程的最後一篇。通過對Android Studio裡OpenCV環境配置講解,快速幫新人解決入門配置問題。
本文內容是本人經過多次踩坑,並參考網上眾多OpenCV On Android的配置教程總結而來,盡希望能幫助學習移動影象處理的朋友們少走彎路,。
本篇文章寫的時候有點匆忙,肯定有遺漏之處,後續將持續補充。同時如有不足之處,希望各位dalao能夠指正,我也將及時修改。配置上如果遇到問題,也可以在評論裡留言,我將盡力幫助解決。
如果您使用的是Eclipse,請參考前一章OpenCV On Android最佳環境配置指南(Eclipse篇)

環境

電腦:Windows10
Java:jdk1.8.0_172
Android Studio:Version 3.1.3
SDK:Android Studio 3.1.3自帶的最新SDK(請不要與Eclipse同用一SDK,以免出錯)
NDK:Android Studio 3.1.3自帶的最新NDK
OpenCV:V3.4.1

注:以上配置基本上為最新版本

問答:

1、我已經會使用Eclipse搭建OpenCV Java和NDK環境了,為什麼還需要看你這篇文章?

答:原因有以下幾點:
1、現實原因,參見為什麼要用Android Studio?,可見Android Studio遲早會稱霸Android IDE界;
2、配置原因,OpenCV在Eclipse配置非常複雜(我當初配置了接近一個月),而Android Studio則簡單到令人淚奔。
3、除錯原因,也是最重要的一點,Eclipse 原生程式碼除錯能力弱,且容易出現誤報

,相比較,Android Studio穩定多了,且除錯非常方便。

2、我已經會使用Android Studio搭建OpenCV Java和NDK環境了,為什麼還需要看你這篇文章,你這篇文章有什麼特殊之處嗎?

答:我也不是一個喜歡重複造輪子的人,既然寫了這篇文章,肯定會去介紹一種不同的配置方式。正如本教程題目所提到的“最新”這個詞,我敢能保證我的配置方式是緊跟“時代潮流”。
傳統的Android Studio配置OpenCV環境與Eclipse無異,都是採用Android.mk檔案進行配置,這種方式配置的缺點在於原生程式碼不具有自動提示功能。而本文采用CMake進行配置,更加方便,簡潔,這種方式也是Android Studio所提倡的。

配置前說明:

本次配置不像上篇介紹Eclipse配置環境那樣編寫多個Demo,本次將使用一個Demo,將OpenCV Java和NDK配置方式完全包含,儘可能幫助大家去理解,請大家不要跳躍式地閱讀。
同時OpenCV Java庫和NDK庫的優缺點在上篇文章裡面已經提及,本文就不再贅述。

一、安裝必要組鍵

1、開啟Android Studio。如果是歡迎介面,選擇Configure->SDK Manager。如果是專案介面,選擇Tools->Android->SDK Manager
2、將選項條切換到SDK Tools,勾上CMake和NDK,然後OK,開始下載(如果之前是勾選上的,則可以忽略此步)。
1.png

下載完後,則可以開始正式建立專案。

二、建立Android Studio工程

Create New Projec,這一步沒什麼好說的,由於是在一個Demo裡介紹Java和NDK配置,所以我們必須提前將Include C++ support勾選上(請注意我的專案名和包名,以免後續JNI編寫時出錯),如下圖:
2.png

接下來幾步都預設,但在Customize the Activity這步,我將Activity Name設定為JavaActivity,如下圖:
3.png

下一步,Customize C++ Support中,C++ Standard設定為C++14,下面兩個CheckBox都勾選上(別問我為什麼,我也不知道該咋解釋)。如下圖:
4.png

Finish,專案建立成功

三、OpenCV Java庫使用指南

環境配置:

第一步、將OpenCV Java庫作為Module匯入。具體步驟為:File->New->Import Module,然後將OpenCV-android-sdk\sdk\java目錄匯入,如下圖,然後Next->Finish
5.png

如果Module匯入後提示下圖中的錯誤,你可以選擇點選藍色連結,Android Studio會自動下載所需的sdk版本。但我通常一般不會這樣做,因為目前市場上,99.9%的Android智慧手機系統版本都高於API15(Android4.0.3 IceCreamSandwich),我們也無需去相容過低的系統版本。
6.png

我的做法是,將檔案列表上方的顯示模式由Android切換至Project,並展開檔案列表。分別開啟app\build.gradle檔案和OpenCVLibrary341\build.gradle檔案,將OpenCVLibrary341\build.gradle的幾個版本號改成與app\build.gradle檔案裡相同即可,然後在文字域上方點選Try Again,如下圖:
7.png

第二步、給專案新增OpenCV Java庫依賴
選單File->Project Structure,在Modules裡選擇app,右側進入Dependencies,點選加號,選擇Module dependency,如下圖:
8.png

選擇OpenCVLibrary341,OK!

程式碼編寫:

在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_java.xml內容修改為以下內容:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <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" />

</LinearLayout>

將JavaActivity.java改為以下內容:

public class JavaActivity extends AppCompatActivity 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 void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //保持螢幕常亮
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_java);
        javaCameraView = (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_3_4_0, 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.rgba();
    }
}

在手機中安裝OpenCV Manager.apk,考慮到相容性,真機一般選擇armeabi或者armeabi-v7a,虛擬機器一般選擇x86,然後安裝我們的程式。如果出現安裝正常,但是執行程式報錯的情況,可能有以下原因:
1、未獲得攝像頭許可權
Android系統出於安全考慮,對許可權的的使用越來越嚴苛。在Android高版本中,僅僅在AndroidManifest.xml中宣告許可權並不能確保使用者一定授予,需要動態獲取許可權,或者使用者在設定中手動授權。
2、未能連線到OpenCV Manager
同樣是出於安全考慮,同時也防止應用之間相互喚醒(類似騰訊、百度全家桶,相互呼叫來保活),Android高版本限制了程序間的呼叫。具體解決方案請百度,因為每個手機開啟方式都不一樣。

免安裝OpenCV Manager

四、OpenCV NDK庫使用指南

環境配置:

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,環境配置好了,嘿嘿嘿,接下來開始程式碼編寫。

程式碼編寫:

選單File->New->Activity->Empty Activity,建立一個新的Activity,其命名下如圖,並設定為啟動頁,Finish
9.png

為了桌面上兩個程式入口不衝突,請在AndroidManifest.xml檔案中給兩個Activity指定label,如下圖:

10.png
佈局檔案activity_jni.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>

JniActivity.java內容如下:

public class JniActivity 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_jni);
        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.png的圖片放置在drawable目錄下,我使用的是我的偶像照片,嘿嘿嘿\^_\^!

test.png

應用層寫好了,現在開始原生層操作:

第一步:生成標頭檔案
開啟Android Studio下方Terminal欄,輸入cd app\src\main\java(所有應用都一樣),回車。然後輸入javah 包名.類名(我們這裡輸入的是javah com.demo.opencvdemo.JniActivity),回車後,將在app\src\main\java目錄下生成一個頭檔案,如下圖所示。最後將該標頭檔案Move到app\src\main\cpp目錄下。
12.png

第二步:編寫NDK程式碼
native-lib.cpp內容修改為:

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

using namespace cv;

extern "C" JNIEXPORT void
JNICALL Java_com_demo_opencvdemo_JniActivity_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, 125, 225);
        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, 125, 225);
        cvtColor(gray, temp, COLOR_GRAY2RGB);
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}

程式碼寫完後,就可以編譯執行程式了。這時可能會出現如下錯誤,這是由於OpenCV doesn’t link on Android x86/x86_64 with ndk r16,簡而言之,就是NDK r16預設使用的是c++_static模板庫,而OpenCV3.4.1是由NDK r14預設的gnustl_static模板庫所生成,這使得NDK r16編譯會出現異常。如果你使用的是ndk r14,將不會出現這種錯誤。
13.png

解決辦法有兩種:
1、更換成舊版本NDK
在SDK Manager中刪除NDK,然後重新下載NDK r14,並將其解壓到AndroidSDK目錄/ndk-bundle目錄中。重啟Android Studio即可(以後更新注意不要升級NDK)。
2、使用gnustl_static模板庫
要想使用gnustl_static模板庫進行編譯,只需要在app\build.gradle中新增arguments引數,新增後內容如下:

...
externalNativeBuild {
      cmake {
           cppFlags "-std=c++14 -frtti -fexceptions"
           arguments '-DANDROID_STL=gnustl_shared'   //支援C++異常處理標準模板庫
      }
 }
...

繼續編譯執行,這時候又會出現下面的錯誤(方法2才會有),這是由x86 abi架構造成的,原因未知。
14.png

解決方法有以下兩種:
1、不生成x86 abi架構
NDK r16能夠編譯"armeabi-v7a", "arm64-v8a", "x86_64","x86",我們只需指定編譯器生成其餘三種abi架構,就可以避免該bug。具體做法如下,在app\build.gradle中增加abiFilters欄位。內容如下:

...
externalNativeBuild {
      cmake {
           cppFlags "-std=c++14 -frtti -fexceptions"
           arguments '-DANDROID_STL=gnustl_shared'   //支援C++異常處理標準模板庫
           abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
      }
 }
...

本方法的缺點是,不能相容x86架構的手機,並且不能在虛擬機器上執行(大部分虛擬機器都是x86架構)。

2、新增編譯引數
我比較喜歡這個方法,既能避免錯誤,有能相容大部分的Android手機。做法如下:在CmakeLists.txt檔案中新增如下內容:

...
endif(OpenCV_FOUND)

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,libippicv.a -Wl,--exclude-libs,libippiw.a")
# ###################### 專案原生模組 ###########################

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

然後編譯程式,將不再報錯,並能夠成功執行。注意,此時可能會預設開啟JavaActivity,退出應用,從桌面進入JniActivity,點選SHOW按鈕,效果如下:

15.png

點選PROCESS,效果如下:

16.png

額······我的偶像,即使變成被Canny檢測了邊緣,還是我的偶像,哈哈哈!!!

五、總結

OpenCV On Android 系列配置教程就到此為止,寫這兩篇文章確實也不容易,修改了很多遍,尤其是這篇Android Studio,算是百忙之中抽空完成的吧,也拖了很久。自己在配置的過程中踩了無數的坑,希望我的經驗能夠幫助到大家少走彎路,同時也虛心接受大家的批評與指正。移動端影象處理的路還長,我也將不斷去學習充實自己,這兩篇文章算是了卻我的一個心願,接下來的時間,我將全身心投入考研,反正大家都加油吧,下面是我創的學習群,我也將不定期幫助大家解決問題,請加群的人保持一個和善的心,分享經驗,共同進步,記住:別人幫助你不是必須的

OpenCV On Android學習群.png