Android 开发中集成 OpenCV (java, c++)、缩减库体积

最近 Ai 项目中需要在安卓上使用 OpenCV,网上资料很多,但大多都比较乱,这里进行了整理和归纳,尽量让大部分人都能够看懂。

本文主要包含以下三块内容,包含三个 Demo 源码:

  1. java + native 库集成
  2. c++ + native 库集成,传递图像原始数据到 C++ 代码中
  3. 在第二点的基础上缩减打包的库体积

目前,本文的 Demo 是在如下环境中验证的,请自行对齐,不然容易出现问题。

NDK: 16.1.4479499
OpenCV: 3.4.0
CMake: 3.10.2
Android Studio: 3.6.3

NDK 和 OpenCV 的版本号比较重要,后两者影响不是很大

源码戳此下载

本教程三个 Demo 实现的都是彩色转灰度,截图如下:

[图片上传失败...(image-affaab-1591887738802)]


下载 OpenCV4Android SDK - 3.4.10


链接:https://opencv.org/releases/

SDK 的文件结构如下:

# 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:这个是重点,以后开发的时候也是用的这里面的东西

etc:识别相关的级联分类器之类的

java:这是 opencv 官方提供的一个 opencv 的 android 库工程,提供了完整的 opencv 能力,因为opencv底层是用c/c++写的,但是现在编程语言很多,java、python等等,所以官方就针对不同的语言平台,对底层库进行了二次封装,使用的时候将该该工程直接作为库导入即可。

native:针对不同的 CPU 架构,这里会有对应的静态或者动态库文件。

jni:一些 cmake 编译脚本和动态库的头文件,里面包含了编写 C++ 代码时需要引入的头文件(include 文件夹),以及在缩减库的时候查看依赖关系和配置的信息。

libs:官方根据不同平台架构打好的.so 动态库,提供完整的 opencv 能力,体积稍大,单个架构对应的.so文件体积在 10M +,一般用于开发调试的时候用

staticlibs:将不同的功能分别做成.a静态库,可以根据使用到的 opencv 能力,选择加载相应的 .a 静态库,有利于降低应用体积。

这里提一下,官网下载的 SDK 一般都不包含 contrib 包对应的实现,因此有一些功能无法使用(像 KCF,MOSSE 等跟踪算法的实现就在 contrib 包里),需要自行编译。

java + native 库的方式集成

通过加载 so 文件的方式,可以不用安装 opencv-manager

  1. 导入(File-New-Import Module) sdk 中 java 库工程 (sdk/java),不出意外,项目下会多一个 openCVLibrary3410 的库。

[图片上传失败...(image-5f3419-1591887738802)]

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

调整 openCVLibrary 的 build.gradle 中 compileSdkVersion 版本到 21 以上,不然运行时会报 Camera2 找不到。

  1. 将 sdk-native-libs 中将对应 cpu 架构(这里只选择了 armeabi-v7a)的文件夹复制到 src/main/jniLibs,如果 jniLibs 文件夹不存在则自己创建,随后在 Module:app 的 build.gradle 文件中,在 android 节点下加入如下代码,随后点击同步。
    sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/jniLibs/libs']
        }
    }
  1. 根据选择的 cpu 架构,还需要在 Module:app 的 build.gradle 中的 defaultConfig 节点下加入如下代码。
    ndk {
        abiFilters "armeabi-v7a"
    }
  1. 最后,在 gradle.properties 中加入 android.useDeprecatedNdk=true,避免一些兼容性的问题

在使用 java 代码开发时,需要加入如下代码加载对应的 so 库。

public class{
    ......
    static {
        // 加载对应的 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);
        
        // 把图片转换为灰度图
        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 调用 OpenCV 的函数了。

java c++ native 库的方式集成


如果希望在 android 中用 c++ 来开发,除了需要引入 native 库外,也需要引入 ndk,这里创建 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 目录下,如果不存在该目录请自己创建一个

  2. 在 src/main/cpp 中创建一个 cpp 文件,这里就称之为 native-lib.app,现在里面啥都不做,先跑通。

  3. 编辑 CMakeLists.txt 文件,如果没有,自己创建一个,内容大致如下

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 源码文件,也就是在第二步中创建的
             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 节点下加入如下内容即可
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }

上一节中的 3, 4, 5 步在这里也同样需要执行。

随后 build 一下,如果不报错,说明 c++ 的环境搭建应该是没有问题了。

这里再讲一下,如何通过 jni 传递图像数据到 C++ 中吧。

