聲明:由于簡書寫作不便,后續將在掘金上更新和發布文章,包括本文。
掘金賬號:徒步青云
簡介
本文是《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,開始下載。
下載完后,就可以開始創建項目了。
二、創建Android Studio工程
Create New Projec,開始選擇模板,這時選擇最后一項Native C++,然后進入配置界面。
這一步需要注意兩個地方
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。
第二步、修改模塊名
默認導入的模塊名為java,為了方便區分,建議修改成opencv,步驟如下:
java模塊右鍵Refactor->Rename
第二步、將導入的opencv模塊從application改成library,步驟如下:
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,如下圖:
勾選上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中
這種方式的缺點就是:麻煩!!!
如果我們已經確定了目標機型,這種方式無疑是比較好的。通常來說,如果是真機,導入armeabi或armeabi-v7a架構的so文件;如果是虛擬機,則一般選擇x86架構的so文件。
方法2實現步驟如下:
1、將OpenCV庫中的OpenCV-android-sdk\sdk\native\libs
目錄下4個子目錄,copy到我們項目的libs目錄下。
2、修改build.gradle文件,添加以下內容:
sourceSets{
main{
jniLibs.srcDirs = ["libs"];
}
}
如圖所示:
做完這一步,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。
為了分清楚桌面上兩個程序入口,請在AndroidManifest.xml文件中給兩個Activity指定label,如下圖:
下面開始編寫布局文件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目錄下,嘿嘿嘿!
應用層寫好了,現在開始原生層操作:
第一步:生成頭文件
打開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
目錄下。
第二步:編寫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按鈕,效果如下:
點擊PROCESS,效果如下:
完美,收工,回家吃飯!
五、總結
OpenCV On Android 系列配置教程就到此為止,寫這兩篇文章確實也不容易,修改了很多遍,尤其是這篇Android Studio,算是百忙之中抽空完成的吧,也拖了很久。自己在配置的過程中踩了無數的坑,希望我的經驗能夠幫助到大家少走彎路,同時也虛心接受大家的批評與指正。