最近 Ai 項(xiàng)目中需要在安卓上使用 OpenCV,網(wǎng)上資料很多,但大多都比較亂,這里進(jìn)行了整理和歸納,盡量讓大部分人都能夠看懂。
本文主要包含以下三塊內(nèi)容,包含三個 Demo 源碼:
- java + native 庫集成
- c++ + native 庫集成,傳遞圖像原始數(shù)據(jù)到 C++ 代碼中
- 在第二點(diǎn)的基礎(chǔ)上縮減打包的庫體積
目前,本文的 Demo 是在如下環(huán)境中驗(yàn)證的,請自行對齊,不然容易出現(xiàn)問題。
NDK: 16.1.4479499
OpenCV: 3.4.0
CMake: 3.10.2
Android Studio: 3.6.3
NDK 和 OpenCV 的版本號比較重要,后兩者影響不是很大
本教程三個 Demo 實(shí)現(xiàn)的都是彩色轉(zhuǎn)灰度,截圖如下:
[圖片上傳失敗...(image-affaab-1591887738802)]
下載 OpenCV4Android SDK - 3.4.10
鏈接:https://opencv.org/releases/
SDK 的文件結(jié)構(gòu)如下:
# edvardzeng @ EDVARDZENG-MB0 in ~/Workspace/OpenCV-android-sdk [15:01:21]
$ tree -L 2 .
.
├── LICENSE
├── README.android
├── apk
├── samples
└── sdk
├── build.gradle
├── etc
├── java
...
└── javadoc (opencv java api 文檔)
└── native
├── 3rdparty
├── jni
...
└── include (c++ 頭文件)
├── libs
└── staticlibs
apk:這個包下面是 opencv-manager 安裝包,這里用不到,畢竟大家也不會以這種方式集成
samples:是 opencv 官方提供的幾個 demo 工程,有工程源代碼,也有打包好的 apk
sdk:這個是重點(diǎn),以后開發(fā)的時候也是用的這里面的東西
etc:識別相關(guān)的級聯(lián)分類器之類的
java:這是 opencv 官方提供的一個 opencv 的 android 庫工程,提供了完整的 opencv 能力,因?yàn)閛pencv底層是用c/c++寫的,但是現(xiàn)在編程語言很多,java、python等等,所以官方就針對不同的語言平臺,對底層庫進(jìn)行了二次封裝,使用的時候?qū)⒃撛摴こ讨苯幼鳛閹鞂?dǎo)入即可。
native:針對不同的 CPU 架構(gòu),這里會有對應(yīng)的靜態(tài)或者動態(tài)庫文件。
jni:一些 cmake 編譯腳本和動態(tài)庫的頭文件,里面包含了編寫 C++ 代碼時需要引入的頭文件(include 文件夾),以及在縮減庫的時候查看依賴關(guān)系和配置的信息。
libs:官方根據(jù)不同平臺架構(gòu)打好的.so 動態(tài)庫,提供完整的 opencv 能力,體積稍大,單個架構(gòu)對應(yīng)的.so文件體積在 10M +,一般用于開發(fā)調(diào)試的時候用
staticlibs:將不同的功能分別做成.a靜態(tài)庫,可以根據(jù)使用到的 opencv 能力,選擇加載相應(yīng)的 .a 靜態(tài)庫,有利于降低應(yīng)用體積。
這里提一下,官網(wǎng)下載的 SDK 一般都不包含 contrib 包對應(yīng)的實(shí)現(xiàn),因此有一些功能無法使用(像 KCF,MOSSE 等跟蹤算法的實(shí)現(xiàn)就在 contrib 包里),需要自行編譯。
java + native 庫的方式集成
通過加載 so 文件的方式,可以不用安裝 opencv-manager
- 導(dǎo)入(File-New-Import Module) sdk 中 java 庫工程 (sdk/java),不出意外,項(xiàng)目下會多一個 openCVLibrary3410 的庫。
[圖片上傳失敗...(image-5f3419-1591887738802)]
- 在 Module:app 的 build.gradle 文件中加入 implementation project(':openCVLibrary${you opencv version}'),然后同步代碼。
調(diào)整 openCVLibrary 的 build.gradle 中 compileSdkVersion 版本到 21 以上,不然運(yùn)行時會報(bào) Camera2 找不到。
- 將 sdk-native-libs 中將對應(yīng) cpu 架構(gòu)(這里只選擇了 armeabi-v7a)的文件夾復(fù)制到 src/main/jniLibs,如果 jniLibs 文件夾不存在則自己創(chuàng)建,隨后在 Module:app 的 build.gradle 文件中,在 android 節(jié)點(diǎn)下加入如下代碼,隨后點(diǎn)擊同步。
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs/libs']
}
}
- 根據(jù)選擇的 cpu 架構(gòu),還需要在 Module:app 的 build.gradle 中的 defaultConfig 節(jié)點(diǎn)下加入如下代碼。
ndk {
abiFilters "armeabi-v7a"
}
- 最后,在 gradle.properties 中加入
android.useDeprecatedNdk=true,避免一些兼容性的問題
在使用 java 代碼開發(fā)時,需要加入如下代碼加載對應(yīng)的 so 庫。
public class{
......
static {
// 加載對應(yīng)的 so 文件,需要去頭 lib,去尾 .so
System.loadLibrary("opencv_java3");
}
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = findViewById(R.id.display_img);
Bitmap bitmap = BitmapFactory.decodeResource(MainActivity.this.getResources(), R.drawable.lenna).copy(Bitmap.Config.ARGB_8888, true);
Mat mat = new Mat();
Utils.bitmapToMat(bitmap, mat);
// 把圖片轉(zhuǎn)換為灰度圖
Mat grayMat = new Mat();
Imgproc.cvtColor(mat, grayMat, Imgproc.COLOR_RGBA2GRAY);
Bitmap grayBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(grayMat, grayBitmap);
// 顯示出來
iv.setImageBitmap(grayBitmap);
}
}
至此,我們就可以在 android 中使用 java 調(diào)用 OpenCV 的函數(shù)了。
java c++ native 庫的方式集成
如果希望在 android 中用 c++ 來開發(fā),除了需要引入 native 庫外,也需要引入 ndk,這里創(chuàng)建 Project 時可以選擇 C++ Project
- 打開
local.properties文件,如下所示:
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Wed Jun 03 17:56:37 CST 2020
ndk.dir=/Users/edvardzeng/Library/Android/sdk/ndk/16.1.4479499
sdk.dir=/Users/edvardzeng/Library/Android/sdk
把 native 庫頭文件(sdk/native/jni/include)拷貝到 src/main/cpp 目錄下,如果不存在該目錄請自己創(chuàng)建一個
在 src/main/cpp 中創(chuàng)建一個 cpp 文件,這里就稱之為 native-lib.app,現(xiàn)在里面啥都不做,先跑通。
編輯 CMakeLists.txt 文件,如果沒有,自己創(chuàng)建一個,內(nèi)容大致如下
cmake_minimum_required(VERSION 3.4.1)
include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
add_library(libopencv_java3 SHARED IMPORTED)
set_target_properties(libopencv_java3 PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/jniLibs/libs/${ANDROID_ABI}/libopencv_java3.so)
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# cpp 源碼文件,也就是在第二步中創(chuàng)建的
src/main/cpp/native-lib.cpp )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib libopencv_java3
# Links the target library to the log library
# included in the NDK.
${log-lib} )
- 隨后在 Module:app 的 build.gradle 中的 android 節(jié)點(diǎn)下加入如下內(nèi)容即可
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
上一節(jié)中的 3, 4, 5 步在這里也同樣需要執(zhí)行。
隨后 build 一下,如果不報(bào)錯,說明 c++ 的環(huán)境搭建應(yīng)該是沒有問題了。
這里再講一下,如何通過 jni 傳遞圖像數(shù)據(jù)到 C++ 中吧。
- 在 MainActivity.java 中添加 native 方法
public class MainActivity extends AppCompatActivity {
...
public native int[] convertToGray(int[] imgData, int width, int height);
...
}
這里,函數(shù)前面有 native 關(guān)鍵字,鼠標(biāo)移動到該方法,按住 alt + enter,在彈出的窗口里確認(rèn)創(chuàng)建對應(yīng)的 C++ 函數(shù),這個時候就會跳轉(zhuǎn)到 native-lib.cpp
文件上來,代碼如下:
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_cv_cvdemo2_MainActivity_convertToGray(JNIEnv *env, jobject thiz, jintArray img_data,
jint width, jint height) {
// put your code here
}
這里定義了圖像中像素點(diǎn)數(shù)據(jù)傳入的方式是 int 的數(shù)組。
- 在 java 中獲得調(diào)用 Bitmap 的 getPixels 方法獲得像素點(diǎn)的 int 值,核心代碼如下
Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.lenna).copy(Bitmap.Config.ARGB_8888, true);
int width = image.getWidth();
int height = image.getHeight();
int[] pixel = new int[width * height];
image.getPixels(pixel, 0, width, 0, 0, width, height);
// 這里調(diào)用的 native 方法轉(zhuǎn)換成為灰度圖,這里的 C++ 實(shí)現(xiàn)在下面小節(jié)
int[] grayPixels = convertToGray(pixel, width, height);
// 把返回的灰度圖像素值數(shù)組轉(zhuǎn)換成 Bitmap
Bitmap grayBp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
grayBp.setPixels(grayPixels, 0, width, 0, 0, width, height);
iv.setImageBitmap(grayBp);
- native-lib.cpp 使用 cv2 方法轉(zhuǎn)換圖像為灰度圖并且返回
這里的代碼編寫涉及到安卓 NDK 的一些編程概念,詳細(xì)的可以查看官方文檔。
但這里的代碼還是比較簡單易懂的。
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_cv_cvdemo2_MainActivity_convertToGray(JNIEnv *env, jobject thiz, jintArray img_data,
jint width, jint height) {
// TODO: implement convertToGray()
jint* cbuf;
cbuf = env->GetIntArrayElements(img_data, JNI_FALSE);
Mat inp_img(height, width, CV_8UC4, (unsigned char *)cbuf);
Mat gray_img;
cvtColor(inp_img, gray_img, CV_BGRA2GRAY);
Mat ret_img;
cvtColor(gray_img, ret_img, CV_GRAY2BGRA);
int size = width * height;
jintArray result = env->NewIntArray(size);
uchar *ptr = ret_img.data;
env->SetIntArrayRegion(result, 0, size, (const jint *) ptr);
env->ReleaseIntArrayElements(img_data, cbuf, 0);
return result;
}
若無意外,Build - Run 后便可運(yùn)行看到效果
縮減 OpenCV 庫體積
單純的 opencv 動態(tài)鏈接庫有 12.3 MB,如果要集成到客戶端 apk 中顯然還是大了一些,雖然我們可以使用動態(tài)分發(fā) so 庫來降低安裝包的大小,但一般來說我們都沒有用到完整的 opencv 能力,所以這里需要針對我們用到的庫,對其進(jìn)行縮減。
[圖片上傳失敗...(image-46641d-1591887738802)]
縮減的方式有兩種。
- 根據(jù)用到的模塊,選擇性的引用 OpenCV4Android 的靜態(tài)鏈接庫,來生成自己的動態(tài)鏈接庫
- 從源碼編譯生成屬于自己的 so 或 a 庫
這里介紹第一種,第二種會獲得更小的庫體積,但需要開發(fā)者對 OpenCV 的源碼有教深的了解。
在上小節(jié) Demo 的基礎(chǔ)上,我們可以把 so 文件刪了,然后先把 sdk/native/staticlibs/armeabi-v7a 下的全部 .a 文件都復(fù)制到 jniLibs/libs/armeabi-v7a 中。
隨后,把
sdk/natvie/jni/include文件夾下的頭文件移動到 cpp/include 文件夾里(如無創(chuàng)建一個)
編輯 cpp/CMakeLists.txt 文件,把頭文件包含進(jìn)去
set(libs ${CMAKE_SOURCE_DIR}/..)
include_directories(${libs}/cpp/include)
隨后 Build - Refresh Linked C++ Project 后,native-lib.cpp 里現(xiàn)在就已經(jīng)可以 #include <opencv/cv.h> 等頭文件了。
- 頭文件有了,接下來把相關(guān)的靜態(tài)鏈接庫包含進(jìn)去。
這一步主要的難點(diǎn)判斷自己的代碼引用了哪些模塊,以及模塊之間的依賴關(guān)系,一不小心就會出現(xiàn) libcpufeatures || tbb || tegra_hal 等庫找不到的錯誤,這些是屬于第三方依賴庫,存放在 sdk/native/3rdparty 文件夾中。
[圖片上傳失敗...(image-377314-1591887738802)]
網(wǎng)上有蠻多奇淫技巧來分析庫之間的依賴的,但是實(shí)際上在 sdk 中的 sdk/native/jni/abi-armeabi-v7a/OpenCVModules.cmake 就已經(jīng)包含了引入靜態(tài)庫所需要的參數(shù)和依賴關(guān)系,譬如在該文件的定義中,opencv_core 依賴了 tbb, tegra_hal, libcpufeature 等靜態(tài)庫。
......
add_library(opencv_core STATIC IMPORTED)
set_target_properties(opencv_core PROPERTIES
INTERFACE_LINK_LIBRARIES "$<LINK_ONLY:dl>;$<LINK_ONLY:m>;$<LINK_ONLY:log>;$<LINK_ONLY:tbb>;$<LINK_ONLY:tegra_hal>;$<LINK_ONLY:z>;$<LINK_ONLY:libcpufeatures>;$<LINK_ONLY:tegra_hal>"
)
......
這個時候看回我們轉(zhuǎn)換灰度圖的 C++ 代碼,我們只使用到了 opencv_core 和 opencv_imgproc,我們把對應(yīng)的配置從 OpenCVModules.cmake 拷貝到 cpp/CMakeLists.txt 中去,并在 target_link_libraries 中鏈接對應(yīng)的庫即可。
cpp/CMakeLists.txt 過于冗長,這里就不貼了。但這里需要注意的是靜態(tài)庫的添加順序需要和 OpenCVModules.cmake 保持一致,不然編譯會報(bào)錯
最后編譯運(yùn)行,Done。
那么生成的 so 到底有多大呢?其實(shí)我們可以,我們可以對 apk 進(jìn)行解壓,里面有一個 lib 文件夾,包含了我們剛才編譯出的 so 文件,可以看到,才 2.4mb 大小
[圖片上傳失敗...(image-baf8f5-1591887738802)]
好了,這篇文章到這就結(jié)束了。