Android 開發(fā)中集成 OpenCV (java, c++)、縮減庫體積

最近 Ai 項(xiàng)目中需要在安卓上使用 OpenCV,網(wǎng)上資料很多,但大多都比較亂,這里進(jìn)行了整理和歸納,盡量讓大部分人都能夠看懂。

本文主要包含以下三塊內(nèi)容,包含三個 Demo 源碼:

  1. java + native 庫集成
  2. c++ + native 庫集成,傳遞圖像原始數(shù)據(jù)到 C++ 代碼中
  3. 在第二點(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

  1. 導(dǎo)入(File-New-Import Module) sdk 中 java 庫工程 (sdk/java),不出意外,項(xiàng)目下會多一個 openCVLibrary3410 的庫。

[圖片上傳失敗...(image-5f3419-1591887738802)]

  1. 在 Module:app 的 build.gradle 文件中加入 implementation project(':openCVLibrary${you opencv version}'),然后同步代碼。

調(diào)整 openCVLibrary 的 build.gradle 中 compileSdkVersion 版本到 21 以上,不然運(yùn)行時會報(bào) Camera2 找不到。

  1. 將 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']
        }
    }
  1. 根據(jù)選擇的 cpu 架構(gòu),還需要在 Module:app 的 build.gradle 中的 defaultConfig 節(jié)點(diǎn)下加入如下代碼。
    ndk {
        abiFilters "armeabi-v7a"
    }
  1. 最后,在 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

  1. 打開 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
  1. 把 native 庫頭文件(sdk/native/jni/include)拷貝到 src/main/cpp 目錄下,如果不存在該目錄請自己創(chuàng)建一個

  2. 在 src/main/cpp 中創(chuàng)建一個 cpp 文件,這里就稱之為 native-lib.app,現(xiàn)在里面啥都不做,先跑通。

  3. 編輯 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} )
  1. 隨后在 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++ 中吧。

  1. 在 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ù)組。

  1. 在 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);
  1. 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 的源碼有教深的了解。

  1. 在上小節(jié) Demo 的基礎(chǔ)上,我們可以把 so 文件刪了,然后先把 sdk/native/staticlibs/armeabi-v7a 下的全部 .a 文件都復(fù)制到 jniLibs/libs/armeabi-v7a 中。

  2. 隨后,把 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> 等頭文件了。

  1. 頭文件有了,接下來把相關(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é)束了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容