  1. 在 MainActivity.java 中添加 native 方法
public class MainActivity extends AppCompatActivity {
    ...
    public native int[] convertToGray(int[] imgData, int width, int height);
    ...
}

这里,函数前面有 native 关键字,鼠标移动到该方法,按住 alt + enter,在弹出的窗口里确认创建对应的 C++ 函数,这个时候就会跳转到 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
}

这里定义了图像中像素点数据传入的方式是 int 的数组。

  1. 在 java 中获得调用 Bitmap 的 getPixels 方法获得像素点的 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);
// 这里调用的 native 方法转换成为灰度图,这里的 C++ 实现在下面小节
int[] grayPixels = convertToGray(pixel, width, height);

// 把返回的灰度图像素值数组转换成 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 方法转换图像为灰度图并且返回

这里的代码编写涉及到安卓 NDK 的一些编程概念,详细的可以查看官方文档
但这里的代码还是比较简单易懂的。

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 后便可运行看到效果

缩减 OpenCV 库体积


单纯的 opencv 动态链接库有 12.3 MB,如果要集成到客户端 apk 中显然还是大了一些,虽然我们可以使用动态分发 so 库来降低安装包的大小,但一般来说我们都没有用到完整的 opencv 能力,所以这里需要针对我们用到的库,对其进行缩减。

[图片上传失败...(image-46641d-1591887738802)]

缩减的方式有两种。

  • 根据用到的模块,选择性的引用 OpenCV4Android 的静态链接库,来生成自己的动态链接库
  • 从源码编译生成属于自己的 so 或 a 库

这里介绍第一种,第二种会获得更小的库体积,但需要开发者对 OpenCV 的源码有教深的了解。

  1. 在上小节 Demo 的基础上,我们可以把 so 文件删了,然后先把 sdk/native/staticlibs/armeabi-v7a 下的全部 .a 文件都复制到 jniLibs/libs/armeabi-v7a 中。

  2. 随后,把 sdk/natvie/jni/include 文件夹下的头文件移动到 cpp/include 文件夹里(如无创建一个)

编辑 cpp/CMakeLists.txt 文件,把头文件包含进去

set(libs ${CMAKE_SOURCE_DIR}/..)
include_directories(${libs}/cpp/include)

随后 Build - Refresh Linked C++ Project 后,native-lib.cpp 里现在就已经可以 #include <opencv/cv.h> 等头文件了。

  1. 头文件有了,接下来把相关的静态链接库包含进去。

这一步主要的难点判断自己的代码引用了哪些模块,以及模块之间的依赖关系,一不小心就会出现 libcpufeatures || tbb || tegra_hal 等库找不到的错误,这些是属于第三方依赖库,存放在 sdk/native/3rdparty 文件夹中。

[图片上传失败...(image-377314-1591887738802)]

网上有蛮多奇淫技巧来分析库之间的依赖的,但是实际上在 sdk 中的 sdk/native/jni/abi-armeabi-v7a/OpenCVModules.cmake 就已经包含了引入静态库所需要的参数和依赖关系,譬如在该文件的定义中,opencv_core 依赖了 tbb, tegra_hal, libcpufeature 等静态库。

......

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>"
        )
......

这个时候看回我们转换灰度图的 C++ 代码,我们只使用到了 opencv_core 和 opencv_imgproc,我们把对应的配置从 OpenCVModules.cmake 拷贝到 cpp/CMakeLists.txt 中去,并在 target_link_libraries 中链接对应的库即可。

cpp/CMakeLists.txt 过于冗长,这里就不贴了。但这里需要注意的是静态库的添加顺序需要和 OpenCVModules.cmake 保持一致,不然编译会报错

最后编译运行,Done。

那么生成的 so 到底有多大呢?其实我们可以,我们可以对 apk 进行解压,里面有一个 lib 文件夹,包含了我们刚才编译出的 so 文件,可以看到,才 2.4mb 大小

[图片上传失败...(image-baf8f5-1591887738802)]

好了,这篇文章到这就结束了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341