Android studio :在Android和jni端添加opencv库
- jni库编译要点以及Android端调用opencv(import ...)
- 一、编写jni文件,加载第三方库,编译本地jni接口和其它C++文件为库
- 1. 编写CMakeLists.txt 文件,在build.gradle(app)里面添加编译选项,如下(主要是添加部分),加入CMakeLists.txt所在路径:
- 2. c++文件就可以放在"app\src\main"下建立的“jni”文件夹内,上面提到的CMakeLists.txt和需要编译的文件一起放着(感觉只要路径对,应该都行),此时就需要写对应接口文件给Android端调用(个人用的java语言,目前还没怎么用kotlin,所以是给java端调用):
- 1)java层可以专门建一个类用于调用jni层,如:
- 2)jni层编写对应接口,cmd打开终端(确保java添加到环境变量去了),cd到上面NDKUtils.java所在路径下,输入如下 :" javac -h jni .\NDKUtils.java "指令,就会在当前路径下生成需要的文件,头文件.h 就在jni文件夹(文件夹名字和指令里面那个一致)里,写jni接口就是实现这些声明,这些声明中就可以调用C++端代码了。
- 3)"app\src\main\jni"文件夹下建立一个cpp文件用来编写jni接口,当然前面生成的头文件也可以放到同一个目录下,下面是自己写的一个测试例子:
- 4). 修改CMakeLists.txt文件:
- 5). Android端调用JNI接口
- 二、Android端调用opencv库
jni库编译要点以及Android端调用opencv(import …)
一、编写jni文件,加载第三方库,编译本地jni接口和其它C++文件为库
1. 编写CMakeLists.txt 文件,在build.gradle(app)里面添加编译选项,如下(主要是添加部分),加入CMakeLists.txt所在路径:
android {
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++11 -fexceptions "//c++的编译选项,慎重选择
abiFilters "arm64-v8a", "armeabi-v7a"//用于指定生成不同平台的库?
}
}
}
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"//关键CMakeLists路径
version "3.10.2"//版本,貌似可以不用填
}
}
}
2. c++文件就可以放在"app\src\main"下建立的“jni”文件夹内,上面提到的CMakeLists.txt和需要编译的文件一起放着(感觉只要路径对,应该都行),此时就需要写对应接口文件给Android端调用(个人用的java语言,目前还没怎么用kotlin,所以是给java端调用):
1)java层可以专门建一个类用于调用jni层,如:
package com.example.gg;//比如工程包的路径
public class NDKUtils {
static {
System.loadLibrary("testFG");//当前生成并调用的库名,保持一致,此处省略前缀lib;
}
public static native String show();//函数,供java端调用,jni端实现
public static native String show2(ParamInfo p);//函数,供java端调用,jni端实现
public static ParamInfo par;//类中类,本意是用来jni和java交互数据
public NDKUtils()
{
par= new ParamInfo();
par.path1="pass path1 message from android";
par.path2="pass path2 message from android";
par.path3="pass path3 message from android";
}
public class ParamInfo {
public String path1;//比如String 可以放路径
}
}
2)jni层编写对应接口,cmd打开终端(确保java添加到环境变量去了),cd到上面NDKUtils.java所在路径下,输入如下 :" javac -h jni .\NDKUtils.java "指令,就会在当前路径下生成需要的文件,头文件.h 就在jni文件夹(文件夹名字和指令里面那个一致)里,写jni接口就是实现这些声明,这些声明中就可以调用C++端代码了。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_gg_NDKUtils */
#ifndef _Included_com_example_gg_NDKUtils
#define _Included_com_example_gg_NDKUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_gg_NDKUtils
* Method: show
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_gg_NDKUtils_show
(JNIEnv *, jclass);
/*
* Class: com_example_gg_NDKUtils
* Method: show2
* Signature: (Lcom/example/gg/NDKUtils/ParamInfo;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_gg_NDKUtils_show2
(JNIEnv *, jclass, jobject);
#ifdef __cplusplus
}
#endif
#endif
3)"app\src\main\jni"文件夹下建立一个cpp文件用来编写jni接口,当然前面生成的头文件也可以放到同一个目录下,下面是自己写的一个测试例子:
#include <jni.h>
#include <string>
#include <opencv2/opencv.hpp>//第三方库等其它需要在jni端调用的库,都在CMakeLists.txt里面添加修改
#include "jniT1.h"//此处是自己另外编译的库 libnative-lib 里面对应的头文件,
using namespace std;
typedef struct{//数据交互:jni端结构体,对应Android端类中类"com/example/gg/NDKUtils$ParamInfo"
string path1;//其实名字不必一模一样
string path2;
string path3;
}ParamInfo;
extern "C" JNIEXPORT jstring JNICALL Java_com_example_gg_NDKUtils_show
(JNIEnv *env, jclass obj)
{
string st="";
string a= mul(4,88);//libnative-lib.so 里面的string mul(int a, int b);
st = "good C++ "+a;
return env->NewStringUTF(st.c_str());//返回值可以在TextView里面显示
};
extern "C" JNIEXPORT jstring JNICALL Java_com_example_gg_NDKUtils_show2
(JNIEnv *env, jclass obj , jobject path)
{
//这里是从Android端传递一些数据来,还有很多类型,个人也刚接触就不一一列举了,
jclass jcInfo = env->FindClass("com/example/gg/NDKUtils$ParamInfo");
ParamInfo para;
jfieldID jfd = env->GetFieldID(jcInfo, "path1", "Ljava/lang/String;");
jstring jfs1 = (jstring)env->GetObjectField(path, jfd);
para.path1 = env->GetStringUTFChars(jfs1, 0);
jfd = (env->GetFieldID(jcInfo, "path2","Ljava/lang/String;"));
jfs1 = (jstring)env->GetObjectField(path, jfd);
para.path2 = env->GetStringUTFChars(jfs1, 0);
jfd = env->GetFieldID(jcInfo, "path3", "Ljava/lang/String;");
jfs1 = (jstring)env->GetObjectField(path, jfd);
para.path3 = env->GetStringUTFChars(jfs1, 0);
string st="";
st = para.path1+"\r\n";
st=st+para.path2+"\r\n";;
st=st+para.path3+"\r\n";;
return env->NewStringUTF(st.c_str());
};
4). 修改CMakeLists.txt文件:
其实可以通过Android studio建立一个本地“Native C++”工程,里面会自动生成CMakeLists.txt的模板,个人也是基于那个模板和别人的CMakeLists.txt文件修改的,简要如下(这里为了好看,不用#注释,下面“//”注释是为了显示方便,以下注释为个人理解):
cmake_minimum_required(VERSION 3.10.2)//字面意思是cmake的版本最低要求
project("myapplication")//工程名字,好像改其它没太大影响
//字面意思,CMAKE_CURRENT_SOURCE_DIR为当前CMakeLists.txt所在目录,当前层还有一个libs的文件夹
set(LIBRARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs)//设置一个路径,里面存放第三方库;
// build opencv,拷贝的,这一块opencv,主要设置OpenCV_ANDROID_SDK 这个OpenCV-android版本的路径
//OpenCV-android-sdk-4.5.1能编译成功,OpenCV-android-sdk-3.4.13编译好像各种问题
set( OpenCV_ANDROID_SDK D:\\soft_app\\app2\\opencv\\Android\\OpenCV-android-sdk-4.5.1)
set( OpenCV_DIR ${OpenCV_ANDROID_SDK}/sdk/native/jni )
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
include_directories(${OpenCV_INCLUDE_DIRS})
message(STATUS "OpenCV library status:")
message(STATUS " version: ${OpenCV_VERSION}")
message(STATUS " libraries: ${OpenCV_LIBS}")
message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}")
else(OpenCV_FOUND)
message(FATAL_ERROR "OpenCV library not found")
endif(OpenCV_FOUND)
//这里是将需要编译的cpp等文件打包一起,好管理,后面大概${SRCS}就能直接代表那些文件了
set(SRCS
gg1.cpp//注意路径,这个是当前路径下,如果有复合路径可以找找更智能的方法
)
add_library( # Sets the name of the library.
testFG//当前最终库名,省略前缀,用的时候就System.loadLibrary("testFG");
# Sets the library as a shared library.
SHARED//听名字是意为共享库,没怎么研究
# Provides a relative path to your source file(s).
${SRCS}//就是前面的一些cpp文件,当前路径下头文件不用加进来,如果有其它路径,估计要包含路径
)
//ANDROID_ABI 这个是对应 "arm64-v8a", "armeabi-v7a"等,貌似是统称,好像自动识别的
//${LIBRARY_DIR}/${ANDROID_ABI}/libnative-lib.so 就是库文件的确切地址
add_library(libnative-lib SHARED IMPORTED)//这是载入自己另外编译的so库,库前缀带上
set_target_properties(libnative-lib PROPERTIES IMPORTED_LOCATION
${LIBRARY_DIR}/${ANDROID_ABI}/libnative-lib.so)
//这个不太懂,默认就有的,没怎么细究,没去动
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.
testFG//第一个是本地将要生成的库,编译写C++(包括jni)文件等
libnative-lib//自己编译的第三方库
${OpenCV_LIBRARIES}opencv的第三方库
//Links the target library to the log library included in the NDK.
${log-lib} )
添加完毕后依次在Android studio界面上选择“Build“ --“Make Project”, “build"文件夹下面搜索.so文件,可以找到不同"arm64-v8a”, "armeabi-v7a"等对应的库(这个和编译设置有关),要在手机等设备上跑,还需将这些库(和文件夹一起)放到指定地方,并在在build.gradle(app)添加路径:
android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']//可以放到这个路径下,自己这边这个文件夹自动就有
// jniLibs.srcDirs = ['src/main/jniLibs']//或者可以选择放这个路径下,这个是自己建的
}
}
}
5). Android端调用JNI接口
注意:需要的资源如模型、图片、.xml等一些文件会放置到“src\main\assets”下面,个人是自己建立“assets”文件夹然后内部放文件,而Android设备(手机的)“assets”里面的文件是打包到apk里,不能直接cv::Mat image = cv::imread(image_path);读,一般需要特定接口调用,如
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String show="";
TextView tv = findViewById(R.id.textView1);
try {
String[] li = this.getAssets().list("");
for(String st:li) show +=st+"\r\n";
tv.setText(show);
//InputStream is = this.getAssets().open("1.txt");//打开“src\main\assets”下的1.txt文件
} catch (IOException e) {
e.printStackTrace();
}
}
Android端调用上面jni例子如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//个人测试,从“src\main\assets”下直接读取文件有问题,不知道有没什么好办法
TextView tv = findViewById(R.id.ggt);
NDKUtils test = new NDKUtils();
File fileDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "tem");
if (!fileDir.exists()) {
fileDir.mkdirs();
}
test.par.path1 = fileName+"/1.jpg";//该路径下可以直接存储,测试可这样
test.par.path2 = fileName+"/1.tflite";
test.par.path3 = fileName+"//1.yaml";
tv.setText(test.show2(test.par));
}
二、Android端调用opencv库
这里其实挺有疑问的,难道不能编译一个可以同时在Android (import)和jni端(include)的opencv?个人刚接触Android的不久,所以就还是分开编译分开用,罗列一些解决办法,这里顺便说下,个人用的算是当前很新的:Android studio 4.1.1了;ndkVersion ‘22.0.7026061’;JDK 用过“jdk-15.0.1”,还有Android studio内部自带的,Android SDK 优先使用“Android 11.0(R)-API Level 30”。
网上找了一种java端加载opencv的方法,具体网上讲的挺多的,这里就不过于详细赘述了。加载不同版本opencv出现不同结果,大体是通过"File" -> “New” -> “Import Module”,然后框里面填opencv的路径,最后填“Finish”按钮的完成,后面再做些配置,和先前讲jni的部分有些相关。下面简述下个人配置OpenCV-android-sdk-3.4.13 和 OpenCV-android-sdk-4.5.1结果。
1.OpenCV-android-sdk-3.4.13
1).导入模块:“File” -> “New” -> “Import Module”,然后填写opencv路径
比如个人填的opencv Module路径是“D:\soft_app\app2\opencv\Android\OpenCV-android-sdk-3.4.13\sdk\java”,这个这个版本这个路径下对应Android studio工程空间会小很多;build.gradle(app)端(另外一个build.gradle(opencv的)应该也是可以的,不过库要和路径对应放置)添加路径,别重复,当然库也要对应放到里面去,比如自己库就在opencv的路径“D:\soft_app\app2\opencv\Android\OpenCV-android-sdk-3.4.13\sdk\native\libs”下;
android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']//可以放到这个路径下,自己这边这个文件夹自动就有
// jniLibs.srcDirs = ['src/main/jniLibs']//或者可以选择放这个路径下,这个是自己建的
}
}
}
2). 修改 opencv 的build.gradle文件,比如下列:
网上说主要是保持两个build.gradle文件compileSdkVersion ,buildToolsVersion ,minSdkVersion ,targetSdkVersion 一致。
apply plugin: 'com.android.library'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
sourceSets {//路径不要重复放置,opencv的这个放置了,另外一个build.gradle文件就没必要放
main {
//jniLibs.srcDirs = ['src/main/jniLibs']
jniLibs.srcDirs = ['libs']
}
}
defaultConfig {
minSdkVersion 16
targetSdkVersion 30
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
ndkVersion '22.0.7026061'
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
3). 配置OpenCV的依赖关系,“File” -> “Project Structure” ->“Dependencies”
选中需要opencv的那个Module,点击“+”可以添加模块,然后选中刚添如的opencv模块,自己有遇到过找不到opencv模块情况,后来按照 2)先配好就有了。
模块的名字也可以改,选中模块,“右键” ->“Refactor” ->“Rename”就行。
大致就是上面的,可以清理下工程,再“Build“ -->“Make Project”,别忘记将库拷贝到对应文件夹下。build文件夹下都是生成的一些文件,蛮占空间,删掉build文件夹内容,重新“Build“ -->“Make Project”又会有了,里面有so库。
2. OpenCV-android-sdk-4.5.1
如果用上面的方式配置OpenCV-android-sdk-4.5.1安装到手机上,手机会载入opencv失败,apk直接崩溃:
1)一种解决方式是设置模块路径时候设置到外层的/sdk而不是里程的/java,比如OpenCV4.5.1可以填“D:\soft_app\app2\opencv\Android\OpenCV-android-sdk-4.5.1\sdk”路径,好处是库的路径和文件都会自动配置(不过依赖关系还是要自己配置的),坏处是工程空间占用太大,编译时间长(估计多编译了很多文件),个人表示有点难受。
2)另外一种算是折中方案了,观察两个路径下生成(“Build ”–>“Build Bundle(s) / APK(s)”–>" Build APK(s)")的不同apk(改后缀名为rar或者其它的)解压后发现能跑的那个竟然多出了一个“libc++_shared.so”文件,于是将这文件拷贝到崩溃的那个工程对应路径下面重新编译、安装、运行,竟然可以了,具体原因还不清楚,所以只能算是折中方案了,需要“libc++_shared.so”。
3. Android端opencv载入和调用
opencv版本4.0貌似推荐 “System.loadLibrary(“opencv_java4”)” or "OpenCVLoader.initDebug()"加载opencv,加载以后才能用。
package com.example.myapplication3;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import org.opencv.android.BaseLoaderCallback;//opencv 库
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Mat;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "t3";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//OpenCV库加载并初始化成功后的回调函数
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
// TODO Auto-generated method stub
switch (status){
case BaseLoaderCallback.SUCCESS:
Log.i(TAG, "成功加载");
break;
default:
super.onManagerConnected(status);
Log.i(TAG, "加载失败");
break;
}
}
};
@Override
public void onResume()
{
super.onResume();
//新版本貌似推荐如下加载方式
// - use "System.loadLibrary("opencv_java4")" or "OpenCVLoader.initDebug()"
if (!OpenCVLoader.initDebug()) {
Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization");
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, this, mLoaderCallback);
} else {
Log.d(TAG, "OpenCV library found inside package. Using it!");
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
Mat mM= new Mat();//加载以后才能调用opencv 库
}
}
最后,花了好长时间才写完,还通宵了,哎呀,写博客累;懂就很快,不懂就需探索寻找方法还是太耗时间了,愿以后对自己好点。自己做个记录,万一以后自己忘记,也查看,也希望对认真的人有帮助,如果有什么疑问不足的地方可以留言和其他人讨论,时间长了自己也会忘记,不知道怎么回复,所以一般不怎么回复了,谢谢,仅供参考。