最近 Ai 项目中需要在安卓上使用 OpenCV,网上资料很多,但大多都比较乱,这里进行了整理和归纳,尽量让大部分人都能够看懂。
本文主要包含以下三块内容,包含三个 Demo 源码:
目前,本文的 Demo 是在如下环境中验证的,请自行对齐,不然容易出现问题。
NDK: 16.1.4479499
OpenCV: 3.4.0
CMake: 3.10.2
Android Studio: 3.6.3
NDK 和 OpenCV 的版本号比较重要,后两者影响不是很大
源码戳此下载
本教程三个 Demo 实现的都是彩色转灰度,截图如下:
链接: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 包里),需要自行编译。
通过加载 so 文件的方式,可以不用安装 opencv-manager
调整 openCVLibrary 的 build.gradle 中 compileSdkVersion 版本到 21 以上,不然运行时会报 Camera2 找不到。
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs/libs']
}
}
ndk {
abiFilters "armeabi-v7a"
}
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 的函数了。
如果希望在 android 中用 c++ 来开发,除了需要引入 native 库外,也需要引入 ndk,这里创建 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 目录下,如果不存在该目录请自己创建一个
在 src/main/cpp 中创建一个 cpp 文件,这里就称之为 native-lib.app,现在里面啥都不做,先跑通。
编辑 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} )
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
上一节中的 3, 4, 5 步在这里也同样需要执行。
随后 build 一下,如果不报错,说明 c++ 的环境搭建应该是没有问题了。
这里再讲一下,如何通过 jni 传递图像数据到 C++ 中吧。
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 的数组。
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);
这里的代码编写涉及到安卓 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 动态链接库有 12.3 MB,如果要集成到客户端 apk 中显然还是大了一些,虽然我们可以使用动态分发 so 库来降低安装包的大小,但一般来说我们都没有用到完整的 opencv 能力,所以这里需要针对我们用到的库,对其进行缩减。
缩减的方式有两种。
这里介绍第一种,第二种会获得更小的库体积,但需要开发者对 OpenCV 的源码有教深的了解。
在上小节 Demo 的基础上,我们可以把 so 文件删了,然后先把 sdk/native/staticlibs/armeabi-v7a 下的全部 .a 文件都复制到 jniLibs/libs/armeabi-v7a 中。
随后,把 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
这一步主要的难点判断自己的代码引用了哪些模块,以及模块之间的依赖关系,一不小心就会出现 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 大小
好了,这篇文章到这就结束了。