Android 开发中集成 OpenCV (java, c++),以及缩减库大小

文章目录

    • 下载 OpenCV4Android SDK - 3.4.10
    • java + native 库的方式集成
    • java c++ native 库的方式集成
    • 缩减 OpenCV 库体积

最近 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 实现的都是彩色转灰度,截图如下:

Android 开发中集成 OpenCV (java, c++),以及缩减库大小_第1张图片


下载 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 的库。

Android 开发中集成 OpenCV (java, c++),以及缩减库大小_第2张图片

  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 能力,所以这里需要针对我们用到的库,对其进行缩减。

Android 开发中集成 OpenCV (java, c++),以及缩减库大小_第3张图片

缩减的方式有两种。

  • 根据用到的模块,选择性的引用 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 等头文件了。

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

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

网上有蛮多奇淫技巧来分析库之间的依赖的,但是实际上在 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 "$;$;$;$;$;$;$;$"
        )
......

这个时候看回我们转换灰度图的 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 大小

Android 开发中集成 OpenCV (java, c++),以及缩减库大小_第4张图片

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

你可能感兴趣的:(android,计算机视觉,opencv,tensorflow)